用现代C++重构Kilo编辑器:从终端交互到语法高亮的系统编程实践

1. 项目概述:为什么我们要再造一个“轮子”?

如果你是一个C++开发者,或者正在深入学习这门语言,你很可能已经用遍了各种强大的IDE和文本编辑器,比如Visual Studio Code、CLion,甚至是老牌的Vim。那么,为什么我们还要花时间去从零构建一个名为“Kilo”的轻量级文本编辑器呢?这听起来像是一个经典的“重新发明轮子”的问题。但恰恰相反,这个项目是深入理解系统编程、终端交互、数据结构与算法,以及现代C++工程实践的一个绝佳切入点。

Kilo最初是一个用C语言编写、代码量控制在1000行以内的终端文本编辑器,以其简洁和直指核心的实现而闻名。它麻雀虽小,五脏俱全,具备了基本的文件编辑、光标移动、搜索、语法高亮等核心功能。而我们今天要做的,就是将其“现代化”——用现代C++(C++17/20)的思想和特性来重构它。这不仅仅是语言的简单替换,更是一次设计哲学的升级:从面向过程的C风格,转向利用RAII管理资源、使用标准库容器和算法、构建清晰的类层次结构的现代C++风格。

通过这个项目,你将亲手触摸到那些在高级框架背后默默工作的底层机制:终端是如何处理按键输入的?屏幕内容是如何刷新的?如何高效地管理一个可能很大的文本缓冲区?如何实现实时的语法高亮?这些问题在常规应用开发中往往被封装起来,而在这里,你需要直面它们。这对于巩固你的C++基础、理解底层系统交互、以及提升解决复杂问题的能力,有着不可替代的价值。它适合所有希望超越语法层面,真正理解“程序如何与机器对话”的中级C++学习者。

2. 核心架构设计与现代C++理念

2.1 从C到C++:设计哲学的转变

原始的Kilo编辑器是一个典型的单文件C程序,全局变量和函数交织在一起。虽然代码紧凑,但对于学习和扩展来说,结构并不清晰。我们的重构首要任务就是进行合理的 职责分离 资源封装

我们将整个编辑器划分为几个核心类:

  • Editor :作为总控制器,持有其他核心组件的引用,协调整个编辑流程(主循环、事件分发)。
  • Buffer :负责文本数据的存储与管理。这是从原始全局字符数组 char** row 演进而来。我们将使用 std::vector<std::string> std::deque<std::string> 来管理文本行,利用其自动管理内存的特性,彻底告别手动的 malloc/free
  • Terminal :封装所有与终端交互的脏活累活。包括原始模式(Raw Mode)的设置与恢复、读取按键序列、获取终端尺寸、以及向屏幕输出ANSI转义序列来控制光标和颜色。这个类的设计完美体现了RAII(Resource Acquisition Is Initialization)思想:在构造函数中进入原始模式,在析构函数中恢复终端原始状态,确保异常安全。
  • View :负责视图逻辑。它知道当前光标在文本缓冲区( Buffer )中的位置,以及由于屏幕尺寸限制,应该将缓冲区的哪一部分渲染出来(即行偏移 rowoff 和列偏移 coloff )。
  • SyntaxHighlighter (可选但推荐):将语法高亮逻辑从主渲染循环中剥离。它接收一行文本,根据语言规则(如C++关键字、字符串、注释)返回一个包含颜色信息的数组,实现关注点分离。

这种类级别的划分,使得代码更易读、易测试、也易扩展。例如,未来如果想增加Vim模式,我们可以在 Editor 中切换不同的输入处理策略,而无需改动 Buffer Terminal

2.2 关键数据结构的选型与权衡

文本缓冲区的实现是编辑器的性能核心。原始Kilo使用 char** 指针数组,每一行是一个 char*

在现代C++中,我们有更安全、功能更丰富的选择:

  1. std::vector<std::string> :这是最直观的选择。 vector 提供了连续的存储和快速的随机访问(O(1)访问第N行),插入和删除行在尾部是O(1),在中间或头部是O(n)。对于大多数编辑操作(行内编辑、在光标处插入行)来说性能足够。其内存连续性对缓存友好。
  2. std::deque<std::string> :如果你预期会在文本开头频繁插入/删除行(虽然不常见), deque 在头尾的插入删除都是O(1)。不过,它的内存不是完全连续的,随机访问性能略低于 vector
  3. Gap Buffer 或 Piece Table :这是专业级编辑器(如VS Code)处理超大文件的利器。它们通过维护一个“间隙”或记录文本片段来优化插入和删除操作,使其在光标附近接近O(1)。但实现复杂度远高于 vector

