Add code_actions as formatter type (#10121)

This fixes #8992 and solves a problem that ESLint/Prettier/... users
have been running into:

They want to format _only_ with ESLint, which is *not* a primary
language server (so `formatter: language server` does not help) and it
is not a formatter.

What they want to use is what they get when they have configured
something like this:

```json
{
  "languages": {
    "JavaScript": {
      "code_actions_on_format": {
        "source.fixAll.eslint": true
      }
    }
  }
}
```

BUT they don't want to run the formatter.

So what this PR does is to add a new formatter type: `code_actions`.

With that, users can only use code actions to format:

```json
{
  "languages": {
    "JavaScript": {
      "formatter": {
        "code_actions": {
          "source.fixAll.eslint": true
        }
      }
    }
  }
}
```

This means that when formatting (via `editor: format` or on-save) only
the code actions that are specified are being executed, no formatter.


Release Notes:

- Added a new `formatter`/`format_on_save` option: `code_actions`. When
configured, this uses language server code actions to format a buffer.
This can be used if one wants to, for example, format a buffer with
ESLint and *not* run prettier or another formatter afterwards. Example
configuration: `{"languages": {"JavaScript": {"formatter":
{"code_actions": {"source.fixAll.eslint": true}}}}}`
([#8992](https://github.com/zed-industries/zed/issues/8992)).

---------

Co-authored-by: JH Chabran <jh@chabran.fr>
Co-authored-by: Marshall Bowers <elliott.codes@gmail.com>
This commit is contained in:
Thorsten Ball 2024-04-03 16:16:03 +02:00 committed by GitHub
parent 654504d5ee
commit eb231d0449
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 131 additions and 85 deletions

View File

@ -241,7 +241,8 @@ pub struct LanguageSettingsContent {
///
/// Default: false
pub always_treat_brackets_as_autoclosed: Option<bool>,
/// Which code actions to run on save
/// Which code actions to run on save after the formatter.
/// These are not run if formatting is off.
///
/// Default: {} (or {"source.organizeImports": true} for Go).
pub code_actions_on_format: Option<HashMap<String, bool>>,
@ -292,6 +293,8 @@ pub enum FormatOnSave {
/// The arguments to pass to the program.
arguments: Arc<[String]>,
},
/// Files should be formatted using code actions executed by language servers.
CodeActions(HashMap<String, bool>),
}
/// Controls how whitespace should be displayedin the editor.
@ -325,6 +328,8 @@ pub enum Formatter {
/// The arguments to pass to the program.
arguments: Arc<[String]>,
},
/// Files should be formatted using code actions executed by language servers.
CodeActions(HashMap<String, bool>),
}
/// The settings for inlay hints.

View File

@ -31,7 +31,9 @@ pub fn prettier_plugins_for_language<'a>(
) -> Option<&'a Vec<Arc<str>>> {
match &language_settings.formatter {
Formatter::Prettier { .. } | Formatter::Auto => {}
Formatter::LanguageServer | Formatter::External { .. } => return None,
Formatter::LanguageServer | Formatter::External { .. } | Formatter::CodeActions(_) => {
return None
}
};
if language.prettier_parser_name().is_some() {
Some(language.prettier_plugins())

View File

@ -4539,93 +4539,27 @@ impl Project {
buffer.end_transaction(cx)
})?;
for (lsp_adapter, language_server) in adapters_and_servers.iter() {
// Apply the code actions on
let code_actions: Vec<lsp::CodeActionKind> = settings
.code_actions_on_format
.iter()
.flat_map(|(kind, enabled)| {
if *enabled {
Some(kind.clone().into())
} else {
None
}
})
.collect();
#[allow(clippy::nonminimal_bool)]
if !code_actions.is_empty()
&& !(trigger == FormatTrigger::Save
&& settings.format_on_save == FormatOnSave::Off)
{
let actions = project
.update(&mut cx, |this, cx| {
this.request_lsp(
buffer.clone(),
LanguageServerToQuery::Other(language_server.server_id()),
GetCodeActions {
range: text::Anchor::MIN..text::Anchor::MAX,
kinds: Some(code_actions),
},
cx,
)
})?
.await?;
for mut action in actions {
Self::try_resolve_code_action(&language_server, &mut action)
.await
.context("resolving a formatting code action")?;
if let Some(edit) = action.lsp_action.edit {
if edit.changes.is_none() && edit.document_changes.is_none() {
continue;
}
let new = Self::deserialize_workspace_edit(
project
.upgrade()
.ok_or_else(|| anyhow!("project dropped"))?,
edit,
push_to_history,
lsp_adapter.clone(),
language_server.clone(),
&mut cx,
)
.await?;
project_transaction.0.extend(new.0);
}
if let Some(command) = action.lsp_action.command {
project.update(&mut cx, |this, _| {
this.last_workspace_edits_by_language_server
.remove(&language_server.server_id());
})?;
language_server
.request::<lsp::request::ExecuteCommand>(
lsp::ExecuteCommandParams {
command: command.command,
arguments: command.arguments.unwrap_or_default(),
..Default::default()
},
)
.await?;
project.update(&mut cx, |this, _| {
project_transaction.0.extend(
this.last_workspace_edits_by_language_server
.remove(&language_server.server_id())
.unwrap_or_default()
.0,
)
})?;
}
}
}
// Apply the `code_actions_on_format` before we run the formatter.
let code_actions = deserialize_code_actions(&settings.code_actions_on_format);
#[allow(clippy::nonminimal_bool)]
if !code_actions.is_empty()
&& !(trigger == FormatTrigger::Save && settings.format_on_save == FormatOnSave::Off)
{
Self::execute_code_actions_on_servers(
&project,
&adapters_and_servers,
code_actions,
buffer,
push_to_history,
&mut project_transaction,
&mut cx,
)
.await?;
}
// Apply language-specific formatting using either the primary language server
// or external command.
// Except for code actions, which are applied with all connected language servers.
let primary_language_server = adapters_and_servers
.first()
.cloned()
@ -4638,6 +4572,22 @@ impl Project {
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 {
@ -8832,6 +8782,82 @@ impl Project {
anyhow::Ok(())
}
async fn execute_code_actions_on_servers(
project: &WeakModel<Project>,
adapters_and_servers: &Vec<(Arc<CachedLspAdapter>, Arc<LanguageServer>)>,
code_actions: Vec<lsp::CodeActionKind>,
buffer: &Model<Buffer>,
push_to_history: bool,
project_transaction: &mut ProjectTransaction,
cx: &mut AsyncAppContext,
) -> Result<(), anyhow::Error> {
for (lsp_adapter, language_server) in adapters_and_servers.iter() {
let code_actions = code_actions.clone();
let actions = project
.update(cx, move |this, cx| {
let request = GetCodeActions {
range: text::Anchor::MIN..text::Anchor::MAX,
kinds: Some(code_actions),
};
let server = LanguageServerToQuery::Other(language_server.server_id());
this.request_lsp(buffer.clone(), server, request, cx)
})?
.await?;
for mut action in actions {
Self::try_resolve_code_action(&language_server, &mut action)
.await
.context("resolving a formatting code action")?;
if let Some(edit) = action.lsp_action.edit {
if edit.changes.is_none() && edit.document_changes.is_none() {
continue;
}
let new = Self::deserialize_workspace_edit(
project
.upgrade()
.ok_or_else(|| anyhow!("project dropped"))?,
edit,
push_to_history,
lsp_adapter.clone(),
language_server.clone(),
cx,
)
.await?;
project_transaction.0.extend(new.0);
}
if let Some(command) = action.lsp_action.command {
project.update(cx, |this, _| {
this.last_workspace_edits_by_language_server
.remove(&language_server.server_id());
})?;
language_server
.request::<lsp::request::ExecuteCommand>(lsp::ExecuteCommandParams {
command: command.command,
arguments: command.arguments.unwrap_or_default(),
..Default::default()
})
.await?;
project.update(cx, |this, _| {
project_transaction.0.extend(
this.last_workspace_edits_by_language_server
.remove(&language_server.server_id())
.unwrap_or_default()
.0,
)
})?;
}
}
}
Ok(())
}
async fn handle_refresh_inlay_hints(
this: Model<Self>,
_: TypedEnvelope<proto::RefreshInlayHints>,
@ -9671,6 +9697,19 @@ impl Project {
}
}
fn deserialize_code_actions(code_actions: &HashMap<String, bool>) -> Vec<lsp::CodeActionKind> {
code_actions
.iter()
.flat_map(|(kind, enabled)| {
if *enabled {
Some(kind.clone().into())
} else {
None
}
})
.collect()
}
#[allow(clippy::too_many_arguments)]
async fn search_snapshots(
snapshots: &Vec<LocalSnapshot>,