on master help sign in
Change default formatter for any language (#2942)

* Change default formatter for any language * Fix clippy error * Close stdin for Stdio formatters * Better indentation and pattern matching * Return Result<Option<...>> for fn format instead of Option * Remove unwrap for stdin * Handle FormatterErrors instead of Result<Option<...>> * Use Transaction instead of LspFormatting * Use Transaction directly in Document::format * Perform stdin type formatting asynchronously * Rename formatter.type values to kebab-case * Debug format for displaying io::ErrorKind (msrv fix) * Solve conflict? * Use only stdio type formatters * Remove FormatterType enum * Remove old comment * Check if the formatter exited correctly * Add formatter configuration to the book * Avoid allocations when writing to stdin and formatting errors * Remove unused import Co-authored-by: Gokul Soumya <gokulps15@gmail.com>

commit219d2c25156a496ed2923d4cef256352bb1302e5 authorPiergiorgioZagaria <75474806+PiergiorgioZagaria@users.noreply.github.com> dateThu 4 Aug 2022, 06:01 (4 years ago) parentc5f8a83
7 files changed+114−32
book/src/languages.md+2 helix-core/src/syntax.rs+12 helix-lsp/src/lib.rs−16 helix-term/src/commands.rs+4−4 helix-view/src/document.rs+93−10 helix-view/src/editor.rs+1−2 languages.toml+2
book/src/languages.md+2
@@ -40,6 +40,7 @@ file-types = ["mylang", "myl"]
4040comment-token = "#"
4141indent = { tab-width = 2, unit = " " }
4242language-server = { command = "mylang-lsp", args = ["--stdio"] }
43+formatter = { command = "mylang-formatter" , args = ["--stdin"] }
4344```
4445
4546These configuration keys are available:
@@ -59,6 +60,7 @@ These configuration keys are available:
5960| `language-server` | The Language Server to run. See the Language Server configuration section below. |
6061| `config` | Language Server configuration |
6162| `grammar` | The tree-sitter grammar to use (defaults to the value of `name`) |
63+| `formatter` | The formatter for the language, it will take precedence over the lsp when defined. The formatter must be able to take the original file as input from stdin and write the formatted file to stdout |
6264
6365### Language Server configuration
6466
helix-core/src/syntax.rs+12
@@ -79,6 +79,9 @@ pub struct LanguageConfiguration {
7979 #[serde(default)]
8080 pub auto_format: bool,
8181
82+ #[serde(skip_serializing_if = "Option::is_none")]
83+ pub formatter: Option<FormatterConfiguration>,
84+
8285 #[serde(default)]
8386 pub diagnostic_severity: Severity,
8487
@@ -126,6 +129,15 @@ pub struct LanguageServerConfiguration {
126129 pub language_id: Option<String>,
127130}
128131
132+#[derive(Debug, Clone, Serialize, Deserialize)]
133+#[serde(rename_all = "kebab-case")]
134+pub struct FormatterConfiguration {
135+ pub command: String,
136+ #[serde(default)]
137+ #[serde(skip_serializing_if = "Vec::is_empty")]
138+ pub args: Vec<String>,
139+}
140+
129141#[derive(Debug, PartialEq, Clone, Deserialize, Serialize)]
130142#[serde(rename_all = "kebab-case")]
131143pub struct AdvancedCompletion {
helix-lsp/src/lib.rs−16
@@ -213,22 +213,6 @@ pub mod util {
213213 }),
214214 )
215215 }
216−
217− /// The result of asking the language server to format the document. This can be turned into a
218− /// `Transaction`, but the advantage of not doing that straight away is that this one is
219− /// `Send` and `Sync`.
220− #[derive(Clone, Debug)]
221− pub struct LspFormatting {
222− pub doc: Rope,
223− pub edits: Vec<lsp::TextEdit>,
224− pub offset_encoding: OffsetEncoding,
225− }
226−
227− impl From<LspFormatting> for Transaction {
228− fn from(fmt: LspFormatting) -> Transaction {
229− generate_transaction_from_edits(&fmt.doc, fmt.edits, fmt.offset_encoding)
230− }
231− }
232216}
233217
234218#[derive(Debug, PartialEq, Clone)]
helix-term/src/commands.rs+4−4
@@ -28,7 +28,7 @@ use helix_core::{
2828};
2929use helix_view::{
3030 clipboard::ClipboardType,
31− document::{Mode, SCRATCH_BUFFER_NAME},
31+ document::{FormatterError, Mode, SCRATCH_BUFFER_NAME},
3232 editor::{Action, Motion},
3333 info::Info,
3434 input::KeyEvent,
@@ -2471,14 +2471,14 @@ async fn make_format_callback(
24712471 doc_id: DocumentId,
24722472 doc_version: i32,
24732473 modified: Modified,
2474− format: impl Future<Output = helix_lsp::util::LspFormatting> + Send + 'static,
2474+ format: impl Future<Output = Result<Transaction, FormatterError>> + Send + 'static,
24752475) -> anyhow::Result<job::Callback> {
2476− let format = format.await;
2476+ let format = format.await?;
24772477 let call: job::Callback = Box::new(move |editor, _compositor| {
24782478 let view_id = view!(editor).id;
24792479 if let Some(doc) = editor.document_mut(doc_id) {
24802480 if doc.version() == doc_version {
2481− doc.apply(&Transaction::from(format), view_id);
2481+ doc.apply(&format, view_id);
24822482 doc.append_changes_to_history(view_id);
24832483 doc.detect_indent_and_line_ending();
24842484 if let Modified::SetUnmodified = modified {
helix-view/src/document.rs+93−10
@@ -1,4 +1,6 @@
11use anyhow::{anyhow, bail, Context, Error};
2+use futures_util::future::BoxFuture;
3+use futures_util::FutureExt;
24use helix_core::auto_pairs::AutoPairs;
35use helix_core::Range;
46use serde::de::{self, Deserialize, Deserializer};
@@ -20,7 +22,6 @@ use helix_core::{
2022 ChangeSet, Diagnostic, LineEnding, Rope, RopeBuilder, Selection, State, Syntax, Transaction,
2123 DEFAULT_LINE_ENDING,
2224};
23−use helix_lsp::util::LspFormatting;
2425
2526use crate::{DocumentId, Editor, ViewId};
2627
@@ -397,7 +398,7 @@ impl Document {
397398
398399 /// The same as [`format`], but only returns formatting changes if auto-formatting
399400 /// is configured.
400− pub fn auto_format(&self) -> Option<impl Future<Output = LspFormatting> + 'static> {
401+ pub fn auto_format(&self) -> Option<BoxFuture<'static, Result<Transaction, FormatterError>>> {
401402 if self.language_config()?.auto_format {
402403 self.format()
403404 } else {
@@ -407,7 +408,56 @@ impl Document {
407408
408409 /// If supported, returns the changes that should be applied to this document in order
409410 /// to format it nicely.
410− pub fn format(&self) -> Option<impl Future<Output = LspFormatting> + 'static> {
411+ // We can't use anyhow::Result here since the output of the future has to be
412+ // clonable to be used as shared future. So use a custom error type.
413+ pub fn format(&self) -> Option<BoxFuture<'static, Result<Transaction, FormatterError>>> {
414+ if let Some(formatter) = self.language_config().and_then(|c| c.formatter.clone()) {
415+ use std::process::Stdio;
416+ let text = self.text().clone();
417+ let mut process = tokio::process::Command::new(&formatter.command);
418+ process
419+ .args(&formatter.args)
420+ .stdin(Stdio::piped())
421+ .stdout(Stdio::piped())
422+ .stderr(Stdio::piped());
423+
424+ let formatting_future = async move {
425+ let mut process = process
426+ .spawn()
427+ .map_err(|e| FormatterError::SpawningFailed {
428+ command: formatter.command.clone(),
429+ error: e.kind(),
430+ })?;
431+ {
432+ let mut stdin = process.stdin.take().ok_or(FormatterError::BrokenStdin)?;
433+ to_writer(&mut stdin, encoding::UTF_8, &text)
434+ .await
435+ .map_err(|_| FormatterError::BrokenStdin)?;
436+ }
437+
438+ let output = process
439+ .wait_with_output()
440+ .await
441+ .map_err(|_| FormatterError::WaitForOutputFailed)?;
442+
443+ if !output.stderr.is_empty() {
444+ return Err(FormatterError::Stderr(
445+ String::from_utf8_lossy(&output.stderr).to_string(),
446+ ));
447+ }
448+
449+ if !output.status.success() {
450+ return Err(FormatterError::NonZeroExitStatus);
451+ }
452+
453+ let str = String::from_utf8(output.stdout)
454+ .map_err(|_| FormatterError::InvalidUtf8Output)?;
455+
456+ Ok(helix_core::diff::compare_ropes(&text, &Rope::from(str)))
457+ };
458+ return Some(formatting_future.boxed());
459+ };
460+
411461 let language_server = self.language_server()?;
412462 let text = self.text.clone();
413463 let offset_encoding = language_server.offset_encoding();
@@ -427,13 +477,13 @@ impl Document {
427477 log::warn!("LSP formatting failed: {}", e);
428478 Default::default()
429479 });
430− LspFormatting {
431− doc: text,
480+ Ok(helix_lsp::util::generate_transaction_from_edits(
481+ &text,
432482 edits,
433483 offset_encoding,
434− }
484+ ))
435485 };
436− Some(fut)
486+ Some(fut.boxed())
437487 }
438488
439489 pub fn save(&mut self, force: bool) -> impl Future<Output = Result<(), anyhow::Error>> {
@@ -442,7 +492,7 @@ impl Document {
442492
443493 pub fn format_and_save(
444494 &mut self,
445− formatting: Option<impl Future<Output = LspFormatting>>,
495+ formatting: Option<impl Future<Output = Result<Transaction, FormatterError>>>,
446496 force: bool,
447497 ) -> impl Future<Output = anyhow::Result<()>> {
448498 self.save_impl(formatting, force)
@@ -454,7 +504,7 @@ impl Document {
454504 /// at its `path()`.
455505 ///
456506 /// If `formatting` is present, it supplies some changes that we apply to the text before saving.
457− fn save_impl<F: Future<Output = LspFormatting>>(
507+ fn save_impl<F: Future<Output = Result<Transaction, FormatterError>>>(
458508 &mut self,
459509 formatting: Option<F>,
460510 force: bool,
@@ -488,7 +538,8 @@ impl Document {
488538 }
489539
490540 if let Some(fmt) = formatting {
491− let success = Transaction::from(fmt.await).changes().apply(&mut text);
541+ let transaction = fmt.await?;
542+ let success = transaction.changes().apply(&mut text);
492543 if !success {
493544 // This shouldn't happen, because the transaction changes were generated
494545 // from the same text we're saving.
@@ -1034,6 +1085,38 @@ impl Default for Document {
10341085 }
10351086}
10361087
1088+#[derive(Clone, Debug)]
1089+pub enum FormatterError {
1090+ SpawningFailed {
1091+ command: String,
1092+ error: std::io::ErrorKind,
1093+ },
1094+ BrokenStdin,
1095+ WaitForOutputFailed,
1096+ Stderr(String),
1097+ InvalidUtf8Output,
1098+ DiskReloadError(String),
1099+ NonZeroExitStatus,
1100+}
1101+
1102+impl std::error::Error for FormatterError {}
1103+
1104+impl Display for FormatterError {
1105+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1106+ match self {
1107+ Self::SpawningFailed { command, error } => {
1108+ write!(f, "Failed to spawn formatter {}: {:?}", command, error)
1109+ }
1110+ Self::BrokenStdin => write!(f, "Could not write to formatter stdin"),
1111+ Self::WaitForOutputFailed => write!(f, "Waiting for formatter output failed"),
1112+ Self::Stderr(output) => write!(f, "Formatter error: {}", output),
1113+ Self::InvalidUtf8Output => write!(f, "Invalid UTF-8 formatter output"),
1114+ Self::DiskReloadError(error) => write!(f, "Error reloading file from disk: {}", error),
1115+ Self::NonZeroExitStatus => write!(f, "Formatter exited with non zero exit status:"),
1116+ }
1117+ }
1118+}
1119+
10371120#[cfg(test)]
10381121mod test {
10391122 use super::*;
helix-view/src/editor.rs+1−2
@@ -613,7 +613,6 @@ impl Editor {
613613 syn_loader: Arc<syntax::Loader>,
614614 config: Box<dyn DynAccess<Config>>,
615615 ) -> Self {
616− let language_servers = helix_lsp::Registry::new();
617616 let conf = config.load();
618617 let auto_pairs = (&conf.auto_pairs).into();
619618
@@ -629,7 +628,7 @@ impl Editor {
629628 macro_recording: None,
630629 macro_replaying: Vec::new(),
631630 theme: theme_loader.default(),
632− language_servers,
631+ language_servers: helix_lsp::Registry::new(),
633632 diagnostics: BTreeMap::new(),
634633 debugger: None,
635634 debugger_events: SelectAll::new(),
languages.toml+2
@@ -706,6 +706,8 @@ auto-format = true
706706comment-token = "//"
707707language-server = { command = "zls" }
708708indent = { tab-width = 4, unit = " " }
709+formatter = { command = "zig" , args = ["fmt", "--stdin"] }
710+
709711
710712[[grammar]]
711713name = "zig"