实现一个简化版vim

概述

使用C++实现一个轻量级的vim编辑器,称为miniVim,适用于 Linux/Unix/macOS 系统,依赖标准 C++11 和 POSIX 终端控制接口,无需第三方库。

实现的特性

多模式编辑

  • 普通模式(NORMAL):默认模式,用于导航和执行命令
  • 插入模式(INSERT):输入文本内容
  • 命令模式(COMMAND):执行保存、退出等文件级操作

文件处理

  • 高效加载大文件(按行存储,避免全文件一次性读入内存瓶颈)
  • 修改状态跟踪([+] 提示未保存更改)

光标与导航

  • 基础移动:h(左)、j(下)、k(上)、l(右)
  • 行首/行尾:0 / $
  • 文件跳转:gg(开头)、G(结尾)
  • 行号跳转:在命令模式输入行号(如 :10

插入操作

  • i:光标前插入
  • I:行首插入
  • a:光标后插入
  • A:行尾插入
  • o:当前行下方新建行并插入
  • O:当前行上方新建行并插入

文本操作

  • x:删除当前字符
  • dd:剪切整行
  • yy:复制整行
  • p:粘贴(在当前行下方)

命令模式功能

  • :q:退出(有修改时提示)
  • :q!:强制退出
  • :w:保存文件
  • :wq:x:保存并退出
  • :w <filename>:另存为
  • :<line_number>:跳转到指定行

用户界面

  • 实时状态栏:显示当前模式、文件名、修改状态

简单原理

采用 Raw Mode(原始模式) 控制终端,绕过系统行缓冲和回显机制,实现即时按键响应。

ANSI 转义序列:控制光标、清屏、颜色等

  • \x1b[H:光标移至左上角
  • \x1b[row;colH:精确定位光标
  • \x1b[K:清除从光标到行尾
  • \x1b[7m / \x1b[m:反色/重置颜色

数据结构设计

struct EditorState {
    std::vector<std::string> lines;   // 文件内容,每行一个字符串
    int cursor_x, cursor_y;           // 逻辑光标位置(0-based)
    EditorMode mode;                  // 当前编辑模式
    std::string filename;             // 当前编辑文件名
    bool modified;                    // 是否有未保存修改
    std::string clipboard;            // 剪贴板(仅支持整行)
};
  • 行列表存储:适合文本编辑场景,插入/删除行复杂度 O(n),但实际使用中 n 为行号,性能可接受
  • 内存友好:仅加载必要内容
  • 简单高效:避免复杂数据结构,降低维护成本

清屏(\x1b[2J)会导致明显闪烁,采用增量覆盖渲染,避免闪烁。

渲染流程:

  1. 获取终端尺寸(ioctl(TIOCGWINSZ)
  2. 计算可视区域(rows - 1 行用于编辑,1 行为状态栏)
  3. 逐行精确定位绘制
    • 使用 \x1b[row;1H 移动到每行开头
    • 输出截断后的行内容(不超过终端宽度)
    • \x1b[K 清除行尾残留
  4. 绘制状态栏(最后一行)
  5. 定位终端光标到逻辑光标位置
  6. 强制刷新输出缓冲(std::cout.flush()

文件IO优化

读取文件

  • 使用 std::getline 逐行读入,避免大内存分配
  • 空文件自动初始化为 [""]
  • 时间复杂度:O(N),N 为文件行数

保存文件

  • 逐行写入,末尾不添加多余换行
  • 保存成功后重置 modified = false

大文件支持

  • 仅存储文本内容,无额外元数据
  • 实测可流畅编辑 100MB+ 文件

完整代码

//minivim.cpp
#include <iostream>
#include <fstream>
#include <vector>
#include <string>
#include <sstream>
#include <termios.h>
#include <unistd.h>
#include <sys/ioctl.h>
#include <cstring>
#include <cstdlib>
#include <csignal>
#include <algorithm>

void disableRawMode();
void enableRawMode();

// 定义编辑器模式
enum class EditorMode {
    NORMAL,    // 普通模式
    INSERT,    // 插入模式
    COMMAND    // 命令模式
};

// 定义编辑器状态
struct EditorState {
    std::vector<std::string> lines;
    int cursor_x = 0;        // 光标列位置
    int cursor_y = 0;        // 光标行位置
    EditorMode mode = EditorMode::NORMAL;
    std::string filename;
    bool modified = false;   // 文件是否被修改
    std::string clipboard;   // 剪贴板内容
};

// 全局变量用于恢复终端
static struct termios orig_termios;

// 恢复终端设置(用于退出时)
void die(const char* s) {
    disableRawMode();
    perror(s);
    exit(1);
}

// 设置终端为原始模式
void enableRawMode() {
    if (tcgetattr(STDIN_FILENO, &orig_termios) == -1) die("tcgetattr");
    atexit(disableRawMode); // 确保退出时恢复

    struct termios raw = orig_termios;
    raw.c_lflag &= ~(ECHO | ICANON);
    if (tcsetattr(STDIN_FILENO, TCSAFLUSH, &raw) == -1) die("tcsetattr");
}

// 恢复终端正常模式
void disableRawMode() {
    if (tcsetattr(STDIN_FILENO, TCSAFLUSH, &orig_termios) == -1)
        die("tcsetattr");
}

// 获取终端大小
void getTerminalSize(int& rows, int& cols) {
    struct winsize w;
    if (ioctl(STDOUT_FILENO, TIOCGWINSZ, &w) == -1) {
        rows = 24;
        cols = 80;
        return;
    }
    rows = w.ws_row;
    cols = w.ws_col;
}

// 读取单个字符
int readKey() {
    int nread;
    char c;
    while ((nread = read(STDIN_FILENO, &c, 1)) != 1) {
        if (nread == -1) die("read");
    }
    return c;
}

// 读取文件内容到编辑器状态
void readFile(EditorState& state, const std::string& filename) {
    std::ifstream file(filename);
    state.filename = filename;
    
    if (!file.is_open()) {
        state.lines = {""};
        return;
    }
    
    std::string line;
    while (std::getline(file, line)) {
        state.lines.push_back(line);
    }
    
    if (state.lines.empty()) {
        state.lines.push_back("");
    }
    
    file.close();
}

// 保存文件
bool saveFile(EditorState& state) {
    std::ofstream file(state.filename);
    if (!file.is_open()) {
        return false;
    }
    
    for (size_t i = 0; i < state.lines.size(); ++i) {
        file << state.lines[i];
        if (i < state.lines.size() - 1) {
            file << '\n';
        }
    }
    
    file.close();
    state.modified = false;
    return true;
}

// 移动光标到指定位置(1-based)
void moveCursor(int x, int y) {
    std::cout << "\x1b[" << y << ";" << x << "H";
}

void drawStatusBar(EditorState& state, int rows, int cols) {
    // 移动到状态栏行(最后一行),第1列
    std::cout << "\x1b[" << rows << ";1H";
    std::cout << "\x1b[7m"; // 反色背景
    
    std::string modeStr;
    switch (state.mode) {
        case EditorMode::NORMAL: modeStr = "NORMAL"; break;
        case EditorMode::INSERT: modeStr = "INSERT"; break;
        case EditorMode::COMMAND: modeStr = "COMMAND"; break;
    }
    
    std::string status = "--" + modeStr + "-- " + state.filename;
    if (state.modified) {
        status += " [+]";
    }
    
    if ((int)status.length() > cols) {
        status = status.substr(0, cols);
    } else {
        // 填充空格到行尾
        status.append(cols - status.length(), ' ');
    }
    
    std::cout << status;
    std::cout << "\x1b[m"; // 恢复默认样式
}

void drawEditor(EditorState& state) {
    int rows, cols;
    getTerminalSize(rows, cols);
    
    int screen_rows = rows - 1; // 留一行给状态栏
    
    // 逐行绘制,使用精确光标定位
    for (int i = 0; i < screen_rows; i++) {
        // 移动到第 (i+1) 行,第 1 列
        std::cout << "\x1b[" << (i + 1) << ";1H";
        
        if (i < (int)state.lines.size()) {
            std::string line = state.lines[i];
            if ((int)line.length() > cols) {
                line = line.substr(0, cols);
            }
            std::cout << line;
        }
        // 清除该行右侧可能残留的内容
        std::cout << "\x1b[K";
    }
    
    // 清除多余行(如果文件行数 < 屏幕行数)
    for (int i = (int)state.lines.size(); i < screen_rows; i++) {
        std::cout << "\x1b[" << (i + 1) << ";1H\x1b[K";
    }
    
    // 绘制状态栏(最后一行)
    drawStatusBar(state, rows, cols);
    
    // 设置光标位置
    int cursor_screen_y = state.cursor_y + 1;
    int cursor_screen_x = state.cursor_x + 1;
    
    // 限制在可视区域
    if (cursor_screen_y > screen_rows) cursor_screen_y = screen_rows;
    if (cursor_screen_y < 1) cursor_screen_y = 1;
    if (cursor_screen_x > cols) cursor_screen_x = cols;
    if (cursor_screen_x < 1) cursor_screen_x = 1;
    
    // 移动终端光标到正确位置
    std::cout << "\x1b[" << cursor_screen_y << ";" << cursor_screen_x << "H";
    
    // 刷新输出缓冲区
    std::cout.flush();
}


void adjustCursor(EditorState& state) {
    if (state.cursor_y < 0) state.cursor_y = 0;
    if (state.cursor_y >= (int)state.lines.size()) state.cursor_y = state.lines.size() - 1;
    
    if (state.cursor_x < 0) state.cursor_x = 0;
    if (state.cursor_x > (int)state.lines[state.cursor_y].length()) {
        state.cursor_x = state.lines[state.cursor_y].length();
    }
}

// 处理普通模式按键
void handleNormalMode(EditorState& state, int key) {
    switch (key) {
        case 'h': state.cursor_x--; break;
        case 'j': state.cursor_y++; break;
        case 'k': state.cursor_y--; break;
        case 'l': state.cursor_x++; break;
        case '0': state.cursor_x = 0; break;
        case '$': state.cursor_x = state.lines[state.cursor_y].length(); break;
        case 'g': {
            int nextKey = readKey();
            if (nextKey == 'g') {
                state.cursor_y = 0;
                state.cursor_x = 0;
            }
            break;
        }
        case 'G':
            state.cursor_y = state.lines.size() - 1;
            state.cursor_x = 0;
            break;
        case 'i': state.mode = EditorMode::INSERT; break;
        case 'I': state.cursor_x = 0; state.mode = EditorMode::INSERT; break;
        case 'a': state.cursor_x++; state.mode = EditorMode::INSERT; break;
        case 'A': state.cursor_x = state.lines[state.cursor_y].length(); state.mode = EditorMode::INSERT; break;
        case 'o': {
            state.lines.insert(state.lines.begin() + state.cursor_y + 1, "");
            state.cursor_y++;
            state.cursor_x = 0;
            state.mode = EditorMode::INSERT;
            state.modified = true;
            break;
        }
        case 'O': {
            state.lines.insert(state.lines.begin() + state.cursor_y, "");
            state.cursor_x = 0;
            state.mode = EditorMode::INSERT;
            state.modified = true;
            break;
        }
        case 'x': {
            if (state.cursor_x < (int)state.lines[state.cursor_y].length()) {
                state.lines[state.cursor_y].erase(state.cursor_x, 1);
                state.modified = true;
            }
            break;
        }
        case 'd': {
            int nextKey = readKey();
            if (nextKey == 'd') {
                state.clipboard = state.lines[state.cursor_y];
                state.lines.erase(state.lines.begin() + state.cursor_y);
                if (state.lines.empty()) {
                    state.lines.push_back("");
                    state.cursor_y = 0;
                } else if (state.cursor_y >= (int)state.lines.size()) {
                    state.cursor_y = state.lines.size() - 1;
                }
                state.cursor_x = 0;
                state.modified = true;
            }
            break;
        }
        case 'y': {
            int nextKey = readKey();
            if (nextKey == 'y') {
                state.clipboard = state.lines[state.cursor_y];
            }
            break;
        }
        case 'p': {
            if (!state.clipboard.empty()) {
                state.lines.insert(state.lines.begin() + state.cursor_y + 1, state.clipboard);
                state.cursor_y++;
                state.cursor_x = 0;
                state.modified = true;
            }
            break;
        }
        case ':': state.mode = EditorMode::COMMAND; break;
    }
    
    adjustCursor(state);
}

// 处理插入模式按键
void handleInsertMode(EditorState& state, int key) {
    if (key == 27) { // ESC
        state.mode = EditorMode::NORMAL;
        return;
    }
    
    if (key == '\n' || key == '\r') {
        std::string currentLine = state.lines[state.cursor_y];
        std::string newLine = currentLine.substr(state.cursor_x);
        state.lines[state.cursor_y] = currentLine.substr(0, state.cursor_x);
        state.lines.insert(state.lines.begin() + state.cursor_y + 1, newLine);
        state.cursor_y++;
        state.cursor_x = 0;
        state.modified = true;
    } else if (key == 127 || key == 8) { // Backspace
        if (state.cursor_x > 0) {
            state.lines[state.cursor_y].erase(state.cursor_x - 1, 1);
            state.cursor_x--;
            state.modified = true;
        } else if (state.cursor_y > 0) {
            int prevLineLen = state.lines[state.cursor_y - 1].length();
            state.lines[state.cursor_y - 1] += state.lines[state.cursor_y];
            state.lines.erase(state.lines.begin() + state.cursor_y);
            state.cursor_y--;
            state.cursor_x = prevLineLen;
            state.modified = true;
        }
    } else if (key >= 32 && key <= 126) {
        state.lines[state.cursor_y].insert(state.cursor_x, 1, (char)key);
        state.cursor_x++;
        state.modified = true;
    }
}

void handleCommandMode(EditorState& state, std::string& command) {
    bool shouldExit = false;
    bool saveSuccess = false;
    
    if (command == "q") {
        if (state.modified) {
            // 显示提示,但不清除命令(短暂显示)
            std::cout << "\nFile has unsaved changes. Use 'q!' to force quit or 'wq' to save and quit.\n";
            std::cout.flush();
            sleep(2);
        } else {
            shouldExit = true;
        }
    } else if (command == "q!") {
        shouldExit = true;
    } else if (command == "w") {
        saveSuccess = saveFile(state);
        if (saveSuccess) {
            std::cout << "\nFile saved.\n";
        } else {
            std::cout << "\nError saving file.\n";
        }
        std::cout.flush();
        sleep(1);
    } else if (command == "wq" || command == "x") {
        saveSuccess = saveFile(state);
        if (saveSuccess) {
            shouldExit = true;
        } else {
            std::cout << "\nError saving file.\n";
            std::cout.flush();
            sleep(1);
        }
    } else if (command.size() >= 2 && command.substr(0, 2) == "w " && command.size() > 2) {
        std::string newFilename = command.substr(2);
        if (!newFilename.empty()) {
            state.filename = newFilename;
            saveSuccess = saveFile(state);
            if (saveSuccess) {
                std::cout << "\nFile saved as " << newFilename << ".\n";
            } else {
                std::cout << "\nError saving file.\n";
            }
            std::cout.flush();
            sleep(1);
        }
    } else if (!command.empty() && std::all_of(command.begin(), command.end(), ::isdigit)) {
        try {
            int lineNum = std::stoi(command);
            if (lineNum > 0 && lineNum <= (int)state.lines.size()) {
                state.cursor_y = lineNum - 1;
                state.cursor_x = 0;
            }
        } catch (...) {
            // ignore
        }
    }
    
    // 如果需要退出,先清理屏幕再退出
    if (shouldExit) {
        disableRawMode();
        
        // 获取终端大小
        int rows, cols;
        getTerminalSize(rows, cols);
        
        // 移动光标到屏幕底部新行
        std::cout << "\x1b[" << rows << ";1H"; // 移动到状态栏行
        std::cout << "\x1b[K";                 // 清除状态栏
        std::cout << "\n";                     // 换到新行
        std::cout.flush();
        
        exit(0);
    }
    
    state.mode = EditorMode::NORMAL;
}


// 主编辑循环
void editorLoop(EditorState& state) {
    std::string commandBuffer;
    
    while (true) {
        drawEditor(state);
        
        if (state.mode == EditorMode::COMMAND) {
            int rows, cols;
            getTerminalSize(rows, cols);
            std::cout << "\x1b[" << rows << ";1H:";
            std::cout << commandBuffer;
            std::cout << "\x1b[K"; // 清除命令行多余内容
            std::cout.flush();
        }
        
        int key = readKey();
        
        if (state.mode == EditorMode::NORMAL) {
            handleNormalMode(state, key);
        } else if (state.mode == EditorMode::INSERT) {
            handleInsertMode(state, key);
        } else if (state.mode == EditorMode::COMMAND) {
            if (key == '\n' || key == '\r') {
                handleCommandMode(state, commandBuffer);
                commandBuffer.clear();
            } else if (key == 27) {
                state.mode = EditorMode::NORMAL;
                commandBuffer.clear();
            } else if (key == 127 || key == 8) {
                if (!commandBuffer.empty()) {
                    commandBuffer.pop_back();
                }
            } else if (key >= 32 && key <= 126) {
                commandBuffer += (char)key;
            }
        }
    }
}
int main(int argc, char* argv[]) {
    if (argc != 2) {
        std::cerr << "Usage: " << argv[0] << " <filename>\n";
        return 1;
    }
    
    EditorState state;
    readFile(state, argv[1]);
    
    enableRawMode();
    
    // 清屏一次,确保干净开始
    std::cout << "\x1b[2J\x1b[H";
    std::cout.flush();
    
    // 设置异常退出处理器
    std::set_terminate([]() {
        disableRawMode();
        std::abort();
    });
    
    editorLoop(state);
    
    return 0;
}

编译运行

g++ -std=c++11 -O2 -o minivim minivim.cpp
./minivim test.txt