Merge pull request #2267 from dead10ck/fix-write-fail

Write path fixes
pull/4384/head
Blaž Hrastnik 2 years ago committed by GitHub
commit 78c0cdc519
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -35,7 +35,8 @@ to `cargo install` anything either).
Integration tests for helix-term can be run with `cargo integration-test`. Code Integration tests for helix-term can be run with `cargo integration-test`. Code
contributors are strongly encouraged to write integration tests for their code. contributors are strongly encouraged to write integration tests for their code.
Existing tests can be used as examples. Helpers can be found in Existing tests can be used as examples. Helpers can be found in
[helpers.rs][helpers.rs] [helpers.rs][helpers.rs]. The log level can be set with the `HELIX_LOG_LEVEL`
environment variable, e.g. `HELIX_LOG_LEVEL=debug cargo integration-test`.
## Minimum Stable Rust Version (MSRV) Policy ## Minimum Stable Rust Version (MSRV) Policy

@ -7,7 +7,6 @@ use std::collections::HashMap;
use smallvec::SmallVec; use smallvec::SmallVec;
// Heavily based on https://github.com/codemirror/closebrackets/ // Heavily based on https://github.com/codemirror/closebrackets/
pub const DEFAULT_PAIRS: &[(char, char)] = &[ pub const DEFAULT_PAIRS: &[(char, char)] = &[
('(', ')'), ('(', ')'),
('{', '}'), ('{', '}'),

@ -61,6 +61,12 @@ pub struct Configuration {
pub language: Vec<LanguageConfiguration>, pub language: Vec<LanguageConfiguration>,
} }
impl Default for Configuration {
fn default() -> Self {
crate::config::default_syntax_loader()
}
}
// largely based on tree-sitter/cli/src/loader.rs // largely based on tree-sitter/cli/src/loader.rs
#[derive(Debug, Serialize, Deserialize)] #[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case", deny_unknown_fields)] #[serde(rename_all = "kebab-case", deny_unknown_fields)]

@ -1,12 +1,19 @@
use arc_swap::{access::Map, ArcSwap}; use arc_swap::{access::Map, ArcSwap};
use futures_util::Stream; use futures_util::Stream;
use helix_core::{ use helix_core::{
config::{default_syntax_loader, user_syntax_loader},
diagnostic::{DiagnosticTag, NumberOrString}, diagnostic::{DiagnosticTag, NumberOrString},
path::get_relative_path,
pos_at_coords, syntax, Selection, pos_at_coords, syntax, Selection,
}; };
use helix_lsp::{lsp, util::lsp_pos_to_pos, LspProgressMap}; 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 serde_json::json;
use crate::{ use crate::{
@ -19,7 +26,7 @@ use crate::{
ui::{self, overlay::overlayed}, ui::{self, overlay::overlayed},
}; };
use log::{error, warn}; use log::{debug, error, warn};
use std::{ use std::{
io::{stdin, stdout, Write}, io::{stdin, stdout, Write},
sync::Arc, sync::Arc,
@ -102,7 +109,11 @@ fn restore_term() -> Result<(), Error> {
} }
impl Application { 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")] #[cfg(feature = "integration")]
setup_integration_logging(); setup_integration_logging();
@ -129,14 +140,6 @@ impl Application {
}) })
.unwrap_or_else(|| theme_loader.default_theme(true_color)); .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 syn_loader = std::sync::Arc::new(syntax::Loader::new(syn_loader_conf));
let mut compositor = Compositor::new().context("build compositor")?; let mut compositor = Compositor::new().context("build compositor")?;
@ -245,6 +248,10 @@ impl Application {
Ok(app) Ok(app)
} }
#[cfg(feature = "integration")]
fn render(&mut self) {}
#[cfg(not(feature = "integration"))]
fn render(&mut self) { fn render(&mut self) {
let compositor = &mut self.compositor; let compositor = &mut self.compositor;
@ -275,9 +282,6 @@ impl Application {
where where
S: Stream<Item = crossterm::Result<crossterm::event::Event>> + Unpin, S: Stream<Item = crossterm::Result<crossterm::event::Event>> + Unpin,
{ {
#[cfg(feature = "integration")]
let mut idle_handled = false;
loop { loop {
if self.editor.should_close() { if self.editor.should_close() {
return false; return false;
@ -294,26 +298,6 @@ impl Application {
Some(signal) = self.signals.next() => { Some(signal) = self.signals.next() => {
self.handle_signals(signal).await; 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() => { Some(callback) = self.jobs.futures.next() => {
self.jobs.handle_callback(&mut self.editor, &mut self.compositor, callback); self.jobs.handle_callback(&mut self.editor, &mut self.compositor, callback);
self.render(); self.render();
@ -322,26 +306,22 @@ impl Application {
self.jobs.handle_callback(&mut self.editor, &mut self.compositor, callback); self.jobs.handle_callback(&mut self.editor, &mut self.compositor, callback);
self.render(); self.render();
} }
_ = &mut self.editor.idle_timer => { event = self.editor.wait_event() => {
// idle timeout let _idle_handled = self.handle_editor_event(event).await;
self.editor.clear_idle_timer();
self.handle_idle_timeout();
#[cfg(feature = "integration")] #[cfg(feature = "integration")]
{ {
idle_handled = true; if _idle_handled {
return true;
}
} }
} }
} }
// for integration tests only, reset the idle timer after every // 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")] #[cfg(feature = "integration")]
{ {
if idle_handled {
return true;
}
self.editor.reset_idle_timer(); 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>) { pub fn handle_terminal_events(&mut self, event: Result<CrosstermEvent, crossterm::ErrorKind>) {
let mut cx = crate::compositor::Context { let mut cx = crate::compositor::Context {
editor: &mut self.editor, editor: &mut self.editor,
@ -866,11 +951,10 @@ impl Application {
self.event_loop(input_stream).await; self.event_loop(input_stream).await;
let err = self.close().await.err(); let close_errs = self.close().await;
restore_term()?; restore_term()?;
if let Some(err) = err { for err in close_errs {
self.editor.exit_code = 1; self.editor.exit_code = 1;
eprintln!("Error: {}", err); eprintln!("Error: {}", err);
} }
@ -878,13 +962,33 @@ impl Application {
Ok(self.editor.exit_code) Ok(self.editor.exit_code)
} }
pub async fn close(&mut self) -> anyhow::Result<()> { pub async fn close(&mut self) -> Vec<anyhow::Error> {
self.jobs.finish().await?; // [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() { if self.editor.close_language_servers(None).await.is_err() {
log::error!("Timed out waiting for language servers to shutdown"); 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
} }
} }

@ -47,12 +47,13 @@ use movement::Movement;
use crate::{ use crate::{
args, args,
compositor::{self, Component, Compositor}, compositor::{self, Component, Compositor},
job::Callback,
keymap::ReverseKeymap, keymap::ReverseKeymap,
ui::{self, overlay::overlayed, FilePicker, Picker, Popup, Prompt, PromptEvent}, ui::{self, overlay::overlayed, FilePicker, Picker, Popup, Prompt, PromptEvent},
}; };
use crate::job::{self, Job, Jobs}; use crate::job::{self, Jobs};
use futures_util::{FutureExt, StreamExt}; use futures_util::StreamExt;
use std::{collections::HashMap, fmt, future::Future}; use std::{collections::HashMap, fmt, future::Future};
use std::{collections::HashSet, num::NonZeroUsize}; use std::{collections::HashSet, num::NonZeroUsize};
@ -107,10 +108,11 @@ impl<'a> Context<'a> {
let callback = Box::pin(async move { let callback = Box::pin(async move {
let json = call.await?; let json = call.await?;
let response = serde_json::from_value(json)?; let response = serde_json::from_value(json)?;
let call: job::Callback = let call: job::Callback = Callback::EditorCompositor(Box::new(
Box::new(move |editor: &mut Editor, compositor: &mut Compositor| { move |editor: &mut Editor, compositor: &mut Compositor| {
callback(editor, compositor, response) callback(editor, compositor, response)
}); },
));
Ok(call) Ok(call)
}); });
self.jobs.callback(callback); self.jobs.callback(callback);
@ -1925,8 +1927,8 @@ fn global_search(cx: &mut Context) {
let show_picker = async move { let show_picker = async move {
let all_matches: Vec<FileResult> = let all_matches: Vec<FileResult> =
UnboundedReceiverStream::new(all_matches_rx).collect().await; UnboundedReceiverStream::new(all_matches_rx).collect().await;
let call: job::Callback = let call: job::Callback = Callback::EditorCompositor(Box::new(
Box::new(move |editor: &mut Editor, compositor: &mut Compositor| { move |editor: &mut Editor, compositor: &mut Compositor| {
if all_matches.is_empty() { if all_matches.is_empty() {
editor.set_status("No matches found"); editor.set_status("No matches found");
return; return;
@ -1962,7 +1964,8 @@ fn global_search(cx: &mut Context) {
}, },
); );
compositor.push(Box::new(overlayed(picker))); compositor.push(Box::new(overlayed(picker)));
}); },
));
Ok(call) Ok(call)
}; };
cx.jobs.callback(show_picker); cx.jobs.callback(show_picker);
@ -2504,13 +2507,6 @@ fn insert_at_line_end(cx: &mut Context) {
doc.set_selection(view.id, selection); 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, // 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. // it applies them, but only if the doc hasn't changed.
// //
@ -2519,11 +2515,12 @@ enum Modified {
async fn make_format_callback( async fn make_format_callback(
doc_id: DocumentId, doc_id: DocumentId,
doc_version: i32, doc_version: i32,
modified: Modified,
format: impl Future<Output = Result<Transaction, FormatterError>> + Send + 'static, format: impl Future<Output = Result<Transaction, FormatterError>> + Send + 'static,
write: Option<(Option<PathBuf>, bool)>,
) -> anyhow::Result<job::Callback> { ) -> anyhow::Result<job::Callback> {
let format = format.await?; let format = format.await;
let call: job::Callback = Box::new(move |editor, _compositor| {
let call: job::Callback = Callback::Editor(Box::new(move |editor| {
if !editor.documents.contains_key(&doc_id) { if !editor.documents.contains_key(&doc_id) {
return; return;
} }
@ -2531,22 +2528,30 @@ async fn make_format_callback(
let scrolloff = editor.config().scrolloff; let scrolloff = editor.config().scrolloff;
let doc = doc_mut!(editor, &doc_id); let doc = doc_mut!(editor, &doc_id);
let view = view_mut!(editor); let view = view_mut!(editor);
if let Ok(format) = format {
if doc.version() == doc_version { if doc.version() == doc_version {
apply_transaction(&format, doc, view); apply_transaction(&format, doc, view);
doc.append_changes_to_history(view.id); doc.append_changes_to_history(view.id);
doc.detect_indent_and_line_ending(); doc.detect_indent_and_line_ending();
view.ensure_cursor_in_view(doc, scrolloff); view.ensure_cursor_in_view(doc, scrolloff);
if let Modified::SetUnmodified = modified {
doc.reset_modified();
}
} else { } else {
log::info!("discarded formatting changes because the document changed"); 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) Ok(call)
} }
#[derive(PartialEq)] #[derive(PartialEq, Eq)]
pub enum Open { pub enum Open {
Below, Below,
Above, Above,

@ -118,11 +118,14 @@ fn dap_callback<T, F>(
let callback = Box::pin(async move { let callback = Box::pin(async move {
let json = call.await?; let json = call.await?;
let response = serde_json::from_value(json)?; let response = serde_json::from_value(json)?;
let call: Callback = Box::new(move |editor: &mut Editor, compositor: &mut Compositor| { let call: Callback = Callback::EditorCompositor(Box::new(
move |editor: &mut Editor, compositor: &mut Compositor| {
callback(editor, compositor, response) callback(editor, compositor, response)
}); },
));
Ok(call) Ok(call)
}); });
jobs.callback(callback); jobs.callback(callback);
} }
@ -274,10 +277,11 @@ pub fn dap_launch(cx: &mut Context) {
let completions = template.completion.clone(); let completions = template.completion.clone();
let name = template.name.clone(); let name = template.name.clone();
let callback = Box::pin(async move { 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 prompt = debug_parameter_prompt(completions, name, Vec::new()); let prompt = debug_parameter_prompt(completions, name, Vec::new());
compositor.push(Box::new(prompt)); compositor.push(Box::new(prompt));
}); }));
Ok(call) Ok(call)
}); });
cx.jobs.callback(callback); cx.jobs.callback(callback);
@ -332,10 +336,11 @@ fn debug_parameter_prompt(
let config_name = config_name.clone(); let config_name = config_name.clone();
let params = params.clone(); let params = params.clone();
let callback = Box::pin(async move { 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 prompt = debug_parameter_prompt(completions, config_name, params); let prompt = debug_parameter_prompt(completions, config_name, params);
compositor.push(Box::new(prompt)); compositor.push(Box::new(prompt));
}); }));
Ok(call) Ok(call)
}); });
cx.jobs.callback(callback); cx.jobs.callback(callback);
@ -582,7 +587,7 @@ pub fn dap_edit_condition(cx: &mut Context) {
None => return, None => return,
}; };
let callback = Box::pin(async move { 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( let mut prompt = Prompt::new(
"condition:".into(), "condition:".into(),
None, None,
@ -610,7 +615,7 @@ pub fn dap_edit_condition(cx: &mut Context) {
prompt.insert_str(&condition, editor) prompt.insert_str(&condition, editor)
} }
compositor.push(Box::new(prompt)); compositor.push(Box::new(prompt));
}); }));
Ok(call) Ok(call)
}); });
cx.jobs.callback(callback); cx.jobs.callback(callback);
@ -624,7 +629,7 @@ pub fn dap_edit_log(cx: &mut Context) {
None => return, None => return,
}; };
let callback = Box::pin(async move { 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( let mut prompt = Prompt::new(
"log-message:".into(), "log-message:".into(),
None, None,
@ -651,7 +656,7 @@ pub fn dap_edit_log(cx: &mut Context) {
prompt.insert_str(&log_message, editor); prompt.insert_str(&log_message, editor);
} }
compositor.push(Box::new(prompt)); compositor.push(Box::new(prompt));
}); }));
Ok(call) Ok(call)
}); });
cx.jobs.callback(callback); cx.jobs.callback(callback);

