29-配置文件系统Config

29 · 配置文件系统 Config System(TOML-based Options)

本节目标(Goals):实现基于 TOML 子集的配置加载/保存,并让选项真正驱动 ignorecase、tabWidth、状态栏;set / wconfig / config reload 命令可用。
前置知识(Prerequisites):教程 22(搜索的 ignore_case 挂钩子)、17(状态栏)。


1. 配置系统的三条原则

  1. 一个键,一个值:key = value,键用 . 隔级(search.ignorecase);TOML 的 [section] 帮助分组但不改本质;
  2. 类型宽容:读侧提供 get_bool / get_int / get_str,写侧统一存字符串(真值按 true/false/1/0);
  3. 加载-响应分离: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)

  1. 完成 search.ignorecase 全链路:search_forward/backward/replace_all 都吃参数,do_search 与 %s 传入 opt_ignorecase_;
  2. 实现 wconfig 的完整序列化:按 [section] 分组写、字符串加引号、布尔写 true/false;
  3. 让配置支持环境变量展开:config.toml 里 python_path = "$VICARUS_PYDIR";
  4. 挑战:实现 g:options 的 plugin 命名空间——插件用 :set plugin.comment_style=// 配置自身,PluginManager 把该键暴露成 Python vicarus_config()。

下一节 30-性能优化Performance.md:同样的功能,快一倍——从搜索算法到渲染管线的性价比优化清单。

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