27-为插件暴露EditorAPI
27 · 为插件暴露 Editor API(Python API for Plugins)
本节目标(Goals):给插件提供"够得着编辑器内核"的 Python API——缓冲区读写、光标移动、set/insert 文本、消息提示,全部以字符串进出。
前置知识(Prerequisites):教程 25(桥)、26(PluginManager)、09(Cursor)、08(Buffer)。
1. 三层翻译,各司其职
插件从"Python 世界"够到"编辑器内核",要穿过三层:
Python 调用 C++ 世界
vicarus_get_text() ──▶ _vicarus_c.get_text ──▶ std::function (Editor 注册)
(C 函数蹦床) (真实操作 buffer_)
- 桥(PythonBridge):提供
install_callable(name, std::function),把一个 C++ 函数挂成 Python 模块_vicarus_c里的可调用属性; - API 层(PythonAPI):把这些函数包装成
vicarus_<name>的 Python 全局函数(插件友好的平易名字); - Editor(组合根):用 lambda 把真实操作(
replace_all、光标移动……)注册进来。
关键词:string 进、string 出。插件看到的永远是字符串——这是整条链路的铁律。
2. 给桥加 install_callable(蹦床)
python_bridge.hpp 增加类型与声明:
// Python 可调的后端(参数与返回值均为字符串)
using CCallable = std::function<std::string(const std::vector<std::string>&)>;
bool install_callable(const std::string& name, CCallable fn);
实现(python_bridge.cpp,#if VICARUS_ENABLE_PYTHON 之内):
namespace {
// 静态表:名字 → 后端。单桥实例下够用(多实例需升级成实例注册表)
std::map<std::string, PythonBridge::CCallable> g_callables;
PyObject* vicarus_tramp(PyObject* self, PyObject* arg) {
// self 里保存了名字;METH_O 保证 arg 是单个对象(字符串或元组)
const char* name = PyUnicode_Check(self) ? PyUnicode_AsUTF8(self) : "";
std::vector<std::string> args;
if (PyUnicode_Check(arg)) {
args.emplace_back(PyUnicode_AsUTF8(arg));
} else if (PyTuple_Check(arg)) {
for (Py_ssize_t i = 0; i < PyTuple_Size(arg); ++i) {
PyObject* item = PyTuple_GetItem(arg, i);
if (!PyUnicode_Check(item)) { args.clear(); break; }
args.emplace_back(PyUnicode_AsUTF8(item));
}
}
auto it = g_callables.find(name);
if (it == g_callables.end()) {
PyErr_SetString(PyExc_AttributeError, "no such vicarus API");
return nullptr;
}
return PyUnicode_FromString(it->second(args).c_str());
}
PyMethodDef vicarus_mdef = {"tramp", vicarus_tramp, METH_O, nullptr};
} // namespace
bool PythonBridge::install_callable(const std::string& name, CCallable fn) {
if (!initialized_) return false;
g_callables[name] = std::move(fn);
PyObject* mod = PyImport_AddModule("_vicarus_c"); // 常驻模块
if (!mod) return false;
PyObject* f = PyCFunction_NewEx(&vicarus_mdef,
PyUnicode_FromString(name.c_str()), nullptr);
if (!f) return false;
PyModule_AddObject(mod, name.c_str(), f); // PyModule_AddObject 窃取引用
return true;
}
两处引用纪律别漏:
PyCFunction_NewEx的第二个参数(self)是新引用,PyModule_AddObject会窃取它——所以PyUnicode_FromString(name)的结果我们不手动 DÉCR;- 蹦床统一
METH_O:把"字符串或元组"都喂给一样的参数解析。
3. API 层 include/vicarus/extension/python_api.hpp
// include/vicarus/extension/python_api.hpp
#pragma once
#include <vicarus/extension/python_bridge.hpp>
#include <functional>
#include <string>
#include <utility>
#include <vector>
namespace vicarus::extension {
// 把 C++ 后端翻译成插件可见的 vicarus_* 全局函数
class PythonAPI {
public:
explicit PythonAPI(PythonBridge& bridge) : bridge_(bridge) {}
// 登记一个后端;name 会成为 _vicarus_c.<name> 与 vicarus_<name>
bool install(std::string name, PythonBridge::CCallable fn);
// 把所有已登记函数落成 __main__ 里的 vicarus_* 包装(一次 commit)
bool commit();
private:
PythonBridge& bridge_;
std::vector<std::pair<std::string, PythonBridge::CCallable>> table_;
};
} // namespace vicarus::extension
实现:
// src/extension/python_api.cpp
#include <vicarus/extension/python_api.hpp>
namespace vicarus::extension {
bool PythonAPI::install(std::string name, PythonBridge::CCallable fn) {
if (!bridge_.install_callable(name, std::move(fn))) return false;
table_.emplace_back(std::move(name), nullptr); // 占位保留名字供 commit
return true;
}
bool PythonAPI::commit() {
std::string code = "import _vicarus_c\n";
for (const auto& [name, _] : table_) {
code += "def vicarus_" + name + "(*a):\n";
code += " return _vicarus_c." + name + "(a[0] if len(a) == 1 else tuple(a))\n";
}
return bridge_.exec(code);
}
} // namespace vicarus::extension
commit用一行 Python 生成一堆vicarus_xxx包装:一个参数直接传,多个参数打包成元组。C++ 端只造蹦床,可读名字由 Python 阿司匹林式提供。
4. Editor 端接线:真实操作注册进来
Editor 创建 bridge/api/plugins 的地方(初始化尾部):
// Editor::init 尾部
auto& api = python_api_; // PythonAPI python_api_;
api.install("get_text", [this](const std::vector<std::string>&) {
return buffers_.active().to_string();
});
api.install("get_line", [this](const std::vector<std::string>& a) {
if (a.empty()) return std::string();
return line_of(buffers_.active(), std::stoi(a[0])); // 行 → 文本
});
api.install("set_text", [this](const std::vector<std::string>& a) {
if (a.empty()) return std::string("err: set_text needs text");
undo_.begin(buffers_.active(), cursor_.pos());
buffers_.active().replace_all(a[0]);
undo_.end(buffers_.active(), cursor_.pos());
notify_edit();
return std::string();
});
api.install("insert", [this](const std::vector<std::string>& a) {
if (a.empty()) return std::string("err: insert needs text");
cursor_.insert_text(a[0], buffers_.active()); // 教程 09 的光标插入
notify_edit();
return std::string();
});
api.install("get_cursor", [](const std::vector<std::string>&) {
return std::string(); // 改由下面 overload 版(真实做写字)
});
api.install("get_cursor", [this](const std::vector<std::string>&) {
return std::to_string(cursor_.pos().row) + "," + std::to_string(cursor_.pos().col);
});
api.install("set_cursor", [this](const std::vector<std::string>& a) {
if (a.size() < 1) return std::string("err: set_cursor needs 'r,c'");
const std::string rc = a[0];
const size_t comma = rc.find(',');
if (comma == std::string::npos) return std::string("err: bad format");
cursor_.set({std::stoi(rc.substr(0, comma)), std::stoi(rc.substr(comma + 1))});
return std::string();
});
api.install("message", [this](const std::vector<std::string>& a) {
status_bar_->show_message(a.empty() ? "" : a[0]);
return std::string();
});
api.commit(); // 生成 vicarus_* 包装
// 插件装载放在其后,保证插件能用到 vicarus_*
plugin_manager_.scan_and_load("plugins");
注意我连续登记了两个
get_cursor——后一个会覆盖前一个(g_callables是 map)。这是对"安装顺序即优先级"的确认:同名覆盖最后一次生效。真实的编辑器需要考虑"不能被同名插件覆盖核心 API",教程 30 再加更严的访问控制。
5. demo:插件读写缓冲区
plugins/hello/plugin.py 现在可以伸进编辑器了:
def dump():
return vicarus_get_text()
def put(line):
vicarus_set_cursor("0,0")
vicarus_insert(line + "\n")
return "inserted ok"
vicarus_register_command("dump", "dump")
vicarus_register_command("put", "put")
src/main.cpp(临时):加载插件后,用 run_command("put", {"hi from python"}) 写一行,再 run_command("dump") 读全文:
// main 中(桥+manager 初始化后)
pm.run_command("put", {"hello!"});
std::printf("buffer now: [%s]\n",
pm.run_command("dump", {}).value_or("?").c_str());
预期能看到 hello!\n 出现在缓冲区字符串开头——插件已经真实地改写了编辑器内存。这一条链路的打通,就是 Vicarus 可编程性的地基。
本节小结(Summary)
- 三层翻译:桥造蹦床、API 层造包装、Editor 供后端;全程字符串进出;
PyCFunction_NewEx(METH_O)+ 名字存self:一套蹦床服务所有 API;install_callable键名即 Python 属性名,同名后装覆盖(暴露优先级问题,教程 30 加固);- Editor 用 lambda
[]注册 6 个核心操作:读写缓冲、光标、插入、消息; scan_and_load排在api.commit()之后——插件启动即拥有vicarus_*能力。
动手练习(Exercise)
- 补
get_line之前的line_of实现(用buf.line_text(n)或open区间),并在错误时返回"err: line out of range"; - 给 API 加
undo:vicarus_undo()→ 调用 undo_.undo(),并讲清它为什么不直接暴露 undo 栈对象; - 实现
vicarus_register_operator(插件注册类似d的 Operator,复用教程 12 的 Operator 表); - 挑战:安全检查——把"插件对
set_text的调用"放进撤销步,但"插件对message"不进;给 API 加权限标志位access枚举。
下一节
28-构建第一个真实插件BuildingAPlugin.md:把"注释切换/自动格式化"写成真插件,端到端跑通。

浙公网安备 33010602011771号