diff --git a/crates/fresh-editor/plugins/config-schema.json b/crates/fresh-editor/plugins/config-schema.json index be43a9dfe..63f9bd990 100644 --- a/crates/fresh-editor/plugins/config-schema.json +++ b/crates/fresh-editor/plugins/config-schema.json @@ -161,6 +161,13 @@ "shell": null } }, + "live_diff": { + "description": "Live Diff plugin settings.\nControls the inline-diff view rendered against a git reference.", + "$ref": "#/$defs/LiveDiffConfig", + "default": { + "enabled_on_startup": false + } + }, "keybindings": { "description": "Custom keybindings (overrides for the active map)", "type": "array", @@ -1053,6 +1060,17 @@ "command" ] }, + "LiveDiffConfig": { + "description": "Live Diff plugin configuration.\n\nThe Live Diff plugin renders a unified-diff view directly inside the\neditable buffer (gutter glyphs, virtual deletion lines, change\nhighlights) against a git reference. By default it is opt-in: the\nuser toggles it on via the `Live Diff: Toggle (Global)` command.\nThis struct exposes settings that take effect at startup, before\nany toggle command has been issued.", + "type": "object", + "properties": { + "enabled_on_startup": { + "description": "Enable the Live Diff view globally when Fresh starts.\nWhen `true`, the editor begins each session with Live Diff on\n— equivalent to invoking `Live Diff: Toggle (Global)` once\nfrom the command palette. The user can still toggle it off\nfor the current session; the next startup re-enables it.\nDefault: false (opt-in via command palette).", + "type": "boolean", + "default": false + } + } + }, "Keybinding": { "description": "Keybinding definition", "type": "object", diff --git a/crates/fresh-editor/plugins/live_diff.ts b/crates/fresh-editor/plugins/live_diff.ts index ab1b0a09e..7feb2fb45 100644 --- a/crates/fresh-editor/plugins/live_diff.ts +++ b/crates/fresh-editor/plugins/live_diff.ts @@ -1236,6 +1236,7 @@ registerHandler("live_diff_set_default", live_diff_set_default); // ============================================================================= editor.on("after_file_open", (args) => { + applyStartupConfigOnce(); const state = ensureState(args.buffer_id); if (!state) return true; recompute(args.buffer_id).catch((e) => editor.error(`live-diff: ${e}`)); @@ -1243,6 +1244,7 @@ editor.on("after_file_open", (args) => { }); editor.on("buffer_activated", (args) => { + applyStartupConfigOnce(); const state = ensureState(args.buffer_id); if (!state) return true; // Indicators stick around across activations; only repaint if we never @@ -1345,8 +1347,31 @@ editor.exportPluginApi("live-diff", { // Initialization // ============================================================================= +// Honor `live_diff.enabled_on_startup` from user config: if the user has +// opted in, force the global toggle on at the start of every session, +// regardless of the persisted value. The user can still toggle off +// mid-session via the command palette; the next startup re-enables it. +// +// The check is one-shot per session, gated by `startupConfigApplied`. It +// runs lazily — on the first buffer event (open, activate) or the first +// recompute — rather than at plugin load. `editor.getConfig()` reads +// from a snapshot the editor refreshes between ticks, so it can return +// `{}` if the plugin loads before the snapshot has been populated; the +// lazy check guarantees we look after the editor has settled. +let startupConfigApplied = false; +function applyStartupConfigOnce(): void { + if (startupConfigApplied) return; + startupConfigApplied = true; + const config = editor.getConfig() as Record | null; + const section = (config?.live_diff as Record | undefined) ?? undefined; + if (section?.enabled_on_startup === true && !isGlobalEnabled()) { + setGlobalEnabled(true); + } +} + const initBid = editor.getActiveBufferId(); if (initBid !== 0) { + applyStartupConfigOnce(); const state = ensureState(initBid); if (state) { recompute(initBid).catch((e) => editor.error(`live-diff: ${e}`)); diff --git a/crates/fresh-editor/src/config.rs b/crates/fresh-editor/src/config.rs index c8f2b92bd..7cc5ab985 100644 --- a/crates/fresh-editor/src/config.rs +++ b/crates/fresh-editor/src/config.rs @@ -362,6 +362,11 @@ pub struct Config { #[serde(default)] pub terminal: TerminalConfig, + /// Live Diff plugin settings. + /// Controls the inline-diff view rendered against a git reference. + #[serde(default)] + pub live_diff: LiveDiffConfig, + /// Custom keybindings (overrides for the active map) #[serde(default)] pub keybindings: Vec, @@ -1854,6 +1859,34 @@ pub struct TerminalShellConfig { pub args: Vec, } +/// Live Diff plugin configuration. +/// +/// The Live Diff plugin renders a unified-diff view directly inside the +/// editable buffer (gutter glyphs, virtual deletion lines, change +/// highlights) against a git reference. By default it is opt-in: the +/// user toggles it on via the `Live Diff: Toggle (Global)` command. +/// This struct exposes settings that take effect at startup, before +/// any toggle command has been issued. +#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)] +pub struct LiveDiffConfig { + /// Enable the Live Diff view globally when Fresh starts. + /// When `true`, the editor begins each session with Live Diff on + /// — equivalent to invoking `Live Diff: Toggle (Global)` once + /// from the command palette. The user can still toggle it off + /// for the current session; the next startup re-enables it. + /// Default: false (opt-in via command palette). + #[serde(default = "default_false")] + pub enabled_on_startup: bool, +} + +impl Default for LiveDiffConfig { + fn default() -> Self { + Self { + enabled_on_startup: false, + } + } +} + /// Warning notification configuration #[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)] pub struct WarningsConfig { @@ -2417,6 +2450,7 @@ impl Default for Config { file_browser: FileBrowserConfig::default(), clipboard: ClipboardConfig::default(), terminal: TerminalConfig::default(), + live_diff: LiveDiffConfig::default(), keybindings: vec![], // User customizations only; defaults come from active_keybinding_map keybinding_maps: HashMap::new(), // User-defined maps go here active_keybinding_map: default_keybinding_map_name(), diff --git a/crates/fresh-editor/src/partial_config.rs b/crates/fresh-editor/src/partial_config.rs index 845c95af0..4d0562c4c 100644 --- a/crates/fresh-editor/src/partial_config.rs +++ b/crates/fresh-editor/src/partial_config.rs @@ -5,8 +5,8 @@ use crate::config::{ ClipboardConfig, CursorStyle, FileBrowserConfig, FileExplorerConfig, FormatterConfig, - Keybinding, KeybindingMapName, KeymapConfig, LanguageConfig, LineEndingOption, OnSaveAction, - PluginConfig, TerminalConfig, ThemeName, WarningsConfig, + Keybinding, KeybindingMapName, KeymapConfig, LanguageConfig, LineEndingOption, LiveDiffConfig, + OnSaveAction, PluginConfig, TerminalConfig, ThemeName, WarningsConfig, }; use crate::types::LspLanguageConfig; use serde::{Deserialize, Serialize}; @@ -82,6 +82,7 @@ pub struct PartialConfig { pub file_browser: Option, pub clipboard: Option, pub terminal: Option, + pub live_diff: Option, pub keybindings: Option>, pub keybinding_maps: Option>, pub active_keybinding_map: Option, @@ -107,6 +108,7 @@ impl Merge for PartialConfig { merge_partial(&mut self.file_browser, &other.file_browser); merge_partial(&mut self.clipboard, &other.clipboard); merge_partial(&mut self.terminal, &other.terminal); + merge_partial(&mut self.live_diff, &other.live_diff); merge_partial(&mut self.warnings, &other.warnings); merge_partial(&mut self.packages, &other.packages); @@ -414,6 +416,20 @@ impl Merge for PartialTerminalConfig { } } +/// Partial Live Diff plugin configuration. +#[derive(Debug, Clone, Default, Deserialize, Serialize)] +#[serde(default)] +pub struct PartialLiveDiffConfig { + pub enabled_on_startup: Option, +} + +impl Merge for PartialLiveDiffConfig { + fn merge_from(&mut self, other: &Self) { + self.enabled_on_startup + .merge_from(&other.enabled_on_startup); + } +} + /// Partial warnings configuration. #[derive(Debug, Clone, Default, Deserialize, Serialize)] #[serde(default)] @@ -859,6 +875,24 @@ impl PartialTerminalConfig { } } +impl From<&LiveDiffConfig> for PartialLiveDiffConfig { + fn from(cfg: &LiveDiffConfig) -> Self { + Self { + enabled_on_startup: Some(cfg.enabled_on_startup), + } + } +} + +impl PartialLiveDiffConfig { + pub fn resolve(self, defaults: &LiveDiffConfig) -> LiveDiffConfig { + LiveDiffConfig { + enabled_on_startup: self + .enabled_on_startup + .unwrap_or(defaults.enabled_on_startup), + } + } +} + impl From<&WarningsConfig> for PartialWarningsConfig { fn from(cfg: &WarningsConfig) -> Self { Self { @@ -988,6 +1022,7 @@ impl From<&crate::config::Config> for PartialConfig { file_browser: Some(PartialFileBrowserConfig::from(&cfg.file_browser)), clipboard: Some(PartialClipboardConfig::from(&cfg.clipboard)), terminal: Some(PartialTerminalConfig::from(&cfg.terminal)), + live_diff: Some(PartialLiveDiffConfig::from(&cfg.live_diff)), keybindings: Some(cfg.keybindings.clone()), keybinding_maps: Some(cfg.keybinding_maps.clone()), active_keybinding_map: Some(cfg.active_keybinding_map.clone()), @@ -1188,6 +1223,10 @@ impl PartialConfig { .terminal .map(|e| e.resolve(&defaults.terminal)) .unwrap_or_else(|| defaults.terminal.clone()), + live_diff: self + .live_diff + .map(|e| e.resolve(&defaults.live_diff)) + .unwrap_or_else(|| defaults.live_diff.clone()), keybindings: self .keybindings .unwrap_or_else(|| defaults.keybindings.clone()), diff --git a/crates/fresh-editor/tests/e2e/plugins/live_diff.rs b/crates/fresh-editor/tests/e2e/plugins/live_diff.rs index 872f93cee..e21ce933a 100644 --- a/crates/fresh-editor/tests/e2e/plugins/live_diff.rs +++ b/crates/fresh-editor/tests/e2e/plugins/live_diff.rs @@ -74,6 +74,51 @@ fn enable_live_diff_globally(harness: &mut EditorTestHarness) { // Tests // ============================================================================= +/// `live_diff.enabled_on_startup = true` in the user config replaces the +/// usual "open command palette, run Toggle (Global)" handshake — the +/// plugin force-enables the global toggle on init, so a modified file +/// shows diff decorations on first render with no user input. Regression +/// guard for the `enabled_on_startup` config field added in +/// fresh#1950. +#[test] +#[cfg_attr(target_os = "windows", ignore)] +fn test_live_diff_enabled_on_startup_config_renders_without_manual_toggle() { + let repo = GitTestRepo::new(); + repo.setup_typical_project(); + repo.setup_live_diff_plugin(); + + let original_dir = repo.change_to_repo_dir(); + let _guard = DirGuard::new(original_dir); + + // Same diff shape as the "added line" test below, but we never + // invoke `Live Diff: Toggle (Global)`. If the config wiring is + // broken the gutter stays empty and `wait_until` will block until + // the external test timeout fires. + repo.modify_file( + "src/utils.rs", + r#"// brand new top line added by the agent +pub fn format_output(msg: &str) -> String { + format!("[INFO] {}", msg) +} + +pub fn validate_config(config: &Config) -> bool { + config.port > 0 && !config.host.is_empty() +} +"#, + ); + + let mut config = Config::default(); + config.live_diff.enabled_on_startup = true; + let mut harness = + EditorTestHarness::with_config_and_working_dir(120, 40, config, repo.path.clone()).unwrap(); + + open_file(&mut harness, &repo.path, "src/utils.rs"); + + harness + .wait_until(|h| has_glyph(&h.screen_to_string(), '+')) + .unwrap(); +} + /// vs HEAD: an added line shows `+` in the gutter once the file is opened. /// Live-diff fetches `git show HEAD:` and diffs against the on-disk /// content (which has one new line vs HEAD), so the new line should be