-
-
Notifications
You must be signed in to change notification settings - Fork 2.5k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Change default formatter for any language #2942
Changes from all commits
163d262
9965cd4
1af38a0
29e24b6
124dd5d
b5dc579
2e70770
06ad531
7416d6f
b819c21
48d69b2
15685fe
57bb4fc
016e724
f7ecf9c
acdce30
aeb4f8f
d711be0
7ec0288
1ae13bb
fb93d1e
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,4 +1,6 @@ | ||
use anyhow::{anyhow, bail, Context, Error}; | ||
use futures_util::future::BoxFuture; | ||
use futures_util::FutureExt; | ||
use helix_core::auto_pairs::AutoPairs; | ||
use helix_core::Range; | ||
use serde::de::{self, Deserialize, Deserializer}; | ||
|
@@ -20,7 +22,6 @@ use helix_core::{ | |
ChangeSet, Diagnostic, LineEnding, Rope, RopeBuilder, Selection, State, Syntax, Transaction, | ||
DEFAULT_LINE_ENDING, | ||
}; | ||
use helix_lsp::util::LspFormatting; | ||
|
||
use crate::{DocumentId, Editor, ViewId}; | ||
|
||
|
@@ -397,7 +398,7 @@ impl Document { | |
|
||
/// The same as [`format`], but only returns formatting changes if auto-formatting | ||
/// is configured. | ||
pub fn auto_format(&self) -> Option<impl Future<Output = LspFormatting> + 'static> { | ||
pub fn auto_format(&self) -> Option<BoxFuture<'static, Result<Transaction, FormatterError>>> { | ||
if self.language_config()?.auto_format { | ||
self.format() | ||
} else { | ||
|
@@ -407,7 +408,56 @@ impl Document { | |
|
||
/// If supported, returns the changes that should be applied to this document in order | ||
/// to format it nicely. | ||
pub fn format(&self) -> Option<impl Future<Output = LspFormatting> + 'static> { | ||
// 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<BoxFuture<'static, Result<Transaction, FormatterError>>> { | ||
if let Some(formatter) = self.language_config().and_then(|c| c.formatter.clone()) { | ||
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 { | ||
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.stderr.is_empty() { | ||
return Err(FormatterError::Stderr( | ||
String::from_utf8_lossy(&output.stderr).to_string(), | ||
)); | ||
} | ||
PiergiorgioZagaria marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
if !output.status.success() { | ||
return Err(FormatterError::NonZeroExitStatus); | ||
} | ||
|
||
let str = String::from_utf8(output.stdout) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You can use There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
.map_err(|_| FormatterError::InvalidUtf8Output)?; | ||
|
||
Ok(helix_core::diff::compare_ropes(&text, &Rope::from(str))) | ||
}; | ||
return Some(formatting_future.boxed()); | ||
}; | ||
|
||
let language_server = self.language_server()?; | ||
let text = self.text.clone(); | ||
let offset_encoding = language_server.offset_encoding(); | ||
|
@@ -427,13 +477,13 @@ impl Document { | |
log::warn!("LSP formatting failed: {}", e); | ||
Default::default() | ||
}); | ||
LspFormatting { | ||
doc: text, | ||
Ok(helix_lsp::util::generate_transaction_from_edits( | ||
&text, | ||
edits, | ||
offset_encoding, | ||
} | ||
)) | ||
}; | ||
Some(fut) | ||
Some(fut.boxed()) | ||
} | ||
|
||
pub fn save(&mut self, force: bool) -> impl Future<Output = Result<(), anyhow::Error>> { | ||
|
@@ -442,7 +492,7 @@ impl Document { | |
|
||
pub fn format_and_save( | ||
&mut self, | ||
formatting: Option<impl Future<Output = LspFormatting>>, | ||
formatting: Option<impl Future<Output = Result<Transaction, FormatterError>>>, | ||
force: bool, | ||
) -> impl Future<Output = anyhow::Result<()>> { | ||
self.save_impl(formatting, force) | ||
|
@@ -454,7 +504,7 @@ impl Document { | |
/// at its `path()`. | ||
/// | ||
/// If `formatting` is present, it supplies some changes that we apply to the text before saving. | ||
fn save_impl<F: Future<Output = LspFormatting>>( | ||
fn save_impl<F: Future<Output = Result<Transaction, FormatterError>>>( | ||
&mut self, | ||
formatting: Option<F>, | ||
force: bool, | ||
|
@@ -488,7 +538,8 @@ impl Document { | |
} | ||
|
||
if let Some(fmt) = formatting { | ||
let success = Transaction::from(fmt.await).changes().apply(&mut text); | ||
let transaction = fmt.await?; | ||
let success = transaction.changes().apply(&mut text); | ||
if !success { | ||
// This shouldn't happen, because the transaction changes were generated | ||
// from the same text we're saving. | ||
|
@@ -1034,6 +1085,38 @@ impl Default for Document { | |
} | ||
} | ||
|
||
#[derive(Clone, Debug)] | ||
pub enum FormatterError { | ||
SpawningFailed { | ||
command: String, | ||
error: std::io::ErrorKind, | ||
}, | ||
BrokenStdin, | ||
WaitForOutputFailed, | ||
Stderr(String), | ||
InvalidUtf8Output, | ||
DiskReloadError(String), | ||
NonZeroExitStatus, | ||
} | ||
|
||
impl std::error::Error for FormatterError {} | ||
|
||
impl Display for FormatterError { | ||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { | ||
match self { | ||
Self::SpawningFailed { command, error } => { | ||
write!(f, "Failed to spawn formatter {}: {:?}", command, error) | ||
} | ||
Self::BrokenStdin => write!(f, "Could not write to formatter stdin"), | ||
Self::WaitForOutputFailed => write!(f, "Waiting for formatter output failed"), | ||
Self::Stderr(output) => write!(f, "Formatter error: {}", output), | ||
Self::InvalidUtf8Output => write!(f, "Invalid UTF-8 formatter output"), | ||
Self::DiskReloadError(error) => write!(f, "Error reloading file from disk: {}", error), | ||
Self::NonZeroExitStatus => write!(f, "Formatter exited with non zero exit status:"), | ||
} | ||
} | ||
} | ||
|
||
#[cfg(test)] | ||
mod test { | ||
use super::*; | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Stderr doesn't always mean there's an error. There are programs that use it to print diagnostic messages so that e.g. you can still pipe stdout to a file and see informational messages in the output. The only indication of error we can really use reliably is the exit code. What we should probably do is move this whole if expression inside the following one that checks the exit code. The enum variants can probably change the enum to get rid of
Stderr
and add anOption<String>
toNonZeroExitStatus
.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ok, I'll open a new PR to fix it.
Btw the function
shell_impl
inhelix-term/src/commands.rs
, which is used when calling the shell with ! Or | doesn't implement these checks, should we add them there too?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I see, in that case, yeah, those need to be fixed too. The presence of output on stderr does not necessarily mean there was an error.