From 317a1bb07bd00ec0e7c0c257010454dbf2522187 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Tue, 8 Mar 2022 19:00:54 +0100 Subject: [PATCH] Remove language servers from buffers Co-Authored-By: Nathan Sobo Co-Authored-By: Antonio Scandurra Co-Authored-By: Keith Simmons --- Cargo.lock | 1 + crates/editor/src/editor.rs | 7 +- crates/editor/src/multi_buffer.rs | 6 +- crates/gpui/src/app.rs | 65 +- crates/language/src/buffer.rs | 425 +------ crates/language/src/diagnostic_set.rs | 4 +- crates/language/src/tests.rs | 609 +--------- crates/project/Cargo.toml | 1 + crates/project/src/lsp_command.rs | 52 +- crates/project/src/project.rs | 1600 ++++++++++++++++++++++--- crates/project/src/worktree.rs | 1 + crates/search/src/buffer_search.rs | 2 +- crates/server/src/rpc.rs | 4 +- crates/text/src/anchor.rs | 42 +- 14 files changed, 1584 insertions(+), 1235 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 1e6f8192a8..7aafce3bbb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3591,6 +3591,7 @@ dependencies = [ "serde", "serde_json", "sha2 0.10.2", + "similar", "smol", "sum_tree", "tempdir", diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 857b33f625..649797be30 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -32,8 +32,8 @@ use items::{BufferItemHandle, MultiBufferItemHandle}; use itertools::Itertools as _; pub use language::{char_kind, CharKind}; use language::{ - AnchorRangeExt as _, BracketPair, Buffer, CodeAction, CodeLabel, Completion, Diagnostic, - DiagnosticSeverity, Language, Point, Selection, SelectionGoal, TransactionId, + BracketPair, Buffer, CodeAction, CodeLabel, Completion, Diagnostic, DiagnosticSeverity, + Language, OffsetRangeExt, Point, Selection, SelectionGoal, TransactionId, }; use multi_buffer::MultiBufferChunks; pub use multi_buffer::{ @@ -8235,9 +8235,6 @@ mod tests { .update(cx, |project, cx| project.open_buffer(project_path, cx)) .await .unwrap(); - buffer.update(cx, |buffer, cx| { - buffer.set_language_server(Some(language_server), cx); - }); let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx)); buffer.next_notification(&cx).await; diff --git a/crates/editor/src/multi_buffer.rs b/crates/editor/src/multi_buffer.rs index e05ddc5693..64683faa96 100644 --- a/crates/editor/src/multi_buffer.rs +++ b/crates/editor/src/multi_buffer.rs @@ -8,8 +8,8 @@ use gpui::{AppContext, Entity, ModelContext, ModelHandle, Task}; pub use language::Completion; use language::{ char_kind, Buffer, BufferChunks, BufferSnapshot, CharKind, Chunk, DiagnosticEntry, Event, File, - Language, Outline, OutlineItem, Selection, ToOffset as _, ToPoint as _, ToPointUtf16 as _, - TransactionId, + Language, OffsetRangeExt, Outline, OutlineItem, Selection, ToOffset as _, ToPoint as _, + ToPointUtf16 as _, TransactionId, }; use std::{ cell::{Ref, RefCell}, @@ -25,7 +25,7 @@ use text::{ locator::Locator, rope::TextDimension, subscription::{Subscription, Topic}, - AnchorRangeExt as _, Edit, Point, PointUtf16, TextSummary, + Edit, Point, PointUtf16, TextSummary, }; use theme::SyntaxTheme; diff --git a/crates/gpui/src/app.rs b/crates/gpui/src/app.rs index 85cd865f2b..6727fc5f08 100644 --- a/crates/gpui/src/app.rs +++ b/crates/gpui/src/app.rs @@ -742,7 +742,7 @@ type GlobalActionCallback = dyn FnMut(&dyn AnyAction, &mut MutableAppContext); type SubscriptionCallback = Box bool>; type DelegationCallback = Box, &mut MutableAppContext) -> bool>; type ObservationCallback = Box bool>; -type ReleaseObservationCallback = Box; +type ReleaseObservationCallback = Box; pub struct MutableAppContext { weak_self: Option>>, @@ -1186,14 +1186,20 @@ impl MutableAppContext { E: Entity, E::Event: 'static, H: Handle, - F: 'static + FnMut(&mut Self), + F: 'static + FnMut(&E, &mut Self), { let id = post_inc(&mut self.next_subscription_id); self.release_observations .lock() .entry(handle.id()) .or_default() - .insert(id, Box::new(move |cx| callback(cx))); + .insert( + id, + Box::new(move |released, cx| { + let released = released.downcast_ref().unwrap(); + callback(released, cx) + }), + ); Subscription::ReleaseObservation { id, entity_id: handle.id(), @@ -1552,9 +1558,8 @@ impl MutableAppContext { self.observations.lock().remove(&model_id); let mut model = self.cx.models.remove(&model_id).unwrap(); model.release(self); - self.pending_effects.push_back(Effect::Release { - entity_id: model_id, - }); + self.pending_effects + .push_back(Effect::ModelRelease { model_id, model }); } for (window_id, view_id) in dropped_views { @@ -1580,7 +1585,7 @@ impl MutableAppContext { } self.pending_effects - .push_back(Effect::Release { entity_id: view_id }); + .push_back(Effect::ViewRelease { view_id, view }); } for key in dropped_element_states { @@ -1607,7 +1612,12 @@ impl MutableAppContext { self.notify_view_observers(window_id, view_id) } Effect::Deferred(callback) => callback(self), - Effect::Release { entity_id } => self.notify_release_observers(entity_id), + Effect::ModelRelease { model_id, model } => { + self.notify_release_observers(model_id, model.as_any()) + } + Effect::ViewRelease { view_id, view } => { + self.notify_release_observers(view_id, view.as_any()) + } Effect::Focus { window_id, view_id } => { self.focus(window_id, view_id); } @@ -1781,11 +1791,11 @@ impl MutableAppContext { } } - fn notify_release_observers(&mut self, entity_id: usize) { + fn notify_release_observers(&mut self, entity_id: usize, entity: &dyn Any) { let callbacks = self.release_observations.lock().remove(&entity_id); if let Some(callbacks) = callbacks { for (_, mut callback) in callbacks { - callback(self); + callback(entity, self); } } } @@ -2112,8 +2122,13 @@ pub enum Effect { view_id: usize, }, Deferred(Box), - Release { - entity_id: usize, + ModelRelease { + model_id: usize, + model: Box, + }, + ViewRelease { + view_id: usize, + view: Box, }, Focus { window_id: usize, @@ -2142,9 +2157,13 @@ impl Debug for Effect { .field("view_id", view_id) .finish(), Effect::Deferred(_) => f.debug_struct("Effect::Deferred").finish(), - Effect::Release { entity_id } => f - .debug_struct("Effect::Release") - .field("entity_id", entity_id) + Effect::ModelRelease { model_id, .. } => f + .debug_struct("Effect::ModelRelease") + .field("model_id", model_id) + .finish(), + Effect::ViewRelease { view_id, .. } => f + .debug_struct("Effect::ViewRelease") + .field("view_id", view_id) .finish(), Effect::Focus { window_id, view_id } => f .debug_struct("Effect::Focus") @@ -2395,13 +2414,13 @@ impl<'a, T: Entity> ModelContext<'a, T> { ) -> Subscription where S: Entity, - F: 'static + FnMut(&mut T, &mut ModelContext), + F: 'static + FnMut(&mut T, &S, &mut ModelContext), { let observer = self.weak_handle(); - self.app.observe_release(handle, move |cx| { + self.app.observe_release(handle, move |released, cx| { if let Some(observer) = observer.upgrade(cx) { observer.update(cx, |observer, cx| { - callback(observer, cx); + callback(observer, released, cx); }); } }) @@ -2677,13 +2696,13 @@ impl<'a, T: View> ViewContext<'a, T> { where E: Entity, H: Handle, - F: 'static + FnMut(&mut T, &mut ViewContext), + F: 'static + FnMut(&mut T, &E, &mut ViewContext), { let observer = self.weak_handle(); - self.app.observe_release(handle, move |cx| { + self.app.observe_release(handle, move |released, cx| { if let Some(observer) = observer.upgrade(cx) { observer.update(cx, |observer, cx| { - callback(observer, cx); + callback(observer, released, cx); }); } }) @@ -4403,12 +4422,12 @@ mod tests { cx.observe_release(&model, { let model_release_observed = model_release_observed.clone(); - move |_| model_release_observed.set(true) + move |_, _| model_release_observed.set(true) }) .detach(); cx.observe_release(&view, { let view_release_observed = view_release_observed.clone(); - move |_| view_release_observed.set(true) + move |_, _| view_release_observed.set(true) }) .detach(); diff --git a/crates/language/src/buffer.rs b/crates/language/src/buffer.rs index 3a7838cd19..dfe2d5795d 100644 --- a/crates/language/src/buffer.rs +++ b/crates/language/src/buffer.rs @@ -7,16 +7,14 @@ pub use crate::{ use crate::{ diagnostic_set::{DiagnosticEntry, DiagnosticGroup}, outline::OutlineItem, - range_from_lsp, CodeLabel, Outline, ToLspPosition, + CodeLabel, Outline, }; use anyhow::{anyhow, Result}; use clock::ReplicaId; use futures::FutureExt as _; use gpui::{AppContext, Entity, ModelContext, MutableAppContext, Task}; use lazy_static::lazy_static; -use lsp::LanguageServer; use parking_lot::Mutex; -use postage::{prelude::Stream, sink::Sink, watch}; use similar::{ChangeTag, TextDiff}; use smol::future::yield_now; use std::{ @@ -26,7 +24,7 @@ use std::{ ffi::OsString, future::Future, iter::{Iterator, Peekable}, - ops::{Deref, DerefMut, Range, Sub}, + ops::{Deref, DerefMut, Range}, path::{Path, PathBuf}, str, sync::Arc, @@ -34,11 +32,11 @@ use std::{ vec, }; use sum_tree::TreeMap; -use text::{operation_queue::OperationQueue, rope::TextDimension}; -pub use text::{Buffer as TextBuffer, Operation as _, *}; +use text::operation_queue::OperationQueue; +pub use text::{Buffer as TextBuffer, BufferSnapshot as TextBufferSnapshot, Operation as _, *}; use theme::SyntaxTheme; use tree_sitter::{InputEdit, QueryCursor, Tree}; -use util::{post_inc, TryFutureExt as _}; +use util::TryFutureExt as _; #[cfg(any(test, feature = "test-support"))] pub use tree_sitter_rust; @@ -70,7 +68,6 @@ pub struct Buffer { diagnostics_update_count: usize, diagnostics_timestamp: clock::Lamport, file_update_count: usize, - language_server: Option, completion_triggers: Vec, deferred_ops: OperationQueue, } @@ -126,21 +123,6 @@ pub struct CodeAction { pub lsp_action: lsp::CodeAction, } -struct LanguageServerState { - server: Arc, - latest_snapshot: watch::Sender, - pending_snapshots: BTreeMap, - next_version: usize, - _maintain_server: Task>, -} - -#[derive(Clone)] -struct LanguageServerSnapshot { - buffer_snapshot: text::BufferSnapshot, - version: usize, - path: Arc, -} - #[derive(Clone, Debug, PartialEq, Eq)] pub enum Operation { Buffer(text::Operation), @@ -479,15 +461,6 @@ impl Buffer { self } - pub fn with_language_server( - mut self, - server: Arc, - cx: &mut ModelContext, - ) -> Self { - self.set_language_server(Some(server), cx); - self - } - fn build(buffer: TextBuffer, file: Option>) -> Self { let saved_mtime; if let Some(file) = file.as_ref() { @@ -514,7 +487,6 @@ impl Buffer { diagnostics_update_count: 0, diagnostics_timestamp: Default::default(), file_update_count: 0, - language_server: None, completion_triggers: Default::default(), deferred_ops: OperationQueue::new(), } @@ -536,6 +508,14 @@ impl Buffer { } } + pub fn as_text_snapshot(&self) -> &text::BufferSnapshot { + &self.text + } + + pub fn text_snapshot(&self) -> text::BufferSnapshot { + self.text.snapshot() + } + pub fn file(&self) -> Option<&dyn File> { self.file.as_deref() } @@ -561,123 +541,15 @@ impl Buffer { }) } + pub fn saved_version(&self) -> &clock::Global { + &self.saved_version + } + pub fn set_language(&mut self, language: Option>, cx: &mut ModelContext) { self.language = language; self.reparse(cx); } - pub fn set_language_server( - &mut self, - language_server: Option>, - cx: &mut ModelContext, - ) { - self.language_server = if let Some((server, file)) = - language_server.zip(self.file.as_ref().and_then(|f| f.as_local())) - { - let initial_snapshot = LanguageServerSnapshot { - buffer_snapshot: self.text.snapshot(), - version: 0, - path: file.abs_path(cx).into(), - }; - let (latest_snapshot_tx, mut latest_snapshot_rx) = - watch::channel_with::(initial_snapshot.clone()); - - Some(LanguageServerState { - latest_snapshot: latest_snapshot_tx, - pending_snapshots: BTreeMap::from_iter([(0, initial_snapshot)]), - next_version: 1, - server: server.clone(), - _maintain_server: cx.spawn_weak(|this, mut cx| async move { - let capabilities = server.capabilities().await.or_else(|| { - log::info!("language server exited"); - if let Some(this) = this.upgrade(&cx) { - this.update(&mut cx, |this, _| this.language_server = None); - } - None - })?; - - let triggers = capabilities - .completion_provider - .and_then(|c| c.trigger_characters) - .unwrap_or_default(); - this.upgrade(&cx)?.update(&mut cx, |this, cx| { - let lamport_timestamp = this.text.lamport_clock.tick(); - this.completion_triggers = triggers.clone(); - this.send_operation( - Operation::UpdateCompletionTriggers { - triggers, - lamport_timestamp, - }, - cx, - ); - cx.notify(); - }); - - let maintain_changes = cx.background().spawn(async move { - let initial_snapshot = - latest_snapshot_rx.recv().await.ok_or_else(|| { - anyhow!("buffer dropped before sending DidOpenTextDocument") - })?; - server - .notify::( - lsp::DidOpenTextDocumentParams { - text_document: lsp::TextDocumentItem::new( - lsp::Url::from_file_path(initial_snapshot.path).unwrap(), - Default::default(), - initial_snapshot.version as i32, - initial_snapshot.buffer_snapshot.text(), - ), - }, - ) - .await?; - - let mut prev_version = initial_snapshot.buffer_snapshot.version().clone(); - while let Some(snapshot) = latest_snapshot_rx.recv().await { - let uri = lsp::Url::from_file_path(&snapshot.path).unwrap(); - let buffer_snapshot = snapshot.buffer_snapshot.clone(); - let content_changes = buffer_snapshot - .edits_since::<(PointUtf16, usize)>(&prev_version) - .map(|edit| { - let edit_start = edit.new.start.0; - let edit_end = edit_start + (edit.old.end.0 - edit.old.start.0); - let new_text = buffer_snapshot - .text_for_range(edit.new.start.1..edit.new.end.1) - .collect(); - lsp::TextDocumentContentChangeEvent { - range: Some(lsp::Range::new( - edit_start.to_lsp_position(), - edit_end.to_lsp_position(), - )), - range_length: None, - text: new_text, - } - }) - .collect(); - let changes = lsp::DidChangeTextDocumentParams { - text_document: lsp::VersionedTextDocumentIdentifier::new( - uri, - snapshot.version as i32, - ), - content_changes, - }; - server - .notify::(changes) - .await?; - - prev_version = snapshot.buffer_snapshot.version().clone(); - } - - Ok::<_, anyhow::Error>(()) - }); - - maintain_changes.log_err().await - }), - }) - } else { - None - }; - } - pub fn did_save( &mut self, version: clock::Global, @@ -784,10 +656,6 @@ impl Buffer { self.language.as_ref() } - pub fn language_server(&self) -> Option<&Arc> { - self.language_server.as_ref().map(|state| &state.server) - } - pub fn parse_count(&self) -> usize { self.parse_count } @@ -899,100 +767,14 @@ impl Buffer { cx.notify(); } - pub fn update_diagnostics( - &mut self, - mut diagnostics: Vec>, - version: Option, - cx: &mut ModelContext, - ) -> Result<()> - where - T: Copy + Ord + TextDimension + Sub + Clip + ToPoint, - { - 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 version = version.map(|version| version as usize); - let content = - if let Some((version, language_server)) = version.zip(self.language_server.as_mut()) { - language_server.snapshot_for_version(version)? - } else { - self.deref() - }; - - 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 mut edits_since_save = content.edits_since::(&self.saved_version).peekable(); - let mut last_edit_old_end = T::default(); - let mut last_edit_new_end = T::default(); - 'outer: for entry in diagnostics { - let mut start = entry.range.start; - let mut end = entry.range.end; - - // Some diagnostics are based on files on disk instead of buffers' - // current contents. Adjust these diagnostics' ranges to reflect - // any unsaved edits. - if entry.diagnostic.is_disk_based { - while let Some(edit) = edits_since_save.peek() { - if edit.old.end <= start { - last_edit_old_end = edit.old.end; - last_edit_new_end = edit.new.end; - edits_since_save.next(); - } else if edit.old.start <= end && edit.old.end >= start { - continue 'outer; - } else { - break; - } - } - - let start_overshoot = start - last_edit_old_end; - start = last_edit_new_end; - start.add_assign(&start_overshoot); - - let end_overshoot = end - last_edit_old_end; - end = last_edit_new_end; - end.add_assign(&end_overshoot); - } - - let range = start.clip(Bias::Left, content)..end.clip(Bias::Right, content); - let mut range = range.start.to_point(content)..range.end.to_point(content); - // Expand empty ranges by one character - if range.start == range.end { - range.end.column += 1; - range.end = content.clip_point(range.end, Bias::Right); - if range.start == range.end && range.end.column > 0 { - range.start.column -= 1; - range.start = content.clip_point(range.start, Bias::Left); - } - } - - sanitized_diagnostics.push(DiagnosticEntry { - range, - diagnostic: entry.diagnostic, - }); - } - drop(edits_since_save); - - let set = DiagnosticSet::new(sanitized_diagnostics, content); + pub fn update_diagnostics(&mut self, diagnostics: DiagnosticSet, cx: &mut ModelContext) { let lamport_timestamp = self.text.lamport_clock.tick(); - self.apply_diagnostic_update(set.clone(), lamport_timestamp, cx); - let op = Operation::UpdateDiagnostics { - diagnostics: set.iter().cloned().collect(), + diagnostics: diagnostics.iter().cloned().collect(), lamport_timestamp, }; + self.apply_diagnostic_update(diagnostics, lamport_timestamp, cx); self.send_operation(op, cx); - Ok(()) } fn request_autoindent(&mut self, cx: &mut ModelContext) { @@ -1305,30 +1087,6 @@ impl Buffer { self.set_active_selections(Arc::from([]), cx); } - fn update_language_server(&mut self, cx: &AppContext) { - let language_server = if let Some(language_server) = self.language_server.as_mut() { - language_server - } else { - return; - }; - let file = if let Some(file) = self.file.as_ref().and_then(|f| f.as_local()) { - file - } else { - return; - }; - - let version = post_inc(&mut language_server.next_version); - let snapshot = LanguageServerSnapshot { - buffer_snapshot: self.text.snapshot(), - version, - path: Arc::from(file.abs_path(cx)), - }; - language_server - .pending_snapshots - .insert(version, snapshot.clone()); - let _ = language_server.latest_snapshot.blocking_send(snapshot); - } - pub fn set_text(&mut self, text: T, cx: &mut ModelContext) -> Option where T: Into, @@ -1455,115 +1213,6 @@ impl Buffer { Some(edit_id) } - pub fn edits_from_lsp( - &mut self, - lsp_edits: impl 'static + Send + IntoIterator, - version: Option, - cx: &mut ModelContext, - ) -> Task, String)>>> { - let snapshot = if let Some((version, state)) = version.zip(self.language_server.as_mut()) { - state - .snapshot_for_version(version as usize) - .map(Clone::clone) - } else { - Ok(TextBuffer::deref(self).clone()) - }; - - cx.background().spawn(async move { - let snapshot = snapshot?; - let mut lsp_edits = lsp_edits - .into_iter() - .map(|edit| (range_from_lsp(edit.range), edit.new_text)) - .peekable(); - - let mut edits = Vec::new(); - while let Some((mut range, mut new_text)) = lsp_edits.next() { - // 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 > range.end { - if next_range.start.row > range.end.row + 1 - || next_range.start.column > 0 - || snapshot.clip_point_utf16( - PointUtf16::new(range.end.row, u32::MAX), - Bias::Left, - ) > range.end - { - break; - } - new_text.push('\n'); - } - range.end = next_range.end; - new_text.push_str(&next_text); - lsp_edits.next(); - } - - if snapshot.clip_point_utf16(range.start, Bias::Left) != range.start - || snapshot.clip_point_utf16(range.end, Bias::Left) != range.end - { - return Err(anyhow!("invalid edits received from language server")); - } - - // 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.clone()..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.clone()..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) - }) - } - fn did_edit( &mut self, old_version: &clock::Global, @@ -1575,7 +1224,6 @@ impl Buffer { } self.reparse(cx); - self.update_language_server(cx); cx.emit(Event::Edited); if !was_dirty { @@ -1788,7 +1436,7 @@ impl Buffer { } pub fn completion_triggers(&self) -> &[String] { - &self.completion_triggers + todo!() } } @@ -1843,23 +1491,6 @@ impl Buffer { impl Entity for Buffer { type Event = Event; - - fn release(&mut self, cx: &mut gpui::MutableAppContext) { - if let Some(file) = self.file.as_ref() { - if let Some((lang_server, file)) = self.language_server.as_ref().zip(file.as_local()) { - let request = lang_server - .server - .notify::( - lsp::DidCloseTextDocumentParams { - text_document: lsp::TextDocumentIdentifier::new( - lsp::Url::from_file_path(file.abs_path(cx)).unwrap(), - ), - }, - ); - cx.foreground().spawn(request).detach_and_log_err(cx); - } - } - } } impl Deref for Buffer { @@ -2592,20 +2223,6 @@ impl operation_queue::Operation for Operation { } } -impl LanguageServerState { - fn snapshot_for_version(&mut self, version: usize) -> Result<&text::BufferSnapshot> { - const OLD_VERSIONS_TO_RETAIN: usize = 10; - - self.pending_snapshots - .retain(|&v, _| v + OLD_VERSIONS_TO_RETAIN >= version); - let snapshot = self - .pending_snapshots - .get(&version) - .ok_or_else(|| anyhow!("missing snapshot"))?; - Ok(&snapshot.buffer_snapshot) - } -} - impl Default for Diagnostic { fn default() -> Self { Self { diff --git a/crates/language/src/diagnostic_set.rs b/crates/language/src/diagnostic_set.rs index 9c2091739f..7dbc99d2d1 100644 --- a/crates/language/src/diagnostic_set.rs +++ b/crates/language/src/diagnostic_set.rs @@ -6,7 +6,7 @@ use std::{ ops::Range, }; use sum_tree::{self, Bias, SumTree}; -use text::{Anchor, FromAnchor, Point, ToOffset}; +use text::{Anchor, FromAnchor, PointUtf16, ToOffset}; #[derive(Clone, Debug)] pub struct DiagnosticSet { @@ -46,7 +46,7 @@ impl DiagnosticSet { pub fn new(iter: I, buffer: &text::BufferSnapshot) -> Self where - I: IntoIterator>, + I: IntoIterator>, { let mut entries = iter.into_iter().collect::>(); entries.sort_unstable_by_key(|entry| (entry.range.start, Reverse(entry.range.end))); diff --git a/crates/language/src/tests.rs b/crates/language/src/tests.rs index 5ccd400e0c..972821f77b 100644 --- a/crates/language/src/tests.rs +++ b/crates/language/src/tests.rs @@ -6,7 +6,6 @@ use rand::prelude::*; use std::{ cell::RefCell, env, - iter::FromIterator, ops::Range, rc::Rc, time::{Duration, Instant}, @@ -558,584 +557,6 @@ fn test_autoindent_adjusts_lines_when_only_text_changes(cx: &mut MutableAppConte }); } -#[gpui::test] -async fn test_diagnostics(cx: &mut gpui::TestAppContext) { - let (language_server, mut fake) = cx.update(lsp::LanguageServer::fake); - let mut rust_lang = rust_lang(); - rust_lang.config.language_server = Some(LanguageServerConfig { - disk_based_diagnostic_sources: HashSet::from_iter(["disk".to_string()]), - ..Default::default() - }); - - let text = " - fn a() { A } - fn b() { BB } - fn c() { CCC } - " - .unindent(); - - let buffer = cx.add_model(|cx| { - Buffer::from_file(0, text, Box::new(FakeFile::new("/some/path")), cx) - .with_language(Arc::new(rust_lang), cx) - .with_language_server(language_server, cx) - }); - - let open_notification = fake - .receive_notification::() - .await; - - // Edit the buffer, moving the content down - buffer.update(cx, |buffer, cx| buffer.edit([0..0], "\n\n", cx)); - let change_notification_1 = fake - .receive_notification::() - .await; - assert!(change_notification_1.text_document.version > open_notification.text_document.version); - - buffer.update(cx, |buffer, cx| { - // Receive diagnostics for an earlier version of the buffer. - buffer - .update_diagnostics( - vec![ - DiagnosticEntry { - range: PointUtf16::new(0, 9)..PointUtf16::new(0, 10), - diagnostic: Diagnostic { - severity: DiagnosticSeverity::ERROR, - message: "undefined variable 'A'".to_string(), - is_disk_based: true, - group_id: 0, - is_primary: true, - ..Default::default() - }, - }, - DiagnosticEntry { - range: PointUtf16::new(1, 9)..PointUtf16::new(1, 11), - diagnostic: Diagnostic { - severity: DiagnosticSeverity::ERROR, - message: "undefined variable 'BB'".to_string(), - is_disk_based: true, - group_id: 1, - is_primary: true, - ..Default::default() - }, - }, - DiagnosticEntry { - range: PointUtf16::new(2, 9)..PointUtf16::new(2, 12), - diagnostic: Diagnostic { - severity: DiagnosticSeverity::ERROR, - is_disk_based: true, - message: "undefined variable 'CCC'".to_string(), - group_id: 2, - is_primary: true, - ..Default::default() - }, - }, - ], - Some(open_notification.text_document.version), - cx, - ) - .unwrap(); - - // The diagnostics have moved down since they were created. - assert_eq!( - buffer - .snapshot() - .diagnostics_in_range::<_, Point>(Point::new(3, 0)..Point::new(5, 0)) - .collect::>(), - &[ - DiagnosticEntry { - range: Point::new(3, 9)..Point::new(3, 11), - diagnostic: Diagnostic { - severity: DiagnosticSeverity::ERROR, - message: "undefined variable 'BB'".to_string(), - is_disk_based: true, - group_id: 1, - is_primary: true, - ..Default::default() - }, - }, - DiagnosticEntry { - range: Point::new(4, 9)..Point::new(4, 12), - diagnostic: Diagnostic { - severity: DiagnosticSeverity::ERROR, - message: "undefined variable 'CCC'".to_string(), - is_disk_based: true, - group_id: 2, - is_primary: true, - ..Default::default() - } - } - ] - ); - assert_eq!( - chunks_with_diagnostics(buffer, 0..buffer.len()), - [ - ("\n\nfn a() { ".to_string(), None), - ("A".to_string(), Some(DiagnosticSeverity::ERROR)), - (" }\nfn b() { ".to_string(), None), - ("BB".to_string(), Some(DiagnosticSeverity::ERROR)), - (" }\nfn c() { ".to_string(), None), - ("CCC".to_string(), Some(DiagnosticSeverity::ERROR)), - (" }\n".to_string(), None), - ] - ); - assert_eq!( - chunks_with_diagnostics(buffer, Point::new(3, 10)..Point::new(4, 11)), - [ - ("B".to_string(), Some(DiagnosticSeverity::ERROR)), - (" }\nfn c() { ".to_string(), None), - ("CC".to_string(), Some(DiagnosticSeverity::ERROR)), - ] - ); - - // Ensure overlapping diagnostics are highlighted correctly. - buffer - .update_diagnostics( - vec![ - DiagnosticEntry { - range: PointUtf16::new(0, 9)..PointUtf16::new(0, 10), - diagnostic: Diagnostic { - severity: DiagnosticSeverity::ERROR, - message: "undefined variable 'A'".to_string(), - is_disk_based: true, - group_id: 0, - is_primary: true, - ..Default::default() - }, - }, - DiagnosticEntry { - range: PointUtf16::new(0, 9)..PointUtf16::new(0, 12), - diagnostic: Diagnostic { - severity: DiagnosticSeverity::WARNING, - message: "unreachable statement".to_string(), - group_id: 1, - is_primary: true, - ..Default::default() - }, - }, - ], - Some(open_notification.text_document.version), - cx, - ) - .unwrap(); - assert_eq!( - buffer - .snapshot() - .diagnostics_in_range::<_, Point>(Point::new(2, 0)..Point::new(3, 0)) - .collect::>(), - &[ - DiagnosticEntry { - range: Point::new(2, 9)..Point::new(2, 12), - diagnostic: Diagnostic { - severity: DiagnosticSeverity::WARNING, - message: "unreachable statement".to_string(), - group_id: 1, - is_primary: true, - ..Default::default() - } - }, - DiagnosticEntry { - range: Point::new(2, 9)..Point::new(2, 10), - diagnostic: Diagnostic { - severity: DiagnosticSeverity::ERROR, - message: "undefined variable 'A'".to_string(), - is_disk_based: true, - group_id: 0, - is_primary: true, - ..Default::default() - }, - } - ] - ); - assert_eq!( - chunks_with_diagnostics(buffer, Point::new(2, 0)..Point::new(3, 0)), - [ - ("fn a() { ".to_string(), None), - ("A".to_string(), Some(DiagnosticSeverity::ERROR)), - (" }".to_string(), Some(DiagnosticSeverity::WARNING)), - ("\n".to_string(), None), - ] - ); - assert_eq!( - chunks_with_diagnostics(buffer, Point::new(2, 10)..Point::new(3, 0)), - [ - (" }".to_string(), Some(DiagnosticSeverity::WARNING)), - ("\n".to_string(), None), - ] - ); - }); - - // Keep editing the buffer and ensure disk-based diagnostics get translated according to the - // changes since the last save. - buffer.update(cx, |buffer, cx| { - buffer.edit(Some(Point::new(2, 0)..Point::new(2, 0)), " ", cx); - buffer.edit(Some(Point::new(2, 8)..Point::new(2, 10)), "(x: usize)", cx); - }); - let change_notification_2 = fake - .receive_notification::() - .await; - assert!( - change_notification_2.text_document.version > change_notification_1.text_document.version - ); - - buffer.update(cx, |buffer, cx| { - buffer - .update_diagnostics( - vec![ - DiagnosticEntry { - range: PointUtf16::new(1, 9)..PointUtf16::new(1, 11), - diagnostic: Diagnostic { - severity: DiagnosticSeverity::ERROR, - message: "undefined variable 'BB'".to_string(), - is_disk_based: true, - group_id: 1, - is_primary: true, - ..Default::default() - }, - }, - DiagnosticEntry { - range: PointUtf16::new(0, 9)..PointUtf16::new(0, 10), - diagnostic: Diagnostic { - severity: DiagnosticSeverity::ERROR, - message: "undefined variable 'A'".to_string(), - is_disk_based: true, - group_id: 0, - is_primary: true, - ..Default::default() - }, - }, - ], - Some(change_notification_2.text_document.version), - cx, - ) - .unwrap(); - assert_eq!( - buffer - .snapshot() - .diagnostics_in_range::<_, Point>(0..buffer.len()) - .collect::>(), - &[ - DiagnosticEntry { - range: Point::new(2, 21)..Point::new(2, 22), - diagnostic: Diagnostic { - severity: DiagnosticSeverity::ERROR, - message: "undefined variable 'A'".to_string(), - is_disk_based: true, - group_id: 0, - is_primary: true, - ..Default::default() - } - }, - DiagnosticEntry { - range: Point::new(3, 9)..Point::new(3, 11), - diagnostic: Diagnostic { - severity: DiagnosticSeverity::ERROR, - message: "undefined variable 'BB'".to_string(), - is_disk_based: true, - group_id: 1, - is_primary: true, - ..Default::default() - }, - } - ] - ); - }); -} - -#[gpui::test] -async fn test_language_server_has_exited(cx: &mut gpui::TestAppContext) { - let (language_server, fake) = cx.update(lsp::LanguageServer::fake); - - // Simulate the language server failing to start up. - drop(fake); - - let buffer = cx.add_model(|cx| { - Buffer::from_file(0, "", Box::new(FakeFile::new("/some/path")), cx) - .with_language(Arc::new(rust_lang()), cx) - .with_language_server(language_server, cx) - }); - - // Run the buffer's task that retrieves the server's capabilities. - cx.foreground().advance_clock(Duration::from_millis(1)); - - buffer.read_with(cx, |buffer, _| { - assert!(buffer.language_server().is_none()); - }); -} - -#[gpui::test] -async fn test_edits_from_lsp_with_past_version(cx: &mut gpui::TestAppContext) { - let (language_server, mut fake) = cx.update(lsp::LanguageServer::fake); - - let text = " - fn a() { - f1(); - } - fn b() { - f2(); - } - fn c() { - f3(); - } - " - .unindent(); - - let buffer = cx.add_model(|cx| { - Buffer::from_file(0, text, Box::new(FakeFile::new("/some/path")), cx) - .with_language(Arc::new(rust_lang()), cx) - .with_language_server(language_server, cx) - }); - - let lsp_document_version = fake - .receive_notification::() - .await - .text_document - .version; - - // Simulate editing the buffer after the language server computes some edits. - buffer.update(cx, |buffer, cx| { - buffer.edit( - [Point::new(0, 0)..Point::new(0, 0)], - "// above first function\n", - cx, - ); - buffer.edit( - [Point::new(2, 0)..Point::new(2, 0)], - " // inside first function\n", - cx, - ); - buffer.edit( - [Point::new(6, 4)..Point::new(6, 4)], - "// inside second function ", - cx, - ); - - assert_eq!( - buffer.text(), - " - // above first function - fn a() { - // inside first function - f1(); - } - fn b() { - // inside second function f2(); - } - fn c() { - f3(); - } - " - .unindent() - ); - }); - - let edits = buffer - .update(cx, |buffer, cx| { - buffer.edits_from_lsp( - vec![ - // replace body of first function - lsp::TextEdit { - range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(3, 0)), - new_text: " - fn a() { - f10(); - } - " - .unindent(), - }, - // edit inside second function - lsp::TextEdit { - range: lsp::Range::new(lsp::Position::new(4, 6), lsp::Position::new(4, 6)), - new_text: "00".into(), - }, - // edit inside third function via two distinct edits - lsp::TextEdit { - range: lsp::Range::new(lsp::Position::new(7, 5), lsp::Position::new(7, 5)), - new_text: "4000".into(), - }, - lsp::TextEdit { - range: lsp::Range::new(lsp::Position::new(7, 5), lsp::Position::new(7, 6)), - new_text: "".into(), - }, - ], - Some(lsp_document_version), - cx, - ) - }) - .await - .unwrap(); - - buffer.update(cx, |buffer, cx| { - for (range, new_text) in edits { - buffer.edit([range], new_text, cx); - } - assert_eq!( - buffer.text(), - " - // above first function - fn a() { - // inside first function - f10(); - } - fn b() { - // inside second function f200(); - } - fn c() { - f4000(); - } - " - .unindent() - ); - }); -} - -#[gpui::test] -async fn test_edits_from_lsp_with_edits_on_adjacent_lines(cx: &mut gpui::TestAppContext) { - let text = " - use a::b; - use a::c; - - fn f() { - b(); - c(); - } - " - .unindent(); - - let buffer = cx.add_model(|cx| Buffer::new(0, text, cx)); - - // Simulate the language server sending us a small edit in the form of a very large diff. - // Rust-analyzer does this when performing a merge-imports code action. - let edits = buffer - .update(cx, |buffer, cx| { - buffer.edits_from_lsp( - [ - // Replace the first use statement without editing the semicolon. - lsp::TextEdit { - range: lsp::Range::new(lsp::Position::new(0, 4), lsp::Position::new(0, 8)), - new_text: "a::{b, c}".into(), - }, - // Reinsert the remainder of the file between the semicolon and the final - // newline of the file. - lsp::TextEdit { - range: lsp::Range::new(lsp::Position::new(0, 9), lsp::Position::new(0, 9)), - new_text: "\n\n".into(), - }, - lsp::TextEdit { - range: lsp::Range::new(lsp::Position::new(0, 9), lsp::Position::new(0, 9)), - new_text: " - fn f() { - b(); - c(); - }" - .unindent(), - }, - // Delete everything after the first newline of the file. - lsp::TextEdit { - range: lsp::Range::new(lsp::Position::new(1, 0), lsp::Position::new(7, 0)), - new_text: "".into(), - }, - ], - None, - cx, - ) - }) - .await - .unwrap(); - - buffer.update(cx, |buffer, cx| { - let edits = edits - .into_iter() - .map(|(range, text)| { - ( - range.start.to_point(&buffer)..range.end.to_point(&buffer), - text, - ) - }) - .collect::>(); - - assert_eq!( - edits, - [ - (Point::new(0, 4)..Point::new(0, 8), "a::{b, c}".into()), - (Point::new(1, 0)..Point::new(2, 0), "".into()) - ] - ); - - for (range, new_text) in edits { - buffer.edit([range], new_text, cx); - } - assert_eq!( - buffer.text(), - " - use a::{b, c}; - - fn f() { - b(); - c(); - } - " - .unindent() - ); - }); -} - -#[gpui::test] -async fn test_empty_diagnostic_ranges(cx: &mut gpui::TestAppContext) { - cx.add_model(|cx| { - let text = concat!( - "let one = ;\n", // - "let two = \n", - "let three = 3;\n", - ); - - let mut buffer = Buffer::new(0, text, cx); - buffer.set_language(Some(Arc::new(rust_lang())), cx); - buffer - .update_diagnostics( - vec![ - DiagnosticEntry { - range: PointUtf16::new(0, 10)..PointUtf16::new(0, 10), - diagnostic: Diagnostic { - severity: DiagnosticSeverity::ERROR, - message: "syntax error 1".to_string(), - ..Default::default() - }, - }, - DiagnosticEntry { - range: PointUtf16::new(1, 10)..PointUtf16::new(1, 10), - diagnostic: Diagnostic { - severity: DiagnosticSeverity::ERROR, - message: "syntax error 2".to_string(), - ..Default::default() - }, - }, - ], - None, - cx, - ) - .unwrap(); - - // An empty range is extended forward to include the following character. - // At the end of a line, an empty range is extended backward to include - // the preceding character. - let chunks = chunks_with_diagnostics(&buffer, 0..buffer.len()); - assert_eq!( - chunks - .iter() - .map(|(s, d)| (s.as_str(), *d)) - .collect::>(), - &[ - ("let one = ", None), - (";", Some(DiagnosticSeverity::ERROR)), - ("\nlet two =", None), - (" ", Some(DiagnosticSeverity::ERROR)), - ("\nlet three = 3;\n", None) - ] - ); - buffer - }); -} - #[gpui::test] fn test_serialization(cx: &mut gpui::MutableAppContext) { let mut now = Instant::now(); @@ -1253,9 +674,10 @@ fn test_random_collaboration(cx: &mut MutableAppContext, mut rng: StdRng) { 40..=49 if mutation_count != 0 && replica_id == 0 => { let entry_count = rng.gen_range(1..=5); buffer.update(cx, |buffer, cx| { - let diagnostics = (0..entry_count) - .map(|_| { + let diagnostics = DiagnosticSet::new( + (0..entry_count).map(|_| { let range = buffer.random_byte_range(0, &mut rng); + let range = range.to_point_utf16(buffer); DiagnosticEntry { range, diagnostic: Diagnostic { @@ -1263,10 +685,11 @@ fn test_random_collaboration(cx: &mut MutableAppContext, mut rng: StdRng) { ..Default::default() }, } - }) - .collect(); + }), + buffer, + ); log::info!("peer {} setting diagnostics: {:?}", replica_id, diagnostics); - buffer.update_diagnostics(diagnostics, None, cx).unwrap(); + buffer.update_diagnostics(diagnostics, cx); }); mutation_count -= 1; } @@ -1370,24 +793,6 @@ fn test_random_collaboration(cx: &mut MutableAppContext, mut rng: StdRng) { } } -fn chunks_with_diagnostics( - buffer: &Buffer, - range: Range, -) -> Vec<(String, Option)> { - let mut chunks: Vec<(String, Option)> = Vec::new(); - for chunk in buffer.snapshot().chunks(range, true) { - if chunks - .last() - .map_or(false, |prev_chunk| prev_chunk.1 == chunk.diagnostic) - { - chunks.last_mut().unwrap().0.push_str(chunk.text); - } else { - chunks.push((chunk.text.to_string(), chunk.diagnostic)); - } - } - chunks -} - #[test] fn test_contiguous_ranges() { assert_eq!( diff --git a/crates/project/Cargo.toml b/crates/project/Cargo.toml index e4ff8376ec..89f6999efa 100644 --- a/crates/project/Cargo.toml +++ b/crates/project/Cargo.toml @@ -42,6 +42,7 @@ regex = "1.5" serde = { version = "1", features = ["derive"] } serde_json = { version = "1.0.64", features = ["preserve_order"] } sha2 = "0.10" +similar = "1.3" smol = "1.2.5" toml = "0.5" diff --git a/crates/project/src/lsp_command.rs b/crates/project/src/lsp_command.rs index 4b2a7d89c1..abd0edd363 100644 --- a/crates/project/src/lsp_command.rs +++ b/crates/project/src/lsp_command.rs @@ -223,21 +223,19 @@ impl LspCommand for PerformRename { mut cx: AsyncAppContext, ) -> Result { if let Some(edit) = message { - let (language_name, language_server) = buffer.read_with(&cx, |buffer, _| { - let language = buffer - .language() - .ok_or_else(|| anyhow!("buffer's language was removed"))?; - let language_server = buffer - .language_server() - .cloned() - .ok_or_else(|| anyhow!("buffer's language server was removed"))?; - Ok::<_, anyhow::Error>((language.name().to_string(), language_server)) - })?; + let language_server = project + .read_with(&cx, |project, cx| { + project.language_server_for_buffer(&buffer, cx).cloned() + }) + .ok_or_else(|| anyhow!("no language server found for buffer"))?; + let language = buffer + .read_with(&cx, |buffer, _| buffer.language().cloned()) + .ok_or_else(|| anyhow!("no language for buffer"))?; Project::deserialize_workspace_edit( project, edit, self.push_to_history, - language_name, + language.name(), language_server, &mut cx, ) @@ -343,14 +341,14 @@ impl LspCommand for GetDefinition { mut cx: AsyncAppContext, ) -> Result> { let mut definitions = Vec::new(); - let (language, language_server) = buffer - .read_with(&cx, |buffer, _| { - buffer - .language() - .cloned() - .zip(buffer.language_server().cloned()) + let language_server = project + .read_with(&cx, |project, cx| { + project.language_server_for_buffer(&buffer, cx).cloned() }) - .ok_or_else(|| anyhow!("buffer no longer has language server"))?; + .ok_or_else(|| anyhow!("no language server found for buffer"))?; + let language = buffer + .read_with(&cx, |buffer, _| buffer.language().cloned()) + .ok_or_else(|| anyhow!("no language for buffer"))?; if let Some(message) = message { let mut unresolved_locations = Vec::new(); @@ -375,7 +373,7 @@ impl LspCommand for GetDefinition { .update(&mut cx, |this, cx| { this.open_local_buffer_via_lsp( target_uri, - language.name().to_string(), + language.name(), language_server.clone(), cx, ) @@ -519,14 +517,14 @@ impl LspCommand for GetReferences { mut cx: AsyncAppContext, ) -> Result> { let mut references = Vec::new(); - let (language, language_server) = buffer - .read_with(&cx, |buffer, _| { - buffer - .language() - .cloned() - .zip(buffer.language_server().cloned()) + let language_server = project + .read_with(&cx, |project, cx| { + project.language_server_for_buffer(&buffer, cx).cloned() }) - .ok_or_else(|| anyhow!("buffer no longer has language server"))?; + .ok_or_else(|| anyhow!("no language server found for buffer"))?; + let language = buffer + .read_with(&cx, |buffer, _| buffer.language().cloned()) + .ok_or_else(|| anyhow!("no language for buffer"))?; if let Some(locations) = locations { for lsp_location in locations { @@ -534,7 +532,7 @@ impl LspCommand for GetReferences { .update(&mut cx, |this, cx| { this.open_local_buffer_via_lsp( lsp_location.uri, - language.name().to_string(), + language.name(), language_server.clone(), cx, ) diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 4c44258781..855a45b5dc 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -16,9 +16,10 @@ use gpui::{ }; use language::{ proto::{deserialize_anchor, deserialize_version, serialize_anchor, serialize_version}, - range_from_lsp, Anchor, AnchorRangeExt, Bias, Buffer, CodeAction, CodeLabel, Completion, - Diagnostic, DiagnosticEntry, Event as BufferEvent, File as _, Language, LanguageRegistry, - Operation, PointUtf16, ToLspPosition, ToOffset, ToPointUtf16, Transaction, + range_from_lsp, Anchor, Bias, Buffer, CodeAction, CodeLabel, Completion, Diagnostic, + DiagnosticEntry, DiagnosticSet, Event as BufferEvent, File as _, Language, LanguageRegistry, + LocalFile, OffsetRangeExt, Operation, PointUtf16, TextBufferSnapshot, ToLspPosition, ToOffset, + ToPointUtf16, Transaction, }; use lsp::{DiagnosticSeverity, DocumentHighlightKind, LanguageServer}; use lsp_command::*; @@ -26,10 +27,11 @@ use postage::watch; use rand::prelude::*; use search::SearchQuery; use sha2::{Digest, Sha256}; +use similar::{ChangeTag, TextDiff}; use smol::block_on; use std::{ cell::RefCell, - cmp, + cmp::{self, Ordering}, convert::TryInto, hash::Hash, mem, @@ -48,9 +50,8 @@ pub struct Project { worktrees: Vec, active_entry: Option, languages: Arc, - language_servers: HashMap<(WorktreeId, String), Arc>, - started_language_servers: - HashMap<(WorktreeId, String), Shared>>>>, + language_servers: HashMap<(WorktreeId, Arc), Arc>, + started_language_servers: HashMap<(WorktreeId, Arc), Task>>>, client: Arc, user_store: ModelHandle, fs: Arc, @@ -67,6 +68,7 @@ pub struct Project { loading_local_worktrees: HashMap, Shared, Arc>>>>, opened_buffers: HashMap, + buffer_snapshots: HashMap>, nonce: u128, } @@ -285,6 +287,7 @@ impl Project { shared_buffers: Default::default(), loading_buffers: Default::default(), loading_local_worktrees: Default::default(), + buffer_snapshots: Default::default(), client_state: ProjectClientState::Local { is_shared: false, remote_id_tx, @@ -371,6 +374,7 @@ impl Project { language_servers: Default::default(), started_language_servers: Default::default(), opened_buffers: Default::default(), + buffer_snapshots: Default::default(), nonce: StdRng::from_entropy().gen(), }; for worktree in worktrees { @@ -722,7 +726,7 @@ impl Project { let buffer = cx.add_model(|cx| { Buffer::new(self.replica_id(), "", cx).with_language(language::PLAIN_TEXT.clone(), cx) }); - self.register_buffer(&buffer, None, cx)?; + self.register_buffer(&buffer, cx)?; Ok(buffer) } @@ -797,15 +801,9 @@ impl Project { let worktree = worktree.as_local_mut().unwrap(); worktree.load_buffer(path, cx) }); - let worktree = worktree.downgrade(); cx.spawn(|this, mut cx| async move { let buffer = load_buffer.await?; - let worktree = worktree - .upgrade(&cx) - .ok_or_else(|| anyhow!("worktree was removed"))?; - this.update(&mut cx, |this, cx| { - this.register_buffer(&buffer, Some(&worktree), cx) - })?; + this.update(&mut cx, |this, cx| this.register_buffer(&buffer, cx))?; Ok(buffer) }) } @@ -838,7 +836,7 @@ impl Project { fn open_local_buffer_via_lsp( &mut self, abs_path: lsp::Url, - lang_name: String, + lang_name: Arc, lang_server: Arc, cx: &mut ModelContext, ) -> Task>> { @@ -890,7 +888,8 @@ impl Project { }) .await?; this.update(&mut cx, |this, cx| { - this.assign_language_to_buffer(&buffer, Some(&worktree), cx); + this.assign_language_to_buffer(&buffer, cx); + this.register_buffer_with_language_servers(&buffer, cx); }); Ok(()) }) @@ -916,7 +915,6 @@ impl Project { fn register_buffer( &mut self, buffer: &ModelHandle, - worktree: Option<&ModelHandle>, cx: &mut ModelContext, ) -> Result<()> { let remote_id = buffer.read(cx).remote_id(); @@ -944,109 +942,215 @@ impl Project { remote_id ))?, } - cx.become_delegate(buffer, Self::on_buffer_event).detach(); - self.assign_language_to_buffer(buffer, worktree, cx); + cx.become_delegate(buffer, |this, buffer, event, cx| { + this.on_buffer_event(buffer, event, cx); + }) + .detach(); + + self.assign_language_to_buffer(buffer, cx); + self.register_buffer_with_language_servers(buffer, cx); Ok(()) } + fn register_buffer_with_language_servers( + &mut self, + buffer_handle: &ModelHandle, + cx: &mut ModelContext, + ) { + let buffer = buffer_handle.read(cx); + if let Some(file) = File::from_dyn(buffer.file()) { + let worktree_id = file.worktree_id(cx); + if file.is_local() { + let uri = lsp::Url::from_file_path(file.abs_path(cx)).unwrap(); + let initial_snapshot = buffer.as_text_snapshot(); + self.buffer_snapshots + .insert(buffer.remote_id(), vec![(0, initial_snapshot.clone())]); + + let mut notifications = Vec::new(); + let did_open_text_document = lsp::DidOpenTextDocumentParams { + text_document: lsp::TextDocumentItem::new( + uri, + Default::default(), + 0, + initial_snapshot.text(), + ), + }; + + for lang_server in self.language_servers_for_worktree(worktree_id) { + notifications.push( + lang_server.notify::( + did_open_text_document.clone(), + ), + ); + } + + if let Some(local_worktree) = file.worktree.read(cx).as_local() { + if let Some(diagnostics) = local_worktree.diagnostics_for_path(file.path()) { + self.update_buffer_diagnostics(&buffer_handle, diagnostics, None, cx) + .log_err(); + } + } + + cx.observe_release(buffer_handle, |this, buffer, cx| { + if let Some(file) = File::from_dyn(buffer.file()) { + let worktree_id = file.worktree_id(cx); + if file.is_local() { + let uri = lsp::Url::from_file_path(file.abs_path(cx)).unwrap(); + let mut notifications = Vec::new(); + for lang_server in this.language_servers_for_worktree(worktree_id) { + notifications.push( + lang_server.notify::( + lsp::DidCloseTextDocumentParams { + text_document: lsp::TextDocumentIdentifier::new( + uri.clone(), + ), + }, + ), + ); + } + cx.background() + .spawn(futures::future::try_join_all(notifications)) + .detach_and_log_err(cx); + } + } + }) + .detach(); + + cx.background() + .spawn(futures::future::try_join_all(notifications)) + .detach_and_log_err(cx); + } + } + } + fn on_buffer_event( &mut self, buffer: ModelHandle, event: BufferEvent, cx: &mut ModelContext, - ) { + ) -> Option<()> { match event { BufferEvent::Operation(operation) => { - if let Some(project_id) = self.remote_id() { - let request = self.client.request(proto::UpdateBuffer { - project_id, - buffer_id: buffer.read(cx).remote_id(), - operations: vec![language::proto::serialize_operation(&operation)], - }); - cx.background().spawn(request).detach_and_log_err(cx); + let project_id = self.remote_id()?; + let request = self.client.request(proto::UpdateBuffer { + project_id, + buffer_id: buffer.read(cx).remote_id(), + operations: vec![language::proto::serialize_operation(&operation)], + }); + cx.background().spawn(request).detach_and_log_err(cx); + } + BufferEvent::Edited => { + let buffer = buffer.read(cx); + let file = File::from_dyn(buffer.file())?; + let worktree_id = file.worktree_id(cx); + let abs_path = file.as_local()?.abs_path(cx); + let uri = lsp::Url::from_file_path(abs_path).unwrap(); + let buffer_snapshots = self.buffer_snapshots.entry(buffer.remote_id()).or_default(); + let (version, prev_snapshot) = buffer_snapshots.last()?; + let next_snapshot = buffer.text_snapshot(); + let next_version = version + 1; + + let content_changes = buffer + .edits_since::<(PointUtf16, usize)>(prev_snapshot.version()) + .map(|edit| { + let edit_start = edit.new.start.0; + let edit_end = edit_start + (edit.old.end.0 - edit.old.start.0); + let new_text = next_snapshot + .text_for_range(edit.new.start.1..edit.new.end.1) + .collect(); + lsp::TextDocumentContentChangeEvent { + range: Some(lsp::Range::new( + edit_start.to_lsp_position(), + edit_end.to_lsp_position(), + )), + range_length: None, + text: new_text, + } + }) + .collect(); + + let changes = lsp::DidChangeTextDocumentParams { + text_document: lsp::VersionedTextDocumentIdentifier::new(uri, next_version), + content_changes, + }; + + buffer_snapshots.push((next_version, next_snapshot)); + + let mut notifications = Vec::new(); + for lang_server in self.language_servers_for_worktree(worktree_id) { + notifications.push( + lang_server + .notify::(changes.clone()), + ); } + + cx.background() + .spawn(futures::future::try_join_all(notifications)) + .detach_and_log_err(cx); } BufferEvent::Saved => { - if let Some(file) = File::from_dyn(buffer.read(cx).file()) { - let worktree_id = file.worktree_id(cx); - if let Some(abs_path) = file.as_local().map(|file| file.abs_path(cx)) { - let text_document = lsp::TextDocumentIdentifier { - uri: lsp::Url::from_file_path(abs_path).unwrap(), - }; + let file = File::from_dyn(buffer.read(cx).file())?; + let worktree_id = file.worktree_id(cx); + let abs_path = file.as_local()?.abs_path(cx); + let text_document = lsp::TextDocumentIdentifier { + uri: lsp::Url::from_file_path(abs_path).unwrap(), + }; - let mut notifications = Vec::new(); - for ((lang_server_worktree_id, _), lang_server) in &self.language_servers { - if *lang_server_worktree_id != worktree_id { - continue; - } - - notifications.push( - lang_server.notify::( - lsp::DidSaveTextDocumentParams { - text_document: text_document.clone(), - text: None, - }, - ), - ); - } - - cx.background() - .spawn(futures::future::try_join_all(notifications)) - .detach_and_log_err(cx); - } + let mut notifications = Vec::new(); + for lang_server in self.language_servers_for_worktree(worktree_id) { + notifications.push( + lang_server.notify::( + lsp::DidSaveTextDocumentParams { + text_document: text_document.clone(), + text: None, + }, + ), + ); } + + cx.background() + .spawn(futures::future::try_join_all(notifications)) + .detach_and_log_err(cx); } _ => {} } + + None + } + + fn language_servers_for_worktree( + &self, + worktree_id: WorktreeId, + ) -> impl Iterator> { + self.language_servers.iter().filter_map( + move |((lang_server_worktree_id, _), lang_server)| { + if *lang_server_worktree_id == worktree_id { + Some(lang_server) + } else { + None + } + }, + ) } fn assign_language_to_buffer( &mut self, buffer: &ModelHandle, - worktree: Option<&ModelHandle>, cx: &mut ModelContext, ) -> Option<()> { - let (path, full_path) = { - let file = buffer.read(cx).file()?; - (file.path().clone(), file.full_path(cx)) - }; + // If the buffer has a language, set it and start the language server if we haven't already. + let full_path = buffer.read(cx).file()?.full_path(cx); + let language = self.languages.select_language(&full_path)?; + buffer.update(cx, |buffer, cx| { + buffer.set_language(Some(language.clone()), cx); + }); - // If the buffer has a language, set it and start/assign the language server - if let Some(language) = self.languages.select_language(&full_path) { - buffer.update(cx, |buffer, cx| { - buffer.set_language(Some(language.clone()), cx); - }); - - // For local worktrees, start a language server if needed. - // Also assign the language server and any previously stored diagnostics to the buffer. - if let Some(local_worktree) = worktree.and_then(|w| w.read(cx).as_local()) { - let worktree_id = local_worktree.id(); - let worktree_abs_path = local_worktree.abs_path().clone(); - let buffer = buffer.downgrade(); - let language_server = - self.start_language_server(worktree_id, worktree_abs_path, language, cx); - - cx.spawn_weak(|_, mut cx| async move { - if let Some(language_server) = language_server.await { - if let Some(buffer) = buffer.upgrade(&cx) { - buffer.update(&mut cx, |buffer, cx| { - buffer.set_language_server(Some(language_server), cx); - }); - } - } - }) - .detach(); - } - } - - if let Some(local_worktree) = worktree.and_then(|w| w.read(cx).as_local()) { - if let Some(diagnostics) = local_worktree.diagnostics_for_path(&path) { - buffer.update(cx, |buffer, cx| { - buffer.update_diagnostics(diagnostics, None, cx).log_err(); - }); - } - } + let file = File::from_dyn(buffer.read(cx).file())?; + let worktree = file.worktree.read(cx).as_local()?; + let worktree_id = worktree.id(); + let worktree_abs_path = worktree.abs_path().clone(); + self.start_language_server(worktree_id, worktree_abs_path, language, cx); None } @@ -1057,14 +1161,14 @@ impl Project { worktree_path: Arc, language: Arc, cx: &mut ModelContext, - ) -> Shared>>> { + ) { enum LspEvent { DiagnosticsStart, DiagnosticsUpdate(lsp::PublishDiagnosticsParams), DiagnosticsFinish, } - let key = (worktree_id, language.name().to_string()); + let key = (worktree_id, language.name()); self.started_language_servers .entry(key.clone()) .or_insert_with(|| { @@ -1077,11 +1181,44 @@ impl Project { let rpc = self.client.clone(); cx.spawn_weak(|this, mut cx| async move { let language_server = language_server?.await.log_err()?; - if let Some(this) = this.upgrade(&cx) { - this.update(&mut cx, |this, _| { - this.language_servers.insert(key, language_server.clone()); - }); - } + let this = this.upgrade(&cx)?; + let mut open_notifications = Vec::new(); + this.update(&mut cx, |this, cx| { + this.language_servers.insert(key, language_server.clone()); + for buffer in this.opened_buffers.values() { + if let Some(buffer) = buffer.upgrade(cx) { + let buffer = buffer.read(cx); + if let Some(file) = File::from_dyn(buffer.file()) { + if let Some(file) = file.as_local() { + let versions = this + .buffer_snapshots + .entry(buffer.remote_id()) + .or_insert_with(|| vec![(0, buffer.text_snapshot())]); + let (version, initial_snapshot) = versions.last().unwrap(); + let uri = + lsp::Url::from_file_path(file.abs_path(cx)).unwrap(); + open_notifications.push( + language_server + .notify::( + lsp::DidOpenTextDocumentParams { + text_document: lsp::TextDocumentItem::new( + uri, + Default::default(), + *version, + initial_snapshot.text(), + ), + }, + ), + ); + } + } + } + } + }); + + futures::future::try_join_all(open_notifications) + .await + .log_err(); let disk_based_sources = language .disk_based_diagnostic_sources() @@ -1153,6 +1290,7 @@ impl Project { .detach(); // Process all the LSP events. + let this = this.downgrade(); cx.spawn(|mut cx| async move { while let Ok(message) = diagnostics_rx.recv().await { let this = this.upgrade(&cx)?; @@ -1194,9 +1332,7 @@ impl Project { Some(language_server) }) - .shared() - }) - .clone() + }); } pub fn update_diagnostics( @@ -1326,9 +1462,7 @@ impl Project { .file() .map_or(false, |file| *file.path() == project_path.path) { - buffer.update(cx, |buffer, cx| { - buffer.update_diagnostics(diagnostics.clone(), version, cx) - })?; + self.update_buffer_diagnostics(&buffer, diagnostics.clone(), version, cx)?; break; } } @@ -1343,6 +1477,90 @@ impl Project { Ok(()) } + fn update_buffer_diagnostics( + &mut self, + buffer: &ModelHandle, + mut diagnostics: Vec>, + version: Option, + 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, 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 mut edits_since_save = snapshot + .edits_since::(buffer.read(cx).saved_version()) + .peekable(); + let mut last_edit_old_end = PointUtf16::zero(); + let mut last_edit_new_end = PointUtf16::zero(); + 'outer: for entry in diagnostics { + let mut start = entry.range.start; + let mut end = entry.range.end; + + // Some diagnostics are based on files on disk instead of buffers' + // current contents. Adjust these diagnostics' ranges to reflect + // any unsaved edits. + if entry.diagnostic.is_disk_based { + while let Some(edit) = edits_since_save.peek() { + if edit.old.end <= start { + last_edit_old_end = edit.old.end; + last_edit_new_end = edit.new.end; + edits_since_save.next(); + } else if edit.old.start <= end && edit.old.end >= start { + continue 'outer; + } else { + break; + } + } + + let start_overshoot = start - last_edit_old_end; + start = last_edit_new_end; + start += start_overshoot; + + let end_overshoot = end - last_edit_old_end; + end = last_edit_new_end; + end += end_overshoot; + } + + let mut range = snapshot.clip_point_utf16(start, Bias::Left) + ..snapshot.clip_point_utf16(end, Bias::Right); + + // Expand empty ranges by one character + if range.start == range.end { + range.end.column += 1; + range.end = snapshot.clip_point_utf16(range.end, Bias::Right); + if range.start == range.end && range.end.column > 0 { + range.start.column -= 1; + range.start = snapshot.clip_point_utf16(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(set, cx)); + Ok(()) + } + pub fn format( &self, buffers: HashSet>, @@ -1361,7 +1579,7 @@ impl Project { if let Some(lang) = buffer.language() { if let Some(server) = self .language_servers - .get(&(worktree.read(cx).id(), lang.name().to_string())) + .get(&(worktree.read(cx).id(), lang.name())) { lang_server = server.clone(); } else { @@ -1449,9 +1667,9 @@ impl Project { }; if let Some(lsp_edits) = lsp_edits { - let edits = buffer - .update(&mut cx, |buffer, cx| { - buffer.edits_from_lsp(lsp_edits, None, cx) + let edits = this + .update(&mut cx, |this, cx| { + this.edits_from_lsp(&buffer, lsp_edits, None, cx) }) .await?; buffer.update(&mut cx, |buffer, cx| { @@ -1616,10 +1834,10 @@ impl Project { cx: &mut ModelContext, ) -> Task>> { if self.is_local() { - let language_server = if let Some(server) = self - .language_servers - .get(&(symbol.source_worktree_id, symbol.language_name.clone())) - { + let language_server = if let Some(server) = self.language_servers.get(&( + symbol.source_worktree_id, + Arc::from(symbol.language_name.as_str()), + )) { server.clone() } else { return Task::ready(Err(anyhow!( @@ -1645,7 +1863,7 @@ impl Project { self.open_local_buffer_via_lsp( symbol_uri, - symbol.language_name.clone(), + Arc::from(symbol.language_name.as_str()), language_server, cx, ) @@ -1689,11 +1907,12 @@ impl Project { if worktree.read(cx).as_local().is_some() { let buffer_abs_path = buffer_abs_path.unwrap(); - let lang_server = if let Some(server) = source_buffer.language_server().cloned() { - server - } else { - return Task::ready(Ok(Default::default())); - }; + let lang_server = + if let Some(server) = self.language_server_for_buffer(&source_buffer_handle, cx) { + server.clone() + } else { + return Task::ready(Ok(Default::default())); + }; cx.spawn(|_, cx| async move { let completions = lang_server @@ -1800,19 +2019,22 @@ impl Project { let buffer_id = buffer.remote_id(); if self.is_local() { - let lang_server = if let Some(language_server) = buffer.language_server() { - language_server.clone() - } else { - return Task::ready(Err(anyhow!("buffer does not have a language server"))); - }; + let lang_server = + if let Some(server) = self.language_server_for_buffer(&buffer_handle, cx) { + server.clone() + } else { + return Task::ready(Ok(Default::default())); + }; - cx.spawn(|_, mut cx| async move { + cx.spawn(|this, mut cx| async move { let resolved_completion = lang_server .request::(completion.lsp_completion) .await?; if let Some(edits) = resolved_completion.additional_text_edits { - let edits = buffer_handle - .update(&mut cx, |buffer, cx| buffer.edits_from_lsp(edits, None, cx)) + let edits = this + .update(&mut cx, |this, cx| { + this.edits_from_lsp(&buffer_handle, edits, None, cx) + }) .await?; buffer_handle.update(&mut cx, |buffer, cx| { buffer.finalize_last_transaction(); @@ -1892,7 +2114,7 @@ impl Project { let lang_name; let lang_server; if let Some(lang) = buffer.language() { - lang_name = lang.name().to_string(); + lang_name = lang.name(); if let Some(server) = self .language_servers .get(&(worktree.read(cx).id(), lang_name.clone())) @@ -1993,15 +2215,16 @@ impl Project { if self.is_local() { let buffer = buffer_handle.read(cx); let lang_name = if let Some(lang) = buffer.language() { - lang.name().to_string() + lang.name() } else { return Task::ready(Ok(Default::default())); }; - let lang_server = if let Some(language_server) = buffer.language_server() { - language_server.clone() - } else { - return Task::ready(Err(anyhow!("buffer does not have a language server"))); - }; + let lang_server = + if let Some(server) = self.language_server_for_buffer(&buffer_handle, cx) { + server.clone() + } else { + return Task::ready(Ok(Default::default())); + }; let range = action.range.to_point_utf16(buffer); cx.spawn(|this, mut cx| async move { @@ -2074,7 +2297,7 @@ impl Project { this: ModelHandle, edit: lsp::WorkspaceEdit, push_to_history: bool, - language_name: String, + language_name: Arc, language_server: Arc, cx: &mut AsyncAppContext, ) -> Result { @@ -2158,13 +2381,18 @@ impl Project { }) .await?; - let edits = buffer_to_edit - .update(cx, |buffer, cx| { + let edits = this + .update(cx, |this, cx| { let edits = op.edits.into_iter().map(|edit| match edit { lsp::OneOf::Left(edit) => edit, lsp::OneOf::Right(edit) => edit.text_edit, }); - buffer.edits_from_lsp(edits, op.text_document.version, cx) + this.edits_from_lsp( + &buffer_to_edit, + edits, + op.text_document.version, + cx, + ) }) .await?; @@ -2441,7 +2669,9 @@ impl Project { let buffer = buffer_handle.read(cx); if self.is_local() { let file = File::from_dyn(buffer.file()).and_then(File::as_local); - if let Some((file, language_server)) = file.zip(buffer.language_server().cloned()) { + if let Some((file, language_server)) = + file.zip(self.language_server_for_buffer(&buffer_handle, cx).cloned()) + { let lsp_params = request.to_lsp(&file.abs_path(cx), cx); return cx.spawn(|this, cx| async move { if !language_server @@ -2602,7 +2832,7 @@ impl Project { self.worktrees .push(WorktreeHandle::Strong(worktree.clone())); } else { - cx.observe_release(&worktree, |this, cx| { + cx.observe_release(&worktree, |this, _, cx| { this.worktrees .retain(|worktree| worktree.upgrade(cx).is_some()); cx.notify(); @@ -3441,9 +3671,7 @@ impl Project { Buffer::from_proto(replica_id, buffer, buffer_file, cx).unwrap() }); - this.update(&mut cx, |this, cx| { - this.register_buffer(&buffer, buffer_worktree.as_ref(), cx) - })?; + this.update(&mut cx, |this, cx| this.register_buffer(&buffer, cx))?; *opened_buffer_tx.borrow_mut().borrow_mut() = (); Ok(buffer) @@ -3570,6 +3798,161 @@ impl Project { .await } } + + fn edits_from_lsp( + &mut self, + buffer: &ModelHandle, + lsp_edits: impl 'static + Send + IntoIterator, + version: Option, + cx: &mut ModelContext, + ) -> Task, String)>>> { + let snapshot = self.buffer_snapshot_for_lsp_version(buffer, version, cx); + cx.background().spawn(async move { + let snapshot = snapshot?; + let mut lsp_edits = lsp_edits + .into_iter() + .map(|edit| (range_from_lsp(edit.range), edit.new_text)) + .peekable(); + + let mut edits = Vec::new(); + while let Some((mut range, mut new_text)) = lsp_edits.next() { + // 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 > range.end { + if next_range.start.row > range.end.row + 1 + || next_range.start.column > 0 + || snapshot.clip_point_utf16( + PointUtf16::new(range.end.row, u32::MAX), + Bias::Left, + ) > range.end + { + break; + } + new_text.push('\n'); + } + range.end = next_range.end; + new_text.push_str(&next_text); + lsp_edits.next(); + } + + if snapshot.clip_point_utf16(range.start, Bias::Left) != range.start + || snapshot.clip_point_utf16(range.end, Bias::Left) != range.end + { + return Err(anyhow!("invalid edits received from language server")); + } + + // 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.clone()..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.clone()..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) + }) + } + + fn buffer_snapshot_for_lsp_version( + &mut self, + buffer: &ModelHandle, + 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) + .ok_or_else(|| anyhow!("no snapshot found for buffer {}", buffer_id))?; + let mut found_snapshot = None; + snapshots.retain(|(snapshot_version, snapshot)| { + if snapshot_version + OLD_VERSIONS_TO_RETAIN < version { + false + } else { + if *snapshot_version == version { + found_snapshot = Some(snapshot.clone()); + } + true + } + }); + + found_snapshot.ok_or_else(|| { + anyhow!( + "snapshot not found for buffer {} at version {}", + buffer_id, + version + ) + }) + } else { + Ok((**buffer.read(cx)).clone()) + } + } + + fn language_server_for_buffer( + &self, + buffer: &ModelHandle, + cx: &AppContext, + ) -> Option<&Arc> { + let buffer = buffer.read(cx); + if let Some((file, language)) = File::from_dyn(buffer.file()).zip(buffer.language()) { + let worktree_id = file.worktree_id(cx); + self.language_servers.get(&(worktree_id, language.name())) + } else { + None + } + } } impl WorktreeHandle { @@ -3802,7 +4185,8 @@ mod tests { use futures::StreamExt; use gpui::test::subscribe; use language::{ - tree_sitter_rust, AnchorRangeExt, Diagnostic, LanguageConfig, LanguageServerConfig, Point, + tree_sitter_rust, Diagnostic, LanguageConfig, LanguageServerConfig, OffsetRangeExt, Point, + ToPoint, }; use lsp::Url; use serde_json::json; @@ -3875,7 +4259,232 @@ mod tests { } #[gpui::test] - async fn test_language_server_diagnostics(cx: &mut gpui::TestAppContext) { + async fn test_managing_language_servers(cx: &mut gpui::TestAppContext) { + cx.foreground().forbid_parking(); + + let (lsp_config, mut fake_rust_servers) = LanguageServerConfig::fake(); + let rust_language = Arc::new(Language::new( + LanguageConfig { + name: "Rust".into(), + path_suffixes: vec!["rs".to_string()], + language_server: Some(lsp_config), + ..Default::default() + }, + Some(tree_sitter_rust::language()), + )); + + let (json_lsp_config, mut fake_json_servers) = LanguageServerConfig::fake(); + let json_language = Arc::new(Language::new( + LanguageConfig { + name: "JSON".into(), + path_suffixes: vec!["json".to_string()], + language_server: Some(json_lsp_config), + ..Default::default() + }, + None, + )); + + let fs = FakeFs::new(cx.background()); + fs.insert_tree( + "/the-root", + json!({ + "test.rs": "const A: i32 = 1;", + "Cargo.toml": "a = 1", + "package.json": "{\"a\": 1}", + }), + ) + .await; + + let project = Project::test(fs, cx); + project.update(cx, |project, _| { + project.languages.add(rust_language); + project.languages.add(json_language); + }); + + let worktree_id = project + .update(cx, |project, cx| { + project.find_or_create_local_worktree("/the-root", true, cx) + }) + .await + .unwrap() + .0 + .read_with(cx, |tree, _| tree.id()); + + // Open a buffer without an associated language server. + let toml_buffer = project + .update(cx, |project, cx| { + project.open_buffer((worktree_id, "Cargo.toml"), cx) + }) + .await + .unwrap(); + + // Open a buffer with an associated language server. + let rust_buffer = project + .update(cx, |project, cx| { + project.open_buffer((worktree_id, "test.rs"), cx) + }) + .await + .unwrap(); + + // A server is started up, and it is notified about both open buffers. + let mut fake_rust_server = fake_rust_servers.next().await.unwrap(); + assert_eq!( + fake_rust_server + .receive_notification::() + .await + .text_document, + lsp::TextDocumentItem { + uri: lsp::Url::from_file_path("/the-root/Cargo.toml").unwrap(), + version: 0, + text: "a = 1".to_string(), + language_id: Default::default() + } + ); + assert_eq!( + fake_rust_server + .receive_notification::() + .await + .text_document, + lsp::TextDocumentItem { + uri: lsp::Url::from_file_path("/the-root/test.rs").unwrap(), + version: 0, + text: "const A: i32 = 1;".to_string(), + language_id: Default::default() + } + ); + + // Edit a buffer. The changes are reported to the language server. + rust_buffer.update(cx, |buffer, cx| buffer.edit([16..16], "2", cx)); + assert_eq!( + fake_rust_server + .receive_notification::() + .await + .text_document, + lsp::VersionedTextDocumentIdentifier::new( + lsp::Url::from_file_path("/the-root/test.rs").unwrap(), + 1 + ) + ); + + // Open a third buffer with a different associated language server. + let json_buffer = project + .update(cx, |project, cx| { + project.open_buffer((worktree_id, "package.json"), cx) + }) + .await + .unwrap(); + + // Another language server is started up, and it is notified about + // all three open buffers. + let mut fake_json_server = fake_json_servers.next().await.unwrap(); + assert_eq!( + fake_json_server + .receive_notification::() + .await + .text_document, + lsp::TextDocumentItem { + uri: lsp::Url::from_file_path("/the-root/Cargo.toml").unwrap(), + version: 0, + text: "a = 1".to_string(), + language_id: Default::default() + } + ); + assert_eq!( + fake_json_server + .receive_notification::() + .await + .text_document, + lsp::TextDocumentItem { + uri: lsp::Url::from_file_path("/the-root/package.json").unwrap(), + version: 0, + text: "{\"a\": 1}".to_string(), + language_id: Default::default() + } + ); + assert_eq!( + fake_json_server + .receive_notification::() + .await + .text_document, + lsp::TextDocumentItem { + uri: lsp::Url::from_file_path("/the-root/test.rs").unwrap(), + version: 1, + text: "const A: i32 = 12;".to_string(), + language_id: Default::default() + } + ); + + // The first language server is also notified about the new open buffer. + assert_eq!( + fake_rust_server + .receive_notification::() + .await + .text_document, + lsp::TextDocumentItem { + uri: lsp::Url::from_file_path("/the-root/package.json").unwrap(), + version: 0, + text: "{\"a\": 1}".to_string(), + language_id: Default::default() + } + ); + + // Edit a buffer. The changes are reported to both the language servers. + toml_buffer.update(cx, |buffer, cx| buffer.edit([5..5], "23", cx)); + assert_eq!( + fake_rust_server + .receive_notification::() + .await + .text_document, + lsp::VersionedTextDocumentIdentifier::new( + lsp::Url::from_file_path("/the-root/Cargo.toml").unwrap(), + 1 + ) + ); + assert_eq!( + fake_json_server + .receive_notification::() + .await, + lsp::DidChangeTextDocumentParams { + text_document: lsp::VersionedTextDocumentIdentifier::new( + lsp::Url::from_file_path("/the-root/Cargo.toml").unwrap(), + 1 + ), + content_changes: vec![lsp::TextDocumentContentChangeEvent { + range: Some(lsp::Range::new( + lsp::Position::new(0, 5), + lsp::Position::new(0, 5) + )), + range_length: None, + text: "23".to_string(), + }], + }, + ); + + // Close a buffer. Both language servers are notified. + cx.update(|_| drop(json_buffer)); + let close_message = lsp::DidCloseTextDocumentParams { + text_document: lsp::TextDocumentIdentifier::new( + lsp::Url::from_file_path("/the-root/package.json").unwrap(), + ), + }; + assert_eq!( + fake_json_server + .receive_notification::() + .await, + close_message, + ); + assert_eq!( + fake_rust_server + .receive_notification::() + .await, + close_message, + ); + } + + #[gpui::test] + async fn test_disk_based_diagnostics_progress(cx: &mut gpui::TestAppContext) { + cx.foreground().forbid_parking(); + let (language_server_config, mut fake_servers) = LanguageServerConfig::fake(); let progress_token = language_server_config .disk_based_diagnostics_progress_token @@ -3903,9 +4512,7 @@ mod tests { .await; let project = Project::test(fs, cx); - project.update(cx, |project, _| { - Arc::get_mut(&mut project.languages).unwrap().add(language); - }); + project.update(cx, |project, _| project.languages.add(language)); let (tree, _) = project .update(cx, |project, cx| { @@ -3993,6 +4600,699 @@ mod tests { }); } + #[gpui::test] + async fn test_transforming_disk_based_diagnostics(cx: &mut gpui::TestAppContext) { + cx.foreground().forbid_parking(); + + let (mut lsp_config, mut fake_servers) = LanguageServerConfig::fake(); + lsp_config + .disk_based_diagnostic_sources + .insert("disk".to_string()); + let language = Arc::new(Language::new( + LanguageConfig { + name: "Rust".into(), + path_suffixes: vec!["rs".to_string()], + language_server: Some(lsp_config), + ..Default::default() + }, + Some(tree_sitter_rust::language()), + )); + + let text = " + fn a() { A } + fn b() { BB } + fn c() { CCC } + " + .unindent(); + + let fs = FakeFs::new(cx.background()); + fs.insert_tree("/dir", json!({ "a.rs": text })).await; + + let project = Project::test(fs, cx); + project.update(cx, |project, _| project.languages.add(language)); + + let worktree_id = project + .update(cx, |project, cx| { + project.find_or_create_local_worktree("/dir", true, cx) + }) + .await + .unwrap() + .0 + .read_with(cx, |tree, _| tree.id()); + + let buffer = project + .update(cx, |project, cx| { + project.open_buffer((worktree_id, "a.rs"), cx) + }) + .await + .unwrap(); + + let mut fake_server = fake_servers.next().await.unwrap(); + let open_notification = fake_server + .receive_notification::() + .await; + + // Edit the buffer, moving the content down + buffer.update(cx, |buffer, cx| buffer.edit([0..0], "\n\n", cx)); + let change_notification_1 = fake_server + .receive_notification::() + .await; + assert!( + change_notification_1.text_document.version > open_notification.text_document.version + ); + + // Report some diagnostics for the initial version of the buffer + fake_server + .notify::(lsp::PublishDiagnosticsParams { + uri: lsp::Url::from_file_path("/dir/a.rs").unwrap(), + version: Some(open_notification.text_document.version), + diagnostics: vec![ + lsp::Diagnostic { + range: lsp::Range::new(lsp::Position::new(0, 9), lsp::Position::new(0, 10)), + severity: Some(DiagnosticSeverity::ERROR), + message: "undefined variable 'A'".to_string(), + source: Some("disk".to_string()), + ..Default::default() + }, + lsp::Diagnostic { + range: lsp::Range::new(lsp::Position::new(1, 9), lsp::Position::new(1, 11)), + severity: Some(DiagnosticSeverity::ERROR), + message: "undefined variable 'BB'".to_string(), + source: Some("disk".to_string()), + ..Default::default() + }, + lsp::Diagnostic { + range: lsp::Range::new(lsp::Position::new(2, 9), lsp::Position::new(2, 12)), + severity: Some(DiagnosticSeverity::ERROR), + source: Some("disk".to_string()), + message: "undefined variable 'CCC'".to_string(), + ..Default::default() + }, + ], + }) + .await; + + // The diagnostics have moved down since they were created. + buffer.next_notification(cx).await; + buffer.read_with(cx, |buffer, _| { + assert_eq!( + buffer + .snapshot() + .diagnostics_in_range::<_, Point>(Point::new(3, 0)..Point::new(5, 0)) + .collect::>(), + &[ + DiagnosticEntry { + range: Point::new(3, 9)..Point::new(3, 11), + diagnostic: Diagnostic { + severity: DiagnosticSeverity::ERROR, + message: "undefined variable 'BB'".to_string(), + is_disk_based: true, + group_id: 1, + is_primary: true, + ..Default::default() + }, + }, + DiagnosticEntry { + range: Point::new(4, 9)..Point::new(4, 12), + diagnostic: Diagnostic { + severity: DiagnosticSeverity::ERROR, + message: "undefined variable 'CCC'".to_string(), + is_disk_based: true, + group_id: 2, + is_primary: true, + ..Default::default() + } + } + ] + ); + assert_eq!( + chunks_with_diagnostics(buffer, 0..buffer.len()), + [ + ("\n\nfn a() { ".to_string(), None), + ("A".to_string(), Some(DiagnosticSeverity::ERROR)), + (" }\nfn b() { ".to_string(), None), + ("BB".to_string(), Some(DiagnosticSeverity::ERROR)), + (" }\nfn c() { ".to_string(), None), + ("CCC".to_string(), Some(DiagnosticSeverity::ERROR)), + (" }\n".to_string(), None), + ] + ); + assert_eq!( + chunks_with_diagnostics(buffer, Point::new(3, 10)..Point::new(4, 11)), + [ + ("B".to_string(), Some(DiagnosticSeverity::ERROR)), + (" }\nfn c() { ".to_string(), None), + ("CC".to_string(), Some(DiagnosticSeverity::ERROR)), + ] + ); + }); + + // Ensure overlapping diagnostics are highlighted correctly. + fake_server + .notify::(lsp::PublishDiagnosticsParams { + uri: lsp::Url::from_file_path("/dir/a.rs").unwrap(), + version: Some(open_notification.text_document.version), + diagnostics: vec![ + lsp::Diagnostic { + range: lsp::Range::new(lsp::Position::new(0, 9), lsp::Position::new(0, 10)), + severity: Some(DiagnosticSeverity::ERROR), + message: "undefined variable 'A'".to_string(), + source: Some("disk".to_string()), + ..Default::default() + }, + lsp::Diagnostic { + range: lsp::Range::new(lsp::Position::new(0, 9), lsp::Position::new(0, 12)), + severity: Some(DiagnosticSeverity::WARNING), + message: "unreachable statement".to_string(), + source: Some("disk".to_string()), + ..Default::default() + }, + ], + }) + .await; + + buffer.next_notification(cx).await; + buffer.read_with(cx, |buffer, _| { + assert_eq!( + buffer + .snapshot() + .diagnostics_in_range::<_, Point>(Point::new(2, 0)..Point::new(3, 0)) + .collect::>(), + &[ + DiagnosticEntry { + range: Point::new(2, 9)..Point::new(2, 12), + diagnostic: Diagnostic { + severity: DiagnosticSeverity::WARNING, + message: "unreachable statement".to_string(), + is_disk_based: true, + group_id: 1, + is_primary: true, + ..Default::default() + } + }, + DiagnosticEntry { + range: Point::new(2, 9)..Point::new(2, 10), + diagnostic: Diagnostic { + severity: DiagnosticSeverity::ERROR, + message: "undefined variable 'A'".to_string(), + is_disk_based: true, + group_id: 0, + is_primary: true, + ..Default::default() + }, + } + ] + ); + assert_eq!( + chunks_with_diagnostics(buffer, Point::new(2, 0)..Point::new(3, 0)), + [ + ("fn a() { ".to_string(), None), + ("A".to_string(), Some(DiagnosticSeverity::ERROR)), + (" }".to_string(), Some(DiagnosticSeverity::WARNING)), + ("\n".to_string(), None), + ] + ); + assert_eq!( + chunks_with_diagnostics(buffer, Point::new(2, 10)..Point::new(3, 0)), + [ + (" }".to_string(), Some(DiagnosticSeverity::WARNING)), + ("\n".to_string(), None), + ] + ); + }); + + // Keep editing the buffer and ensure disk-based diagnostics get translated according to the + // changes since the last save. + buffer.update(cx, |buffer, cx| { + buffer.edit(Some(Point::new(2, 0)..Point::new(2, 0)), " ", cx); + buffer.edit(Some(Point::new(2, 8)..Point::new(2, 10)), "(x: usize)", cx); + }); + let change_notification_2 = + fake_server.receive_notification::(); + assert!( + change_notification_2.await.text_document.version + > change_notification_1.text_document.version + ); + + // Handle out-of-order diagnostics + fake_server + .notify::(lsp::PublishDiagnosticsParams { + uri: lsp::Url::from_file_path("/dir/a.rs").unwrap(), + version: Some(open_notification.text_document.version), + diagnostics: vec![ + lsp::Diagnostic { + range: lsp::Range::new(lsp::Position::new(1, 9), lsp::Position::new(1, 11)), + severity: Some(DiagnosticSeverity::ERROR), + message: "undefined variable 'BB'".to_string(), + source: Some("disk".to_string()), + ..Default::default() + }, + lsp::Diagnostic { + range: lsp::Range::new(lsp::Position::new(0, 9), lsp::Position::new(0, 10)), + severity: Some(DiagnosticSeverity::WARNING), + message: "undefined variable 'A'".to_string(), + source: Some("disk".to_string()), + ..Default::default() + }, + ], + }) + .await; + + buffer.next_notification(cx).await; + buffer.read_with(cx, |buffer, _| { + assert_eq!( + buffer + .snapshot() + .diagnostics_in_range::<_, Point>(0..buffer.len()) + .collect::>(), + &[ + DiagnosticEntry { + range: Point::new(2, 21)..Point::new(2, 22), + diagnostic: Diagnostic { + severity: DiagnosticSeverity::WARNING, + message: "undefined variable 'A'".to_string(), + is_disk_based: true, + group_id: 1, + is_primary: true, + ..Default::default() + } + }, + DiagnosticEntry { + range: Point::new(3, 9)..Point::new(3, 11), + diagnostic: Diagnostic { + severity: DiagnosticSeverity::ERROR, + message: "undefined variable 'BB'".to_string(), + is_disk_based: true, + group_id: 0, + is_primary: true, + ..Default::default() + }, + } + ] + ); + }); + } + + #[gpui::test] + async fn test_empty_diagnostic_ranges(cx: &mut gpui::TestAppContext) { + cx.foreground().forbid_parking(); + + let text = concat!( + "let one = ;\n", // + "let two = \n", + "let three = 3;\n", + ); + + let fs = FakeFs::new(cx.background()); + fs.insert_tree("/dir", json!({ "a.rs": text })).await; + + let project = Project::test(fs, cx); + let worktree_id = project + .update(cx, |project, cx| { + project.find_or_create_local_worktree("/dir", true, cx) + }) + .await + .unwrap() + .0 + .read_with(cx, |tree, _| tree.id()); + + let buffer = project + .update(cx, |project, cx| { + project.open_buffer((worktree_id, "a.rs"), cx) + }) + .await + .unwrap(); + + project.update(cx, |project, cx| { + project + .update_buffer_diagnostics( + &buffer, + vec![ + DiagnosticEntry { + range: PointUtf16::new(0, 10)..PointUtf16::new(0, 10), + diagnostic: Diagnostic { + severity: DiagnosticSeverity::ERROR, + message: "syntax error 1".to_string(), + ..Default::default() + }, + }, + DiagnosticEntry { + range: PointUtf16::new(1, 10)..PointUtf16::new(1, 10), + diagnostic: Diagnostic { + severity: DiagnosticSeverity::ERROR, + message: "syntax error 2".to_string(), + ..Default::default() + }, + }, + ], + None, + cx, + ) + .unwrap(); + }); + + // An empty range is extended forward to include the following character. + // At the end of a line, an empty range is extended backward to include + // the preceding character. + buffer.read_with(cx, |buffer, _| { + let chunks = chunks_with_diagnostics(&buffer, 0..buffer.len()); + assert_eq!( + chunks + .iter() + .map(|(s, d)| (s.as_str(), *d)) + .collect::>(), + &[ + ("let one = ", None), + (";", Some(DiagnosticSeverity::ERROR)), + ("\nlet two =", None), + (" ", Some(DiagnosticSeverity::ERROR)), + ("\nlet three = 3;\n", None) + ] + ); + }); + } + + #[gpui::test] + async fn test_edits_from_lsp_with_past_version(cx: &mut gpui::TestAppContext) { + cx.foreground().forbid_parking(); + + let (lsp_config, mut fake_servers) = LanguageServerConfig::fake(); + let language = Arc::new(Language::new( + LanguageConfig { + name: "Rust".into(), + path_suffixes: vec!["rs".to_string()], + language_server: Some(lsp_config), + ..Default::default() + }, + Some(tree_sitter_rust::language()), + )); + + let text = " + fn a() { + f1(); + } + fn b() { + f2(); + } + fn c() { + f3(); + } + " + .unindent(); + + let fs = FakeFs::new(cx.background()); + fs.insert_tree( + "/dir", + json!({ + "a.rs": text.clone(), + }), + ) + .await; + + let project = Project::test(fs, cx); + project.update(cx, |project, _| project.languages.add(language)); + + let worktree_id = project + .update(cx, |project, cx| { + project.find_or_create_local_worktree("/dir", true, cx) + }) + .await + .unwrap() + .0 + .read_with(cx, |tree, _| tree.id()); + + let buffer = project + .update(cx, |project, cx| { + project.open_buffer((worktree_id, "a.rs"), cx) + }) + .await + .unwrap(); + + let mut fake_server = fake_servers.next().await.unwrap(); + let lsp_document_version = fake_server + .receive_notification::() + .await + .text_document + .version; + + // Simulate editing the buffer after the language server computes some edits. + buffer.update(cx, |buffer, cx| { + buffer.edit( + [Point::new(0, 0)..Point::new(0, 0)], + "// above first function\n", + cx, + ); + buffer.edit( + [Point::new(2, 0)..Point::new(2, 0)], + " // inside first function\n", + cx, + ); + buffer.edit( + [Point::new(6, 4)..Point::new(6, 4)], + "// inside second function ", + cx, + ); + + assert_eq!( + buffer.text(), + " + // above first function + fn a() { + // inside first function + f1(); + } + fn b() { + // inside second function f2(); + } + fn c() { + f3(); + } + " + .unindent() + ); + }); + + let edits = project + .update(cx, |project, cx| { + project.edits_from_lsp( + &buffer, + vec![ + // replace body of first function + lsp::TextEdit { + range: lsp::Range::new( + lsp::Position::new(0, 0), + lsp::Position::new(3, 0), + ), + new_text: " + fn a() { + f10(); + } + " + .unindent(), + }, + // edit inside second function + lsp::TextEdit { + range: lsp::Range::new( + lsp::Position::new(4, 6), + lsp::Position::new(4, 6), + ), + new_text: "00".into(), + }, + // edit inside third function via two distinct edits + lsp::TextEdit { + range: lsp::Range::new( + lsp::Position::new(7, 5), + lsp::Position::new(7, 5), + ), + new_text: "4000".into(), + }, + lsp::TextEdit { + range: lsp::Range::new( + lsp::Position::new(7, 5), + lsp::Position::new(7, 6), + ), + new_text: "".into(), + }, + ], + Some(lsp_document_version), + cx, + ) + }) + .await + .unwrap(); + + buffer.update(cx, |buffer, cx| { + for (range, new_text) in edits { + buffer.edit([range], new_text, cx); + } + assert_eq!( + buffer.text(), + " + // above first function + fn a() { + // inside first function + f10(); + } + fn b() { + // inside second function f200(); + } + fn c() { + f4000(); + } + " + .unindent() + ); + }); + } + + #[gpui::test] + async fn test_edits_from_lsp_with_edits_on_adjacent_lines(cx: &mut gpui::TestAppContext) { + cx.foreground().forbid_parking(); + + let text = " + use a::b; + use a::c; + + fn f() { + b(); + c(); + } + " + .unindent(); + + let fs = FakeFs::new(cx.background()); + fs.insert_tree( + "/dir", + json!({ + "a.rs": text.clone(), + }), + ) + .await; + + let project = Project::test(fs, cx); + let worktree_id = project + .update(cx, |project, cx| { + project.find_or_create_local_worktree("/dir", true, cx) + }) + .await + .unwrap() + .0 + .read_with(cx, |tree, _| tree.id()); + + let buffer = project + .update(cx, |project, cx| { + project.open_buffer((worktree_id, "a.rs"), cx) + }) + .await + .unwrap(); + + // Simulate the language server sending us a small edit in the form of a very large diff. + // Rust-analyzer does this when performing a merge-imports code action. + let edits = project + .update(cx, |project, cx| { + project.edits_from_lsp( + &buffer, + [ + // Replace the first use statement without editing the semicolon. + lsp::TextEdit { + range: lsp::Range::new( + lsp::Position::new(0, 4), + lsp::Position::new(0, 8), + ), + new_text: "a::{b, c}".into(), + }, + // Reinsert the remainder of the file between the semicolon and the final + // newline of the file. + lsp::TextEdit { + range: lsp::Range::new( + lsp::Position::new(0, 9), + lsp::Position::new(0, 9), + ), + new_text: "\n\n".into(), + }, + lsp::TextEdit { + range: lsp::Range::new( + lsp::Position::new(0, 9), + lsp::Position::new(0, 9), + ), + new_text: " + fn f() { + b(); + c(); + }" + .unindent(), + }, + // Delete everything after the first newline of the file. + lsp::TextEdit { + range: lsp::Range::new( + lsp::Position::new(1, 0), + lsp::Position::new(7, 0), + ), + new_text: "".into(), + }, + ], + None, + cx, + ) + }) + .await + .unwrap(); + + buffer.update(cx, |buffer, cx| { + let edits = edits + .into_iter() + .map(|(range, text)| { + ( + range.start.to_point(&buffer)..range.end.to_point(&buffer), + text, + ) + }) + .collect::>(); + + assert_eq!( + edits, + [ + (Point::new(0, 4)..Point::new(0, 8), "a::{b, c}".into()), + (Point::new(1, 0)..Point::new(2, 0), "".into()) + ] + ); + + for (range, new_text) in edits { + buffer.edit([range], new_text, cx); + } + assert_eq!( + buffer.text(), + " + use a::{b, c}; + + fn f() { + b(); + c(); + } + " + .unindent() + ); + }); + } + + fn chunks_with_diagnostics( + buffer: &Buffer, + range: Range, + ) -> Vec<(String, Option)> { + let mut chunks: Vec<(String, Option)> = Vec::new(); + for chunk in buffer.snapshot().chunks(range, true) { + if chunks + .last() + .map_or(false, |prev_chunk| prev_chunk.1 == chunk.diagnostic) + { + chunks.last_mut().unwrap().0.push_str(chunk.text); + } else { + chunks.push((chunk.text.to_string(), chunk.diagnostic)); + } + } + chunks + } + #[gpui::test] async fn test_search_worktree_without_files(cx: &mut gpui::TestAppContext) { let dir = temp_tree(json!({ diff --git a/crates/project/src/worktree.rs b/crates/project/src/worktree.rs index 1130063c98..290f44c5cf 100644 --- a/crates/project/src/worktree.rs +++ b/crates/project/src/worktree.rs @@ -556,6 +556,7 @@ impl LocalWorktree { } pub fn diagnostics_for_path(&self, path: &Path) -> Option>> { + dbg!(&self.diagnostics); self.diagnostics.get(path).cloned() } diff --git a/crates/search/src/buffer_search.rs b/crates/search/src/buffer_search.rs index 9a8f4a77d7..a5184ea5f3 100644 --- a/crates/search/src/buffer_search.rs +++ b/crates/search/src/buffer_search.rs @@ -5,7 +5,7 @@ use gpui::{ action, elements::*, keymap::Binding, platform::CursorStyle, Entity, MutableAppContext, RenderContext, Subscription, Task, View, ViewContext, ViewHandle, WeakViewHandle, }; -use language::AnchorRangeExt; +use language::OffsetRangeExt; use postage::watch; use project::search::SearchQuery; use std::ops::Range; diff --git a/crates/server/src/rpc.rs b/crates/server/src/rpc.rs index 0b5f5cf880..7fa4dc7db9 100644 --- a/crates/server/src/rpc.rs +++ b/crates/server/src/rpc.rs @@ -1011,8 +1011,8 @@ mod tests { }; use gpui::{executor, ModelHandle, TestAppContext}; use language::{ - tree_sitter_rust, AnchorRangeExt, Diagnostic, DiagnosticEntry, Language, LanguageConfig, - LanguageRegistry, LanguageServerConfig, Point, ToLspPosition, + tree_sitter_rust, Diagnostic, DiagnosticEntry, Language, LanguageConfig, LanguageRegistry, + LanguageServerConfig, OffsetRangeExt, Point, ToLspPosition, }; use lsp; use parking_lot::Mutex; diff --git a/crates/text/src/anchor.rs b/crates/text/src/anchor.rs index a14a16cbc4..28da998d67 100644 --- a/crates/text/src/anchor.rs +++ b/crates/text/src/anchor.rs @@ -1,5 +1,5 @@ use super::{Point, ToOffset}; -use crate::{rope::TextDimension, BufferSnapshot, PointUtf16, ToPointUtf16}; +use crate::{rope::TextDimension, BufferSnapshot, PointUtf16, ToPoint, ToPointUtf16}; use anyhow::Result; use std::{cmp::Ordering, fmt::Debug, ops::Range}; use sum_tree::Bias; @@ -74,11 +74,33 @@ impl Anchor { } } +pub trait OffsetRangeExt { + fn to_offset(&self, snapshot: &BufferSnapshot) -> Range; + fn to_point(&self, snapshot: &BufferSnapshot) -> Range; + fn to_point_utf16(&self, snapshot: &BufferSnapshot) -> Range; +} + +impl OffsetRangeExt for Range +where + T: ToOffset, +{ + fn to_offset(&self, snapshot: &BufferSnapshot) -> Range { + self.start.to_offset(snapshot)..self.end.to_offset(&snapshot) + } + + fn to_point(&self, snapshot: &BufferSnapshot) -> Range { + self.start.to_offset(snapshot).to_point(snapshot) + ..self.end.to_offset(snapshot).to_point(snapshot) + } + + fn to_point_utf16(&self, snapshot: &BufferSnapshot) -> Range { + self.start.to_offset(snapshot).to_point_utf16(snapshot) + ..self.end.to_offset(snapshot).to_point_utf16(snapshot) + } +} + pub trait AnchorRangeExt { fn cmp(&self, b: &Range, buffer: &BufferSnapshot) -> Result; - fn to_offset(&self, content: &BufferSnapshot) -> Range; - fn to_point(&self, content: &BufferSnapshot) -> Range; - fn to_point_utf16(&self, content: &BufferSnapshot) -> Range; } impl AnchorRangeExt for Range { @@ -88,16 +110,4 @@ impl AnchorRangeExt for Range { ord @ _ => ord, }) } - - fn to_offset(&self, content: &BufferSnapshot) -> Range { - self.start.to_offset(&content)..self.end.to_offset(&content) - } - - fn to_point(&self, content: &BufferSnapshot) -> Range { - self.start.summary::(&content)..self.end.summary::(&content) - } - - fn to_point_utf16(&self, content: &BufferSnapshot) -> Range { - self.start.to_point_utf16(content)..self.end.to_point_utf16(content) - } }