summaryrefslogtreecommitdiff
path: root/helix-term/src
diff options
context:
space:
mode:
authorBlaž Hrastnik2022-10-20 14:11:22 +0000
committerGitHub2022-10-20 14:11:22 +0000
commit78c0cdc519a2c76842441103b1ed716bb7c0a4e1 (patch)
treea7a551c4fd458a6dec3ccb94bc31055f7c8c9077 /helix-term/src
parent8c9bb23650ba3c0c0bc7b25a359f997e130feb25 (diff)
parent756253b43f5ec1d8cc6fce9e6ebcf3f9fee5bc5a (diff)
Merge pull request #2267 from dead10ck/fix-write-fail
Write path fixes
Diffstat (limited to 'helix-term/src')
-rw-r--r--helix-term/src/application.rs208
-rw-r--r--helix-term/src/commands.rs63
-rw-r--r--helix-term/src/commands/dap.rs35
-rw-r--r--helix-term/src/commands/typed.rs152
-rw-r--r--helix-term/src/compositor.rs10
-rw-r--r--helix-term/src/job.rs36
-rw-r--r--helix-term/src/main.rs12
-rw-r--r--helix-term/src/ui/editor.rs10
-rw-r--r--helix-term/src/ui/mod.rs6
9 files changed, 354 insertions, 178 deletions
diff --git a/helix-term/src/application.rs b/helix-term/src/application.rs
index 4bb36b59..b4b4a675 100644
--- a/helix-term/src/application.rs
+++ b/helix-term/src/application.rs
@@ -1,12 +1,19 @@
use arc_swap::{access::Map, ArcSwap};
use futures_util::Stream;
use helix_core::{
- config::{default_syntax_loader, user_syntax_loader},
diagnostic::{DiagnosticTag, NumberOrString},
+ path::get_relative_path,
pos_at_coords, syntax, Selection,
};
use helix_lsp::{lsp, util::lsp_pos_to_pos, LspProgressMap};
-use helix_view::{align_view, editor::ConfigEvent, theme, tree::Layout, Align, Editor};
+use helix_view::{
+ align_view,
+ document::DocumentSavedEventResult,
+ editor::{ConfigEvent, EditorEvent},
+ theme,
+ tree::Layout,
+ Align, Editor,
+};
use serde_json::json;
use crate::{
@@ -19,7 +26,7 @@ use crate::{
ui::{self, overlay::overlayed},
};
-use log::{error, warn};
+use log::{debug, error, warn};
use std::{
io::{stdin, stdout, Write},
sync::Arc,
@@ -102,7 +109,11 @@ fn restore_term() -> Result<(), Error> {
}
impl Application {
- pub fn new(args: Args, config: Config) -> Result<Self, Error> {
+ pub fn new(
+ args: Args,
+ config: Config,
+ syn_loader_conf: syntax::Configuration,
+ ) -> Result<Self, Error> {
#[cfg(feature = "integration")]
setup_integration_logging();
@@ -129,14 +140,6 @@ impl Application {
})
.unwrap_or_else(|| theme_loader.default_theme(true_color));
- let syn_loader_conf = user_syntax_loader().unwrap_or_else(|err| {
- eprintln!("Bad language config: {}", err);
- eprintln!("Press <ENTER> to continue with default language config");
- use std::io::Read;
- // This waits for an enter press.
- let _ = std::io::stdin().read(&mut []);
- default_syntax_loader()
- });
let syn_loader = std::sync::Arc::new(syntax::Loader::new(syn_loader_conf));
let mut compositor = Compositor::new().context("build compositor")?;
@@ -245,6 +248,10 @@ impl Application {
Ok(app)
}
+ #[cfg(feature = "integration")]
+ fn render(&mut self) {}
+
+ #[cfg(not(feature = "integration"))]
fn render(&mut self) {
let compositor = &mut self.compositor;
@@ -275,9 +282,6 @@ impl Application {
where
S: Stream<Item = crossterm::Result<crossterm::event::Event>> + Unpin,
{
- #[cfg(feature = "integration")]
- let mut idle_handled = false;
-
loop {
if self.editor.should_close() {
return false;
@@ -294,26 +298,6 @@ impl Application {
Some(signal) = self.signals.next() => {
self.handle_signals(signal).await;
}
- Some((id, call)) = self.editor.language_servers.incoming.next() => {
- self.handle_language_server_message(call, id).await;
- // limit render calls for fast language server messages
- let last = self.editor.language_servers.incoming.is_empty();
-
- if last || self.last_render.elapsed() > LSP_DEADLINE {
- self.render();
- self.last_render = Instant::now();
- }
- }
- Some(payload) = self.editor.debugger_events.next() => {
- let needs_render = self.editor.handle_debugger_message(payload).await;
- if needs_render {
- self.render();
- }
- }
- Some(config_event) = self.editor.config_events.1.recv() => {
- self.handle_config_events(config_event);
- self.render();
- }
Some(callback) = self.jobs.futures.next() => {
self.jobs.handle_callback(&mut self.editor, &mut self.compositor, callback);
self.render();
@@ -322,26 +306,22 @@ impl Application {
self.jobs.handle_callback(&mut self.editor, &mut self.compositor, callback);
self.render();
}
- _ = &mut self.editor.idle_timer => {
- // idle timeout
- self.editor.clear_idle_timer();
- self.handle_idle_timeout();
+ event = self.editor.wait_event() => {
+ let _idle_handled = self.handle_editor_event(event).await;
#[cfg(feature = "integration")]
{
- idle_handled = true;
+ if _idle_handled {
+ return true;
+ }
}
}
}
// for integration tests only, reset the idle timer after every
- // event to make a signal when test events are done processing
+ // event to signal when test events are done processing
#[cfg(feature = "integration")]
{
- if idle_handled {
- return true;
- }
-
self.editor.reset_idle_timer();
}
}
@@ -446,6 +426,111 @@ impl Application {
}
}
+ pub fn handle_document_write(&mut self, doc_save_event: DocumentSavedEventResult) {
+ let doc_save_event = match doc_save_event {
+ Ok(event) => event,
+ Err(err) => {
+ self.editor.set_error(err.to_string());
+ return;
+ }
+ };
+
+ let doc = match self.editor.document_mut(doc_save_event.doc_id) {
+ None => {
+ warn!(
+ "received document saved event for non-existent doc id: {}",
+ doc_save_event.doc_id
+ );
+
+ return;
+ }
+ Some(doc) => doc,
+ };
+
+ debug!(
+ "document {:?} saved with revision {}",
+ doc.path(),
+ doc_save_event.revision
+ );
+
+ doc.set_last_saved_revision(doc_save_event.revision);
+
+ let lines = doc_save_event.text.len_lines();
+ let bytes = doc_save_event.text.len_bytes();
+
+ if doc.path() != Some(&doc_save_event.path) {
+ if let Err(err) = doc.set_path(Some(&doc_save_event.path)) {
+ log::error!(
+ "error setting path for doc '{:?}': {}",
+ doc.path(),
+ err.to_string(),
+ );
+
+ self.editor.set_error(err.to_string());
+ return;
+ }
+
+ let loader = self.editor.syn_loader.clone();
+
+ // borrowing the same doc again to get around the borrow checker
+ let doc = doc_mut!(self.editor, &doc_save_event.doc_id);
+ let id = doc.id();
+ doc.detect_language(loader);
+ let _ = self.editor.refresh_language_server(id);
+ }
+
+ // TODO: fix being overwritten by lsp
+ self.editor.set_status(format!(
+ "'{}' written, {}L {}B",
+ get_relative_path(&doc_save_event.path).to_string_lossy(),
+ lines,
+ bytes
+ ));
+ }
+
+ #[inline(always)]
+ pub async fn handle_editor_event(&mut self, event: EditorEvent) -> bool {
+ log::debug!("received editor event: {:?}", event);
+
+ match event {
+ EditorEvent::DocumentSaved(event) => {
+ self.handle_document_write(event);
+ self.render();
+ }
+ EditorEvent::ConfigEvent(event) => {
+ self.handle_config_events(event);
+ self.render();
+ }
+ EditorEvent::LanguageServerMessage((id, call)) => {
+ self.handle_language_server_message(call, id).await;
+ // limit render calls for fast language server messages
+ let last = self.editor.language_servers.incoming.is_empty();
+
+ if last || self.last_render.elapsed() > LSP_DEADLINE {
+ self.render();
+ self.last_render = Instant::now();
+ }
+ }
+ EditorEvent::DebuggerEvent(payload) => {
+ let needs_render = self.editor.handle_debugger_message(payload).await;
+ if needs_render {
+ self.render();
+ }
+ }
+ EditorEvent::IdleTimer => {
+ self.editor.clear_idle_timer();
+ self.handle_idle_timeout();
+
+ #[cfg(feature = "integration")]
+ {
+ return true;
+ }
+ }
+ }
+
+ false
+ }
+
pub fn handle_terminal_events(&mut self, event: Result<CrosstermEvent, crossterm::ErrorKind>) {
let mut cx = crate::compositor::Context {
editor: &mut self.editor,
@@ -866,11 +951,10 @@ impl Application {
self.event_loop(input_stream).await;
- let err = self.close().await.err();
-
+ let close_errs = self.close().await;
restore_term()?;
- if let Some(err) = err {
+ for err in close_errs {
self.editor.exit_code = 1;
eprintln!("Error: {}", err);
}
@@ -878,13 +962,33 @@ impl Application {
Ok(self.editor.exit_code)
}
- pub async fn close(&mut self) -> anyhow::Result<()> {
- self.jobs.finish().await?;
+ pub async fn close(&mut self) -> Vec<anyhow::Error> {
+ // [NOTE] we intentionally do not return early for errors because we
+ // want to try to run as much cleanup as we can, regardless of
+ // errors along the way
+ let mut errs = Vec::new();
+
+ if let Err(err) = self
+ .jobs
+ .finish(&mut self.editor, Some(&mut self.compositor))
+ .await
+ {
+ log::error!("Error executing job: {}", err);
+ errs.push(err);
+ };
+
+ if let Err(err) = self.editor.flush_writes().await {
+ log::error!("Error writing: {}", err);
+ errs.push(err);
+ }
if self.editor.close_language_servers(None).await.is_err() {
log::error!("Timed out waiting for language servers to shutdown");
- };
+ errs.push(anyhow::format_err!(
+ "Timed out waiting for language servers to shutdown"
+ ));
+ }
- Ok(())
+ errs
}
}
diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs
index 5073651b..87bbd6c6 100644
--- a/helix-term/src/commands.rs
+++ b/helix-term/src/commands.rs
@@ -47,12 +47,13 @@ use movement::Movement;
use crate::{
args,
compositor::{self, Component, Compositor},
+ job::Callback,
keymap::ReverseKeymap,
ui::{self, overlay::overlayed, FilePicker, Picker, Popup, Prompt, PromptEvent},
};
-use crate::job::{self, Job, Jobs};
-use futures_util::{FutureExt, StreamExt};
+use crate::job::{self, Jobs};
+use futures_util::StreamExt;
use std::{collections::HashMap, fmt, future::Future};
use std::{collections::HashSet, num::NonZeroUsize};
@@ -107,10 +108,11 @@ impl<'a> Context<'a> {
let callback = Box::pin(async move {
let json = call.await?;
let response = serde_json::from_value(json)?;
- let call: job::Callback =
- Box::new(move |editor: &mut Editor, compositor: &mut Compositor| {
+ let call: job::Callback = Callback::EditorCompositor(Box::new(
+ move |editor: &mut Editor, compositor: &mut Compositor| {
callback(editor, compositor, response)
- });
+ },
+ ));
Ok(call)
});
self.jobs.callback(callback);
@@ -1925,8 +1927,8 @@ fn global_search(cx: &mut Context) {
let show_picker = async move {
let all_matches: Vec<FileResult> =
UnboundedReceiverStream::new(all_matches_rx).collect().await;
- let call: job::Callback =
- Box::new(move |editor: &mut Editor, compositor: &mut Compositor| {
+ let call: job::Callback = Callback::EditorCompositor(Box::new(
+ move |editor: &mut Editor, compositor: &mut Compositor| {
if all_matches.is_empty() {
editor.set_status("No matches found");
return;
@@ -1962,7 +1964,8 @@ fn global_search(cx: &mut Context) {
},
);
compositor.push(Box::new(overlayed(picker)));
- });
+ },
+ ));
Ok(call)
};
cx.jobs.callback(show_picker);
@@ -2504,13 +2507,6 @@ fn insert_at_line_end(cx: &mut Context) {
doc.set_selection(view.id, selection);
}
-/// Sometimes when applying formatting changes we want to mark the buffer as unmodified, for
-/// example because we just applied the same changes while saving.
-enum Modified {
- SetUnmodified,
- LeaveModified,
-}
-
// Creates an LspCallback that waits for formatting changes to be computed. When they're done,
// it applies them, but only if the doc hasn't changed.
//
@@ -2519,11 +2515,12 @@ enum Modified {
async fn make_format_callback(
doc_id: DocumentId,
doc_version: i32,
- modified: Modified,
format: impl Future<Output = Result<Transaction, FormatterError>> + Send + 'static,
+ write: Option<(Option<PathBuf>, bool)>,
) -> anyhow::Result<job::Callback> {
- let format = format.await?;
- let call: job::Callback = Box::new(move |editor, _compositor| {
+ let format = format.await;
+
+ let call: job::Callback = Callback::Editor(Box::new(move |editor| {
if !editor.documents.contains_key(&doc_id) {
return;
}
@@ -2531,22 +2528,30 @@ async fn make_format_callback(
let scrolloff = editor.config().scrolloff;
let doc = doc_mut!(editor, &doc_id);
let view = view_mut!(editor);
- if doc.version() == doc_version {
- apply_transaction(&format, doc, view);
- doc.append_changes_to_history(view.id);
- doc.detect_indent_and_line_ending();
- view.ensure_cursor_in_view(doc, scrolloff);
- if let Modified::SetUnmodified = modified {
- doc.reset_modified();
+
+ if let Ok(format) = format {
+ if doc.version() == doc_version {
+ apply_transaction(&format, doc, view);
+ doc.append_changes_to_history(view.id);
+ doc.detect_indent_and_line_ending();
+ view.ensure_cursor_in_view(doc, scrolloff);
+ } else {
+ log::info!("discarded formatting changes because the document changed");
}
- } else {
- log::info!("discarded formatting changes because the document changed");
}
- });
+
+ if let Some((path, force)) = write {
+ let id = doc.id();
+ if let Err(err) = editor.save(id, path, force) {
+ editor.set_error(format!("Error saving: {}", err));
+ }
+ }
+ }));
+
Ok(call)
}
-#[derive(PartialEq)]
+#[derive(PartialEq, Eq)]
pub enum Open {
Below,
Above,
diff --git a/helix-term/src/commands/dap.rs b/helix-term/src/commands/dap.rs
index 12a3fbc7..c27417e3 100644
--- a/helix-term/src/commands/dap.rs
+++ b/helix-term/src/commands/dap.rs
@@ -118,11 +118,14 @@ fn dap_callback<T, F>(
let callback = Box::pin(async move {
let json = call.await?;
let response = serde_json::from_value(json)?;
- let call: Callback = Box::new(move |editor: &mut Editor, compositor: &mut Compositor| {
- callback(editor, compositor, response)
- });
+ let call: Callback = Callback::EditorCompositor(Box::new(
+ move |editor: &mut Editor, compositor: &mut Compositor| {
+ callback(editor, compositor, response)
+ },
+ ));
Ok(call)
});
+
jobs.callback(callback);
}
@@ -274,10 +277,11 @@ pub fn dap_launch(cx: &mut Context) {
let completions = template.completion.clone();
let name = template.name.clone();
let callback = Box::pin(async move {
- let call: Callback = Box::new(move |_editor, compositor| {
- let prompt = debug_parameter_prompt(completions, name, Vec::new());
- compositor.push(Box::new(prompt));
- });
+ let call: Callback =
+ Callback::EditorCompositor(Box::new(move |_editor, compositor| {
+ let prompt = debug_parameter_prompt(completions, name, Vec::new());
+ compositor.push(Box::new(prompt));
+ }));
Ok(call)
});
cx.jobs.callback(callback);
@@ -332,10 +336,11 @@ fn debug_parameter_prompt(
let config_name = config_name.clone();
let params = params.clone();
let callback = Box::pin(async move {
- let call: Callback = Box::new(move |_editor, compositor| {
- let prompt = debug_parameter_prompt(completions, config_name, params);
- compositor.push(Box::new(prompt));
- });
+ let call: Callback =
+ Callback::EditorCompositor(Box::new(move |_editor, compositor| {
+ let prompt = debug_parameter_prompt(completions, config_name, params);
+ compositor.push(Box::new(prompt));
+ }));
Ok(call)
});
cx.jobs.callback(callback);
@@ -582,7 +587,7 @@ pub fn dap_edit_condition(cx: &mut Context) {
None => return,
};
let callback = Box::pin(async move {
- let call: Callback = Box::new(move |editor, compositor| {
+ let call: Callback = Callback::EditorCompositor(Box::new(move |editor, compositor| {
let mut prompt = Prompt::new(
"condition:".into(),
None,
@@ -610,7 +615,7 @@ pub fn dap_edit_condition(cx: &mut Context) {
prompt.insert_str(&condition, editor)
}
compositor.push(Box::new(prompt));
- });
+ }));
Ok(call)
});
cx.jobs.callback(callback);
@@ -624,7 +629,7 @@ pub fn dap_edit_log(cx: &mut Context) {
None => return,
};
let callback = Box::pin(async move {
- let call: Callback = Box::new(move |editor, compositor| {
+ let call: Callback = Callback::EditorCompositor(Box::new(move |editor, compositor| {
let mut prompt = Prompt::new(
"log-message:".into(),
None,
@@ -651,7 +656,7 @@ pub fn dap_edit_log(cx: &mut Context) {
prompt.insert_str(&log_message, editor);
}
compositor.push(Box::new(prompt));
- });
+ }));
Ok(call)
});
cx.jobs.callback(callback);
diff --git a/helix-term/src/commands/typed.rs b/helix-term/src/commands/typed.rs
index 13a0adcf..f20e71c2 100644
--- a/helix-term/src/commands/typed.rs
+++ b/helix-term/src/commands/typed.rs
@@ -1,5 +1,7 @@
use std::ops::Deref;
+use crate::job::Job;
+
use super::*;
use helix_view::{
@@ -19,6 +21,8 @@ pub struct TypableCommand {
}
fn quit(cx: &mut compositor::Context, args: &[Cow<str>], event: PromptEvent) -> anyhow::Result<()> {
+ log::debug!("quitting...");
+
if event != PromptEvent::Validate {
return Ok(());
}
@@ -30,6 +34,7 @@ fn quit(cx: &mut compositor::Context, args: &[Cow<str>], event: PromptEvent) ->
buffers_remaining_impl(cx.editor)?
}
+ cx.block_try_flush_writes()?;
cx.editor.close(view!(cx.editor).id);
Ok(())
@@ -70,14 +75,16 @@ fn open(cx: &mut compositor::Context, args: &[Cow<str>], event: PromptEvent) ->
}
fn buffer_close_by_ids_impl(
- editor: &mut Editor,
+ cx: &mut compositor::Context,
doc_ids: &[DocumentId],
force: bool,
) -> anyhow::Result<()> {
+ cx.block_try_flush_writes()?;
+
let (modified_ids, modified_names): (Vec<_>, Vec<_>) = doc_ids
.iter()
.filter_map(|&doc_id| {
- if let Err(CloseError::BufferModified(name)) = editor.close_document(doc_id, force) {
+ if let Err(CloseError::BufferModified(name)) = cx.editor.close_document(doc_id, force) {
Some((doc_id, name))
} else {
None
@@ -86,11 +93,11 @@ fn buffer_close_by_ids_impl(
.unzip();
if let Some(first) = modified_ids.first() {
- let current = doc!(editor);
+ let current = doc!(cx.editor);
// If the current document is unmodified, and there are modified
// documents, switch focus to the first modified doc.
if !modified_ids.contains(&current.id()) {
- editor.switch(*first, Action::Replace);
+ cx.editor.switch(*first, Action::Replace);
}
bail!(
"{} unsaved buffer(s) remaining: {:?}",
@@ -149,7 +156,7 @@ fn buffer_close(
}
let document_ids = buffer_gather_paths_impl(cx.editor, args);
- buffer_close_by_ids_impl(cx.editor, &document_ids, false)
+ buffer_close_by_ids_impl(cx, &document_ids, false)
}
fn force_buffer_close(
@@ -162,7 +169,7 @@ fn force_buffer_close(
}
let document_ids = buffer_gather_paths_impl(cx.editor, args);
- buffer_close_by_ids_impl(cx.editor, &document_ids, true)
+ buffer_close_by_ids_impl(cx, &document_ids, true)
}
fn buffer_gather_others_impl(editor: &mut Editor) -> Vec<DocumentId> {
@@ -184,7 +191,7 @@ fn buffer_close_others(
}
let document_ids = buffer_gather_others_impl(cx.editor);
- buffer_close_by_ids_impl(cx.editor, &document_ids, false)
+ buffer_close_by_ids_impl(cx, &document_ids, false)
}
fn force_buffer_close_others(
@@ -197,7 +204,7 @@ fn force_buffer_close_others(
}
let document_ids = buffer_gather_others_impl(cx.editor);
- buffer_close_by_ids_impl(cx.editor, &document_ids, true)
+ buffer_close_by_ids_impl(cx, &document_ids, true)
}
fn buffer_gather_all_impl(editor: &mut Editor) -> Vec<DocumentId> {
@@ -214,7 +221,7 @@ fn buffer_close_all(
}
let document_ids = buffer_gather_all_impl(cx.editor);
- buffer_close_by_ids_impl(cx.editor, &document_ids, false)
+ buffer_close_by_ids_impl(cx, &document_ids, false)
}
fn force_buffer_close_all(
@@ -227,7 +234,7 @@ fn force_buffer_close_all(
}
let document_ids = buffer_gather_all_impl(cx.editor);
- buffer_close_by_ids_impl(cx.editor, &document_ids, true)
+ buffer_close_by_ids_impl(cx, &document_ids, true)
}
fn buffer_next(
@@ -261,39 +268,29 @@ fn write_impl(
path: Option<&Cow<str>>,
force: bool,
) -> anyhow::Result<()> {
- let auto_format = cx.editor.config().auto_format;
+ let editor_auto_fmt = cx.editor.config().auto_format;
let jobs = &mut cx.jobs;
let doc = doc_mut!(cx.editor);
+ let path = path.map(AsRef::as_ref);
- if let Some(ref path) = path {
- doc.set_path(Some(path.as_ref().as_ref()))
- .context("invalid filepath")?;
- }
- if doc.path().is_none() {
- bail!("cannot write a buffer without a filename");
- }
- let fmt = if auto_format {
+ let fmt = if editor_auto_fmt {
doc.auto_format().map(|fmt| {
- let shared = fmt.shared();
let callback = make_format_callback(
doc.id(),
doc.version(),
- Modified::SetUnmodified,
- shared.clone(),
+ fmt,
+ Some((path.map(Into::into), force)),
);
- jobs.callback(callback);
- shared
+
+ jobs.add(Job::with_callback(callback).wait_before_exiting());
})
} else {
None
};
- let future = doc.format_and_save(fmt, force);
- cx.jobs.add(Job::new(future).wait_before_exiting());
- if path.is_some() {
+ if fmt.is_none() {
let id = doc.id();
- doc.detect_language(cx.editor.syn_loader.clone());
- let _ = cx.editor.refresh_language_server(id);
+ cx.editor.save(id, path, force)?;
}
Ok(())
@@ -348,8 +345,7 @@ fn format(
let doc = doc!(cx.editor);
if let Some(format) = doc.format() {
- let callback =
- make_format_callback(doc.id(), doc.version(), Modified::LeaveModified, format);
+ let callback = make_format_callback(doc.id(), doc.version(), format, None);
cx.jobs.callback(callback);
}
@@ -520,7 +516,7 @@ fn write_quit(
}
write_impl(cx, args.first(), false)?;
- helix_lsp::block_on(cx.jobs.finish())?;
+ cx.block_try_flush_writes()?;
quit(cx, &[], event)
}
@@ -534,6 +530,7 @@ fn force_write_quit(
}
write_impl(cx, args.first(), true)?;
+ cx.block_try_flush_writes()?;
force_quit(cx, &[], event)
}
@@ -573,40 +570,50 @@ fn write_all_impl(
return Ok(());
}
- let mut errors = String::new();
+ let mut errors: Vec<&'static str> = Vec::new();
let auto_format = cx.editor.config().auto_format;
let jobs = &mut cx.jobs;
+
// save all documents
- for doc in &mut cx.editor.documents.values_mut() {
- if doc.path().is_none() {
- errors.push_str("cannot write a buffer without a filename\n");
- continue;
- }
+ let saves: Vec<_> = cx
+ .editor
+ .documents
+ .values()
+ .filter_map(|doc| {
+ if doc.path().is_none() {
+ errors.push("cannot write a buffer without a filename\n");
+ return None;
+ }
- if !doc.is_modified() {
- continue;
- }
+ if !doc.is_modified() {
+ return None;
+ }
- let fmt = if auto_format {
- doc.auto_format().map(|fmt| {
- let shared = fmt.shared();
- let callback = make_format_callback(
- doc.id(),
- doc.version(),
- Modified::SetUnmodified,
- shared.clone(),
- );
- jobs.callback(callback);
- shared
- })
- } else {
+ let fmt = if auto_format {
+ doc.auto_format().map(|fmt| {
+ let callback =
+ make_format_callback(doc.id(), doc.version(), fmt, Some((None, force)));
+ jobs.add(Job::with_callback(callback).wait_before_exiting());
+ })
+ } else {
+ None
+ };
+
+ if fmt.is_none() {
+ return Some(doc.id());
+ }
None
- };
- let future = doc.format_and_save(fmt, force);
- jobs.add(Job::new(future).wait_before_exiting());
+ })
+ .collect();
+
+ // manually call save for the rest of docs that don't have a formatter
+ for id in saves {
+ cx.editor.save::<PathBuf>(id, None, force)?;
}
if quit {
+ cx.block_try_flush_writes()?;
+
if !force {
buffers_remaining_impl(cx.editor)?;
}
@@ -618,7 +625,11 @@ fn write_all_impl(
}
}
- bail!(errors)
+ if !errors.is_empty() && !force {
+ bail!("{:?}", errors);
+ }
+
+ Ok(())
}
fn write_all(
@@ -680,6 +691,7 @@ fn quit_all(
return Ok(());
}
+ cx.block_try_flush_writes()?;
quit_all_impl(cx.editor, false)
}
@@ -708,8 +720,9 @@ fn cquit(
.first()
.and_then(|code| code.parse::<i32>().ok())
.unwrap_or(1);
- cx.editor.exit_code = exit_code;
+ cx.editor.exit_code = exit_code;
+ cx.block_try_flush_writes()?;
quit_all_impl(cx.editor, false)
}
@@ -1064,12 +1077,13 @@ fn tree_sitter_scopes(
let contents = format!("```json\n{:?}\n````", scopes);
let callback = async move {
- let call: job::Callback =
- Box::new(move |editor: &mut Editor, compositor: &mut Compositor| {
+ let call: job::Callback = Callback::EditorCompositor(Box::new(
+ move |editor: &mut Editor, compositor: &mut Compositor| {
let contents = ui::Markdown::new(contents, editor.syn_loader.clone());
let popup = Popup::new("hover", contents).auto_close(true);
compositor.replace_or_push("hover", popup);
- });
+ },
+ ));
Ok(call)
};
@@ -1492,12 +1506,13 @@ fn tree_sitter_subtree(
contents.push_str("\n```");
let callback = async move {
- let call: job::Callback =
- Box::new(move |editor: &mut Editor, compositor: &mut Compositor| {
+ let call: job::Callback = Callback::EditorCompositor(Box::new(
+ move |editor: &mut Editor, compositor: &mut Compositor| {
let contents = ui::Markdown::new(contents, editor.syn_loader.clone());
let popup = Popup::new("hover", contents).auto_close(true);
compositor.replace_or_push("hover", popup);
- });
+ },
+ ));
Ok(call)
};
@@ -1605,8 +1620,8 @@ fn run_shell_command(
if !output.is_empty() {
let callback = async move {
- let call: job::Callback =
- Box::new(move |editor: &mut Editor, compositor: &mut Compositor| {
+ let call: job::Callback = Callback::EditorCompositor(Box::new(
+ move |editor: &mut Editor, compositor: &mut Compositor| {
let contents = ui::Markdown::new(
format!("```sh\n{}\n```", output),
editor.syn_loader.clone(),
@@ -1615,7 +1630,8 @@ fn run_shell_command(
helix_core::Position::new(editor.cursor().0.unwrap_or_default().row, 2),
));
compositor.replace_or_push("shell", popup);
- });
+ },
+ ));
Ok(call)
};
diff --git a/helix-term/src/compositor.rs b/helix-term/src/compositor.rs
index c0898dae..971dc52d 100644
--- a/helix-term/src/compositor.rs
+++ b/helix-term/src/compositor.rs
@@ -27,6 +27,16 @@ pub struct Context<'a> {
pub jobs: &'a mut Jobs,
}
+impl<'a> Context<'a> {
+ /// Waits on all pending jobs, and then tries to flush all pending write
+ /// operations for all documents.
+ pub fn block_try_flush_writes(&mut self) -> anyhow::Result<()> {
+ tokio::task::block_in_place(|| helix_lsp::block_on(self.jobs.finish(self.editor, None)))?;
+ tokio::task::block_in_place(|| helix_lsp::block_on(self.editor.flush_writes()))?;
+ Ok(())
+ }
+}
+
pub trait Component: Any + AnyComponent {
/// Process input events, return true if handled.
fn handle_event(&mut self, _event: &Event, _ctx: &mut Context) -> EventResult {
diff --git a/helix-term/src/job.rs b/helix-term/src/job.rs
index e5147992..2888b6eb 100644
--- a/helix-term/src/job.rs
+++ b/helix-term/src/job.rs
@@ -5,7 +5,11 @@ use crate::compositor::Compositor;
use futures_util::future::{BoxFuture, Future, FutureExt};
use futures_util::stream::{FuturesUnordered, StreamExt};
-pub type Callback = Box<dyn FnOnce(&mut Editor, &mut Compositor) + Send>;
+pub enum Callback {
+ EditorCompositor(Box<dyn FnOnce(&mut Editor, &mut Compositor) + Send>),
+ Editor(Box<dyn FnOnce(&mut Editor) + Send>),
+}
+
pub type JobFuture = BoxFuture<'static, anyhow::Result<Option<Callback>>>;
pub struct Job {
@@ -68,9 +72,10 @@ impl Jobs {
) {
match call {
Ok(None) => {}
- Ok(Some(call)) => {
- call(editor, compositor);
- }
+ Ok(Some(call)) => match call {
+ Callback::EditorCompositor(call) => call(editor, compositor),
+ Callback::Editor(call) => call(editor),
+ },
Err(e) => {
editor.set_error(format!("Async job failed: {}", e));
}
@@ -93,13 +98,32 @@ impl Jobs {
}
/// Blocks until all the jobs that need to be waited on are done.
- pub async fn finish(&mut self) -> anyhow::Result<()> {
+ pub async fn finish(
+ &mut self,
+ editor: &mut Editor,
+ mut compositor: Option<&mut Compositor>,
+ ) -> anyhow::Result<()> {
log::debug!("waiting on jobs...");
let mut wait_futures = std::mem::take(&mut self.wait_futures);
+
while let (Some(job), tail) = wait_futures.into_future().await {
match job {
- Ok(_) => {
+ Ok(callback) => {
wait_futures = tail;
+
+ if let Some(callback) = callback {
+ // clippy doesn't realize this is an error without the derefs
+ #[allow(clippy::needless_option_as_deref)]
+ match callback {
+ Callback::EditorCompositor(call) if compositor.is_some() => {
+ call(editor, compositor.as_deref_mut().unwrap())
+ }
+ Callback::Editor(call) => call(editor),
+
+ // skip callbacks for which we don't have the necessary references
+ _ => (),
+ }
+ }
}
Err(e) => {
self.wait_futures = tail;
diff --git a/helix-term/src/main.rs b/helix-term/src/main.rs
index 726bf9e3..96b695c6 100644
--- a/helix-term/src/main.rs
+++ b/helix-term/src/main.rs
@@ -139,8 +139,18 @@ FLAGS:
Err(err) => return Err(Error::new(err)),
};
+ let syn_loader_conf = helix_core::config::user_syntax_loader().unwrap_or_else(|err| {
+ eprintln!("Bad language config: {}", err);
+ eprintln!("Press <ENTER> to continue with default language config");
+ use std::io::Read;
+ // This waits for an enter press.
+ let _ = std::io::stdin().read(&mut []);
+ helix_core::config::default_syntax_loader()
+ });
+
// TODO: use the thread local executor to spawn the application task separately from the work pool
- let mut app = Application::new(args, config).context("unable to create new application")?;
+ let mut app = Application::new(args, config, syn_loader_conf)
+ .context("unable to create new application")?;
let exit_code = app.run(&mut EventStream::new()).await?;
diff --git a/helix-term/src/ui/editor.rs b/helix-term/src/ui/editor.rs
index 3cd2130a..73dfd52c 100644
--- a/helix-term/src/ui/editor.rs
+++ b/helix-term/src/ui/editor.rs
@@ -1,7 +1,8 @@
use crate::{
commands,
compositor::{Component, Context, Event, EventResult},
- job, key,
+ job::{self, Callback},
+ key,
keymap::{KeymapResult, Keymaps},
ui::{Completion, ProgressSpinners},
};
@@ -944,9 +945,10 @@ impl EditorView {
// TODO: Use an on_mode_change hook to remove signature help
cxt.jobs.callback(async {
- let call: job::Callback = Box::new(|_editor, compositor| {
- compositor.remove(SignatureHelp::ID);
- });
+ let call: job::Callback =
+ Callback::EditorCompositor(Box::new(|_editor, compositor| {
+ compositor.remove(SignatureHelp::ID);
+ }));
Ok(call)
});
}
diff --git a/helix-term/src/ui/mod.rs b/helix-term/src/ui/mod.rs
index 6ac4dbb7..f99dea0b 100644
--- a/helix-term/src/ui/mod.rs
+++ b/helix-term/src/ui/mod.rs
@@ -14,7 +14,7 @@ mod statusline;
mod text;
use crate::compositor::{Component, Compositor};
-use crate::job;
+use crate::job::{self, Callback};
pub use completion::Completion;
pub use editor::EditorView;
pub use markdown::Markdown;
@@ -121,7 +121,7 @@ pub fn regex_prompt(
if event == PromptEvent::Validate {
let callback = async move {
- let call: job::Callback = Box::new(
+ let call: job::Callback = Callback::EditorCompositor(Box::new(
move |_editor: &mut Editor, compositor: &mut Compositor| {
let contents = Text::new(format!("{}", err));
let size = compositor.size();
@@ -135,7 +135,7 @@ pub fn regex_prompt(
compositor.replace_or_push("invalid-regex", popup);
},
- );
+ ));
Ok(call)
};