From 48eed7499f66dc356f54027746ce8fa6ba845eb4 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Mon, 9 Dec 2024 22:47:13 -0700 Subject: [PATCH] Move diagnostics to the LocalLspStore (#21782) This should be a no-op, but clarifies that some fields of the LspStore were never actually used in the remote case. Release Notes: - N/A --- crates/project/src/lsp_command.rs | 11 +- crates/project/src/lsp_store.rs | 2182 ++++++++++++++------------- crates/project/src/project_tests.rs | 6 +- 3 files changed, 1118 insertions(+), 1081 deletions(-) diff --git a/crates/project/src/lsp_command.rs b/crates/project/src/lsp_command.rs index d317f5a4d496cbab78901db8e2990d7f3da92c1e..9b83fca8a91bf6e75a616227b06410d03f09fbc1 100644 --- a/crates/project/src/lsp_command.rs +++ b/crates/project/src/lsp_command.rs @@ -1,9 +1,10 @@ mod signature_help; use crate::{ - lsp_store::LspStore, CodeAction, CoreCompletion, DocumentHighlight, Hover, HoverBlock, - HoverBlockKind, InlayHint, InlayHintLabel, InlayHintLabelPart, InlayHintLabelPartTooltip, - InlayHintTooltip, Location, LocationLink, MarkupContent, ProjectTransaction, ResolveState, + lsp_store::{LocalLspStore, LspStore}, + CodeAction, CoreCompletion, DocumentHighlight, Hover, HoverBlock, HoverBlockKind, InlayHint, + InlayHintLabel, InlayHintLabelPart, InlayHintLabelPartTooltip, InlayHintTooltip, Location, + LocationLink, MarkupContent, ProjectTransaction, ResolveState, }; use anyhow::{anyhow, Context, Result}; use async_trait::async_trait; @@ -348,7 +349,7 @@ impl LspCommand for PerformRename { if let Some(edit) = message { let (lsp_adapter, lsp_server) = language_server_for_buffer(&lsp_store, &buffer, server_id, &mut cx)?; - LspStore::deserialize_workspace_edit( + LocalLspStore::deserialize_workspace_edit( lsp_store, edit, self.push_to_history, @@ -2306,7 +2307,7 @@ impl LspCommand for OnTypeFormatting { if let Some(edits) = message { let (lsp_adapter, lsp_server) = language_server_for_buffer(&lsp_store, &buffer, server_id, &mut cx)?; - LspStore::deserialize_text_edits( + LocalLspStore::deserialize_text_edits( lsp_store, buffer, edits, diff --git a/crates/project/src/lsp_store.rs b/crates/project/src/lsp_store.rs index 8f93e0bf67c9a068120e27e9d46fba57ce639cfd..aff4fceabb0abf0155d6b4af60ead137d097e522 100644 --- a/crates/project/src/lsp_store.rs +++ b/crates/project/src/lsp_store.rs @@ -149,6 +149,18 @@ pub struct LocalLspStore { HashMap)>, prettier_store: Model, current_lsp_settings: HashMap, + next_diagnostic_group_id: usize, + diagnostics: HashMap< + WorktreeId, + HashMap< + Arc, + Vec<( + LanguageServerId, + Vec>>, + )>, + >, + >, + buffer_snapshots: HashMap>>, // buffer_id -> server_id -> vec of snapshots _subscription: gpui::Subscription, } @@ -263,7 +275,7 @@ impl LocalLspStore { if !code_actions.is_empty() && !(trigger == FormatTrigger::Save && settings.format_on_save == FormatOnSave::Off) { - LspStore::execute_code_actions_on_servers( + Self::execute_code_actions_on_servers( &lsp_store, &adapters_and_servers, code_actions, @@ -532,7 +544,7 @@ impl LocalLspStore { match format_target { FormatTarget::Buffer => Some(FormatOperation::Lsp( - LspStore::format_via_lsp( + Self::format_via_lsp( &lsp_store, &buffer.handle, buffer_abs_path, @@ -544,7 +556,7 @@ impl LocalLspStore { .context("failed to format via language server")?, )), FormatTarget::Ranges(selections) => Some(FormatOperation::Lsp( - LspStore::format_range_via_lsp( + Self::format_range_via_lsp( &lsp_store, &buffer.handle, selections.as_slice(), @@ -581,7 +593,7 @@ impl LocalLspStore { Formatter::CodeActions(code_actions) => { let code_actions = deserialize_code_actions(code_actions); if !code_actions.is_empty() { - LspStore::execute_code_actions_on_servers( + Self::execute_code_actions_on_servers( &lsp_store, adapters_and_servers, code_actions, @@ -598,6 +610,124 @@ impl LocalLspStore { anyhow::Ok(result) } + pub async fn format_range_via_lsp( + this: &WeakModel, + buffer: &Model, + selections: &[Selection], + abs_path: &Path, + language_server: &Arc, + settings: &LanguageSettings, + cx: &mut AsyncAppContext, + ) -> Result, String)>> { + let capabilities = &language_server.capabilities(); + let range_formatting_provider = capabilities.document_range_formatting_provider.as_ref(); + if range_formatting_provider.map_or(false, |provider| provider == &OneOf::Left(false)) { + return Err(anyhow!( + "{} language server does not support range formatting", + language_server.name() + )); + } + + let uri = lsp::Url::from_file_path(abs_path) + .map_err(|_| anyhow!("failed to convert abs path to uri"))?; + let text_document = lsp::TextDocumentIdentifier::new(uri); + + let lsp_edits = { + let ranges = selections.into_iter().map(|s| { + let start = lsp::Position::new(s.start.row, s.start.column); + let end = lsp::Position::new(s.end.row, s.end.column); + lsp::Range::new(start, end) + }); + + let mut edits = None; + for range in ranges { + if let Some(mut edit) = language_server + .request::(lsp::DocumentRangeFormattingParams { + text_document: text_document.clone(), + range, + options: lsp_command::lsp_formatting_options(settings), + work_done_progress_params: Default::default(), + }) + .await? + { + edits.get_or_insert_with(Vec::new).append(&mut edit); + } + } + edits + }; + + if let Some(lsp_edits) = lsp_edits { + this.update(cx, |this, cx| { + this.as_local_mut().unwrap().edits_from_lsp( + buffer, + lsp_edits, + language_server.server_id(), + None, + cx, + ) + })? + .await + } else { + Ok(Vec::with_capacity(0)) + } + } + + #[allow(clippy::too_many_arguments)] + async fn format_via_lsp( + this: &WeakModel, + buffer: &Model, + abs_path: &Path, + language_server: &Arc, + settings: &LanguageSettings, + cx: &mut AsyncAppContext, + ) -> Result, String)>> { + let uri = lsp::Url::from_file_path(abs_path) + .map_err(|_| anyhow!("failed to convert abs path to uri"))?; + let text_document = lsp::TextDocumentIdentifier::new(uri); + let capabilities = &language_server.capabilities(); + + let formatting_provider = capabilities.document_formatting_provider.as_ref(); + let range_formatting_provider = capabilities.document_range_formatting_provider.as_ref(); + + let lsp_edits = if matches!(formatting_provider, Some(p) if *p != OneOf::Left(false)) { + language_server + .request::(lsp::DocumentFormattingParams { + text_document, + options: lsp_command::lsp_formatting_options(settings), + work_done_progress_params: Default::default(), + }) + .await? + } else if matches!(range_formatting_provider, Some(p) if *p != OneOf::Left(false)) { + let buffer_start = lsp::Position::new(0, 0); + let buffer_end = buffer.update(cx, |b, _| point_to_lsp(b.max_point_utf16()))?; + language_server + .request::(lsp::DocumentRangeFormattingParams { + text_document: text_document.clone(), + range: lsp::Range::new(buffer_start, buffer_end), + options: lsp_command::lsp_formatting_options(settings), + work_done_progress_params: Default::default(), + }) + .await? + } else { + None + }; + + if let Some(lsp_edits) = lsp_edits { + this.update(cx, |this, cx| { + this.as_local_mut().unwrap().edits_from_lsp( + buffer, + lsp_edits, + language_server.server_id(), + None, + cx, + ) + })? + .await + } else { + Ok(Vec::with_capacity(0)) + } + } + async fn format_via_external_command( buffer: &FormattableBuffer, command: &str, @@ -670,105 +800,788 @@ impl LocalLspStore { .await, )) } -} - -#[derive(Debug)] -pub struct FormattableBuffer { - handle: Model, - abs_path: Option, - env: Option>, -} - -pub struct RemoteLspStore { - upstream_client: Option, - upstream_project_id: u64, -} -#[allow(clippy::large_enum_variant)] -pub enum LspStoreMode { - Local(LocalLspStore), // ssh host and collab host - Remote(RemoteLspStore), // collab guest -} + async fn try_resolve_code_action( + lang_server: &LanguageServer, + action: &mut CodeAction, + ) -> anyhow::Result<()> { + if GetCodeActions::can_resolve_actions(&lang_server.capabilities()) + && action.lsp_action.data.is_some() + && (action.lsp_action.command.is_none() || action.lsp_action.edit.is_none()) + { + action.lsp_action = lang_server + .request::(action.lsp_action.clone()) + .await?; + } -impl LspStoreMode { - fn is_local(&self) -> bool { - matches!(self, LspStoreMode::Local(_)) + anyhow::Ok(()) } - fn is_remote(&self) -> bool { - matches!(self, LspStoreMode::Remote(_)) - } -} + fn register_buffer_with_language_servers( + &mut self, + buffer_handle: &Model, + language_server_ids: &HashMap<(WorktreeId, LanguageServerName), LanguageServerId>, + cx: &mut ModelContext, + ) { + let buffer = buffer_handle.read(cx); + let buffer_id = buffer.remote_id(); -pub struct LspStore { - mode: LspStoreMode, - last_formatting_failure: Option, - downstream_client: Option<(AnyProtoClient, u64)>, - nonce: u128, - buffer_store: Model, - worktree_store: Model, - toolchain_store: Option>, - buffer_snapshots: HashMap>>, // buffer_id -> server_id -> vec of snapshots - pub languages: Arc, - language_server_ids: HashMap<(WorktreeId, LanguageServerName), LanguageServerId>, - pub language_server_statuses: BTreeMap, - active_entry: Option, - _maintain_workspace_config: (Task>, watch::Sender<()>), - _maintain_buffer_languages: Task<()>, - next_diagnostic_group_id: usize, - diagnostic_summaries: - HashMap, HashMap>>, - diagnostics: HashMap< - WorktreeId, - HashMap< - Arc, - Vec<( - LanguageServerId, - Vec>>, - )>, - >, - >, -} + if let Some(file) = File::from_dyn(buffer.file()) { + if !file.is_local() { + return; + } -pub enum LspStoreEvent { - LanguageServerAdded(LanguageServerId, LanguageServerName, Option), - LanguageServerRemoved(LanguageServerId), - LanguageServerUpdate { - language_server_id: LanguageServerId, - message: proto::update_language_server::Variant, - }, - LanguageServerLog(LanguageServerId, LanguageServerLogType, String), - LanguageServerPrompt(LanguageServerPromptRequest), - LanguageDetected { - buffer: Model, - new_language: Option>, - }, - Notification(String), - RefreshInlayHints, - DiagnosticsUpdated { - language_server_id: LanguageServerId, - path: ProjectPath, - }, - DiskBasedDiagnosticsStarted { - language_server_id: LanguageServerId, - }, - DiskBasedDiagnosticsFinished { - language_server_id: LanguageServerId, - }, - SnippetEdit { - buffer_id: BufferId, - edits: Vec<(lsp::Range, Snippet)>, - most_recent_edit: clock::Lamport, - }, -} + let abs_path = file.abs_path(cx); + let Some(uri) = lsp::Url::from_file_path(&abs_path).log_err() else { + return; + }; + let initial_snapshot = buffer.text_snapshot(); + let worktree_id = file.worktree_id(cx); + let Some(languages) = buffer.language_registry() else { + return; + }; + let language = buffer.language().cloned(); -#[derive(Clone, Debug, Serialize)] -pub struct LanguageServerStatus { - pub name: String, - pub pending_work: BTreeMap, - pub has_pending_diagnostic_updates: bool, - progress_tokens: HashSet, -} + if let Some(diagnostics) = self.diagnostics.get(&worktree_id) { + for (server_id, diagnostics) in + diagnostics.get(file.path()).cloned().unwrap_or_default() + { + self.update_buffer_diagnostics(buffer_handle, server_id, None, diagnostics, cx) + .log_err(); + } + } + + if let Some(language) = language { + for adapter in languages.lsp_adapters(&language.name()) { + let server = language_server_ids + .get(&(worktree_id, adapter.name.clone())) + .and_then(|id| self.language_servers.get(id)) + .and_then(|server_state| { + if let LanguageServerState::Running { server, .. } = server_state { + Some(server.clone()) + } else { + None + } + }); + let server = match server { + Some(server) => server, + None => continue, + }; + + server + .notify::( + lsp::DidOpenTextDocumentParams { + text_document: lsp::TextDocumentItem::new( + uri.clone(), + adapter.language_id(&language.name()), + 0, + initial_snapshot.text(), + ), + }, + ) + .log_err(); + + buffer_handle.update(cx, |buffer, cx| { + buffer.set_completion_triggers( + server.server_id(), + server + .capabilities() + .completion_provider + .as_ref() + .and_then(|provider| { + provider + .trigger_characters + .as_ref() + .map(|characters| characters.iter().cloned().collect()) + }) + .unwrap_or_default(), + cx, + ); + }); + + let snapshot = LspBufferSnapshot { + version: 0, + snapshot: initial_snapshot.clone(), + }; + self.buffer_snapshots + .entry(buffer_id) + .or_default() + .insert(server.server_id(), vec![snapshot]); + } + } + } + } + + fn update_buffer_diagnostics( + &mut self, + buffer: &Model, + server_id: LanguageServerId, + version: Option, + mut diagnostics: Vec>>, + cx: &mut ModelContext, + ) -> Result<()> { + fn compare_diagnostics(a: &Diagnostic, b: &Diagnostic) -> Ordering { + Ordering::Equal + .then_with(|| b.is_primary.cmp(&a.is_primary)) + .then_with(|| a.is_disk_based.cmp(&b.is_disk_based)) + .then_with(|| a.severity.cmp(&b.severity)) + .then_with(|| a.message.cmp(&b.message)) + } + + let snapshot = self.buffer_snapshot_for_lsp_version(buffer, server_id, version, cx)?; + + diagnostics.sort_unstable_by(|a, b| { + Ordering::Equal + .then_with(|| a.range.start.cmp(&b.range.start)) + .then_with(|| b.range.end.cmp(&a.range.end)) + .then_with(|| compare_diagnostics(&a.diagnostic, &b.diagnostic)) + }); + + let mut sanitized_diagnostics = Vec::new(); + let edits_since_save = Patch::new( + snapshot + .edits_since::>(buffer.read(cx).saved_version()) + .collect(), + ); + for entry in diagnostics { + let start; + let end; + if entry.diagnostic.is_disk_based { + // Some diagnostics are based on files on disk instead of buffers' + // current contents. Adjust these diagnostics' ranges to reflect + // any unsaved edits. + start = edits_since_save.old_to_new(entry.range.start); + end = edits_since_save.old_to_new(entry.range.end); + } else { + start = entry.range.start; + end = entry.range.end; + } + + let mut range = snapshot.clip_point_utf16(start, Bias::Left) + ..snapshot.clip_point_utf16(end, Bias::Right); + + // Expand empty ranges by one codepoint + if range.start == range.end { + // This will be go to the next boundary when being clipped + range.end.column += 1; + range.end = snapshot.clip_point_utf16(Unclipped(range.end), Bias::Right); + if range.start == range.end && range.end.column > 0 { + range.start.column -= 1; + range.start = snapshot.clip_point_utf16(Unclipped(range.start), Bias::Left); + } + } + + sanitized_diagnostics.push(DiagnosticEntry { + range, + diagnostic: entry.diagnostic, + }); + } + drop(edits_since_save); + + let set = DiagnosticSet::new(sanitized_diagnostics, &snapshot); + buffer.update(cx, |buffer, cx| { + buffer.update_diagnostics(server_id, set, cx) + }); + Ok(()) + } + + fn buffer_snapshot_for_lsp_version( + &mut self, + buffer: &Model, + server_id: LanguageServerId, + version: Option, + cx: &AppContext, + ) -> Result { + const OLD_VERSIONS_TO_RETAIN: i32 = 10; + + if let Some(version) = version { + let buffer_id = buffer.read(cx).remote_id(); + let snapshots = self + .buffer_snapshots + .get_mut(&buffer_id) + .and_then(|m| m.get_mut(&server_id)) + .ok_or_else(|| { + anyhow!("no snapshots found for buffer {buffer_id} and server {server_id}") + })?; + + let found_snapshot = snapshots + .binary_search_by_key(&version, |e| e.version) + .map(|ix| snapshots[ix].snapshot.clone()) + .map_err(|_| { + anyhow!("snapshot not found for buffer {buffer_id} server {server_id} at version {version}") + })?; + + snapshots.retain(|snapshot| snapshot.version + OLD_VERSIONS_TO_RETAIN >= version); + Ok(found_snapshot) + } else { + Ok((buffer.read(cx)).text_snapshot()) + } + } + + async fn execute_code_actions_on_servers( + this: &WeakModel, + adapters_and_servers: &[(Arc, Arc)], + code_actions: Vec, + buffer: &Model, + 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 = this + .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( + this.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 { + this.update(cx, |this, _| { + if let LspStoreMode::Local(mode) = &mut this.mode { + mode.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?; + + this.update(cx, |this, _| { + if let LspStoreMode::Local(mode) = &mut this.mode { + project_transaction.0.extend( + mode.last_workspace_edits_by_language_server + .remove(&language_server.server_id()) + .unwrap_or_default() + .0, + ) + } + })?; + } + } + } + + Ok(()) + } + + pub async fn deserialize_text_edits( + this: Model, + buffer_to_edit: Model, + edits: Vec, + push_to_history: bool, + _: Arc, + language_server: Arc, + cx: &mut AsyncAppContext, + ) -> Result> { + let edits = this + .update(cx, |this, cx| { + this.as_local_mut().unwrap().edits_from_lsp( + &buffer_to_edit, + edits, + language_server.server_id(), + None, + cx, + ) + })? + .await?; + + let transaction = buffer_to_edit.update(cx, |buffer, cx| { + buffer.finalize_last_transaction(); + buffer.start_transaction(); + for (range, text) in edits { + buffer.edit([(range, text)], None, cx); + } + + if buffer.end_transaction(cx).is_some() { + let transaction = buffer.finalize_last_transaction().unwrap().clone(); + if !push_to_history { + buffer.forget_transaction(transaction.id); + } + Some(transaction) + } else { + None + } + })?; + + Ok(transaction) + } + + #[allow(clippy::type_complexity)] + pub(crate) fn edits_from_lsp( + &mut self, + buffer: &Model, + lsp_edits: impl 'static + Send + IntoIterator, + server_id: LanguageServerId, + version: Option, + cx: &mut ModelContext, + ) -> Task, String)>>> { + let snapshot = self.buffer_snapshot_for_lsp_version(buffer, server_id, version, cx); + cx.background_executor().spawn(async move { + let snapshot = snapshot?; + let mut lsp_edits = lsp_edits + .into_iter() + .map(|edit| (range_from_lsp(edit.range), edit.new_text)) + .collect::>(); + lsp_edits.sort_by_key(|(range, _)| range.start); + + let mut lsp_edits = lsp_edits.into_iter().peekable(); + let mut edits = Vec::new(); + while let Some((range, mut new_text)) = lsp_edits.next() { + // Clip invalid ranges provided by the language server. + let mut range = snapshot.clip_point_utf16(range.start, Bias::Left) + ..snapshot.clip_point_utf16(range.end, Bias::Left); + + // Combine any LSP edits that are adjacent. + // + // Also, combine LSP edits that are separated from each other by only + // a newline. This is important because for some code actions, + // Rust-analyzer rewrites the entire buffer via a series of edits that + // are separated by unchanged newline characters. + // + // In order for the diffing logic below to work properly, any edits that + // cancel each other out must be combined into one. + while let Some((next_range, next_text)) = lsp_edits.peek() { + if next_range.start.0 > range.end { + if next_range.start.0.row > range.end.row + 1 + || next_range.start.0.column > 0 + || snapshot.clip_point_utf16( + Unclipped(PointUtf16::new(range.end.row, u32::MAX)), + Bias::Left, + ) > range.end + { + break; + } + new_text.push('\n'); + } + range.end = snapshot.clip_point_utf16(next_range.end, Bias::Left); + new_text.push_str(next_text); + lsp_edits.next(); + } + + // For multiline edits, perform a diff of the old and new text so that + // we can identify the changes more precisely, preserving the locations + // of any anchors positioned in the unchanged regions. + if range.end.row > range.start.row { + let mut offset = range.start.to_offset(&snapshot); + let old_text = snapshot.text_for_range(range).collect::(); + + let diff = TextDiff::from_lines(old_text.as_str(), &new_text); + let mut moved_since_edit = true; + for change in diff.iter_all_changes() { + let tag = change.tag(); + let value = change.value(); + match tag { + ChangeTag::Equal => { + offset += value.len(); + moved_since_edit = true; + } + ChangeTag::Delete => { + let start = snapshot.anchor_after(offset); + let end = snapshot.anchor_before(offset + value.len()); + if moved_since_edit { + edits.push((start..end, String::new())); + } else { + edits.last_mut().unwrap().0.end = end; + } + offset += value.len(); + moved_since_edit = false; + } + ChangeTag::Insert => { + if moved_since_edit { + let anchor = snapshot.anchor_after(offset); + edits.push((anchor..anchor, value.to_string())); + } else { + edits.last_mut().unwrap().1.push_str(value); + } + moved_since_edit = false; + } + } + } + } else if range.end == range.start { + let anchor = snapshot.anchor_after(range.start); + edits.push((anchor..anchor, new_text)); + } else { + let edit_start = snapshot.anchor_after(range.start); + let edit_end = snapshot.anchor_before(range.end); + edits.push((edit_start..edit_end, new_text)); + } + } + + Ok(edits) + }) + } + + pub(crate) async fn deserialize_workspace_edit( + this: Model, + edit: lsp::WorkspaceEdit, + push_to_history: bool, + lsp_adapter: Arc, + language_server: Arc, + cx: &mut AsyncAppContext, + ) -> Result { + let fs = this.read_with(cx, |this, _| this.as_local().unwrap().fs.clone())?; + + 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 project_transaction = ProjectTransaction::default(); + for operation in operations { + match operation { + lsp::DocumentChangeOperation::Op(lsp::ResourceOp::Create(op)) => { + let abs_path = op + .uri + .to_file_path() + .map_err(|_| anyhow!("can't convert URI to path"))?; + + if let Some(parent_path) = abs_path.parent() { + fs.create_dir(parent_path).await?; + } + if abs_path.ends_with("/") { + fs.create_dir(&abs_path).await?; + } else { + fs.create_file( + &abs_path, + op.options + .map(|options| fs::CreateOptions { + overwrite: options.overwrite.unwrap_or(false), + ignore_if_exists: options.ignore_if_exists.unwrap_or(false), + }) + .unwrap_or_default(), + ) + .await?; + } + } + + lsp::DocumentChangeOperation::Op(lsp::ResourceOp::Rename(op)) => { + let source_abs_path = op + .old_uri + .to_file_path() + .map_err(|_| anyhow!("can't convert URI to path"))?; + let target_abs_path = op + .new_uri + .to_file_path() + .map_err(|_| anyhow!("can't convert URI to path"))?; + fs.rename( + &source_abs_path, + &target_abs_path, + op.options + .map(|options| fs::RenameOptions { + overwrite: options.overwrite.unwrap_or(false), + ignore_if_exists: options.ignore_if_exists.unwrap_or(false), + }) + .unwrap_or_default(), + ) + .await?; + } + + lsp::DocumentChangeOperation::Op(lsp::ResourceOp::Delete(op)) => { + let abs_path = op + .uri + .to_file_path() + .map_err(|_| anyhow!("can't convert URI to path"))?; + let options = op + .options + .map(|options| fs::RemoveOptions { + recursive: options.recursive.unwrap_or(false), + ignore_if_not_exists: options.ignore_if_not_exists.unwrap_or(false), + }) + .unwrap_or_default(); + if abs_path.ends_with("/") { + fs.remove_dir(&abs_path, options).await?; + } else { + fs.remove_file(&abs_path, options).await?; + } + } + + lsp::DocumentChangeOperation::Edit(op) => { + let buffer_to_edit = this + .update(cx, |this, cx| { + this.open_local_buffer_via_lsp( + op.text_document.uri.clone(), + language_server.server_id(), + lsp_adapter.name.clone(), + cx, + ) + })? + .await?; + + let edits = this + .update(cx, |this, cx| { + let path = buffer_to_edit.read(cx).project_path(cx); + let active_entry = this.active_entry; + let is_active_entry = path.clone().map_or(false, |project_path| { + this.worktree_store + .read(cx) + .entry_for_path(&project_path, cx) + .map_or(false, |entry| Some(entry.id) == active_entry) + }); + let local = this.as_local_mut().unwrap(); + + let (mut edits, mut snippet_edits) = (vec![], vec![]); + for edit in op.edits { + match edit { + Edit::Plain(edit) => edits.push(edit), + Edit::Annotated(edit) => edits.push(edit.text_edit), + Edit::Snippet(edit) => { + let Ok(snippet) = Snippet::parse(&edit.snippet.value) + else { + continue; + }; + + if is_active_entry { + snippet_edits.push((edit.range, snippet)); + } else { + // Since this buffer is not focused, apply a normal edit. + edits.push(TextEdit { + range: edit.range, + new_text: snippet.text, + }); + } + } + } + } + if !snippet_edits.is_empty() { + let buffer_id = buffer_to_edit.read(cx).remote_id(); + let version = if let Some(buffer_version) = op.text_document.version + { + local + .buffer_snapshot_for_lsp_version( + &buffer_to_edit, + language_server.server_id(), + Some(buffer_version), + cx, + ) + .ok() + .map(|snapshot| snapshot.version) + } else { + Some(buffer_to_edit.read(cx).saved_version().clone()) + }; + + let most_recent_edit = version.and_then(|version| { + version.iter().max_by_key(|timestamp| timestamp.value) + }); + // Check if the edit that triggered that edit has been made by this participant. + + if let Some(most_recent_edit) = most_recent_edit { + cx.emit(LspStoreEvent::SnippetEdit { + buffer_id, + edits: snippet_edits, + most_recent_edit, + }); + } + } + + local.edits_from_lsp( + &buffer_to_edit, + edits, + language_server.server_id(), + op.text_document.version, + cx, + ) + })? + .await?; + + let transaction = buffer_to_edit.update(cx, |buffer, cx| { + buffer.finalize_last_transaction(); + buffer.start_transaction(); + for (range, text) in edits { + buffer.edit([(range, text)], None, cx); + } + let transaction = if buffer.end_transaction(cx).is_some() { + let transaction = buffer.finalize_last_transaction().unwrap().clone(); + if !push_to_history { + buffer.forget_transaction(transaction.id); + } + Some(transaction) + } else { + None + }; + + transaction + })?; + if let Some(transaction) = transaction { + project_transaction.0.insert(buffer_to_edit, transaction); + } + } + } + } + + Ok(project_transaction) + } + + async fn on_lsp_workspace_edit( + this: WeakModel, + params: lsp::ApplyWorkspaceEditParams, + server_id: LanguageServerId, + adapter: Arc, + mut cx: AsyncAppContext, + ) -> Result { + let this = this + .upgrade() + .ok_or_else(|| anyhow!("project project closed"))?; + let language_server = this + .update(&mut cx, |this, _| this.language_server_for_id(server_id))? + .ok_or_else(|| anyhow!("language server not found"))?; + let transaction = Self::deserialize_workspace_edit( + this.clone(), + params.edit, + true, + adapter.clone(), + language_server.clone(), + &mut cx, + ) + .await + .log_err(); + this.update(&mut cx, |this, _| { + if let Some(transaction) = transaction { + this.as_local_mut() + .unwrap() + .last_workspace_edits_by_language_server + .insert(server_id, transaction); + } + })?; + Ok(lsp::ApplyWorkspaceEditResponse { + applied: true, + failed_change: None, + failure_reason: None, + }) + } +} + +#[derive(Debug)] +pub struct FormattableBuffer { + handle: Model, + abs_path: Option, + env: Option>, +} + +pub struct RemoteLspStore { + upstream_client: Option, + upstream_project_id: u64, +} + +#[allow(clippy::large_enum_variant)] +pub enum LspStoreMode { + Local(LocalLspStore), // ssh host and collab host + Remote(RemoteLspStore), // collab guest +} + +impl LspStoreMode { + fn is_local(&self) -> bool { + matches!(self, LspStoreMode::Local(_)) + } + + fn is_remote(&self) -> bool { + matches!(self, LspStoreMode::Remote(_)) + } +} + +pub struct LspStore { + mode: LspStoreMode, + last_formatting_failure: Option, + downstream_client: Option<(AnyProtoClient, u64)>, + nonce: u128, + buffer_store: Model, + worktree_store: Model, + toolchain_store: Option>, + pub languages: Arc, + language_server_ids: HashMap<(WorktreeId, LanguageServerName), LanguageServerId>, + pub language_server_statuses: BTreeMap, + active_entry: Option, + _maintain_workspace_config: (Task>, watch::Sender<()>), + _maintain_buffer_languages: Task<()>, + diagnostic_summaries: + HashMap, HashMap>>, +} + +pub enum LspStoreEvent { + LanguageServerAdded(LanguageServerId, LanguageServerName, Option), + LanguageServerRemoved(LanguageServerId), + LanguageServerUpdate { + language_server_id: LanguageServerId, + message: proto::update_language_server::Variant, + }, + LanguageServerLog(LanguageServerId, LanguageServerLogType, String), + LanguageServerPrompt(LanguageServerPromptRequest), + LanguageDetected { + buffer: Model, + new_language: Option>, + }, + Notification(String), + RefreshInlayHints, + DiagnosticsUpdated { + language_server_id: LanguageServerId, + path: ProjectPath, + }, + DiskBasedDiagnosticsStarted { + language_server_id: LanguageServerId, + }, + DiskBasedDiagnosticsFinished { + language_server_id: LanguageServerId, + }, + SnippetEdit { + buffer_id: BufferId, + edits: Vec<(lsp::Range, Snippet)>, + most_recent_edit: clock::Lamport, + }, +} + +#[derive(Clone, Debug, Serialize)] +pub struct LanguageServerStatus { + pub name: String, + pub pending_work: BTreeMap, + pub has_pending_diagnostic_updates: bool, + progress_tokens: HashSet, +} #[derive(Clone, Debug)] struct CoreSymbol { @@ -906,11 +1719,14 @@ impl LspStore { language_server_watcher_registrations: Default::default(), current_lsp_settings: ProjectSettings::get_global(cx).lsp.clone(), buffers_being_formatted: Default::default(), + buffer_snapshots: Default::default(), prettier_store, environment, http_client, fs, yarn, + next_diagnostic_group_id: Default::default(), + diagnostics: Default::default(), _subscription: cx.on_app_quit(|this, cx| { this.as_local_mut().unwrap().shutdown_language_servers(cx) }), @@ -924,10 +1740,7 @@ impl LspStore { language_server_ids: Default::default(), language_server_statuses: Default::default(), nonce: StdRng::from_entropy().gen(), - buffer_snapshots: Default::default(), - next_diagnostic_group_id: Default::default(), diagnostic_summaries: Default::default(), - diagnostics: Default::default(), active_entry: None, _maintain_workspace_config, @@ -983,10 +1796,7 @@ impl LspStore { language_server_ids: Default::default(), language_server_statuses: Default::default(), nonce: StdRng::from_entropy().gen(), - buffer_snapshots: Default::default(), - next_diagnostic_group_id: Default::default(), diagnostic_summaries: Default::default(), - diagnostics: Default::default(), active_entry: None, toolchain_store, _maintain_workspace_config, @@ -1522,100 +2332,6 @@ impl LspStore { cx.notify(); } - pub async fn execute_code_actions_on_servers( - this: &WeakModel, - adapters_and_servers: &[(Arc, Arc)], - code_actions: Vec, - buffer: &Model, - 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 = this - .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( - this.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 { - this.update(cx, |this, _| { - if let LspStoreMode::Local(mode) = &mut this.mode { - mode.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?; - - this.update(cx, |this, _| { - if let LspStoreMode::Local(mode) = &mut this.mode { - project_transaction.0.extend( - mode.last_workspace_edits_by_language_server - .remove(&language_server.server_id()) - .unwrap_or_default() - .0, - ) - } - })?; - } - } - } - - Ok(()) - } - - async fn try_resolve_code_action( - lang_server: &LanguageServer, - action: &mut CodeAction, - ) -> anyhow::Result<()> { - if GetCodeActions::can_resolve_actions(&lang_server.capabilities()) - && action.lsp_action.data.is_some() - && (action.lsp_action.command.is_none() || action.lsp_action.edit.is_none()) - { - action.lsp_action = lang_server - .request::(action.lsp_action.clone()) - .await?; - } - - anyhow::Ok(()) - } - pub fn apply_code_action( &self, buffer_handle: Model, @@ -1653,12 +2369,12 @@ impl LspStore { return Task::ready(Ok(Default::default())); }; cx.spawn(move |this, mut cx| async move { - Self::try_resolve_code_action(&lang_server, &mut action) + LocalLspStore::try_resolve_code_action(&lang_server, &mut action) .await .context("resolving a code action")?; if let Some(edit) = action.lsp_action.edit { if edit.changes.is_some() || edit.document_changes.is_some() { - return Self::deserialize_workspace_edit( + return LocalLspStore::deserialize_workspace_edit( this.upgrade().ok_or_else(|| anyhow!("no app present"))?, edit, push_to_history, @@ -1854,167 +2570,62 @@ impl LspStore { buffer .update(&mut cx, |buffer, _| { - buffer.wait_for_edits(Some(position.timestamp)) - })? - .await?; - this.update(&mut cx, |this, cx| { - let position = position.to_point_utf16(buffer.read(cx)); - this.on_type_format(buffer, position, trigger, false, cx) - })? - .await - }) - } else { - Task::ready(Err(anyhow!("No upstream client or local language server"))) - } - } - - pub fn on_type_format( - &mut self, - buffer: Model, - position: T, - trigger: String, - push_to_history: bool, - cx: &mut ModelContext, - ) -> Task>> { - let position = position.to_point_utf16(buffer.read(cx)); - self.on_type_format_impl(buffer, position, trigger, push_to_history, cx) - } - - fn on_type_format_impl( - &mut self, - buffer: Model, - position: PointUtf16, - trigger: String, - push_to_history: bool, - cx: &mut ModelContext, - ) -> Task>> { - let options = buffer.update(cx, |buffer, cx| { - lsp_command::lsp_formatting_options( - language_settings( - buffer.language_at(position).map(|l| l.name()), - buffer.file(), - cx, - ) - .as_ref(), - ) - }); - self.request_lsp( - buffer.clone(), - LanguageServerToQuery::Primary, - OnTypeFormatting { - position, - trigger, - options, - push_to_history, - }, - cx, - ) - } - - pub async fn format_via_lsp( - this: &WeakModel, - buffer: &Model, - abs_path: &Path, - language_server: &Arc, - settings: &LanguageSettings, - cx: &mut AsyncAppContext, - ) -> Result, String)>> { - let uri = lsp::Url::from_file_path(abs_path) - .map_err(|_| anyhow!("failed to convert abs path to uri"))?; - let text_document = lsp::TextDocumentIdentifier::new(uri); - let capabilities = &language_server.capabilities(); - - let formatting_provider = capabilities.document_formatting_provider.as_ref(); - let range_formatting_provider = capabilities.document_range_formatting_provider.as_ref(); - - let lsp_edits = if matches!(formatting_provider, Some(p) if *p != OneOf::Left(false)) { - language_server - .request::(lsp::DocumentFormattingParams { - text_document, - options: lsp_command::lsp_formatting_options(settings), - work_done_progress_params: Default::default(), - }) - .await? - } else if matches!(range_formatting_provider, Some(p) if *p != OneOf::Left(false)) { - let buffer_start = lsp::Position::new(0, 0); - let buffer_end = buffer.update(cx, |b, _| point_to_lsp(b.max_point_utf16()))?; - language_server - .request::(lsp::DocumentRangeFormattingParams { - text_document: text_document.clone(), - range: lsp::Range::new(buffer_start, buffer_end), - options: lsp_command::lsp_formatting_options(settings), - work_done_progress_params: Default::default(), - }) - .await? - } else { - None - }; - - if let Some(lsp_edits) = lsp_edits { - this.update(cx, |this, cx| { - this.edits_from_lsp(buffer, lsp_edits, language_server.server_id(), None, cx) - })? - .await - } else { - Ok(Vec::with_capacity(0)) - } - } - pub async fn format_range_via_lsp( - this: &WeakModel, - buffer: &Model, - selections: &[Selection], - abs_path: &Path, - language_server: &Arc, - settings: &LanguageSettings, - cx: &mut AsyncAppContext, - ) -> Result, String)>> { - let capabilities = &language_server.capabilities(); - let range_formatting_provider = capabilities.document_range_formatting_provider.as_ref(); - if range_formatting_provider.map_or(false, |provider| provider == &OneOf::Left(false)) { - return Err(anyhow!( - "{} language server does not support range formatting", - language_server.name() - )); - } - - let uri = lsp::Url::from_file_path(abs_path) - .map_err(|_| anyhow!("failed to convert abs path to uri"))?; - let text_document = lsp::TextDocumentIdentifier::new(uri); - - let lsp_edits = { - let ranges = selections.into_iter().map(|s| { - let start = lsp::Position::new(s.start.row, s.start.column); - let end = lsp::Position::new(s.end.row, s.end.column); - lsp::Range::new(start, end) - }); - - let mut edits = None; - for range in ranges { - if let Some(mut edit) = language_server - .request::(lsp::DocumentRangeFormattingParams { - text_document: text_document.clone(), - range, - options: lsp_command::lsp_formatting_options(settings), - work_done_progress_params: Default::default(), - }) - .await? - { - edits.get_or_insert_with(Vec::new).append(&mut edit); - } - } - edits - }; - - if let Some(lsp_edits) = lsp_edits { - this.update(cx, |this, cx| { - this.edits_from_lsp(buffer, lsp_edits, language_server.server_id(), None, cx) - })? - .await + buffer.wait_for_edits(Some(position.timestamp)) + })? + .await?; + this.update(&mut cx, |this, cx| { + let position = position.to_point_utf16(buffer.read(cx)); + this.on_type_format(buffer, position, trigger, false, cx) + })? + .await + }) } else { - Ok(Vec::with_capacity(0)) + Task::ready(Err(anyhow!("No upstream client or local language server"))) } } + pub fn on_type_format( + &mut self, + buffer: Model, + position: T, + trigger: String, + push_to_history: bool, + cx: &mut ModelContext, + ) -> Task>> { + let position = position.to_point_utf16(buffer.read(cx)); + self.on_type_format_impl(buffer, position, trigger, push_to_history, cx) + } + + fn on_type_format_impl( + &mut self, + buffer: Model, + position: PointUtf16, + trigger: String, + push_to_history: bool, + cx: &mut ModelContext, + ) -> Task>> { + let options = buffer.update(cx, |buffer, cx| { + lsp_command::lsp_formatting_options( + language_settings( + buffer.language_at(position).map(|l| l.name()), + buffer.file(), + cx, + ) + .as_ref(), + ) + }); + self.request_lsp( + buffer.clone(), + LanguageServerToQuery::Primary, + OnTypeFormatting { + position, + trigger, + options, + push_to_history, + }, + cx, + ) + } pub fn code_actions( &mut self, buffer_handle: &Model, @@ -2495,7 +3106,7 @@ impl LspStore { if let Some(edits) = additional_text_edits { let edits = this .update(&mut cx, |this, cx| { - this.edits_from_lsp( + this.as_local_mut().unwrap().edits_from_lsp( &buffer_handle, edits, lang_server.server_id(), @@ -2983,6 +3594,8 @@ impl LspStore { let language_server = language_server.clone(); let buffer_snapshots = self + .as_local_mut() + .unwrap() .buffer_snapshots .get_mut(&buffer.remote_id()) .and_then(|m| m.get_mut(&language_server.server_id()))?; @@ -3220,7 +3833,13 @@ impl LspStore { } fn remove_worktree(&mut self, id_to_remove: WorktreeId, cx: &mut ModelContext) { - self.diagnostics.remove(&id_to_remove); + if let Some(local) = self.as_local_mut() { + local.diagnostics.remove(&id_to_remove); + local.prettier_store.update(cx, |prettier_store, cx| { + prettier_store.remove_worktree(id_to_remove, cx); + }) + } + self.diagnostic_summaries.remove(&id_to_remove); let mut servers_to_remove = HashMap::default(); @@ -3250,12 +3869,6 @@ impl LspStore { } cx.emit(LspStoreEvent::LanguageServerRemoved(server_id_to_remove)); } - - if let Some(local) = self.as_local() { - local.prettier_store.update(cx, |prettier_store, cx| { - prettier_store.remove_worktree(id_to_remove, cx); - }) - } } pub fn shared( @@ -3323,97 +3936,21 @@ impl LspStore { .insert((worktree_id, language_server_name), language_server_id); } - #[track_caller] pub(crate) fn register_buffer_with_language_servers( &mut self, buffer_handle: &Model, cx: &mut ModelContext, ) { - let available_language = self.detect_language_for_buffer(buffer_handle, cx); - - let buffer = buffer_handle.read(cx); - let buffer_id = buffer.remote_id(); - - if let Some(file) = File::from_dyn(buffer.file()) { - if !file.is_local() { - return; - } - - let abs_path = file.abs_path(cx); - let Some(uri) = lsp::Url::from_file_path(&abs_path).log_err() else { - return; - }; - let initial_snapshot = buffer.text_snapshot(); - let worktree_id = file.worktree_id(cx); - - if let Some(diagnostics) = self.diagnostics.get(&worktree_id) { - for (server_id, diagnostics) in - diagnostics.get(file.path()).cloned().unwrap_or_default() - { - self.update_buffer_diagnostics(buffer_handle, server_id, None, diagnostics, cx) - .log_err(); - } - } - - if let Some(language) = available_language { - for adapter in self.languages.lsp_adapters(&language.name()) { - let server = self - .language_server_ids - .get(&(worktree_id, adapter.name.clone())) - .and_then(|id| self.as_local()?.language_servers.get(id)) - .and_then(|server_state| { - if let LanguageServerState::Running { server, .. } = server_state { - Some(server.clone()) - } else { - None - } - }); - let server = match server { - Some(server) => server, - None => continue, - }; - - server - .notify::( - lsp::DidOpenTextDocumentParams { - text_document: lsp::TextDocumentItem::new( - uri.clone(), - adapter.language_id(&language.name()), - 0, - initial_snapshot.text(), - ), - }, - ) - .log_err(); - - buffer_handle.update(cx, |buffer, cx| { - buffer.set_completion_triggers( - server.server_id(), - server - .capabilities() - .completion_provider - .as_ref() - .and_then(|provider| { - provider - .trigger_characters - .as_ref() - .map(|characters| characters.iter().cloned().collect()) - }) - .unwrap_or_default(), - cx, - ); - }); - - let snapshot = LspBufferSnapshot { - version: 0, - snapshot: initial_snapshot.clone(), - }; - self.buffer_snapshots - .entry(buffer_id) - .or_default() - .insert(server.server_id(), vec![snapshot]); - } + self.detect_language_for_buffer(buffer_handle, cx); + match &mut self.mode { + LspStoreMode::Local(local) => { + local.register_buffer_with_language_servers( + buffer_handle, + &self.language_server_ids, + cx, + ); } + _ => {} } } @@ -3442,7 +3979,10 @@ impl LspStore { } } - self.buffer_snapshots.remove(&buffer.remote_id()); + self.as_local_mut() + .unwrap() + .buffer_snapshots + .remove(&buffer.remote_id()); let file_url = lsp::Url::from_file_path(old_path).unwrap(); for (_, language_server) in self.language_servers_for_buffer(buffer, cx) { language_server @@ -3477,7 +4017,13 @@ impl LspStore { }; if let Some(buffer) = self.buffer_store.read(cx).get_by_path(&project_path, cx) { - self.update_buffer_diagnostics(&buffer, server_id, version, diagnostics.clone(), cx)?; + self.as_local_mut().unwrap().update_buffer_diagnostics( + &buffer, + server_id, + version, + diagnostics.clone(), + cx, + )?; } let updated = worktree.update(cx, |worktree, cx| { @@ -3506,8 +4052,13 @@ impl LspStore { diagnostics: Vec>>, _: &mut ModelContext, ) -> Result { + let local = match &mut self.mode { + LspStoreMode::Local(local_lsp_store) => local_lsp_store, + _ => anyhow::bail!("update_worktree_diagnostics called on remote"), + }; + let summaries_for_tree = self.diagnostic_summaries.entry(worktree_id).or_default(); - let diagnostics_for_tree = self.diagnostics.entry(worktree_id).or_default(); + let diagnostics_for_tree = local.diagnostics.entry(worktree_id).or_default(); let summaries_by_server_id = summaries_for_tree.entry(worktree_path.clone()).or_default(); let old_summary = summaries_by_server_id @@ -3670,108 +4221,35 @@ impl LspStore { .await?; if worktree.update(&mut cx, |worktree, _| worktree.is_local())? { lsp_store - .update(&mut cx, |lsp_store, cx| { - lsp_store.register_language_server( - worktree.read(cx).id(), - language_server_name, - language_server_id, - ) - }) - .ok(); - } - let worktree_root = worktree.update(&mut cx, |worktree, _| worktree.abs_path())?; - let relative_path = if let Some(known_path) = known_relative_path { - known_path - } else { - abs_path.strip_prefix(worktree_root)?.into() - }; - (worktree, relative_path) - }; - let project_path = ProjectPath { - worktree_id: worktree.update(&mut cx, |worktree, _| worktree.id())?, - path: relative_path, - }; - lsp_store - .update(&mut cx, |lsp_store, cx| { - lsp_store.buffer_store().update(cx, |buffer_store, cx| { - buffer_store.open_buffer(project_path, cx) - }) - })? - .await - }) - } - - pub(crate) fn update_buffer_diagnostics( - &mut self, - buffer: &Model, - server_id: LanguageServerId, - version: Option, - mut diagnostics: Vec>>, - cx: &mut ModelContext, - ) -> Result<()> { - fn compare_diagnostics(a: &Diagnostic, b: &Diagnostic) -> Ordering { - Ordering::Equal - .then_with(|| b.is_primary.cmp(&a.is_primary)) - .then_with(|| a.is_disk_based.cmp(&b.is_disk_based)) - .then_with(|| a.severity.cmp(&b.severity)) - .then_with(|| a.message.cmp(&b.message)) - } - - let snapshot = self.buffer_snapshot_for_lsp_version(buffer, server_id, version, cx)?; - - diagnostics.sort_unstable_by(|a, b| { - Ordering::Equal - .then_with(|| a.range.start.cmp(&b.range.start)) - .then_with(|| b.range.end.cmp(&a.range.end)) - .then_with(|| compare_diagnostics(&a.diagnostic, &b.diagnostic)) - }); - - let mut sanitized_diagnostics = Vec::new(); - let edits_since_save = Patch::new( - snapshot - .edits_since::>(buffer.read(cx).saved_version()) - .collect(), - ); - for entry in diagnostics { - let start; - let end; - if entry.diagnostic.is_disk_based { - // Some diagnostics are based on files on disk instead of buffers' - // current contents. Adjust these diagnostics' ranges to reflect - // any unsaved edits. - start = edits_since_save.old_to_new(entry.range.start); - end = edits_since_save.old_to_new(entry.range.end); - } else { - start = entry.range.start; - end = entry.range.end; - } - - let mut range = snapshot.clip_point_utf16(start, Bias::Left) - ..snapshot.clip_point_utf16(end, Bias::Right); - - // Expand empty ranges by one codepoint - if range.start == range.end { - // This will be go to the next boundary when being clipped - range.end.column += 1; - range.end = snapshot.clip_point_utf16(Unclipped(range.end), Bias::Right); - if range.start == range.end && range.end.column > 0 { - range.start.column -= 1; - range.start = snapshot.clip_point_utf16(Unclipped(range.start), Bias::Left); - } - } - - sanitized_diagnostics.push(DiagnosticEntry { - range, - diagnostic: entry.diagnostic, - }); - } - drop(edits_since_save); - - let set = DiagnosticSet::new(sanitized_diagnostics, &snapshot); - buffer.update(cx, |buffer, cx| { - buffer.update_diagnostics(server_id, set, cx) - }); - Ok(()) + .update(&mut cx, |lsp_store, cx| { + lsp_store.register_language_server( + worktree.read(cx).id(), + language_server_name, + language_server_id, + ) + }) + .ok(); + } + let worktree_root = worktree.update(&mut cx, |worktree, _| worktree.abs_path())?; + let relative_path = if let Some(known_path) = known_relative_path { + known_path + } else { + abs_path.strip_prefix(worktree_root)?.into() + }; + (worktree, relative_path) + }; + let project_path = ProjectPath { + worktree_id: worktree.update(&mut cx, |worktree, _| worktree.id())?, + path: relative_path, + }; + lsp_store + .update(&mut cx, |lsp_store, cx| { + lsp_store.buffer_store().update(cx, |buffer_store, cx| { + buffer_store.open_buffer(project_path, cx) + }) + })? + .await + }) } fn request_multiple_lsp_locally( @@ -4408,7 +4886,7 @@ impl LspStore { .await .flatten()?; - Self::deserialize_workspace_edit( + LocalLspStore::deserialize_workspace_edit( this.upgrade()?, edit, false, @@ -4687,44 +5165,6 @@ impl LspStore { } } - async fn on_lsp_workspace_edit( - this: WeakModel, - params: lsp::ApplyWorkspaceEditParams, - server_id: LanguageServerId, - adapter: Arc, - mut cx: AsyncAppContext, - ) -> Result { - let this = this - .upgrade() - .ok_or_else(|| anyhow!("project project closed"))?; - let language_server = this - .update(&mut cx, |this, _| this.language_server_for_id(server_id))? - .ok_or_else(|| anyhow!("language server not found"))?; - let transaction = Self::deserialize_workspace_edit( - this.clone(), - params.edit, - true, - adapter.clone(), - language_server.clone(), - &mut cx, - ) - .await - .log_err(); - this.update(&mut cx, |this, _| { - if let Some(transaction) = transaction { - this.as_local_mut() - .unwrap() - .last_workspace_edits_by_language_server - .insert(server_id, transaction); - } - })?; - Ok(lsp::ApplyWorkspaceEditResponse { - applied: true, - failed_change: None, - failure_reason: None, - }) - } - fn on_lsp_progress( &mut self, progress: lsp::ProgressParams, @@ -4946,111 +5386,6 @@ impl LspStore { } } - #[allow(clippy::type_complexity)] - pub(crate) fn edits_from_lsp( - &mut self, - buffer: &Model, - lsp_edits: impl 'static + Send + IntoIterator, - server_id: LanguageServerId, - version: Option, - cx: &mut ModelContext, - ) -> Task, String)>>> { - let snapshot = self.buffer_snapshot_for_lsp_version(buffer, server_id, version, cx); - cx.background_executor().spawn(async move { - let snapshot = snapshot?; - let mut lsp_edits = lsp_edits - .into_iter() - .map(|edit| (range_from_lsp(edit.range), edit.new_text)) - .collect::>(); - lsp_edits.sort_by_key(|(range, _)| range.start); - - let mut lsp_edits = lsp_edits.into_iter().peekable(); - let mut edits = Vec::new(); - while let Some((range, mut new_text)) = lsp_edits.next() { - // Clip invalid ranges provided by the language server. - let mut range = snapshot.clip_point_utf16(range.start, Bias::Left) - ..snapshot.clip_point_utf16(range.end, Bias::Left); - - // Combine any LSP edits that are adjacent. - // - // Also, combine LSP edits that are separated from each other by only - // a newline. This is important because for some code actions, - // Rust-analyzer rewrites the entire buffer via a series of edits that - // are separated by unchanged newline characters. - // - // In order for the diffing logic below to work properly, any edits that - // cancel each other out must be combined into one. - while let Some((next_range, next_text)) = lsp_edits.peek() { - if next_range.start.0 > range.end { - if next_range.start.0.row > range.end.row + 1 - || next_range.start.0.column > 0 - || snapshot.clip_point_utf16( - Unclipped(PointUtf16::new(range.end.row, u32::MAX)), - Bias::Left, - ) > range.end - { - break; - } - new_text.push('\n'); - } - range.end = snapshot.clip_point_utf16(next_range.end, Bias::Left); - new_text.push_str(next_text); - lsp_edits.next(); - } - - // For multiline edits, perform a diff of the old and new text so that - // we can identify the changes more precisely, preserving the locations - // of any anchors positioned in the unchanged regions. - if range.end.row > range.start.row { - let mut offset = range.start.to_offset(&snapshot); - let old_text = snapshot.text_for_range(range).collect::(); - - let diff = TextDiff::from_lines(old_text.as_str(), &new_text); - let mut moved_since_edit = true; - for change in diff.iter_all_changes() { - let tag = change.tag(); - let value = change.value(); - match tag { - ChangeTag::Equal => { - offset += value.len(); - moved_since_edit = true; - } - ChangeTag::Delete => { - let start = snapshot.anchor_after(offset); - let end = snapshot.anchor_before(offset + value.len()); - if moved_since_edit { - edits.push((start..end, String::new())); - } else { - edits.last_mut().unwrap().0.end = end; - } - offset += value.len(); - moved_since_edit = false; - } - ChangeTag::Insert => { - if moved_since_edit { - let anchor = snapshot.anchor_after(offset); - edits.push((anchor..anchor, value.to_string())); - } else { - edits.last_mut().unwrap().1.push_str(value); - } - moved_since_edit = false; - } - } - } - } else if range.end == range.start { - let anchor = snapshot.anchor_after(range.start); - edits.push((anchor..anchor, new_text)); - } else { - let edit_start = snapshot.anchor_after(range.start); - let edit_end = snapshot.anchor_before(range.end); - edits.push((edit_start..edit_end, new_text)); - } - } - - Ok(edits) - }) - } - pub async fn handle_resolve_completion_documentation( this: Model, envelope: TypedEnvelope, @@ -5944,94 +6279,78 @@ impl LspStore { cx: &mut ModelContext, ) -> Task> { let key = (worktree_id, adapter_name); - if self.mode.is_local() { - if let Some(server_id) = self.language_server_ids.remove(&key) { - let name = key.1; - log::info!("stopping language server {name}"); - - // Remove other entries for this language server as well - let mut orphaned_worktrees = vec![worktree_id]; - let other_keys = self.language_server_ids.keys().cloned().collect::>(); - for other_key in other_keys { - if self.language_server_ids.get(&other_key) == Some(&server_id) { - self.language_server_ids.remove(&other_key); - orphaned_worktrees.push(other_key.0); - } - } + if !self.mode.is_local() { + return Task::ready(Vec::new()); + }; + let Some(server_id) = self.language_server_ids.remove(&key) else { + return Task::ready(Vec::new()); + }; + let name = key.1; + log::info!("stopping language server {name}"); + + // Remove other entries for this language server as well + let mut orphaned_worktrees = vec![worktree_id]; + let other_keys = self.language_server_ids.keys().cloned().collect::>(); + for other_key in other_keys { + if self.language_server_ids.get(&other_key) == Some(&server_id) { + self.language_server_ids.remove(&other_key); + orphaned_worktrees.push(other_key.0); + } + } - self.buffer_store.update(cx, |buffer_store, cx| { - for buffer in buffer_store.buffers() { - buffer.update(cx, |buffer, cx| { - buffer.update_diagnostics( - server_id, - DiagnosticSet::new([], buffer), - cx, - ); - buffer.set_completion_triggers(server_id, Default::default(), cx); - }); - } + self.buffer_store.update(cx, |buffer_store, cx| { + for buffer in buffer_store.buffers() { + buffer.update(cx, |buffer, cx| { + buffer.update_diagnostics(server_id, DiagnosticSet::new([], buffer), cx); + buffer.set_completion_triggers(server_id, Default::default(), cx); }); + } + }); - for (worktree_id, summaries) in self.diagnostic_summaries.iter_mut() { - summaries.retain(|path, summaries_by_server_id| { - if summaries_by_server_id.remove(&server_id).is_some() { - if let Some((client, project_id)) = self.downstream_client.clone() { - client - .send(proto::UpdateDiagnosticSummary { - project_id, - worktree_id: worktree_id.to_proto(), - summary: Some(proto::DiagnosticSummary { - path: path.to_string_lossy().to_string(), - language_server_id: server_id.0 as u64, - error_count: 0, - warning_count: 0, - }), - }) - .log_err(); - } - !summaries_by_server_id.is_empty() - } else { - true - } - }); + for (worktree_id, summaries) in self.diagnostic_summaries.iter_mut() { + summaries.retain(|path, summaries_by_server_id| { + if summaries_by_server_id.remove(&server_id).is_some() { + if let Some((client, project_id)) = self.downstream_client.clone() { + client + .send(proto::UpdateDiagnosticSummary { + project_id, + worktree_id: worktree_id.to_proto(), + summary: Some(proto::DiagnosticSummary { + path: path.to_string_lossy().to_string(), + language_server_id: server_id.0 as u64, + error_count: 0, + warning_count: 0, + }), + }) + .log_err(); + } + !summaries_by_server_id.is_empty() + } else { + true } + }); + } - for diagnostics in self.diagnostics.values_mut() { - diagnostics.retain(|_, diagnostics_by_server_id| { - if let Ok(ix) = - diagnostics_by_server_id.binary_search_by_key(&server_id, |e| e.0) - { - diagnostics_by_server_id.remove(ix); - !diagnostics_by_server_id.is_empty() - } else { - true - } - }); + self.language_server_statuses.remove(&server_id); + let local = self.as_local_mut().unwrap(); + for diagnostics in local.diagnostics.values_mut() { + diagnostics.retain(|_, diagnostics_by_server_id| { + if let Ok(ix) = diagnostics_by_server_id.binary_search_by_key(&server_id, |e| e.0) { + diagnostics_by_server_id.remove(ix); + !diagnostics_by_server_id.is_empty() + } else { + true } - - self.as_local_mut() - .unwrap() - .language_server_watched_paths - .remove(&server_id); - self.language_server_statuses.remove(&server_id); - cx.notify(); - - let server_state = self - .as_local_mut() - .unwrap() - .language_servers - .remove(&server_id); - cx.emit(LspStoreEvent::LanguageServerRemoved(server_id)); - cx.spawn(move |_, cx| async move { - Self::shutdown_language_server(server_state, name, cx).await; - orphaned_worktrees - }) - } else { - Task::ready(Vec::new()) - } - } else { - Task::ready(Vec::new()) + }); } + local.language_server_watched_paths.remove(&server_id); + let server_state = local.language_servers.remove(&server_id); + cx.notify(); + cx.emit(LspStoreEvent::LanguageServerRemoved(server_id)); + cx.spawn(move |_, cx| async move { + Self::shutdown_language_server(server_state, name, cx).await; + orphaned_worktrees + }) } pub fn restart_language_servers_for_buffers( @@ -6411,7 +6730,7 @@ impl LspStore { let adapter = adapter.clone(); let this = this.clone(); move |params, cx| { - Self::on_lsp_workspace_edit( + LocalLspStore::on_lsp_workspace_edit( this.clone(), params, server_id, @@ -6619,6 +6938,9 @@ impl LspStore { disk_based_sources: &[String], cx: &mut ModelContext, ) -> Result<()> { + if !self.mode.is_local() { + anyhow::bail!("called update_diagnostics on remote"); + } let abs_path = params .uri .to_file_path() @@ -6661,7 +6983,7 @@ impl LspStore { (diagnostic.severity, is_unnecessary), ); } else { - let group_id = post_inc(&mut self.next_diagnostic_group_id); + let group_id = post_inc(&mut self.as_local_mut().unwrap().next_diagnostic_group_id); let is_disk_based = source.map_or(false, |source| disk_based_sources.contains(source)); @@ -6815,7 +7137,7 @@ impl LspStore { } // Tell the language server about every open buffer in the worktree that matches the language. - self.buffer_store.update(cx, |buffer_store, cx| { + self.buffer_store.clone().update(cx, |buffer_store, cx| { for buffer_handle in buffer_store.buffers() { let buffer = buffer_handle.read(cx); let file = match File::from_dyn(buffer.file()) { @@ -6843,6 +7165,8 @@ impl LspStore { }; let versions = self + .as_local_mut() + .unwrap() .buffer_snapshots .entry(buffer.remote_id()) .or_default() @@ -6887,44 +7211,11 @@ impl LspStore { .unwrap_or_default(), cx, ) - }); - } - }); - - cx.notify(); - } - - fn buffer_snapshot_for_lsp_version( - &mut self, - buffer: &Model, - server_id: LanguageServerId, - version: Option, - cx: &AppContext, - ) -> Result { - const OLD_VERSIONS_TO_RETAIN: i32 = 10; - - if let Some(version) = version { - let buffer_id = buffer.read(cx).remote_id(); - let snapshots = self - .buffer_snapshots - .get_mut(&buffer_id) - .and_then(|m| m.get_mut(&server_id)) - .ok_or_else(|| { - anyhow!("no snapshots found for buffer {buffer_id} and server {server_id}") - })?; - - let found_snapshot = snapshots - .binary_search_by_key(&version, |e| e.version) - .map(|ix| snapshots[ix].snapshot.clone()) - .map_err(|_| { - anyhow!("snapshot not found for buffer {buffer_id} server {server_id} at version {version}") - })?; + }); + } + }); - snapshots.retain(|snapshot| snapshot.version + OLD_VERSIONS_TO_RETAIN >= version); - Ok(found_snapshot) - } else { - Ok((buffer.read(cx)).text_snapshot()) - } + cx.notify(); } pub fn language_servers_running_disk_based_diagnostics( @@ -7206,261 +7497,6 @@ impl LspStore { } } - pub async fn deserialize_text_edits( - this: Model, - buffer_to_edit: Model, - edits: Vec, - push_to_history: bool, - _: Arc, - language_server: Arc, - cx: &mut AsyncAppContext, - ) -> Result> { - let edits = this - .update(cx, |this, cx| { - this.edits_from_lsp( - &buffer_to_edit, - edits, - language_server.server_id(), - None, - cx, - ) - })? - .await?; - - let transaction = buffer_to_edit.update(cx, |buffer, cx| { - buffer.finalize_last_transaction(); - buffer.start_transaction(); - for (range, text) in edits { - buffer.edit([(range, text)], None, cx); - } - - if buffer.end_transaction(cx).is_some() { - let transaction = buffer.finalize_last_transaction().unwrap().clone(); - if !push_to_history { - buffer.forget_transaction(transaction.id); - } - Some(transaction) - } else { - None - } - })?; - - Ok(transaction) - } - - pub(crate) async fn deserialize_workspace_edit( - this: Model, - edit: lsp::WorkspaceEdit, - push_to_history: bool, - lsp_adapter: Arc, - language_server: Arc, - cx: &mut AsyncAppContext, - ) -> Result { - let fs = this.read_with(cx, |this, _| this.as_local().unwrap().fs.clone())?; - - 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 project_transaction = ProjectTransaction::default(); - for operation in operations { - match operation { - lsp::DocumentChangeOperation::Op(lsp::ResourceOp::Create(op)) => { - let abs_path = op - .uri - .to_file_path() - .map_err(|_| anyhow!("can't convert URI to path"))?; - - if let Some(parent_path) = abs_path.parent() { - fs.create_dir(parent_path).await?; - } - if abs_path.ends_with("/") { - fs.create_dir(&abs_path).await?; - } else { - fs.create_file( - &abs_path, - op.options - .map(|options| fs::CreateOptions { - overwrite: options.overwrite.unwrap_or(false), - ignore_if_exists: options.ignore_if_exists.unwrap_or(false), - }) - .unwrap_or_default(), - ) - .await?; - } - } - - lsp::DocumentChangeOperation::Op(lsp::ResourceOp::Rename(op)) => { - let source_abs_path = op - .old_uri - .to_file_path() - .map_err(|_| anyhow!("can't convert URI to path"))?; - let target_abs_path = op - .new_uri - .to_file_path() - .map_err(|_| anyhow!("can't convert URI to path"))?; - fs.rename( - &source_abs_path, - &target_abs_path, - op.options - .map(|options| fs::RenameOptions { - overwrite: options.overwrite.unwrap_or(false), - ignore_if_exists: options.ignore_if_exists.unwrap_or(false), - }) - .unwrap_or_default(), - ) - .await?; - } - - lsp::DocumentChangeOperation::Op(lsp::ResourceOp::Delete(op)) => { - let abs_path = op - .uri - .to_file_path() - .map_err(|_| anyhow!("can't convert URI to path"))?; - let options = op - .options - .map(|options| fs::RemoveOptions { - recursive: options.recursive.unwrap_or(false), - ignore_if_not_exists: options.ignore_if_not_exists.unwrap_or(false), - }) - .unwrap_or_default(); - if abs_path.ends_with("/") { - fs.remove_dir(&abs_path, options).await?; - } else { - fs.remove_file(&abs_path, options).await?; - } - } - - lsp::DocumentChangeOperation::Edit(op) => { - let buffer_to_edit = this - .update(cx, |this, cx| { - this.open_local_buffer_via_lsp( - op.text_document.uri.clone(), - language_server.server_id(), - lsp_adapter.name.clone(), - cx, - ) - })? - .await?; - - let edits = this - .update(cx, |this, cx| { - let path = buffer_to_edit.read(cx).project_path(cx); - let active_entry = this.active_entry; - let is_active_entry = path.clone().map_or(false, |project_path| { - this.worktree_store - .read(cx) - .entry_for_path(&project_path, cx) - .map_or(false, |entry| Some(entry.id) == active_entry) - }); - - let (mut edits, mut snippet_edits) = (vec![], vec![]); - for edit in op.edits { - match edit { - Edit::Plain(edit) => edits.push(edit), - Edit::Annotated(edit) => edits.push(edit.text_edit), - Edit::Snippet(edit) => { - let Ok(snippet) = Snippet::parse(&edit.snippet.value) - else { - continue; - }; - - if is_active_entry { - snippet_edits.push((edit.range, snippet)); - } else { - // Since this buffer is not focused, apply a normal edit. - edits.push(TextEdit { - range: edit.range, - new_text: snippet.text, - }); - } - } - } - } - if !snippet_edits.is_empty() { - let buffer_id = buffer_to_edit.read(cx).remote_id(); - let version = if let Some(buffer_version) = op.text_document.version - { - this.buffer_snapshot_for_lsp_version( - &buffer_to_edit, - language_server.server_id(), - Some(buffer_version), - cx, - ) - .ok() - .map(|snapshot| snapshot.version) - } else { - Some(buffer_to_edit.read(cx).saved_version().clone()) - }; - - let most_recent_edit = version.and_then(|version| { - version.iter().max_by_key(|timestamp| timestamp.value) - }); - // Check if the edit that triggered that edit has been made by this participant. - - if let Some(most_recent_edit) = most_recent_edit { - cx.emit(LspStoreEvent::SnippetEdit { - buffer_id, - edits: snippet_edits, - most_recent_edit, - }); - } - } - - this.edits_from_lsp( - &buffer_to_edit, - edits, - language_server.server_id(), - op.text_document.version, - cx, - ) - })? - .await?; - - let transaction = buffer_to_edit.update(cx, |buffer, cx| { - buffer.finalize_last_transaction(); - buffer.start_transaction(); - for (range, text) in edits { - buffer.edit([(range, text)], None, cx); - } - let transaction = if buffer.end_transaction(cx).is_some() { - let transaction = buffer.finalize_last_transaction().unwrap().clone(); - if !push_to_history { - buffer.forget_transaction(transaction.id); - } - Some(transaction) - } else { - None - }; - - transaction - })?; - if let Some(transaction) = transaction { - project_transaction.0.insert(buffer_to_edit, transaction); - } - } - } - } - - Ok(project_transaction) - } - fn serialize_symbol(symbol: &Symbol) -> proto::Symbol { proto::Symbol { language_server_name: symbol.language_server_name.0.to_string(), diff --git a/crates/project/src/project_tests.rs b/crates/project/src/project_tests.rs index ab6fb9d9f9d2fa80641d5e59f144aef99c115fd4..57903b6ac4901d0b1f332b42c716607f94bed3c7 100644 --- a/crates/project/src/project_tests.rs +++ b/crates/project/src/project_tests.rs @@ -2222,7 +2222,7 @@ async fn test_edits_from_lsp2_with_past_version(cx: &mut gpui::TestAppContext) { let edits = lsp_store .update(cx, |lsp_store, cx| { - lsp_store.edits_from_lsp( + lsp_store.as_local_mut().unwrap().edits_from_lsp( &buffer, vec![ // replace body of first function @@ -2317,7 +2317,7 @@ async fn test_edits_from_lsp2_with_edits_on_adjacent_lines(cx: &mut gpui::TestAp // Rust-analyzer does this when performing a merge-imports code action. let edits = lsp_store .update(cx, |lsp_store, cx| { - lsp_store.edits_from_lsp( + lsp_store.as_local_mut().unwrap().edits_from_lsp( &buffer, [ // Replace the first use statement without editing the semicolon. @@ -2426,7 +2426,7 @@ async fn test_invalid_edits_from_lsp2(cx: &mut gpui::TestAppContext) { // with ranges sometimes being inverted or pointing to invalid locations. let edits = lsp_store .update(cx, |lsp_store, cx| { - lsp_store.edits_from_lsp( + lsp_store.as_local_mut().unwrap().edits_from_lsp( &buffer, [ lsp::TextEdit {