From 33e3f86611ecd28eeccabd02c52517635066fcf0 Mon Sep 17 00:00:00 2001 From: Hendrik Wolff Date: Tue, 11 Jun 2024 00:39:06 +0200 Subject: [PATCH] Auto Save All Buffers After A Delay (#10899) * auto save after delay * configable * clearer names * init * working with some odd behaviour * working with greater consistency * Apply reviewer suggestions - Remove unneccessary field - Remove blocking save * Improve auto-save configuration Auto save can be configured to trigger on focus loss: ```toml auto-save.focus-lost = true|false ``` and after a time delay (in milli seconds) since last keypress: ```toml auto-save.after-delay.enable = true|false auto-save.after-delay.timeout = [0, u64::MAX] # default: 3000 ``` * Remove boilerplate and unnecessary types * Remove more useless types * Update docs for auto-save.after-delay * Fix wording of (doc) comments relating to auto-save * book: Move auto-save descriptions to separate section --------- Co-authored-by: Miguel Perez Co-authored-by: Miguel Perez --- book/src/editor.md | 11 ++++- helix-term/src/handlers.rs | 7 +++ helix-term/src/handlers/auto_save.rs | 61 +++++++++++++++++++++++++ helix-term/src/ui/editor.rs | 2 +- helix-view/src/editor.rs | 66 ++++++++++++++++++++++++++-- helix-view/src/handlers.rs | 1 + 6 files changed, 143 insertions(+), 5 deletions(-) create mode 100644 helix-term/src/handlers/auto_save.rs diff --git a/book/src/editor.md b/book/src/editor.md index 88541a7bc0e1..ba03e90e581e 100644 --- a/book/src/editor.md +++ b/book/src/editor.md @@ -32,7 +32,6 @@ | `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` | | `idle-timeout` | Time in milliseconds since last keypress before idle timers trigger. | `250` | | `completion-timeout` | Time in milliseconds after typing a word character before completions are shown, set to 5 for instant. | `250` | | `preview-completion-insert` | Whether to apply completion item instantly when selected | `true` | @@ -222,6 +221,16 @@ name = "rust" '<' = '>' ``` +### `[editor.auto-save]` Section + +Control auto save behavior. + +| Key | Description | Default | +|--|--|---------| +| `focus-lost` | 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` | +| `after-delay.enable` | Enable automatic saving after `auto-save.after-delay.timeout` milliseconds have passed since last edit. | `false` | +| `after-delay.timeout` | Time in milliseconds since last edit before auto save timer triggers. | `3000` | + ### `[editor.search]` Section Search specific options. diff --git a/helix-term/src/handlers.rs b/helix-term/src/handlers.rs index d45809d36a94..fc927313785a 100644 --- a/helix-term/src/handlers.rs +++ b/helix-term/src/handlers.rs @@ -5,12 +5,14 @@ use helix_event::AsyncHook; use crate::config::Config; use crate::events; +use crate::handlers::auto_save::AutoSaveHandler; use crate::handlers::completion::CompletionHandler; use crate::handlers::signature_help::SignatureHelpHandler; pub use completion::trigger_auto_completion; pub use helix_view::handlers::Handlers; +mod auto_save; pub mod completion; mod signature_help; @@ -19,11 +21,16 @@ pub fn setup(config: Arc>) -> Handlers { let completions = CompletionHandler::new(config).spawn(); let signature_hints = SignatureHelpHandler::new().spawn(); + let auto_save = AutoSaveHandler::new().spawn(); + let handlers = Handlers { completions, signature_hints, + auto_save, }; + completion::register_hooks(&handlers); signature_help::register_hooks(&handlers); + auto_save::register_hooks(&handlers); handlers } diff --git a/helix-term/src/handlers/auto_save.rs b/helix-term/src/handlers/auto_save.rs new file mode 100644 index 000000000000..d3f7f6fc124a --- /dev/null +++ b/helix-term/src/handlers/auto_save.rs @@ -0,0 +1,61 @@ +use std::time::Duration; + +use anyhow::Ok; +use arc_swap::access::Access; + +use helix_event::{register_hook, send_blocking}; +use helix_view::{events::DocumentDidChange, handlers::Handlers, Editor}; +use tokio::time::Instant; + +use crate::{ + commands, compositor, + job::{self, Jobs}, +}; + +#[derive(Debug)] +pub(super) struct AutoSaveHandler; + +impl AutoSaveHandler { + pub fn new() -> AutoSaveHandler { + AutoSaveHandler + } +} + +impl helix_event::AsyncHook for AutoSaveHandler { + type Event = u64; + + fn handle_event( + &mut self, + timeout: Self::Event, + _: Option, + ) -> Option { + Some(Instant::now() + Duration::from_millis(timeout)) + } + + fn finish_debounce(&mut self) { + job::dispatch_blocking(move |editor, _| request_auto_save(editor)) + } +} + +fn request_auto_save(editor: &mut Editor) { + let context = &mut compositor::Context { + editor, + scroll: Some(0), + jobs: &mut Jobs::new(), + }; + + if let Err(e) = commands::typed::write_all_impl(context, false, false) { + context.editor.set_error(format!("{}", e)); + } +} + +pub(super) fn register_hooks(handlers: &Handlers) { + let tx = handlers.auto_save.clone(); + register_hook!(move |event: &mut DocumentDidChange<'_>| { + let config = event.doc.config.load(); + if config.auto_save.after_delay.enable { + send_blocking(&tx, config.auto_save.after_delay.timeout); + } + Ok(()) + }); +} diff --git a/helix-term/src/ui/editor.rs b/helix-term/src/ui/editor.rs index 97f90f625549..d584afbb005a 100644 --- a/helix-term/src/ui/editor.rs +++ b/helix-term/src/ui/editor.rs @@ -1451,7 +1451,7 @@ impl Component for EditorView { EventResult::Consumed(None) } Event::FocusLost => { - if context.editor.config().auto_save { + if context.editor.config().auto_save.focus_lost { 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 ef4918539a54..635f72619dde 100644 --- a/helix-view/src/editor.rs +++ b/helix-view/src/editor.rs @@ -55,6 +55,8 @@ use arc_swap::{ ArcSwap, }; +pub const DEFAULT_AUTO_SAVE_DELAY: u64 = 3000; + fn deserialize_duration_millis<'de, D>(deserializer: D) -> Result where D: serde::Deserializer<'de>, @@ -266,8 +268,11 @@ pub struct Config { pub auto_completion: bool, /// Automatic formatting on save. Defaults to true. pub auto_format: bool, - /// Automatic save on focus lost. Defaults to false. - pub auto_save: bool, + /// Automatic save on focus lost and/or after delay. + /// Time delay in milliseconds since last edit after which auto save timer triggers. + /// Time delay defaults to false with 3000ms delay. Focus lost defaults to false. + #[serde(deserialize_with = "deserialize_auto_save")] + pub auto_save: AutoSave, /// Set a global text_width pub text_width: usize, /// Time in milliseconds since last keypress before idle timers trigger. @@ -771,6 +776,61 @@ impl WhitespaceRender { } } +#[derive(Debug, Default, Clone, PartialEq, Eq, Deserialize, Serialize)] +#[serde(rename_all = "kebab-case")] +pub struct AutoSave { + /// Auto save after a delay in milliseconds. Defaults to disabled. + #[serde(default)] + pub after_delay: AutoSaveAfterDelay, + /// Auto save on focus lost. Defaults to false. + #[serde(default)] + pub focus_lost: bool, +} + +#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)] +#[serde(deny_unknown_fields)] +pub struct AutoSaveAfterDelay { + #[serde(default)] + /// Enable auto save after delay. Defaults to false. + pub enable: bool, + #[serde(default = "default_auto_save_delay")] + /// Time delay in milliseconds. Defaults to [DEFAULT_AUTO_SAVE_DELAY]. + pub timeout: u64, +} + +impl Default for AutoSaveAfterDelay { + fn default() -> Self { + Self { + enable: false, + timeout: DEFAULT_AUTO_SAVE_DELAY, + } + } +} + +fn default_auto_save_delay() -> u64 { + DEFAULT_AUTO_SAVE_DELAY +} + +fn deserialize_auto_save<'de, D>(deserializer: D) -> Result +where + D: serde::Deserializer<'de>, +{ + #[derive(Deserialize, Serialize)] + #[serde(untagged, deny_unknown_fields, rename_all = "kebab-case")] + enum AutoSaveToml { + EnableFocusLost(bool), + AutoSave(AutoSave), + } + + match AutoSaveToml::deserialize(deserializer)? { + AutoSaveToml::EnableFocusLost(focus_lost) => Ok(AutoSave { + focus_lost, + ..Default::default() + }), + AutoSaveToml::AutoSave(auto_save) => Ok(auto_save), + } +} + #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[serde(default)] pub struct WhitespaceCharacters { @@ -881,7 +941,7 @@ impl Default for Config { auto_pairs: AutoPairConfig::default(), auto_completion: true, auto_format: true, - auto_save: false, + auto_save: AutoSave::default(), idle_timeout: Duration::from_millis(250), completion_timeout: Duration::from_millis(250), preview_completion_insert: true, diff --git a/helix-view/src/handlers.rs b/helix-view/src/handlers.rs index 724e7b1921ca..352abb881306 100644 --- a/helix-view/src/handlers.rs +++ b/helix-view/src/handlers.rs @@ -11,6 +11,7 @@ pub struct Handlers { // only public because most of the actual implementation is in helix-term right now :/ pub completions: Sender, pub signature_hints: Sender, + pub auto_save: Sender, } impl Handlers {