@ -1,5 +1,7 @@
use std::ops::Deref; use std::ops::Deref;
use crate::job::Job;
use super::*; use super::*;
use helix_view::{ use helix_view::{
@ -19,6 +21,8 @@ pub struct TypableCommand {
} }
fn quit(cx: &mut compositor::Context, args: &[Cow<str>], event: PromptEvent) -> anyhow::Result<()> { fn quit(cx: &mut compositor::Context, args: &[Cow<str>], event: PromptEvent) -> anyhow::Result<()> {
log::debug!("quitting...");
if event != PromptEvent::Validate { if event != PromptEvent::Validate {
return Ok(()); return Ok(());
} }
@ -30,6 +34,7 @@ fn quit(cx: &mut compositor::Context, args: &[Cow<str>], event: PromptEvent) ->
buffers_remaining_impl(cx.editor)? buffers_remaining_impl(cx.editor)?
} }
cx.block_try_flush_writes()?;
cx.editor.close(view!(cx.editor).id); cx.editor.close(view!(cx.editor).id);
Ok(()) Ok(())
@ -70,14 +75,16 @@ fn open(cx: &mut compositor::Context, args: &[Cow<str>], event: PromptEvent) ->
} }
fn buffer_close_by_ids_impl( fn buffer_close_by_ids_impl(
editor: &mut Editor, cx: &mut compositor::Context,
doc_ids: &[DocumentId], doc_ids: &[DocumentId],
force: bool, force: bool,
) -> anyhow::Result<()> { ) -> anyhow::Result<()> {
cx.block_try_flush_writes()?;
let (modified_ids, modified_names): (Vec<_>, Vec<_>) = doc_ids let (modified_ids, modified_names): (Vec<_>, Vec<_>) = doc_ids
.iter() .iter()
.filter_map(|&doc_id| { .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)) Some((doc_id, name))
} else { } else {
None None
@ -86,11 +93,11 @@ fn buffer_close_by_ids_impl(
.unzip(); .unzip();
if let Some(first) = modified_ids.first() { 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 // If the current document is unmodified, and there are modified
// documents, switch focus to the first modified doc. // documents, switch focus to the first modified doc.
if !modified_ids.contains(&current.id()) { if !modified_ids.contains(&current.id()) {
editor.switch(*first, Action::Replace); cx.editor.switch(*first, Action::Replace);
} }
bail!( bail!(
"{} unsaved buffer(s) remaining: {:?}", "{} unsaved buffer(s) remaining: {:?}",
@ -149,7 +156,7 @@ fn buffer_close(
} }
let document_ids = buffer_gather_paths_impl(cx.editor, args); 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( fn force_buffer_close(
@ -162,7 +169,7 @@ fn force_buffer_close(
} }
let document_ids = buffer_gather_paths_impl(cx.editor, args); 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> { 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); 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( fn force_buffer_close_others(
@ -197,7 +204,7 @@ fn force_buffer_close_others(
} }
let document_ids = buffer_gather_others_impl(cx.editor); 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> { 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); 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( fn force_buffer_close_all(
@ -227,7 +234,7 @@ fn force_buffer_close_all(
} }
let document_ids = buffer_gather_all_impl(cx.editor); 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( fn buffer_next(
@ -261,39 +268,29 @@ fn write_impl(
path: Option<&Cow<str>>, path: Option<&Cow<str>>,
force: bool, force: bool,
) -> anyhow::Result<()> { ) -> 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 jobs = &mut cx.jobs;
let doc = doc_mut!(cx.editor); let doc = doc_mut!(cx.editor);
let path = path.map(AsRef::as_ref);
if let Some(ref path) = path { let fmt = if editor_auto_fmt {
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 {
doc.auto_format().map(|fmt| { doc.auto_format().map(|fmt| {
let shared = fmt.shared();
let callback = make_format_callback( let callback = make_format_callback(
doc.id(), doc.id(),
doc.version(), doc.version(),
Modified::SetUnmodified, fmt,
shared.clone(), Some((path.map(Into::into), force)),
); );
jobs.callback(callback);
shared jobs.add(Job::with_callback(callback).wait_before_exiting());
}) })
} else { } else {
None 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(); let id = doc.id();
doc.detect_language(cx.editor.syn_loader.clone()); cx.editor.save(id, path, force)?;
let _ = cx.editor.refresh_language_server(id);
} }
Ok(()) Ok(())
@ -348,8 +345,7 @@ fn format(
let doc = doc!(cx.editor); let doc = doc!(cx.editor);
if let Some(format) = doc.format() { if let Some(format) = doc.format() {
let callback = let callback = make_format_callback(doc.id(), doc.version(), format, None);
make_format_callback(doc.id(), doc.version(), Modified::LeaveModified, format);
cx.jobs.callback(callback); cx.jobs.callback(callback);
} }
@ -520,7 +516,7 @@ fn write_quit(
} }
write_impl(cx, args.first(), false)?; write_impl(cx, args.first(), false)?;
helix_lsp::block_on(cx.jobs.finish())?; cx.block_try_flush_writes()?;
quit(cx, &[], event) quit(cx, &[], event)
} }
@ -534,6 +530,7 @@ fn force_write_quit(
} }
write_impl(cx, args.first(), true)?; write_impl(cx, args.first(), true)?;
cx.block_try_flush_writes()?;
force_quit(cx, &[], event) force_quit(cx, &[], event)
} }
@ -573,40 +570,50 @@ fn write_all_impl(
return Ok(()); return Ok(());
} }
let mut errors = String::new(); let mut errors: Vec<&'static str> = Vec::new();
let auto_format = cx.editor.config().auto_format; let auto_format = cx.editor.config().auto_format;
let jobs = &mut cx.jobs; let jobs = &mut cx.jobs;
// save all documents // save all documents
for doc in &mut cx.editor.documents.values_mut() { let saves: Vec<_> = cx
.editor
.documents
.values()
.filter_map(|doc| {
if doc.path().is_none() { if doc.path().is_none() {
errors.push_str("cannot write a buffer without a filename\n"); errors.push("cannot write a buffer without a filename\n");
continue; return None;
} }
if !doc.is_modified() { if !doc.is_modified() {
continue; return None;
} }
let fmt = if auto_format { let fmt = if auto_format {
doc.auto_format().map(|fmt| { doc.auto_format().map(|fmt| {
let shared = fmt.shared(); let callback =
let callback = make_format_callback( make_format_callback(doc.id(), doc.version(), fmt, Some((None, force)));
doc.id(), jobs.add(Job::with_callback(callback).wait_before_exiting());
doc.version(),
Modified::SetUnmodified,
shared.clone(),
);
jobs.callback(callback);
shared
}) })
} else { } else {
None None
}; };
let future = doc.format_and_save(fmt, force);
jobs.add(Job::new(future).wait_before_exiting()); if fmt.is_none() {
return Some(doc.id());
}
None
})
.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 { if quit {
cx.block_try_flush_writes()?;
if !force { if !force {
buffers_remaining_impl(cx.editor)?; 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( fn write_all(
@ -680,6 +691,7 @@ fn quit_all(
return Ok(()); return Ok(());
} }
cx.block_try_flush_writes()?;
quit_all_impl(cx.editor, false) quit_all_impl(cx.editor, false)
} }
@ -708,8 +720,9 @@ fn cquit(
.first() .first()
.and_then(|code| code.parse::<i32>().ok()) .and_then(|code| code.parse::<i32>().ok())
.unwrap_or(1); .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) quit_all_impl(cx.editor, false)
} }
@ -1064,12 +1077,13 @@ fn tree_sitter_scopes(
let contents = format!("```json\n{:?}\n````", scopes); let contents = format!("```json\n{:?}\n````", scopes);
let callback = async move { let callback = async move {
let call: job::Callback = let call: job::Callback = Callback::EditorCompositor(Box::new(
Box::new(move |editor: &mut Editor, compositor: &mut Compositor| { move |editor: &mut Editor, compositor: &mut Compositor| {
let contents = ui::Markdown::new(contents, editor.syn_loader.clone()); let contents = ui::Markdown::new(contents, editor.syn_loader.clone());
let popup = Popup::new("hover", contents).auto_close(true); let popup = Popup::new("hover", contents).auto_close(true);
compositor.replace_or_push("hover", popup); compositor.replace_or_push("hover", popup);
}); },
));
Ok(call) Ok(call)
}; };
@ -1492,12 +1506,13 @@ fn tree_sitter_subtree(
contents.push_str("\n```"); contents.push_str("\n```");
let callback = async move { let callback = async move {
let call: job::Callback = let call: job::Callback = Callback::EditorCompositor(Box::new(
Box::new(move |editor: &mut Editor, compositor: &mut Compositor| { move |editor: &mut Editor, compositor: &mut Compositor| {
let contents = ui::Markdown::new(contents, editor.syn_loader.clone()); let contents = ui::Markdown::new(contents, editor.syn_loader.clone());
let popup = Popup::new("hover", contents).auto_close(true); let popup = Popup::new("hover", contents).auto_close(true);
compositor.replace_or_push("hover", popup); compositor.replace_or_push("hover", popup);
}); },
));
Ok(call) Ok(call)
}; };
@ -1605,8 +1620,8 @@ fn run_shell_command(
if !output.is_empty() { if !output.is_empty() {
let callback = async move { let callback = async move {
let call: job::Callback = let call: job::Callback = Callback::EditorCompositor(Box::new(
Box::new(move |editor: &mut Editor, compositor: &mut Compositor| { move |editor: &mut Editor, compositor: &mut Compositor| {
let contents = ui::Markdown::new( let contents = ui::Markdown::new(
format!("```sh\n{}\n```", output), format!("```sh\n{}\n```", output),
editor.syn_loader.clone(), editor.syn_loader.clone(),
@ -1615,7 +1630,8 @@ fn run_shell_command(
helix_core::Position::new(editor.cursor().0.unwrap_or_default().row, 2), helix_core::Position::new(editor.cursor().0.unwrap_or_default().row, 2),
)); ));
compositor.replace_or_push("shell", popup); compositor.replace_or_push("shell", popup);
}); },
));
Ok(call) Ok(call)
}; };

@ -27,6 +27,16 @@ pub struct Context<'a> {
pub jobs: &'a mut Jobs, 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 { pub trait Component: Any + AnyComponent {
/// Process input events, return true if handled. /// Process input events, return true if handled.
fn handle_event(&mut self, _event: &Event, _ctx: &mut Context) -> EventResult { fn handle_event(&mut self, _event: &Event, _ctx: &mut Context) -> EventResult {

@ -5,7 +5,11 @@ use crate::compositor::Compositor;
use futures_util::future::{BoxFuture, Future, FutureExt}; use futures_util::future::{BoxFuture, Future, FutureExt};
use futures_util::stream::{FuturesUnordered, StreamExt}; 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 type JobFuture = BoxFuture<'static, anyhow::Result<Option<Callback>>>;
pub struct Job { pub struct Job {
@ -68,9 +72,10 @@ impl Jobs {
) { ) {
match call { match call {
Ok(None) => {} Ok(None) => {}
Ok(Some(call)) => { Ok(Some(call)) => match call {
call(editor, compositor); Callback::EditorCompositor(call) => call(editor, compositor),
} Callback::Editor(call) => call(editor),
},
Err(e) => { Err(e) => {
editor.set_error(format!("Async job failed: {}", 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. /// 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..."); log::debug!("waiting on jobs...");
let mut wait_futures = std::mem::take(&mut self.wait_futures); let mut wait_futures = std::mem::take(&mut self.wait_futures);
while let (Some(job), tail) = wait_futures.into_future().await { while let (Some(job), tail) = wait_futures.into_future().await {
match job { match job {
Ok(_) => { Ok(callback) => {
wait_futures = tail; 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) => { Err(e) => {
self.wait_futures = tail; self.wait_futures = tail;

@ -139,8 +139,18 @@ FLAGS:
Err(err) => return Err(Error::new(err)), 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 // 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?; let exit_code = app.run(&mut EventStream::new()).await?;

@ -1,7 +1,8 @@
use crate::{ use crate::{
commands, commands,
compositor::{Component, Context, Event, EventResult}, compositor::{Component, Context, Event, EventResult},
job, key, job::{self, Callback},
key,
keymap::{KeymapResult, Keymaps}, keymap::{KeymapResult, Keymaps},
ui::{Completion, ProgressSpinners}, ui::{Completion, ProgressSpinners},
}; };
@ -944,9 +945,10 @@ impl EditorView {
// TODO: Use an on_mode_change hook to remove signature help // TODO: Use an on_mode_change hook to remove signature help
cxt.jobs.callback(async { cxt.jobs.callback(async {
let call: job::Callback = Box::new(|_editor, compositor| { let call: job::Callback =
Callback::EditorCompositor(Box::new(|_editor, compositor| {
compositor.remove(SignatureHelp::ID); compositor.remove(SignatureHelp::ID);
}); }));
Ok(call) Ok(call)
}); });
} }

@ -14,7 +14,7 @@ mod statusline;
mod text; mod text;
use crate::compositor::{Component, Compositor}; use crate::compositor::{Component, Compositor};
use crate::job; use crate::job::{self, Callback};
pub use completion::Completion; pub use completion::Completion;
pub use editor::EditorView; pub use editor::EditorView;
pub use markdown::Markdown; pub use markdown::Markdown;
@ -121,7 +121,7 @@ pub fn regex_prompt(
if event == PromptEvent::Validate { if event == PromptEvent::Validate {
let callback = async move { 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| { move |_editor: &mut Editor, compositor: &mut Compositor| {
let contents = Text::new(format!("{}", err)); let contents = Text::new(format!("{}", err));
let size = compositor.size(); let size = compositor.size();
@ -135,7 +135,7 @@ pub fn regex_prompt(
compositor.replace_or_push("invalid-regex", popup); compositor.replace_or_push("invalid-regex", popup);
}, },
); ));
Ok(call) Ok(call)
}; };

@ -22,5 +22,6 @@ mod test {
mod commands; mod commands;
mod movement; mod movement;
mod prompt; mod prompt;
mod splits;
mod write; mod write;
} }

@ -8,6 +8,7 @@ async fn auto_indent_c() -> anyhow::Result<()> {
..Default::default() ..Default::default()
}, },
Config::default(), Config::default(),
helpers::test_syntax_conf(None),
// switches to append mode? // switches to append mode?
( (
helpers::platform_line("void foo() {#[|}]#").as_ref(), helpers::platform_line("void foo() {#[|}]#").as_ref(),

@ -13,6 +13,7 @@ async fn auto_pairs_basic() -> anyhow::Result<()> {
}, },
..Default::default() ..Default::default()
}, },
helpers::test_syntax_conf(None),
("#[\n|]#", "i(<esc>", "(#[|\n]#"), ("#[\n|]#", "i(<esc>", "(#[|\n]#"),
) )
.await?; .await?;

@ -1,21 +1,25 @@
use std::{ use std::ops::RangeInclusive;
io::{Read, Write},
ops::RangeInclusive,
};
use helix_core::diagnostic::Severity; use helix_core::diagnostic::Severity;
use helix_term::application::Application;
use super::*; use super::*;
#[tokio::test] #[tokio::test(flavor = "multi_thread")]
async fn test_write_quit_fail() -> anyhow::Result<()> { async fn test_write_quit_fail() -> anyhow::Result<()> {
let file = helpers::new_readonly_tempfile()?; let file = helpers::new_readonly_tempfile()?;
let mut app = helpers::AppBuilder::new()
.with_file(file.path(), None)
.build()?;
test_key_sequence( test_key_sequence(
&mut helpers::app_with_file(file.path())?, &mut app,
Some("ihello<esc>:wq<ret>"), Some("ihello<esc>:wq<ret>"),
Some(&|app| { Some(&|app| {
let mut docs: Vec<_> = app.editor.documents().collect();
assert_eq!(1, docs.len());
let doc = docs.pop().unwrap();
assert_eq!(Some(file.path()), doc.path().map(PathBuf::as_path));
assert_eq!(&Severity::Error, app.editor.get_status().unwrap().1); assert_eq!(&Severity::Error, app.editor.get_status().unwrap().1);
}), }),
false, false,
@ -25,11 +29,10 @@ async fn test_write_quit_fail() -> anyhow::Result<()> {
Ok(()) Ok(())
} }
#[tokio::test] #[tokio::test(flavor = "multi_thread")]
#[ignore]
async fn test_buffer_close_concurrent() -> anyhow::Result<()> { async fn test_buffer_close_concurrent() -> anyhow::Result<()> {
test_key_sequences( test_key_sequences(
&mut Application::new(Args::default(), Config::default())?, &mut helpers::AppBuilder::new().build()?,
vec![ vec![
( (
None, None,
@ -69,8 +72,12 @@ async fn test_buffer_close_concurrent() -> anyhow::Result<()> {
command.push_str(":buffer<minus>close<ret>"); command.push_str(":buffer<minus>close<ret>");
let mut app = helpers::AppBuilder::new()
.with_file(file.path(), None)
.build()?;
test_key_sequence( test_key_sequence(
&mut helpers::app_with_file(file.path())?, &mut app,
Some(&command), Some(&command),
Some(&|app| { Some(&|app| {
assert!(!app.editor.is_err(), "error: {:?}", app.editor.get_status()); assert!(!app.editor.is_err(), "error: {:?}", app.editor.get_status());
@ -82,12 +89,7 @@ async fn test_buffer_close_concurrent() -> anyhow::Result<()> {
) )
.await?; .await?;
file.as_file_mut().flush()?; helpers::assert_file_has_content(file.as_file_mut(), &RANGE.end().to_string())?;
file.as_file_mut().sync_all()?;
let mut file_content = String::new();
file.as_file_mut().read_to_string(&mut file_content)?;
assert_eq!(RANGE.end().to_string(), file_content);
Ok(()) Ok(())
} }

@ -1,10 +1,15 @@
use std::{io::Write, path::PathBuf, time::Duration}; use std::{
fs::File,
io::{Read, Write},
path::PathBuf,
time::Duration,
};
use anyhow::bail; use anyhow::bail;
use crossterm::event::{Event, KeyEvent}; use crossterm::event::{Event, KeyEvent};
use helix_core::{test, Selection, Transaction}; use helix_core::{diagnostic::Severity, test, Selection, Transaction};
use helix_term::{application::Application, args::Args, config::Config}; use helix_term::{application::Application, args::Args, config::Config};
use helix_view::{doc, input::parse_macro}; use helix_view::{doc, input::parse_macro, Editor};
use tempfile::NamedTempFile; use tempfile::NamedTempFile;
use tokio_stream::wrappers::UnboundedReceiverStream; use tokio_stream::wrappers::UnboundedReceiverStream;
@ -56,7 +61,9 @@ pub async fn test_key_sequences(
for (i, (in_keys, test_fn)) in inputs.into_iter().enumerate() { for (i, (in_keys, test_fn)) in inputs.into_iter().enumerate() {
if let Some(in_keys) = in_keys { if let Some(in_keys) = in_keys {
for key_event in parse_macro(in_keys)?.into_iter() { for key_event in parse_macro(in_keys)?.into_iter() {
tx.send(Ok(Event::Key(KeyEvent::from(key_event))))?; let key = Event::Key(KeyEvent::from(key_event));
log::trace!("sending key: {:?}", key);
tx.send(Ok(key))?;
} }
} }
@ -70,7 +77,7 @@ pub async fn test_key_sequences(
// verify if it exited on the last iteration if it should have and // verify if it exited on the last iteration if it should have and
// the inverse // the inverse
if i == num_inputs - 1 && app_exited != should_exit { if i == num_inputs - 1 && app_exited != should_exit {
bail!("expected app to exit: {} != {}", app_exited, should_exit); bail!("expected app to exit: {} != {}", should_exit, app_exited);
} }
if let Some(test) = test_fn { if let Some(test) = test_fn {
@ -87,7 +94,17 @@ pub async fn test_key_sequences(
tokio::time::timeout(TIMEOUT, event_loop).await?; tokio::time::timeout(TIMEOUT, event_loop).await?;
} }
app.close().await?; let errs = app.close().await;
if !errs.is_empty() {
log::error!("Errors closing app");
for err in errs {
log::error!("{}", err);
}
bail!("Error closing app");
}
Ok(()) Ok(())
} }
@ -101,7 +118,7 @@ pub async fn test_key_sequence_with_input_text<T: Into<TestCase>>(
let test_case = test_case.into(); let test_case = test_case.into();
let mut app = match app { let mut app = match app {
Some(app) => app, Some(app) => app,
None => Application::new(Args::default(), Config::default())?, None => Application::new(Args::default(), Config::default(), test_syntax_conf(None))?,
}; };
let (view, doc) = helix_view::current!(app.editor); let (view, doc) = helix_view::current!(app.editor);
@ -125,16 +142,30 @@ pub async fn test_key_sequence_with_input_text<T: Into<TestCase>>(
.await .await
} }
/// Generates language configs that merge in overrides, like a user language
/// config. The argument string must be a raw TOML document.
pub fn test_syntax_conf(overrides: Option<String>) -> helix_core::syntax::Configuration {
let mut lang = helix_loader::config::default_lang_config();
if let Some(overrides) = overrides {
let override_toml = toml::from_str(&overrides).unwrap();
lang = helix_loader::merge_toml_values(lang, override_toml, 3);
}
lang.try_into().unwrap()
}
/// Use this for very simple test cases where there is one input /// Use this for very simple test cases where there is one input
/// document, selection, and sequence of key presses, and you just /// document, selection, and sequence of key presses, and you just
/// want to verify the resulting document and selection. /// want to verify the resulting document and selection.
pub async fn test_with_config<T: Into<TestCase>>( pub async fn test_with_config<T: Into<TestCase>>(
args: Args, args: Args,
config: Config, config: Config,
syn_conf: helix_core::syntax::Configuration,
test_case: T, test_case: T,
) -> anyhow::Result<()> { ) -> anyhow::Result<()> {
let test_case = test_case.into(); let test_case = test_case.into();
let app = Application::new(args, config)?; let app = Application::new(args, config, syn_conf)?;
test_key_sequence_with_input_text( test_key_sequence_with_input_text(
Some(app), Some(app),
@ -155,7 +186,13 @@ pub async fn test_with_config<T: Into<TestCase>>(
} }
pub async fn test<T: Into<TestCase>>(test_case: T) -> anyhow::Result<()> { pub async fn test<T: Into<TestCase>>(test_case: T) -> anyhow::Result<()> {
test_with_config(Args::default(), Config::default(), test_case).await test_with_config(
Args::default(),
Config::default(),
test_syntax_conf(None),
test_case,
)
.await
} }
pub fn temp_file_with_contents<S: AsRef<str>>( pub fn temp_file_with_contents<S: AsRef<str>>(
@ -200,14 +237,75 @@ pub fn new_readonly_tempfile() -> anyhow::Result<NamedTempFile> {
Ok(file) Ok(file)
} }
/// Creates a new Application with default config that opens the given file #[derive(Default)]
/// path pub struct AppBuilder {
pub fn app_with_file<P: Into<PathBuf>>(path: P) -> anyhow::Result<Application> { args: Args,
Application::new( config: Config,
Args { syn_conf: helix_core::syntax::Configuration,
files: vec![(path.into(), helix_core::Position::default())], input: Option<(String, Selection)>,
..Default::default() }
},
Config::default(), impl AppBuilder {
) pub fn new() -> Self {
AppBuilder::default()
}
pub fn with_file<P: Into<PathBuf>>(
mut self,
path: P,
pos: Option<helix_core::Position>,
) -> Self {
self.args.files.push((path.into(), pos.unwrap_or_default()));
self
}
pub fn with_config(mut self, config: Config) -> Self {
self.config = config;
self
}
pub fn with_input_text<S: Into<String>>(mut self, input_text: S) -> Self {
self.input = Some(test::print(&input_text.into()));
self
}
pub fn with_lang_config(mut self, syn_conf: helix_core::syntax::Configuration) -> Self {
self.syn_conf = syn_conf;
self
}
pub fn build(self) -> anyhow::Result<Application> {
let mut app = Application::new(self.args, self.config, self.syn_conf)?;
if let Some((text, selection)) = self.input {
let (view, doc) = helix_view::current!(app.editor);
let sel = doc.selection(view.id).clone();
let trans = Transaction::change_by_selection(doc.text(), &sel, |_| {
(0, doc.text().len_chars(), Some((text.clone()).into()))
})
.with_selection(selection);
// replace the initial text with the input text
doc.apply(&trans, view.id);
}
Ok(app)
}
}
pub fn assert_file_has_content(file: &mut File, content: &str) -> anyhow::Result<()> {
file.flush()?;
file.sync_all()?;
let mut file_content = String::new();
file.read_to_string(&mut file_content)?;
assert_eq!(content, file_content);
Ok(())
}
pub fn assert_status_not_error(editor: &Editor) {
if let Some((_, sev)) = editor.get_status() {
assert_ne!(&Severity::Error, sev);
}
} }

@ -70,7 +70,9 @@ async fn insert_to_normal_mode_cursor_position() -> anyhow::Result<()> {
async fn cursor_position_newly_opened_file() -> anyhow::Result<()> { async fn cursor_position_newly_opened_file() -> anyhow::Result<()> {
let test = |content: &str, expected_sel: Selection| -> anyhow::Result<()> { let test = |content: &str, expected_sel: Selection| -> anyhow::Result<()> {
let file = helpers::temp_file_with_contents(content)?; let file = helpers::temp_file_with_contents(content)?;
let mut app = helpers::app_with_file(file.path())?; let mut app = helpers::AppBuilder::new()
.with_file(file.path(), None)
.build()?;
let (view, doc) = helix_view::current!(app.editor); let (view, doc) = helix_view::current!(app.editor);
let sel = doc.selection(view.id).clone(); let sel = doc.selection(view.id).clone();
@ -115,6 +117,7 @@ async fn select_mode_tree_sitter_next_function_is_union_of_objects() -> anyhow::
..Default::default() ..Default::default()
}, },
Config::default(), Config::default(),
helpers::test_syntax_conf(None),
( (
helpers::platform_line(indoc! {"\ helpers::platform_line(indoc! {"\
#[/|]#// Increments #[/|]#// Increments
@ -146,6 +149,7 @@ async fn select_mode_tree_sitter_prev_function_unselects_object() -> anyhow::Res
..Default::default() ..Default::default()
}, },
Config::default(), Config::default(),
helpers::test_syntax_conf(None),
( (
helpers::platform_line(indoc! {"\ helpers::platform_line(indoc! {"\
/// Increments /// Increments
@ -178,6 +182,7 @@ async fn select_mode_tree_sitter_prev_function_goes_backwards_to_object() -> any
..Default::default() ..Default::default()
}, },
Config::default(), Config::default(),
helpers::test_syntax_conf(None),
( (
helpers::platform_line(indoc! {"\ helpers::platform_line(indoc! {"\
/// Increments /// Increments
@ -208,6 +213,7 @@ async fn select_mode_tree_sitter_prev_function_goes_backwards_to_object() -> any
..Default::default() ..Default::default()
}, },
Config::default(), Config::default(),
helpers::test_syntax_conf(None),
( (
helpers::platform_line(indoc! {"\ helpers::platform_line(indoc! {"\
/// Increments /// Increments

@ -1,11 +1,9 @@
use super::*; use super::*;
use helix_term::application::Application;
#[tokio::test] #[tokio::test]
async fn test_history_completion() -> anyhow::Result<()> { async fn test_history_completion() -> anyhow::Result<()> {
test_key_sequence( test_key_sequence(
&mut Application::new(Args::default(), Config::default())?, &mut AppBuilder::new().build()?,
Some(":asdf<ret>:theme d<C-n><tab>"), Some(":asdf<ret>:theme d<C-n><tab>"),
Some(&|app| { Some(&|app| {
assert!(!app.editor.is_err()); assert!(!app.editor.is_err());

@ -0,0 +1,129 @@
use super::*;
#[tokio::test(flavor = "multi_thread")]
async fn test_split_write_quit_all() -> anyhow::Result<()> {
let mut file1 = tempfile::NamedTempFile::new()?;
let mut file2 = tempfile::NamedTempFile::new()?;
let mut file3 = tempfile::NamedTempFile::new()?;
let mut app = helpers::AppBuilder::new()
.with_file(file1.path(), None)
.build()?;
test_key_sequences(
&mut app,
vec![
(
Some(&format!(
"ihello1<esc>:sp<ret>:o {}<ret>ihello2<esc>:sp<ret>:o {}<ret>ihello3<esc>",
file2.path().to_string_lossy(),
file3.path().to_string_lossy()
)),
Some(&|app| {
let docs: Vec<_> = app.editor.documents().collect();
assert_eq!(3, docs.len());
let doc1 = docs
.iter()
.find(|doc| doc.path().unwrap() == file1.path())
.unwrap();
assert_eq!("hello1", doc1.text().to_string());
let doc2 = docs
.iter()
.find(|doc| doc.path().unwrap() == file2.path())
.unwrap();
assert_eq!("hello2", doc2.text().to_string());
let doc3 = docs
.iter()
.find(|doc| doc.path().unwrap() == file3.path())
.unwrap();
assert_eq!("hello3", doc3.text().to_string());
helpers::assert_status_not_error(&app.editor);
assert_eq!(3, app.editor.tree.views().count());
}),
),
(
Some(":wqa<ret>"),
Some(&|app| {
helpers::assert_status_not_error(&app.editor);
assert_eq!(0, app.editor.tree.views().count());
}),
),
],
true,
)
.await?;
helpers::assert_file_has_content(file1.as_file_mut(), "hello1")?;
helpers::assert_file_has_content(file2.as_file_mut(), "hello2")?;
helpers::assert_file_has_content(file3.as_file_mut(), "hello3")?;
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn test_split_write_quit_same_file() -> anyhow::Result<()> {
let mut file = tempfile::NamedTempFile::new()?;
let mut app = helpers::AppBuilder::new()
.with_file(file.path(), None)
.build()?;
test_key_sequences(
&mut app,
vec![
(
Some("O<esc>ihello<esc>:sp<ret>ogoodbye<esc>"),
Some(&|app| {
assert_eq!(2, app.editor.tree.views().count());
helpers::assert_status_not_error(&app.editor);
let mut docs: Vec<_> = app.editor.documents().collect();
assert_eq!(1, docs.len());
let doc = docs.pop().unwrap();
assert_eq!(
helpers::platform_line("hello\ngoodbye"),
doc.text().to_string()
);
assert!(doc.is_modified());
}),
),
(
Some(":wq<ret>"),
Some(&|app| {
helpers::assert_status_not_error(&app.editor);
assert_eq!(1, app.editor.tree.views().count());
let mut docs: Vec<_> = app.editor.documents().collect();
assert_eq!(1, docs.len());
let doc = docs.pop().unwrap();
assert_eq!(
helpers::platform_line("hello\ngoodbye"),
doc.text().to_string()
);
assert!(!doc.is_modified());
}),
),
],
false,
)
.await?;
helpers::assert_file_has_content(
file.as_file_mut(),
&helpers::platform_line("hello\ngoodbye"),
)?;
Ok(())
}

@ -4,7 +4,6 @@ use std::{
}; };
use helix_core::diagnostic::Severity; use helix_core::diagnostic::Severity;
use helix_term::application::Application;
use helix_view::doc; use helix_view::doc;
use super::*; use super::*;
@ -12,9 +11,12 @@ use super::*;
#[tokio::test] #[tokio::test]
async fn test_write() -> anyhow::Result<()> { async fn test_write() -> anyhow::Result<()> {
let mut file = tempfile::NamedTempFile::new()?; let mut file = tempfile::NamedTempFile::new()?;
let mut app = helpers::AppBuilder::new()
.with_file(file.path(), None)
.build()?;
test_key_sequence( test_key_sequence(
&mut helpers::app_with_file(file.path())?, &mut app,
Some("ithe gostak distims the doshes<ret><esc>:w<ret>"), Some("ithe gostak distims the doshes<ret><esc>:w<ret>"),
None, None,
false, false,
@ -35,12 +37,15 @@ async fn test_write() -> anyhow::Result<()> {
Ok(()) Ok(())
} }
#[tokio::test] #[tokio::test(flavor = "multi_thread")]
async fn test_write_quit() -> anyhow::Result<()> { async fn test_write_quit() -> anyhow::Result<()> {
let mut file = tempfile::NamedTempFile::new()?; let mut file = tempfile::NamedTempFile::new()?;
let mut app = helpers::AppBuilder::new()
.with_file(file.path(), None)
.build()?;
test_key_sequence( test_key_sequence(
&mut helpers::app_with_file(file.path())?, &mut app,
Some("ithe gostak distims the doshes<ret><esc>:wq<ret>"), Some("ithe gostak distims the doshes<ret><esc>:wq<ret>"),
None, None,
true, true,
@ -61,25 +66,21 @@ async fn test_write_quit() -> anyhow::Result<()> {
Ok(()) Ok(())
} }
#[tokio::test] #[tokio::test(flavor = "multi_thread")]
#[ignore]
async fn test_write_concurrent() -> anyhow::Result<()> { async fn test_write_concurrent() -> anyhow::Result<()> {
let mut file = tempfile::NamedTempFile::new()?; let mut file = tempfile::NamedTempFile::new()?;
let mut command = String::new(); let mut command = String::new();
const RANGE: RangeInclusive<i32> = 1..=5000; const RANGE: RangeInclusive<i32> = 1..=5000;
let mut app = helpers::AppBuilder::new()
.with_file(file.path(), None)
.build()?;
for i in RANGE { for i in RANGE {
let cmd = format!("%c{}<esc>:w<ret>", i); let cmd = format!("%c{}<esc>:w<ret>", i);
command.push_str(&cmd); command.push_str(&cmd);
} }
test_key_sequence( test_key_sequence(&mut app, Some(&command), None, false).await?;
&mut helpers::app_with_file(file.path())?,
Some(&command),
None,
false,
)
.await?;
file.as_file_mut().flush()?; file.as_file_mut().flush()?;
file.as_file_mut().sync_all()?; file.as_file_mut().sync_all()?;
@ -92,12 +93,14 @@ async fn test_write_concurrent() -> anyhow::Result<()> {
} }
#[tokio::test] #[tokio::test]
#[ignore]
async fn test_write_fail_mod_flag() -> anyhow::Result<()> { async fn test_write_fail_mod_flag() -> anyhow::Result<()> {
let file = helpers::new_readonly_tempfile()?; let file = helpers::new_readonly_tempfile()?;
let mut app = helpers::AppBuilder::new()
.with_file(file.path(), None)
.build()?;
test_key_sequences( test_key_sequences(
&mut helpers::app_with_file(file.path())?, &mut app,
vec![ vec![
( (
None, None,
@ -131,12 +134,127 @@ async fn test_write_fail_mod_flag() -> anyhow::Result<()> {
} }
#[tokio::test] #[tokio::test]
#[ignore] async fn test_write_scratch_to_new_path() -> anyhow::Result<()> {
let mut file = tempfile::NamedTempFile::new()?;
test_key_sequence(
&mut AppBuilder::new().build()?,
Some(format!("ihello<esc>:w {}<ret>", file.path().to_string_lossy()).as_ref()),
Some(&|app| {
assert!(!app.editor.is_err());
let mut docs: Vec<_> = app.editor.documents().collect();
assert_eq!(1, docs.len());
let doc = docs.pop().unwrap();
assert_eq!(Some(&file.path().to_path_buf()), doc.path());
}),
false,
)
.await?;
helpers::assert_file_has_content(file.as_file_mut(), &helpers::platform_line("hello"))?;
Ok(())
}
#[tokio::test]
async fn test_write_scratch_no_path_fails() -> anyhow::Result<()> {
helpers::test_key_sequence_with_input_text(
None,
("#[\n|]#", "ihello<esc>:w<ret>", "hello#[\n|]#"),
&|app| {
assert!(app.editor.is_err());
let mut docs: Vec<_> = app.editor.documents().collect();
assert_eq!(1, docs.len());
let doc = docs.pop().unwrap();
assert_eq!(None, doc.path());
},
false,
)
.await?;
Ok(())
}
#[tokio::test]
async fn test_write_auto_format_fails_still_writes() -> anyhow::Result<()> {
let mut file = tempfile::Builder::new().suffix(".rs").tempfile()?;
let lang_conf = indoc! {r#"
[[language]]
name = "rust"
formatter = { command = "bash", args = [ "-c", "exit 1" ] }
"#};
let mut app = helpers::AppBuilder::new()
.with_file(file.path(), None)
.with_input_text("#[l|]#et foo = 0;\n")
.with_lang_config(helpers::test_syntax_conf(Some(lang_conf.into())))
.build()?;
test_key_sequences(&mut app, vec![(Some(":w<ret>"), None)], false).await?;
// file still saves
helpers::assert_file_has_content(file.as_file_mut(), "let foo = 0;\n")?;
Ok(())
}
#[tokio::test]
async fn test_write_new_path() -> anyhow::Result<()> {
let mut file1 = tempfile::NamedTempFile::new().unwrap();
let mut file2 = tempfile::NamedTempFile::new().unwrap();
let mut app = helpers::AppBuilder::new()
.with_file(file1.path(), None)
.build()?;
test_key_sequences(
&mut app,
vec![
(
Some("ii can eat glass, it will not hurt me<ret><esc>:w<ret>"),
Some(&|app| {
let doc = doc!(app.editor);
assert!(!app.editor.is_err());
assert_eq!(file1.path(), doc.path().unwrap());
}),
),
(
Some(&format!(":w {}<ret>", file2.path().to_string_lossy())),
Some(&|app| {
let doc = doc!(app.editor);
assert!(!app.editor.is_err());
assert_eq!(file2.path(), doc.path().unwrap());
assert!(app.editor.document_by_path(file1.path()).is_none());
}),
),
],
false,
)
.await?;
helpers::assert_file_has_content(
file1.as_file_mut(),
&helpers::platform_line("i can eat glass, it will not hurt me\n"),
)?;
helpers::assert_file_has_content(
file2.as_file_mut(),
&helpers::platform_line("i can eat glass, it will not hurt me\n"),
)?;
Ok(())
}
#[tokio::test]
async fn test_write_fail_new_path() -> anyhow::Result<()> { async fn test_write_fail_new_path() -> anyhow::Result<()> {
let file = helpers::new_readonly_tempfile()?; let file = helpers::new_readonly_tempfile()?;
test_key_sequences( test_key_sequences(
&mut Application::new(Args::default(), Config::default())?, &mut AppBuilder::new().build()?,
vec![ vec![
( (
None, None,

@ -83,6 +83,18 @@ impl Serialize for Mode {
} }
} }
/// A snapshot of the text of a document that we want to write out to disk
#[derive(Debug, Clone)]
pub struct DocumentSavedEvent {
pub revision: usize,
pub doc_id: DocumentId,
pub path: PathBuf,
pub text: Rope,
}
pub type DocumentSavedEventResult = Result<DocumentSavedEvent, anyhow::Error>;
pub type DocumentSavedEventFuture = BoxFuture<'static, DocumentSavedEventResult>;
pub struct Document { pub struct Document {
pub(crate) id: DocumentId, pub(crate) id: DocumentId,
text: Rope, text: Rope,
@ -492,45 +504,61 @@ impl Document {
Some(fut.boxed()) Some(fut.boxed())
} }
pub fn save(&mut self, force: bool) -> impl Future<Output = Result<(), anyhow::Error>> { pub fn save<P: Into<PathBuf>>(
self.save_impl::<futures_util::future::Ready<_>>(None, force)
}
pub fn format_and_save(
&mut self, &mut self,
formatting: Option<impl Future<Output = Result<Transaction, FormatterError>>>, path: Option<P>,
force: bool, force: bool,
) -> impl Future<Output = anyhow::Result<()>> { ) -> Result<
self.save_impl(formatting, force) impl Future<Output = Result<DocumentSavedEvent, anyhow::Error>> + 'static + Send,
anyhow::Error,
> {
let path = path.map(|path| path.into());
self.save_impl(path, force)
// futures_util::future::Ready<_>,
} }
// TODO: do we need some way of ensuring two save operations on the same doc can't run at once?
// or is that handled by the OS/async layer
/// The `Document`'s text is encoded according to its encoding and written to the file located /// The `Document`'s text is encoded according to its encoding and written to the file located
/// at its `path()`. /// at its `path()`.
/// fn save_impl(
/// If `formatting` is present, it supplies some changes that we apply to the text before saving.
fn save_impl<F: Future<Output = Result<Transaction, FormatterError>>>(
&mut self, &mut self,
formatting: Option<F>, path: Option<PathBuf>,
force: bool, force: bool,
) -> impl Future<Output = Result<(), anyhow::Error>> { ) -> Result<
impl Future<Output = Result<DocumentSavedEvent, anyhow::Error>> + 'static + Send,
anyhow::Error,
> {
log::debug!(
"submitting save of doc '{:?}'",
self.path().map(|path| path.to_string_lossy())
);
// we clone and move text + path into the future so that we asynchronously save the current // we clone and move text + path into the future so that we asynchronously save the current
// state without blocking any further edits. // state without blocking any further edits.
let text = self.text().clone();
let path = match path {
Some(path) => helix_core::path::get_canonicalized_path(&path)?,
None => {
if self.path.is_none() {
bail!("Can't save with no path set!");
}
let mut text = self.text().clone(); self.path.as_ref().unwrap().clone()
let path = self.path.clone().expect("Can't save with no path set!"); }
let identifier = self.identifier(); };
let identifier = self.path().map(|_| self.identifier());
let language_server = self.language_server.clone(); let language_server = self.language_server.clone();
// mark changes up to now as saved // mark changes up to now as saved
self.reset_modified(); let current_rev = self.get_current_revision();
let doc_id = self.id();
let encoding = self.encoding; let encoding = self.encoding;
// We encode the file according to the `Document`'s encoding. // We encode the file according to the `Document`'s encoding.
async move { let future = async move {
use tokio::fs::File; use tokio::fs::File;
if let Some(parent) = path.parent() { if let Some(parent) = path.parent() {
// TODO: display a prompt asking the user if the directories should be created // TODO: display a prompt asking the user if the directories should be created
@ -543,39 +571,34 @@ impl Document {
} }
} }
if let Some(fmt) = formatting { let mut file = File::create(&path).await?;
match fmt.await {
Ok(transaction) => {
let success = transaction.changes().apply(&mut text);
if !success {
// This shouldn't happen, because the transaction changes were generated
// from the same text we're saving.
log::error!("failed to apply format changes before saving");
}
}
Err(err) => {
// formatting failed: report error, and save file without modifications
log::error!("{}", err);
}
}
}
let mut file = File::create(path).await?;
to_writer(&mut file, encoding, &text).await?; to_writer(&mut file, encoding, &text).await?;
let event = DocumentSavedEvent {
revision: current_rev,
doc_id,
path,
text: text.clone(),
};
if let Some(language_server) = language_server { if let Some(language_server) = language_server {
if !language_server.is_initialized() { if !language_server.is_initialized() {
return Ok(()); return Ok(event);
} }
if let Some(identifier) = identifier {
if let Some(notification) = if let Some(notification) =
language_server.text_document_did_save(identifier, &text) language_server.text_document_did_save(identifier, &text)
{ {
notification.await?; notification.await?;
} }
} }
Ok(())
} }
Ok(event)
};
Ok(future)
} }
/// Detect the programming language based on the file type. /// Detect the programming language based on the file type.
@ -930,6 +953,12 @@ impl Document {
let history = self.history.take(); let history = self.history.take();
let current_revision = history.current_revision(); let current_revision = history.current_revision();
self.history.set(history); self.history.set(history);
log::debug!(
"id {} modified - last saved: {}, current: {}",
self.id,
self.last_saved_revision,
current_revision
);
current_revision != self.last_saved_revision || !self.changes.is_empty() current_revision != self.last_saved_revision || !self.changes.is_empty()
} }
@ -941,6 +970,30 @@ impl Document {
self.last_saved_revision = current_revision; self.last_saved_revision = current_revision;
} }
/// Set the document's latest saved revision to the given one.
pub fn set_last_saved_revision(&mut self, rev: usize) {
log::debug!(
"doc {} revision updated {} -> {}",
self.id,
self.last_saved_revision,
rev
);
self.last_saved_revision = rev;
}
/// Get the document's latest saved revision.
pub fn get_last_saved_revision(&mut self) -> usize {
self.last_saved_revision
}
/// Get the current revision number
pub fn get_current_revision(&mut self) -> usize {
let history = self.history.take();
let current_revision = history.current_revision();
self.history.set(history);
current_revision
}
/// Corresponding language scope name. Usually `source.<lang>`. /// Corresponding language scope name. Usually `source.<lang>`.
pub fn language_scope(&self) -> Option<&str> { pub fn language_scope(&self) -> Option<&str> {
self.language self.language

@ -1,6 +1,6 @@
use crate::{ use crate::{
clipboard::{get_clipboard_provider, ClipboardProvider}, clipboard::{get_clipboard_provider, ClipboardProvider},
document::Mode, document::{DocumentSavedEventFuture, DocumentSavedEventResult, Mode},
graphics::{CursorKind, Rect}, graphics::{CursorKind, Rect},
info::Info, info::Info,
input::KeyEvent, input::KeyEvent,
@ -9,8 +9,9 @@ use crate::{
Document, DocumentId, View, ViewId, Document, DocumentId, View, ViewId,
}; };
use futures_util::future;
use futures_util::stream::select_all::SelectAll; use futures_util::stream::select_all::SelectAll;
use futures_util::{future, StreamExt};
use helix_lsp::Call;
use tokio_stream::wrappers::UnboundedReceiverStream; use tokio_stream::wrappers::UnboundedReceiverStream;
use std::{ use std::{
@ -28,7 +29,7 @@ use tokio::{
time::{sleep, Duration, Instant, Sleep}, time::{sleep, Duration, Instant, Sleep},
}; };
use anyhow::Error; use anyhow::{anyhow, bail, Error};
pub use helix_core::diagnostic::Severity; pub use helix_core::diagnostic::Severity;
pub use helix_core::register::Registers; pub use helix_core::register::Registers;
@ -65,7 +66,7 @@ where
) )
} }
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case", default, deny_unknown_fields)] #[serde(rename_all = "kebab-case", default, deny_unknown_fields)]
pub struct FilePickerConfig { pub struct FilePickerConfig {
/// IgnoreOptions /// IgnoreOptions
@ -172,7 +173,7 @@ pub struct Config {
pub color_modes: bool, pub color_modes: bool,
} }
#[derive(Debug, Default, Clone, PartialEq, Serialize, Deserialize)] #[derive(Debug, Default, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(default, rename_all = "kebab-case", deny_unknown_fields)] #[serde(default, rename_all = "kebab-case", deny_unknown_fields)]
pub struct TerminalConfig { pub struct TerminalConfig {
pub command: String, pub command: String,
@ -225,7 +226,7 @@ pub fn get_terminal_provider() -> Option<TerminalConfig> {
None None
} }
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(default, rename_all = "kebab-case", deny_unknown_fields)] #[serde(default, rename_all = "kebab-case", deny_unknown_fields)]
pub struct LspConfig { pub struct LspConfig {
/// Display LSP progress messages below statusline /// Display LSP progress messages below statusline
@ -246,7 +247,7 @@ impl Default for LspConfig {
} }
} }
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case", default, deny_unknown_fields)] #[serde(rename_all = "kebab-case", default, deny_unknown_fields)]
pub struct SearchConfig { pub struct SearchConfig {
/// Smart case: Case insensitive searching unless pattern contains upper case characters. Defaults to true. /// Smart case: Case insensitive searching unless pattern contains upper case characters. Defaults to true.
@ -255,7 +256,7 @@ pub struct SearchConfig {
pub wrap_around: bool, pub wrap_around: bool,
} }
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case", default, deny_unknown_fields)] #[serde(rename_all = "kebab-case", default, deny_unknown_fields)]
pub struct StatusLineConfig { pub struct StatusLineConfig {
pub left: Vec<StatusLineElement>, pub left: Vec<StatusLineElement>,
@ -279,7 +280,7 @@ impl Default for StatusLineConfig {
} }
} }
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case", default, deny_unknown_fields)] #[serde(rename_all = "kebab-case", default, deny_unknown_fields)]
pub struct ModeConfig { pub struct ModeConfig {
pub normal: String, pub normal: String,
@ -458,7 +459,7 @@ impl std::str::FromStr for GutterType {
} }
} }
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(default)] #[serde(default)]
pub struct WhitespaceConfig { pub struct WhitespaceConfig {
pub render: WhitespaceRender, pub render: WhitespaceRender,
@ -643,12 +644,21 @@ pub struct Breakpoint {
pub log_message: Option<String>, pub log_message: Option<String>,
} }
use futures_util::stream::{Flatten, Once};
pub struct Editor { pub struct Editor {
/// Current editing mode. /// Current editing mode.
pub mode: Mode, pub mode: Mode,
pub tree: Tree, pub tree: Tree,
pub next_document_id: DocumentId, pub next_document_id: DocumentId,
pub documents: BTreeMap<DocumentId, Document>, pub documents: BTreeMap<DocumentId, Document>,
// We Flatten<> to resolve the inner DocumentSavedEventFuture. For that we need a stream of streams, hence the Once<>.
// https://stackoverflow.com/a/66875668
pub saves: HashMap<DocumentId, UnboundedSender<Once<DocumentSavedEventFuture>>>,
pub save_queue: SelectAll<Flatten<UnboundedReceiverStream<Once<DocumentSavedEventFuture>>>>,
pub write_count: usize,
pub count: Option<std::num::NonZeroUsize>, pub count: Option<std::num::NonZeroUsize>,
pub selected_register: Option<char>, pub selected_register: Option<char>,
pub registers: Registers, pub registers: Registers,
@ -688,6 +698,15 @@ pub struct Editor {
pub config_events: (UnboundedSender<ConfigEvent>, UnboundedReceiver<ConfigEvent>), pub config_events: (UnboundedSender<ConfigEvent>, UnboundedReceiver<ConfigEvent>),
} }
#[derive(Debug)]
pub enum EditorEvent {
DocumentSaved(DocumentSavedEventResult),
ConfigEvent(ConfigEvent),
LanguageServerMessage((usize, Call)),
DebuggerEvent(dap::Payload),
IdleTimer,
}
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub enum ConfigEvent { pub enum ConfigEvent {
Refresh, Refresh,
@ -719,6 +738,8 @@ pub enum CloseError {
DoesNotExist, DoesNotExist,
/// Buffer is modified /// Buffer is modified
BufferModified(String), BufferModified(String),
/// Document failed to save
SaveError(anyhow::Error),
} }
impl Editor { impl Editor {
@ -739,6 +760,9 @@ impl Editor {
tree: Tree::new(area), tree: Tree::new(area),
next_document_id: DocumentId::default(), next_document_id: DocumentId::default(),
documents: BTreeMap::new(), documents: BTreeMap::new(),
saves: HashMap::new(),
save_queue: SelectAll::new(),
write_count: 0,
count: None, count: None,
selected_register: None, selected_register: None,
macro_recording: None, macro_recording: None,
@ -804,12 +828,16 @@ impl Editor {
#[inline] #[inline]
pub fn set_status<T: Into<Cow<'static, str>>>(&mut self, status: T) { pub fn set_status<T: Into<Cow<'static, str>>>(&mut self, status: T) {
self.status_msg = Some((status.into(), Severity::Info)); let status = status.into();
log::debug!("editor status: {}", status);
self.status_msg = Some((status, Severity::Info));
} }
#[inline] #[inline]
pub fn set_error<T: Into<Cow<'static, str>>>(&mut self, error: T) { pub fn set_error<T: Into<Cow<'static, str>>>(&mut self, error: T) {
self.status_msg = Some((error.into(), Severity::Error)); let error = error.into();
log::error!("editor error: {}", error);
self.status_msg = Some((error, Severity::Error));
} }
#[inline] #[inline]
@ -1034,6 +1062,13 @@ impl Editor {
DocumentId(unsafe { NonZeroUsize::new_unchecked(self.next_document_id.0.get() + 1) }); DocumentId(unsafe { NonZeroUsize::new_unchecked(self.next_document_id.0.get() + 1) });
doc.id = id; doc.id = id;
self.documents.insert(id, doc); self.documents.insert(id, doc);
let (save_sender, save_receiver) = tokio::sync::mpsc::unbounded_channel();
self.saves.insert(id, save_sender);
let stream = UnboundedReceiverStream::new(save_receiver).flatten();
self.save_queue.push(stream);
id id
} }
@ -1080,16 +1115,19 @@ impl Editor {
} }
pub fn close_document(&mut self, doc_id: DocumentId, force: bool) -> Result<(), CloseError> { pub fn close_document(&mut self, doc_id: DocumentId, force: bool) -> Result<(), CloseError> {
let doc = match self.documents.get(&doc_id) { let doc = match self.documents.get_mut(&doc_id) {
Some(doc) => doc, Some(doc) => doc,
None => return Err(CloseError::DoesNotExist), None => return Err(CloseError::DoesNotExist),
}; };
if !force && doc.is_modified() { if !force && doc.is_modified() {
return Err(CloseError::BufferModified(doc.display_name().into_owned())); return Err(CloseError::BufferModified(doc.display_name().into_owned()));
} }
// This will also disallow any follow-up writes
self.saves.remove(&doc_id);
if let Some(language_server) = doc.language_server() { if let Some(language_server) = doc.language_server() {
// TODO: track error
tokio::spawn(language_server.text_document_did_close(doc.identifier())); tokio::spawn(language_server.text_document_did_close(doc.identifier()));
} }
@ -1152,6 +1190,32 @@ impl Editor {
Ok(()) Ok(())
} }
pub fn save<P: Into<PathBuf>>(
&mut self,
doc_id: DocumentId,
path: Option<P>,
force: bool,
) -> anyhow::Result<()> {
// convert a channel of futures to pipe into main queue one by one
// via stream.then() ? then push into main future
let path = path.map(|path| path.into());
let doc = doc_mut!(self, &doc_id);
let future = doc.save(path, force)?;
use futures_util::stream;
self.saves
.get(&doc_id)
.ok_or_else(|| anyhow::format_err!("saves are closed for this document!"))?
.send(stream::once(Box::pin(future)))
.map_err(|err| anyhow!("failed to send save event: {}", err))?;
self.write_count += 1;
Ok(())
}
pub fn resize(&mut self, area: Rect) { pub fn resize(&mut self, area: Rect) {
if self.tree.resize(area) { if self.tree.resize(area) {
self._refresh(); self._refresh();
@ -1252,14 +1316,14 @@ impl Editor {
} }
} }
/// Closes language servers with timeout. The default timeout is 500 ms, use /// Closes language servers with timeout. The default timeout is 10000 ms, use
/// `timeout` parameter to override this. /// `timeout` parameter to override this.
pub async fn close_language_servers( pub async fn close_language_servers(
&self, &self,
timeout: Option<u64>, timeout: Option<u64>,
) -> Result<(), tokio::time::error::Elapsed> { ) -> Result<(), tokio::time::error::Elapsed> {
tokio::time::timeout( tokio::time::timeout(
Duration::from_millis(timeout.unwrap_or(500)), Duration::from_millis(timeout.unwrap_or(3000)),
future::join_all( future::join_all(
self.language_servers self.language_servers
.iter_clients() .iter_clients()
@ -1269,4 +1333,48 @@ impl Editor {
.await .await
.map(|_| ()) .map(|_| ())
} }
pub async fn wait_event(&mut self) -> EditorEvent {
tokio::select! {
biased;
Some(event) = self.save_queue.next() => {
self.write_count -= 1;
EditorEvent::DocumentSaved(event)
}
Some(config_event) = self.config_events.1.recv() => {
EditorEvent::ConfigEvent(config_event)
}
Some(message) = self.language_servers.incoming.next() => {
EditorEvent::LanguageServerMessage(message)
}
Some(event) = self.debugger_events.next() => {
EditorEvent::DebuggerEvent(event)
}
_ = &mut self.idle_timer => {
EditorEvent::IdleTimer
}
}
}
pub async fn flush_writes(&mut self) -> anyhow::Result<()> {
while self.write_count > 0 {
if let Some(save_event) = self.save_queue.next().await {
self.write_count -= 1;
let save_event = match save_event {
Ok(event) => event,
Err(err) => {
self.set_error(err.to_string());
bail!(err);
}
};
let doc = doc_mut!(self, &save_event.doc_id);
doc.set_last_saved_revision(save_event.revision);
}
}
Ok(())
}
} }

Loading…
Cancel
Save