diff --git a/book/src/configuration.md b/book/src/configuration.md index 4c8ff0647cbf..7d952a8b1c69 100644 --- a/book/src/configuration.md +++ b/book/src/configuration.md @@ -50,7 +50,7 @@ Its settings will be merged with the configuration directory `config.toml` and t | `gutters` | Gutters to display: Available are `diagnostics` and `diff` and `line-numbers` and `spacer`, note that `diagnostics` also includes other features like breakpoints, 1-width padding will be inserted if gutters is non-empty | `["diagnostics", "spacer", "line-numbers", "spacer", "diff"]` | | `auto-completion` | Enable automatic pop up of auto-completion | `true` | | `auto-format` | Enable automatic formatting on save | `true` | -| `auto-save` | Enable automatic saving on the focus moving away from Helix. Requires [focus event support](https://github.com/helix-editor/helix/wiki/Terminal-Support) from your terminal | `false` | +| `auto-save` | Enable automatic saving on certain triggers, such as `"unfocused"` (when the terminal looses focus)[^1] or `"normal-mode"` (when entering normal mode) | `[]` | | `idle-timeout` | Time in milliseconds since last keypress before idle timers trigger. Used for autocompletion, set to 0 for instant | `400` | | `completion-trigger-len` | The min-length of word under cursor to trigger autocompletion | `2` | | `completion-replace` | Set to `true` to make completions always replace the entire word and not just the part before the cursor | `false` | @@ -63,6 +63,8 @@ Its settings will be merged with the configuration directory `config.toml` and t | `text-width` | Maximum line length. Used for the `:reflow` command and soft-wrapping if `soft-wrap.wrap_at_text_width` is set | `80` | | `workspace-lsp-roots` | Directories relative to the workspace root that are treated as LSP roots. Should only be set in `.helix/config.toml` | `[]` | +[^1]: Requires [focus event support](https://github.com/helix-editor/helix/wiki/Terminal-Support) from your terminal. + ### `[editor.statusline]` Section Allows configuring the statusline at the bottom of the editor. diff --git a/helix-term/src/ui/editor.rs b/helix-term/src/ui/editor.rs index fd8e8fb21b47..2eb7eac10103 100644 --- a/helix-term/src/ui/editor.rs +++ b/helix-term/src/ui/editor.rs @@ -23,7 +23,7 @@ use helix_core::{ }; use helix_view::{ document::{Mode, SavePoint, SCRATCH_BUFFER_NAME}, - editor::{CompleteAction, CursorShapeConfig}, + editor::{AutoSaveTrigger, CompleteAction, CursorShapeConfig}, graphics::{Color, CursorKind, Modifier, Rect, Style}, input::{KeyEvent, MouseButton, MouseEvent, MouseEventKind}, keyboard::{KeyCode, KeyModifiers}, @@ -1346,7 +1346,13 @@ impl Component for EditorView { Event::IdleTimeout => self.handle_idle_timeout(&mut cx), Event::FocusGained => EventResult::Ignored(None), Event::FocusLost => { - if context.editor.config().auto_save { + // Trigger autosave if configured + if context + .editor + .config() + .auto_save + .contains(&AutoSaveTrigger::Unfocused) + { if let Err(e) = commands::typed::write_all_impl(context, false, false) { context.editor.set_error(format!("{}", e)); } diff --git a/helix-view/src/editor.rs b/helix-view/src/editor.rs index 34c59b9b4b75..b616ccd70852 100644 --- a/helix-view/src/editor.rs +++ b/helix-view/src/editor.rs @@ -211,6 +211,55 @@ impl Default for FilePickerConfig { } } +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "kebab-case")] +pub enum AutoSaveTrigger { + /// Auto save when the terminal window looses focus + Unfocused, + /// Auto save when the editor (re-)enters normal mode. + NormalMode, +} + +/// Parse the value of the `auto-save` key. +/// Can be either a boolean or a list of [AutoSaveTrigger]s. +fn deserialize_autosave_compat<'de, D>(deserializer: D) -> Result, D::Error> +where + D: Deserializer<'de>, +{ + struct AutoSaveVisitor; + + impl<'de> serde::de::Visitor<'de> for AutoSaveVisitor { + type Value = Vec; + + fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { + formatter.write_str("a list of autosave triggers or a boolean") + } + + /// New configuration: a list of AutoSaveTriggers. + fn visit_seq(self, seq: S) -> Result + where + S: serde::de::SeqAccess<'de>, + { + Deserialize::deserialize(serde::de::value::SeqAccessDeserializer::new(seq)) + } + + /// Old configuration: + /// if false => no trigger + /// if true => Only the Unfocused trigger. + fn visit_bool(self, value: bool) -> Result + where + E: serde::de::Error, + { + match value { + false => Ok(Vec::new()), + true => Ok(vec![AutoSaveTrigger::Unfocused]), + } + } + } + + deserializer.deserialize_any(AutoSaveVisitor) +} + #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "kebab-case", default, deny_unknown_fields)] pub struct Config { @@ -241,7 +290,8 @@ pub struct Config { /// Automatic formatting on save. Defaults to true. pub auto_format: bool, /// Automatic save on focus lost. Defaults to false. - pub auto_save: bool, + #[serde(deserialize_with = "deserialize_autosave_compat")] + pub auto_save: Vec, /// Set a global text_width pub text_width: usize, /// Time in milliseconds since last keypress before idle timers trigger. @@ -732,7 +782,7 @@ impl Default for Config { auto_pairs: AutoPairConfig::default(), auto_completion: true, auto_format: true, - auto_save: false, + auto_save: Vec::new(), idle_timeout: Duration::from_millis(400), completion_trigger_len: 2, auto_info: true, @@ -1658,6 +1708,25 @@ impl Editor { doc.set_selection(view.id, selection); doc.restore_cursor = false; } + + // Trigger autosave if configured + if self + .config() + .auto_save + .contains(&AutoSaveTrigger::NormalMode) + { + if self.save::(doc!(self).id, None, false).is_ok() { + // TODO: make 'modified' icon in statusline disappear. + } + + // Silently ignore errors. + // Common sources of errors include: + // - No file path set on focused document + // - Partent directory doesn't exist. + // + // In either case, autosave freaking out is probably not + // desired. + } } pub fn current_stack_frame(&self) -> Option<&StackFrame> {