Allow user to use multiple formatters (#14846)

Fixes #4822
- [x] Release note
- [ ] Surface formatting errors via a toast
- [x] Doc updates
- [x] Have "language-server" accept an optional name of the server.

Release Notes:

- `format` and `format_on_save` now accept an array of formatting
actions to run.
- `language_server` formatter option now accepts the name of a language
server to use (e.g. `{"language_server": {"name: "ruff"}}`); when not
specified, a primary language server is used.

---------

Co-authored-by: Thorsten <thorsten@zed.dev>
This commit is contained in:
Piotr Osiewicz 2024-07-23 20:05:09 +02:00 committed by GitHub
parent 53b711c2b4
commit 3d1bf09299
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 565 additions and 130 deletions

View file

@ -43,7 +43,7 @@ use itertools::Itertools;
use language::{
language_settings::{
language_settings, AllLanguageSettings, FormatOnSave, Formatter, InlayHintKind,
LanguageSettings,
LanguageSettings, SelectedFormatter,
},
markdown, point_to_lsp, prepare_completion_documentation,
proto::{
@ -5056,107 +5056,180 @@ impl Project {
.as_ref()
.zip(buffer_abs_path.as_ref());
let mut format_operation = None;
let prettier_settings = buffer.read_with(&mut cx, |buffer, cx| {
language_settings(buffer.language(), buffer.file(), cx)
.prettier
.clone()
})?;
match (&settings.formatter, &settings.format_on_save) {
(_, FormatOnSave::Off) if trigger == FormatTrigger::Save => {}
(Formatter::CodeActions(code_actions), FormatOnSave::On | FormatOnSave::Off)
| (_, FormatOnSave::CodeActions(code_actions)) => {
let code_actions = deserialize_code_actions(code_actions);
if !code_actions.is_empty() {
Self::execute_code_actions_on_servers(
&project,
&adapters_and_servers,
code_actions,
buffer,
push_to_history,
&mut project_transaction,
&mut cx,
)
.await?;
}
}
(Formatter::LanguageServer, FormatOnSave::On | FormatOnSave::Off)
| (_, FormatOnSave::LanguageServer) => {
if let Some((language_server, buffer_abs_path)) = server_and_buffer {
format_operation = Some(FormatOperation::Lsp(
Self::format_via_lsp(
&project,
buffer,
buffer_abs_path,
language_server,
&settings,
&mut cx,
)
.await
.context("failed to format via language server")?,
));
}
}
let mut format_operations: Vec<FormatOperation> = vec![];
{
match trigger {
FormatTrigger::Save => {
match &settings.format_on_save {
FormatOnSave::Off => {
// nothing
}
FormatOnSave::On => {
match &settings.formatter {
SelectedFormatter::Auto => {
// do the auto-format: prefer prettier, fallback to primary language server
let diff = {
if prettier_settings.allowed {
Self::perform_format(
&Formatter::Prettier,
server_and_buffer,
project.clone(),
buffer,
buffer_abs_path,
&settings,
&adapters_and_servers,
push_to_history,
&mut project_transaction,
&mut cx,
)
.await
} else {
Self::perform_format(
&Formatter::LanguageServer { name: None },
server_and_buffer,
project.clone(),
buffer,
buffer_abs_path,
&settings,
&adapters_and_servers,
push_to_history,
&mut project_transaction,
&mut cx,
)
.await
}
}
.log_err()
.flatten();
if let Some(op) = diff {
format_operations.push(op);
}
}
SelectedFormatter::List(formatters) => {
for formatter in formatters.as_ref() {
let diff = Self::perform_format(
formatter,
server_and_buffer,
project.clone(),
buffer,
buffer_abs_path,
&settings,
&adapters_and_servers,
push_to_history,
&mut project_transaction,
&mut cx,
)
.await
.log_err()
.flatten();
if let Some(op) = diff {
format_operations.push(op);
}
(
Formatter::External { command, arguments },
FormatOnSave::On | FormatOnSave::Off,
)
| (_, FormatOnSave::External { command, arguments }) => {
let buffer_abs_path = buffer_abs_path.as_ref().map(|path| path.as_path());
format_operation = Self::format_via_external_command(
buffer,
buffer_abs_path,
command,
arguments,
&mut cx,
)
.await
.context(format!(
"failed to format via external command {:?}",
command
))?
.map(FormatOperation::External);
}
(Formatter::Auto, FormatOnSave::On | FormatOnSave::Off) => {
let prettier = if prettier_settings.allowed {
prettier_support::format_with_prettier(&project, buffer, &mut cx)
.await
.transpose()
.ok()
.flatten()
} else {
None
};
if let Some(operation) = prettier {
format_operation = Some(operation);
} else if let Some((language_server, buffer_abs_path)) = server_and_buffer {
format_operation = Some(FormatOperation::Lsp(
Self::format_via_lsp(
&project,
buffer,
buffer_abs_path,
language_server,
&settings,
&mut cx,
)
.await
.context("failed to format via language server")?,
));
// format with formatter
}
}
}
}
FormatOnSave::List(formatters) => {
for formatter in formatters.as_ref() {
let diff = Self::perform_format(
&formatter,
server_and_buffer,
project.clone(),
buffer,
buffer_abs_path,
&settings,
&adapters_and_servers,
push_to_history,
&mut project_transaction,
&mut cx,
)
.await
.log_err()
.flatten();
if let Some(op) = diff {
format_operations.push(op);
}
}
}
}
}
}
(Formatter::Prettier, FormatOnSave::On | FormatOnSave::Off) => {
if prettier_settings.allowed {
if let Some(operation) =
prettier_support::format_with_prettier(&project, buffer, &mut cx).await
{
format_operation = Some(operation?);
FormatTrigger::Manual => {
match &settings.formatter {
SelectedFormatter::Auto => {
// do the auto-format: prefer prettier, fallback to primary language server
let diff = {
if prettier_settings.allowed {
Self::perform_format(
&Formatter::Prettier,
server_and_buffer,
project.clone(),
buffer,
buffer_abs_path,
&settings,
&adapters_and_servers,
push_to_history,
&mut project_transaction,
&mut cx,
)
.await
} else {
Self::perform_format(
&Formatter::LanguageServer { name: None },
server_and_buffer,
project.clone(),
buffer,
buffer_abs_path,
&settings,
&adapters_and_servers,
push_to_history,
&mut project_transaction,
&mut cx,
)
.await
}
}
.log_err()
.flatten();
if let Some(op) = diff {
format_operations.push(op)
}
}
SelectedFormatter::List(formatters) => {
for formatter in formatters.as_ref() {
// format with formatter
let diff = Self::perform_format(
formatter,
server_and_buffer,
project.clone(),
buffer,
buffer_abs_path,
&settings,
&adapters_and_servers,
push_to_history,
&mut project_transaction,
&mut cx,
)
.await
.log_err()
.flatten();
if let Some(op) = diff {
format_operations.push(op);
}
}
}
}
}
}
};
}
buffer.update(&mut cx, |b, cx| {
// If the buffer had its whitespace formatted and was edited while the language-specific
@ -5166,13 +5239,13 @@ impl Project {
if b.peek_undo_stack()
.map_or(true, |e| e.transaction_id() != transaction_id)
{
format_operation.take();
format_operations.clear();
}
}
// Apply any language-specific formatting, and group the two formatting operations
// in the buffer's undo history.
if let Some(operation) = format_operation {
for operation in format_operations {
match operation {
FormatOperation::Lsp(edits) => {
b.edit(edits, None, cx);
@ -5204,6 +5277,91 @@ impl Project {
Ok(project_transaction)
}
#[allow(clippy::too_many_arguments)]
async fn perform_format(
formatter: &Formatter,
primary_server_and_buffer: Option<(&Arc<LanguageServer>, &PathBuf)>,
project: WeakModel<Project>,
buffer: &Model<Buffer>,
buffer_abs_path: &Option<PathBuf>,
settings: &LanguageSettings,
adapters_and_servers: &Vec<(Arc<CachedLspAdapter>, Arc<LanguageServer>)>,
push_to_history: bool,
transaction: &mut ProjectTransaction,
mut cx: &mut AsyncAppContext,
) -> Result<Option<FormatOperation>, anyhow::Error> {
let result = match formatter {
Formatter::LanguageServer { name } => {
if let Some((language_server, buffer_abs_path)) = primary_server_and_buffer {
let language_server = if let Some(name) = name {
adapters_and_servers
.iter()
.find_map(|(adapter, server)| {
adapter.name.0.as_ref().eq(name.as_str()).then_some(server)
})
.unwrap_or_else(|| language_server)
} else {
language_server
};
Some(FormatOperation::Lsp(
Self::format_via_lsp(
&project,
buffer,
buffer_abs_path,
language_server,
settings,
cx,
)
.await
.context("failed to format via language server")?,
))
} else {
None
}
}
Formatter::Prettier => {
prettier_support::format_with_prettier(&project, buffer, &mut cx)
.await
.transpose()
.ok()
.flatten()
}
Formatter::External { command, arguments } => {
let buffer_abs_path = buffer_abs_path.as_ref().map(|path| path.as_path());
Self::format_via_external_command(
buffer,
buffer_abs_path,
&command,
&arguments,
&mut cx,
)
.await
.context(format!(
"failed to format via external command {:?}",
command
))?
.map(FormatOperation::External)
}
Formatter::CodeActions(code_actions) => {
let code_actions = deserialize_code_actions(&code_actions);
if !code_actions.is_empty() {
Self::execute_code_actions_on_servers(
&project,
&adapters_and_servers,
code_actions,
buffer,
push_to_history,
transaction,
cx,
)
.await?;
}
None
}
};
anyhow::Ok(result)
}
async fn format_via_lsp(
this: &WeakModel<Self>,
buffer: &Model<Buffer>,