diff --git a/assets/settings/default.json b/assets/settings/default.json index 6f575b4725..8d12c54fde 100644 --- a/assets/settings/default.json +++ b/assets/settings/default.json @@ -482,6 +482,7 @@ "deno": { "enable": false }, + "code_actions_on_format": {}, // Different settings for specific languages. "languages": { "Plain Text": { @@ -492,7 +493,10 @@ }, "Go": { "tab_size": 4, - "hard_tabs": true + "hard_tabs": true, + "code_actions_on_format": { + "source.organizeImports": true + } }, "Markdown": { "soft_wrap": "preferred_line_length" diff --git a/crates/editor/src/items.rs b/crates/editor/src/items.rs index 418307df82..a6ff8dd043 100644 --- a/crates/editor/src/items.rs +++ b/crates/editor/src/items.rs @@ -704,10 +704,12 @@ impl Item for Editor { fn save(&mut self, project: Model, cx: &mut ViewContext) -> Task> { 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(); - cx.spawn(|_, mut cx| async move { - format.await?; + cx.spawn(|this, mut cx| async move { + this.update(&mut cx, |this, cx| { + this.perform_format(project.clone(), FormatTrigger::Save, cx) + })? + .await?; if buffers.len() == 1 { project diff --git a/crates/language/src/language_settings.rs b/crates/language/src/language_settings.rs index eeb674189f..9de4c11aee 100644 --- a/crates/language/src/language_settings.rs +++ b/crates/language/src/language_settings.rs @@ -93,6 +93,8 @@ pub struct LanguageSettings { pub inlay_hints: InlayHintSettings, /// Whether to automatically close brackets. pub use_autoclose: bool, + /// Which code actions to run on save + pub code_actions_on_format: HashMap, } /// The settings for [GitHub Copilot](https://github.com/features/copilot). @@ -215,6 +217,11 @@ pub struct LanguageSettingsContent { /// /// Default: true pub use_autoclose: Option, + + /// Which code actions to run on save + /// + /// Default: {} (or {"source.organizeImports": true} for Go). + pub code_actions_on_format: Option>, } /// 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.show_wrap_guides, src.show_wrap_guides); merge(&mut settings.wrap_guides, src.wrap_guides.clone()); + merge( + &mut settings.code_actions_on_format, + src.code_actions_on_format.clone(), + ); merge( &mut settings.preferred_line_length, diff --git a/crates/project/src/lsp_command.rs b/crates/project/src/lsp_command.rs index 20bcd4b7b3..202da1e973 100644 --- a/crates/project/src/lsp_command.rs +++ b/crates/project/src/lsp_command.rs @@ -123,6 +123,7 @@ pub(crate) struct GetCompletions { pub(crate) struct GetCodeActions { pub range: Range, + pub kinds: Option>, } pub(crate) struct OnTypeFormatting { @@ -1603,7 +1604,10 @@ impl LspCommand for GetCodeActions { partial_result_params: Default::default(), context: lsp::CodeActionContext { diagnostics: relevant_diagnostics, - only: language_server.code_action_kinds(), + only: self + .kinds + .clone() + .or_else(|| language_server.code_action_kinds()), ..lsp::CodeActionContext::default() }, } @@ -1664,7 +1668,10 @@ impl LspCommand for GetCodeActions { })? .await?; - Ok(Self { range: start..end }) + Ok(Self { + range: start..end, + kinds: None, + }) } fn response_to_proto( diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index ce9b64a1d7..20843a8e35 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -4150,10 +4150,11 @@ impl Project { let buffer = buffer_handle.read(cx); let file = File::from_dyn(buffer.file())?; 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) - .map(|s| s.1.clone()); - Some((buffer_handle, buffer_abs_path, server)) + .map(|(a, s)| (Some(a.clone()), Some(s.clone()))) + .unwrap_or((None, None)); + Some((buffer_handle, buffer_abs_path, adapter, server)) }) .collect::>(); @@ -4161,7 +4162,7 @@ impl Project { // Do not allow multiple concurrent formatting requests for the // same buffer. project.update(&mut cx, |this, cx| { - buffers_with_paths_and_servers.retain(|(buffer, _, _)| { + buffers_with_paths_and_servers.retain(|(buffer, _, _, _)| { this.buffers_being_formatted .insert(buffer.read(cx).remote_id()) }); @@ -4173,7 +4174,7 @@ impl Project { let buffers = &buffers_with_paths_and_servers; move || { this.update(&mut cx, |this, cx| { - for (buffer, _, _) in buffers { + for (buffer, _, _, _) in buffers { this.buffers_being_formatted .remove(&buffer.read(cx).remote_id()); } @@ -4183,7 +4184,9 @@ impl Project { }); 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| { language_settings(buffer.language(), buffer.file(), cx).clone() })?; @@ -4214,6 +4217,88 @@ impl Project { 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 = 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::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 // or external command. let mut format_operation = None; @@ -4323,6 +4408,8 @@ impl Project { if let Some(transaction_id) = whitespace_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( buffer_handle.clone(), LanguageServerToQuery::Primary, - GetCodeActions { range }, + GetCodeActions { range, kinds: None }, cx, ) } @@ -5178,6 +5265,103 @@ impl Project { self.code_actions_impl(buffer_handle, range, cx) } + pub fn apply_code_actions_on_save( + &self, + buffers: HashSet>, + cx: &mut ModelContext, + ) -> Task> { + 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::>(); + + 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 = + 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::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( &self, buffer_handle: Model,