on master help sign in
Merge pull request #2267 from dead10ck/fix-write-fail

Write path fixes

commit78c0cdc519a2c76842441103b1ed716bb7c0a4e1 authorBlaž Hrastnik <blaz@mxxn.io> dateThu 20 Oct 2022, 23:11 (4 years ago) parent8c9bb23756253b
23 files changed+995−298
docs/CONTRIBUTING.md+2−1 helix-core/src/auto_pairs.rs−1 helix-core/src/syntax.rs+6 helix-term/src/application.rs+156−52 helix-term/src/commands.rs+34−29 helix-term/src/commands/dap.rs+20−15 helix-term/src/commands/typed.rs+84−68 helix-term/src/compositor.rs+10 helix-term/src/job.rs+30−6 helix-term/src/main.rs+11−1 helix-term/src/ui/editor.rs+6−4 helix-term/src/ui/mod.rs+3−3 helix-term/tests/integration.rs+1 helix-term/tests/test/auto_indent.rs+1 helix-term/tests/test/auto_pairs.rs+1 helix-term/tests/test/commands.rs+19−17 helix-term/tests/test/helpers.rs+117−19 helix-term/tests/test/movement.rs+7−1 helix-term/tests/test/prompt.rs+1−3 helix-term/tests/test/splits.rs+129 helix-term/tests/test/write.rs+135−17 helix-view/src/document.rs+98−45 helix-view/src/editor.rs+124−16
docs/CONTRIBUTING.md+2−1
@@ -35,7 +35,8 @@ to `cargo install` anything either).
3535Integration tests for helix-term can be run with `cargo integration-test`. Code
3636contributors are strongly encouraged to write integration tests for their code.
3737Existing tests can be used as examples. Helpers can be found in
38−[helpers.rs][helpers.rs]
38+[helpers.rs][helpers.rs]. The log level can be set with the `HELIX_LOG_LEVEL`
39+environment variable, e.g. `HELIX_LOG_LEVEL=debug cargo integration-test`.
3940
4041## Minimum Stable Rust Version (MSRV) Policy
4142
helix-core/src/auto_pairs.rs−1
@@ -7,7 +7,6 @@ use std::collections::HashMap;
77use smallvec::SmallVec;
88
99// Heavily based on https://github.com/codemirror/closebrackets/
10−
1110pub const DEFAULT_PAIRS: &[(char, char)] = &[
1211 ('(', ')'),
1312 ('{', '}'),
helix-core/src/syntax.rs+6
@@ -61,6 +61,12 @@ pub struct Configuration {
6161 pub language: Vec<LanguageConfiguration>,
6262}
6363
64+impl Default for Configuration {
65+ fn default() -> Self {
66+ crate::config::default_syntax_loader()
67+ }
68+}
69+
6470// largely based on tree-sitter/cli/src/loader.rs
6571#[derive(Debug, Serialize, Deserialize)]
6672#[serde(rename_all = "kebab-case", deny_unknown_fields)]
helix-term/src/application.rs+156−52
@@ -1,12 +1,19 @@
11use arc_swap::{access::Map, ArcSwap};
22use futures_util::Stream;
33use helix_core::{
4− config::{default_syntax_loader, user_syntax_loader},
54 diagnostic::{DiagnosticTag, NumberOrString},
5+ path::get_relative_path,
66 pos_at_coords, syntax, Selection,
77};
88use helix_lsp::{lsp, util::lsp_pos_to_pos, LspProgressMap};
9−use helix_view::{align_view, editor::ConfigEvent, theme, tree::Layout, Align, Editor};
9+use helix_view::{
10+ align_view,
11+ document::DocumentSavedEventResult,
12+ editor::{ConfigEvent, EditorEvent},
13+ theme,
14+ tree::Layout,
15+ Align, Editor,
16+};
1017use serde_json::json;
1118
1219use crate::{
@@ -19,7 +26,7 @@ use crate::{
1926 ui::{self, overlay::overlayed},
2027};
2128
22−use log::{error, warn};
29+use log::{debug, error, warn};
2330use std::{
2431 io::{stdin, stdout, Write},
2532 sync::Arc,
@@ -102,7 +109,11 @@ fn restore_term() -> Result<(), Error> {
102109}
103110
104111impl Application {
105− pub fn new(args: Args, config: Config) -> Result<Self, Error> {
112+ pub fn new(
113+ args: Args,
114+ config: Config,
115+ syn_loader_conf: syntax::Configuration,
116+ ) -> Result<Self, Error> {
106117 #[cfg(feature = "integration")]
107118 setup_integration_logging();
108119
@@ -129,14 +140,6 @@ impl Application {
129140 })
130141 .unwrap_or_else(|| theme_loader.default_theme(true_color));
131142
132− let syn_loader_conf = user_syntax_loader().unwrap_or_else(|err| {
133− eprintln!("Bad language config: {}", err);
134− eprintln!("Press <ENTER> to continue with default language config");
135− use std::io::Read;
136− // This waits for an enter press.
137− let _ = std::io::stdin().read(&mut []);
138− default_syntax_loader()
139− });
140143 let syn_loader = std::sync::Arc::new(syntax::Loader::new(syn_loader_conf));
141144
142145 let mut compositor = Compositor::new().context("build compositor")?;
@@ -245,6 +248,10 @@ impl Application {
245248 Ok(app)
246249 }
247250
251+ #[cfg(feature = "integration")]
252+ fn render(&mut self) {}
253+
254+ #[cfg(not(feature = "integration"))]
248255 fn render(&mut self) {
249256 let compositor = &mut self.compositor;
250257
@@ -275,9 +282,6 @@ impl Application {
275282 where
276283 S: Stream<Item = crossterm::Result<crossterm::event::Event>> + Unpin,
277284 {
278− #[cfg(feature = "integration")]
279− let mut idle_handled = false;
280−
281285 loop {
282286 if self.editor.should_close() {
283287 return false;
@@ -294,26 +298,6 @@ impl Application {
294298 Some(signal) = self.signals.next() => {
295299 self.handle_signals(signal).await;
296300 }
297− Some((id, call)) = self.editor.language_servers.incoming.next() => {
298− self.handle_language_server_message(call, id).await;
299− // limit render calls for fast language server messages
300− let last = self.editor.language_servers.incoming.is_empty();
301−
302− if last || self.last_render.elapsed() > LSP_DEADLINE {
303− self.render();
304− self.last_render = Instant::now();
305− }
306− }
307− Some(payload) = self.editor.debugger_events.next() => {
308− let needs_render = self.editor.handle_debugger_message(payload).await;
309− if needs_render {
310− self.render();
311− }
312− }
313− Some(config_event) = self.editor.config_events.1.recv() => {
314− self.handle_config_events(config_event);
315− self.render();
316− }
317301 Some(callback) = self.jobs.futures.next() => {
318302 self.jobs.handle_callback(&mut self.editor, &mut self.compositor, callback);
319303 self.render();
@@ -322,26 +306,22 @@ impl Application {
322306 self.jobs.handle_callback(&mut self.editor, &mut self.compositor, callback);
323307 self.render();
324308 }
325− _ = &mut self.editor.idle_timer => {
326− // idle timeout
327− self.editor.clear_idle_timer();
328− self.handle_idle_timeout();
309+ event = self.editor.wait_event() => {
310+ let _idle_handled = self.handle_editor_event(event).await;
329311
330312 #[cfg(feature = "integration")]
331313 {
332− idle_handled = true;
314+ if _idle_handled {
315+ return true;
316+ }
333317 }
334318 }
335319 }
336320
337321 // for integration tests only, reset the idle timer after every
338− // event to make a signal when test events are done processing
322+ // event to signal when test events are done processing
339323 #[cfg(feature = "integration")]
340324 {
341− if idle_handled {
342− return true;
343− }
344−
345325 self.editor.reset_idle_timer();
346326 }
347327 }
@@ -446,6 +426,111 @@ impl Application {
446426 }
447427 }
448428
429+ pub fn handle_document_write(&mut self, doc_save_event: DocumentSavedEventResult) {
430+ let doc_save_event = match doc_save_event {
431+ Ok(event) => event,
432+ Err(err) => {
433+ self.editor.set_error(err.to_string());
434+ return;
435+ }
436+ };
437+
438+ let doc = match self.editor.document_mut(doc_save_event.doc_id) {
439+ None => {
440+ warn!(
441+ "received document saved event for non-existent doc id: {}",
442+ doc_save_event.doc_id
443+ );
444+
445+ return;
446+ }
447+ Some(doc) => doc,
448+ };
449+
450+ debug!(
451+ "document {:?} saved with revision {}",
452+ doc.path(),
453+ doc_save_event.revision
454+ );
455+
456+ doc.set_last_saved_revision(doc_save_event.revision);
457+
458+ let lines = doc_save_event.text.len_lines();
459+ let bytes = doc_save_event.text.len_bytes();
460+
461+ if doc.path() != Some(&doc_save_event.path) {
462+ if let Err(err) = doc.set_path(Some(&doc_save_event.path)) {
463+ log::error!(
464+ "error setting path for doc '{:?}': {}",
465+ doc.path(),
466+ err.to_string(),
467+ );
468+
469+ self.editor.set_error(err.to_string());
470+ return;
471+ }
472+
473+ let loader = self.editor.syn_loader.clone();
474+
475+ // borrowing the same doc again to get around the borrow checker
476+ let doc = doc_mut!(self.editor, &doc_save_event.doc_id);
477+ let id = doc.id();
478+ doc.detect_language(loader);
479+ let _ = self.editor.refresh_language_server(id);
480+ }
481+
482+ // TODO: fix being overwritten by lsp
483+ self.editor.set_status(format!(
484+ "'{}' written, {}L {}B",
485+ get_relative_path(&doc_save_event.path).to_string_lossy(),
486+ lines,
487+ bytes
488+ ));
489+ }
490+
491+ #[inline(always)]
492+ pub async fn handle_editor_event(&mut self, event: EditorEvent) -> bool {
493+ log::debug!("received editor event: {:?}", event);
494+
495+ match event {
496+ EditorEvent::DocumentSaved(event) => {
497+ self.handle_document_write(event);
498+ self.render();
499+ }
500+ EditorEvent::ConfigEvent(event) => {
501+ self.handle_config_events(event);
502+ self.render();
503+ }
504+ EditorEvent::LanguageServerMessage((id, call)) => {
505+ self.handle_language_server_message(call, id).await;
506+ // limit render calls for fast language server messages
507+ let last = self.editor.language_servers.incoming.is_empty();
508+
509+ if last || self.last_render.elapsed() > LSP_DEADLINE {
510+ self.render();
511+ self.last_render = Instant::now();
512+ }
513+ }
514+ EditorEvent::DebuggerEvent(payload) => {
515+ let needs_render = self.editor.handle_debugger_message(payload).await;
516+ if needs_render {
517+ self.render();
518+ }
519+ }
520+ EditorEvent::IdleTimer => {
521+ self.editor.clear_idle_timer();
522+ self.handle_idle_timeout();
523+
524+ #[cfg(feature = "integration")]
525+ {
526+ return true;
527+ }
528+ }
529+ }
530+
531+ false
532+ }
533+
449534 pub fn handle_terminal_events(&mut self, event: Result<CrosstermEvent, crossterm::ErrorKind>) {
450535 let mut cx = crate::compositor::Context {
451536 editor: &mut self.editor,
@@ -866,11 +951,10 @@ impl Application {
866951
867952 self.event_loop(input_stream).await;
868953
869− let err = self.close().await.err();
870−
954+ let close_errs = self.close().await;
871955 restore_term()?;
872956
873− if let Some(err) = err {
957+ for err in close_errs {
874958 self.editor.exit_code = 1;
875959 eprintln!("Error: {}", err);
876960 }
@@ -878,13 +962,33 @@ impl Application {
878962 Ok(self.editor.exit_code)
879963 }
880964
881− pub async fn close(&mut self) -> anyhow::Result<()> {
882− self.jobs.finish().await?;
965+ pub async fn close(&mut self) -> Vec<anyhow::Error> {
966+ // [NOTE] we intentionally do not return early for errors because we
967+ // want to try to run as much cleanup as we can, regardless of
968+ // errors along the way
969+ let mut errs = Vec::new();
970+
971+ if let Err(err) = self
972+ .jobs
973+ .finish(&mut self.editor, Some(&mut self.compositor))
974+ .await
975+ {
976+ log::error!("Error executing job: {}", err);
977+ errs.push(err);
978+ };
979+
980+ if let Err(err) = self.editor.flush_writes().await {
981+ log::error!("Error writing: {}", err);
982+ errs.push(err);
983+ }
883984
884985 if self.editor.close_language_servers(None).await.is_err() {
885986 log::error!("Timed out waiting for language servers to shutdown");
886− };
987+ errs.push(anyhow::format_err!(
988+ "Timed out waiting for language servers to shutdown"
989+ ));
990+ }
887991
888− Ok(())
992+ errs
889993 }
890994}
helix-term/src/commands.rs+34−29
@@ -47,12 +47,13 @@ use movement::Movement;
4747use crate::{
4848 args,
4949 compositor::{self, Component, Compositor},
50+ job::Callback,
5051 keymap::ReverseKeymap,
5152 ui::{self, overlay::overlayed, FilePicker, Picker, Popup, Prompt, PromptEvent},
5253};
5354
54−use crate::job::{self, Job, Jobs};
55−use futures_util::{FutureExt, StreamExt};
55+use crate::job::{self, Jobs};
56+use futures_util::StreamExt;
5657use std::{collections::HashMap, fmt, future::Future};
5758use std::{collections::HashSet, num::NonZeroUsize};
5859
@@ -107,10 +108,11 @@ impl<'a> Context<'a> {
107108 let callback = Box::pin(async move {
108109 let json = call.await?;
109110 let response = serde_json::from_value(json)?;
110− let call: job::Callback =
111− Box::new(move |editor: &mut Editor, compositor: &mut Compositor| {
111+ let call: job::Callback = Callback::EditorCompositor(Box::new(
112+ move |editor: &mut Editor, compositor: &mut Compositor| {
112113 callback(editor, compositor, response)
113− });
114+ },
115+ ));
114116 Ok(call)
115117 });
116118 self.jobs.callback(callback);
@@ -1925,8 +1927,8 @@ fn global_search(cx: &mut Context) {
19251927 let show_picker = async move {
19261928 let all_matches: Vec<FileResult> =
19271929 UnboundedReceiverStream::new(all_matches_rx).collect().await;
1928− let call: job::Callback =
1929− Box::new(move |editor: &mut Editor, compositor: &mut Compositor| {
1930+ let call: job::Callback = Callback::EditorCompositor(Box::new(
1931+ move |editor: &mut Editor, compositor: &mut Compositor| {
19301932 if all_matches.is_empty() {
19311933 editor.set_status("No matches found");
19321934 return;
@@ -1962,7 +1964,8 @@ fn global_search(cx: &mut Context) {
19621964 },
19631965 );
19641966 compositor.push(Box::new(overlayed(picker)));
1965− });
1967+ },
1968+ ));
19661969 Ok(call)
19671970 };
19681971 cx.jobs.callback(show_picker);
@@ -2504,13 +2507,6 @@ fn insert_at_line_end(cx: &mut Context) {
25042507 doc.set_selection(view.id, selection);
25052508}
25062509
2507−/// Sometimes when applying formatting changes we want to mark the buffer as unmodified, for
2508−/// example because we just applied the same changes while saving.
2509−enum Modified {
2510− SetUnmodified,
2511− LeaveModified,
2512−}
2513−
25142510// Creates an LspCallback that waits for formatting changes to be computed. When they're done,
25152511// it applies them, but only if the doc hasn't changed.
25162512//
@@ -2519,11 +2515,12 @@ enum Modified {
25192515async fn make_format_callback(
25202516 doc_id: DocumentId,
25212517 doc_version: i32,
2522− modified: Modified,
25232518 format: impl Future<Output = Result<Transaction, FormatterError>> + Send + 'static,
2519+ write: Option<(Option<PathBuf>, bool)>,
25242520) -> anyhow::Result<job::Callback> {
2525− let format = format.await?;
2526− let call: job::Callback = Box::new(move |editor, _compositor| {
2521+ let format = format.await;
2522+
2523+ let call: job::Callback = Callback::Editor(Box::new(move |editor| {
25272524 if !editor.documents.contains_key(&doc_id) {
25282525 return;
25292526 }
@@ -2531,22 +2528,30 @@ async fn make_format_callback(
25312528 let scrolloff = editor.config().scrolloff;
25322529 let doc = doc_mut!(editor, &doc_id);
25332530 let view = view_mut!(editor);
2534− if doc.version() == doc_version {
2535− apply_transaction(&format, doc, view);
2536− doc.append_changes_to_history(view.id);
2537− doc.detect_indent_and_line_ending();
2538− view.ensure_cursor_in_view(doc, scrolloff);
2539− if let Modified::SetUnmodified = modified {
2540− doc.reset_modified();
2531+
2532+ if let Ok(format) = format {
2533+ if doc.version() == doc_version {
2534+ apply_transaction(&format, doc, view);
2535+ doc.append_changes_to_history(view.id);
2536+ doc.detect_indent_and_line_ending();
2537+ view.ensure_cursor_in_view(doc, scrolloff);
2538+ } else {
2539+ log::info!("discarded formatting changes because the document changed");
25412540 }
2542− } else {
2543− log::info!("discarded formatting changes because the document changed");
25442541 }
2545− });
2542+
2543+ if let Some((path, force)) = write {
2544+ let id = doc.id();
2545+ if let Err(err) = editor.save(id, path, force) {
2546+ editor.set_error(format!("Error saving: {}", err));
2547+ }
2548+ }
2549+ }));
2550+
25462551 Ok(call)
25472552}
25482553
2549−#[derive(PartialEq)]
2554+#[derive(PartialEq, Eq)]
25502555pub enum Open {
25512556 Below,
25522557 Above,
helix-term/src/commands/dap.rs+20−15
@@ -118,11 +118,14 @@ fn dap_callback<T, F>(
118118 let callback = Box::pin(async move {
119119 let json = call.await?;
120120 let response = serde_json::from_value(json)?;
121− let call: Callback = Box::new(move |editor: &mut Editor, compositor: &mut Compositor| {
122− callback(editor, compositor, response)
123− });
121+ let call: Callback = Callback::EditorCompositor(Box::new(
122+ move |editor: &mut Editor, compositor: &mut Compositor| {
123+ callback(editor, compositor, response)
124+ },
125+ ));
124126 Ok(call)
125127 });
128+
126129 jobs.callback(callback);
127130}
128131
@@ -274,10 +277,11 @@ pub fn dap_launch(cx: &mut Context) {
274277 let completions = template.completion.clone();
275278 let name = template.name.clone();
276279 let callback = Box::pin(async move {
277− let call: Callback = Box::new(move |_editor, compositor| {
278− let prompt = debug_parameter_prompt(completions, name, Vec::new());
279− compositor.push(Box::new(prompt));
280− });
280+ let call: Callback =
281+ Callback::EditorCompositor(Box::new(move |_editor, compositor| {
282+ let prompt = debug_parameter_prompt(completions, name, Vec::new());
283+ compositor.push(Box::new(prompt));
284+ }));
281285 Ok(call)
282286 });
283287 cx.jobs.callback(callback);
@@ -332,10 +336,11 @@ fn debug_parameter_prompt(
332336 let config_name = config_name.clone();
333337 let params = params.clone();
334338 let callback = Box::pin(async move {
335− let call: Callback = Box::new(move |_editor, compositor| {
336− let prompt = debug_parameter_prompt(completions, config_name, params);
337− compositor.push(Box::new(prompt));
338− });
339+ let call: Callback =
340+ Callback::EditorCompositor(Box::new(move |_editor, compositor| {
341+ let prompt = debug_parameter_prompt(completions, config_name, params);
342+ compositor.push(Box::new(prompt));
343+ }));
339344 Ok(call)
340345 });
341346 cx.jobs.callback(callback);
@@ -582,7 +587,7 @@ pub fn dap_edit_condition(cx: &mut Context) {
582587 None => return,
583588 };
584589 let callback = Box::pin(async move {
585− let call: Callback = Box::new(move |editor, compositor| {
590+ let call: Callback = Callback::EditorCompositor(Box::new(move |editor, compositor| {
586591 let mut prompt = Prompt::new(
587592 "condition:".into(),
588593 None,
@@ -610,7 +615,7 @@ pub fn dap_edit_condition(cx: &mut Context) {
610615 prompt.insert_str(&condition, editor)
611616 }
612617 compositor.push(Box::new(prompt));
613− });
618+ }));
614619 Ok(call)
615620 });
616621 cx.jobs.callback(callback);
@@ -624,7 +629,7 @@ pub fn dap_edit_log(cx: &mut Context) {
624629 None => return,
625630 };
626631 let callback = Box::pin(async move {
627− let call: Callback = Box::new(move |editor, compositor| {
632+ let call: Callback = Callback::EditorCompositor(Box::new(move |editor, compositor| {
628633 let mut prompt = Prompt::new(
629634 "log-message:".into(),
630635 None,
@@ -651,7 +656,7 @@ pub fn dap_edit_log(cx: &mut Context) {
651656 prompt.insert_str(&log_message, editor);
652657 }
653658 compositor.push(Box::new(prompt));
654− });
659+ }));
655660 Ok(call)
656661 });
657662 cx.jobs.callback(callback);
helix-term/src/commands/typed.rs+84−68
@@ -1,5 +1,7 @@
11use std::ops::Deref;
22
3+use crate::job::Job;
4+
35use super::*;
46
57use helix_view::{
@@ -19,6 +21,8 @@ pub struct TypableCommand {
1921}
2022
2123fn quit(cx: &mut compositor::Context, args: &[Cow<str>], event: PromptEvent) -> anyhow::Result<()> {
24+ log::debug!("quitting...");
25+
2226 if event != PromptEvent::Validate {
2327 return Ok(());
2428 }
@@ -30,6 +34,7 @@ fn quit(cx: &mut compositor::Context, args: &[Cow<str>], event: PromptEvent) ->
3034 buffers_remaining_impl(cx.editor)?
3135 }
3236
37+ cx.block_try_flush_writes()?;
3338 cx.editor.close(view!(cx.editor).id);
3439
3540 Ok(())
@@ -70,14 +75,16 @@ fn open(cx: &mut compositor::Context, args: &[Cow<str>], event: PromptEvent) ->
7075}
7176
7277fn buffer_close_by_ids_impl(
73− editor: &mut Editor,
78+ cx: &mut compositor::Context,
7479 doc_ids: &[DocumentId],
7580 force: bool,
7681) -> anyhow::Result<()> {
82+ cx.block_try_flush_writes()?;
83+
7784 let (modified_ids, modified_names): (Vec<_>, Vec<_>) = doc_ids
7885 .iter()
7986 .filter_map(|&doc_id| {
80− if let Err(CloseError::BufferModified(name)) = editor.close_document(doc_id, force) {
87+ if let Err(CloseError::BufferModified(name)) = cx.editor.close_document(doc_id, force) {
8188 Some((doc_id, name))
8289 } else {
8390 None
@@ -86,11 +93,11 @@ fn buffer_close_by_ids_impl(
8693 .unzip();
8794
8895 if let Some(first) = modified_ids.first() {
89− let current = doc!(editor);
96+ let current = doc!(cx.editor);
9097 // If the current document is unmodified, and there are modified
9198 // documents, switch focus to the first modified doc.
9299 if !modified_ids.contains(&current.id()) {
93− editor.switch(*first, Action::Replace);
100+ cx.editor.switch(*first, Action::Replace);
94101 }
95102 bail!(
96103 "{} unsaved buffer(s) remaining: {:?}",
@@ -149,7 +156,7 @@ fn buffer_close(
149156 }
150157
151158 let document_ids = buffer_gather_paths_impl(cx.editor, args);
152− buffer_close_by_ids_impl(cx.editor, &document_ids, false)
159+ buffer_close_by_ids_impl(cx, &document_ids, false)
153160}
154161
155162fn force_buffer_close(
@@ -162,7 +169,7 @@ fn force_buffer_close(
162169 }
163170
164171 let document_ids = buffer_gather_paths_impl(cx.editor, args);
165− buffer_close_by_ids_impl(cx.editor, &document_ids, true)
172+ buffer_close_by_ids_impl(cx, &document_ids, true)
166173}
167174
168175fn buffer_gather_others_impl(editor: &mut Editor) -> Vec<DocumentId> {
@@ -184,7 +191,7 @@ fn buffer_close_others(
184191 }
185192
186193 let document_ids = buffer_gather_others_impl(cx.editor);
187− buffer_close_by_ids_impl(cx.editor, &document_ids, false)
194+ buffer_close_by_ids_impl(cx, &document_ids, false)
188195}
189196
190197fn force_buffer_close_others(
@@ -197,7 +204,7 @@ fn force_buffer_close_others(
197204 }
198205
199206 let document_ids = buffer_gather_others_impl(cx.editor);
200− buffer_close_by_ids_impl(cx.editor, &document_ids, true)
207+ buffer_close_by_ids_impl(cx, &document_ids, true)
201208}
202209
203210fn buffer_gather_all_impl(editor: &mut Editor) -> Vec<DocumentId> {
@@ -214,7 +221,7 @@ fn buffer_close_all(
214221 }
215222
216223 let document_ids = buffer_gather_all_impl(cx.editor);
217− buffer_close_by_ids_impl(cx.editor, &document_ids, false)
224+ buffer_close_by_ids_impl(cx, &document_ids, false)
218225}
219226
220227fn force_buffer_close_all(
@@ -227,7 +234,7 @@ fn force_buffer_close_all(
227234 }
228235
229236 let document_ids = buffer_gather_all_impl(cx.editor);
230− buffer_close_by_ids_impl(cx.editor, &document_ids, true)
237+ buffer_close_by_ids_impl(cx, &document_ids, true)
231238}
232239
233240fn buffer_next(
@@ -261,39 +268,29 @@ fn write_impl(
261268 path: Option<&Cow<str>>,
262269 force: bool,
263270) -> anyhow::Result<()> {
264− let auto_format = cx.editor.config().auto_format;
271+ let editor_auto_fmt = cx.editor.config().auto_format;
265272 let jobs = &mut cx.jobs;
266273 let doc = doc_mut!(cx.editor);
274+ let path = path.map(AsRef::as_ref);
267275
268− if let Some(ref path) = path {
269− doc.set_path(Some(path.as_ref().as_ref()))
270− .context("invalid filepath")?;
271− }
272− if doc.path().is_none() {
273− bail!("cannot write a buffer without a filename");
274− }
275− let fmt = if auto_format {
276+ let fmt = if editor_auto_fmt {
276277 doc.auto_format().map(|fmt| {
277− let shared = fmt.shared();
278278 let callback = make_format_callback(
279279 doc.id(),
280280 doc.version(),
281− Modified::SetUnmodified,
282− shared.clone(),
281+ fmt,
282+ Some((path.map(Into::into), force)),
283283 );
284− jobs.callback(callback);
285− shared
284+
285+ jobs.add(Job::with_callback(callback).wait_before_exiting());
286286 })
287287 } else {
288288 None
289289 };
290− let future = doc.format_and_save(fmt, force);
291− cx.jobs.add(Job::new(future).wait_before_exiting());
292290
293− if path.is_some() {
291+ if fmt.is_none() {
294292 let id = doc.id();
295− doc.detect_language(cx.editor.syn_loader.clone());
296− let _ = cx.editor.refresh_language_server(id);
293+ cx.editor.save(id, path, force)?;
297294 }
298295
299296 Ok(())
@@ -348,8 +345,7 @@ fn format(
348345
349346 let doc = doc!(cx.editor);
350347 if let Some(format) = doc.format() {
351− let callback =
352− make_format_callback(doc.id(), doc.version(), Modified::LeaveModified, format);
348+ let callback = make_format_callback(doc.id(), doc.version(), format, None);
353349 cx.jobs.callback(callback);
354350 }
355351
@@ -520,7 +516,7 @@ fn write_quit(
520516 }
521517
522518 write_impl(cx, args.first(), false)?;
523− helix_lsp::block_on(cx.jobs.finish())?;
519+ cx.block_try_flush_writes()?;
524520 quit(cx, &[], event)
525521}
526522
@@ -534,6 +530,7 @@ fn force_write_quit(
534530 }
535531
536532 write_impl(cx, args.first(), true)?;
533+ cx.block_try_flush_writes()?;
537534 force_quit(cx, &[], event)
538535}
539536
@@ -573,40 +570,50 @@ fn write_all_impl(
573570 return Ok(());
574571 }
575572
576− let mut errors = String::new();
573+ let mut errors: Vec<&'static str> = Vec::new();
577574 let auto_format = cx.editor.config().auto_format;
578575 let jobs = &mut cx.jobs;
576+
579577 // save all documents
580− for doc in &mut cx.editor.documents.values_mut() {
581− if doc.path().is_none() {
582− errors.push_str("cannot write a buffer without a filename\n");
583− continue;
584− }
578+ let saves: Vec<_> = cx
579+ .editor
580+ .documents
581+ .values()
582+ .filter_map(|doc| {
583+ if doc.path().is_none() {
584+ errors.push("cannot write a buffer without a filename\n");
585+ return None;
586+ }
585587
586− if !doc.is_modified() {
587− continue;
588− }
588+ if !doc.is_modified() {
589+ return None;
590+ }
589591
590− let fmt = if auto_format {
591− doc.auto_format().map(|fmt| {
592− let shared = fmt.shared();
593− let callback = make_format_callback(
594− doc.id(),
595− doc.version(),
596− Modified::SetUnmodified,
597− shared.clone(),
598− );
599− jobs.callback(callback);
600− shared
601− })
602− } else {
592+ let fmt = if auto_format {
593+ doc.auto_format().map(|fmt| {
594+ let callback =
595+ make_format_callback(doc.id(), doc.version(), fmt, Some((None, force)));
596+ jobs.add(Job::with_callback(callback).wait_before_exiting());
597+ })
598+ } else {
599+ None
600+ };
601+
602+ if fmt.is_none() {
603+ return Some(doc.id());
604+ }
603605 None
604− };
605− let future = doc.format_and_save(fmt, force);
606− jobs.add(Job::new(future).wait_before_exiting());
606+ })
607+ .collect();
608+
609+ // manually call save for the rest of docs that don't have a formatter
610+ for id in saves {
611+ cx.editor.save::<PathBuf>(id, None, force)?;
607612 }
608613
609614 if quit {
615+ cx.block_try_flush_writes()?;
616+
610617 if !force {
611618 buffers_remaining_impl(cx.editor)?;
612619 }
@@ -618,7 +625,11 @@ fn write_all_impl(
618625 }
619626 }
620627
621− bail!(errors)
628+ if !errors.is_empty() && !force {
629+ bail!("{:?}", errors);
630+ }
631+
632+ Ok(())
622633}
623634
624635fn write_all(
@@ -680,6 +691,7 @@ fn quit_all(
680691 return Ok(());
681692 }
682693
694+ cx.block_try_flush_writes()?;
683695 quit_all_impl(cx.editor, false)
684696}
685697
@@ -708,8 +720,9 @@ fn cquit(
708720 .first()
709721 .and_then(|code| code.parse::<i32>().ok())
710722 .unwrap_or(1);
711− cx.editor.exit_code = exit_code;
712723
724+ cx.editor.exit_code = exit_code;
725+ cx.block_try_flush_writes()?;
713726 quit_all_impl(cx.editor, false)
714727}
715728
@@ -1064,12 +1077,13 @@ fn tree_sitter_scopes(
10641077 let contents = format!("```json\n{:?}\n````", scopes);
10651078
10661079 let callback = async move {
1067− let call: job::Callback =
1068− Box::new(move |editor: &mut Editor, compositor: &mut Compositor| {
1080+ let call: job::Callback = Callback::EditorCompositor(Box::new(
1081+ move |editor: &mut Editor, compositor: &mut Compositor| {
10691082 let contents = ui::Markdown::new(contents, editor.syn_loader.clone());
10701083 let popup = Popup::new("hover", contents).auto_close(true);
10711084 compositor.replace_or_push("hover", popup);
1072− });
1085+ },
1086+ ));
10731087 Ok(call)
10741088 };
10751089
@@ -1492,12 +1506,13 @@ fn tree_sitter_subtree(
14921506 contents.push_str("\n```");
14931507
14941508 let callback = async move {
1495− let call: job::Callback =
1496− Box::new(move |editor: &mut Editor, compositor: &mut Compositor| {
1509+ let call: job::Callback = Callback::EditorCompositor(Box::new(
1510+ move |editor: &mut Editor, compositor: &mut Compositor| {
14971511 let contents = ui::Markdown::new(contents, editor.syn_loader.clone());
14981512 let popup = Popup::new("hover", contents).auto_close(true);
14991513 compositor.replace_or_push("hover", popup);
1500− });
1514+ },
1515+ ));
15011516 Ok(call)
15021517 };
15031518
@@ -1605,8 +1620,8 @@ fn run_shell_command(
16051620
16061621 if !output.is_empty() {
16071622 let callback = async move {
1608− let call: job::Callback =
1609− Box::new(move |editor: &mut Editor, compositor: &mut Compositor| {
1623+ let call: job::Callback = Callback::EditorCompositor(Box::new(
1624+ move |editor: &mut Editor, compositor: &mut Compositor| {
16101625 let contents = ui::Markdown::new(
16111626 format!("```sh\n{}\n```", output),
16121627 editor.syn_loader.clone(),
@@ -1615,7 +1630,8 @@ fn run_shell_command(
16151630 helix_core::Position::new(editor.cursor().0.unwrap_or_default().row, 2),
16161631 ));
16171632 compositor.replace_or_push("shell", popup);
1618− });
1633+ },
1634+ ));
16191635 Ok(call)
16201636 };
16211637
helix-term/src/compositor.rs+10
@@ -27,6 +27,16 @@ pub struct Context<'a> {
2727 pub jobs: &'a mut Jobs,
2828}
2929
30+impl<'a> Context<'a> {
31+ /// Waits on all pending jobs, and then tries to flush all pending write
32+ /// operations for all documents.
33+ pub fn block_try_flush_writes(&mut self) -> anyhow::Result<()> {
34+ tokio::task::block_in_place(|| helix_lsp::block_on(self.jobs.finish(self.editor, None)))?;
35+ tokio::task::block_in_place(|| helix_lsp::block_on(self.editor.flush_writes()))?;
36+ Ok(())
37+ }
38+}
39+
3040pub trait Component: Any + AnyComponent {
3141 /// Process input events, return true if handled.
3242 fn handle_event(&mut self, _event: &Event, _ctx: &mut Context) -> EventResult {
helix-term/src/job.rs+30−6
@@ -5,7 +5,11 @@ use crate::compositor::Compositor;
55use futures_util::future::{BoxFuture, Future, FutureExt};
66use futures_util::stream::{FuturesUnordered, StreamExt};
77
8−pub type Callback = Box<dyn FnOnce(&mut Editor, &mut Compositor) + Send>;
8+pub enum Callback {
9+ EditorCompositor(Box<dyn FnOnce(&mut Editor, &mut Compositor) + Send>),
10+ Editor(Box<dyn FnOnce(&mut Editor) + Send>),
11+}
12+
913pub type JobFuture = BoxFuture<'static, anyhow::Result<Option<Callback>>>;
1014
1115pub struct Job {
@@ -68,9 +72,10 @@ impl Jobs {
6872 ) {
6973 match call {
7074 Ok(None) => {}
71− Ok(Some(call)) => {
72− call(editor, compositor);
73− }
75+ Ok(Some(call)) => match call {
76+ Callback::EditorCompositor(call) => call(editor, compositor),
77+ Callback::Editor(call) => call(editor),
78+ },
7479 Err(e) => {
7580 editor.set_error(format!("Async job failed: {}", e));
7681 }
@@ -93,13 +98,32 @@ impl Jobs {
9398 }
9499
95100 /// Blocks until all the jobs that need to be waited on are done.
96− pub async fn finish(&mut self) -> anyhow::Result<()> {
101+ pub async fn finish(
102+ &mut self,
103+ editor: &mut Editor,
104+ mut compositor: Option<&mut Compositor>,
105+ ) -> anyhow::Result<()> {
97106 log::debug!("waiting on jobs...");
98107 let mut wait_futures = std::mem::take(&mut self.wait_futures);
108+
99109 while let (Some(job), tail) = wait_futures.into_future().await {
100110 match job {
101− Ok(_) => {
111+ Ok(callback) => {
102112 wait_futures = tail;
113+
114+ if let Some(callback) = callback {
115+ // clippy doesn't realize this is an error without the derefs
116+ #[allow(clippy::needless_option_as_deref)]
117+ match callback {
118+ Callback::EditorCompositor(call) if compositor.is_some() => {
119+ call(editor, compositor.as_deref_mut().unwrap())
120+ }
121+ Callback::Editor(call) => call(editor),
122+
123+ // skip callbacks for which we don't have the necessary references
124+ _ => (),
125+ }
126+ }
103127 }
104128 Err(e) => {
105129 self.wait_futures = tail;
helix-term/src/main.rs+11−1
@@ -139,8 +139,18 @@ FLAGS:
139139 Err(err) => return Err(Error::new(err)),
140140 };
141141
142+ let syn_loader_conf = helix_core::config::user_syntax_loader().unwrap_or_else(|err| {
143+ eprintln!("Bad language config: {}", err);
144+ eprintln!("Press <ENTER> to continue with default language config");
145+ use std::io::Read;
146+ // This waits for an enter press.
147+ let _ = std::io::stdin().read(&mut []);
148+ helix_core::config::default_syntax_loader()
149+ });
150+
142151 // TODO: use the thread local executor to spawn the application task separately from the work pool
143− let mut app = Application::new(args, config).context("unable to create new application")?;
152+ let mut app = Application::new(args, config, syn_loader_conf)
153+ .context("unable to create new application")?;
144154
145155 let exit_code = app.run(&mut EventStream::new()).await?;
146156
helix-term/src/ui/editor.rs+6−4
@@ -1,7 +1,8 @@
11use crate::{
22 commands,
33 compositor::{Component, Context, Event, EventResult},
4− job, key,
4+ job::{self, Callback},
5+ key,
56 keymap::{KeymapResult, Keymaps},
67 ui::{Completion, ProgressSpinners},
78};
@@ -944,9 +945,10 @@ impl EditorView {
944945
945946 // TODO: Use an on_mode_change hook to remove signature help
946947 cxt.jobs.callback(async {
947− let call: job::Callback = Box::new(|_editor, compositor| {
948− compositor.remove(SignatureHelp::ID);
949− });
948+ let call: job::Callback =
949+ Callback::EditorCompositor(Box::new(|_editor, compositor| {
950+ compositor.remove(SignatureHelp::ID);
951+ }));
950952 Ok(call)
951953 });
952954 }
helix-term/src/ui/mod.rs+3−3
@@ -14,7 +14,7 @@ mod statusline;
1414mod text;
1515
1616use crate::compositor::{Component, Compositor};
17−use crate::job;
17+use crate::job::{self, Callback};
1818pub use completion::Completion;
1919pub use editor::EditorView;
2020pub use markdown::Markdown;
@@ -121,7 +121,7 @@ pub fn regex_prompt(
121121
122122 if event == PromptEvent::Validate {
123123 let callback = async move {
124− let call: job::Callback = Box::new(
124+ let call: job::Callback = Callback::EditorCompositor(Box::new(
125125 move |_editor: &mut Editor, compositor: &mut Compositor| {
126126 let contents = Text::new(format!("{}", err));
127127 let size = compositor.size();
@@ -135,7 +135,7 @@ pub fn regex_prompt(
135135
136136 compositor.replace_or_push("invalid-regex", popup);
137137 },
138− );
138+ ));
139139 Ok(call)
140140 };
141141
helix-term/tests/integration.rs+1
@@ -22,5 +22,6 @@ mod test {
2222 mod commands;
2323 mod movement;
2424 mod prompt;
25+ mod splits;
2526 mod write;
2627}
helix-term/tests/test/auto_indent.rs+1
@@ -8,6 +8,7 @@ async fn auto_indent_c() -> anyhow::Result<()> {
88 ..Default::default()
99 },
1010 Config::default(),
11+ helpers::test_syntax_conf(None),
1112 // switches to append mode?
1213 (
1314 helpers::platform_line("void foo() {#[|}]#").as_ref(),
helix-term/tests/test/auto_pairs.rs+1
@@ -13,6 +13,7 @@ async fn auto_pairs_basic() -> anyhow::Result<()> {
1313 },
1414 ..Default::default()
1515 },
16+ helpers::test_syntax_conf(None),
1617 ("#[\n|]#", "i(<esc>", "(#[|\n]#"),
1718 )
1819 .await?;
helix-term/tests/test/commands.rs+19−17
@@ -1,21 +1,25 @@
1−use std::{
2− io::{Read, Write},
3− ops::RangeInclusive,
4−};
1+use std::ops::RangeInclusive;
52
63use helix_core::diagnostic::Severity;
7−use helix_term::application::Application;
84
95use super::*;
106
11−#[tokio::test]
7+#[tokio::test(flavor = "multi_thread")]
128async fn test_write_quit_fail() -> anyhow::Result<()> {
139 let file = helpers::new_readonly_tempfile()?;
10+ let mut app = helpers::AppBuilder::new()
11+ .with_file(file.path(), None)
12+ .build()?;
1413
1514 test_key_sequence(
16− &mut helpers::app_with_file(file.path())?,
15+ &mut app,
1716 Some("ihello<esc>:wq<ret>"),
1817 Some(&|app| {
18+ let mut docs: Vec<_> = app.editor.documents().collect();
19+ assert_eq!(1, docs.len());
20+
21+ let doc = docs.pop().unwrap();
22+ assert_eq!(Some(file.path()), doc.path().map(PathBuf::as_path));
1923 assert_eq!(&Severity::Error, app.editor.get_status().unwrap().1);
2024 }),
2125 false,
@@ -25,11 +29,10 @@ async fn test_write_quit_fail() -> anyhow::Result<()> {
2529 Ok(())
2630}
2731
28−#[tokio::test]
29−#[ignore]
32+#[tokio::test(flavor = "multi_thread")]
3033async fn test_buffer_close_concurrent() -> anyhow::Result<()> {
3134 test_key_sequences(
32− &mut Application::new(Args::default(), Config::default())?,
35+ &mut helpers::AppBuilder::new().build()?,
3336 vec![
3437 (
3538 None,
@@ -69,8 +72,12 @@ async fn test_buffer_close_concurrent() -> anyhow::Result<()> {
6972
7073 command.push_str(":buffer<minus>close<ret>");
7174
75+ let mut app = helpers::AppBuilder::new()
76+ .with_file(file.path(), None)
77+ .build()?;
78+
7279 test_key_sequence(
73− &mut helpers::app_with_file(file.path())?,
80+ &mut app,
7481 Some(&command),
7582 Some(&|app| {
7683 assert!(!app.editor.is_err(), "error: {:?}", app.editor.get_status());
@@ -82,12 +89,7 @@ async fn test_buffer_close_concurrent() -> anyhow::Result<()> {
8289 )
8390 .await?;
8491
85− file.as_file_mut().flush()?;
86− file.as_file_mut().sync_all()?;
87−
88− let mut file_content = String::new();
89− file.as_file_mut().read_to_string(&mut file_content)?;
90− assert_eq!(RANGE.end().to_string(), file_content);
92+ helpers::assert_file_has_content(file.as_file_mut(), &RANGE.end().to_string())?;
9193
9294 Ok(())
9395}
helix-term/tests/test/helpers.rs+117−19
@@ -1,10 +1,15 @@
1−use std::{io::Write, path::PathBuf, time::Duration};
1+use std::{
2+ fs::File,
3+ io::{Read, Write},
4+ path::PathBuf,
5+ time::Duration,
6+};
27
38use anyhow::bail;
49use crossterm::event::{Event, KeyEvent};
5−use helix_core::{test, Selection, Transaction};
10+use helix_core::{diagnostic::Severity, test, Selection, Transaction};
611use helix_term::{application::Application, args::Args, config::Config};
7−use helix_view::{doc, input::parse_macro};
12+use helix_view::{doc, input::parse_macro, Editor};
813use tempfile::NamedTempFile;
914use tokio_stream::wrappers::UnboundedReceiverStream;
1015
@@ -56,7 +61,9 @@ pub async fn test_key_sequences(
5661 for (i, (in_keys, test_fn)) in inputs.into_iter().enumerate() {
5762 if let Some(in_keys) = in_keys {
5863 for key_event in parse_macro(in_keys)?.into_iter() {
59− tx.send(Ok(Event::Key(KeyEvent::from(key_event))))?;
64+ let key = Event::Key(KeyEvent::from(key_event));
65+ log::trace!("sending key: {:?}", key);
66+ tx.send(Ok(key))?;
6067 }
6168 }
6269
@@ -70,7 +77,7 @@ pub async fn test_key_sequences(
7077 // verify if it exited on the last iteration if it should have and
7178 // the inverse
7279 if i == num_inputs - 1 && app_exited != should_exit {
73− bail!("expected app to exit: {} != {}", app_exited, should_exit);
80+ bail!("expected app to exit: {} != {}", should_exit, app_exited);
7481 }
7582
7683 if let Some(test) = test_fn {
@@ -87,7 +94,17 @@ pub async fn test_key_sequences(
8794 tokio::time::timeout(TIMEOUT, event_loop).await?;
8895 }
8996
90− app.close().await?;
97+ let errs = app.close().await;
98+
99+ if !errs.is_empty() {
100+ log::error!("Errors closing app");
101+
102+ for err in errs {
103+ log::error!("{}", err);
104+ }
105+
106+ bail!("Error closing app");
107+ }
91108
92109 Ok(())
93110}
@@ -101,7 +118,7 @@ pub async fn test_key_sequence_with_input_text<T: Into<TestCase>>(
101118 let test_case = test_case.into();
102119 let mut app = match app {
103120 Some(app) => app,
104− None => Application::new(Args::default(), Config::default())?,
121+ None => Application::new(Args::default(), Config::default(), test_syntax_conf(None))?,
105122 };
106123
107124 let (view, doc) = helix_view::current!(app.editor);
@@ -125,16 +142,30 @@ pub async fn test_key_sequence_with_input_text<T: Into<TestCase>>(
125142 .await
126143}
127144
145+/// Generates language configs that merge in overrides, like a user language
146+/// config. The argument string must be a raw TOML document.
147+pub fn test_syntax_conf(overrides: Option<String>) -> helix_core::syntax::Configuration {
148+ let mut lang = helix_loader::config::default_lang_config();
149+
150+ if let Some(overrides) = overrides {
151+ let override_toml = toml::from_str(&overrides).unwrap();
152+ lang = helix_loader::merge_toml_values(lang, override_toml, 3);
153+ }
154+
155+ lang.try_into().unwrap()
156+}
157+
128158/// Use this for very simple test cases where there is one input
129159/// document, selection, and sequence of key presses, and you just
130160/// want to verify the resulting document and selection.
131161pub async fn test_with_config<T: Into<TestCase>>(
132162 args: Args,
133163 config: Config,
164+ syn_conf: helix_core::syntax::Configuration,
134165 test_case: T,
135166) -> anyhow::Result<()> {
136167 let test_case = test_case.into();
137− let app = Application::new(args, config)?;
168+ let app = Application::new(args, config, syn_conf)?;
138169
139170 test_key_sequence_with_input_text(
140171 Some(app),
@@ -155,7 +186,13 @@ pub async fn test_with_config<T: Into<TestCase>>(
155186}
156187
157188pub async fn test<T: Into<TestCase>>(test_case: T) -> anyhow::Result<()> {
158− test_with_config(Args::default(), Config::default(), test_case).await
189+ test_with_config(
190+ Args::default(),
191+ Config::default(),
192+ test_syntax_conf(None),
193+ test_case,
194+ )
195+ .await
159196}
160197
161198pub fn temp_file_with_contents<S: AsRef<str>>(
@@ -200,14 +237,75 @@ pub fn new_readonly_tempfile() -> anyhow::Result<NamedTempFile> {
200237 Ok(file)
201238}
202239
203−/// Creates a new Application with default config that opens the given file
204−/// path
205−pub fn app_with_file<P: Into<PathBuf>>(path: P) -> anyhow::Result<Application> {
206− Application::new(
207− Args {
208− files: vec![(path.into(), helix_core::Position::default())],
209− ..Default::default()
210− },
211− Config::default(),
212− )
240+#[derive(Default)]
241+pub struct AppBuilder {
242+ args: Args,
243+ config: Config,
244+ syn_conf: helix_core::syntax::Configuration,
245+ input: Option<(String, Selection)>,
246+}
247+
248+impl AppBuilder {
249+ pub fn new() -> Self {
250+ AppBuilder::default()
251+ }
252+
253+ pub fn with_file<P: Into<PathBuf>>(
254+ mut self,
255+ path: P,
256+ pos: Option<helix_core::Position>,
257+ ) -> Self {
258+ self.args.files.push((path.into(), pos.unwrap_or_default()));
259+ self
260+ }
261+
262+ pub fn with_config(mut self, config: Config) -> Self {
263+ self.config = config;
264+ self
265+ }
266+
267+ pub fn with_input_text<S: Into<String>>(mut self, input_text: S) -> Self {
268+ self.input = Some(test::print(&input_text.into()));
269+ self
270+ }
271+
272+ pub fn with_lang_config(mut self, syn_conf: helix_core::syntax::Configuration) -> Self {
273+ self.syn_conf = syn_conf;
274+ self
275+ }
276+
277+ pub fn build(self) -> anyhow::Result<Application> {
278+ let mut app = Application::new(self.args, self.config, self.syn_conf)?;
279+
280+ if let Some((text, selection)) = self.input {
281+ let (view, doc) = helix_view::current!(app.editor);
282+ let sel = doc.selection(view.id).clone();
283+ let trans = Transaction::change_by_selection(doc.text(), &sel, |_| {
284+ (0, doc.text().len_chars(), Some((text.clone()).into()))
285+ })
286+ .with_selection(selection);
287+
288+ // replace the initial text with the input text
289+ doc.apply(&trans, view.id);
290+ }
291+
292+ Ok(app)
293+ }
294+}
295+
296+pub fn assert_file_has_content(file: &mut File, content: &str) -> anyhow::Result<()> {
297+ file.flush()?;
298+ file.sync_all()?;
299+
300+ let mut file_content = String::new();
301+ file.read_to_string(&mut file_content)?;
302+ assert_eq!(content, file_content);
303+
304+ Ok(())
305+}
306+
307+pub fn assert_status_not_error(editor: &Editor) {
308+ if let Some((_, sev)) = editor.get_status() {
309+ assert_ne!(&Severity::Error, sev);
310+ }
213311}
helix-term/tests/test/movement.rs+7−1
@@ -70,7 +70,9 @@ async fn insert_to_normal_mode_cursor_position() -> anyhow::Result<()> {
7070async fn cursor_position_newly_opened_file() -> anyhow::Result<()> {
7171 let test = |content: &str, expected_sel: Selection| -> anyhow::Result<()> {
7272 let file = helpers::temp_file_with_contents(content)?;
73− let mut app = helpers::app_with_file(file.path())?;
73+ let mut app = helpers::AppBuilder::new()
74+ .with_file(file.path(), None)
75+ .build()?;
7476
7577 let (view, doc) = helix_view::current!(app.editor);
7678 let sel = doc.selection(view.id).clone();
@@ -115,6 +117,7 @@ async fn select_mode_tree_sitter_next_function_is_union_of_objects() -> anyhow::
115117 ..Default::default()
116118 },
117119 Config::default(),
120+ helpers::test_syntax_conf(None),
118121 (
119122 helpers::platform_line(indoc! {"\
120123 #[/|]#// Increments
@@ -146,6 +149,7 @@ async fn select_mode_tree_sitter_prev_function_unselects_object() -> anyhow::Res
146149 ..Default::default()
147150 },
148151 Config::default(),
152+ helpers::test_syntax_conf(None),
149153 (
150154 helpers::platform_line(indoc! {"\
151155 /// Increments
@@ -178,6 +182,7 @@ async fn select_mode_tree_sitter_prev_function_goes_backwards_to_object() -> any
178182 ..Default::default()
179183 },
180184 Config::default(),
185+ helpers::test_syntax_conf(None),
181186 (
182187 helpers::platform_line(indoc! {"\
183188 /// Increments
@@ -208,6 +213,7 @@ async fn select_mode_tree_sitter_prev_function_goes_backwards_to_object() -> any
208213 ..Default::default()
209214 },
210215 Config::default(),
216+ helpers::test_syntax_conf(None),
211217 (
212218 helpers::platform_line(indoc! {"\
213219 /// Increments
helix-term/tests/test/prompt.rs+1−3
@@ -1,11 +1,9 @@
11use super::*;
22
3−use helix_term::application::Application;
4−
53#[tokio::test]
64async fn test_history_completion() -> anyhow::Result<()> {
75 test_key_sequence(
8− &mut Application::new(Args::default(), Config::default())?,
6+ &mut AppBuilder::new().build()?,
97 Some(":asdf<ret>:theme d<C-n><tab>"),
108 Some(&|app| {
119 assert!(!app.editor.is_err());
helix-term/tests/test/splits.rsnew file+129
@@ -0,0 +1,129 @@
1+use super::*;
2+
3+#[tokio::test(flavor = "multi_thread")]
4+async fn test_split_write_quit_all() -> anyhow::Result<()> {
5+ let mut file1 = tempfile::NamedTempFile::new()?;
6+ let mut file2 = tempfile::NamedTempFile::new()?;
7+ let mut file3 = tempfile::NamedTempFile::new()?;
8+
9+ let mut app = helpers::AppBuilder::new()
10+ .with_file(file1.path(), None)
11+ .build()?;
12+
13+ test_key_sequences(
14+ &mut app,
15+ vec![
16+ (
17+ Some(&format!(
18+ "ihello1<esc>:sp<ret>:o {}<ret>ihello2<esc>:sp<ret>:o {}<ret>ihello3<esc>",
19+ file2.path().to_string_lossy(),
20+ file3.path().to_string_lossy()
21+ )),
22+ Some(&|app| {
23+ let docs: Vec<_> = app.editor.documents().collect();
24+ assert_eq!(3, docs.len());
25+
26+ let doc1 = docs
27+ .iter()
28+ .find(|doc| doc.path().unwrap() == file1.path())
29+ .unwrap();
30+
31+ assert_eq!("hello1", doc1.text().to_string());
32+
33+ let doc2 = docs
34+ .iter()
35+ .find(|doc| doc.path().unwrap() == file2.path())
36+ .unwrap();
37+
38+ assert_eq!("hello2", doc2.text().to_string());
39+
40+ let doc3 = docs
41+ .iter()
42+ .find(|doc| doc.path().unwrap() == file3.path())
43+ .unwrap();
44+
45+ assert_eq!("hello3", doc3.text().to_string());
46+
47+ helpers::assert_status_not_error(&app.editor);
48+ assert_eq!(3, app.editor.tree.views().count());
49+ }),
50+ ),
51+ (
52+ Some(":wqa<ret>"),
53+ Some(&|app| {
54+ helpers::assert_status_not_error(&app.editor);
55+ assert_eq!(0, app.editor.tree.views().count());
56+ }),
57+ ),
58+ ],
59+ true,
60+ )
61+ .await?;
62+
63+ helpers::assert_file_has_content(file1.as_file_mut(), "hello1")?;
64+ helpers::assert_file_has_content(file2.as_file_mut(), "hello2")?;
65+ helpers::assert_file_has_content(file3.as_file_mut(), "hello3")?;
66+
67+ Ok(())
68+}
69+
70+#[tokio::test(flavor = "multi_thread")]
71+async fn test_split_write_quit_same_file() -> anyhow::Result<()> {
72+ let mut file = tempfile::NamedTempFile::new()?;
73+ let mut app = helpers::AppBuilder::new()
74+ .with_file(file.path(), None)
75+ .build()?;
76+
77+ test_key_sequences(
78+ &mut app,
79+ vec![
80+ (
81+ Some("O<esc>ihello<esc>:sp<ret>ogoodbye<esc>"),
82+ Some(&|app| {
83+ assert_eq!(2, app.editor.tree.views().count());
84+ helpers::assert_status_not_error(&app.editor);
85+
86+ let mut docs: Vec<_> = app.editor.documents().collect();
87+ assert_eq!(1, docs.len());
88+
89+ let doc = docs.pop().unwrap();
90+
91+ assert_eq!(
92+ helpers::platform_line("hello\ngoodbye"),
93+ doc.text().to_string()
94+ );
95+
96+ assert!(doc.is_modified());
97+ }),
98+ ),
99+ (
100+ Some(":wq<ret>"),
101+ Some(&|app| {
102+ helpers::assert_status_not_error(&app.editor);
103+ assert_eq!(1, app.editor.tree.views().count());
104+
105+ let mut docs: Vec<_> = app.editor.documents().collect();
106+ assert_eq!(1, docs.len());
107+
108+ let doc = docs.pop().unwrap();
109+
110+ assert_eq!(
111+ helpers::platform_line("hello\ngoodbye"),
112+ doc.text().to_string()
113+ );
114+
115+ assert!(!doc.is_modified());
116+ }),
117+ ),
118+ ],
119+ false,
120+ )
121+ .await?;
122+
123+ helpers::assert_file_has_content(
124+ file.as_file_mut(),
125+ &helpers::platform_line("hello\ngoodbye"),
126+ )?;
127+
128+ Ok(())
129+}
helix-term/tests/test/write.rs+135−17
@@ -4,7 +4,6 @@ use std::{
44};
55
66use helix_core::diagnostic::Severity;
7−use helix_term::application::Application;
87use helix_view::doc;
98
109use super::*;
@@ -12,9 +11,12 @@ use super::*;
1211#[tokio::test]
1312async fn test_write() -> anyhow::Result<()> {
1413 let mut file = tempfile::NamedTempFile::new()?;
14+ let mut app = helpers::AppBuilder::new()
15+ .with_file(file.path(), None)
16+ .build()?;
1517
1618 test_key_sequence(
17− &mut helpers::app_with_file(file.path())?,
19+ &mut app,
1820 Some("ithe gostak distims the doshes<ret><esc>:w<ret>"),
1921 None,
2022 false,
@@ -35,12 +37,15 @@ async fn test_write() -> anyhow::Result<()> {
3537 Ok(())
3638}
3739
38−#[tokio::test]
40+#[tokio::test(flavor = "multi_thread")]
3941async fn test_write_quit() -> anyhow::Result<()> {
4042 let mut file = tempfile::NamedTempFile::new()?;
43+ let mut app = helpers::AppBuilder::new()
44+ .with_file(file.path(), None)
45+ .build()?;
4146
4247 test_key_sequence(
43− &mut helpers::app_with_file(file.path())?,
48+ &mut app,
4449 Some("ithe gostak distims the doshes<ret><esc>:wq<ret>"),
4550 None,
4651 true,
@@ -61,25 +66,21 @@ async fn test_write_quit() -> anyhow::Result<()> {
6166 Ok(())
6267}
6368
64−#[tokio::test]
65−#[ignore]
69+#[tokio::test(flavor = "multi_thread")]
6670async fn test_write_concurrent() -> anyhow::Result<()> {
6771 let mut file = tempfile::NamedTempFile::new()?;
6872 let mut command = String::new();
6973 const RANGE: RangeInclusive<i32> = 1..=5000;
74+ let mut app = helpers::AppBuilder::new()
75+ .with_file(file.path(), None)
76+ .build()?;
7077
7178 for i in RANGE {
7279 let cmd = format!("%c{}<esc>:w<ret>", i);
7380 command.push_str(&cmd);
7481 }
7582
76− test_key_sequence(
77− &mut helpers::app_with_file(file.path())?,
78− Some(&command),
79− None,
80− false,
81− )
82− .await?;
83+ test_key_sequence(&mut app, Some(&command), None, false).await?;
8384
8485 file.as_file_mut().flush()?;
8586 file.as_file_mut().sync_all()?;
@@ -92,12 +93,14 @@ async fn test_write_concurrent() -> anyhow::Result<()> {
9293}
9394
9495#[tokio::test]
95−#[ignore]
9696async fn test_write_fail_mod_flag() -> anyhow::Result<()> {
9797 let file = helpers::new_readonly_tempfile()?;
98+ let mut app = helpers::AppBuilder::new()
99+ .with_file(file.path(), None)
100+ .build()?;
98101
99102 test_key_sequences(
100− &mut helpers::app_with_file(file.path())?,
103+ &mut app,
101104 vec![
102105 (
103106 None,
@@ -131,12 +134,127 @@ async fn test_write_fail_mod_flag() -> anyhow::Result<()> {
131134}
132135
133136#[tokio::test]
134−#[ignore]
137+async fn test_write_scratch_to_new_path() -> anyhow::Result<()> {
138+ let mut file = tempfile::NamedTempFile::new()?;
139+
140+ test_key_sequence(
141+ &mut AppBuilder::new().build()?,
142+ Some(format!("ihello<esc>:w {}<ret>", file.path().to_string_lossy()).as_ref()),
143+ Some(&|app| {
144+ assert!(!app.editor.is_err());
145+
146+ let mut docs: Vec<_> = app.editor.documents().collect();
147+ assert_eq!(1, docs.len());
148+
149+ let doc = docs.pop().unwrap();
150+ assert_eq!(Some(&file.path().to_path_buf()), doc.path());
151+ }),
152+ false,
153+ )
154+ .await?;
155+
156+ helpers::assert_file_has_content(file.as_file_mut(), &helpers::platform_line("hello"))?;
157+
158+ Ok(())
159+}
160+
161+#[tokio::test]
162+async fn test_write_scratch_no_path_fails() -> anyhow::Result<()> {
163+ helpers::test_key_sequence_with_input_text(
164+ None,
165+ ("#[\n|]#", "ihello<esc>:w<ret>", "hello#[\n|]#"),
166+ &|app| {
167+ assert!(app.editor.is_err());
168+
169+ let mut docs: Vec<_> = app.editor.documents().collect();
170+ assert_eq!(1, docs.len());
171+
172+ let doc = docs.pop().unwrap();
173+ assert_eq!(None, doc.path());
174+ },
175+ false,
176+ )
177+ .await?;
178+
179+ Ok(())
180+}
181+
182+#[tokio::test]
183+async fn test_write_auto_format_fails_still_writes() -> anyhow::Result<()> {
184+ let mut file = tempfile::Builder::new().suffix(".rs").tempfile()?;
185+
186+ let lang_conf = indoc! {r#"
187+ [[language]]
188+ name = "rust"
189+ formatter = { command = "bash", args = [ "-c", "exit 1" ] }
190+ "#};
191+
192+ let mut app = helpers::AppBuilder::new()
193+ .with_file(file.path(), None)
194+ .with_input_text("#[l|]#et foo = 0;\n")
195+ .with_lang_config(helpers::test_syntax_conf(Some(lang_conf.into())))
196+ .build()?;
197+
198+ test_key_sequences(&mut app, vec![(Some(":w<ret>"), None)], false).await?;
199+
200+ // file still saves
201+ helpers::assert_file_has_content(file.as_file_mut(), "let foo = 0;\n")?;
202+
203+ Ok(())
204+}
205+
206+#[tokio::test]
207+async fn test_write_new_path() -> anyhow::Result<()> {
208+ let mut file1 = tempfile::NamedTempFile::new().unwrap();
209+ let mut file2 = tempfile::NamedTempFile::new().unwrap();
210+ let mut app = helpers::AppBuilder::new()
211+ .with_file(file1.path(), None)
212+ .build()?;
213+
214+ test_key_sequences(
215+ &mut app,
216+ vec![
217+ (
218+ Some("ii can eat glass, it will not hurt me<ret><esc>:w<ret>"),
219+ Some(&|app| {
220+ let doc = doc!(app.editor);
221+ assert!(!app.editor.is_err());
222+ assert_eq!(file1.path(), doc.path().unwrap());
223+ }),
224+ ),
225+ (
226+ Some(&format!(":w {}<ret>", file2.path().to_string_lossy())),
227+ Some(&|app| {
228+ let doc = doc!(app.editor);
229+ assert!(!app.editor.is_err());
230+ assert_eq!(file2.path(), doc.path().unwrap());
231+ assert!(app.editor.document_by_path(file1.path()).is_none());
232+ }),
233+ ),
234+ ],
235+ false,
236+ )
237+ .await?;
238+
239+ helpers::assert_file_has_content(
240+ file1.as_file_mut(),
241+ &helpers::platform_line("i can eat glass, it will not hurt me\n"),
242+ )?;
243+
244+ helpers::assert_file_has_content(
245+ file2.as_file_mut(),
246+ &helpers::platform_line("i can eat glass, it will not hurt me\n"),
247+ )?;
248+
249+ Ok(())
250+}
251+
252+#[tokio::test]
135253async fn test_write_fail_new_path() -> anyhow::Result<()> {
136254 let file = helpers::new_readonly_tempfile()?;
137255
138256 test_key_sequences(
139− &mut Application::new(Args::default(), Config::default())?,
257+ &mut AppBuilder::new().build()?,
140258 vec![
141259 (
142260 None,
helix-view/src/document.rs+98−45
@@ -83,6 +83,18 @@ impl Serialize for Mode {
8383 }
8484}
8585
86+/// A snapshot of the text of a document that we want to write out to disk
87+#[derive(Debug, Clone)]
88+pub struct DocumentSavedEvent {
89+ pub revision: usize,
90+ pub doc_id: DocumentId,
91+ pub path: PathBuf,
92+ pub text: Rope,
93+}
94+
95+pub type DocumentSavedEventResult = Result<DocumentSavedEvent, anyhow::Error>;
96+pub type DocumentSavedEventFuture = BoxFuture<'static, DocumentSavedEventResult>;
97+
8698pub struct Document {
8799 pub(crate) id: DocumentId,
88100 text: Rope,
@@ -492,45 +504,61 @@ impl Document {
492504 Some(fut.boxed())
493505 }
494506
495− pub fn save(&mut self, force: bool) -> impl Future<Output = Result<(), anyhow::Error>> {
496− self.save_impl::<futures_util::future::Ready<_>>(None, force)
497− }
498−
499− pub fn format_and_save(
507+ pub fn save<P: Into<PathBuf>>(
500508 &mut self,
501− formatting: Option<impl Future<Output = Result<Transaction, FormatterError>>>,
509+ path: Option<P>,
502510 force: bool,
503− ) -> impl Future<Output = anyhow::Result<()>> {
504− self.save_impl(formatting, force)
511+ ) -> Result<
512+ impl Future<Output = Result<DocumentSavedEvent, anyhow::Error>> + 'static + Send,
513+ anyhow::Error,
514+ > {
515+ let path = path.map(|path| path.into());
516+ self.save_impl(path, force)
517+
518+ // futures_util::future::Ready<_>,
505519 }
506520
507− // TODO: do we need some way of ensuring two save operations on the same doc can't run at once?
508− // or is that handled by the OS/async layer
509521 /// The `Document`'s text is encoded according to its encoding and written to the file located
510522 /// at its `path()`.
511− ///
512− /// If `formatting` is present, it supplies some changes that we apply to the text before saving.
513− fn save_impl<F: Future<Output = Result<Transaction, FormatterError>>>(
523+ fn save_impl(
514524 &mut self,
515− formatting: Option<F>,
525+ path: Option<PathBuf>,
516526 force: bool,
517− ) -> impl Future<Output = Result<(), anyhow::Error>> {
527+ ) -> Result<
528+ impl Future<Output = Result<DocumentSavedEvent, anyhow::Error>> + 'static + Send,
529+ anyhow::Error,
530+ > {
531+ log::debug!(
532+ "submitting save of doc '{:?}'",
533+ self.path().map(|path| path.to_string_lossy())
534+ );
535+
518536 // we clone and move text + path into the future so that we asynchronously save the current
519537 // state without blocking any further edits.
538+ let text = self.text().clone();
520539
521− let mut text = self.text().clone();
522− let path = self.path.clone().expect("Can't save with no path set!");
523− let identifier = self.identifier();
540+ let path = match path {
541+ Some(path) => helix_core::path::get_canonicalized_path(&path)?,
542+ None => {
543+ if self.path.is_none() {
544+ bail!("Can't save with no path set!");
545+ }
524546
547+ self.path.as_ref().unwrap().clone()
548+ }
549+ };
550+
551+ let identifier = self.path().map(|_| self.identifier());
525552 let language_server = self.language_server.clone();
526553
527554 // mark changes up to now as saved
528− self.reset_modified();
555+ let current_rev = self.get_current_revision();
556+ let doc_id = self.id();
529557
530558 let encoding = self.encoding;
531559
532560 // We encode the file according to the `Document`'s encoding.
533− async move {
561+ let future = async move {
534562 use tokio::fs::File;
535563 if let Some(parent) = path.parent() {
536564 // TODO: display a prompt asking the user if the directories should be created
@@ -543,39 +571,34 @@ impl Document {
543571 }
544572 }
545573
546− if let Some(fmt) = formatting {
547− match fmt.await {
548− Ok(transaction) => {
549− let success = transaction.changes().apply(&mut text);
550− if !success {
551− // This shouldn't happen, because the transaction changes were generated
552− // from the same text we're saving.
553− log::error!("failed to apply format changes before saving");
554− }
555− }
556− Err(err) => {
557− // formatting failed: report error, and save file without modifications
558− log::error!("{}", err);
559− }
560− }
561− }
562−
563− let mut file = File::create(path).await?;
574+ let mut file = File::create(&path).await?;
564575 to_writer(&mut file, encoding, &text).await?;
565576
577+ let event = DocumentSavedEvent {
578+ revision: current_rev,
579+ doc_id,
580+ path,
581+ text: text.clone(),
582+ };
583+
566584 if let Some(language_server) = language_server {
567585 if !language_server.is_initialized() {
568− return Ok(());
586+ return Ok(event);
569587 }
570− if let Some(notification) =
571− language_server.text_document_did_save(identifier, &text)
572− {
573− notification.await?;
588+
589+ if let Some(identifier) = identifier {
590+ if let Some(notification) =
591+ language_server.text_document_did_save(identifier, &text)
592+ {
593+ notification.await?;
594+ }
574595 }
575596 }
576597
577− Ok(())
578− }
598+ Ok(event)
599+ };
600+
601+ Ok(future)
579602 }
580603
581604 /// Detect the programming language based on the file type.
@@ -930,6 +953,12 @@ impl Document {
930953 let history = self.history.take();
931954 let current_revision = history.current_revision();
932955 self.history.set(history);
956+ log::debug!(
957+ "id {} modified - last saved: {}, current: {}",
958+ self.id,
959+ self.last_saved_revision,
960+ current_revision
961+ );
933962 current_revision != self.last_saved_revision || !self.changes.is_empty()
934963 }
935964
@@ -941,6 +970,30 @@ impl Document {
941970 self.last_saved_revision = current_revision;
942971 }
943972
973+ /// Set the document's latest saved revision to the given one.
974+ pub fn set_last_saved_revision(&mut self, rev: usize) {
975+ log::debug!(
976+ "doc {} revision updated {} -> {}",
977+ self.id,
978+ self.last_saved_revision,
979+ rev
980+ );
981+ self.last_saved_revision = rev;
982+ }
983+
984+ /// Get the document's latest saved revision.
985+ pub fn get_last_saved_revision(&mut self) -> usize {
986+ self.last_saved_revision
987+ }
988+
989+ /// Get the current revision number
990+ pub fn get_current_revision(&mut self) -> usize {
991+ let history = self.history.take();
992+ let current_revision = history.current_revision();
993+ self.history.set(history);
994+ current_revision
995+ }
996+
944997 /// Corresponding language scope name. Usually `source.<lang>`.
945998 pub fn language_scope(&self) -> Option<&str> {
946999 self.language
helix-view/src/editor.rs+124−16
@@ -1,6 +1,6 @@
11use crate::{
22 clipboard::{get_clipboard_provider, ClipboardProvider},
3− document::Mode,
3+ document::{DocumentSavedEventFuture, DocumentSavedEventResult, Mode},
44 graphics::{CursorKind, Rect},
55 info::Info,
66 input::KeyEvent,
@@ -9,8 +9,9 @@ use crate::{
99 Document, DocumentId, View, ViewId,
1010};
1111
12−use futures_util::future;
1312use futures_util::stream::select_all::SelectAll;
13+use futures_util::{future, StreamExt};
14+use helix_lsp::Call;
1415use tokio_stream::wrappers::UnboundedReceiverStream;
1516
1617use std::{
@@ -28,7 +29,7 @@ use tokio::{
2829 time::{sleep, Duration, Instant, Sleep},
2930};
3031
31−use anyhow::Error;
32+use anyhow::{anyhow, bail, Error};
3233
3334pub use helix_core::diagnostic::Severity;
3435pub use helix_core::register::Registers;
@@ -65,7 +66,7 @@ where
6566 )
6667}
6768
68−#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
69+#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
6970#[serde(rename_all = "kebab-case", default, deny_unknown_fields)]
7071pub struct FilePickerConfig {
7172 /// IgnoreOptions
@@ -172,7 +173,7 @@ pub struct Config {
172173 pub color_modes: bool,
173174}
174175
175−#[derive(Debug, Default, Clone, PartialEq, Serialize, Deserialize)]
176+#[derive(Debug, Default, Clone, PartialEq, Eq, Serialize, Deserialize)]
176177#[serde(default, rename_all = "kebab-case", deny_unknown_fields)]
177178pub struct TerminalConfig {
178179 pub command: String,
@@ -225,7 +226,7 @@ pub fn get_terminal_provider() -> Option<TerminalConfig> {
225226 None
226227}
227228
228−#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
229+#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
229230#[serde(default, rename_all = "kebab-case", deny_unknown_fields)]
230231pub struct LspConfig {
231232 /// Display LSP progress messages below statusline
@@ -246,7 +247,7 @@ impl Default for LspConfig {
246247 }
247248}
248249
249−#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
250+#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
250251#[serde(rename_all = "kebab-case", default, deny_unknown_fields)]
251252pub struct SearchConfig {
252253 /// Smart case: Case insensitive searching unless pattern contains upper case characters. Defaults to true.
@@ -255,7 +256,7 @@ pub struct SearchConfig {
255256 pub wrap_around: bool,
256257}
257258
258−#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
259+#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
259260#[serde(rename_all = "kebab-case", default, deny_unknown_fields)]
260261pub struct StatusLineConfig {
261262 pub left: Vec<StatusLineElement>,
@@ -279,7 +280,7 @@ impl Default for StatusLineConfig {
279280 }
280281}
281282
282−#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
283+#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
283284#[serde(rename_all = "kebab-case", default, deny_unknown_fields)]
284285pub struct ModeConfig {
285286 pub normal: String,
@@ -458,7 +459,7 @@ impl std::str::FromStr for GutterType {
458459 }
459460}
460461
461−#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
462+#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
462463#[serde(default)]
463464pub struct WhitespaceConfig {
464465 pub render: WhitespaceRender,
@@ -643,12 +644,21 @@ pub struct Breakpoint {
643644 pub log_message: Option<String>,
644645}
645646
647+use futures_util::stream::{Flatten, Once};
648+
646649pub struct Editor {
647650 /// Current editing mode.
648651 pub mode: Mode,
649652 pub tree: Tree,
650653 pub next_document_id: DocumentId,
651654 pub documents: BTreeMap<DocumentId, Document>,
655+
656+ // We Flatten<> to resolve the inner DocumentSavedEventFuture. For that we need a stream of streams, hence the Once<>.
657+ // https://stackoverflow.com/a/66875668
658+ pub saves: HashMap<DocumentId, UnboundedSender<Once<DocumentSavedEventFuture>>>,
659+ pub save_queue: SelectAll<Flatten<UnboundedReceiverStream<Once<DocumentSavedEventFuture>>>>,
660+ pub write_count: usize,
661+
652662 pub count: Option<std::num::NonZeroUsize>,
653663 pub selected_register: Option<char>,
654664 pub registers: Registers,
@@ -688,6 +698,15 @@ pub struct Editor {
688698 pub config_events: (UnboundedSender<ConfigEvent>, UnboundedReceiver<ConfigEvent>),
689699}
690700
701+#[derive(Debug)]
702+pub enum EditorEvent {
703+ DocumentSaved(DocumentSavedEventResult),
704+ ConfigEvent(ConfigEvent),
705+ LanguageServerMessage((usize, Call)),
706+ DebuggerEvent(dap::Payload),
707+ IdleTimer,
708+}
709+
691710#[derive(Debug, Clone)]
692711pub enum ConfigEvent {
693712 Refresh,
@@ -719,6 +738,8 @@ pub enum CloseError {
719738 DoesNotExist,
720739 /// Buffer is modified
721740 BufferModified(String),
741+ /// Document failed to save
742+ SaveError(anyhow::Error),
722743}
723744
724745impl Editor {
@@ -739,6 +760,9 @@ impl Editor {
739760 tree: Tree::new(area),
740761 next_document_id: DocumentId::default(),
741762 documents: BTreeMap::new(),
763+ saves: HashMap::new(),
764+ save_queue: SelectAll::new(),
765+ write_count: 0,
742766 count: None,
743767 selected_register: None,
744768 macro_recording: None,
@@ -804,12 +828,16 @@ impl Editor {
804828
805829 #[inline]
806830 pub fn set_status<T: Into<Cow<'static, str>>>(&mut self, status: T) {
807− self.status_msg = Some((status.into(), Severity::Info));
831+ let status = status.into();
832+ log::debug!("editor status: {}", status);
833+ self.status_msg = Some((status, Severity::Info));
808834 }
809835
810836 #[inline]
811837 pub fn set_error<T: Into<Cow<'static, str>>>(&mut self, error: T) {
812− self.status_msg = Some((error.into(), Severity::Error));
838+ let error = error.into();
839+ log::error!("editor error: {}", error);
840+ self.status_msg = Some((error, Severity::Error));
813841 }
814842
815843 #[inline]
@@ -1034,6 +1062,13 @@ impl Editor {
10341062 DocumentId(unsafe { NonZeroUsize::new_unchecked(self.next_document_id.0.get() + 1) });
10351063 doc.id = id;
10361064 self.documents.insert(id, doc);
1065+
1066+ let (save_sender, save_receiver) = tokio::sync::mpsc::unbounded_channel();
1067+ self.saves.insert(id, save_sender);
1068+
1069+ let stream = UnboundedReceiverStream::new(save_receiver).flatten();
1070+ self.save_queue.push(stream);
1071+
10371072 id
10381073 }
10391074
@@ -1080,16 +1115,19 @@ impl Editor {
10801115 }
10811116
10821117 pub fn close_document(&mut self, doc_id: DocumentId, force: bool) -> Result<(), CloseError> {
1083− let doc = match self.documents.get(&doc_id) {
1118+ let doc = match self.documents.get_mut(&doc_id) {
10841119 Some(doc) => doc,
10851120 None => return Err(CloseError::DoesNotExist),
10861121 };
1087−
10881122 if !force && doc.is_modified() {
10891123 return Err(CloseError::BufferModified(doc.display_name().into_owned()));
10901124 }
10911125
1126+ // This will also disallow any follow-up writes
1127+ self.saves.remove(&doc_id);
1128+
10921129 if let Some(language_server) = doc.language_server() {
1130+ // TODO: track error
10931131 tokio::spawn(language_server.text_document_did_close(doc.identifier()));
10941132 }
10951133
@@ -1152,6 +1190,32 @@ impl Editor {
11521190 Ok(())
11531191 }
11541192
1193+ pub fn save<P: Into<PathBuf>>(
1194+ &mut self,
1195+ doc_id: DocumentId,
1196+ path: Option<P>,
1197+ force: bool,
1198+ ) -> anyhow::Result<()> {
1199+ // convert a channel of futures to pipe into main queue one by one
1200+ // via stream.then() ? then push into main future
1201+
1202+ let path = path.map(|path| path.into());
1203+ let doc = doc_mut!(self, &doc_id);
1204+ let future = doc.save(path, force)?;
1205+
1206+ use futures_util::stream;
1207+
1208+ self.saves
1209+ .get(&doc_id)
1210+ .ok_or_else(|| anyhow::format_err!("saves are closed for this document!"))?
1211+ .send(stream::once(Box::pin(future)))
1212+ .map_err(|err| anyhow!("failed to send save event: {}", err))?;
1213+
1214+ self.write_count += 1;
1215+
1216+ Ok(())
1217+ }
1218+
11551219 pub fn resize(&mut self, area: Rect) {
11561220 if self.tree.resize(area) {
11571221 self._refresh();
@@ -1252,14 +1316,14 @@ impl Editor {
12521316 }
12531317 }
12541318
1255− /// Closes language servers with timeout. The default timeout is 500 ms, use
1319+ /// Closes language servers with timeout. The default timeout is 10000 ms, use
12561320 /// `timeout` parameter to override this.
12571321 pub async fn close_language_servers(
12581322 &self,
12591323 timeout: Option<u64>,
12601324 ) -> Result<(), tokio::time::error::Elapsed> {
12611325 tokio::time::timeout(
1262− Duration::from_millis(timeout.unwrap_or(500)),
1326+ Duration::from_millis(timeout.unwrap_or(3000)),
12631327 future::join_all(
12641328 self.language_servers
12651329 .iter_clients()
@@ -1269,4 +1333,48 @@ impl Editor {
12691333 .await
12701334 .map(|_| ())
12711335 }
1336+
1337+ pub async fn wait_event(&mut self) -> EditorEvent {
1338+ tokio::select! {
1339+ biased;
1340+
1341+ Some(event) = self.save_queue.next() => {
1342+ self.write_count -= 1;
1343+ EditorEvent::DocumentSaved(event)
1344+ }
1345+ Some(config_event) = self.config_events.1.recv() => {
1346+ EditorEvent::ConfigEvent(config_event)
1347+ }
1348+ Some(message) = self.language_servers.incoming.next() => {
1349+ EditorEvent::LanguageServerMessage(message)
1350+ }
1351+ Some(event) = self.debugger_events.next() => {
1352+ EditorEvent::DebuggerEvent(event)
1353+ }
1354+ _ = &mut self.idle_timer => {
1355+ EditorEvent::IdleTimer
1356+ }
1357+ }
1358+ }
1359+
1360+ pub async fn flush_writes(&mut self) -> anyhow::Result<()> {
1361+ while self.write_count > 0 {
1362+ if let Some(save_event) = self.save_queue.next().await {
1363+ self.write_count -= 1;
1364+
1365+ let save_event = match save_event {
1366+ Ok(event) => event,
1367+ Err(err) => {
1368+ self.set_error(err.to_string());
1369+ bail!(err);
1370+ }
1371+ };
1372+
1373+ let doc = doc_mut!(self, &save_event.doc_id);
1374+ doc.set_last_saved_revision(save_event.revision);
1375+ }
1376+ }
1377+
1378+ Ok(())
1379+ }
12721380}