Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Auto Save All Buffers After A Delay #9732

Closed
wants to merge 10 commits into from
7 changes: 7 additions & 0 deletions helix-term/src/handlers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,24 +7,31 @@ use crate::config::Config;
use crate::events;
use crate::handlers::completion::CompletionHandler;
use crate::handlers::signature_help::SignatureHelpHandler;
use crate::handlers::auto_save::AutoSaveHandler;

pub use completion::trigger_auto_completion;
pub use helix_view::handlers::lsp::SignatureHelpInvoked;
pub use helix_view::handlers::Handlers;

mod completion;
mod signature_help;
mod auto_save;

pub fn setup(config: Arc<ArcSwap<Config>>) -> Handlers {
events::register();

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
}
101 changes: 101 additions & 0 deletions helix-term/src/handlers/auto_save.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
use std::time::Duration;

use anyhow::Ok;
use arc_swap::access::Access;

use helix_event::{register_hook, send_blocking};
use helix_view::{
editor::SaveStyle,
events::DocumentDidChange,
handlers::{
lsp::{AutoSaveEvent, AutoSaveInvoked},
Handlers,
},
Editor,
};
use tokio::time::Instant;

use crate::{
commands, compositor,
job::{self, Jobs},
};

#[derive(Debug)]
enum State {
Closed,
}

#[derive(Debug)]
pub(super) struct AutoSaveHandler {
trigger: Option<AutoSaveInvoked>,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is no need to keep the trigger around , you don't actually use this

state: State,
}

impl AutoSaveHandler {
pub fn new() -> AutoSaveHandler {
AutoSaveHandler {
trigger: None,
state: State::Closed,
}
}
}

impl helix_event::AsyncHook for AutoSaveHandler {
type Event = AutoSaveEvent;

fn handle_event(
&mut self,
event: Self::Event,
_: Option<tokio::time::Instant>,
) -> Option<Instant> {
match event {
AutoSaveEvent::Trigger => {
if matches!(self.state, State::Closed) {
self.trigger = Some(AutoSaveInvoked::Automatic);
return Some(Instant::now() + Duration::from_millis(1000));
}
}
AutoSaveEvent::Cancel => {
self.state = State::Closed;
return None;
}
}

if self.trigger.is_none() {
self.trigger = Some(AutoSaveInvoked::Automatic)
}

Some(Instant::now() + Duration::from_millis(1000))
}

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));
}

if let Err(e) = context.block_try_flush_writes() {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think thsi should be here, this is only intended for shutdown. We don't want to freeze the editor while writing to disk

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 && config.save_style == SaveStyle::AfterDelay {
send_blocking(&tx, AutoSaveEvent::Trigger);
}
Ok(())
});
}
6 changes: 4 additions & 2 deletions helix-term/src/ui/editor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ use helix_core::{
};
use helix_view::{
document::{Mode, SavePoint, SCRATCH_BUFFER_NAME},
editor::{CompleteAction, CursorShapeConfig},
editor::{CompleteAction, CursorShapeConfig, SaveStyle},
graphics::{Color, CursorKind, Modifier, Rect, Style},
input::{KeyEvent, MouseButton, MouseEvent, MouseEventKind},
keyboard::{KeyCode, KeyModifiers},
Expand Down Expand Up @@ -1408,7 +1408,9 @@ impl Component for EditorView {
EventResult::Consumed(None)
}
Event::FocusLost => {
if context.editor.config().auto_save {
if context.editor.config().auto_save
&& context.editor.config().save_style == SaveStyle::FocusLost
{
if let Err(e) = commands::typed::write_all_impl(context, false, false) {
context.editor.set_error(format!("{}", e));
}
Expand Down
18 changes: 18 additions & 0 deletions helix-view/src/editor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -243,6 +243,8 @@ pub struct Config {
pub auto_format: bool,
/// Automatic save on focus lost. Defaults to false.
pub auto_save: bool,
/// When saves are performed. Defaults to on focus lost.
pub save_style: SaveStyle,
/// Set a global text_width
pub text_width: usize,
/// Time in milliseconds since last keypress before idle timers trigger.
Expand All @@ -252,6 +254,13 @@ pub struct Config {
deserialize_with = "deserialize_duration_millis"
)]
pub idle_timeout: Duration,
/// Time in milliseconds since last keypress before auto save timers trigger.
/// Used for various UI timeouts. Defaults to 1000ms.
#[serde(
serialize_with = "serialize_duration_millis",
deserialize_with = "deserialize_duration_millis"
)]
pub save_delay_timeout: Duration,
/// Time in milliseconds after typing a word character before auto completions
/// are shown, set to 5 for instant. Defaults to 250ms.
#[serde(
Expand Down Expand Up @@ -731,6 +740,13 @@ impl WhitespaceRender {
}
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum SaveStyle {
FocusLost,
AfterDelay,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(default)]
pub struct WhitespaceCharacters {
Expand Down Expand Up @@ -840,7 +856,9 @@ impl Default for Config {
auto_completion: true,
auto_format: true,
auto_save: false,
save_style: SaveStyle::FocusLost,
idle_timeout: Duration::from_millis(250),
save_delay_timeout: Duration::from_millis(1000),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe the default should be longer like 5s?

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For me the primary benefit of auto saving after a delay is updating lints from LSPs such as rust-analyzer. IMO one second is the best default for this use case. This also seems to be main use case mentioned on #3451. 1s is also the default in vscode and it works well there.

completion_timeout: Duration::from_millis(250),
preview_completion_insert: true,
completion_trigger_len: 2,
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<lsp::AutoSaveEvent>,
}

impl Handlers {
Expand Down
10 changes: 10 additions & 0 deletions helix-view/src/handlers/lsp.rs
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,16 @@ pub enum SignatureHelpEvent {
RequestComplete { open: bool },
}

#[derive(Debug, PartialEq, Eq, Clone, Copy)]
pub enum AutoSaveInvoked {
Automatic,
}

pub enum AutoSaveEvent {
Trigger,
Cancel,
}

#[derive(Debug)]
pub struct ApplyEditError {
pub kind: ApplyEditErrorKind,
Expand Down
Loading