Fix code action formatters creating separate transaction (#26311)
Closes #24588 Closes #25419 Restructures `LspStore.format_local` a decent bit in order to make how the transaction history is preserved more clear, and in doing so fix various bugs with how the transaction history is handled during a format request (especially when formatting in remote dev) Release Notes: - Fixed an issue that prevented formatting from working when working with remote dev - Fixed an issue when using code actions as a format step where the edits made by the code actions would not be grouped with the other format edits in the undo history
This commit is contained in:
parent
1cf252f8eb
commit
274124256d
3 changed files with 599 additions and 340 deletions
|
@ -4,7 +4,6 @@ pub mod rust_analyzer_ext;
|
|||
|
||||
use crate::{
|
||||
buffer_store::{BufferStore, BufferStoreEvent},
|
||||
deserialize_code_actions,
|
||||
environment::ProjectEnvironment,
|
||||
lsp_command::{self, *},
|
||||
prettier_store::{self, PrettierStore, PrettierStoreEvent},
|
||||
|
@ -15,7 +14,7 @@ use crate::{
|
|||
worktree_store::{WorktreeStore, WorktreeStoreEvent},
|
||||
yarn::YarnPathStore,
|
||||
CodeAction, Completion, CompletionSource, CoreCompletion, Hover, InlayHint, LspAction,
|
||||
ProjectItem as _, ProjectPath, ProjectTransaction, ResolveState, Symbol, ToolchainStore,
|
||||
ProjectItem, ProjectPath, ProjectTransaction, ResolveState, Symbol, ToolchainStore,
|
||||
};
|
||||
use anyhow::{anyhow, Context as _, Result};
|
||||
use async_trait::async_trait;
|
||||
|
@ -82,7 +81,7 @@ use std::{
|
|||
sync::Arc,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
use text::{Anchor, BufferId, LineEnding, OffsetRangeExt, TransactionId};
|
||||
use text::{Anchor, BufferId, LineEnding, OffsetRangeExt};
|
||||
use url::Url;
|
||||
use util::{
|
||||
debug_panic, defer, maybe, merge_json_value_into, paths::SanitizedPath, post_inc, ResultExt,
|
||||
|
@ -114,15 +113,6 @@ pub enum LspFormatTarget {
|
|||
|
||||
pub type OpenLspBufferHandle = Entity<Entity<Buffer>>;
|
||||
|
||||
// Currently, formatting operations are represented differently depending on
|
||||
// whether they come from a language server or an external command.
|
||||
#[derive(Debug)]
|
||||
pub enum FormatOperation {
|
||||
Lsp(Vec<(Range<Anchor>, Arc<str>)>),
|
||||
External(Diff),
|
||||
Prettier(Diff),
|
||||
}
|
||||
|
||||
impl FormatTrigger {
|
||||
fn from_proto(value: i32) -> FormatTrigger {
|
||||
match value {
|
||||
|
@ -1114,17 +1104,27 @@ impl LocalLspStore {
|
|||
.collect::<Vec<_>>()
|
||||
})
|
||||
})?;
|
||||
Self::execute_code_actions_on_servers(
|
||||
for (lsp_adapter, language_server) in adapters_and_servers.iter() {
|
||||
let actions = Self::get_server_code_actions_from_action_kinds(
|
||||
&lsp_store,
|
||||
&adapters_and_servers,
|
||||
language_server.server_id(),
|
||||
vec![kind.clone()],
|
||||
&buffer,
|
||||
buffer,
|
||||
cx,
|
||||
)
|
||||
.await?;
|
||||
Self::execute_code_actions_on_server(
|
||||
&lsp_store,
|
||||
language_server,
|
||||
lsp_adapter,
|
||||
actions,
|
||||
push_to_history,
|
||||
&mut project_transaction,
|
||||
cx,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
}
|
||||
Ok(project_transaction)
|
||||
}
|
||||
|
||||
|
@ -1162,6 +1162,7 @@ impl LocalLspStore {
|
|||
});
|
||||
|
||||
let mut project_transaction = ProjectTransaction::default();
|
||||
|
||||
for buffer in &buffers {
|
||||
let adapters_and_servers = lsp_store.update(cx, |lsp_store, cx| {
|
||||
buffer.handle.update(cx, |buffer, cx| {
|
||||
|
@ -1174,62 +1175,72 @@ impl LocalLspStore {
|
|||
})
|
||||
})?;
|
||||
|
||||
let settings = buffer.handle.update(cx, |buffer, cx| {
|
||||
let settings = buffer.handle.read_with(cx, |buffer, cx| {
|
||||
language_settings(buffer.language().map(|l| l.name()), buffer.file(), cx)
|
||||
.into_owned()
|
||||
})?;
|
||||
|
||||
let remove_trailing_whitespace = settings.remove_trailing_whitespace_on_save;
|
||||
let ensure_final_newline = settings.ensure_final_newline_on_save;
|
||||
let mut transaction_id_format = None;
|
||||
|
||||
// First, format buffer's whitespace according to the settings.
|
||||
let trailing_whitespace_diff = if remove_trailing_whitespace {
|
||||
Some(
|
||||
buffer
|
||||
.handle
|
||||
.update(cx, |b, cx| b.remove_trailing_whitespace(cx))?
|
||||
.await,
|
||||
)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let whitespace_transaction_id = buffer.handle.update(cx, |buffer, cx| {
|
||||
// ensure no transactions created while formatting are
|
||||
// grouped with the previous transaction in the history
|
||||
// based on the transaction group interval
|
||||
buffer.handle.update(cx, |buffer, _| {
|
||||
buffer.finalize_last_transaction();
|
||||
buffer.start_transaction();
|
||||
if let Some(diff) = trailing_whitespace_diff {
|
||||
buffer.apply_diff(diff, cx);
|
||||
}
|
||||
if ensure_final_newline {
|
||||
buffer.ensure_final_newline(cx);
|
||||
}
|
||||
buffer.end_transaction(cx)
|
||||
})?;
|
||||
|
||||
let initial_transaction_id = whitespace_transaction_id;
|
||||
|
||||
// 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)
|
||||
// handle whitespace formatting
|
||||
{
|
||||
Self::execute_code_actions_on_servers(
|
||||
&lsp_store,
|
||||
&adapters_and_servers,
|
||||
code_actions,
|
||||
&buffer.handle,
|
||||
push_to_history,
|
||||
&mut project_transaction,
|
||||
cx,
|
||||
)
|
||||
.await?;
|
||||
if settings.remove_trailing_whitespace_on_save {
|
||||
let diff = buffer
|
||||
.handle
|
||||
.read_with(cx, |buffer, cx| buffer.remove_trailing_whitespace(cx))?
|
||||
.await;
|
||||
buffer.handle.update(cx, |buffer, cx| {
|
||||
buffer.start_transaction();
|
||||
buffer.apply_diff(diff, cx);
|
||||
transaction_id_format =
|
||||
transaction_id_format.or(buffer.end_transaction(cx));
|
||||
if let Some(transaction_id) = transaction_id_format {
|
||||
buffer.group_until_transaction(transaction_id);
|
||||
}
|
||||
})?;
|
||||
}
|
||||
|
||||
let prettier_settings = buffer.handle.read_with(cx, |buffer, cx| {
|
||||
language_settings(buffer.language().map(|l| l.name()), buffer.file(), cx)
|
||||
.prettier
|
||||
.clone()
|
||||
if settings.ensure_final_newline_on_save {
|
||||
buffer.handle.update(cx, |buffer, cx| {
|
||||
buffer.start_transaction();
|
||||
buffer.ensure_final_newline(cx);
|
||||
transaction_id_format =
|
||||
transaction_id_format.or(buffer.end_transaction(cx));
|
||||
if let Some(transaction_id) = transaction_id_format {
|
||||
buffer.group_until_transaction(transaction_id);
|
||||
}
|
||||
})?;
|
||||
}
|
||||
}
|
||||
|
||||
// Formatter for `code_actions_on_format` that runs before
|
||||
// the rest of the formatters
|
||||
let code_actions_on_format_formatter = 'ca_formatter: {
|
||||
let should_run_code_actions_on_format = !matches!(
|
||||
(trigger, &settings.format_on_save),
|
||||
(FormatTrigger::Save, &FormatOnSave::Off)
|
||||
);
|
||||
let have_code_actions_to_run_on_format = settings
|
||||
.code_actions_on_format
|
||||
.values()
|
||||
.any(|enabled| *enabled);
|
||||
|
||||
if should_run_code_actions_on_format {
|
||||
if have_code_actions_to_run_on_format {
|
||||
break 'ca_formatter Some(Formatter::CodeActions(
|
||||
settings.code_actions_on_format.clone(),
|
||||
));
|
||||
}
|
||||
}
|
||||
break 'ca_formatter None;
|
||||
};
|
||||
|
||||
let formatters = match (trigger, &settings.format_on_save) {
|
||||
(FormatTrigger::Save, FormatOnSave::Off) => &[],
|
||||
|
@ -1237,7 +1248,7 @@ impl LocalLspStore {
|
|||
(FormatTrigger::Manual, _) | (FormatTrigger::Save, FormatOnSave::On) => {
|
||||
match &settings.formatter {
|
||||
SelectedFormatter::Auto => {
|
||||
if prettier_settings.allowed {
|
||||
if settings.prettier.allowed {
|
||||
std::slice::from_ref(&Formatter::Prettier)
|
||||
} else {
|
||||
std::slice::from_ref(&Formatter::LanguageServer { name: None })
|
||||
|
@ -1247,40 +1258,128 @@ impl LocalLspStore {
|
|||
}
|
||||
}
|
||||
};
|
||||
Self::execute_formatters(
|
||||
lsp_store.clone(),
|
||||
formatters,
|
||||
|
||||
let formatters = code_actions_on_format_formatter.iter().chain(formatters);
|
||||
|
||||
// helper function to avoid duplicate logic between formatter handlers below
|
||||
// We want to avoid continuing to format the buffer if it has been edited since the start
|
||||
// so we check that the last transaction id on the undo stack matches the one we expect
|
||||
// This check should be done after each "gather" step where we generate a diff or edits to apply,
|
||||
// and before applying them to the buffer to avoid messing up the user's buffer
|
||||
fn err_if_buffer_edited_since_start(
|
||||
buffer: &FormattableBuffer,
|
||||
transaction_id_format: Option<text::TransactionId>,
|
||||
cx: &AsyncApp,
|
||||
) -> Option<anyhow::Error> {
|
||||
let transaction_id_last = buffer
|
||||
.handle
|
||||
.read_with(cx, |buffer, _| {
|
||||
buffer.peek_undo_stack().map(|t| t.transaction_id())
|
||||
})
|
||||
.ok()
|
||||
.flatten();
|
||||
let should_continue_formatting = match (transaction_id_format, transaction_id_last)
|
||||
{
|
||||
(Some(format), Some(last)) => format == last,
|
||||
(Some(_), None) => false,
|
||||
(_, _) => true,
|
||||
};
|
||||
if !should_continue_formatting {
|
||||
return Some(anyhow::anyhow!("Buffer edited while formatting. Aborting"));
|
||||
}
|
||||
return None;
|
||||
}
|
||||
|
||||
// variable used to track errors that occur during the formatting process below,
|
||||
// but that need to not be returned right away (with `?` for example) because we
|
||||
// still need to clean up the transaction history and update the project transaction
|
||||
let mut result = anyhow::Ok(());
|
||||
|
||||
'formatters: for formatter in formatters {
|
||||
match formatter {
|
||||
Formatter::Prettier => {
|
||||
let prettier = lsp_store.read_with(cx, |lsp_store, _cx| {
|
||||
lsp_store.prettier_store().unwrap().downgrade()
|
||||
})?;
|
||||
let diff_result =
|
||||
prettier_store::format_with_prettier(&prettier, &buffer.handle, cx)
|
||||
.await
|
||||
.transpose();
|
||||
let Ok(diff) = diff_result else {
|
||||
result = Err(diff_result.unwrap_err());
|
||||
break 'formatters;
|
||||
};
|
||||
let Some(diff) = diff else {
|
||||
continue 'formatters;
|
||||
};
|
||||
if let Some(err) =
|
||||
err_if_buffer_edited_since_start(buffer, transaction_id_format, &cx)
|
||||
{
|
||||
result = Err(err);
|
||||
break 'formatters;
|
||||
}
|
||||
buffer.handle.update(cx, |buffer, cx| {
|
||||
buffer.start_transaction();
|
||||
buffer.apply_diff(diff, cx);
|
||||
transaction_id_format =
|
||||
transaction_id_format.or(buffer.end_transaction(cx));
|
||||
if let Some(transaction_id) = transaction_id_format {
|
||||
buffer.group_until_transaction(transaction_id);
|
||||
}
|
||||
})?;
|
||||
}
|
||||
Formatter::External { command, arguments } => {
|
||||
let diff_result = Self::format_via_external_command(
|
||||
buffer,
|
||||
&settings,
|
||||
&adapters_and_servers,
|
||||
push_to_history,
|
||||
initial_transaction_id,
|
||||
&mut project_transaction,
|
||||
command.as_ref(),
|
||||
arguments.as_deref(),
|
||||
cx,
|
||||
)
|
||||
.await?;
|
||||
.await
|
||||
.with_context(|| {
|
||||
format!("Failed to format buffer via external command: {}", command)
|
||||
});
|
||||
let Ok(diff) = diff_result else {
|
||||
result = Err(diff_result.unwrap_err());
|
||||
break 'formatters;
|
||||
};
|
||||
let Some(diff) = diff else {
|
||||
continue 'formatters;
|
||||
};
|
||||
if let Some(err) =
|
||||
err_if_buffer_edited_since_start(buffer, transaction_id_format, &cx)
|
||||
{
|
||||
result = Err(err);
|
||||
break 'formatters;
|
||||
}
|
||||
|
||||
Ok(project_transaction)
|
||||
buffer.handle.update(cx, |buffer, cx| {
|
||||
buffer.start_transaction();
|
||||
buffer.apply_diff(diff, cx);
|
||||
transaction_id_format =
|
||||
transaction_id_format.or(buffer.end_transaction(cx));
|
||||
if let Some(transaction_id) = transaction_id_format {
|
||||
buffer.group_until_transaction(transaction_id);
|
||||
}
|
||||
})?;
|
||||
}
|
||||
|
||||
async fn execute_formatters(
|
||||
lsp_store: WeakEntity<LspStore>,
|
||||
formatters: &[Formatter],
|
||||
buffer: &FormattableBuffer,
|
||||
settings: &LanguageSettings,
|
||||
adapters_and_servers: &[(Arc<CachedLspAdapter>, Arc<LanguageServer>)],
|
||||
push_to_history: bool,
|
||||
mut initial_transaction_id: Option<TransactionId>,
|
||||
project_transaction: &mut ProjectTransaction,
|
||||
cx: &mut AsyncApp,
|
||||
) -> anyhow::Result<()> {
|
||||
let mut prev_transaction_id = initial_transaction_id;
|
||||
|
||||
for formatter in formatters {
|
||||
let operation = match formatter {
|
||||
Formatter::LanguageServer { name } => {
|
||||
let Some(language_server) = lsp_store.update(cx, |lsp_store, cx| {
|
||||
let Some(buffer_path_abs) = buffer.abs_path.as_ref() else {
|
||||
log::warn!("Cannot format buffer that is not backed by a file on disk using language servers. Skipping");
|
||||
continue 'formatters;
|
||||
};
|
||||
|
||||
let language_server = 'language_server: {
|
||||
// if a name was provided, try to find the server with that name
|
||||
if let Some(name) = name.as_deref() {
|
||||
for (adapter, server) in &adapters_and_servers {
|
||||
if adapter.name.0.as_ref() == name {
|
||||
break 'language_server server.clone();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// otherwise, fall back to the primary language server for the buffer if one exists
|
||||
let default_lsp = lsp_store.update(cx, |lsp_store, cx| {
|
||||
buffer.handle.update(cx, |buffer, cx| {
|
||||
lsp_store
|
||||
.as_local()
|
||||
|
@ -1288,158 +1387,328 @@ impl LocalLspStore {
|
|||
.primary_language_server_for_buffer(buffer, cx)
|
||||
.map(|(_, lsp)| lsp.clone())
|
||||
})
|
||||
})?
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
let Some(buffer_abs_path) = buffer.abs_path.as_ref() else {
|
||||
continue;
|
||||
};
|
||||
})?;
|
||||
|
||||
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.clone())
|
||||
})
|
||||
.unwrap_or(language_server)
|
||||
if let Some(default_lsp) = default_lsp {
|
||||
break 'language_server default_lsp;
|
||||
} else {
|
||||
language_server
|
||||
log::warn!(
|
||||
"No language server found to format buffer '{:?}'. Skipping",
|
||||
buffer_path_abs.as_path().to_string_lossy()
|
||||
);
|
||||
continue 'formatters;
|
||||
}
|
||||
};
|
||||
|
||||
let result = if let Some(ranges) = &buffer.ranges {
|
||||
let edits_result = if let Some(ranges) = buffer.ranges.as_ref() {
|
||||
Self::format_ranges_via_lsp(
|
||||
&lsp_store,
|
||||
&buffer.handle,
|
||||
ranges,
|
||||
buffer_abs_path,
|
||||
buffer_path_abs,
|
||||
&language_server,
|
||||
settings,
|
||||
&settings,
|
||||
cx,
|
||||
)
|
||||
.await
|
||||
.context("failed to format ranges via language server")?
|
||||
.context("Failed to format ranges via language server")
|
||||
} else {
|
||||
Self::format_via_lsp(
|
||||
&lsp_store,
|
||||
&buffer.handle,
|
||||
buffer_abs_path,
|
||||
buffer_path_abs,
|
||||
&language_server,
|
||||
settings,
|
||||
&settings,
|
||||
cx,
|
||||
)
|
||||
.await
|
||||
.context("failed to format via language server")?
|
||||
.context("failed to format via language server")
|
||||
};
|
||||
|
||||
Some(FormatOperation::Lsp(result))
|
||||
let Ok(edits) = edits_result else {
|
||||
result = Err(edits_result.unwrap_err());
|
||||
break 'formatters;
|
||||
};
|
||||
|
||||
if edits.is_empty() {
|
||||
continue 'formatters;
|
||||
}
|
||||
if let Some(err) =
|
||||
err_if_buffer_edited_since_start(buffer, transaction_id_format, &cx)
|
||||
{
|
||||
result = Err(err);
|
||||
break 'formatters;
|
||||
}
|
||||
|
||||
buffer.handle.update(cx, |buffer, cx| {
|
||||
buffer.start_transaction();
|
||||
buffer.edit(edits, None, cx);
|
||||
transaction_id_format =
|
||||
transaction_id_format.or(buffer.end_transaction(cx));
|
||||
if let Some(transaction_id) = transaction_id_format {
|
||||
buffer.group_until_transaction(transaction_id);
|
||||
}
|
||||
Formatter::Prettier => {
|
||||
let prettier = lsp_store.update(cx, |lsp_store, _cx| {
|
||||
lsp_store.prettier_store().unwrap().downgrade()
|
||||
})?;
|
||||
prettier_store::format_with_prettier(&prettier, &buffer.handle, cx)
|
||||
.await
|
||||
.transpose()?
|
||||
}
|
||||
Formatter::External { command, arguments } => {
|
||||
Self::format_via_external_command(buffer, command, arguments.as_deref(), 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(
|
||||
let Some(buffer_path_abs) = buffer.abs_path.as_ref() else {
|
||||
log::warn!("Cannot format buffer that is not backed by a file on disk using code actions. Skipping");
|
||||
continue 'formatters;
|
||||
};
|
||||
let code_action_kinds = code_actions
|
||||
.iter()
|
||||
.filter_map(|(action_kind, enabled)| {
|
||||
enabled.then_some(action_kind.clone().into())
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
if code_action_kinds.is_empty() {
|
||||
continue 'formatters;
|
||||
}
|
||||
|
||||
let mut actions_and_servers = Vec::new();
|
||||
|
||||
for (index, (_, language_server)) in adapters_and_servers.iter().enumerate()
|
||||
{
|
||||
let actions_result = Self::get_server_code_actions_from_action_kinds(
|
||||
&lsp_store,
|
||||
adapters_and_servers,
|
||||
code_actions,
|
||||
language_server.server_id(),
|
||||
code_action_kinds.clone(),
|
||||
&buffer.handle,
|
||||
push_to_history,
|
||||
project_transaction,
|
||||
cx,
|
||||
)
|
||||
.await?;
|
||||
let buf_transaction_id =
|
||||
project_transaction.0.get(&buffer.handle).map(|t| t.id);
|
||||
// NOTE: same logic as in buffer.handle.update below
|
||||
if initial_transaction_id.is_none() {
|
||||
initial_transaction_id = buf_transaction_id;
|
||||
}
|
||||
if buf_transaction_id.is_some() {
|
||||
prev_transaction_id = buf_transaction_id;
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
};
|
||||
let Some(operation) = operation else {
|
||||
.await
|
||||
.with_context(
|
||||
|| format!("Failed to resolve code actions with kinds {:?} for language server {}",
|
||||
code_action_kinds.iter().map(|kind| kind.as_str()).join(", "),
|
||||
language_server.name())
|
||||
);
|
||||
let Ok(actions) = actions_result else {
|
||||
// note: it may be better to set result to the error and break formatters here
|
||||
// but for now we try to execute the actions that we can resolve and skip the rest
|
||||
log::error!(
|
||||
"Failed to resolve code actions with kinds {:?} with language server {}",
|
||||
code_action_kinds.iter().map(|kind| kind.as_str()).join(", "),
|
||||
language_server.name()
|
||||
);
|
||||
continue;
|
||||
};
|
||||
|
||||
let should_continue_formatting = buffer.handle.update(cx, |b, cx| {
|
||||
// If a previous format succeeded and the buffer was edited while the language-specific
|
||||
// formatting information for this format was being computed, avoid applying the
|
||||
// language-specific formatting, because it can't be grouped with the previous formatting
|
||||
// in the undo history.
|
||||
let should_continue_formatting = match (prev_transaction_id, b.peek_undo_stack()) {
|
||||
(Some(prev_transaction_id), Some(last_history_entry)) => {
|
||||
let last_history_transaction_id = last_history_entry.transaction_id();
|
||||
let is_same_as_prev = last_history_transaction_id == prev_transaction_id;
|
||||
is_same_as_prev
|
||||
for action in actions {
|
||||
actions_and_servers.push((action, index));
|
||||
}
|
||||
(Some(_), None) => false,
|
||||
(_, _) => true,
|
||||
}
|
||||
|
||||
if actions_and_servers.is_empty() {
|
||||
continue 'formatters;
|
||||
}
|
||||
|
||||
'actions: for (mut action, server_index) in actions_and_servers {
|
||||
let server = &adapters_and_servers[server_index].1;
|
||||
|
||||
let describe_code_action = |action: &CodeAction| {
|
||||
format!(
|
||||
"code action '{}' with title \"{}\"",
|
||||
action
|
||||
.lsp_action
|
||||
.action_kind()
|
||||
.unwrap_or("unknown".into())
|
||||
.as_str(),
|
||||
action.lsp_action.title()
|
||||
)
|
||||
};
|
||||
|
||||
if should_continue_formatting {
|
||||
// Apply any language-specific formatting, and group the two formatting operations
|
||||
// in the buffer's undo history.
|
||||
let this_transaction_id = match operation {
|
||||
FormatOperation::Lsp(edits) => b.edit(edits, None, cx),
|
||||
FormatOperation::External(diff) => b.apply_diff(diff, cx),
|
||||
FormatOperation::Prettier(diff) => b.apply_diff(diff, cx),
|
||||
// NOTE: code below duplicated from `Self::deserialize_workspace_edit`
|
||||
// but filters out and logs warnings for code actions that cause unreasonably
|
||||
// difficult handling on our part, such as:
|
||||
// - applying edits that call commands
|
||||
// which can result in arbitrary workspace edits being sent from the server that
|
||||
// have no way of being tied back to the command that initiated them (i.e. we
|
||||
// can't know which edits are part of the format request, or if the server is done sending
|
||||
// actions in response to the command)
|
||||
// - actions that create/delete/modify/rename files other than the one we are formatting
|
||||
// as we then would need to handle such changes correctly in the local history as well
|
||||
// as the remote history through the ProjectTransaction
|
||||
// - actions with snippet edits, as these simply don't make sense in the context of a format request
|
||||
// Supporting these actions is not impossible, but not supported as of yet.
|
||||
|
||||
if let Err(err) =
|
||||
Self::try_resolve_code_action(server, &mut action).await
|
||||
{
|
||||
log::error!(
|
||||
"Failed to resolve {}. Error: {}",
|
||||
describe_code_action(&action),
|
||||
err
|
||||
);
|
||||
continue 'actions;
|
||||
}
|
||||
if let Some(_) = action.lsp_action.command() {
|
||||
log::warn!(
|
||||
"Code actions with commands are not supported while formatting. Skipping {}",
|
||||
describe_code_action(&action),
|
||||
);
|
||||
continue 'actions;
|
||||
}
|
||||
let Some(edit) = action.lsp_action.edit().cloned() else {
|
||||
log::warn!(
|
||||
"No edit found for while formatting. Skipping {}",
|
||||
describe_code_action(&action),
|
||||
);
|
||||
continue 'actions;
|
||||
};
|
||||
if initial_transaction_id.is_none() {
|
||||
initial_transaction_id = this_transaction_id;
|
||||
}
|
||||
if this_transaction_id.is_some() {
|
||||
prev_transaction_id = this_transaction_id;
|
||||
}
|
||||
|
||||
if edit.changes.is_none() && edit.document_changes.is_none() {
|
||||
continue 'actions;
|
||||
}
|
||||
|
||||
if let Some(transaction_id) = initial_transaction_id {
|
||||
b.group_until_transaction(transaction_id);
|
||||
} else if let Some(transaction) = project_transaction.0.get(&buffer.handle) {
|
||||
b.group_until_transaction(transaction.id)
|
||||
let mut operations = Vec::new();
|
||||
if let Some(document_changes) = edit.document_changes {
|
||||
match document_changes {
|
||||
lsp::DocumentChanges::Edits(edits) => operations.extend(
|
||||
edits.into_iter().map(lsp::DocumentChangeOperation::Edit),
|
||||
),
|
||||
lsp::DocumentChanges::Operations(ops) => operations = ops,
|
||||
}
|
||||
} else if let Some(changes) = edit.changes {
|
||||
operations.extend(changes.into_iter().map(|(uri, edits)| {
|
||||
lsp::DocumentChangeOperation::Edit(lsp::TextDocumentEdit {
|
||||
text_document:
|
||||
lsp::OptionalVersionedTextDocumentIdentifier {
|
||||
uri,
|
||||
version: None,
|
||||
},
|
||||
edits: edits.into_iter().map(Edit::Plain).collect(),
|
||||
})
|
||||
}));
|
||||
}
|
||||
|
||||
let mut edits = Vec::with_capacity(operations.len());
|
||||
|
||||
for operation in operations {
|
||||
let op = match operation {
|
||||
lsp::DocumentChangeOperation::Edit(op) => op,
|
||||
lsp::DocumentChangeOperation::Op(_) => {
|
||||
log::warn!(
|
||||
"Code actions which create, delete, or rename files are not supported on format. Skipping {}",
|
||||
describe_code_action(&action),
|
||||
);
|
||||
continue 'actions;
|
||||
}
|
||||
};
|
||||
let Ok(file_path) = op.text_document.uri.to_file_path() else {
|
||||
log::warn!(
|
||||
"Failed to convert URI '{:?}' to file path. Skipping {}",
|
||||
&op.text_document.uri,
|
||||
describe_code_action(&action),
|
||||
);
|
||||
continue 'actions;
|
||||
};
|
||||
if &file_path != buffer_path_abs {
|
||||
log::warn!(
|
||||
"File path '{:?}' does not match buffer path '{:?}'. Skipping {}",
|
||||
file_path,
|
||||
buffer_path_abs,
|
||||
describe_code_action(&action),
|
||||
);
|
||||
continue 'actions;
|
||||
}
|
||||
|
||||
let mut lsp_edits = Vec::new();
|
||||
for edit in op.edits {
|
||||
match edit {
|
||||
Edit::Plain(edit) => {
|
||||
if !lsp_edits.contains(&edit) {
|
||||
lsp_edits.push(edit);
|
||||
}
|
||||
}
|
||||
Edit::Annotated(edit) => {
|
||||
if !lsp_edits.contains(&edit.text_edit) {
|
||||
lsp_edits.push(edit.text_edit);
|
||||
}
|
||||
}
|
||||
Edit::Snippet(_) => {
|
||||
log::warn!(
|
||||
"Code actions which produce snippet edits are not supported during formatting. Skipping {}",
|
||||
describe_code_action(&action),
|
||||
);
|
||||
continue 'actions;
|
||||
}
|
||||
}
|
||||
}
|
||||
let edits_result = lsp_store
|
||||
.update(cx, |lsp_store, cx| {
|
||||
lsp_store.as_local_mut().unwrap().edits_from_lsp(
|
||||
&buffer.handle,
|
||||
lsp_edits,
|
||||
server.server_id(),
|
||||
op.text_document.version,
|
||||
cx,
|
||||
)
|
||||
})?
|
||||
.await
|
||||
.with_context(
|
||||
|| format!(
|
||||
"Failed to resolve edits from LSP for buffer {:?} while handling {}",
|
||||
buffer_path_abs.as_path(),
|
||||
describe_code_action(&action),
|
||||
)
|
||||
).log_err();
|
||||
let Some(resolved_edits) = edits_result else {
|
||||
continue 'actions;
|
||||
};
|
||||
edits.extend(resolved_edits);
|
||||
}
|
||||
|
||||
if let Some(err) =
|
||||
err_if_buffer_edited_since_start(buffer, transaction_id_format, &cx)
|
||||
{
|
||||
result = Err(err);
|
||||
break 'formatters;
|
||||
}
|
||||
buffer.handle.update(cx, |buffer, cx| {
|
||||
buffer.start_transaction();
|
||||
buffer.edit(edits, None, cx);
|
||||
transaction_id_format =
|
||||
transaction_id_format.or(buffer.end_transaction(cx));
|
||||
if let Some(transaction_id) = transaction_id_format {
|
||||
buffer.group_until_transaction(transaction_id);
|
||||
}
|
||||
return should_continue_formatting;
|
||||
})?;
|
||||
if !should_continue_formatting {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
buffer.handle.update(cx, |b, _cx| {
|
||||
if let Some(transaction) = b.finalize_last_transaction().cloned() {
|
||||
let buffer_handle = buffer.handle.clone();
|
||||
buffer.handle.update(cx, |buffer, _| {
|
||||
let Some(transaction_id) = transaction_id_format else {
|
||||
return result;
|
||||
};
|
||||
let Some(transaction_id_last) =
|
||||
buffer.peek_undo_stack().map(|t| t.transaction_id())
|
||||
else {
|
||||
// unwrapping should work here, how would we get a transaction id
|
||||
// with no transaction on the undo stack?
|
||||
// *but* it occasionally panics. Avoiding panics for now...
|
||||
return result;
|
||||
};
|
||||
if transaction_id_last != transaction_id {
|
||||
return result;
|
||||
}
|
||||
let transaction = buffer
|
||||
.finalize_last_transaction()
|
||||
.cloned()
|
||||
.expect("There is a transaction on the undo stack if we were able to peek it");
|
||||
// debug_assert_eq!(transaction.id, transaction_id);
|
||||
if !push_to_history {
|
||||
b.forget_transaction(transaction.id);
|
||||
buffer.forget_transaction(transaction.id);
|
||||
}
|
||||
project_transaction
|
||||
.0
|
||||
.insert(buffer.handle.clone(), transaction);
|
||||
.insert(buffer_handle.clone(), transaction);
|
||||
return result;
|
||||
})??;
|
||||
}
|
||||
}
|
||||
})?;
|
||||
return Ok(());
|
||||
|
||||
return Ok(project_transaction);
|
||||
}
|
||||
|
||||
pub async fn format_ranges_via_lsp(
|
||||
|
@ -2096,29 +2365,35 @@ impl LocalLspStore {
|
|||
}
|
||||
}
|
||||
|
||||
async fn execute_code_actions_on_servers(
|
||||
async fn get_server_code_actions_from_action_kinds(
|
||||
lsp_store: &WeakEntity<LspStore>,
|
||||
adapters_and_servers: &[(Arc<CachedLspAdapter>, Arc<LanguageServer>)],
|
||||
code_actions: Vec<lsp::CodeActionKind>,
|
||||
language_server_id: LanguageServerId,
|
||||
code_action_kinds: Vec<lsp::CodeActionKind>,
|
||||
buffer: &Entity<Buffer>,
|
||||
push_to_history: bool,
|
||||
project_transaction: &mut ProjectTransaction,
|
||||
cx: &mut AsyncApp,
|
||||
) -> Result<(), anyhow::Error> {
|
||||
for (lsp_adapter, language_server) in adapters_and_servers.iter() {
|
||||
let code_actions = code_actions.clone();
|
||||
|
||||
) -> Result<Vec<CodeAction>> {
|
||||
let actions = lsp_store
|
||||
.update(cx, move |this, cx| {
|
||||
let request = GetCodeActions {
|
||||
range: text::Anchor::MIN..text::Anchor::MAX,
|
||||
kinds: Some(code_actions),
|
||||
kinds: Some(code_action_kinds),
|
||||
};
|
||||
let server = LanguageServerToQuery::Other(language_server.server_id());
|
||||
let server = LanguageServerToQuery::Other(language_server_id);
|
||||
this.request_lsp(buffer.clone(), server, request, cx)
|
||||
})?
|
||||
.await?;
|
||||
return Ok(actions);
|
||||
}
|
||||
|
||||
pub async fn execute_code_actions_on_server(
|
||||
lsp_store: &WeakEntity<LspStore>,
|
||||
language_server: &Arc<LanguageServer>,
|
||||
lsp_adapter: &Arc<CachedLspAdapter>,
|
||||
actions: Vec<CodeAction>,
|
||||
push_to_history: bool,
|
||||
project_transaction: &mut ProjectTransaction,
|
||||
cx: &mut AsyncApp,
|
||||
) -> Result<(), anyhow::Error> {
|
||||
for mut action in actions {
|
||||
Self::try_resolve_code_action(language_server, &mut action)
|
||||
.await
|
||||
|
@ -2179,9 +2454,7 @@ impl LocalLspStore {
|
|||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
pub async fn deserialize_text_edits(
|
||||
|
|
|
@ -704,7 +704,7 @@ pub(super) async fn format_with_prettier(
|
|||
prettier_store: &WeakEntity<PrettierStore>,
|
||||
buffer: &Entity<Buffer>,
|
||||
cx: &mut AsyncApp,
|
||||
) -> Option<Result<crate::lsp_store::FormatOperation>> {
|
||||
) -> Option<Result<language::Diff>> {
|
||||
let prettier_instance = prettier_store
|
||||
.update(cx, |prettier_store, cx| {
|
||||
prettier_store.prettier_instance_for_buffer(buffer, cx)
|
||||
|
@ -738,7 +738,6 @@ pub(super) async fn format_with_prettier(
|
|||
let format_result = prettier
|
||||
.format(buffer, buffer_path, ignore_dir, cx)
|
||||
.await
|
||||
.map(crate::lsp_store::FormatOperation::Prettier)
|
||||
.with_context(|| format!("{} failed to format buffer", prettier_description));
|
||||
|
||||
Some(format_result)
|
||||
|
|
|
@ -4699,19 +4699,6 @@ 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()
|
||||
}
|
||||
|
||||
pub struct PathMatchCandidateSet {
|
||||
pub snapshot: Snapshot,
|
||||
pub include_ignored: bool,
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue