on master help sign in
Change default formatter for any language (#2942)
219d2c2PiergiorgioZagaria
book/src/languages.md+2
@@ -40,6 +40,7 @@ file-types = ["mylang", "myl"]
40comment-token = "#"
40comment-token = "#"
41indent = { tab-width = 2, unit = " " }
41indent = { tab-width = 2, unit = " " }
42language-server = { command = "mylang-lsp", args = ["--stdio"] }
42language-server = { command = "mylang-lsp", args = ["--stdio"] }
43+formatter = { command = "mylang-formatter" , args = ["--stdin"] }
43```
44```
44
45
45These configuration keys are available:
46These configuration keys are available:
@@ -59,6 +60,7 @@ These configuration keys are available:
59| `language-server` | The Language Server to run. See the Language Server configuration section below. |
60| `language-server` | The Language Server to run. See the Language Server configuration section below. |
60| `config` | Language Server configuration |
61| `config` | Language Server configuration |
61| `grammar` | The tree-sitter grammar to use (defaults to the value of `name`) |
62| `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 |
62
64
63### Language Server configuration
65### Language Server configuration
64
66
helix-core/src/syntax.rs+12
@@ -79,6 +79,9 @@ pub struct LanguageConfiguration {
79 #[serde(default)]
79 #[serde(default)]
80 pub auto_format: bool,
80 pub auto_format: bool,
81
81
82+ #[serde(skip_serializing_if = "Option::is_none")]
83+ pub formatter: Option<FormatterConfiguration>,
84+
82 #[serde(default)]
85 #[serde(default)]
83 pub diagnostic_severity: Severity,
86 pub diagnostic_severity: Severity,
84
87
@@ -126,6 +129,15 @@ pub struct LanguageServerConfiguration {
126 pub language_id: Option<String>,
129 pub language_id: Option<String>,
127}
130}
128
131
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+
129#[derive(Debug, PartialEq, Clone, Deserialize, Serialize)]
141#[derive(Debug, PartialEq, Clone, Deserialize, Serialize)]
130#[serde(rename_all = "kebab-case")]
142#[serde(rename_all = "kebab-case")]
131pub struct AdvancedCompletion {
143pub struct AdvancedCompletion {
helix-lsp/src/lib.rs−16
@@ -213,22 +213,6 @@ pub mod util {
213 }),
213 }),
214 )
214 )
215 }
215 }
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− }
232}
216}
233
217
234#[derive(Debug, PartialEq, Clone)]
218#[derive(Debug, PartialEq, Clone)]
helix-term/src/commands.rs+4−4
@@ -28,7 +28,7 @@ use helix_core::{
28};
28};
29use helix_view::{
29use helix_view::{
30 clipboard::ClipboardType,
30 clipboard::ClipboardType,
31− document::{Mode, SCRATCH_BUFFER_NAME},
31+ document::{FormatterError, Mode, SCRATCH_BUFFER_NAME},
32 editor::{Action, Motion},
32 editor::{Action, Motion},
33 info::Info,
33 info::Info,
34 input::KeyEvent,
34 input::KeyEvent,
@@ -2471,14 +2471,14 @@ async fn make_format_callback(
2471 doc_id: DocumentId,
2471 doc_id: DocumentId,
2472 doc_version: i32,
2472 doc_version: i32,
2473 modified: Modified,
2473 modified: Modified,
2474− format: impl Future<Output = helix_lsp::util::LspFormatting> + Send + 'static,
2474+ format: impl Future<Output = Result<Transaction, FormatterError>> + Send + 'static,
2475) -> anyhow::Result<job::Callback> {
2475) -> anyhow::Result<job::Callback> {
2476− let format = format.await;
2476+ let format = format.await?;
2477 let call: job::Callback = Box::new(move |editor, _compositor| {
2477 let call: job::Callback = Box::new(move |editor, _compositor| {
2478 let view_id = view!(editor).id;
2478 let view_id = view!(editor).id;
2479 if let Some(doc) = editor.document_mut(doc_id) {
2479 if let Some(doc) = editor.document_mut(doc_id) {
2480 if doc.version() == doc_version {
2480 if doc.version() == doc_version {
2481− doc.apply(&Transaction::from(format), view_id);
2481+ doc.apply(&format, view_id);
2482 doc.append_changes_to_history(view_id);
2482 doc.append_changes_to_history(view_id);
2483 doc.detect_indent_and_line_ending();
2483 doc.detect_indent_and_line_ending();
2484 if let Modified::SetUnmodified = modified {
2484 if let Modified::SetUnmodified = modified {
helix-view/src/document.rs+93−10
@@ -1,4 +1,6 @@
1use anyhow::{anyhow, bail, Context, Error};
1use anyhow::{anyhow, bail, Context, Error};
2+use futures_util::future::BoxFuture;
3+use futures_util::FutureExt;
2use helix_core::auto_pairs::AutoPairs;
4use helix_core::auto_pairs::AutoPairs;
3use helix_core::Range;
5use helix_core::Range;
4use serde::de::{self, Deserialize, Deserializer};
6use serde::de::{self, Deserialize, Deserializer};
@@ -20,7 +22,6 @@ use helix_core::{
20 ChangeSet, Diagnostic, LineEnding, Rope, RopeBuilder, Selection, State, Syntax, Transaction,
22 ChangeSet, Diagnostic, LineEnding, Rope, RopeBuilder, Selection, State, Syntax, Transaction,
21 DEFAULT_LINE_ENDING,
23 DEFAULT_LINE_ENDING,
22};
24};
23−use helix_lsp::util::LspFormatting;
24
25
25use crate::{DocumentId, Editor, ViewId};
26use crate::{DocumentId, Editor, ViewId};
26
27
@@ -397,7 +398,7 @@ impl Document {
397
398
398 /// The same as [`format`], but only returns formatting changes if auto-formatting
399 /// The same as [`format`], but only returns formatting changes if auto-formatting
399 /// is configured.
400 /// 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>>> {
401 if self.language_config()?.auto_format {
402 if self.language_config()?.auto_format {
402 self.format()
403 self.format()
403 } else {
404 } else {
@@ -407,7 +408,56 @@ impl Document {
407
408
408 /// If supported, returns the changes that should be applied to this document in order
409 /// If supported, returns the changes that should be applied to this document in order
409 /// to format it nicely.
410 /// to format it nicely.
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.
410− pub fn format(&self) -> Option<impl Future<Output = LspFormatting> + 'static> {
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+
411 let language_server = self.language_server()?;
461 let language_server = self.language_server()?;
412 let text = self.text.clone();
462 let text = self.text.clone();
413 let offset_encoding = language_server.offset_encoding();
463 let offset_encoding = language_server.offset_encoding();
@@ -427,13 +477,13 @@ impl Document {
427 log::warn!("LSP formatting failed: {}", e);
477 log::warn!("LSP formatting failed: {}", e);
428 Default::default()
478 Default::default()
429 });
479 });
430− LspFormatting {
480+ Ok(helix_lsp::util::generate_transaction_from_edits(
431− doc: text,
481+ &text,
432 edits,
482 edits,
433 offset_encoding,
483 offset_encoding,
434− }
484+ ))
435 };
485 };
436− Some(fut)
486+ Some(fut.boxed())
437 }
487 }
438
488
439 pub fn save(&mut self, force: bool) -> impl Future<Output = Result<(), anyhow::Error>> {
489 pub fn save(&mut self, force: bool) -> impl Future<Output = Result<(), anyhow::Error>> {
@@ -442,7 +492,7 @@ impl Document {
442
492
443 pub fn format_and_save(
493 pub fn format_and_save(
444 &mut self,
494 &mut self,
445− formatting: Option<impl Future<Output = LspFormatting>>,
495+ formatting: Option<impl Future<Output = Result<Transaction, FormatterError>>>,
446 force: bool,
496 force: bool,
447 ) -> impl Future<Output = anyhow::Result<()>> {
497 ) -> impl Future<Output = anyhow::Result<()>> {
448 self.save_impl(formatting, force)
498 self.save_impl(formatting, force)
@@ -454,7 +504,7 @@ impl Document {
454 /// at its `path()`.
504 /// at its `path()`.
455 ///
505 ///
456 /// If `formatting` is present, it supplies some changes that we apply to the text before saving.
506 /// 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>>>(
458 &mut self,
508 &mut self,
459 formatting: Option<F>,
509 formatting: Option<F>,
460 force: bool,
510 force: bool,
@@ -488,7 +538,8 @@ impl Document {
488 }
538 }
489
539
490 if let Some(fmt) = formatting {
540 if let Some(fmt) = formatting {
541+ let transaction = fmt.await?;
491− let success = Transaction::from(fmt.await).changes().apply(&mut text);
542+ let success = transaction.changes().apply(&mut text);
492 if !success {
543 if !success {
493 // This shouldn't happen, because the transaction changes were generated
544 // This shouldn't happen, because the transaction changes were generated
494 // from the same text we're saving.
545 // from the same text we're saving.
@@ -1034,6 +1085,38 @@ impl Default for Document {
1034 }
1085 }
1035}
1086}
1036
1087
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+
1037#[cfg(test)]
1120#[cfg(test)]
1038mod test {
1121mod test {
1039 use super::*;
1122 use super::*;
helix-view/src/editor.rs+1−2
@@ -613,7 +613,6 @@ impl Editor {
613 syn_loader: Arc<syntax::Loader>,
613 syn_loader: Arc<syntax::Loader>,
614 config: Box<dyn DynAccess<Config>>,
614 config: Box<dyn DynAccess<Config>>,
615 ) -> Self {
615 ) -> Self {
616− let language_servers = helix_lsp::Registry::new();
617 let conf = config.load();
616 let conf = config.load();
618 let auto_pairs = (&conf.auto_pairs).into();
617 let auto_pairs = (&conf.auto_pairs).into();
619
618
@@ -629,7 +628,7 @@ impl Editor {
629 macro_recording: None,
628 macro_recording: None,
630 macro_replaying: Vec::new(),
629 macro_replaying: Vec::new(),
631 theme: theme_loader.default(),
630 theme: theme_loader.default(),
632− language_servers,
631+ language_servers: helix_lsp::Registry::new(),
633 diagnostics: BTreeMap::new(),
632 diagnostics: BTreeMap::new(),
634 debugger: None,
633 debugger: None,
635 debugger_events: SelectAll::new(),
634 debugger_events: SelectAll::new(),
languages.toml+2
@@ -706,6 +706,8 @@ auto-format = true
706comment-token = "//"
706comment-token = "//"
707language-server = { command = "zls" }
707language-server = { command = "zls" }
708indent = { tab-width = 4, unit = " " }
708indent = { tab-width = 4, unit = " " }
709+formatter = { command = "zig" , args = ["fmt", "--stdin"] }
710+
709
711
710[[grammar]]
712[[grammar]]
711name = "zig"
713name = "zig"