Skip to content

Commit

Permalink
feat(format): current file in formatter args
Browse files Browse the repository at this point in the history
Add support for a current file placeholder in formatter arguments. This
should make it possible to run custom formatters, such as `rome`
(Typescript), `rustfmt` (Rust) and more.

The changes take inspiration from f7ecf9c and acdce30, but instead of
adding a new field to the formatter struct, it works by looking for a
predefined placeholder (`{}`), which should be replaced, if found. This
should allow for better control over arguments of formatters, allowing
both giving it as a standalone arg (`"{}"`) and as part of one (ie
`"--src-file={}"`). The placeholder, `{}`, is used since it is common,
Rust using it frequently.

Closes: #3596
Signed-off-by: Filip Dutescu <[email protected]>
  • Loading branch information
filipdutescu committed Jan 21, 2023
1 parent b65f104 commit c9eab6f
Show file tree
Hide file tree
Showing 2 changed files with 123 additions and 56 deletions.
37 changes: 23 additions & 14 deletions helix-term/src/commands/typed.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
};
Expand Down Expand Up @@ -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);
}
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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(),
Expand Down
142 changes: 100 additions & 42 deletions helix-view/src/document.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<BoxFuture<'static, Result<Transaction, FormatterError>>> {
pub fn auto_format(
&mut self,
view: &mut View,
provider_registry: &DiffProviderRegistry,
redraw_handle: RedrawHandle,
) -> Option<BoxFuture<'static, Result<Transaction, FormatterError>>> {
if self.language_config()?.auto_format {
self.format()
self.format(view, provider_registry, redraw_handle)
} else {
None
}
Expand All @@ -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<BoxFuture<'static, Result<Transaction, FormatterError>>> {
if let Some(formatter) = self
pub fn format(
&mut self,
view: &mut View,
provider_registry: &DiffProviderRegistry,
redraw_handle: RedrawHandle,
) -> Option<BoxFuture<'static, Result<Transaction, FormatterError>>> {
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()?;
Expand Down

0 comments on commit c9eab6f

Please sign in to comment.