Skip to content

Commit 95a9508

Browse files
PiergiorgioZagariasudormrfbin
authored andcommitted
Change default formatter for any language (helix-editor#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>
1 parent fbfaf27 commit 95a9508

File tree

7 files changed

+114
-32
lines changed

7 files changed

+114
-32
lines changed

book/src/languages.md

+2
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ file-types = ["mylang", "myl"]
4040
comment-token = "#"
4141
indent = { tab-width = 2, unit = " " }
4242
language-server = { command = "mylang-lsp", args = ["--stdio"] }
43+
formatter = { command = "mylang-formatter" , args = ["--stdin"] }
4344
```
4445

4546
These 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
Original file line numberDiff line numberDiff line change
@@ -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")]
131143
pub struct AdvancedCompletion {

helix-lsp/src/lib.rs

-16
Original file line numberDiff line numberDiff line change
@@ -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
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ use helix_core::{
2828
};
2929
use 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,
@@ -2511,14 +2511,14 @@ async fn make_format_callback(
25112511
doc_id: DocumentId,
25122512
doc_version: i32,
25132513
modified: Modified,
2514-
format: impl Future<Output = helix_lsp::util::LspFormatting> + Send + 'static,
2514+
format: impl Future<Output = Result<Transaction, FormatterError>> + Send + 'static,
25152515
) -> anyhow::Result<job::Callback> {
2516-
let format = format.await;
2516+
let format = format.await?;
25172517
let call: job::Callback = Box::new(move |editor, _compositor| {
25182518
let view_id = view!(editor).id;
25192519
if let Some(doc) = editor.document_mut(doc_id) {
25202520
if doc.version() == doc_version {
2521-
doc.apply(&Transaction::from(format), view_id);
2521+
doc.apply(&format, view_id);
25222522
doc.append_changes_to_history(view_id);
25232523
doc.detect_indent_and_line_ending();
25242524
if let Modified::SetUnmodified = modified {

helix-view/src/document.rs

+93-10
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
use anyhow::{anyhow, bail, Context, Error};
2+
use futures_util::future::BoxFuture;
3+
use futures_util::FutureExt;
24
use helix_core::auto_pairs::AutoPairs;
35
use helix_core::Range;
46
use 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

2526
use 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)]
10381121
mod test {
10391122
use super::*;

helix-view/src/editor.rs

+1-2
Original file line numberDiff line numberDiff line change
@@ -679,7 +679,6 @@ impl Editor {
679679
syn_loader: Arc<syntax::Loader>,
680680
config: Box<dyn DynAccess<Config>>,
681681
) -> Self {
682-
let language_servers = helix_lsp::Registry::new();
683682
let conf = config.load();
684683
let auto_pairs = (&conf.auto_pairs).into();
685684

@@ -695,7 +694,7 @@ impl Editor {
695694
macro_recording: None,
696695
macro_replaying: Vec::new(),
697696
theme: theme_loader.default(),
698-
language_servers,
697+
language_servers: helix_lsp::Registry::new(),
699698
diagnostics: BTreeMap::new(),
700699
debugger: None,
701700
debugger_events: SelectAll::new(),

languages.toml

+2
Original file line numberDiff line numberDiff line change
@@ -706,6 +706,8 @@ auto-format = true
706706
comment-token = "//"
707707
language-server = { command = "zls" }
708708
indent = { tab-width = 4, unit = " " }
709+
formatter = { command = "zig" , args = ["fmt", "--stdin"] }
710+
709711

710712
[[grammar]]
711713
name = "zig"

0 commit comments

Comments
 (0)