document should save even if formatter fails

pull/2267/head
Skyler Hawthorne 2 years ago
parent b530a86d1f
commit 3f07885b35

@ -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,7 +1,6 @@
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, path::get_relative_path,
pos_at_coords, syntax, Selection, pos_at_coords, syntax, Selection,
@ -110,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();
@ -137,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")?;

@ -2518,7 +2518,8 @@ async fn make_format_callback(
format: impl Future<Output = Result<Transaction, FormatterError>> + Send + 'static, format: impl Future<Output = Result<Transaction, FormatterError>> + Send + 'static,
write: Option<(Option<PathBuf>, bool)>, 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 = Callback::Editor(Box::new(move |editor| { 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;
@ -2528,20 +2529,22 @@ async fn make_format_callback(
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);
} else {
log::info!("discarded formatting changes because the document changed");
}
}
if let Some((path, force)) = write { if let Some((path, force)) = write {
if let Err(err) = doc.save(path, force) { if let Err(err) = doc.save(path, force) {
editor.set_error(format!("Error saving: {}", err)); editor.set_error(format!("Error saving: {}", err));
} }
} }
} else {
log::info!("discarded formatting changes because the document changed");
}
})); }));
Ok(call) Ok(call)

@ -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?;

@ -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,16 +1,18 @@
use std::ops::RangeInclusive; use std::ops::RangeInclusive;
use helix_core::diagnostic::Severity; use helix_core::diagnostic::Severity;
use helix_term::application::Application;
use super::*; use super::*;
#[tokio::test(flavor = "multi_thread")] #[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(); let mut docs: Vec<_> = app.editor.documents().collect();
@ -30,7 +32,7 @@ async fn test_write_quit_fail() -> anyhow::Result<()> {
#[tokio::test(flavor = "multi_thread")] #[tokio::test(flavor = "multi_thread")]
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,
@ -70,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());

@ -108,7 +108,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);
@ -132,16 +132,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),
@ -162,7 +176,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>>(
@ -207,16 +227,60 @@ 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<()> { pub fn assert_file_has_content(file: &mut File, content: &str) -> anyhow::Result<()> {

@ -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();

@ -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());

@ -6,8 +6,12 @@ async fn test_split_write_quit_all() -> anyhow::Result<()> {
let mut file2 = tempfile::NamedTempFile::new()?; let mut file2 = tempfile::NamedTempFile::new()?;
let mut file3 = tempfile::NamedTempFile::new()?; let mut file3 = tempfile::NamedTempFile::new()?;
let mut app = helpers::AppBuilder::new()
.with_file(file1.path(), None)
.build()?;
test_key_sequences( test_key_sequences(
&mut helpers::app_with_file(file1.path())?, &mut app,
vec![ vec![
( (
Some(&format!( Some(&format!(
@ -66,9 +70,12 @@ async fn test_split_write_quit_all() -> anyhow::Result<()> {
#[tokio::test(flavor = "multi_thread")] #[tokio::test(flavor = "multi_thread")]
async fn test_split_write_quit_same_file() -> anyhow::Result<()> { async fn test_split_write_quit_same_file() -> 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_sequences( test_key_sequences(
&mut helpers::app_with_file(file.path())?, &mut app,
vec![ vec![
( (
Some("O<esc>ihello<esc>:sp<ret>ogoodbye<esc>"), Some("O<esc>ihello<esc>:sp<ret>ogoodbye<esc>"),

@ -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,
@ -38,9 +40,12 @@ async fn test_write() -> anyhow::Result<()> {
#[tokio::test(flavor = "multi_thread")] #[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,
@ -66,19 +71,16 @@ 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()?;
@ -93,9 +95,12 @@ async fn test_write_concurrent() -> anyhow::Result<()> {
#[tokio::test] #[tokio::test]
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,
@ -133,7 +138,7 @@ async fn test_write_scratch_to_new_path() -> anyhow::Result<()> {
let mut file = tempfile::NamedTempFile::new()?; let mut file = tempfile::NamedTempFile::new()?;
test_key_sequence( test_key_sequence(
&mut Application::new(Args::default(), Config::default())?, &mut AppBuilder::new().build()?,
Some(format!("ihello<esc>:w {}<ret>", file.path().to_string_lossy()).as_ref()), Some(format!("ihello<esc>:w {}<ret>", file.path().to_string_lossy()).as_ref()),
Some(&|app| { Some(&|app| {
assert!(!app.editor.is_err()); assert!(!app.editor.is_err());
@ -174,19 +179,40 @@ async fn test_write_scratch_no_path_fails() -> anyhow::Result<()> {
Ok(()) 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] #[tokio::test]
async fn test_write_new_path() -> anyhow::Result<()> { async fn test_write_new_path() -> anyhow::Result<()> {
let mut file1 = tempfile::NamedTempFile::new().unwrap(); let mut file1 = tempfile::NamedTempFile::new().unwrap();
let mut file2 = 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( test_key_sequences(
&mut Application::new( &mut app,
Args {
files: vec![(file1.path().to_path_buf(), Position::default())],
..Default::default()
},
Config::default(),
)?,
vec![ vec![
( (
Some("ii can eat glass, it will not hurt me<ret><esc>:w<ret>"), Some("ii can eat glass, it will not hurt me<ret><esc>:w<ret>"),
@ -228,7 +254,7 @@ 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,

Loading…
Cancel
Save