diff --git a/book/src/languages.md b/book/src/languages.md index ff06dc00d4256..33aaea22464be 100644 --- a/book/src/languages.md +++ b/book/src/languages.md @@ -96,7 +96,7 @@ The `language-server` field takes the following keys: | Key | Description | | --- | ----------- | | `command` | The name of the language server binary to execute. Binaries must be in `$PATH` | -| `args` | A list of arguments to pass to the language server binary | +| `args` | A list of arguments to pass to the language server binary. Those can have `"{}"` as a placeholder for the current file path. | | `timeout` | The maximum time a request to the language server may take, in seconds. Defaults to `20` | | `language-id` | The language name to pass to the language server. Some language servers support multiple languages and use this field to determine which one is being served in a buffer | | `environment` | Any environment variables that will be used when starting the language server `{ "KEY1" = "Value1", "KEY2" = "Value2" }` | diff --git a/helix-term/src/commands/typed.rs b/helix-term/src/commands/typed.rs index de24c4fba5ca9..3d7ffc50d5082 100644 --- a/helix-term/src/commands/typed.rs +++ b/helix-term/src/commands/typed.rs @@ -288,21 +288,23 @@ fn write_impl( ) -> anyhow::Result<()> { let editor_auto_fmt = cx.editor.config().auto_format; let jobs = &mut cx.jobs; + let redraw_handle = cx.editor.redraw_handle.clone(); let (view, doc) = current!(cx.editor); let path = path.map(AsRef::as_ref); let fmt = if editor_auto_fmt { - doc.auto_format().map(|fmt| { - let callback = make_format_callback( - doc.id(), - doc.version(), - view.id, - fmt, - Some((path.map(Into::into), force)), - ); - - jobs.add(Job::with_callback(callback).wait_before_exiting()); - }) + doc.auto_format(view, &cx.editor.diff_providers, redraw_handle) + .map(|fmt| { + let callback = make_format_callback( + doc.id(), + doc.version(), + view.id, + fmt, + Some((path.map(Into::into), force)), + ); + + jobs.add(Job::with_callback(callback).wait_before_exiting()); + }) } else { None }; @@ -363,7 +365,9 @@ fn format( } let (view, doc) = current!(cx.editor); - if let Some(format) = doc.format() { + let redraw_handle = cx.editor.redraw_handle.clone(); + if let Some(format) = doc.format(view, &cx.editor.diff_providers, redraw_handle) { + let view = view!(cx.editor); let callback = make_format_callback(doc.id(), doc.version(), view.id, format, None); cx.jobs.callback(callback); } @@ -586,7 +590,7 @@ pub fn write_all_impl( let mut errors: Vec<&'static str> = Vec::new(); let auto_format = cx.editor.config().auto_format; let jobs = &mut cx.jobs; - let current_view = view!(cx.editor); + let current_view = view_mut!(cx.editor); // save all documents let saves: Vec<_> = cx @@ -620,7 +624,12 @@ pub fn write_all_impl( }; let fmt = if auto_format { - doc.auto_format().map(|fmt| { + doc.auto_format( + current_view, + &cx.editor.diff_providers, + cx.editor.redraw_handle.clone(), + ) + .map(|fmt| { let callback = make_format_callback( doc.id(), doc.version(), diff --git a/helix-view/src/document.rs b/helix-view/src/document.rs index 856e5628ab42e..6349ea6dde640 100644 --- a/helix-view/src/document.rs +++ b/helix-view/src/document.rs @@ -413,9 +413,14 @@ impl Document { /// The same as [`format`], but only returns formatting changes if auto-formatting /// is configured. - pub fn auto_format(&self) -> Option>> { + pub fn auto_format( + &mut self, + view: &mut View, + provider_registry: &DiffProviderRegistry, + redraw_handle: RedrawHandle, + ) -> Option>> { if self.language_config()?.auto_format { - self.format() + self.format(view, provider_registry, redraw_handle) } else { None } @@ -425,61 +430,114 @@ impl Document { /// to format it nicely. // We can't use anyhow::Result here since the output of the future has to be // clonable to be used as shared future. So use a custom error type. - pub fn format(&self) -> Option>> { - if let Some(formatter) = self + pub fn format( + &mut self, + view: &mut View, + provider_registry: &DiffProviderRegistry, + redraw_handle: RedrawHandle, + ) -> Option>> { + if let Some(mut formatter) = self .language_config() .and_then(|c| c.formatter.clone()) .filter(|formatter| which::which(&formatter.command).is_ok()) { use std::process::Stdio; - let text = self.text().clone(); - let mut process = tokio::process::Command::new(&formatter.command); - process - .args(&formatter.args) - .stdin(Stdio::piped()) - .stdout(Stdio::piped()) - .stderr(Stdio::piped()); - - let formatting_future = async move { - let mut process = process - .spawn() - .map_err(|e| FormatterError::SpawningFailed { + let mut needs_file = false; + for i in 0..formatter.args.len() { + if let Some(arg) = formatter.args.get_mut(i) { + if arg.contains("{}") { + let path = self.path()?.to_str().unwrap_or(""); + needs_file = true; + *arg = arg.replace("{}", path); + } + } + } + if !needs_file { + let text = self.text().clone(); + let mut process = tokio::process::Command::new(&formatter.command); + process + .args(&formatter.args) + .stdin(Stdio::piped()) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()); + + let formatting_future = async move { + let mut process = + process + .spawn() + .map_err(|e| FormatterError::SpawningFailed { + command: formatter.command.clone(), + error: e.kind(), + })?; + { + let mut stdin = process.stdin.take().ok_or(FormatterError::BrokenStdin)?; + to_writer(&mut stdin, encoding::UTF_8, &text) + .await + .map_err(|_| FormatterError::BrokenStdin)?; + } + + let output = process + .wait_with_output() + .await + .map_err(|_| FormatterError::WaitForOutputFailed)?; + + if !output.status.success() { + if !output.stderr.is_empty() { + let err = String::from_utf8_lossy(&output.stderr).to_string(); + log::error!("Formatter error: {}", err); + return Err(FormatterError::NonZeroExitStatus(Some(err))); + } + + return Err(FormatterError::NonZeroExitStatus(None)); + } else if !output.stderr.is_empty() { + log::debug!( + "Formatter printed to stderr: {}", + String::from_utf8_lossy(&output.stderr).to_string() + ); + } + + let str = std::str::from_utf8(&output.stdout) + .map_err(|_| FormatterError::InvalidUtf8Output)?; + + Ok(helix_core::diff::compare_ropes(&text, &Rope::from(str))) + }; + Some(formatting_future.boxed()) + } else { + let process = std::process::Command::new(&formatter.command) + .args(&formatter.args) + .stderr(Stdio::piped()) + .spawn(); + + // File type formatters are run synchronously since the doc + // has to be reloaded from disk rather than being resolved + // through a transaction. + let format_and_reload = || -> Result<(), FormatterError> { + let process = process.map_err(|e| FormatterError::SpawningFailed { command: formatter.command.clone(), error: e.kind(), })?; - { - let mut stdin = process.stdin.take().ok_or(FormatterError::BrokenStdin)?; - to_writer(&mut stdin, encoding::UTF_8, &text) - .await - .map_err(|_| FormatterError::BrokenStdin)?; - } + let output = process + .wait_with_output() + .map_err(|_| FormatterError::WaitForOutputFailed)?; - let output = process - .wait_with_output() - .await - .map_err(|_| FormatterError::WaitForOutputFailed)?; - - if !output.status.success() { if !output.stderr.is_empty() { - let err = String::from_utf8_lossy(&output.stderr).to_string(); - log::error!("Formatter error: {}", err); - return Err(FormatterError::NonZeroExitStatus(Some(err))); + return Err(FormatterError::NonZeroExitStatus(Some( + String::from_utf8_lossy(&output.stderr).to_string(), + ))); + } else if let Err(e) = self.reload(view, provider_registry, redraw_handle) { + return Err(FormatterError::DiskReloadError(e.to_string())); } - return Err(FormatterError::NonZeroExitStatus(None)); - } else if !output.stderr.is_empty() { - log::debug!( - "Formatter printed to stderr: {}", - String::from_utf8_lossy(&output.stderr).to_string() - ); - } + Ok(()) + }; - let str = std::str::from_utf8(&output.stdout) - .map_err(|_| FormatterError::InvalidUtf8Output)?; + let format_result = format_and_reload(); + let text = self.text().clone(); - Ok(helix_core::diff::compare_ropes(&text, &Rope::from(str))) + // Generate an empty transaction as a placeholder + let future = async move { format_result.map(|_| Transaction::new(&text)) }; + Some(future.boxed()) }; - return Some(formatting_future.boxed()); }; let language_server = self.language_server()?;