实操心得 :对于学习项目和轻量级编辑器, 强烈推荐从 std::vector<std::string> 开始 。它的简单性和安全性让你能更专注于编辑器逻辑本身,而不是复杂的数据结构调试。只有当你在编辑超大型文件(>10MB)且性能成为瓶颈时,才需要考虑升级到更高级的结构。过早优化是万恶之源。

我们选择 std::vector<std::string> 作为我们的 Buffer 核心存储。同时,我们将行号、修改状态等元信息也封装进 Buffer 类。

// Buffer.hpp 示例片段
#include <vector>
#include <string>
#include <fstream>

class Buffer {
public:
    Buffer() = default;
    bool loadFromFile(const std::string& filepath);
    bool saveToFile(const std::string& filepath) const;

    // 行操作
    std::string& getLine(size_t index);
    const std::string& getLine(size_t index) const;
    void insertLine(size_t index, const std::string& line);
    void deleteLine(size_t index);
    void appendToLine(size_t index, char c);
    void removeCharAt(size_t lineIndex, size_t charIndex);

    size_t numLines() const { return lines_.size(); }
    bool isDirty() const { return dirty_; }
    void setDirty(bool dirty) { dirty_ = dirty; }

private:
    std::vector<std::string> lines_;
    std::string filepath_;
    bool dirty_ = false; // 文件是否被修改未保存
};

2.3 终端交互:原始模式与ANSI转义序列

这是项目中最“系统”的部分。终端默认工作在 规范模式 下,输入会经过行缓冲、处理特殊字符(如Ctrl+C会发送中断信号)后才交给程序。这对于编辑器是灾难性的——我们需要实时响应每一个按键。

因此,我们必须将终端设置为 原始模式 。这需要用到 <termios.h> 中的 tcgetattr tcsetattr 函数。我们需要做的是:

  1. 获取当前终端属性。
  2. 创建一个副本并修改关键标志位:关闭回显( ECHO )、关闭规范模式( ICANON )、关闭对特殊控制字符的处理(如 ISIG 用于Ctrl+C, IXON 用于Ctrl+S/Q)等。
  3. 设置新的终端属性。
  4. 最重要的是,在程序退出前(无论正常退出还是异常崩溃),必须恢复原来的终端属性!否则用户的终端会一直处于奇怪的状态。这就是RAII用武之地。
// Terminal.hpp 示例片段
#include <termios.h>
#include <unistd.h> // for STDIN_FILENO

class Terminal {
public:
    Terminal() {
        tcgetattr(STDIN_FILENO, &original_attrs_);
        raw_attrs_ = original_attrs_;
        raw_attrs_.c_iflag &= ~(BRKINT | ICRNL | INPCK | ISTRIP | IXON);
        raw_attrs_.c_oflag &= ~(OPOST);
        raw_attrs_.c_cflag |= (CS8);
        raw_attrs_.c_lflag &= ~(ECHO | ICANON | IEXTEN | ISIG);
        raw_attrs_.c_cc[VMIN] = 0; // 非阻塞读取
        raw_attrs_.c_cc[VTIME] = 1; // 超时时间(0.1秒)
        enableRawMode();
    }

    ~Terminal() {
        disableRawMode();
    }

    // 禁用拷贝和赋值,确保资源唯一管理
    Terminal(const Terminal&) = delete;
    Terminal& operator=(const Terminal&) = delete;

    int getWindowSize(int& rows, int& cols) const;
    int readKey() const; // 读取一个按键,可能返回多字节序列(如方向键)
    void clearScreen() const;
    void moveCursor(int row, int col) const;

private:
    void enableRawMode() const {
        tcsetattr(STDIN_FILENO, TCSAFLUSH, &raw_attrs_);
    }
    void disableRawMode() const {
        tcsetattr(STDIN_FILENO, TCSAFLUSH, &original_attrs_);
    }

    termios original_attrs_{};
    termios raw_attrs_{};
};

