28-构建第一个真实插件BuildingAPlugin

28 · 构建第一个真实插件 Building a Real Plugin(Comment Toggler)

本节目标(Goals):端到端构建一个真实插件——"注释切换器"(// 加/去),并顺手做一个保存时清尾随空格的 OnSave 钩子,验证整条插件链路。
前置知识(Prerequisites):教程 26(PluginManager)、27(PythonAPI)、21(注释规则可对照)。


1. 先补一个 API:set_line

插件要改"某一整行"。教程 27 的 API 有 get_line 但没有 set_line,补上(正好练一遍"安装 → commit"闭环):

// Editor::init 内,api.commit() 之前追加
api.install("set_line", [this](const std::vector<std::string>& a) {
    if (a.size() < 2) return std::string("err: set_line needs (row, text)");
    Buffer& buf = buffers_.active();
    const int row = std::stoi(a[0]);
    if (row < 0 || row >= buf.line_count()) return std::string("err: row out of range");
    undo_.begin(buf, {row, 0});
    buf.replace_line(row, a[1]);        // 见练习:Buffer 需有这个原语(或 open 拼接)
    undo_.end(buf, {row, (int)a[1].size()});
    notify_edit();
    return std::string();
});

replace_line(row, text) 若 Buffer 还没有,就用 open_BOF row + delete 区间 + insert 拼出来(教程 08 的区间 API 足够支撑)。真实工程里这是 Buffer 的标配方法。


2. 插件本体 plugins/comment/plugin.py

# plugins/comment/plugin.py
# 一款针对 C/C++ 的注释切换插件。

def toggle_comment():
    # 取得当前光标行
    rc  = vicarus_get_cursor()          # "r,c"
    row = int(rc.split(",")[0])
    line = vicarus_get_line(row)

    if line.startswith("// "):
        vicarus_set_line(row, line[3:])      # 去注释
    elif line.startswith("//"):
        vicarus_set_line(row, line[2:])
    else:
        vicarus_set_line(row, "// " + line)  # 加注释
    return "toggled"

def strip_trailing_whitespace():
    # OnSave 钩子:逐行去尾随空白,保留行内换行语义
    text = vicarus_get_text()
    clean = "\n".join(r.rstrip() for r in text.split("\n"))
    # 若内容变了才动(避免每次保存都制造撤销步)
    if clean != text:
        vicarus_set_text(clean)
        return "cleaned trailing spaces"
    return "nothing to clean"

vicarus_register_command("togglecomment", "toggle_comment")
vicarus_watch("OnSave", "strip_trailing_whitespace")

两个插件工程习惯:

  1. 幂等优先:strip_trailing_whitespace 只在 clean != text 时改写——保存钩子不该在没变化时静默制造撤销步;
  2. 错误也是值:辅助函数返回人类可读字符串;PluginManager 的命令路径会把它们带回到状态栏。

3. 跑起来:全链路验证

// src/main.cpp(临时)
#include <vicarus/extension/python_bridge.hpp>
#include <vicarus/extension/plugin_manager.hpp>
#include <vicarus/extension/python_api.hpp>
#include <cstdio>

int main() {
    vicarus::extension::PythonBridge bridge;
    bridge.initialize();

    // 用 PythonAPI 模拟 Editor 注册的最小后端(真实环境由 Editor 注册)
    vicarus::extension::PythonAPI api(bridge);
    std::string buffer_text = "int main() { return 0; }\n   ";
    api.install("get_text",   [&](const std::vector<std::string>&) { return buffer_text; });
    api.install("set_text",   [&](const std::vector<std::string>& a) { buffer_text = a[0]; return std::string(); });
    api.install("get_line",   [&](const std::vector<std::string>& a) {
        int r = std::stoi(a[0]);
        // 简化:按行拆缓存
        static std::vector<std::string> lines;
        lines.clear();
        std::string cur;
        for (char c : buffer_text) { if (c == '\n') { lines.push_back(cur); cur.clear(); } else cur += c; }
        lines.push_back(cur);
        return (r < (int)lines.size()) ? lines[r] : std::string();
    });
    api.install("set_line",   [&](const std::vector<std::string>& a) {
        // 行替换接上(这里用最朴素的拆/并)
        std::string cur, out; int idx = 0; bool done = false;
        for (char c : buffer_text) {
            if (c == '\n') {
                if (idx == std::stoi(a[0])) { out += a[1]; done = true; } else out += cur;
                out += '\n'; cur.clear(); ++idx;
            } else cur += c;
        }
        if (!done && idx == std::stoi(a[0])) out += a[1]; else out += cur;
        buffer_text = out;
        return std::string();
    });
    api.install("get_cursor", [](const std::vector<std::string>&) { return std::string("0,0"); });
    api.commit();

    vicarus::extension::PluginManager pm(bridge);
    pm.scan_and_load("plugins/comment");
    std::printf("before: [%s]\n", buffer_text.c_str());
    std::printf("cmd    : %s\n", pm.run_command("togglecomment", {}).value_or("?").c_str());
    std::printf("after1: [%s]\n", buffer_text.c_str());
    pm.notify(vicarus::extension::HookType::OnSave, {"demo.txt"});
    std::printf("after2: [%s]\n", buffer_text.c_str());
    return 0;
}

预期输出链条:

before: [int main() { return 0; }
   ]
cmd    : toggled
after1: [// int main() { return 0; }
   ]
after2: [// int main() { return 0; }]     ← 尾随空格被清掉

一次运行验证了三件事:命令表命中、缓冲改写、保存钩子触发。从"插件契约"到"改到缓冲"全链路通了。


4. 生命周期:enable / disable / reload

插件不是"装完就固定"的。disable 撤销登记,reload 重新执行文件——改插件代码不需重编译编辑器(这正是 Python 扩展语言的最大福利):

// 伪代码顺序(接到 :plug 子命令后)
pm.disable("comment");      // 命令表移除 togglecomment@comment
// (编辑 plugin.py)……
pm.enable("comment");       // 重新注入 + 重新登记

disable 不主动 Py_Finalize——桥是长生命周期对象;插件的 Python 函数对象在每次 enable 时重新生成,旧引用随 g_callables 键的同名覆盖而自然废弃。


5. 发布形态:一个插件的"最小发布包"

plugins/comment/
├── plugin.py          # 契约入口(必填)
├── README.md          # 说明:功能、绑定命令、钩子、依赖
├── LICENSE            # 可选
└── mod/               # 非契约辅助模块,可被 plugin.py import
    └── helpers.py

约定俗成:plugin.py 只做"登记 + 薄转发",把实在逻辑放手写进 mod/ 其他模块(可测性)。下一篇教程 29 会把它接到"配置系统"里(比如 togglecomment 用哪种注释符号由配置决定)。


本节小结(Summary)

  • 一个真实插件 = 缓存行 API + 幂等钩子 + 两句登记;
  • set_line/get_line 组合出"逐行变换"类插件所需的最小面;
  • 全链路验证脚本(模拟 Editor 后端)能证明:命令、缓冲改写、保存钩子全部生效;
  • disable/enable 提供热重载;桥长生命周期不重置;
  • 发布包结构 plugin.py + README + mod/:入口薄、逻辑内聚。

动手练习(Exercise)

  1. 把 toggle_comment 升级为"感知行首缩进": foo → // foo(保留前导空白);
  2. 配置驱动:插件读一张 settings(由后面教程 29 的 Config 注入),决定用 // 还是 # 还是 --;
  3. 给插件加"状态栏一键绑定":按下某个快捷键时调用 run_command("togglecomment")(键映射表已在教程 20);
  4. 挑战:写第二个插件 "fmt-json",用 import json 把当前缓冲区美化/压缩,提交两个命令 :json fmt / :json min。

下一节 29-配置文件系统AdvancedConfig.md:让编辑器自己说得清自己——TOML 配置一路从解析流到每个开关。

posted @ 2026-09-08 13:51  IcarusLee  阅读(11)  评论(0)    收藏  举报