Skip to content

Commit

Permalink
Auto Save All Buffers After A Delay (#10899)
Browse files Browse the repository at this point in the history
* 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 <[email protected]>
Co-authored-by: Miguel Perez <[email protected]>
  • Loading branch information
3 people authored Jun 10, 2024
1 parent a1cda3c commit 265608a
Show file tree
Hide file tree
Showing 6 changed files with 143 additions and 5 deletions.
11 changes: 10 additions & 1 deletion book/src/editor.md
Original file line number Diff line number Diff line change
Expand Up @@ -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` |
Expand Down Expand Up @@ -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.
Expand Down
7 changes: 7 additions & 0 deletions helix-term/src/handlers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -19,11 +21,16 @@ pub fn setup(config: Arc<ArcSwap<Config>>) -> 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
}
61 changes: 61 additions & 0 deletions helix-term/src/handlers/auto_save.rs
Original file line number Diff line number Diff line change
@@ -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(())
});
}
2 changes: 1 addition & 1 deletion helix-term/src/ui/editor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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));
}
Expand Down
66 changes: 63 additions & 3 deletions helix-view/src/editor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Duration, D::Error>
where
D: serde::Deserializer<'de>,
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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<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)]
#[serde(default)]
pub struct WhitespaceCharacters {
Expand Down Expand Up @@ -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,
Expand Down
1 change: 1 addition & 0 deletions helix-view/src/handlers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ pub struct Handlers {
// only public because most of the actual implementation is in helix-term right now :/
pub completions: Sender<lsp::CompletionEvent>,
pub signature_hints: Sender<lsp::SignatureHelpEvent>,
pub auto_save: Sender<u64>,
}

impl Handlers {
Expand Down

0 comments on commit 265608a

Please sign in to comment.