简介:这是面向 Geany 编辑器用户的 JSON 格式化插件源码包,主要解决开发者在编辑器内快速整理、压缩与校验 JSON 数据的需求。插件支持全文件或选中片段格式化,可配置缩进、斜杠转义,并能一次性区分并格式化文件中多个独立 JSON 实体,适合日常编写配置、调试接口返回或维护日志数据的中高级开发者。压缩包共 176 个文件,以 C 语言源码(17 个 .c、11 个 .h)为核心,附带 59 个 JSON 示例、58 个 golden 测试基准及构建脚本、CMake 配置、说明文档等,包体仅 163KB,结构紧凑,便于学习插件实现与二次开发。目前已有 402 人学习下载。通过阅读源码与测试用例,可以掌握基于 YAJL 库实现 JSON 解析、格式化与验证的完整思路,了解 Geany 插件开发的基本框架和构建流程,对有意扩展编辑器功能的 C 开发者具有直接参考价值。
1. 别再把 JSON 贴进网页工具了:这个 Geany 插件把格式化、压缩和校验一次做完
手写配置文件、接口联调、导出测试数据,这三件事的共同点是:你大概率在用某个轻量编辑器打开 JSON,改几行之后发现缩进全乱了、少了个逗号却死活看不出来。多数人这时候是复制到在线工具里格式化一下、再贴回来,遇到数据量大一点的 JSON,网页工具直接卡死或者截断。Geany-JSON-Prettifier 解决的就是这个场景——在 Geany 编辑器内部直接完成 JSON 的格式化(美化)、压缩(缩小)和语法校验,不需要切换窗口,不需要担心数据被网页工具截断,更不需要在终端里敲一堆手动命令。适合谁?日常用 Geany 写配置、做接口调试、维护测试数据的开发者,尤其是习惯了轻量编辑、不想为这类需求装一个重型 IDE 的人。它不改变你的编辑习惯,只是在右键菜单和快捷键层面多出三个动作。
2. 先跑通最小插件:Geany 的插件骨架、构建环境和第一个菜单项
2.1 Geany 插件到底长什么样:认识 plugin_init 和 plugin_set_info
Geany 的插件体系本质上是一组 C 语言回调函数的集合。Geany 启动时会在插件目录里扫描动态库,加载后调用你的初始化函数;插件卸载时调用清理函数。这个机制和很多编辑器插件类似,但 Geany 有个特点——插件直接运行在编辑器进程内,可以拿到当前打开的文档句柄、选区范围、甚至 Scintilla 编辑控件的底层消息接口,这意味着你能操作的不只是「把字符串替换一下」,而是能精确控制插入位置、选区、撤销分组。
常见的做法是提供三个回调:plugin_init负责注册菜单和初始化资源,plugin_cleanup负责释放。
#include <geanyplugin.h> GeanyPlugin *geany_plugin; static gboolean plugin_init(GeanyPlugin *plugin, GError **error) { /* 在这里注册 UI 菜单、按键绑定、初始化 json-glib 相关对象 */ return TRUE; } static void plugin_cleanup(GeanyPlugin *plugin) { /* 释放运行时创建的对象,Geany 会在插件卸载时自动调用 */ } void plugin_set_info(GeanyPlugin *plugin) { /* 新版本 Geany 要求通过该回调声明插件名称、描述和版本 */ geany_plugin->name = "JSON Prettifier"; geany_plugin->description = "Format, minify and validate JSON in Geany"; geany_plugin->version = "0.1.0"; geany_plugin->funcs->init = plugin_init; geany_plugin->funcs->cleanup = plugin_cleanup; }这段代码不用急着跑,先关注两个关键参数。plugin_set_info里的version字段是字符串,不要和 Geany 的插件 API 版本混淆——Geany 通过编译时的GEANY_API_VERSION宏做兼容检查,运行版本低于编译版本时会拒绝加载并给出提示。geany_plugin这个全局指针在插件加载后会指向当前插件的上下文,后续获取主菜单、按键组都要通过它。
2.2 构建环境:三行命令装齐依赖,注意 glib 版本别太老
Geany 插件开发的依赖有两层:构建时需要 Geany 的头文件和链接库,运行时的核心依赖是 GLib 和 GTK。在 Ubuntu 系或 Debian 系上,装齐一套最小构建环境只需要三条命令。开发机建议直接用geany-dev提供的头文件,而不是自己从源码维护一份,因为 Geany 插件 API 在不同小版本之间有增删,直接用发行版配套的头文件最省心。
sudo apt-get install geany geany-dev libgtk-3-dev libjson-glib-dev pkg-config --modversion geany pkg-config --modversion json-glibpkg-config --modversion geany这一步用来确认头文件版本和运行版本一致。经验是:Geany 1.36 及以上版本支持新的GeanyPlugin结构体,如果你本机是更老的版本,plugin_set_info这种写法会直接编译报错,需要退回到经典写法(用PLUGIN_VERSION_CHECK和PLUGIN_SET_INFO宏)。老版本不是不能用,但代码风格完全不同,建议直接装新版本。json-glib 的版本也值得看一眼,json_generator_set_indent这个函数在非常老的版本里参数行为有差异,遇到格式化不生效的问题时优先怀疑它。
2.3 最小插件:编译、安装、在菜单里看到你的第一个动作
这个最小插件不做任何 JSON 处理,只注册一个菜单项,点击后弹出一个消息对话框。它的意义在于验证整条链路通了——动态库能被加载、回调能被触发、UI 能响应。
#include <geanyplugin.h> GeanyPlugin *geany_plugin; GeanyData *geany_data; static GtkWidget *menu_item = NULL; static void on_hello_clicked(G_GNUC_UNUSED GtkMenuItem *item, G_GNUC_UNUSED gpointer user_data) { /* 弹窗只是验证 UI 链路,后面会替换成真正的格式化逻辑 */ gtk_widget_show_all(gtk_message_dialog_new( GTK_WINDOW(geany->main_windows->window), GTK_DIALOG_DESTROY_WITH_PARENT, GTK_MESSAGE_INFO, GTK_BUTTONS_CLOSE, "JSON plugin loaded")); } static gboolean plugin_init(GeanyPlugin *plugin, GError **error) { /* 把菜单项挂到 Geany 的“工具”菜单下 */ GtkWidget *tools_menu = geany->main_windows->tools_menu; menu_item = gtk_menu_item_new_with_mnemonic("_JSON Prettifier"); g_signal_connect(menu_item, "activate", G_CALLBACK(on_hello_clicked), NULL); gtk_widget_show(menu_item); gtk_container_add(GTK_CONTAINER(tools_menu), menu_item); return TRUE; } static void plugin_cleanup(GeanyPlugin *plugin) { /* 卸载时从菜单移除,避免重复插件加载后出现多个菜单项 */ if (menu_item != NULL) gtk_widget_destroy(menu_item); } void plugin_set_info(GeanyPlugin *plugin) { geany_plugin = plugin; geany_plugin->name = "JSON Prettifier"; geany_plugin->description = "Format, minify and validate JSON"; geany_plugin->version = "0.1.0"; geany_plugin->funcs->init = plugin_init; geany_plugin->funcs->cleanup = plugin_cleanup; }编译命令和安装位置是另一个常见的卡点。Geany 的插件扩展名在 Linux 下是.so,直接放到~/.config/geany/plugins/就可以,Geany 会自动扫描。
gcc -shared -fPIC -o json_prettifier.so json_prettifier.c \ $(pkg-config --cflags --libs geany gtk+-3.0 json-glib-1.0) mkdir -p ~/.config/geany/plugins/ cp json_prettifier.so ~/.config/geany/plugins/-shared -fPIC是动态库的标准编译参数,缺一不可。pkg-config --cflags --libs会把 Geany、GTK、json-glib 三套头文件路径和链接参数一起带进来,不需要手写-I和-l。装好之后重启 Geany,在「工具」菜单里应该能看到JSON Prettifier这个菜单项。注意安装路径按用户目录放就行,不要往系统目录里写,不同发行版的系统插件目录位置差异较大,反而容易装错。
3. 写 JSON 处理核心:格式化、缩小、验证三件套的实现
3.1 解析与生成:为什么选 json-glib 而不是自己写状态机
JSON 的语法看着简单,但自己写解析器处理转义字符、Unicode 代理对、嵌套深度、数字边界,工作量远比想象中大。这个插件选 json-glib 作为底层解析库,理由有三个:一是 Geany 本身基于 GLib,json-glib 是 GNOME 系的标准 JSON 库,引入后不会有额外的依赖冲突;二是它天然基于GObject的类型系统,错误信息直接携带GError结构,往 Geany 的状态栏或对话框里展示错误很方便;三是它支持从字符串或文件流直接解析,格式化输出时可以控制缩进和排序,满足插件所需的三个动作。
json-glib 的核心模型不复杂:JsonParser负责把字符串解析成一棵JsonNode树,JsonGenerator负责把这棵树再序列化成字符串。解析失败时会在GError里给出具体的行号和列号可以用于定位错误位置。
#include <json-glib/json-glib.h> /* 解析 JSON 字符串并返回根节点;失败时通过 error 返回原因 */ static JsonNode *parse_json(const gchar *text, GError **error) { JsonParser *parser = json_parser_new(); JsonNode *root = NULL; if (!json_parser_load_from_data(parser, text, -1, error)) { g_object_unref(parser); return NULL; } /* 取得根节点后需要增加引用计数,否则 parser 销毁后节点会失效 */ root = json_node_ref(json_parser_get_root(parser)); g_object_unref(parser); return root; }json_parser_load_from_data的第三个参数-1表示按字符串长度自动判断,不需要手动传长度。这个函数的返回只代表语法解析通过,不代表「值合法」——比如{"a":1,}这种尾逗号它会报错,但{"a":}它的错误信息可能不够直观,后面讲验证器的时候会再处理。
3.2 把「格式化为 4 空格缩进」做成一个菜单动作
格式化是三个功能里最常用的动作。它的完整链路是:读取当前文档全文 → 解析为节点树 → 用JsonGenerator重新序列化 → 替换当前文档内容。关键参数是json_generator_set_indent和json_generator_set_pretty,前者控制缩进空格数,后者控制是否启用美化模式,二者必须配合使用。
static void format_json_document(void) { GeanyDocument *doc = geany->documents->current; gchar *text, *formatted; JsonNode *root; JsonGenerator *gen; GError *error = NULL; gsize len; /* 1. 获取当前文档的全文内容 */ text = sci_get_contents(doc->editor->sci, NULL); /* 2. 解析;失败则弹窗提示并终止操作 */ root = parse_json(text, &error); if (root == NULL) { gchar *msg = g_strdup_printf("JSON parse error: %s", error->message); gtk_widget_show_all(gtk_message_dialog_new( GTK_WINDOW(geany->main_windows->window), GTK_DIALOG_DESTROY_WITH_PARENT, GTK_MESSAGE_ERROR, GTK_BUTTONS_CLOSE, "%s", msg)); g_error_free(error); g_free(msg); g_free(text); return; } /* 3. 生成格式化字符串:缩进 4 空格,美化模式开启 */ gen = json_generator_new(); json_generator_set_root(gen, root); json_generator_set_pretty(gen, TRUE); json_generator_set_indent(gen, 4); formatted = json_generator_to_data(gen, &len); /* 4. 用格式化结果替换整个文档内容 */ sci_set_text(doc->editor->sci, formatted); sci_set_save_point(doc->editor->sci); /* 重置脏标记便于自行控制保存 */ g_free(formatted); g_object_unref(gen); json_node_unref(root); g_free(text); }json_generator_set_indent(gen, 4)里的 4 是空格数,不是 tab 宽度。如果你习惯 tab 缩进,把4改成1,同时给gen设置json_generator_set_use_tab参数也会生效但不同版本 json-glib 对该参数支持不一致,建议统一用空格。sci_set_save_point这行不是必须的,但格式化后 Geany 会认为文档被修改过,如果你希望格式化动作不改变「修改状态」,保留这行;如果希望触发保存提示,把它删掉。
格式化的行为还有一个很多人没注意的点:json-glib 默认会保持对象的原始 key 顺序。如果你希望 key 按字母排序,需要在生成前调用json_generator_set_compact之外的另一个函数——实际上 json-glib 没有直接提供排序接口,常见的做法是在解析后遍历 root,手动对 members 排序,这个放到最后一章讲。
3.3 缩小器与验证器:一个输出紧凑串,一个报出精确位置
缩小器和格式化器是一对对称操作。格式化的目标是可读性,缩小器的目标是减少存储和传输成本。实现上两者共用解析逻辑,区别只在生成参数。json_generator_set_pretty(gen, FALSE)就是缩小器的核心一行,其他代码和格式化几乎一致。这里贴出完整函数,便于看到差异点。
static gchar *minify_json(const gchar *text, GError **error) { JsonNode *root = parse_json(text, error); JsonGenerator *gen; gchar *out; gsize len; if (root == NULL) return NULL; gen = json_generator_new(); json_generator_set_root(gen, root); /* 关闭 pretty 后,输出就是单行紧凑格式 */ json_generator_set_pretty(gen, FALSE); out = json_generator_to_data(gen, &len); g_object_unref(gen); json_node_unref(root); return out; }注意json_generator_set_pretty(gen, FALSE)输出的是「去掉所有多余空白」的文本,但不会删除 JSON 字符串内部的空格——也就是说{"a": "hello world"}里的"hello world"中的空格不会被压缩,这是正确行为,别指望它帮你压缩字符串内容。
验证器则更直接:不需要生成器,只做解析。不过解析失败的错误信息格式是"line: 3, column: 5 (at char 18): message"这种风格,直接弹窗显示体验不佳。常见的做法是把行号列号单独提炼出来,显示成「第 3 行第 5 列附近出错:expected ':' or '}'」这种格式。
static gboolean validate_json_document(void) { GeanyDocument *doc = geany->documents->current; gchar *text = sci_get_contents(doc->editor->sci, NULL); JsonParser *parser = json_parser_new(); GError *error = NULL; gboolean ok = TRUE; if (!json_parser_load_from_data(parser, text, -1, &error)) { /* 提炼错误位置:错误消息中自带 line 和 column 前缀 */ gchar *msg = g_strdup_printf("JSON error: %s", error->message); /* 这里可以进一步解析 line:xxx column:xxx 并跳转到编辑器的对应行, 但初版先以对话框展示,避免引入光标跳转的边界问题 */ gtk_widget_show_all(gtk_message_dialog_new( GTK_WINDOW(geany->main_windows->window), GTK_DIALOG_DESTROY_WITH_PARENT, GTK_MESSAGE_ERROR, GTK_BUTTONS_CLOSE, "%s", msg)); g_free(msg); g_error_free(error); ok = FALSE; } g_object_unref(parser); g_free(text); return ok; }validate_json_document返回gboolean是为了给后续「保存前自动校验」留接口——如果校验失败,你可以中断保存动作。这个需求很常见,但不是每个用户都需要,建议做成配置项,默认关闭。初版只弹窗提示即可,光标跳转到错误行虽然体验更好,但涉及 Scintilla 的消息通信和光标重置逻辑,后续版本再补反而更稳。
4. 避坑:编译过了装上没反应,亲历的 5 个坑
4.1 坑一:插件编译没问题,但 Geany 菜单里找不到入口
现象:.so文件已放到~/.config/geany/plugins/,重启 Geany 后在「工具」菜单里看不到任何新增项,插件管理器中也没有列出。原因排查了两天,最终发现是头文件和运行时的 API 版本不匹配。Geany 在加载插件时会做版本校验,要求GEANY_API_VERSION完全一致,你用系统自带的geany-dev编译,但运行时如果用的是从源码手动安装的另一套 Geany,版本对不上就会被静默跳过。解决:geany --version看运行版本,pkg-config --modversion geany看编译版本,两者一致后重新编译。还有一次是这个目录权限不对——.config/geany/plugins如果属主是 root,Geany 扫描时会跳过无权限文件,chmod 755解决。
4.2 坑二:格式化中文 JSON 后变成乱码
现象:JSON 里有"name": "张三",点击格式化后整个文档变成乱码,甚至有时直接崩溃。原因:sci_get_contents返回的是 UTF-8 编码的字节串,但 json-glib 解析时默认把所有输入当 UTF-8 处理,这本身没错。真正的坑在sci_set_text——它按 UTF-8 写入,但如果当前文档里混有 BOM 头,Geany 的 Scintilla 控件在设置文本后不会自动重算编码状态,导致显示乱码。解决:格式化前先检测文本开头是否有EF BB BFBOM,有则剥离,格式化完成后再把 BOM 加回去。另外,如果文档本身是 GBK 编码打开的老文件,sci_get_contents拿回来的字节串可能已被 Geany 按 UTF-8 转换过,这时 json-glib 会报非法字符错误,别纠结解析逻辑,先让用户把文件转成 UTF-8。
4.3 坑三:格式化后撤销功能直接罢工
现象:格式化内容正常,但按下 Ctrl+Z 无法一步步撤销到格式化之前的内容,甚至会撤销掉更早文本。原因:sci_set_text会重置整个文档内容,但 Scintilla 的撤销历史是基于「文本改动消息」记录的,sci_set_text相当于一次整体替换,把之前的撤销栈全清掉了。解决:先用sci_get_selection_start和sci_get_selection_end记录选区,然后调用editor->sci级别上的sci_begin_undo_action和sci_end_undo_action包裹替换动作,并在替换前后通过sci_set_selection_start恢复光标位置。这里还有个细节:sci_set_text本身会触发文档修改状态变化,如果你在前面调用了sci_set_save_point重置脏标记,要注意顺序——必须在设置文本之后调用,否则标记被错误重置。
4.4 坑四:快捷键绑定注册不生效,和系统快捷键冲突
现象:在plugin_init里用geany->plugin->key_group注册了Ctrl+Shift+F作为格式化快捷键,但按下没有任何反应,而 Geany 的「查找」里却弹出了搜索框。原因:这个组合键被 Geany 或 GTK 的某个全局加速键占用了。Geany 的按键系统支持配置和重映射,而插件注册的 keybinding 在插件管理器里可以调整,但如果你注册的键位和系统保留键冲突,GTK 加速器表会优先响应它自己的项。解决:不要硬抢系统组合键,改用Ctrl+Alt+J这类冷门组合;同时确认注册 group 的 id 在plugin_init里唯一,多个插件用同一个 key group id 时会互相覆盖。
4.5 坑五:json-glib 老版本没有sort功能,格式化结果 key 顺序「随机」
现象:同样的 JSON 内容,在某些机器上格式化后 key 顺序保持原样,在另一台机器上被打乱。原因:这不是 bug,而是 json-glib 底层用了GHashTable存储对象成员,老版本在解析后按哈希表遍历输出,顺序不稳定;较新版本才保持对象成员顺序输出。解决:不要依赖解析库的默认顺序。需要保持原序,就在解析后手动收集成员顺序;需要排序则显式做排序。这个行为在版本间有差异,插件里应该避免「默认顺序」相关假设。格式化前测试一下当前 json-glib 版本是否稳定,是省心做法。
5. 让它更趁手:只格式化选中区、保存前自动校验和插件配置项
只用菜单触发格式化,用上两周你就会发现手还是离不开鼠标。我自己的做法是把三个最常用的动作都绑上快捷键:格式化绑定Ctrl+Alt+F、缩小绑定Ctrl+Alt+M、校验绑定Ctrl+Alt+V。
/* 以格式化绑定为例,其他两个动作完全同构 */ static void register_keybindings(GeanyPlugin *plugin) { GeanyKeyGroup *group = plugin->key_group; gint kb_id = geany->be_known_keybindings( "json_prettifier_format", "Format JSON", group, on_format_keypress, NULL); /* keybindings 的字符串 id 需要全局唯一,建议用插件名做前缀 */ if (kb_id < 0) g_warning("Failed to register keybinding"); } static void on_format_keypress(guint key_id, GeanyDocument *doc) { if (doc != NULL) format_json_document(); }geany->be_known_keybindings在不同版本里 API 名称有出入,编译报错就查本机 geanyplugin.h 里带keybinding的函数原型,参数结构基本一致:先传唯一 id 字符串,再传描述,然后是 key group 和回调。注册后用户可以在「编辑 → 快捷键」里看到并自定义这个快捷键,不需要在插件里写死。
只格式化选中区是另一个高频需求。默认的格式化处理的是整个文档,但实际工作中你经常只想把某一段缩进乱掉的 JSON 重新排一下。实现思路很简单:读取sci_get_selection_start和sci_get_selection_end拿到选区边界,用sci_get_text_range读取选区文本,对这段文本做格式化,最后用sci_replace_sel替换选区内内容。注意一个边界条件:如果选区只圈中了半个 JSON 对象,解析必然失败,此时弹窗提示「选区不是完整 JSON」,不要静默失败。我在实际使用中还踩过另一个边界:光标在某个 value 内部、没有选中任何文本时,很多人会顺手按快捷键,这时应该回退到「格式化整个文档」,比弹错更快。
保存前自动校验依赖 Geany 的文档信号。用g_signal_connect监听geany->documents上的document-before-save信号,在回调里调用validate_json_document,返回 FALSE 则取消保存。这个功能有一个体验陷阱:如果用户只是临时打开一个文件想手动保存,校验失败导致无法保存会让人很恼火。所以要加一个配置开关——在偏好设置里加一个「保存前校验 JSON」的勾选项。Geany 插件没有内置配置面板生成器,常见的做法是读一个独立的配置文件,放在插件目录下,格式自己定义。我的实现是读json_prettifier.conf里的validate_on_save = true/false,每次保存前读取一次,这样用户改完配置无需重启即可生效。
把这几个功能合起来,这个插件的完成度已经相当于一个能日常使用的工具了。回想一下最初踩过的坑——版本不匹配静默失败、BOM 乱码、撤销栈断裂——每一个都靠实际使用才暴露出来。希望这个插件方向的做法和这些经验能帮你省掉这几趟弯路。如果哪天你在改一个 5000 行的 JSON 时发现右键菜单里已经有「格式化选中区」,而且光标还在老位置没有跳走,那这趟折腾就值了。
本文还有配套的精品资源,点击获取