on master help sign in
Autosave all when the terminal loses focus (#3178)
7e29ee6Charlie Groves
book/src/configuration.md+1
@@ -49,6 +49,7 @@ on unix operating systems.
49| `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"]` |
49| `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"]` |
50| `auto-completion` | Enable automatic pop up of auto-completion. | `true` |
50| `auto-completion` | Enable automatic pop up of auto-completion. | `true` |
51| `auto-format` | Enable automatic formatting on save. | `true` |
51| `auto-format` | Enable automatic formatting on save. | `true` |
52+| `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` |
52| `idle-timeout` | Time in milliseconds since last keypress before idle timers trigger. Used for autocompletion, set to 0 for instant. | `400` |
53| `idle-timeout` | Time in milliseconds since last keypress before idle timers trigger. Used for autocompletion, set to 0 for instant. | `400` |
53| `completion-trigger-len` | The min-length of word under cursor to trigger autocompletion | `2` |
54| `completion-trigger-len` | The min-length of word under cursor to trigger autocompletion | `2` |
54| `auto-info` | Whether to display infoboxes | `true` |
55| `auto-info` | Whether to display infoboxes | `true` |
helix-term/src/application.rs+9−3
@@ -37,8 +37,8 @@ use anyhow::{Context, Error};
37
37
38use crossterm::{
38use crossterm::{
39 event::{
39 event::{
40− DisableBracketedPaste, DisableMouseCapture, EnableBracketedPaste, EnableMouseCapture,
40+ DisableBracketedPaste, DisableFocusChange, DisableMouseCapture, EnableBracketedPaste,
41− Event as CrosstermEvent,
41+ EnableFocusChange, EnableMouseCapture, Event as CrosstermEvent,
42 },
42 },
43 execute, terminal,
43 execute, terminal,
44 tty::IsTty,
44 tty::IsTty,
@@ -102,6 +102,7 @@ fn restore_term() -> Result<(), Error> {
102 execute!(
102 execute!(
103 stdout,
103 stdout,
104 DisableBracketedPaste,
104 DisableBracketedPaste,
105+ DisableFocusChange,
105 terminal::LeaveAlternateScreen
106 terminal::LeaveAlternateScreen
106 )?;
107 )?;
107 terminal::disable_raw_mode()?;
108 terminal::disable_raw_mode()?;
@@ -925,7 +926,12 @@ impl Application {
925 async fn claim_term(&mut self) -> Result<(), Error> {
926 async fn claim_term(&mut self) -> Result<(), Error> {
926 terminal::enable_raw_mode()?;
927 terminal::enable_raw_mode()?;
927 let mut stdout = stdout();
928 let mut stdout = stdout();
929+ execute!(
930+ stdout,
928− execute!(stdout, terminal::EnterAlternateScreen, EnableBracketedPaste)?;
931+ terminal::EnterAlternateScreen,
932+ EnableBracketedPaste,
933+ EnableFocusChange
934+ )?;
929 execute!(stdout, terminal::Clear(terminal::ClearType::All))?;
935 execute!(stdout, terminal::Clear(terminal::ClearType::All))?;
930 if self.config.load().editor.mouse {
936 if self.config.load().editor.mouse {
931 execute!(stdout, EnableMouseCapture)?;
937 execute!(stdout, EnableMouseCapture)?;
helix-term/src/commands/typed.rs+25−45
@@ -559,17 +559,11 @@ pub(super) fn buffers_remaining_impl(editor: &mut Editor) -> anyhow::Result<()>
559 Ok(())
559 Ok(())
560}
560}
561
561
562−fn write_all_impl(
562+pub fn write_all_impl(
563 cx: &mut compositor::Context,
563 cx: &mut compositor::Context,
564− _args: &[Cow<str>],
565− event: PromptEvent,
566− quit: bool,
567 force: bool,
564 force: bool,
565+ write_scratch: bool,
568) -> anyhow::Result<()> {
566) -> anyhow::Result<()> {
569− if event != PromptEvent::Validate {
570− return Ok(());
571− }
572−
573 let mut errors: Vec<&'static str> = Vec::new();
567 let mut errors: Vec<&'static str> = Vec::new();
574 let auto_format = cx.editor.config().auto_format;
568 let auto_format = cx.editor.config().auto_format;
575 let jobs = &mut cx.jobs;
569 let jobs = &mut cx.jobs;
@@ -580,12 +574,13 @@ fn write_all_impl(
580 .documents
574 .documents
581 .values()
575 .values()
582 .filter_map(|doc| {
576 .filter_map(|doc| {
583− if doc.path().is_none() {
577+ if !doc.is_modified() {
584− errors.push("cannot write a buffer without a filename\n");
585 return None;
578 return None;
586 }
579 }
587−
580+ if doc.path().is_none() {
588− if !doc.is_modified() {
581+ if write_scratch {
582+ errors.push("cannot write a buffer without a filename\n");
583+ }
589 return None;
584 return None;
590 }
585 }
591
586
@@ -611,20 +606,6 @@ fn write_all_impl(
611 cx.editor.save::<PathBuf>(id, None, force)?;
606 cx.editor.save::<PathBuf>(id, None, force)?;
612 }
607 }
613
608
614− if quit {
615− cx.block_try_flush_writes()?;
616−
617− if !force {
618− buffers_remaining_impl(cx.editor)?;
619− }
620−
621− // close all views
622− let views: Vec<_> = cx.editor.tree.views().map(|(view, _)| view.id).collect();
623− for view_id in views {
624− cx.editor.close(view_id);
625− }
626− }
627−
628 if !errors.is_empty() && !force {
609 if !errors.is_empty() && !force {
629 bail!("{:?}", errors);
610 bail!("{:?}", errors);
630 }
611 }
@@ -634,49 +615,50 @@ fn write_all_impl(
634
615
635fn write_all(
616fn write_all(
636 cx: &mut compositor::Context,
617 cx: &mut compositor::Context,
637− args: &[Cow<str>],
618+ _args: &[Cow<str>],
638 event: PromptEvent,
619 event: PromptEvent,
639) -> anyhow::Result<()> {
620) -> anyhow::Result<()> {
640 if event != PromptEvent::Validate {
621 if event != PromptEvent::Validate {
641 return Ok(());
622 return Ok(());
642 }
623 }
643
624
644− write_all_impl(cx, args, event, false, false)
625+ write_all_impl(cx, false, true)
645}
626}
646
627
647fn write_all_quit(
628fn write_all_quit(
648 cx: &mut compositor::Context,
629 cx: &mut compositor::Context,
649− args: &[Cow<str>],
630+ _args: &[Cow<str>],
650 event: PromptEvent,
631 event: PromptEvent,
651) -> anyhow::Result<()> {
632) -> anyhow::Result<()> {
652 if event != PromptEvent::Validate {
633 if event != PromptEvent::Validate {
653 return Ok(());
634 return Ok(());
654 }
635 }
655−
656− write_all_impl(cx, args, event, true, false)
636+ write_all_impl(cx, false, true)?;
637+ quit_all_impl(cx, false)
657}
638}
658
639
659fn force_write_all_quit(
640fn force_write_all_quit(
660 cx: &mut compositor::Context,
641 cx: &mut compositor::Context,
661− args: &[Cow<str>],
642+ _args: &[Cow<str>],
662 event: PromptEvent,
643 event: PromptEvent,
663) -> anyhow::Result<()> {
644) -> anyhow::Result<()> {
664 if event != PromptEvent::Validate {
645 if event != PromptEvent::Validate {
665 return Ok(());
646 return Ok(());
666 }
647 }
667−
668− write_all_impl(cx, args, event, true, true)
648+ let _ = write_all_impl(cx, true, true);
649+ quit_all_impl(cx, true)
669}
650}
670
651
671−fn quit_all_impl(editor: &mut Editor, force: bool) -> anyhow::Result<()> {
652+fn quit_all_impl(cx: &mut compositor::Context, force: bool) -> anyhow::Result<()> {
653+ cx.block_try_flush_writes()?;
672 if !force {
654 if !force {
673− buffers_remaining_impl(editor)?;
655+ buffers_remaining_impl(cx.editor)?;
674 }
656 }
675
657
676 // close all views
658 // close all views
677− let views: Vec<_> = editor.tree.views().map(|(view, _)| view.id).collect();
659+ let views: Vec<_> = cx.editor.tree.views().map(|(view, _)| view.id).collect();
678 for view_id in views {
660 for view_id in views {
679− editor.close(view_id);
661+ cx.editor.close(view_id);
680 }
662 }
681
663
682 Ok(())
664 Ok(())
@@ -691,8 +673,7 @@ fn quit_all(
691 return Ok(());
673 return Ok(());
692 }
674 }
693
675
694− cx.block_try_flush_writes()?;
695− quit_all_impl(cx.editor, false)
676+ quit_all_impl(cx, false)
696}
677}
697
678
698fn force_quit_all(
679fn force_quit_all(
@@ -704,7 +685,7 @@ fn force_quit_all(
704 return Ok(());
685 return Ok(());
705 }
686 }
706
687
707− quit_all_impl(cx.editor, true)
688+ quit_all_impl(cx, true)
708}
689}
709
690
710fn cquit(
691fn cquit(
@@ -722,8 +703,7 @@ fn cquit(
722 .unwrap_or(1);
703 .unwrap_or(1);
723
704
724 cx.editor.exit_code = exit_code;
705 cx.editor.exit_code = exit_code;
725− cx.block_try_flush_writes()?;
726− quit_all_impl(cx.editor, false)
706+ quit_all_impl(cx, false)
727}
707}
728
708
729fn force_cquit(
709fn force_cquit(
@@ -741,7 +721,7 @@ fn force_cquit(
741 .unwrap_or(1);
721 .unwrap_or(1);
742 cx.editor.exit_code = exit_code;
722 cx.editor.exit_code = exit_code;
743
723
744− quit_all_impl(cx.editor, true)
724+ quit_all_impl(cx, true)
745}
725}
746
726
747fn theme(
727fn theme(
@@ -2141,7 +2121,7 @@ pub static TYPABLE_COMMAND_MAP: Lazy<HashMap<&'static str, &'static TypableComma
2141 .collect()
2121 .collect()
2142 });
2122 });
2143
2123
2144−pub fn command_mode(cx: &mut Context) {
2124+pub(super) fn command_mode(cx: &mut Context) {
2145 let mut prompt = Prompt::new(
2125 let mut prompt = Prompt::new(
2146 ":".into(),
2126 ":".into(),
2147 Some(':'),
2127 Some(':'),
helix-term/src/ui/editor.rs+9−1
@@ -1426,7 +1426,15 @@ impl Component for EditorView {
1426
1426
1427 Event::Mouse(event) => self.handle_mouse_event(event, &mut cx),
1427 Event::Mouse(event) => self.handle_mouse_event(event, &mut cx),
1428 Event::IdleTimeout => self.handle_idle_timeout(&mut cx),
1428 Event::IdleTimeout => self.handle_idle_timeout(&mut cx),
1429+ Event::FocusGained => EventResult::Ignored(None),
1429− Event::FocusGained | Event::FocusLost => EventResult::Ignored(None),
1430+ Event::FocusLost => {
1431+ if context.editor.config().auto_save {
1432+ if let Err(e) = commands::typed::write_all_impl(context, false, false) {
1433+ context.editor.set_error(format!("{}", e));
1434+ }
1435+ }
1436+ EventResult::Consumed(None)
1437+ }
1430 }
1438 }
1431 }
1439 }
1432
1440
helix-view/src/editor.rs+3
@@ -140,6 +140,8 @@ pub struct Config {
140 pub auto_completion: bool,
140 pub auto_completion: bool,
141 /// Automatic formatting on save. Defaults to true.
141 /// Automatic formatting on save. Defaults to true.
142 pub auto_format: bool,
142 pub auto_format: bool,
143+ /// Automatic save on focus lost. Defaults to false.
144+ pub auto_save: bool,
143 /// Time in milliseconds since last keypress before idle timers trigger.
145 /// Time in milliseconds since last keypress before idle timers trigger.
144 /// Used for autocompletion, set to 0 for instant. Defaults to 400ms.
146 /// Used for autocompletion, set to 0 for instant. Defaults to 400ms.
145 #[serde(
147 #[serde(
@@ -592,6 +594,7 @@ impl Default for Config {
592 auto_pairs: AutoPairConfig::default(),
594 auto_pairs: AutoPairConfig::default(),
593 auto_completion: true,
595 auto_completion: true,
594 auto_format: true,
596 auto_format: true,
597+ auto_save: false,
595 idle_timeout: Duration::from_millis(400),
598 idle_timeout: Duration::from_millis(400),
596 completion_trigger_len: 2,
599 completion_trigger_len: 2,
597 auto_info: true,
600 auto_info: true,