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 <miguelvojito@gmail.com>
Co-authored-by: Miguel Perez <perezoji@cs.fsu.edu>
This commit is contained in:
Hendrik Wolff 2024-06-11 00:39:06 +02:00 committed by GitHub
parent a1cda3c19e
commit 265608a3d8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 143 additions and 5 deletions

View File

@ -32,7 +32,6 @@ ### `[editor]` Section
| `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"]` | | `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-completion` | Enable automatic pop up of auto-completion | `true` |
| `auto-format` | Enable automatic formatting on save | `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` | | `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` | | `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` | | `preview-completion-insert` | Whether to apply completion item instantly when selected | `true` |
@ -222,6 +221,16 @@ ### `[editor.auto-pairs]` Section
'<' = '>' '<' = '>'
``` ```
### `[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 ### `[editor.search]` Section
Search specific options. Search specific options.

View File

@ -5,12 +5,14 @@
use crate::config::Config; use crate::config::Config;
use crate::events; use crate::events;
use crate::handlers::auto_save::AutoSaveHandler;
use crate::handlers::completion::CompletionHandler; use crate::handlers::completion::CompletionHandler;
use crate::handlers::signature_help::SignatureHelpHandler; use crate::handlers::signature_help::SignatureHelpHandler;
pub use completion::trigger_auto_completion; pub use completion::trigger_auto_completion;
pub use helix_view::handlers::Handlers; pub use helix_view::handlers::Handlers;
mod auto_save;
pub mod completion; pub mod completion;
mod signature_help; mod signature_help;
@ -19,11 +21,16 @@ pub fn setup(config: Arc<ArcSwap<Config>>) -> Handlers {
let completions = CompletionHandler::new(config).spawn(); let completions = CompletionHandler::new(config).spawn();
let signature_hints = SignatureHelpHandler::new().spawn(); let signature_hints = SignatureHelpHandler::new().spawn();
let auto_save = AutoSaveHandler::new().spawn();
let handlers = Handlers { let handlers = Handlers {
completions, completions,
signature_hints, signature_hints,
auto_save,
}; };
completion::register_hooks(&handlers); completion::register_hooks(&handlers);
signature_help::register_hooks(&handlers); signature_help::register_hooks(&handlers);
auto_save::register_hooks(&handlers);
handlers handlers
} }

View File

@ -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<tokio::time::Instant>,
) -> Option<Instant> {
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(())
});
}

View File

@ -1451,7 +1451,7 @@ fn handle_event(
EventResult::Consumed(None) EventResult::Consumed(None)
} }
Event::FocusLost => { 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) { if let Err(e) = commands::typed::write_all_impl(context, false, false) {
context.editor.set_error(format!("{}", e)); context.editor.set_error(format!("{}", e));
} }

View File

@ -55,6 +55,8 @@
ArcSwap, ArcSwap,
}; };
pub const DEFAULT_AUTO_SAVE_DELAY: u64 = 3000;
fn deserialize_duration_millis<'de, D>(deserializer: D) -> Result<Duration, D::Error> fn deserialize_duration_millis<'de, D>(deserializer: D) -> Result<Duration, D::Error>
where where
D: serde::Deserializer<'de>, D: serde::Deserializer<'de>,
@ -266,8 +268,11 @@ pub struct Config {
pub auto_completion: bool, pub auto_completion: bool,
/// Automatic formatting on save. Defaults to true. /// Automatic formatting on save. Defaults to true.
pub auto_format: bool, pub auto_format: bool,
/// Automatic save on focus lost. Defaults to false. /// Automatic save on focus lost and/or after delay.
pub auto_save: bool, /// 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 /// Set a global text_width
pub text_width: usize, pub text_width: usize,
/// Time in milliseconds since last keypress before idle timers trigger. /// Time in milliseconds since last keypress before idle timers trigger.
@ -771,6 +776,61 @@ pub fn newline(&self) -> WhitespaceRenderValue {
} }
} }
#[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<AutoSave, D::Error>
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)] #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(default)] #[serde(default)]
pub struct WhitespaceCharacters { pub struct WhitespaceCharacters {
@ -881,7 +941,7 @@ fn default() -> Self {
auto_pairs: AutoPairConfig::default(), auto_pairs: AutoPairConfig::default(),
auto_completion: true, auto_completion: true,
auto_format: true, auto_format: true,
auto_save: false, auto_save: AutoSave::default(),
idle_timeout: Duration::from_millis(250), idle_timeout: Duration::from_millis(250),
completion_timeout: Duration::from_millis(250), completion_timeout: Duration::from_millis(250),
preview_completion_insert: true, preview_completion_insert: true,

View File

@ -11,6 +11,7 @@ pub struct Handlers {
// only public because most of the actual implementation is in helix-term right now :/ // only public because most of the actual implementation is in helix-term right now :/
pub completions: Sender<lsp::CompletionEvent>, pub completions: Sender<lsp::CompletionEvent>,
pub signature_hints: Sender<lsp::SignatureHelpEvent>, pub signature_hints: Sender<lsp::SignatureHelpEvent>,
pub auto_save: Sender<u64>,
} }
impl Handlers { impl Handlers {