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:
parent
654504d5ee
commit
eb231d0449
3 changed files with 131 additions and 85 deletions
|
@ -241,7 +241,8 @@ pub struct LanguageSettingsContent {
|
||||||
///
|
///
|
||||||
/// Default: false
|
/// Default: false
|
||||||
pub always_treat_brackets_as_autoclosed: Option<bool>,
|
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).
|
/// Default: {} (or {"source.organizeImports": true} for Go).
|
||||||
pub code_actions_on_format: Option<HashMap<String, bool>>,
|
pub code_actions_on_format: Option<HashMap<String, bool>>,
|
||||||
|
@ -292,6 +293,8 @@ pub enum FormatOnSave {
|
||||||
/// The arguments to pass to the program.
|
/// The arguments to pass to the program.
|
||||||
arguments: Arc<[String]>,
|
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.
|
/// Controls how whitespace should be displayedin the editor.
|
||||||
|
@ -325,6 +328,8 @@ pub enum Formatter {
|
||||||
/// The arguments to pass to the program.
|
/// The arguments to pass to the program.
|
||||||
arguments: Arc<[String]>,
|
arguments: Arc<[String]>,
|
||||||
},
|
},
|
||||||
|
/// Files should be formatted using code actions executed by language servers.
|
||||||
|
CodeActions(HashMap<String, bool>),
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The settings for inlay hints.
|
/// The settings for inlay hints.
|
||||||
|
|
|
@ -31,7 +31,9 @@ pub fn prettier_plugins_for_language<'a>(
|
||||||
) -> Option<&'a Vec<Arc<str>>> {
|
) -> Option<&'a Vec<Arc<str>>> {
|
||||||
match &language_settings.formatter {
|
match &language_settings.formatter {
|
||||||
Formatter::Prettier { .. } | Formatter::Auto => {}
|
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() {
|
if language.prettier_parser_name().is_some() {
|
||||||
Some(language.prettier_plugins())
|
Some(language.prettier_plugins())
|
||||||
|
|
|
@ -4539,93 +4539,27 @@ impl Project {
|
||||||
buffer.end_transaction(cx)
|
buffer.end_transaction(cx)
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
for (lsp_adapter, language_server) in adapters_and_servers.iter() {
|
// Apply the `code_actions_on_format` before we run the formatter.
|
||||||
// Apply the code actions on
|
let code_actions = deserialize_code_actions(&settings.code_actions_on_format);
|
||||||
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)]
|
#[allow(clippy::nonminimal_bool)]
|
||||||
if !code_actions.is_empty()
|
if !code_actions.is_empty()
|
||||||
&& !(trigger == FormatTrigger::Save
|
&& !(trigger == FormatTrigger::Save && settings.format_on_save == FormatOnSave::Off)
|
||||||
&& settings.format_on_save == FormatOnSave::Off)
|
|
||||||
{
|
{
|
||||||
let actions = project
|
Self::execute_code_actions_on_servers(
|
||||||
.update(&mut cx, |this, cx| {
|
&project,
|
||||||
this.request_lsp(
|
&adapters_and_servers,
|
||||||
buffer.clone(),
|
code_actions,
|
||||||
LanguageServerToQuery::Other(language_server.server_id()),
|
buffer,
|
||||||
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,
|
push_to_history,
|
||||||
lsp_adapter.clone(),
|
&mut project_transaction,
|
||||||
language_server.clone(),
|
|
||||||
&mut cx,
|
&mut cx,
|
||||||
)
|
)
|
||||||
.await?;
|
.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 language-specific formatting using either the primary language server
|
// Apply language-specific formatting using either the primary language server
|
||||||
// or external command.
|
// or external command.
|
||||||
|
// Except for code actions, which are applied with all connected language servers.
|
||||||
let primary_language_server = adapters_and_servers
|
let primary_language_server = adapters_and_servers
|
||||||
.first()
|
.first()
|
||||||
.cloned()
|
.cloned()
|
||||||
|
@ -4638,6 +4572,22 @@ impl Project {
|
||||||
match (&settings.formatter, &settings.format_on_save) {
|
match (&settings.formatter, &settings.format_on_save) {
|
||||||
(_, FormatOnSave::Off) if trigger == FormatTrigger::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)
|
(Formatter::LanguageServer, FormatOnSave::On | FormatOnSave::Off)
|
||||||
| (_, FormatOnSave::LanguageServer) => {
|
| (_, FormatOnSave::LanguageServer) => {
|
||||||
if let Some((language_server, buffer_abs_path)) = server_and_buffer {
|
if let Some((language_server, buffer_abs_path)) = server_and_buffer {
|
||||||
|
@ -8832,6 +8782,82 @@ impl Project {
|
||||||
anyhow::Ok(())
|
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(
|
async fn handle_refresh_inlay_hints(
|
||||||
this: Model<Self>,
|
this: Model<Self>,
|
||||||
_: TypedEnvelope<proto::RefreshInlayHints>,
|
_: 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)]
|
#[allow(clippy::too_many_arguments)]
|
||||||
async fn search_snapshots(
|
async fn search_snapshots(
|
||||||
snapshots: &Vec<LocalSnapshot>,
|
snapshots: &Vec<LocalSnapshot>,
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue