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

Autosave all when the terminal loses focus #3178

Merged
merged 3 commits into from Oct 21, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions book/src/configuration.md
Expand Up @@ -49,6 +49,7 @@ on unix operating systems.
| `gutters` | Gutters to display: Available are `diagnostics` 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", "line-numbers"]` |
| `auto-completion` | Enable automatic pop up of auto-completion. | `true` |
| `auto-format` | Enable automatic formatting on save. | `true` |
| `auto-save` | Enable automatic saving on focus moving away from Helix. Requires [focus event support](https://github.com/helix-editor/helix/wiki/Terminal-Support) from your terminal. | `false` |
Copy link
Member

Choose a reason for hiding this comment

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

I think it would be better to have this as an array to prevent breaking config breakage if new auto save events are added, like you mentioned in the opening comment:

...I think an option set like the gutters option could make sense. For this, we'd have an option autosave with a default value of ["focus-lost"]...

Copy link
Member

@dead10ck dead10ck Oct 18, 2022

Choose a reason for hiding this comment

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

This doesn't have to be a breaking change. The type could just become an enum where the value is allowed to be either a boolean or a list of strings. This is what the auto pairs config option does. True could just translate to the default list internally.

In fact, this is even better for being able to disable the feature. It makes much more sense to set auto-save = false than auto-save = []

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yeah, having it function like auto-pairs for expansion was my rough thinking. Happy to change to a list now if that's the preferred way.

| `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` |
| `auto-info` | Whether to display infoboxes | `true` |
Expand Down
12 changes: 9 additions & 3 deletions helix-term/src/application.rs
Expand Up @@ -37,8 +37,8 @@ use anyhow::{Context, Error};

use crossterm::{
event::{
DisableBracketedPaste, DisableMouseCapture, EnableBracketedPaste, EnableMouseCapture,
Event as CrosstermEvent,
DisableBracketedPaste, DisableFocusChange, DisableMouseCapture, EnableBracketedPaste,
EnableFocusChange, EnableMouseCapture, Event as CrosstermEvent,
},
execute, terminal,
tty::IsTty,
Expand Down Expand Up @@ -102,6 +102,7 @@ fn restore_term() -> Result<(), Error> {
execute!(
stdout,
DisableBracketedPaste,
DisableFocusChange,
terminal::LeaveAlternateScreen
)?;
terminal::disable_raw_mode()?;
Expand Down Expand Up @@ -925,7 +926,12 @@ impl Application {
async fn claim_term(&mut self) -> Result<(), Error> {
terminal::enable_raw_mode()?;
let mut stdout = stdout();
execute!(stdout, terminal::EnterAlternateScreen, EnableBracketedPaste)?;
execute!(
stdout,
terminal::EnterAlternateScreen,
EnableBracketedPaste,
EnableFocusChange
)?;
execute!(stdout, terminal::Clear(terminal::ClearType::All))?;
if self.config.load().editor.mouse {
execute!(stdout, EnableMouseCapture)?;
Expand Down
70 changes: 25 additions & 45 deletions helix-term/src/commands/typed.rs
Expand Up @@ -559,17 +559,11 @@ pub(super) fn buffers_remaining_impl(editor: &mut Editor) -> anyhow::Result<()>
Ok(())
}

fn write_all_impl(
pub fn write_all_impl(
cx: &mut compositor::Context,
_args: &[Cow<str>],
event: PromptEvent,
quit: bool,
force: bool,
write_scratch: bool,
) -> anyhow::Result<()> {
if event != PromptEvent::Validate {
return Ok(());
}

let mut errors: Vec<&'static str> = Vec::new();
let auto_format = cx.editor.config().auto_format;
let jobs = &mut cx.jobs;
Expand All @@ -580,12 +574,13 @@ fn write_all_impl(
.documents
.values()
.filter_map(|doc| {
if doc.path().is_none() {
errors.push("cannot write a buffer without a filename\n");
if !doc.is_modified() {
return None;
}

if !doc.is_modified() {
if doc.path().is_none() {
if write_scratch {
errors.push("cannot write a buffer without a filename\n");
}
return None;
}

Expand All @@ -611,20 +606,6 @@ fn write_all_impl(
cx.editor.save::<PathBuf>(id, None, force)?;
}

if quit {
cx.block_try_flush_writes()?;

if !force {
buffers_remaining_impl(cx.editor)?;
}

// close all views
let views: Vec<_> = cx.editor.tree.views().map(|(view, _)| view.id).collect();
for view_id in views {
cx.editor.close(view_id);
}
}

if !errors.is_empty() && !force {
bail!("{:?}", errors);
}
Expand All @@ -634,49 +615,50 @@ fn write_all_impl(

fn write_all(
cx: &mut compositor::Context,
args: &[Cow<str>],
_args: &[Cow<str>],
event: PromptEvent,
) -> anyhow::Result<()> {
if event != PromptEvent::Validate {
return Ok(());
}

write_all_impl(cx, args, event, false, false)
write_all_impl(cx, false, true)
}

fn write_all_quit(
cx: &mut compositor::Context,
args: &[Cow<str>],
_args: &[Cow<str>],
event: PromptEvent,
) -> anyhow::Result<()> {
if event != PromptEvent::Validate {
return Ok(());
}

write_all_impl(cx, args, event, true, false)
write_all_impl(cx, false, true)?;
quit_all_impl(cx, false)
}

fn force_write_all_quit(
cx: &mut compositor::Context,
args: &[Cow<str>],
_args: &[Cow<str>],
event: PromptEvent,
) -> anyhow::Result<()> {
if event != PromptEvent::Validate {
return Ok(());
}

write_all_impl(cx, args, event, true, true)
let _ = write_all_impl(cx, true, true);
quit_all_impl(cx, true)
}

fn quit_all_impl(editor: &mut Editor, force: bool) -> anyhow::Result<()> {
fn quit_all_impl(cx: &mut compositor::Context, force: bool) -> anyhow::Result<()> {
cx.block_try_flush_writes()?;
if !force {
buffers_remaining_impl(editor)?;
buffers_remaining_impl(cx.editor)?;
}

// close all views
let views: Vec<_> = editor.tree.views().map(|(view, _)| view.id).collect();
let views: Vec<_> = cx.editor.tree.views().map(|(view, _)| view.id).collect();
for view_id in views {
editor.close(view_id);
cx.editor.close(view_id);
}

Ok(())
Expand All @@ -691,8 +673,7 @@ fn quit_all(
return Ok(());
}

cx.block_try_flush_writes()?;
quit_all_impl(cx.editor, false)
quit_all_impl(cx, false)
}

fn force_quit_all(
Expand All @@ -704,7 +685,7 @@ fn force_quit_all(
return Ok(());
}

quit_all_impl(cx.editor, true)
quit_all_impl(cx, true)
}

fn cquit(
Expand All @@ -722,8 +703,7 @@ fn cquit(
.unwrap_or(1);

cx.editor.exit_code = exit_code;
cx.block_try_flush_writes()?;
quit_all_impl(cx.editor, false)
quit_all_impl(cx, false)
}

fn force_cquit(
Expand All @@ -741,7 +721,7 @@ fn force_cquit(
.unwrap_or(1);
cx.editor.exit_code = exit_code;

quit_all_impl(cx.editor, true)
quit_all_impl(cx, true)
}

fn theme(
Expand Down Expand Up @@ -2141,7 +2121,7 @@ pub static TYPABLE_COMMAND_MAP: Lazy<HashMap<&'static str, &'static TypableComma
.collect()
});

pub fn command_mode(cx: &mut Context) {
pub(super) fn command_mode(cx: &mut Context) {
Copy link
Member

Choose a reason for hiding this comment

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

Why the change here?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Before my making write_all_impl pub, everything else exposed for import in commands.rs was done so through pub(super). I assumed this was supposed to as well and was trying to maintain consistency.

let mut prompt = Prompt::new(
":".into(),
Some(':'),
Expand Down
10 changes: 9 additions & 1 deletion helix-term/src/ui/editor.rs
Expand Up @@ -1426,7 +1426,15 @@ impl Component for EditorView {

Event::Mouse(event) => self.handle_mouse_event(event, &mut cx),
Event::IdleTimeout => self.handle_idle_timeout(&mut cx),
Event::FocusGained | Event::FocusLost => EventResult::Ignored(None),
Event::FocusGained => EventResult::Ignored(None),
Event::FocusLost => {
if context.editor.config().auto_save {
if let Err(e) = commands::typed::write_all_impl(context, false, false) {
context.editor.set_error(format!("{}", e));
}
}
EventResult::Consumed(None)
}
}
}

Expand Down
3 changes: 3 additions & 0 deletions helix-view/src/editor.rs
Expand Up @@ -139,6 +139,8 @@ 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,
/// Time in milliseconds since last keypress before idle timers trigger.
/// Used for autocompletion, set to 0 for instant. Defaults to 400ms.
#[serde(
Expand Down Expand Up @@ -591,6 +593,7 @@ impl Default for Config {
auto_pairs: AutoPairConfig::default(),
auto_completion: true,
auto_format: true,
auto_save: false,
idle_timeout: Duration::from_millis(400),
completion_trigger_len: 2,
auto_info: true,
Expand Down