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")
两个插件工程习惯:
- 幂等优先:
strip_trailing_whitespace只在clean != text时改写——保存钩子不该在没变化时静默制造撤销步; - 错误也是值:辅助函数返回人类可读字符串;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)
- 把
toggle_comment升级为"感知行首缩进":foo→// foo(保留前导空白); - 配置驱动:插件读一张
settings(由后面教程 29 的 Config 注入),决定用//还是#还是--; - 给插件加"状态栏一键绑定":按下某个快捷键时调用
run_command("togglecomment")(键映射表已在教程 20); - 挑战:写第二个插件 "fmt-json",用
import json把当前缓冲区美化/压缩,提交两个命令:json fmt / :json min。
下一节
29-配置文件系统AdvancedConfig.md:让编辑器自己说得清自己——TOML 配置一路从解析流到每个开关。

浙公网安备 33010602011771号