Fresh Editor Configuration System Design
Overview
This document outlines the design for the next generation of the Fresh editor's configuration system. The current implementation relies on a single merged Config struct loaded from JSON, which lacks the flexibility of multi-layered overrides (System vs User vs Project) and robust "dual-writer" support.
The new design implements a 4-Level Overlay Architecture with Recursive Merging, utilizing standard JSON as the primary configuration format.
Architectural Goals
- 4-Level Hierarchy: Clearly distinguish between System (defaults), User (global), Project (local), and Session (volatile) settings.
- Deep Merging: recursive merging of configuration objects (maps/lists) rather than simple replacement.
- Minimal Persistence: Only save the delta (changes) from the layer below.
- Simplicity: Use standard JSON to ensure maximum compatibility and ease of implementation.
- Schema Evolution: Robust versioning and migration strategy.
1. The 4-Level Overlay Semantics
The configuration will be resolved by merging layers in the following order (lowest to highest precedence):
| Level | Source | Path (Linux/macOS) | Purpose | Mutability |
|---|---|---|---|---|
| 1. System | Embedded Binary | src/config.rs (Hardcoded) | Immutable defaults for all users. | Read-Only |
| 2. User | Global File | ~/.config/fresh/config.json | User preferences (theme, keymaps). | Read/Write |
| 3. Project | Local File | $PROJECT_ROOT/.fresh/config.json | Project-specific overrides (indentation, build commands). | Read/Write |
| 4. Session | Runtime/Volatile | Memory / .fresh/session.json | Temporary state (open files, cursor pos). | Read/Write |
Resolution Logic:EffectiveConfig = Merge(System, Merge(User, Merge(Project, Session)))
Multi-Root Workspaces (Future)
For multi-root workspaces, a 5th level "Workspace" can be inserted between User and Project.
2. Merging Strategy
The system will employ a Deep Merge strategy:
- Scalars (Int, Bool, String): Higher precedence overwrites lower precedence.
- Maps (HashMap/Objects): Recursively merged. Keys present in higher precedence override keys in lower. New keys are added.
- Lists (Arrays): Replace by default. (A new list in Project replaces the User list).
Rust Implementation Pattern
We will split the current Config struct into:
PartialConfig: A struct where all fields areOption<T>, representing a layer that might define values.ResolvedConfig: The final struct (similar to currentConfig) where all fields are concrete types.
// Represents a single layer (User, Project, etc.)
#[derive(Deserialize, Serialize)]
struct PartialConfig {
theme: Option<String>,
editor: Option<PartialEditorConfig>,
// ...
}
// Represents the final merged state used by the editor
struct Config {
theme: String,
editor: EditorConfig,
// ...
}3. Format Selection: JSON
We will use standard JSON as the configuration format.
- Primary Format:
config.json(User/Project). - Ecosystem: Universal support for syntax highlighting, linting, and automated tools.
Programmatic Edits: Since standard JSON does not support comments, we can safely use serde_json to serialize the PartialConfig layers back to disk when settings are changed via the UI.
4. Minimal Persistence
To avoid "setting drift" (where user config accumulates defaults), we implement Delta Serialization:
When saving a setting (e.g., changing tab_size to 2 in Project scope):
- Calculate Parent Value: Resolve
System + User. Say the result is4. - Compare: The new value
2differs from4. - Write Delta: We write
{"editor": {"tab_size": 2}}to the Project layer file.
If the user sets tab_size back to 4 (the parent value):
- Compare: New value
4equals parent4. - Prune: We remove the
tab_sizekey from the Project layer file, letting it inherit again.
5. Migration Strategy
We will use Sequential Programmatic Migrations handled at load time.
- Version Field: Every config file has a
versionfield (default 0). - Migrators: A chain of functions
fn migrate_v0_to_v1(serde_json::Value) -> serde_json::Value. - Process:
- Load raw JSON file.
- Apply
v0 -> v1,v1 -> v2, etc., untilCURRENT_VERSIONis reached. - Deserialization into
PartialConfighappens after migration.
6. Conditional Configuration Layers
To support advanced scenarios like platform-specific keybindings or language-specific indentation, we introduce Conditional Layers that are injected into the merge stack dynamically.
Platform Overrides
The editor will automatically look for and load platform-specific config files if they exist. These are merged after the main User config but before the Project config.
config_linux.jsonconfig_macos.jsonconfig_windows.json
Resolution: System -> User -> User(Platform) -> Project -> Session
Syntax-Specific Overrides
When a buffer with a specific language ID (e.g., "python") is active, the editor calculates an "Effective Configuration" for that buffer by injecting a language-specific layer.
This layer is derived from the languages key in the resolved config.
// config.json
{
"editor": { "tab_size": 4 },
"languages": {
"python": {
"editor": { "tab_size": 4 } // Explicit language override
},
"ruby": {
"editor": { "tab_size": 2 }
}
}
}Resolution for a Buffer:EffectiveBufferConfig = Merge(GlobalConfig, LanguageConfig)
Implementation Plan
- Refactor Config Structs: Split
ConfigintoPartialConfig(for layers) andResolvedConfig. - Implement Layer Loading: Update
config_io.rsto loadSystem,User,Projectindependently. - Implement Merge Logic: Write a recursive merge function for
PartialConfig. - Update Save Logic: Ensure
save_to_filecalculates the delta against the merged parent layers. - UI Integration: Update the Settings UI to modify the appropriate layer.
Schema Validation
We will continue to use schemars to generate JSON Schema. This provides out-of-the-box autocomplete and validation in most modern text editors.