readKey() 函数需要处理多字节序列,比如方向键、PageUp/Down等,它们会发送像 \x1b[A (上箭头)这样的转义序列。我们需要一次读取多个字节并解析。

屏幕渲染则依赖于 ANSI转义序列 。例如:

  • \x1b[2J :清屏。
  • \x1b[H :将光标移动到左上角(1,1)。
  • \x1b[<行>;<列>H :将光标移动到指定位置。
  • \x1b[<颜色码>m :设置颜色,如 \x1b[32m 是绿色, \x1b[0m 是重置。

我们的 Terminal 类会封装这些晦涩的序列,提供像 clearScreen() moveCursor(row, col) 这样清晰的接口。

3. 核心模块实现详解

3.1 文本缓冲区(Buffer)的稳健实现

Buffer 类的实现不仅要正确,更要健壮,能处理各种边界情况。

文件加载与保存 : 加载文件时,不能假设文件一定存在或可读。我们使用 std::ifstream 并检查其状态。对于跨平台换行符,一个简单可靠的方法是使用 std::getline ,它会自动处理 \n \r\n

bool Buffer::loadFromFile(const std::string& path) {
    std::ifstream file(path);
    if (!file.is_open()) {
        return false; // 或抛出异常
    }
    lines_.clear();
    std::string line;
    while (std::getline(file, line)) {
        // 注意:getline会丢弃换行符,我们需要把它加回来吗?
        // 在内部存储时通常不加,在保存时再添加。
        lines_.push_back(std::move(line));
    }
    filepath_ = path;
    dirty_ = false;
    return true;
}

保存时,使用 std::ofstream ,并在每行末尾写入换行符( \n )。

行与字符的编辑操作 : 这是最常被调用的部分,必须高效且安全。所有接受索引 size_t index 的函数, 首要任务就是进行边界检查

void Buffer::insertLine(size_t index, const std::string& line) {
    if (index > lines_.size()) { // 允许在末尾插入(index == size())
        index = lines_.size();
    }
    auto it = lines_.begin() + index;
    lines_.insert(it, line);
    dirty_ = true;
}

void Buffer::appendToLine(size_t index, char c) {
    if (index >= lines_.size()) {
        // 如果行不存在,可以自动添加新行,或者视为错误。
        // 这里我们选择在末尾添加新行。
        lines_.push_back(std::string(1, c));
    } else {
        lines_[index].push_back(c);
    }
    dirty_ = true;
}

注意事项 std::vector::insert 在中间插入会导致后续元素移动,对于超大文件,频繁在开头插入行会有性能问题。这就是我们之前讨论数据结构选型的原因。但在实际编辑中,用户更多是在光标所在行附近操作, vector 的表现是可以接受的。

3.2 视图(View)与渲染逻辑

View 类负责将抽象的文本缓冲区映射到具体的屏幕窗口。它需要维护几个关键状态:

  • cursor_x , cursor_y :光标在 文本缓冲区 中的位置(从0开始)。
  • rowoff , coloff :由于屏幕有限,缓冲区中哪一行/列应该显示在屏幕的左上角。

渲染一帧的流程如下:

  1. 清屏并重置光标 :调用 Terminal::clearScreen() moveCursor(0,0)
  2. 计算渲染范围 :从 rowoff 开始,渲染最多 screen_rows 行。对于每一行,从 coloff 开始,渲染最多 screen_cols 个字符。
  3. 行号显示 :可以在左侧预留几列显示行号,如 printf(“%6d “, row_index + 1)
  4. 逐行渲染
    • 获取当前行字符串。
    • 如果该行有语法高亮信息,则遍历每个字符,根据高亮类型输出带颜色的ANSI序列和字符。
    • 如果该行长度小于 coloff ,则此行在屏幕上不可见(跳过)。
    • 截取从 coloff 开始的子串进行渲染,如果长度超过 screen_cols 则截断。
  5. 状态栏与消息栏 :屏幕的最后一行或两行用于显示状态(文件名、是否已修改、行号/列号)和临时消息(如“已保存”)。
  6. 定位光标 :最后,根据光标在缓冲区中的位置 (cursor_y, cursor_x) ,减去偏移量 (rowoff, coloff) ,得到光标在屏幕上的实际位置,调用 moveCursor() 将光标放置过去。

滚动 :当光标移动超出当前视图范围时,需要调整 rowoff coloff 。例如,如果 cursor_y < rowoff ,则令 rowoff = cursor_y ;如果 cursor_y >= rowoff + screen_rows ,则令 rowoff = cursor_y - screen_rows + 1 。列方向的滚动逻辑类似。

3.3 输入处理与编辑命令映射

Editor 的主循环中,我们不断调用 Terminal::readKey() 读取按键,然后根据当前模式(普通模式、插入模式,我们这里先实现插入模式)将其分发给不同的处理函数。

我们需要解析的按键大致分为几类:

  1. 控制字符 :如 Ctrl+Q (退出)、 Ctrl+S (保存)。在原始模式下, Ctrl+字母 会产生 1-26 的 ASCII 码( Ctrl+A 是 1, Ctrl+C 是 3,等等)。我们将 Ctrl+Q 映射为退出。
  2. 转义序列 :以 \x1b (ESC) 开头。我们需要继续读取后续字符来判断是方向键( [A / [B / [C / [D )、Home键( [H )、End键( [F )等。
  3. 普通可打印字符 :直接插入到缓冲区当前光标位置。
  4. 特殊编辑键 :回车( \r )、退格( \x7f \x08 )、删除( \x1b[3~ )、Tab( \t ,通常需要扩展为若干个空格)。
// 简化的输入处理逻辑
void Editor::processKeypress() {
    int key = terminal_.readKey();
    if (key == -1) return; // 超时,无输入

    switch (key) {
        case CTRL_KEY('q'): // 自定义宏,如 #define CTRL_KEY(k) ((k) & 0x1f)
            exitEditor();
            break;
        case CTRL_KEY('s'):
            saveFile();
            break;
        case '\x1b': { // ESC 序列
            handleEscapeSequence();
            break;
        }
        case '\r': // 回车
            insertNewline();
            break;
        case '\x7f': // 退格 (Backspace)
            deleteCharBeforeCursor();
            break;
        default:
            if (std::isprint(key) || key == '\t') { // 可打印字符或Tab
                insertChar(key);
            }
            break;
    }
    // 处理完按键后,可能需要滚动视图以确保光标可见
    view_.scrollToCursor(buffer_, terminal_.getScreenSize());
}

handleEscapeSequence() 函数需要继续读取2个或更多字节来识别具体的功能键。

4. 进阶功能实现:语法高亮与搜索

4.1 实现一个可扩展的语法高亮器

语法高亮的本质是根据一系列规则(正则表达式或状态机)将文本划分为不同的类别(如关键字、字符串、注释、数字),并为每个类别分配一个颜色。

我们可以定义一个 SyntaxRule 结构体,包含一个正则表达式 std::regex 和一个代表类型的枚举值。 SyntaxHighlighter 类则包含一个支持不同文件扩展名的规则集映射。

enum class HighlightType {
    NORMAL, NUMBER, KEYWORD, STRING, COMMENT, TYPE, PREPROCESSOR
};

struct SyntaxRule {
    std::regex pattern;
    HighlightType type;
};

class SyntaxHighlighter {
public:
    SyntaxHighlighter();
    std::vector<HighlightType> highlightLine(const std::string& line, const std::string& file_ext);

private:
    std::unordered_map<std::string, std::vector<SyntaxRule>> lang_rules_;

    void initCPPRules();
};

highlightLine 函数中,我们遍历整行字符串,对每个规则进行 std::regex_search ,并记录匹配到的位置和类型。这里有一个难点:规则可能有重叠(比如字符串内的内容不应再被关键字规则匹配)。一个简单的策略是按优先级顺序应用规则,或者使用一个状态机来跟踪是否在字符串或注释中。

实操心得 :对于学习项目,不必追求完美的、覆盖所有边界情况的高亮。可以先实现一个简单的、基于关键词列表和简单正则(如 //.* 表示单行注释, ”([^”\\]|\\.)*” 表示字符串)的高亮。这已经能带来显著的视觉提升。性能上,可以缓存已高亮行的结果,只有当该行被修改后才重新高亮。

渲染时, View 类在绘制每一行前,先获取该行的高亮类型数组,然后遍历每个字符,根据其类型输出对应的ANSI颜色序列,再输出字符本身。

4.2 实时搜索功能的实现

搜索功能需要两个模式: 输入模式 浏览模式

  1. 进入搜索 :按下 Ctrl+F (或其他快捷键),在状态栏显示 “Search:” 提示,并进入一个临时的输入状态。
  2. 输入查询 :在此状态下,读取用户输入的字符,构建查询字符串,并 实时 在缓冲区中从光标后开始向前或向后搜索。
  3. 高亮与跳转 :一旦找到匹配项,立即将视图滚动到该位置,并高亮显示匹配的文本(例如反色显示)。用户按回车确认当前位置,或按 n / p 查找下一个/上一个匹配项。
  4. 退出搜索 :按ESC退出搜索模式,恢复正常编辑。

实现的关键在于:

  • 实时反馈 :每输入一个字符,就执行一次搜索并刷新屏幕。这要求搜索算法足够快。
  • 搜索算法 :对于轻量级编辑器,使用 std::string::find 进行简单的子串匹配已经足够。如果想支持不区分大小写或正则表达式,可以使用 std::regex_search ,但要注意性能。
  • 高亮显示 :在渲染匹配行时,需要知道匹配的起始和结束列,并在那个区间使用不同的颜色或反色属性( \x1b[7m )进行渲染。
void Editor::search() {
    std::string query;
    int saved_cx = view_.cursor_x, saved_cy = view_.cursor_y;
    int direction = 1; // 1 for forward, -1 for backward
    int last_match = -1;

    while (true) {
        // 在状态栏显示 “Search: [query]”
        setStatusMessage(“Search: %s (Use ESC to cancel, n/p for next/prev)”, query.c_str());
        refreshScreen();

        int key = terminal_.readKey();
        if (key == ‘\x1b’) { // ESC
            break;
        } else if (key == ‘\r’) { // Enter
            if (last_match != -1) {
                view_.cursor_y = last_match;
                // 还需要计算光标x位置
            }
            break;
        } else if (key == ‘n’ || key == ‘N’) {
            direction = 1;
            // 执行搜索逻辑,更新 last_match 和光标位置
            performSearch(query, direction);
        } else if (key == ‘p’ || key == ‘P’) {
            direction = -1;
            performSearch(query, direction);
        } else if (key == ‘\x7f’) { // Backspace
            if (!query.empty()) query.pop_back();
            // 回退后重新搜索
            performSearch(query, direction);
        } else if (std::isprint(key)) {
            query.push_back(key);
            // 输入新字符后实时搜索
            performSearch(query, direction);
        }
    }
    clearStatusMessage();
}

5. 项目构建、调试与性能优化

5.1 使用CMake构建跨平台项目

虽然我们的编辑器严重依赖POSIX终端API( <termios.h> , <unistd.h> ),在Windows上需要其他方案(如 <conio.h> 或Windows API),但使用CMake可以更好地管理项目结构,并为未来可能的跨平台适配打下基础。

一个简单的 CMakeLists.txt

cmake_minimum_required(VERSION 3.10)
project(KiloCPP VERSION 1.0.0 LANGUAGES CXX)

set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)

# 添加可执行目标
add_executable(kilo_cpp
    src/main.cpp
    src/Editor.cpp
    src/Buffer.cpp
    src/Terminal.cpp
    src/View.cpp
    src/SyntaxHighlighter.cpp
)

# 包含头文件目录
target_include_directories(kilo_cpp PRIVATE include)

# 在Linux/macOS上需要链接 curses 库?实际上我们直接使用 termios 和 ANSI 序列,不需要 curses。
# 但如果想用 ncurses 来简化终端操作,可以取消注释下面这行。
# target_link_libraries(kilo_cpp PRIVATE curses)

# 设置编译优化选项
if(CMAKE_BUILD_TYPE STREQUAL “Release”)
    target_compile_options(kilo_cpp PRIVATE -O2 -DNDEBUG)
else()
    target_compile_options(kilo_cpp PRIVATE -g -O0 -Wall -Wextra)
endif()

将源代码按类分到 src/ 目录,头文件放到 include/ 目录,使项目结构清晰。

5.2 调试技巧与常见问题排查

开发终端应用的一个主要挑战是,当程序崩溃时,终端可能还处于原始模式,导致输入输出混乱。有几种应对策略:

  1. 使用终端复用器 :在 tmux screen 会话中运行你的编辑器。这样即使编辑器崩溃,你只需要切换到另一个 tmux 窗口或 screen 会话,原来的终端状态不会受影响。
  2. 编写安全的 Terminal :如前所述,利用RAII在析构函数中恢复终端状态。即使程序异常退出,只要 Terminal 对象被正确析构,终端状态就能恢复。确保 Terminal 是全局或主函数中最早创建、最晚销毁的对象之一。
  3. 添加紧急恢复信号处理 :捕获 SIGSEGV (段错误)等信号,在信号处理函数中强制恢复终端模式并退出。
    #include <csignal>
    void signalHandler(int sig) {
        // 尝试恢复终端(注意:信号处理函数中只能使用异步信号安全的函数)
        write(STDOUT_FILENO, “\x1b[?25h”, 6); // 显示光标
        write(STDOUT_FILENO, “\x1b[2J”, 4); // 清屏
        write(STDOUT_FILENO, “\x1b[H”, 3); // 光标归位
        struct termios orig;
        tcgetattr(STDIN_FILENO, &orig);
        orig.c_lflag |= (ECHO | ICANON | ISIG);
        tcsetattr(STDIN_FILENO, TCSANOW, &orig);
        _exit(1);
    }
    // 在main函数开始处注册
    signal(SIGSEGV, signalHandler);
    

常见问题速查表

问题现象 可能原因 排查与解决
程序退出后终端无响应,输入不显示 未正确恢复终端原始属性 检查 Terminal 析构函数是否被调用,或程序是否被 SIGKILL 杀死。在 main 开头用 atexit 注册恢复函数作为双保险。
方向键、退格键输入异常 readKey() 函数未能正确解析转义序列 打印接收到的按键字节序列(十六进制),与标准ANSI序列对比。确保读取是 非阻塞 的,并且正确处理多字节读取。
屏幕闪烁或渲染残影 每帧渲染前未清屏,或渲染逻辑有误 确保每帧都从屏幕左上角开始重绘。可以尝试“双缓冲”思路:先将一整帧内容构建在一个 std::string 缓冲区中,最后一次性 write 到终端。
编辑大文件时卡顿 1. 每帧全量渲染。2. 语法高亮算法效率低。3. 数据结构 vector 中间插入慢。 1. 实现脏矩形渲染,只重绘变化的部分。2. 优化高亮规则,缓存结果。3. 评估是否需升级数据结构(如Piece Table)。
窗口大小改变后布局错乱 未捕获 SIGWINCH 信号 注册 SIGWINCH 信号处理函数,在其中重新获取终端尺寸 ( ioctl(TIOCGWINSZ) ) 并刷新屏幕。

5.3 性能优化实战

对于文本编辑器,性能瓶颈通常出现在 渲染 大规模编辑 上。

  • 渲染优化

    • 脏矩形渲染 :记录屏幕上哪些区域(行范围)的内容发生了变化,只重绘这些区域。这需要维护一个屏幕内容的“快照”用于对比。
    • 避免不必要的清屏 \x1b[2J 清屏操作在慢速网络终端上可能耗时。如果只是追加或修改少数行,可以只移动光标到特定位置进行覆盖绘制。
    • 批量输出 :将一帧中所有要输出的ANSI序列和文本拼接成一个大的 std::string std::stringstream ,最后通过一次 write 系统调用输出,这比多次调用 printf std::cout 高效得多。
  • 编辑操作优化

    • 延迟高亮 :当用户快速输入时,不必每输入一个字符就对整行甚至整个文件重新进行语法高亮。可以设置一个定时器或计数器,在用户停止输入一段时间(如200毫秒)后再触发高亮计算。
    • 行级修改跟踪 :在 Buffer 中记录哪些行被修改了,只有被修改的行才需要重新计算高亮和渲染。
  • 数据结构优化 : 当 std::vector<std::string> 成为瓶颈时(例如,在万行文件的开头频繁插入行),可以考虑引入 行块(Chunk) 的概念。例如,将每100行文本打包成一个 struct Chunk { std::array<std::string, 100> lines; } ,然后用 std::deque<Chunk> 管理。这样,插入/删除一个块影响的范围更小。这是向 Piece Table 过渡的中间方案。

构建一个现代C++版的Kilo编辑器,是一次从语言特性到系统编程的深度之旅。它强迫你思考内存、性能、API设计和用户体验。当你最终能用自己编写的编辑器修改它的源代码时,那种成就感是无可比拟的。这个项目没有终点,你可以持续为它添加功能:多标签页、剪贴板集成、插件系统、LSP(语言服务器协议)支持等等。每一次添加新功能,都是对你C++和软件设计能力的又一次锤炼。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值