Add "code_actions_on_format" (#7860)

This lets Go programmers configure `"code_actions_on_format": {
  "source.organizeImports": true,
}` so that they don't have to manage their imports manually

I landed on `code_actions_on_format` instead of `code_actions_on_save`
(the
VSCode version of this) because I want to run these when I explicitly
format
(and not if `format_on_save` is disabled).

Co-Authored-By: Thorsten <thorsten@zed.dev>

Release Notes:

- Added `"code_actions_on_format"` to control additional formatting
steps on format/save
([#5232](https://github.com/zed-industries/zed/issues/5232)).
- Added a `"code_actions_on_format"` of `"source.organizeImports"` for
Go ([#4886](https://github.com/zed-industries/zed/issues/4886)).

Co-authored-by: Thorsten <thorsten@zed.dev>
This commit is contained in:
Conrad Irwin 2024-02-15 14:19:57 -07:00 committed by GitHub
parent e1ae0d46da
commit ea322e1d1c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 221 additions and 13 deletions

View File

@ -482,6 +482,7 @@
"deno": { "deno": {
"enable": false "enable": false
}, },
"code_actions_on_format": {},
// Different settings for specific languages. // Different settings for specific languages.
"languages": { "languages": {
"Plain Text": { "Plain Text": {
@ -492,7 +493,10 @@
}, },
"Go": { "Go": {
"tab_size": 4, "tab_size": 4,
"hard_tabs": true "hard_tabs": true,
"code_actions_on_format": {
"source.organizeImports": true
}
}, },
"Markdown": { "Markdown": {
"soft_wrap": "preferred_line_length" "soft_wrap": "preferred_line_length"

View File

@ -704,10 +704,12 @@ impl Item for Editor {
fn save(&mut self, project: Model<Project>, cx: &mut ViewContext<Self>) -> Task<Result<()>> { fn save(&mut self, project: Model<Project>, cx: &mut ViewContext<Self>) -> Task<Result<()>> {
self.report_editor_event("save", None, cx); self.report_editor_event("save", None, cx);
let format = self.perform_format(project.clone(), FormatTrigger::Save, cx);
let buffers = self.buffer().clone().read(cx).all_buffers(); let buffers = self.buffer().clone().read(cx).all_buffers();
cx.spawn(|_, mut cx| async move { cx.spawn(|this, mut cx| async move {
format.await?; this.update(&mut cx, |this, cx| {
this.perform_format(project.clone(), FormatTrigger::Save, cx)
})?
.await?;
if buffers.len() == 1 { if buffers.len() == 1 {
project project

View File

@ -93,6 +93,8 @@ pub struct LanguageSettings {
pub inlay_hints: InlayHintSettings, pub inlay_hints: InlayHintSettings,
/// Whether to automatically close brackets. /// Whether to automatically close brackets.
pub use_autoclose: bool, pub use_autoclose: bool,
/// Which code actions to run on save
pub code_actions_on_format: HashMap<String, bool>,
} }
/// The settings for [GitHub Copilot](https://github.com/features/copilot). /// The settings for [GitHub Copilot](https://github.com/features/copilot).
@ -215,6 +217,11 @@ pub struct LanguageSettingsContent {
/// ///
/// Default: true /// Default: true
pub use_autoclose: Option<bool>, pub use_autoclose: Option<bool>,
/// Which code actions to run on save
///
/// Default: {} (or {"source.organizeImports": true} for Go).
pub code_actions_on_format: Option<HashMap<String, bool>>,
} }
/// The contents of the GitHub Copilot settings. /// The contents of the GitHub Copilot settings.
@ -550,6 +557,10 @@ fn merge_settings(settings: &mut LanguageSettings, src: &LanguageSettingsContent
merge(&mut settings.use_autoclose, src.use_autoclose); merge(&mut settings.use_autoclose, src.use_autoclose);
merge(&mut settings.show_wrap_guides, src.show_wrap_guides); merge(&mut settings.show_wrap_guides, src.show_wrap_guides);
merge(&mut settings.wrap_guides, src.wrap_guides.clone()); merge(&mut settings.wrap_guides, src.wrap_guides.clone());
merge(
&mut settings.code_actions_on_format,
src.code_actions_on_format.clone(),
);
merge( merge(
&mut settings.preferred_line_length, &mut settings.preferred_line_length,

View File

@ -123,6 +123,7 @@ pub(crate) struct GetCompletions {
pub(crate) struct GetCodeActions { pub(crate) struct GetCodeActions {
pub range: Range<Anchor>, pub range: Range<Anchor>,
pub kinds: Option<Vec<lsp::CodeActionKind>>,
} }
pub(crate) struct OnTypeFormatting { pub(crate) struct OnTypeFormatting {
@ -1603,7 +1604,10 @@ impl LspCommand for GetCodeActions {
partial_result_params: Default::default(), partial_result_params: Default::default(),
context: lsp::CodeActionContext { context: lsp::CodeActionContext {
diagnostics: relevant_diagnostics, diagnostics: relevant_diagnostics,
only: language_server.code_action_kinds(), only: self
.kinds
.clone()
.or_else(|| language_server.code_action_kinds()),
..lsp::CodeActionContext::default() ..lsp::CodeActionContext::default()
}, },
} }
@ -1664,7 +1668,10 @@ impl LspCommand for GetCodeActions {
})? })?
.await?; .await?;
Ok(Self { range: start..end }) Ok(Self {
range: start..end,
kinds: None,
})
} }
fn response_to_proto( fn response_to_proto(

View File

@ -4150,10 +4150,11 @@ impl Project {
let buffer = buffer_handle.read(cx); let buffer = buffer_handle.read(cx);
let file = File::from_dyn(buffer.file())?; let file = File::from_dyn(buffer.file())?;
let buffer_abs_path = file.as_local().map(|f| f.abs_path(cx)); let buffer_abs_path = file.as_local().map(|f| f.abs_path(cx));
let server = self let (adapter, server) = self
.primary_language_server_for_buffer(buffer, cx) .primary_language_server_for_buffer(buffer, cx)
.map(|s| s.1.clone()); .map(|(a, s)| (Some(a.clone()), Some(s.clone())))
Some((buffer_handle, buffer_abs_path, server)) .unwrap_or((None, None));
Some((buffer_handle, buffer_abs_path, adapter, server))
}) })
.collect::<Vec<_>>(); .collect::<Vec<_>>();
@ -4161,7 +4162,7 @@ impl Project {
// Do not allow multiple concurrent formatting requests for the // Do not allow multiple concurrent formatting requests for the
// same buffer. // same buffer.
project.update(&mut cx, |this, cx| { project.update(&mut cx, |this, cx| {
buffers_with_paths_and_servers.retain(|(buffer, _, _)| { buffers_with_paths_and_servers.retain(|(buffer, _, _, _)| {
this.buffers_being_formatted this.buffers_being_formatted
.insert(buffer.read(cx).remote_id()) .insert(buffer.read(cx).remote_id())
}); });
@ -4173,7 +4174,7 @@ impl Project {
let buffers = &buffers_with_paths_and_servers; let buffers = &buffers_with_paths_and_servers;
move || { move || {
this.update(&mut cx, |this, cx| { this.update(&mut cx, |this, cx| {
for (buffer, _, _) in buffers { for (buffer, _, _, _) in buffers {
this.buffers_being_formatted this.buffers_being_formatted
.remove(&buffer.read(cx).remote_id()); .remove(&buffer.read(cx).remote_id());
} }
@ -4183,7 +4184,9 @@ impl Project {
}); });
let mut project_transaction = ProjectTransaction::default(); let mut project_transaction = ProjectTransaction::default();
for (buffer, buffer_abs_path, language_server) in &buffers_with_paths_and_servers { for (buffer, buffer_abs_path, lsp_adapter, language_server) in
&buffers_with_paths_and_servers
{
let settings = buffer.update(&mut cx, |buffer, cx| { let settings = buffer.update(&mut cx, |buffer, cx| {
language_settings(buffer.language(), buffer.file(), cx).clone() language_settings(buffer.language(), buffer.file(), cx).clone()
})?; })?;
@ -4214,6 +4217,88 @@ impl Project {
buffer.end_transaction(cx) buffer.end_transaction(cx)
})?; })?;
if let (Some(lsp_adapter), Some(language_server)) =
(lsp_adapter, language_server)
{
// 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();
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 action in actions {
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 language-specific formatting using either a language server // Apply language-specific formatting using either a language server
// or external command. // or external command.
let mut format_operation = None; let mut format_operation = None;
@ -4323,6 +4408,8 @@ impl Project {
if let Some(transaction_id) = whitespace_transaction_id { if let Some(transaction_id) = whitespace_transaction_id {
b.group_until_transaction(transaction_id); b.group_until_transaction(transaction_id);
} else if let Some(transaction) = project_transaction.0.get(buffer) {
b.group_until_transaction(transaction.id)
} }
} }
@ -5162,7 +5249,7 @@ impl Project {
self.request_lsp( self.request_lsp(
buffer_handle.clone(), buffer_handle.clone(),
LanguageServerToQuery::Primary, LanguageServerToQuery::Primary,
GetCodeActions { range }, GetCodeActions { range, kinds: None },
cx, cx,
) )
} }
@ -5178,6 +5265,103 @@ impl Project {
self.code_actions_impl(buffer_handle, range, cx) self.code_actions_impl(buffer_handle, range, cx)
} }
pub fn apply_code_actions_on_save(
&self,
buffers: HashSet<Model<Buffer>>,
cx: &mut ModelContext<Self>,
) -> Task<Result<ProjectTransaction>> {
if !self.is_local() {
return Task::ready(Ok(Default::default()));
}
let buffers_with_adapters_and_servers = buffers
.into_iter()
.filter_map(|buffer_handle| {
let buffer = buffer_handle.read(cx);
self.primary_language_server_for_buffer(buffer, cx)
.map(|(a, s)| (buffer_handle, a.clone(), s.clone()))
})
.collect::<Vec<_>>();
cx.spawn(move |this, mut cx| async move {
for (buffer_handle, lsp_adapter, lang_server) in buffers_with_adapters_and_servers {
let actions = this
.update(&mut cx, |this, cx| {
let buffer = buffer_handle.read(cx);
let kinds: Vec<lsp::CodeActionKind> =
language_settings(buffer.language(), buffer.file(), cx)
.code_actions_on_format
.iter()
.flat_map(|(kind, enabled)| {
if *enabled {
Some(kind.clone().into())
} else {
None
}
})
.collect();
if kinds.is_empty() {
return Task::ready(Ok(vec![]));
}
this.request_lsp(
buffer_handle.clone(),
LanguageServerToQuery::Other(lang_server.server_id()),
GetCodeActions {
range: text::Anchor::MIN..text::Anchor::MAX,
kinds: Some(kinds),
},
cx,
)
})?
.await?;
for action in actions {
if let Some(edit) = action.lsp_action.edit {
if edit.changes.is_some() || edit.document_changes.is_some() {
return Self::deserialize_workspace_edit(
this.upgrade().ok_or_else(|| anyhow!("no app present"))?,
edit,
true,
lsp_adapter.clone(),
lang_server.clone(),
&mut cx,
)
.await;
}
}
if let Some(command) = action.lsp_action.command {
this.update(&mut cx, |this, _| {
this.last_workspace_edits_by_language_server
.remove(&lang_server.server_id());
})?;
let result = lang_server
.request::<lsp::request::ExecuteCommand>(lsp::ExecuteCommandParams {
command: command.command,
arguments: command.arguments.unwrap_or_default(),
..Default::default()
})
.await;
if let Err(err) = result {
// TODO: LSP ERROR
return Err(err);
}
return Ok(this.update(&mut cx, |this, _| {
this.last_workspace_edits_by_language_server
.remove(&lang_server.server_id())
.unwrap_or_default()
})?);
}
}
}
Ok(ProjectTransaction::default())
})
}
pub fn apply_code_action( pub fn apply_code_action(
&self, &self,
buffer_handle: Model<Buffer>, buffer_handle: Model<Buffer>,