29-配置文件系统Config
29 · 配置文件系统 Config System(TOML-based Options)
本节目标(Goals):实现基于 TOML 子集的配置加载/保存,并让选项真正驱动
ignorecase、tabWidth、状态栏;set / wconfig / config reload命令可用。
前置知识(Prerequisites):教程 22(搜索的ignore_case挂钩子)、17(状态栏)。
1. 配置系统的三条原则
- 一个键,一个值:
key = value,键用.隔级(search.ignorecase);TOML 的[section]帮助分组但不改本质; - 类型宽容:读侧提供
get_bool / get_int / get_str,写侧统一存字符串(真值按true/false/1/0); - 加载-响应分离:
load()只把文件灌进ConfigStore;真正的行为变更在apply_config()——Editor 拿到配置后把各选项喂给搜索、缩进、状态栏。"读"和"用"解耦,将来热重载只需重新load + apply。
2. 核心 include/vicarus/infra/config.hpp
// include/vicarus/infra/config.hpp
#pragma once
#include <map>
#include <optional>
#include <string>
namespace vicarus::infra {
// 极简 TOML 子集:section + key = value + 注释
class ConfigStore {
public:
bool load(const std::string& path); // 读文件失败返回 false
bool parse_string(const std::string& text); // 不落盘直接喂文本
void clear() noexcept { kv_.clear(); }
std::optional<std::string> get_str(const std::string& key) const;
std::optional<bool> get_bool(const std::string& key) const;
std::optional<long> get_int (const std::string& key) const;
void set(const std::string& key, const std::string& value); // 会话内覆盖
[[nodiscard]] std::size_t size() const noexcept { return kv_.size(); }
private:
std::map<std::string, std::string> kv_;
};
} // namespace vicarus::infra
用
std::map而非unordered_map:配置数量极少,却换来有序、可打印、可列目录的能力(:set无参数时按序枚举,观感佳)。
3. 实现 src/infra/config.cpp
// src/infra/config.cpp
#include <vicarus/infra/config.hpp>
#include <fstream>
#include <sstream>
namespace vicarus::infra {
namespace {
std::string trim(std::string s) {
while (!s.empty() && (s.front() == ' ' || s.front() == '\t')) s.erase(0, 1);
while (!s.empty() && (s.back() == ' ' || s.back() == '\t')) s.pop_back();
return s;
}
} // namespace
bool ConfigStore::load(const std::string& path) {
std::ifstream ifs(path);
if (!ifs) return false;
std::ostringstream buf;
buf << ifs.rdbuf();
return parse_string(buf.str());
}
bool ConfigStore::parse_string(const std::string& text) {
std::istringstream ss(text);
std::string line, section;
while (std::getline(ss, line)) {
line = trim(line);
if (line.empty() || line[0] == '#') continue; // 空/注释
if (line.front() == '[' && line.back() == ']') { // [section]
section = trim(line.substr(1, line.size() - 2));
continue;
}
const size_t eq = line.find('=');
if (eq == std::string::npos) continue;
std::string k = trim(line.substr(0, eq));
std::string v = trim(line.substr(eq + 1));
if (v == "true") v = "1";
else if (v == "false") v = "0";
else if (v.size() >= 2 && v.front() == '"' && v.back() == '"')
v = v.substr(1, v.size() - 2); // 剥引号
kv_[section.empty() ? k : section + "." + k] = v;
}
return true;
}
std::optional<std::string> ConfigStore::get_str(const std::string& key) const {
const auto it = kv_.find(key);
return it == kv_.end() ? std::nullopt : std::optional(it->second);
}
std::optional<bool> ConfigStore::get_bool(const std::string& key) const {
const auto it = kv_.find(key);
if (it == kv_.end()) return std::nullopt;
return std::optional(it->second == "1" || it->second == "true");
}
std::optional<long> ConfigStore::get_int(const std::string& key) const {
const auto it = kv_.find(key);
if (it == kv_.end()) return std::nullopt;
try { return std::optional(std::stol(it->second)); }
catch (...) { return std::nullopt; }
}
void ConfigStore::set(const std::string& key, const std::string& value) {
kv_[key] = value;
}
} // namespace vicarus::infra
TOML 子集够用:真/假、整数、字符串、节分组、注释。缺嵌套表/数组/多行串——对编辑器配置是完美取舍(够用、好讲、易测)。
4. 默认配置与 apply_config
默认文件 ~/.config/vicarus/config.toml:
# Vicarus 默认配置(教程示范)
[editor]
tabwidth = 4
number = true # 显示行号
[search]
ignorecase = true
[ui]
statusbar_style = "inverse"
Editor 侧:
// editor.hpp
infra::ConfigStore config_;
bool opt_ignorecase_ = true;
int opt_tabwidth_ = 4;
bool opt_number_ = true;
// editor.cpp —— 装载入口
void Editor::load_config(const std::string& path) {
config_.clear();
if (!config_.load(path)) {
config_.parse_string(默认文本); // 无文件也能工作
status_bar_->show_message("no config, using defaults");
}
apply_config();
}
void Editor::apply_config() {
opt_tabwidth_ = config_.get_int("editor.tabwidth").value_or(4);
opt_number_ = config_.get_bool("editor.number").value_or(true);
opt_ignorecase_ = config_.get_bool("search.ignorecase").value_or(true);
status_style_changed_ = config_.get_str("ui.statusbar_style").value_or("inverse");
notify_edit(); // 触发重绘让选项立刻显效
}
喂给搜索(教程 22 的 do_search 升级):
// core::search_forward 新参数 ignore_case;Editor 传 true/false 进去
const auto hit = opt_ignorecase_
? header_case_search()
: ...;
实做时给
search_forward/backward/replace_all的iequal加一个bool ignore_case参数(教程 22 的练习 1 正好是它)。
缩进(教程 10 的 Tab 产生空格):insert_text 收到 \t 时按 opt_tabwidth_ 展开:
if (ch == '\t') {
const int n = opt_tabwidth_ - (cursor_.col() % opt_tabwidth_);
cursor_.insert_text(std::string(n, ' '), buffers_.active());
} else { ... }
5. set 命令族
ex_execute 新增:
:set {opt} → 布尔切换到 true,数字/字符串打印现值
:set {opt}? → 打印当前值
:set {opt}={val} → 设置
:set → 枚举全部选项(提升状态栏分辨率)
:wconfig [path] → 把 config_ 序列化写回文件
布尔开关与数字解析用 ConfigStore::set 落回 config_ 再 apply_config():
// ex_execute 中 "set " 前缀解析
const std::string arg = line.substr(4);
const size_t eq = arg.find('=');
if (eq == std::string::npos) {
if (!arg.empty() && arg.back() == '?') {
const std::string k = arg.substr(0, arg.size() - 1);
status_bar_->show_message(k + "=" + config_.get_str(k).value_or("<unset>"));
} else if (!arg.empty()) {
config_.set(arg, "1"); // 布尔默认打开
apply_config();
}
} else {
config_.set(arg.substr(0, eq), arg.substr(eq + 1));
apply_config();
}
wconfig 序列化(反向写出 TOML 行即可,这里只列骨架):
// :wconfig → load 目录逐个写出 section 行
// 演示:把 kv_ 排序按 "."前段分组写 [section]
for (const auto& [k, v] : map_sorted) { ... fout << k << " = " << v << '\n'; }
6. 验证 demo(纯逻辑)
// src/main.cpp(临时)
#include <vicarus/infra/config.hpp>
#include <cstdio>
int main() {
vicarus::infra::ConfigStore cfg;
cfg.parse_string(R"(
# demo
[editor]
tabwidth = 4
[search]
ignorecase = true
)");
std::printf("ignorecase=%d tabwidth=%ld\n",
cfg.get_bool("search.ignorecase").value_or(false),
cfg.get_int("editor.tabwidth").value_or(-1));
return 0;
}
输出:
ignorecase=1 tabwidth=4
本节小结(Summary)
- TOML 子集解析:节分组 +
键=值+ 类型宽容,几十行搞定; - 加载与响应分离:
load()把文件灌进展,apply_config()才让选项生效; - 三个示范选项贯通全工程:
ignorecase→搜索、tabwidth→插入、number→行号绘制; set/wconfig让选项在会话内可改、可持久化;- 默认配置兜底:无文件也有一套 workable defaults。
动手练习(Exercise)
- 完成
search.ignorecase全链路:search_forward/backward/replace_all都吃参数,do_search与%s传入opt_ignorecase_; - 实现
wconfig的完整序列化:按[section]分组写、字符串加引号、布尔写true/false; - 让配置支持环境变量展开:
config.toml里python_path = "$VICARUS_PYDIR"; - 挑战:实现
g:options的 plugin 命名空间——插件用:set plugin.comment_style=//配置自身,PluginManager 把该键暴露成 Pythonvicarus_config()。
下一节
30-性能优化Performance.md:同样的功能,快一倍——从搜索算法到渲染管线的性价比优化清单。

浙公网安备 33010602011771号