on master help sign in
Merge pull request #2267 from dead10ck/fix-write-fail
78c0cdcBlaž Hrastnik
docs/CONTRIBUTING.md+2−1
@@ -35,7 +35,8 @@ to `cargo install` anything either).
35Integration tests for helix-term can be run with `cargo integration-test`. Code
35Integration tests for helix-term can be run with `cargo integration-test`. Code
36contributors are strongly encouraged to write integration tests for their code.
36contributors are strongly encouraged to write integration tests for their code.
37Existing tests can be used as examples. Helpers can be found in
37Existing 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`.
39
40
40## Minimum Stable Rust Version (MSRV) Policy
41## Minimum Stable Rust Version (MSRV) Policy
41
42
helix-core/src/auto_pairs.rs−1
@@ -7,7 +7,6 @@ use std::collections::HashMap;
7use smallvec::SmallVec;
7use smallvec::SmallVec;
8
8
9// Heavily based on https://github.com/codemirror/closebrackets/
9// Heavily based on https://github.com/codemirror/closebrackets/
10−
11pub const DEFAULT_PAIRS: &[(char, char)] = &[
10pub const DEFAULT_PAIRS: &[(char, char)] = &[
12 ('(', ')'),
11 ('(', ')'),
13 ('{', '}'),
12 ('{', '}'),
helix-core/src/syntax.rs+6
@@ -61,6 +61,12 @@ pub struct Configuration {
61 pub language: Vec<LanguageConfiguration>,
61 pub language: Vec<LanguageConfiguration>,
62}
62}
63
63
64+impl Default for Configuration {
65+ fn default() -> Self {
66+ crate::config::default_syntax_loader()
67+ }
68+}
69+
64// largely based on tree-sitter/cli/src/loader.rs
70// largely based on tree-sitter/cli/src/loader.rs
65#[derive(Debug, Serialize, Deserialize)]
71#[derive(Debug, Serialize, Deserialize)]
66#[serde(rename_all = "kebab-case", deny_unknown_fields)]
72#[serde(rename_all = "kebab-case", deny_unknown_fields)]
helix-term/src/application.rs+156−52
@@ -1,12 +1,19 @@
1use arc_swap::{access::Map, ArcSwap};
1use arc_swap::{access::Map, ArcSwap};
2use futures_util::Stream;
2use futures_util::Stream;
3use helix_core::{
3use helix_core::{
4− config::{default_syntax_loader, user_syntax_loader},
5 diagnostic::{DiagnosticTag, NumberOrString},
4 diagnostic::{DiagnosticTag, NumberOrString},
5+ path::get_relative_path,
6 pos_at_coords, syntax, Selection,
6 pos_at_coords, syntax, Selection,
7};
7};
8use helix_lsp::{lsp, util::lsp_pos_to_pos, LspProgressMap};
8use 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+};
10use serde_json::json;
17use serde_json::json;
11
18
12use crate::{
19use crate::{
@@ -19,7 +26,7 @@ use crate::{
19 ui::{self, overlay::overlayed},
26 ui::{self, overlay::overlayed},
20};
27};
21
28
22−use log::{error, warn};
29+use log::{debug, error, warn};
23use std::{
30use std::{
24 io::{stdin, stdout, Write},
31 io::{stdin, stdout, Write},
25 sync::Arc,
32 sync::Arc,
@@ -102,7 +109,11 @@ fn restore_term() -> Result<(), Error> {
102}
109}
103
110
104impl Application {
111impl Application {
112+ pub fn new(
113+ args: Args,
114+ config: Config,
115+ syn_loader_conf: syntax::Configuration,
105− pub fn new(args: Args, config: Config) -> Result<Self, Error> {
116+ ) -> Result<Self, Error> {
106 #[cfg(feature = "integration")]
117 #[cfg(feature = "integration")]
107 setup_integration_logging();
118 setup_integration_logging();
108
119
@@ -129,14 +140,6 @@ impl Application {
129 })
140 })
130 .unwrap_or_else(|| theme_loader.default_theme(true_color));
141 .unwrap_or_else(|| theme_loader.default_theme(true_color));
131
142
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− });
140 let syn_loader = std::sync::Arc::new(syntax::Loader::new(syn_loader_conf));
143 let syn_loader = std::sync::Arc::new(syntax::Loader::new(syn_loader_conf));
141
144
142 let mut compositor = Compositor::new().context("build compositor")?;
145 let mut compositor = Compositor::new().context("build compositor")?;
@@ -245,6 +248,10 @@ impl Application {
245 Ok(app)
248 Ok(app)
246 }
249 }
247
250
251+ #[cfg(feature = "integration")]
252+ fn render(&mut self) {}
253+
254+ #[cfg(not(feature = "integration"))]
248 fn render(&mut self) {
255 fn render(&mut self) {
249 let compositor = &mut self.compositor;
256 let compositor = &mut self.compositor;
250
257
@@ -275,9 +282,6 @@ impl Application {
275 where
282 where
276 S: Stream<Item = crossterm::Result<crossterm::event::Event>> + Unpin,
283 S: Stream<Item = crossterm::Result<crossterm::event::Event>> + Unpin,
277 {
284 {
278− #[cfg(feature = "integration")]
279− let mut idle_handled = false;
280−
281 loop {
285 loop {
282 if self.editor.should_close() {
286 if self.editor.should_close() {
283 return false;
287 return false;
@@ -294,26 +298,6 @@ impl Application {
294 Some(signal) = self.signals.next() => {
298 Some(signal) = self.signals.next() => {
295 self.handle_signals(signal).await;
299 self.handle_signals(signal).await;
296 }
300 }
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− }
317 Some(callback) = self.jobs.futures.next() => {
301 Some(callback) = self.jobs.futures.next() => {
318 self.jobs.handle_callback(&mut self.editor, &mut self.compositor, callback);
302 self.jobs.handle_callback(&mut self.editor, &mut self.compositor, callback);
319 self.render();
303 self.render();
@@ -322,26 +306,22 @@ impl Application {
322 self.jobs.handle_callback(&mut self.editor, &mut self.compositor, callback);
306 self.jobs.handle_callback(&mut self.editor, &mut self.compositor, callback);
323 self.render();
307 self.render();
324 }
308 }
325− _ = &mut self.editor.idle_timer => {
309+ event = self.editor.wait_event() => {
326− // idle timeout
310+ let _idle_handled = self.handle_editor_event(event).await;
327− self.editor.clear_idle_timer();
328− self.handle_idle_timeout();
329
311
330 #[cfg(feature = "integration")]
312 #[cfg(feature = "integration")]
331 {
313 {
332− idle_handled = true;
314+ if _idle_handled {
315+ return true;
316+ }
333 }
317 }
334 }
318 }
335 }
319 }
336
320
337 // for integration tests only, reset the idle timer after every
321 // 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
339 #[cfg(feature = "integration")]
323 #[cfg(feature = "integration")]
340 {
324 {
341− if idle_handled {
342− return true;
343− }
344−
345 self.editor.reset_idle_timer();
325 self.editor.reset_idle_timer();
346 }
326 }
347 }
327 }
@@ -446,6 +426,111 @@ impl Application {
446 }
426 }
447 }
427 }
448
428
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+
449 pub fn handle_terminal_events(&mut self, event: Result<CrosstermEvent, crossterm::ErrorKind>) {
534 pub fn handle_terminal_events(&mut self, event: Result<CrosstermEvent, crossterm::ErrorKind>) {
450 let mut cx = crate::compositor::Context {
535 let mut cx = crate::compositor::Context {
451 editor: &mut self.editor,
536 editor: &mut self.editor,
@@ -866,11 +951,10 @@ impl Application {
866
951
867 self.event_loop(input_stream).await;
952 self.event_loop(input_stream).await;
868
953
869− let err = self.close().await.err();
954+ let close_errs = self.close().await;
870−
871 restore_term()?;
955 restore_term()?;
872
956
873− if let Some(err) = err {
957+ for err in close_errs {
874 self.editor.exit_code = 1;
958 self.editor.exit_code = 1;
875 eprintln!("Error: {}", err);
959 eprintln!("Error: {}", err);
876 }
960 }
@@ -878,13 +962,33 @@ impl Application {
878 Ok(self.editor.exit_code)
962 Ok(self.editor.exit_code)
879 }
963 }
880
964
881− pub async fn close(&mut self) -> anyhow::Result<()> {
965+ pub async fn close(&mut self) -> Vec<anyhow::Error> {
882− self.jobs.finish().await?;
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+ }
883
984
884 if self.editor.close_language_servers(None).await.is_err() {
985 if self.editor.close_language_servers(None).await.is_err() {
885 log::error!("Timed out waiting for language servers to shutdown");
986 log::error!("Timed out waiting for language servers to shutdown");
987+ errs.push(anyhow::format_err!(
988+ "Timed out waiting for language servers to shutdown"
989+ ));
886− };
990+ }
887
991
888− Ok(())
992+ errs
889 }
993 }
890}
994}
helix-term/src/commands.rs+34−29
@@ -47,12 +47,13 @@ use movement::Movement;
47use crate::{
47use crate::{
48 args,
48 args,
49 compositor::{self, Component, Compositor},
49 compositor::{self, Component, Compositor},
50+ job::Callback,
50 keymap::ReverseKeymap,
51 keymap::ReverseKeymap,
51 ui::{self, overlay::overlayed, FilePicker, Picker, Popup, Prompt, PromptEvent},
52 ui::{self, overlay::overlayed, FilePicker, Picker, Popup, Prompt, PromptEvent},
52};
53};
53
54
54−use crate::job::{self, Job, Jobs};
55+use crate::job::{self, Jobs};
55−use futures_util::{FutureExt, StreamExt};
56+use futures_util::StreamExt;
56use std::{collections::HashMap, fmt, future::Future};
57use std::{collections::HashMap, fmt, future::Future};
57use std::{collections::HashSet, num::NonZeroUsize};
58use std::{collections::HashSet, num::NonZeroUsize};
58
59
@@ -107,10 +108,11 @@ impl<'a> Context<'a> {
107 let callback = Box::pin(async move {
108 let callback = Box::pin(async move {
108 let json = call.await?;
109 let json = call.await?;
109 let response = serde_json::from_value(json)?;
110 let response = serde_json::from_value(json)?;
110− let call: job::Callback =
111+ let call: job::Callback = Callback::EditorCompositor(Box::new(
111− Box::new(move |editor: &mut Editor, compositor: &mut Compositor| {
112+ move |editor: &mut Editor, compositor: &mut Compositor| {
112 callback(editor, compositor, response)
113 callback(editor, compositor, response)
114+ },
113− });
115+ ));
114 Ok(call)
116 Ok(call)
115 });
117 });
116 self.jobs.callback(callback);
118 self.jobs.callback(callback);
@@ -1925,8 +1927,8 @@ fn global_search(cx: &mut Context) {
1925 let show_picker = async move {
1927 let show_picker = async move {
1926 let all_matches: Vec<FileResult> =
1928 let all_matches: Vec<FileResult> =
1927 UnboundedReceiverStream::new(all_matches_rx).collect().await;
1929 UnboundedReceiverStream::new(all_matches_rx).collect().await;
1928− let call: job::Callback =
1930+ let call: job::Callback = Callback::EditorCompositor(Box::new(
1929− Box::new(move |editor: &mut Editor, compositor: &mut Compositor| {
1931+ move |editor: &mut Editor, compositor: &mut Compositor| {
1930 if all_matches.is_empty() {
1932 if all_matches.is_empty() {
1931 editor.set_status("No matches found");
1933 editor.set_status("No matches found");
1932 return;
1934 return;
@@ -1962,7 +1964,8 @@ fn global_search(cx: &mut Context) {
1962 },
1964 },
1963 );
1965 );
1964 compositor.push(Box::new(overlayed(picker)));
1966 compositor.push(Box::new(overlayed(picker)));
1967+ },
1965− });
1968+ ));
1966 Ok(call)
1969 Ok(call)
1967 };
1970 };
1968 cx.jobs.callback(show_picker);
1971 cx.jobs.callback(show_picker);
@@ -2504,13 +2507,6 @@ fn insert_at_line_end(cx: &mut Context) {
2504 doc.set_selection(view.id, selection);
2507 doc.set_selection(view.id, selection);
2505}
2508}
2506
2509
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−
2514// Creates an LspCallback that waits for formatting changes to be computed. When they're done,
2510// Creates an LspCallback that waits for formatting changes to be computed. When they're done,
2515// it applies them, but only if the doc hasn't changed.
2511// it applies them, but only if the doc hasn't changed.
2516//
2512//
@@ -2519,11 +2515,12 @@ enum Modified {
2519async fn make_format_callback(
2515async fn make_format_callback(
2520 doc_id: DocumentId,
2516 doc_id: DocumentId,
2521 doc_version: i32,
2517 doc_version: i32,
2522− modified: Modified,
2523 format: impl Future<Output = Result<Transaction, FormatterError>> + Send + 'static,
2518 format: impl Future<Output = Result<Transaction, FormatterError>> + Send + 'static,
2519+ write: Option<(Option<PathBuf>, bool)>,
2524) -> anyhow::Result<job::Callback> {
2520) -> anyhow::Result<job::Callback> {
2525− let format = format.await?;
2521+ let format = format.await;
2522+
2526− let call: job::Callback = Box::new(move |editor, _compositor| {
2523+ let call: job::Callback = Callback::Editor(Box::new(move |editor| {
2527 if !editor.documents.contains_key(&doc_id) {
2524 if !editor.documents.contains_key(&doc_id) {
2528 return;
2525 return;
2529 }
2526 }
@@ -2531,22 +2528,30 @@ async fn make_format_callback(
2531 let scrolloff = editor.config().scrolloff;
2528 let scrolloff = editor.config().scrolloff;
2532 let doc = doc_mut!(editor, &doc_id);
2529 let doc = doc_mut!(editor, &doc_id);
2533 let view = view_mut!(editor);
2530 let view = view_mut!(editor);
2531+
2532+ if let Ok(format) = format {
2534− if doc.version() == doc_version {
2533+ if doc.version() == doc_version {
2535− apply_transaction(&format, doc, view);
2534+ apply_transaction(&format, doc, view);
2536− doc.append_changes_to_history(view.id);
2535+ doc.append_changes_to_history(view.id);
2537− doc.detect_indent_and_line_ending();
2536+ doc.detect_indent_and_line_ending();
2538− view.ensure_cursor_in_view(doc, scrolloff);
2537+ view.ensure_cursor_in_view(doc, scrolloff);
2539− if let Modified::SetUnmodified = modified {
2538+ } else {
2540− doc.reset_modified();
2539+ log::info!("discarded formatting changes because the document changed");
2541 }
2540 }
2542− } else {
2543− log::info!("discarded formatting changes because the document changed");
2544 }
2541 }
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+ }
2545− });
2549+ }));
2550+
2546 Ok(call)
2551 Ok(call)
2547}
2552}
2548
2553
2549−#[derive(PartialEq)]
2554+#[derive(PartialEq, Eq)]
2550pub enum Open {
2555pub enum Open {
2551 Below,
2556 Below,
2552 Above,
2557 Above,
helix-term/src/commands/dap.rs+20−15
@@ -118,11 +118,14 @@ fn dap_callback<T, F>(
118 let callback = Box::pin(async move {
118 let callback = Box::pin(async move {
119 let json = call.await?;
119 let json = call.await?;
120 let response = serde_json::from_value(json)?;
120 let response = serde_json::from_value(json)?;
121+ let call: Callback = Callback::EditorCompositor(Box::new(
121− let call: Callback = Box::new(move |editor: &mut Editor, compositor: &mut Compositor| {
122+ move |editor: &mut Editor, compositor: &mut Compositor| {
122− callback(editor, compositor, response)
123+ callback(editor, compositor, response)
124+ },
123− });
125+ ));
124 Ok(call)
126 Ok(call)
125 });
127 });
128+
126 jobs.callback(callback);
129 jobs.callback(callback);
127}
130}
128
131
@@ -274,10 +277,11 @@ pub fn dap_launch(cx: &mut Context) {
274 let completions = template.completion.clone();
277 let completions = template.completion.clone();
275 let name = template.name.clone();
278 let name = template.name.clone();
276 let callback = Box::pin(async move {
279 let callback = Box::pin(async move {
280+ let call: Callback =
277− let call: Callback = Box::new(move |_editor, compositor| {
281+ Callback::EditorCompositor(Box::new(move |_editor, compositor| {
278− let prompt = debug_parameter_prompt(completions, name, Vec::new());
282+ let prompt = debug_parameter_prompt(completions, name, Vec::new());
279− compositor.push(Box::new(prompt));
283+ compositor.push(Box::new(prompt));
280− });
284+ }));
281 Ok(call)
285 Ok(call)
282 });
286 });
283 cx.jobs.callback(callback);
287 cx.jobs.callback(callback);
@@ -332,10 +336,11 @@ fn debug_parameter_prompt(
332 let config_name = config_name.clone();
336 let config_name = config_name.clone();
333 let params = params.clone();
337 let params = params.clone();
334 let callback = Box::pin(async move {
338 let callback = Box::pin(async move {
339+ let call: Callback =
335− let call: Callback = Box::new(move |_editor, compositor| {
340+ Callback::EditorCompositor(Box::new(move |_editor, compositor| {
336− let prompt = debug_parameter_prompt(completions, config_name, params);
341+ let prompt = debug_parameter_prompt(completions, config_name, params);
337− compositor.push(Box::new(prompt));
342+ compositor.push(Box::new(prompt));
338− });
343+ }));
339 Ok(call)
344 Ok(call)
340 });
345 });
341 cx.jobs.callback(callback);
346 cx.jobs.callback(callback);
@@ -582,7 +587,7 @@ pub fn dap_edit_condition(cx: &mut Context) {
582 None => return,
587 None => return,
583 };
588 };
584 let callback = Box::pin(async move {
589 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| {
586 let mut prompt = Prompt::new(
591 let mut prompt = Prompt::new(
587 "condition:".into(),
592 "condition:".into(),
588 None,
593 None,
@@ -610,7 +615,7 @@ pub fn dap_edit_condition(cx: &mut Context) {
610 prompt.insert_str(&condition, editor)
615 prompt.insert_str(&condition, editor)
611 }
616 }
612 compositor.push(Box::new(prompt));
617 compositor.push(Box::new(prompt));
613− });
618+ }));
614 Ok(call)
619 Ok(call)
615 });
620 });
616 cx.jobs.callback(callback);
621 cx.jobs.callback(callback);
@@ -624,7 +629,7 @@ pub fn dap_edit_log(cx: &mut Context) {
624 None => return,
629 None => return,
625 };
630 };
626 let callback = Box::pin(async move {
631 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| {
628 let mut prompt = Prompt::new(
633 let mut prompt = Prompt::new(
629 "log-message:".into(),
634 "log-message:".into(),
630 None,
635 None,
@@ -651,7 +656,7 @@ pub fn dap_edit_log(cx: &mut Context) {
651 prompt.insert_str(&log_message, editor);
656 prompt.insert_str(&log_message, editor);
652 }
657 }
653 compositor.push(Box::new(prompt));
658 compositor.push(Box::new(prompt));
654− });
659+ }));
655 Ok(call)
660 Ok(call)
656 });
661 });
657 cx.jobs.callback(callback);
662 cx.jobs.callback(callback);
helix-term/src/commands/typed.rs+84−68
@@ -1,5 +1,7 @@
1use std::ops::Deref;
1use std::ops::Deref;
2
2
3+use crate::job::Job;
4+
3use super::*;
5use super::*;
4
6
5use helix_view::{
7use helix_view::{
@@ -19,6 +21,8 @@ pub struct TypableCommand {
19}
21}
20
22
21fn quit(cx: &mut compositor::Context, args: &[Cow<str>], event: PromptEvent) -> anyhow::Result<()> {
23fn quit(cx: &mut compositor::Context, args: &[Cow<str>], event: PromptEvent) -> anyhow::Result<()> {
24+ log::debug!("quitting...");
25+
22 if event != PromptEvent::Validate {
26 if event != PromptEvent::Validate {
23 return Ok(());
27 return Ok(());
24 }
28 }
@@ -30,6 +34,7 @@ fn quit(cx: &mut compositor::Context, args: &[Cow<str>], event: PromptEvent) ->
30 buffers_remaining_impl(cx.editor)?
34 buffers_remaining_impl(cx.editor)?
31 }
35 }
32
36
37+ cx.block_try_flush_writes()?;
33 cx.editor.close(view!(cx.editor).id);
38 cx.editor.close(view!(cx.editor).id);
34
39
35 Ok(())
40 Ok(())
@@ -70,14 +75,16 @@ fn open(cx: &mut compositor::Context, args: &[Cow<str>], event: PromptEvent) ->
70}
75}
71
76
72fn buffer_close_by_ids_impl(
77fn buffer_close_by_ids_impl(
73− editor: &mut Editor,
78+ cx: &mut compositor::Context,
74 doc_ids: &[DocumentId],
79 doc_ids: &[DocumentId],
75 force: bool,
80 force: bool,
76) -> anyhow::Result<()> {
81) -> anyhow::Result<()> {
82+ cx.block_try_flush_writes()?;
83+
77 let (modified_ids, modified_names): (Vec<_>, Vec<_>) = doc_ids
84 let (modified_ids, modified_names): (Vec<_>, Vec<_>) = doc_ids
78 .iter()
85 .iter()
79 .filter_map(|&doc_id| {
86 .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) {
81 Some((doc_id, name))
88 Some((doc_id, name))
82 } else {
89 } else {
83 None
90 None
@@ -86,11 +93,11 @@ fn buffer_close_by_ids_impl(
86 .unzip();
93 .unzip();
87
94
88 if let Some(first) = modified_ids.first() {
95 if let Some(first) = modified_ids.first() {
89− let current = doc!(editor);
96+ let current = doc!(cx.editor);
90 // If the current document is unmodified, and there are modified
97 // If the current document is unmodified, and there are modified
91 // documents, switch focus to the first modified doc.
98 // documents, switch focus to the first modified doc.
92 if !modified_ids.contains(&current.id()) {
99 if !modified_ids.contains(&current.id()) {
93− editor.switch(*first, Action::Replace);
100+ cx.editor.switch(*first, Action::Replace);
94 }
101 }
95 bail!(
102 bail!(
96 "{} unsaved buffer(s) remaining: {:?}",
103 "{} unsaved buffer(s) remaining: {:?}",
@@ -149,7 +156,7 @@ fn buffer_close(
149 }
156 }
150
157
151 let document_ids = buffer_gather_paths_impl(cx.editor, args);
158 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)
153}
160}
154
161
155fn force_buffer_close(
162fn force_buffer_close(
@@ -162,7 +169,7 @@ fn force_buffer_close(
162 }
169 }
163
170
164 let document_ids = buffer_gather_paths_impl(cx.editor, args);
171 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)
166}
173}
167
174
168fn buffer_gather_others_impl(editor: &mut Editor) -> Vec<DocumentId> {
175fn buffer_gather_others_impl(editor: &mut Editor) -> Vec<DocumentId> {
@@ -184,7 +191,7 @@ fn buffer_close_others(
184 }
191 }
185
192
186 let document_ids = buffer_gather_others_impl(cx.editor);
193 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)
188}
195}
189
196
190fn force_buffer_close_others(
197fn force_buffer_close_others(
@@ -197,7 +204,7 @@ fn force_buffer_close_others(
197 }
204 }
198
205
199 let document_ids = buffer_gather_others_impl(cx.editor);
206 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)
201}
208}
202
209
203fn buffer_gather_all_impl(editor: &mut Editor) -> Vec<DocumentId> {
210fn buffer_gather_all_impl(editor: &mut Editor) -> Vec<DocumentId> {
@@ -214,7 +221,7 @@ fn buffer_close_all(
214 }
221 }
215
222
216 let document_ids = buffer_gather_all_impl(cx.editor);
223 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)
218}
225}
219
226
220fn force_buffer_close_all(
227fn force_buffer_close_all(
@@ -227,7 +234,7 @@ fn force_buffer_close_all(
227 }
234 }
228
235
229 let document_ids = buffer_gather_all_impl(cx.editor);
236 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)
231}
238}
232
239
233fn buffer_next(
240fn buffer_next(
@@ -261,39 +268,29 @@ fn write_impl(
261 path: Option<&Cow<str>>,
268 path: Option<&Cow<str>>,
262 force: bool,
269 force: bool,
263) -> anyhow::Result<()> {
270) -> anyhow::Result<()> {
264− let auto_format = cx.editor.config().auto_format;
271+ let editor_auto_fmt = cx.editor.config().auto_format;
265 let jobs = &mut cx.jobs;
272 let jobs = &mut cx.jobs;
266 let doc = doc_mut!(cx.editor);
273 let doc = doc_mut!(cx.editor);
274+ let path = path.map(AsRef::as_ref);
267
275
268− if let Some(ref path) = path {
276+ let fmt = if editor_auto_fmt {
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 doc.auto_format().map(|fmt| {
277 doc.auto_format().map(|fmt| {
277− let shared = fmt.shared();
278 let callback = make_format_callback(
278 let callback = make_format_callback(
279 doc.id(),
279 doc.id(),
280 doc.version(),
280 doc.version(),
281− Modified::SetUnmodified,
281+ fmt,
282− shared.clone(),
282+ Some((path.map(Into::into), force)),
283 );
283 );
284− jobs.callback(callback);
284+
285− shared
285+ jobs.add(Job::with_callback(callback).wait_before_exiting());
286 })
286 })
287 } else {
287 } else {
288 None
288 None
289 };
289 };
290− let future = doc.format_and_save(fmt, force);
291− cx.jobs.add(Job::new(future).wait_before_exiting());
292
290
293− if path.is_some() {
291+ if fmt.is_none() {
294 let id = doc.id();
292 let id = doc.id();
295− doc.detect_language(cx.editor.syn_loader.clone());
293+ cx.editor.save(id, path, force)?;
296− let _ = cx.editor.refresh_language_server(id);
297 }
294 }
298
295
299 Ok(())
296 Ok(())
@@ -348,8 +345,7 @@ fn format(
348
345
349 let doc = doc!(cx.editor);
346 let doc = doc!(cx.editor);
350 if let Some(format) = doc.format() {
347 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);
353 cx.jobs.callback(callback);
349 cx.jobs.callback(callback);
354 }
350 }
355
351
@@ -520,7 +516,7 @@ fn write_quit(
520 }
516 }
521
517
522 write_impl(cx, args.first(), false)?;
518 write_impl(cx, args.first(), false)?;
523− helix_lsp::block_on(cx.jobs.finish())?;
519+ cx.block_try_flush_writes()?;
524 quit(cx, &[], event)
520 quit(cx, &[], event)
525}
521}
526
522
@@ -534,6 +530,7 @@ fn force_write_quit(
534 }
530 }
535
531
536 write_impl(cx, args.first(), true)?;
532 write_impl(cx, args.first(), true)?;
533+ cx.block_try_flush_writes()?;
537 force_quit(cx, &[], event)
534 force_quit(cx, &[], event)
538}
535}
539
536
@@ -573,40 +570,50 @@ fn write_all_impl(
573 return Ok(());
570 return Ok(());
574 }
571 }
575
572
576− let mut errors = String::new();
573+ let mut errors: Vec<&'static str> = Vec::new();
577 let auto_format = cx.editor.config().auto_format;
574 let auto_format = cx.editor.config().auto_format;
578 let jobs = &mut cx.jobs;
575 let jobs = &mut cx.jobs;
576+
579 // save all documents
577 // save all documents
580− for doc in &mut cx.editor.documents.values_mut() {
578+ let saves: Vec<_> = cx
579+ .editor
580+ .documents
581+ .values()
582+ .filter_map(|doc| {
581− if doc.path().is_none() {
583+ if doc.path().is_none() {
582− errors.push_str("cannot write a buffer without a filename\n");
584+ errors.push("cannot write a buffer without a filename\n");
583− continue;
585+ return None;
584− }
586+ }
585
587
586− if !doc.is_modified() {
588+ if !doc.is_modified() {
587− continue;
589+ return None;
588− }
590+ }
589
591
590− let fmt = if auto_format {
592+ let fmt = if auto_format {
591− doc.auto_format().map(|fmt| {
593+ doc.auto_format().map(|fmt| {
592− let shared = fmt.shared();
593− let callback = make_format_callback(
594+ let callback =
594− doc.id(),
595+ make_format_callback(doc.id(), doc.version(), fmt, Some((None, force)));
595− doc.version(),
596+ jobs.add(Job::with_callback(callback).wait_before_exiting());
596− Modified::SetUnmodified,
597− shared.clone(),
598− );
599− jobs.callback(callback);
600− shared
601− })
597+ })
602− } else {
598+ } else {
599+ None
600+ };
601+
602+ if fmt.is_none() {
603+ return Some(doc.id());
604+ }
603 None
605 None
604− };
606+ })
605− let future = doc.format_and_save(fmt, force);
607+ .collect();
606− jobs.add(Job::new(future).wait_before_exiting());
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)?;
607 }
612 }
608
613
609 if quit {
614 if quit {
615+ cx.block_try_flush_writes()?;
616+
610 if !force {
617 if !force {
611 buffers_remaining_impl(cx.editor)?;
618 buffers_remaining_impl(cx.editor)?;
612 }
619 }
@@ -618,7 +625,11 @@ fn write_all_impl(
618 }
625 }
619 }
626 }
620
627
628+ if !errors.is_empty() && !force {
621− bail!(errors)
629+ bail!("{:?}", errors);
630+ }
631+
632+ Ok(())
622}
633}
623
634
624fn write_all(
635fn write_all(
@@ -680,6 +691,7 @@ fn quit_all(
680 return Ok(());
691 return Ok(());
681 }
692 }
682
693
694+ cx.block_try_flush_writes()?;
683 quit_all_impl(cx.editor, false)
695 quit_all_impl(cx.editor, false)
684}
696}
685
697
@@ -708,8 +720,9 @@ fn cquit(
708 .first()
720 .first()
709 .and_then(|code| code.parse::<i32>().ok())
721 .and_then(|code| code.parse::<i32>().ok())
710 .unwrap_or(1);
722 .unwrap_or(1);
711− cx.editor.exit_code = exit_code;
712
723
724+ cx.editor.exit_code = exit_code;
725+ cx.block_try_flush_writes()?;
713 quit_all_impl(cx.editor, false)
726 quit_all_impl(cx.editor, false)
714}
727}
715
728
@@ -1064,12 +1077,13 @@ fn tree_sitter_scopes(
1064 let contents = format!("```json\n{:?}\n````", scopes);
1077 let contents = format!("```json\n{:?}\n````", scopes);
1065
1078
1066 let callback = async move {
1079 let callback = async move {
1067− let call: job::Callback =
1080+ let call: job::Callback = Callback::EditorCompositor(Box::new(
1068− Box::new(move |editor: &mut Editor, compositor: &mut Compositor| {
1081+ move |editor: &mut Editor, compositor: &mut Compositor| {
1069 let contents = ui::Markdown::new(contents, editor.syn_loader.clone());
1082 let contents = ui::Markdown::new(contents, editor.syn_loader.clone());
1070 let popup = Popup::new("hover", contents).auto_close(true);
1083 let popup = Popup::new("hover", contents).auto_close(true);
1071 compositor.replace_or_push("hover", popup);
1084 compositor.replace_or_push("hover", popup);
1085+ },
1072− });
1086+ ));
1073 Ok(call)
1087 Ok(call)
1074 };
1088 };
1075
1089
@@ -1492,12 +1506,13 @@ fn tree_sitter_subtree(
1492 contents.push_str("\n```");
1506 contents.push_str("\n```");
1493
1507
1494 let callback = async move {
1508 let callback = async move {
1495− let call: job::Callback =
1509+ let call: job::Callback = Callback::EditorCompositor(Box::new(
1496− Box::new(move |editor: &mut Editor, compositor: &mut Compositor| {
1510+ move |editor: &mut Editor, compositor: &mut Compositor| {
1497 let contents = ui::Markdown::new(contents, editor.syn_loader.clone());
1511 let contents = ui::Markdown::new(contents, editor.syn_loader.clone());
1498 let popup = Popup::new("hover", contents).auto_close(true);
1512 let popup = Popup::new("hover", contents).auto_close(true);
1499 compositor.replace_or_push("hover", popup);
1513 compositor.replace_or_push("hover", popup);
1514+ },
1500− });
1515+ ));
1501 Ok(call)
1516 Ok(call)
1502 };
1517 };
1503
1518
@@ -1605,8 +1620,8 @@ fn run_shell_command(
1605
1620
1606 if !output.is_empty() {
1621 if !output.is_empty() {
1607 let callback = async move {
1622 let callback = async move {
1608− let call: job::Callback =
1623+ let call: job::Callback = Callback::EditorCompositor(Box::new(
1609− Box::new(move |editor: &mut Editor, compositor: &mut Compositor| {
1624+ move |editor: &mut Editor, compositor: &mut Compositor| {
1610 let contents = ui::Markdown::new(
1625 let contents = ui::Markdown::new(
1611 format!("```sh\n{}\n```", output),
1626 format!("```sh\n{}\n```", output),
1612 editor.syn_loader.clone(),
1627 editor.syn_loader.clone(),
@@ -1615,7 +1630,8 @@ fn run_shell_command(
1615 helix_core::Position::new(editor.cursor().0.unwrap_or_default().row, 2),
1630 helix_core::Position::new(editor.cursor().0.unwrap_or_default().row, 2),
1616 ));
1631 ));
1617 compositor.replace_or_push("shell", popup);
1632 compositor.replace_or_push("shell", popup);
1633+ },
1618− });
1634+ ));
1619 Ok(call)
1635 Ok(call)
1620 };
1636 };
1621
1637
helix-term/src/compositor.rs+10
@@ -27,6 +27,16 @@ pub struct Context<'a> {
27 pub jobs: &'a mut Jobs,
27 pub jobs: &'a mut Jobs,
28}
28}
29
29
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+
30pub trait Component: Any + AnyComponent {
40pub trait Component: Any + AnyComponent {
31 /// Process input events, return true if handled.
41 /// Process input events, return true if handled.
32 fn handle_event(&mut self, _event: &Event, _ctx: &mut Context) -> EventResult {
42 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;
5use futures_util::future::{BoxFuture, Future, FutureExt};
5use futures_util::future::{BoxFuture, Future, FutureExt};
6use futures_util::stream::{FuturesUnordered, StreamExt};
6use futures_util::stream::{FuturesUnordered, StreamExt};
7
7
8+pub enum Callback {
8−pub type Callback = Box<dyn FnOnce(&mut Editor, &mut Compositor) + Send>;
9+ EditorCompositor(Box<dyn FnOnce(&mut Editor, &mut Compositor) + Send>),
10+ Editor(Box<dyn FnOnce(&mut Editor) + Send>),
11+}
12+
9pub type JobFuture = BoxFuture<'static, anyhow::Result<Option<Callback>>>;
13pub type JobFuture = BoxFuture<'static, anyhow::Result<Option<Callback>>>;
10
14
11pub struct Job {
15pub struct Job {
@@ -68,9 +72,10 @@ impl Jobs {
68 ) {
72 ) {
69 match call {
73 match call {
70 Ok(None) => {}
74 Ok(None) => {}
71− Ok(Some(call)) => {
75+ Ok(Some(call)) => match call {
72− call(editor, compositor);
76+ Callback::EditorCompositor(call) => call(editor, compositor),
77+ Callback::Editor(call) => call(editor),
73− }
78+ },
74 Err(e) => {
79 Err(e) => {
75 editor.set_error(format!("Async job failed: {}", e));
80 editor.set_error(format!("Async job failed: {}", e));
76 }
81 }
@@ -93,13 +98,32 @@ impl Jobs {
93 }
98 }
94
99
95 /// Blocks until all the jobs that need to be waited on are done.
100 /// 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<()> {
97 log::debug!("waiting on jobs...");
106 log::debug!("waiting on jobs...");
98 let mut wait_futures = std::mem::take(&mut self.wait_futures);
107 let mut wait_futures = std::mem::take(&mut self.wait_futures);
108+
99 while let (Some(job), tail) = wait_futures.into_future().await {
109 while let (Some(job), tail) = wait_futures.into_future().await {
100 match job {
110 match job {
101− Ok(_) => {
111+ Ok(callback) => {
102 wait_futures = tail;
112 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+ }
103 }
127 }
104 Err(e) => {
128 Err(e) => {
105 self.wait_futures = tail;
129 self.wait_futures = tail;
helix-term/src/main.rs+11−1
@@ -139,8 +139,18 @@ FLAGS:
139 Err(err) => return Err(Error::new(err)),
139 Err(err) => return Err(Error::new(err)),
140 };
140 };
141
141
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+
142 // TODO: use the thread local executor to spawn the application task separately from the work pool
151 // TODO: use the thread local executor to spawn the application task separately from the work pool
152+ let mut app = Application::new(args, config, syn_loader_conf)
143− let mut app = Application::new(args, config).context("unable to create new application")?;
153+ .context("unable to create new application")?;
144
154
145 let exit_code = app.run(&mut EventStream::new()).await?;
155 let exit_code = app.run(&mut EventStream::new()).await?;
146
156
helix-term/src/ui/editor.rs+6−4
@@ -1,7 +1,8 @@
1use crate::{
1use crate::{
2 commands,
2 commands,
3 compositor::{Component, Context, Event, EventResult},
3 compositor::{Component, Context, Event, EventResult},
4+ job::{self, Callback},
4− job, key,
5+ key,
5 keymap::{KeymapResult, Keymaps},
6 keymap::{KeymapResult, Keymaps},
6 ui::{Completion, ProgressSpinners},
7 ui::{Completion, ProgressSpinners},
7};
8};
@@ -944,9 +945,10 @@ impl EditorView {
944
945
945 // TODO: Use an on_mode_change hook to remove signature help
946 // TODO: Use an on_mode_change hook to remove signature help
946 cxt.jobs.callback(async {
947 cxt.jobs.callback(async {
947− let call: job::Callback = Box::new(|_editor, compositor| {
948+ let call: job::Callback =
949+ Callback::EditorCompositor(Box::new(|_editor, compositor| {
948− compositor.remove(SignatureHelp::ID);
950+ compositor.remove(SignatureHelp::ID);
949− });
951+ }));
950 Ok(call)
952 Ok(call)
951 });
953 });
952 }
954 }
helix-term/src/ui/mod.rs+3−3
@@ -14,7 +14,7 @@ mod statusline;
14mod text;
14mod text;
15
15
16use crate::compositor::{Component, Compositor};
16use crate::compositor::{Component, Compositor};
17−use crate::job;
17+use crate::job::{self, Callback};
18pub use completion::Completion;
18pub use completion::Completion;
19pub use editor::EditorView;
19pub use editor::EditorView;
20pub use markdown::Markdown;
20pub use markdown::Markdown;
@@ -121,7 +121,7 @@ pub fn regex_prompt(
121
121
122 if event == PromptEvent::Validate {
122 if event == PromptEvent::Validate {
123 let callback = async move {
123 let callback = async move {
124− let call: job::Callback = Box::new(
124+ let call: job::Callback = Callback::EditorCompositor(Box::new(
125 move |_editor: &mut Editor, compositor: &mut Compositor| {
125 move |_editor: &mut Editor, compositor: &mut Compositor| {
126 let contents = Text::new(format!("{}", err));
126 let contents = Text::new(format!("{}", err));
127 let size = compositor.size();
127 let size = compositor.size();
@@ -135,7 +135,7 @@ pub fn regex_prompt(
135
135
136 compositor.replace_or_push("invalid-regex", popup);
136 compositor.replace_or_push("invalid-regex", popup);
137 },
137 },
138− );
138+ ));
139 Ok(call)
139 Ok(call)
140 };
140 };
141
141
helix-term/tests/integration.rs+1
@@ -22,5 +22,6 @@ mod test {
22 mod commands;
22 mod commands;
23 mod movement;
23 mod movement;
24 mod prompt;
24 mod prompt;
25+ mod splits;
25 mod write;
26 mod write;
26}
27}
helix-term/tests/test/auto_indent.rs+1
@@ -8,6 +8,7 @@ async fn auto_indent_c() -> anyhow::Result<()> {
8 ..Default::default()
8 ..Default::default()
9 },
9 },
10 Config::default(),
10 Config::default(),
11+ helpers::test_syntax_conf(None),
11 // switches to append mode?
12 // switches to append mode?
12 (
13 (
13 helpers::platform_line("void foo() {#[|}]#").as_ref(),
14 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<()> {
13 },
13 },
14 ..Default::default()
14 ..Default::default()
15 },
15 },
16+ helpers::test_syntax_conf(None),
16 ("#[\n|]#", "i(<esc>", "(#[|\n]#"),
17 ("#[\n|]#", "i(<esc>", "(#[|\n]#"),
17 )
18 )
18 .await?;
19 .await?;
helix-term/tests/test/commands.rs+19−17
@@ -1,21 +1,25 @@
1−use std::{
2− io::{Read, Write},
3− ops::RangeInclusive,
1+use std::ops::RangeInclusive;
4−};
5
2
6use helix_core::diagnostic::Severity;
3use helix_core::diagnostic::Severity;
7−use helix_term::application::Application;
8
4
9use super::*;
5use super::*;
10
6
11−#[tokio::test]
7+#[tokio::test(flavor = "multi_thread")]
12async fn test_write_quit_fail() -> anyhow::Result<()> {
8async fn test_write_quit_fail() -> anyhow::Result<()> {
13 let file = helpers::new_readonly_tempfile()?;
9 let file = helpers::new_readonly_tempfile()?;
10+ let mut app = helpers::AppBuilder::new()
11+ .with_file(file.path(), None)
12+ .build()?;
14
13
15 test_key_sequence(
14 test_key_sequence(
16− &mut helpers::app_with_file(file.path())?,
15+ &mut app,
17 Some("ihello<esc>:wq<ret>"),
16 Some("ihello<esc>:wq<ret>"),
18 Some(&|app| {
17 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));
19 assert_eq!(&Severity::Error, app.editor.get_status().unwrap().1);
23 assert_eq!(&Severity::Error, app.editor.get_status().unwrap().1);
20 }),
24 }),
21 false,
25 false,
@@ -25,11 +29,10 @@ async fn test_write_quit_fail() -> anyhow::Result<()> {
25 Ok(())
29 Ok(())
26}
30}
27
31
28−#[tokio::test]
32+#[tokio::test(flavor = "multi_thread")]
29−#[ignore]
30async fn test_buffer_close_concurrent() -> anyhow::Result<()> {
33async fn test_buffer_close_concurrent() -> anyhow::Result<()> {
31 test_key_sequences(
34 test_key_sequences(
32− &mut Application::new(Args::default(), Config::default())?,
35+ &mut helpers::AppBuilder::new().build()?,
33 vec![
36 vec![
34 (
37 (
35 None,
38 None,
@@ -69,8 +72,12 @@ async fn test_buffer_close_concurrent() -> anyhow::Result<()> {
69
72
70 command.push_str(":buffer<minus>close<ret>");
73 command.push_str(":buffer<minus>close<ret>");
71
74
75+ let mut app = helpers::AppBuilder::new()
76+ .with_file(file.path(), None)
77+ .build()?;
78+
72 test_key_sequence(
79 test_key_sequence(
73− &mut helpers::app_with_file(file.path())?,
80+ &mut app,
74 Some(&command),
81 Some(&command),
75 Some(&|app| {
82 Some(&|app| {
76 assert!(!app.editor.is_err(), "error: {:?}", app.editor.get_status());
83 assert!(!app.editor.is_err(), "error: {:?}", app.editor.get_status());
@@ -82,12 +89,7 @@ async fn test_buffer_close_concurrent() -> anyhow::Result<()> {
82 )
89 )
83 .await?;
90 .await?;
84
91
85− file.as_file_mut().flush()?;
92+ helpers::assert_file_has_content(file.as_file_mut(), &RANGE.end().to_string())?;
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);
91
93
92 Ok(())
94 Ok(())
93}
95}
helix-term/tests/test/helpers.rs+117−19
@@ -1,10 +1,15 @@
1+use std::{
2+ fs::File,
3+ io::{Read, Write},
4+ path::PathBuf,
1−use std::{io::Write, path::PathBuf, time::Duration};
5+ time::Duration,
6+};
2
7
3use anyhow::bail;
8use anyhow::bail;
4use crossterm::event::{Event, KeyEvent};
9use crossterm::event::{Event, KeyEvent};
5−use helix_core::{test, Selection, Transaction};
10+use helix_core::{diagnostic::Severity, test, Selection, Transaction};
6use helix_term::{application::Application, args::Args, config::Config};
11use 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};
8use tempfile::NamedTempFile;
13use tempfile::NamedTempFile;
9use tokio_stream::wrappers::UnboundedReceiverStream;
14use tokio_stream::wrappers::UnboundedReceiverStream;
10
15
@@ -56,7 +61,9 @@ pub async fn test_key_sequences(
56 for (i, (in_keys, test_fn)) in inputs.into_iter().enumerate() {
61 for (i, (in_keys, test_fn)) in inputs.into_iter().enumerate() {
57 if let Some(in_keys) = in_keys {
62 if let Some(in_keys) = in_keys {
58 for key_event in parse_macro(in_keys)?.into_iter() {
63 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))?;
60 }
67 }
61 }
68 }
62
69
@@ -70,7 +77,7 @@ pub async fn test_key_sequences(
70 // verify if it exited on the last iteration if it should have and
77 // verify if it exited on the last iteration if it should have and
71 // the inverse
78 // the inverse
72 if i == num_inputs - 1 && app_exited != should_exit {
79 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);
74 }
81 }
75
82
76 if let Some(test) = test_fn {
83 if let Some(test) = test_fn {
@@ -87,7 +94,17 @@ pub async fn test_key_sequences(
87 tokio::time::timeout(TIMEOUT, event_loop).await?;
94 tokio::time::timeout(TIMEOUT, event_loop).await?;
88 }
95 }
89
96
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+ }
91
108
92 Ok(())
109 Ok(())
93}
110}
@@ -101,7 +118,7 @@ pub async fn test_key_sequence_with_input_text<T: Into<TestCase>>(
101 let test_case = test_case.into();
118 let test_case = test_case.into();
102 let mut app = match app {
119 let mut app = match app {
103 Some(app) => app,
120 Some(app) => app,
104− None => Application::new(Args::default(), Config::default())?,
121+ None => Application::new(Args::default(), Config::default(), test_syntax_conf(None))?,
105 };
122 };
106
123
107 let (view, doc) = helix_view::current!(app.editor);
124 let (view, doc) = helix_view::current!(app.editor);
@@ -125,16 +142,30 @@ pub async fn test_key_sequence_with_input_text<T: Into<TestCase>>(
125 .await
142 .await
126}
143}
127
144
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+
128/// Use this for very simple test cases where there is one input
158/// Use this for very simple test cases where there is one input
129/// document, selection, and sequence of key presses, and you just
159/// document, selection, and sequence of key presses, and you just
130/// want to verify the resulting document and selection.
160/// want to verify the resulting document and selection.
131pub async fn test_with_config<T: Into<TestCase>>(
161pub async fn test_with_config<T: Into<TestCase>>(
132 args: Args,
162 args: Args,
133 config: Config,
163 config: Config,
164+ syn_conf: helix_core::syntax::Configuration,
134 test_case: T,
165 test_case: T,
135) -> anyhow::Result<()> {
166) -> anyhow::Result<()> {
136 let test_case = test_case.into();
167 let test_case = test_case.into();
137− let app = Application::new(args, config)?;
168+ let app = Application::new(args, config, syn_conf)?;
138
169
139 test_key_sequence_with_input_text(
170 test_key_sequence_with_input_text(
140 Some(app),
171 Some(app),
@@ -155,7 +186,13 @@ pub async fn test_with_config<T: Into<TestCase>>(
155}
186}
156
187
157pub async fn test<T: Into<TestCase>>(test_case: T) -> anyhow::Result<()> {
188pub 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
159}
196}
160
197
161pub fn temp_file_with_contents<S: AsRef<str>>(
198pub fn temp_file_with_contents<S: AsRef<str>>(
@@ -200,14 +237,75 @@ pub fn new_readonly_tempfile() -> anyhow::Result<NamedTempFile> {
200 Ok(file)
237 Ok(file)
201}
238}
202
239
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()
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()
210− },
251+ }
211− Config::default(),
252+
212− )
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+ }
213}
311}
helix-term/tests/test/movement.rs+7−1
@@ -70,7 +70,9 @@ async fn insert_to_normal_mode_cursor_position() -> anyhow::Result<()> {
70async fn cursor_position_newly_opened_file() -> anyhow::Result<()> {
70async fn cursor_position_newly_opened_file() -> anyhow::Result<()> {
71 let test = |content: &str, expected_sel: Selection| -> anyhow::Result<()> {
71 let test = |content: &str, expected_sel: Selection| -> anyhow::Result<()> {
72 let file = helpers::temp_file_with_contents(content)?;
72 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()?;
74
76
75 let (view, doc) = helix_view::current!(app.editor);
77 let (view, doc) = helix_view::current!(app.editor);
76 let sel = doc.selection(view.id).clone();
78 let sel = doc.selection(view.id).clone();
@@ -115,6 +117,7 @@ async fn select_mode_tree_sitter_next_function_is_union_of_objects() -> anyhow::
115 ..Default::default()
117 ..Default::default()
116 },
118 },
117 Config::default(),
119 Config::default(),
120+ helpers::test_syntax_conf(None),
118 (
121 (
119 helpers::platform_line(indoc! {"\
122 helpers::platform_line(indoc! {"\
120 #[/|]#// Increments
123 #[/|]#// Increments
@@ -146,6 +149,7 @@ async fn select_mode_tree_sitter_prev_function_unselects_object() -> anyhow::Res
146 ..Default::default()
149 ..Default::default()
147 },
150 },
148 Config::default(),
151 Config::default(),
152+ helpers::test_syntax_conf(None),
149 (
153 (
150 helpers::platform_line(indoc! {"\
154 helpers::platform_line(indoc! {"\
151 /// Increments
155 /// Increments
@@ -178,6 +182,7 @@ async fn select_mode_tree_sitter_prev_function_goes_backwards_to_object() -> any
178 ..Default::default()
182 ..Default::default()
179 },
183 },
180 Config::default(),
184 Config::default(),
185+ helpers::test_syntax_conf(None),
181 (
186 (
182 helpers::platform_line(indoc! {"\
187 helpers::platform_line(indoc! {"\
183 /// Increments
188 /// Increments
@@ -208,6 +213,7 @@ async fn select_mode_tree_sitter_prev_function_goes_backwards_to_object() -> any
208 ..Default::default()
213 ..Default::default()
209 },
214 },
210 Config::default(),
215 Config::default(),
216+ helpers::test_syntax_conf(None),
211 (
217 (
212 helpers::platform_line(indoc! {"\
218 helpers::platform_line(indoc! {"\
213 /// Increments
219 /// Increments
helix-term/tests/test/prompt.rs+1−3
@@ -1,11 +1,9 @@
1use super::*;
1use super::*;
2
2
3−use helix_term::application::Application;
4−
5#[tokio::test]
3#[tokio::test]
6async fn test_history_completion() -> anyhow::Result<()> {
4async fn test_history_completion() -> anyhow::Result<()> {
7 test_key_sequence(
5 test_key_sequence(
8− &mut Application::new(Args::default(), Config::default())?,
6+ &mut AppBuilder::new().build()?,
9 Some(":asdf<ret>:theme d<C-n><tab>"),
7 Some(":asdf<ret>:theme d<C-n><tab>"),
10 Some(&|app| {
8 Some(&|app| {
11 assert!(!app.editor.is_err());
9 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::{
4};
4};
5
5
6use helix_core::diagnostic::Severity;
6use helix_core::diagnostic::Severity;
7−use helix_term::application::Application;
8use helix_view::doc;
7use helix_view::doc;
9
8
10use super::*;
9use super::*;
@@ -12,9 +11,12 @@ use super::*;
12#[tokio::test]
11#[tokio::test]
13async fn test_write() -> anyhow::Result<()> {
12async fn test_write() -> anyhow::Result<()> {
14 let mut file = tempfile::NamedTempFile::new()?;
13 let mut file = tempfile::NamedTempFile::new()?;
14+ let mut app = helpers::AppBuilder::new()
15+ .with_file(file.path(), None)
16+ .build()?;
15
17
16 test_key_sequence(
18 test_key_sequence(
17− &mut helpers::app_with_file(file.path())?,
19+ &mut app,
18 Some("ithe gostak distims the doshes<ret><esc>:w<ret>"),
20 Some("ithe gostak distims the doshes<ret><esc>:w<ret>"),
19 None,
21 None,
20 false,
22 false,
@@ -35,12 +37,15 @@ async fn test_write() -> anyhow::Result<()> {
35 Ok(())
37 Ok(())
36}
38}
37
39
38−#[tokio::test]
40+#[tokio::test(flavor = "multi_thread")]
39async fn test_write_quit() -> anyhow::Result<()> {
41async fn test_write_quit() -> anyhow::Result<()> {
40 let mut file = tempfile::NamedTempFile::new()?;
42 let mut file = tempfile::NamedTempFile::new()?;
43+ let mut app = helpers::AppBuilder::new()
44+ .with_file(file.path(), None)
45+ .build()?;
41
46
42 test_key_sequence(
47 test_key_sequence(
43− &mut helpers::app_with_file(file.path())?,
48+ &mut app,
44 Some("ithe gostak distims the doshes<ret><esc>:wq<ret>"),
49 Some("ithe gostak distims the doshes<ret><esc>:wq<ret>"),
45 None,
50 None,
46 true,
51 true,
@@ -61,25 +66,21 @@ async fn test_write_quit() -> anyhow::Result<()> {
61 Ok(())
66 Ok(())
62}
67}
63
68
64−#[tokio::test]
69+#[tokio::test(flavor = "multi_thread")]
65−#[ignore]
66async fn test_write_concurrent() -> anyhow::Result<()> {
70async fn test_write_concurrent() -> anyhow::Result<()> {
67 let mut file = tempfile::NamedTempFile::new()?;
71 let mut file = tempfile::NamedTempFile::new()?;
68 let mut command = String::new();
72 let mut command = String::new();
69 const RANGE: RangeInclusive<i32> = 1..=5000;
73 const RANGE: RangeInclusive<i32> = 1..=5000;
74+ let mut app = helpers::AppBuilder::new()
75+ .with_file(file.path(), None)
76+ .build()?;
70
77
71 for i in RANGE {
78 for i in RANGE {
72 let cmd = format!("%c{}<esc>:w<ret>", i);
79 let cmd = format!("%c{}<esc>:w<ret>", i);
73 command.push_str(&cmd);
80 command.push_str(&cmd);
74 }
81 }
75
82
76− test_key_sequence(
83+ test_key_sequence(&mut app, Some(&command), None, false).await?;
77− &mut helpers::app_with_file(file.path())?,
78− Some(&command),
79− None,
80− false,
81− )
82− .await?;
83
84
84 file.as_file_mut().flush()?;
85 file.as_file_mut().flush()?;
85 file.as_file_mut().sync_all()?;
86 file.as_file_mut().sync_all()?;
@@ -92,12 +93,14 @@ async fn test_write_concurrent() -> anyhow::Result<()> {
92}
93}
93
94
94#[tokio::test]
95#[tokio::test]
95−#[ignore]
96async fn test_write_fail_mod_flag() -> anyhow::Result<()> {
96async fn test_write_fail_mod_flag() -> anyhow::Result<()> {
97 let file = helpers::new_readonly_tempfile()?;
97 let file = helpers::new_readonly_tempfile()?;
98+ let mut app = helpers::AppBuilder::new()
99+ .with_file(file.path(), None)
100+ .build()?;
98
101
99 test_key_sequences(
102 test_key_sequences(
100− &mut helpers::app_with_file(file.path())?,
103+ &mut app,
101 vec![
104 vec![
102 (
105 (
103 None,
106 None,
@@ -131,12 +134,127 @@ async fn test_write_fail_mod_flag() -> anyhow::Result<()> {
131}
134}
132
135
133#[tokio::test]
136#[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]
135async fn test_write_fail_new_path() -> anyhow::Result<()> {
253async fn test_write_fail_new_path() -> anyhow::Result<()> {
136 let file = helpers::new_readonly_tempfile()?;
254 let file = helpers::new_readonly_tempfile()?;
137
255
138 test_key_sequences(
256 test_key_sequences(
139− &mut Application::new(Args::default(), Config::default())?,
257+ &mut AppBuilder::new().build()?,
140 vec![
258 vec![
141 (
259 (
142 None,
260 None,
helix-view/src/document.rs+98−45
@@ -83,6 +83,18 @@ impl Serialize for Mode {
83 }
83 }
84}
84}
85
85
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+
86pub struct Document {
98pub struct Document {
87 pub(crate) id: DocumentId,
99 pub(crate) id: DocumentId,
88 text: Rope,
100 text: Rope,
@@ -492,45 +504,61 @@ impl Document {
492 Some(fut.boxed())
504 Some(fut.boxed())
493 }
505 }
494
506
495− pub fn save(&mut self, force: bool) -> impl Future<Output = Result<(), anyhow::Error>> {
507+ pub fn save<P: Into<PathBuf>>(
496− self.save_impl::<futures_util::future::Ready<_>>(None, force)
497− }
498−
499− pub fn format_and_save(
500 &mut self,
508 &mut self,
501− formatting: Option<impl Future<Output = Result<Transaction, FormatterError>>>,
509+ path: Option<P>,
502 force: bool,
510 force: bool,
511+ ) -> Result<
503− ) -> impl Future<Output = anyhow::Result<()>> {
512+ impl Future<Output = Result<DocumentSavedEvent, anyhow::Error>> + 'static + Send,
513+ anyhow::Error,
514+ > {
515+ let path = path.map(|path| path.into());
504− self.save_impl(formatting, force)
516+ self.save_impl(path, force)
517+
518+ // futures_util::future::Ready<_>,
505 }
519 }
506
520
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
509 /// The `Document`'s text is encoded according to its encoding and written to the file located
521 /// The `Document`'s text is encoded according to its encoding and written to the file located
510 /// at its `path()`.
522 /// at its `path()`.
511− ///
523+ fn save_impl(
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>>>(
514 &mut self,
524 &mut self,
515− formatting: Option<F>,
525+ path: Option<PathBuf>,
516 force: bool,
526 force: bool,
527+ ) -> Result<
517− ) -> impl Future<Output = Result<(), anyhow::Error>> {
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+
518 // we clone and move text + path into the future so that we asynchronously save the current
536 // we clone and move text + path into the future so that we asynchronously save the current
519 // state without blocking any further edits.
537 // state without blocking any further edits.
538+ let text = self.text().clone();
520
539
521− let mut text = self.text().clone();
540+ let path = match path {
541+ Some(path) => helix_core::path::get_canonicalized_path(&path)?,
542+ None => {
543+ if self.path.is_none() {
522− let path = self.path.clone().expect("Can't save with no path set!");
544+ bail!("Can't save with no path set!");
523− let identifier = self.identifier();
545+ }
524
546
547+ self.path.as_ref().unwrap().clone()
548+ }
549+ };
550+
551+ let identifier = self.path().map(|_| self.identifier());
525 let language_server = self.language_server.clone();
552 let language_server = self.language_server.clone();
526
553
527 // mark changes up to now as saved
554 // 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();
529
557
530 let encoding = self.encoding;
558 let encoding = self.encoding;
531
559
532 // We encode the file according to the `Document`'s encoding.
560 // We encode the file according to the `Document`'s encoding.
533− async move {
561+ let future = async move {
534 use tokio::fs::File;
562 use tokio::fs::File;
535 if let Some(parent) = path.parent() {
563 if let Some(parent) = path.parent() {
536 // TODO: display a prompt asking the user if the directories should be created
564 // TODO: display a prompt asking the user if the directories should be created
@@ -543,39 +571,34 @@ impl Document {
543 }
571 }
544 }
572 }
545
573
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?;
564 to_writer(&mut file, encoding, &text).await?;
575 to_writer(&mut file, encoding, &text).await?;
565
576
577+ let event = DocumentSavedEvent {
578+ revision: current_rev,
579+ doc_id,
580+ path,
581+ text: text.clone(),
582+ };
583+
566 if let Some(language_server) = language_server {
584 if let Some(language_server) = language_server {
567 if !language_server.is_initialized() {
585 if !language_server.is_initialized() {
568− return Ok(());
586+ return Ok(event);
569 }
587 }
570− if let Some(notification) =
588+
589+ if let Some(identifier) = identifier {
590+ if let Some(notification) =
571− language_server.text_document_did_save(identifier, &text)
591+ language_server.text_document_did_save(identifier, &text)
572− {
592+ {
573− notification.await?;
593+ notification.await?;
594+ }
574 }
595 }
575 }
596 }
576
597
577− Ok(())
598+ Ok(event)
578− }
599+ };
600+
601+ Ok(future)
579 }
602 }
580
603
581 /// Detect the programming language based on the file type.
604 /// Detect the programming language based on the file type.
@@ -930,6 +953,12 @@ impl Document {
930 let history = self.history.take();
953 let history = self.history.take();
931 let current_revision = history.current_revision();
954 let current_revision = history.current_revision();
932 self.history.set(history);
955 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+ );
933 current_revision != self.last_saved_revision || !self.changes.is_empty()
962 current_revision != self.last_saved_revision || !self.changes.is_empty()
934 }
963 }
935
964
@@ -941,6 +970,30 @@ impl Document {
941 self.last_saved_revision = current_revision;
970 self.last_saved_revision = current_revision;
942 }
971 }
943
972
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+
944 /// Corresponding language scope name. Usually `source.<lang>`.
997 /// Corresponding language scope name. Usually `source.<lang>`.
945 pub fn language_scope(&self) -> Option<&str> {
998 pub fn language_scope(&self) -> Option<&str> {
946 self.language
999 self.language
helix-view/src/editor.rs+124−16
@@ -1,6 +1,6 @@
1use crate::{
1use crate::{
2 clipboard::{get_clipboard_provider, ClipboardProvider},
2 clipboard::{get_clipboard_provider, ClipboardProvider},
3− document::Mode,
3+ document::{DocumentSavedEventFuture, DocumentSavedEventResult, Mode},
4 graphics::{CursorKind, Rect},
4 graphics::{CursorKind, Rect},
5 info::Info,
5 info::Info,
6 input::KeyEvent,
6 input::KeyEvent,
@@ -9,8 +9,9 @@ use crate::{
9 Document, DocumentId, View, ViewId,
9 Document, DocumentId, View, ViewId,
10};
10};
11
11
12−use futures_util::future;
13use futures_util::stream::select_all::SelectAll;
12use futures_util::stream::select_all::SelectAll;
13+use futures_util::{future, StreamExt};
14+use helix_lsp::Call;
14use tokio_stream::wrappers::UnboundedReceiverStream;
15use tokio_stream::wrappers::UnboundedReceiverStream;
15
16
16use std::{
17use std::{
@@ -28,7 +29,7 @@ use tokio::{
28 time::{sleep, Duration, Instant, Sleep},
29 time::{sleep, Duration, Instant, Sleep},
29};
30};
30
31
31−use anyhow::Error;
32+use anyhow::{anyhow, bail, Error};
32
33
33pub use helix_core::diagnostic::Severity;
34pub use helix_core::diagnostic::Severity;
34pub use helix_core::register::Registers;
35pub use helix_core::register::Registers;
@@ -65,7 +66,7 @@ where
65 )
66 )
66}
67}
67
68
68−#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
69+#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
69#[serde(rename_all = "kebab-case", default, deny_unknown_fields)]
70#[serde(rename_all = "kebab-case", default, deny_unknown_fields)]
70pub struct FilePickerConfig {
71pub struct FilePickerConfig {
71 /// IgnoreOptions
72 /// IgnoreOptions
@@ -172,7 +173,7 @@ pub struct Config {
172 pub color_modes: bool,
173 pub color_modes: bool,
173}
174}
174
175
175−#[derive(Debug, Default, Clone, PartialEq, Serialize, Deserialize)]
176+#[derive(Debug, Default, Clone, PartialEq, Eq, Serialize, Deserialize)]
176#[serde(default, rename_all = "kebab-case", deny_unknown_fields)]
177#[serde(default, rename_all = "kebab-case", deny_unknown_fields)]
177pub struct TerminalConfig {
178pub struct TerminalConfig {
178 pub command: String,
179 pub command: String,
@@ -225,7 +226,7 @@ pub fn get_terminal_provider() -> Option<TerminalConfig> {
225 None
226 None
226}
227}
227
228
228−#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
229+#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
229#[serde(default, rename_all = "kebab-case", deny_unknown_fields)]
230#[serde(default, rename_all = "kebab-case", deny_unknown_fields)]
230pub struct LspConfig {
231pub struct LspConfig {
231 /// Display LSP progress messages below statusline
232 /// Display LSP progress messages below statusline
@@ -246,7 +247,7 @@ impl Default for LspConfig {
246 }
247 }
247}
248}
248
249
249−#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
250+#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
250#[serde(rename_all = "kebab-case", default, deny_unknown_fields)]
251#[serde(rename_all = "kebab-case", default, deny_unknown_fields)]
251pub struct SearchConfig {
252pub struct SearchConfig {
252 /// Smart case: Case insensitive searching unless pattern contains upper case characters. Defaults to true.
253 /// Smart case: Case insensitive searching unless pattern contains upper case characters. Defaults to true.
@@ -255,7 +256,7 @@ pub struct SearchConfig {
255 pub wrap_around: bool,
256 pub wrap_around: bool,
256}
257}
257
258
258−#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
259+#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
259#[serde(rename_all = "kebab-case", default, deny_unknown_fields)]
260#[serde(rename_all = "kebab-case", default, deny_unknown_fields)]
260pub struct StatusLineConfig {
261pub struct StatusLineConfig {
261 pub left: Vec<StatusLineElement>,
262 pub left: Vec<StatusLineElement>,
@@ -279,7 +280,7 @@ impl Default for StatusLineConfig {
279 }
280 }
280}
281}
281
282
282−#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
283+#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
283#[serde(rename_all = "kebab-case", default, deny_unknown_fields)]
284#[serde(rename_all = "kebab-case", default, deny_unknown_fields)]
284pub struct ModeConfig {
285pub struct ModeConfig {
285 pub normal: String,
286 pub normal: String,
@@ -458,7 +459,7 @@ impl std::str::FromStr for GutterType {
458 }
459 }
459}
460}
460
461
461−#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
462+#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
462#[serde(default)]
463#[serde(default)]
463pub struct WhitespaceConfig {
464pub struct WhitespaceConfig {
464 pub render: WhitespaceRender,
465 pub render: WhitespaceRender,
@@ -643,12 +644,21 @@ pub struct Breakpoint {
643 pub log_message: Option<String>,
644 pub log_message: Option<String>,
644}
645}
645
646
647+use futures_util::stream::{Flatten, Once};
648+
646pub struct Editor {
649pub struct Editor {
647 /// Current editing mode.
650 /// Current editing mode.
648 pub mode: Mode,
651 pub mode: Mode,
649 pub tree: Tree,
652 pub tree: Tree,
650 pub next_document_id: DocumentId,
653 pub next_document_id: DocumentId,
651 pub documents: BTreeMap<DocumentId, Document>,
654 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+
652 pub count: Option<std::num::NonZeroUsize>,
662 pub count: Option<std::num::NonZeroUsize>,
653 pub selected_register: Option<char>,
663 pub selected_register: Option<char>,
654 pub registers: Registers,
664 pub registers: Registers,
@@ -688,6 +698,15 @@ pub struct Editor {
688 pub config_events: (UnboundedSender<ConfigEvent>, UnboundedReceiver<ConfigEvent>),
698 pub config_events: (UnboundedSender<ConfigEvent>, UnboundedReceiver<ConfigEvent>),
689}
699}
690
700
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+
691#[derive(Debug, Clone)]
710#[derive(Debug, Clone)]
692pub enum ConfigEvent {
711pub enum ConfigEvent {
693 Refresh,
712 Refresh,
@@ -719,6 +738,8 @@ pub enum CloseError {
719 DoesNotExist,
738 DoesNotExist,
720 /// Buffer is modified
739 /// Buffer is modified
721 BufferModified(String),
740 BufferModified(String),
741+ /// Document failed to save
742+ SaveError(anyhow::Error),
722}
743}
723
744
724impl Editor {
745impl Editor {
@@ -739,6 +760,9 @@ impl Editor {
739 tree: Tree::new(area),
760 tree: Tree::new(area),
740 next_document_id: DocumentId::default(),
761 next_document_id: DocumentId::default(),
741 documents: BTreeMap::new(),
762 documents: BTreeMap::new(),
763+ saves: HashMap::new(),
764+ save_queue: SelectAll::new(),
765+ write_count: 0,
742 count: None,
766 count: None,
743 selected_register: None,
767 selected_register: None,
744 macro_recording: None,
768 macro_recording: None,
@@ -804,12 +828,16 @@ impl Editor {
804
828
805 #[inline]
829 #[inline]
806 pub fn set_status<T: Into<Cow<'static, str>>>(&mut self, status: T) {
830 pub fn set_status<T: Into<Cow<'static, str>>>(&mut self, status: T) {
831+ let status = status.into();
832+ log::debug!("editor status: {}", status);
807− self.status_msg = Some((status.into(), Severity::Info));
833+ self.status_msg = Some((status, Severity::Info));
808 }
834 }
809
835
810 #[inline]
836 #[inline]
811 pub fn set_error<T: Into<Cow<'static, str>>>(&mut self, error: T) {
837 pub fn set_error<T: Into<Cow<'static, str>>>(&mut self, error: T) {
838+ let error = error.into();
839+ log::error!("editor error: {}", error);
812− self.status_msg = Some((error.into(), Severity::Error));
840+ self.status_msg = Some((error, Severity::Error));
813 }
841 }
814
842
815 #[inline]
843 #[inline]
@@ -1034,6 +1062,13 @@ impl Editor {
1034 DocumentId(unsafe { NonZeroUsize::new_unchecked(self.next_document_id.0.get() + 1) });
1062 DocumentId(unsafe { NonZeroUsize::new_unchecked(self.next_document_id.0.get() + 1) });
1035 doc.id = id;
1063 doc.id = id;
1036 self.documents.insert(id, doc);
1064 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+
1037 id
1072 id
1038 }
1073 }
1039
1074
@@ -1080,16 +1115,19 @@ impl Editor {
1080 }
1115 }
1081
1116
1082 pub fn close_document(&mut self, doc_id: DocumentId, force: bool) -> Result<(), CloseError> {
1117 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) {
1084 Some(doc) => doc,
1119 Some(doc) => doc,
1085 None => return Err(CloseError::DoesNotExist),
1120 None => return Err(CloseError::DoesNotExist),
1086 };
1121 };
1087−
1088 if !force && doc.is_modified() {
1122 if !force && doc.is_modified() {
1089 return Err(CloseError::BufferModified(doc.display_name().into_owned()));
1123 return Err(CloseError::BufferModified(doc.display_name().into_owned()));
1090 }
1124 }
1091
1125
1126+ // This will also disallow any follow-up writes
1127+ self.saves.remove(&doc_id);
1128+
1092 if let Some(language_server) = doc.language_server() {
1129 if let Some(language_server) = doc.language_server() {
1130+ // TODO: track error
1093 tokio::spawn(language_server.text_document_did_close(doc.identifier()));
1131 tokio::spawn(language_server.text_document_did_close(doc.identifier()));
1094 }
1132 }
1095
1133
@@ -1152,6 +1190,32 @@ impl Editor {
1152 Ok(())
1190 Ok(())
1153 }
1191 }
1154
1192
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+
1155 pub fn resize(&mut self, area: Rect) {
1219 pub fn resize(&mut self, area: Rect) {
1156 if self.tree.resize(area) {
1220 if self.tree.resize(area) {
1157 self._refresh();
1221 self._refresh();
@@ -1252,14 +1316,14 @@ impl Editor {
1252 }
1316 }
1253 }
1317 }
1254
1318
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
1256 /// `timeout` parameter to override this.
1320 /// `timeout` parameter to override this.
1257 pub async fn close_language_servers(
1321 pub async fn close_language_servers(
1258 &self,
1322 &self,
1259 timeout: Option<u64>,
1323 timeout: Option<u64>,
1260 ) -> Result<(), tokio::time::error::Elapsed> {
1324 ) -> Result<(), tokio::time::error::Elapsed> {
1261 tokio::time::timeout(
1325 tokio::time::timeout(
1262− Duration::from_millis(timeout.unwrap_or(500)),
1326+ Duration::from_millis(timeout.unwrap_or(3000)),
1263 future::join_all(
1327 future::join_all(
1264 self.language_servers
1328 self.language_servers
1265 .iter_clients()
1329 .iter_clients()
@@ -1269,4 +1333,48 @@ impl Editor {
1269 .await
1333 .await
1270 .map(|_| ())
1334 .map(|_| ())
1271 }
1335 }
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+ }
1272}
1380}