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/client/src/user.rs b/crates/client/src/user.rs index 5e7d29bfa6..bd56ed3e1b 100644 --- a/crates/client/src/user.rs +++ b/crates/client/src/user.rs @@ -186,7 +186,7 @@ impl UserStore { cx: &mut ModelContext, ) -> Task>> { if let Some(user) = self.users.get(&user_id).cloned() { - return cx.spawn_weak(|_, _| async move { Ok(user) }); + return cx.foreground().spawn(async move { Ok(user) }); } let load_users = self.load_users(vec![user_id], cx); diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 857b33f625..1ffe18114b 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::{ @@ -5912,9 +5912,9 @@ pub fn styled_runs_for_code_label<'a>( #[cfg(test)] mod tests { use super::*; - use language::LanguageConfig; + use language::{LanguageConfig, LanguageServerConfig}; use lsp::FakeLanguageServer; - use project::{FakeFs, ProjectPath}; + use project::FakeFs; use smol::stream::StreamExt; use std::{cell::RefCell, rc::Rc, time::Instant}; use text::Point; @@ -8196,18 +8196,24 @@ mod tests { #[gpui::test] async fn test_completion(cx: &mut gpui::TestAppContext) { let settings = cx.read(Settings::test); - let (language_server, mut fake) = cx.update(|cx| { - lsp::LanguageServer::fake_with_capabilities( - lsp::ServerCapabilities { - completion_provider: Some(lsp::CompletionOptions { - trigger_characters: Some(vec![".".to_string(), ":".to_string()]), - ..Default::default() - }), - ..Default::default() - }, - cx, - ) + + let (mut language_server_config, mut fake_servers) = LanguageServerConfig::fake(); + language_server_config.set_fake_capabilities(lsp::ServerCapabilities { + completion_provider: Some(lsp::CompletionOptions { + trigger_characters: Some(vec![".".to_string(), ":".to_string()]), + ..Default::default() + }), + ..Default::default() }); + let language = Arc::new(Language::new( + LanguageConfig { + name: "Rust".into(), + path_suffixes: vec!["rs".to_string()], + language_server: Some(language_server_config), + ..Default::default() + }, + Some(tree_sitter_rust::language()), + )); let text = " one @@ -8217,31 +8223,26 @@ mod tests { .unindent(); let fs = FakeFs::new(cx.background().clone()); - fs.insert_file("/file", text).await; + fs.insert_file("/file.rs", text).await; let project = Project::test(fs, cx); + project.update(cx, |project, _| project.languages().add(language)); - let (worktree, relative_path) = project + let worktree_id = project .update(cx, |project, cx| { - project.find_or_create_local_worktree("/file", true, cx) + project.find_or_create_local_worktree("/file.rs", true, cx) }) .await - .unwrap(); - let project_path = ProjectPath { - worktree_id: worktree.read_with(cx, |worktree, _| worktree.id()), - path: relative_path.into(), - }; + .unwrap() + .0 + .read_with(cx, |tree, _| tree.id()); let buffer = project - .update(cx, |project, cx| project.open_buffer(project_path, cx)) + .update(cx, |project, cx| project.open_buffer((worktree_id, ""), cx)) .await .unwrap(); - buffer.update(cx, |buffer, cx| { - buffer.set_language_server(Some(language_server), cx); - }); + let mut fake_server = fake_servers.next().await.unwrap(); let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx)); - buffer.next_notification(&cx).await; - let (_, editor) = cx.add_window(|cx| build_editor(buffer, settings, cx)); editor.update(cx, |editor, cx| { @@ -8251,8 +8252,8 @@ mod tests { }); handle_completion_request( - &mut fake, - "/file", + &mut fake_server, + "/file.rs", Point::new(0, 4), vec![ (Point::new(0, 4)..Point::new(0, 4), "first_completion"), @@ -8282,7 +8283,7 @@ mod tests { }); handle_resolve_completion_request( - &mut fake, + &mut fake_server, Some((Point::new(2, 5)..Point::new(2, 5), "\nadditional edit")), ) .await; @@ -8315,8 +8316,8 @@ mod tests { }); handle_completion_request( - &mut fake, - "/file", + &mut fake_server, + "/file.rs", Point::new(2, 7), vec![ (Point::new(2, 6)..Point::new(2, 7), "fourth_completion"), @@ -8334,8 +8335,8 @@ mod tests { }); handle_completion_request( - &mut fake, - "/file", + &mut fake_server, + "/file.rs", Point::new(2, 8), vec![ (Point::new(2, 6)..Point::new(2, 8), "fourth_completion"), @@ -8364,7 +8365,7 @@ mod tests { ); apply_additional_edits }); - handle_resolve_completion_request(&mut fake, None).await; + handle_resolve_completion_request(&mut fake_server, None).await; apply_additional_edits.await.unwrap(); async fn handle_completion_request( 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 6548223933..e91963bfa6 100644 --- a/crates/gpui/src/app.rs +++ b/crates/gpui/src/app.rs @@ -741,7 +741,7 @@ type GlobalActionCallback = dyn FnMut(&dyn AnyAction, &mut MutableAppContext); type SubscriptionCallback = Box bool>; type ObservationCallback = Box bool>; -type ReleaseObservationCallback = Box; +type ReleaseObservationCallback = Box; pub struct MutableAppContext { weak_self: Option>>, @@ -1154,14 +1154,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(), @@ -1520,9 +1526,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 { @@ -1548,7 +1553,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 { @@ -1575,7 +1580,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); } @@ -1738,11 +1748,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); } } } @@ -2069,8 +2079,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, @@ -2099,9 +2114,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") @@ -2332,13 +2351,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); }); } }) @@ -2594,13 +2613,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); }); } }) @@ -4061,7 +4080,7 @@ mod tests { } #[crate::test(self)] - fn test_subscribe_and_emit_from_model(cx: &mut MutableAppContext) { + fn test_model_events(cx: &mut MutableAppContext) { #[derive(Default)] struct Model { events: Vec, @@ -4073,11 +4092,11 @@ mod tests { let handle_1 = cx.add_model(|_| Model::default()); let handle_2 = cx.add_model(|_| Model::default()); - handle_1.update(cx, |_, c| { - c.subscribe(&handle_2, move |model: &mut Model, emitter, event, c| { + handle_1.update(cx, |_, cx| { + cx.subscribe(&handle_2, move |model: &mut Model, emitter, event, cx| { model.events.push(*event); - c.subscribe(&emitter, |model, _, event, _| { + cx.subscribe(&emitter, |model, _, event, _| { model.events.push(*event * 2); }) .detach(); @@ -4294,12 +4313,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(); @@ -4316,7 +4335,7 @@ mod tests { } #[crate::test(self)] - fn test_subscribe_and_emit_from_view(cx: &mut MutableAppContext) { + fn test_view_events(cx: &mut MutableAppContext) { #[derive(Default)] struct View { events: Vec, @@ -4346,18 +4365,18 @@ mod tests { let handle_2 = cx.add_view(window_id, |_| View::default()); let handle_3 = cx.add_model(|_| Model); - handle_1.update(cx, |_, c| { - c.subscribe(&handle_2, move |me, emitter, event, c| { + handle_1.update(cx, |_, cx| { + cx.subscribe(&handle_2, move |me, emitter, event, cx| { me.events.push(*event); - c.subscribe(&emitter, |me, _, event, _| { + cx.subscribe(&emitter, |me, _, event, _| { me.events.push(*event * 2); }) .detach(); }) .detach(); - c.subscribe(&handle_3, |me, _, event, _| { + cx.subscribe(&handle_3, |me, _, event, _| { me.events.push(*event); }) .detach(); diff --git a/crates/language/src/buffer.rs b/crates/language/src/buffer.rs index f1bc581932..3d79ecadd6 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,11 +68,8 @@ pub struct Buffer { diagnostics_update_count: usize, diagnostics_timestamp: clock::Lamport, file_update_count: usize, - language_server: Option, completion_triggers: Vec, deferred_ops: OperationQueue, - #[cfg(test)] - pub(crate) operations: Vec, } pub struct BufferSnapshot { @@ -128,22 +123,7 @@ 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)] +#[derive(Clone, Debug, PartialEq, Eq)] pub enum Operation { Buffer(text::Operation), UpdateDiagnostics { @@ -160,8 +140,9 @@ pub enum Operation { }, } -#[derive(Clone, Debug, Eq, PartialEq)] +#[derive(Clone, Debug, PartialEq, Eq)] pub enum Event { + Operation(Operation), Edited, Dirtied, Saved, @@ -202,10 +183,6 @@ pub trait File { cx: &mut MutableAppContext, ) -> Task>; - fn buffer_updated(&self, buffer_id: u64, operation: Operation, cx: &mut MutableAppContext); - - fn buffer_removed(&self, buffer_id: u64, cx: &mut MutableAppContext); - fn as_any(&self) -> &dyn Any; fn to_proto(&self) -> rpc::proto::File; @@ -226,83 +203,6 @@ pub trait LocalFile: File { ); } -#[cfg(any(test, feature = "test-support"))] -pub struct FakeFile { - pub path: Arc, -} - -#[cfg(any(test, feature = "test-support"))] -impl FakeFile { - pub fn new(path: impl AsRef) -> Self { - Self { - path: path.as_ref().into(), - } - } -} - -#[cfg(any(test, feature = "test-support"))] -impl File for FakeFile { - fn as_local(&self) -> Option<&dyn LocalFile> { - Some(self) - } - - fn mtime(&self) -> SystemTime { - SystemTime::UNIX_EPOCH - } - - fn path(&self) -> &Arc { - &self.path - } - - fn full_path(&self, _: &AppContext) -> PathBuf { - self.path.to_path_buf() - } - - fn file_name(&self, _: &AppContext) -> OsString { - self.path.file_name().unwrap().to_os_string() - } - - fn is_deleted(&self) -> bool { - false - } - - fn save( - &self, - _: u64, - _: Rope, - _: clock::Global, - cx: &mut MutableAppContext, - ) -> Task> { - cx.spawn(|_| async move { Ok((Default::default(), SystemTime::UNIX_EPOCH)) }) - } - - fn buffer_updated(&self, _: u64, _: Operation, _: &mut MutableAppContext) {} - - fn buffer_removed(&self, _: u64, _: &mut MutableAppContext) {} - - fn as_any(&self) -> &dyn Any { - self - } - - fn to_proto(&self) -> rpc::proto::File { - unimplemented!() - } -} - -#[cfg(any(test, feature = "test-support"))] -impl LocalFile for FakeFile { - fn abs_path(&self, _: &AppContext) -> PathBuf { - self.path.to_path_buf() - } - - fn load(&self, cx: &AppContext) -> Task> { - cx.background().spawn(async move { Ok(Default::default()) }) - } - - fn buffer_reloaded(&self, _: u64, _: &clock::Global, _: SystemTime, _: &mut MutableAppContext) { - } -} - pub(crate) struct QueryCursorHandle(Option); #[derive(Clone)] @@ -488,15 +388,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() { @@ -523,11 +414,8 @@ 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(), - #[cfg(test)] - operations: Default::default(), } } @@ -547,6 +435,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() } @@ -572,123 +468,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, @@ -702,26 +490,6 @@ impl Buffer { self.file = Some(new_file); self.file_update_count += 1; } - if let Some((state, local_file)) = &self - .language_server - .as_ref() - .zip(self.file.as_ref().and_then(|f| f.as_local())) - { - cx.background() - .spawn( - state - .server - .notify::( - lsp::DidSaveTextDocumentParams { - text_document: lsp::TextDocumentIdentifier { - uri: lsp::Url::from_file_path(local_file.abs_path(cx)).unwrap(), - }, - text: None, - }, - ), - ) - .detach() - } cx.emit(Event::Saved); cx.notify(); } @@ -815,10 +583,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 } @@ -930,100 +694,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) { @@ -1336,30 +1014,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, @@ -1486,115 +1140,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, @@ -1606,7 +1151,6 @@ impl Buffer { } self.reparse(cx); - self.update_language_server(cx); cx.emit(Event::Edited); if !was_dirty { @@ -1745,16 +1289,8 @@ impl Buffer { } } - #[cfg(not(test))] - pub fn send_operation(&mut self, operation: Operation, cx: &mut ModelContext) { - if let Some(file) = &self.file { - file.buffer_updated(self.remote_id(), operation, cx.as_mut()); - } - } - - #[cfg(test)] - pub fn send_operation(&mut self, operation: Operation, _: &mut ModelContext) { - self.operations.push(operation); + fn send_operation(&mut self, operation: Operation, cx: &mut ModelContext) { + cx.emit(Event::Operation(operation)); } pub fn remove_peer(&mut self, replica_id: ReplicaId, cx: &mut ModelContext) { @@ -1826,6 +1362,19 @@ impl Buffer { redone } + pub fn set_completion_triggers(&mut self, triggers: Vec, cx: &mut ModelContext) { + self.completion_triggers = triggers.clone(); + let lamport_timestamp = self.text.lamport_clock.tick(); + self.send_operation( + Operation::UpdateCompletionTriggers { + triggers, + lamport_timestamp, + }, + cx, + ); + cx.notify(); + } + pub fn completion_triggers(&self) -> &[String] { &self.completion_triggers } @@ -1882,24 +1431,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() { - file.buffer_removed(self.remote_id(), cx); - 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 { @@ -2632,20 +2163,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/language.rs b/crates/language/src/language.rs index a056d84c40..5af18241b6 100644 --- a/crates/language/src/language.rs +++ b/crates/language/src/language.rs @@ -245,31 +245,46 @@ impl LanguageRegistry { root_path: Arc, http_client: Arc, cx: &mut MutableAppContext, - ) -> Option>>> { + ) -> Option>> { #[cfg(any(test, feature = "test-support"))] - if let Some(config) = &language.config.language_server { - if let Some(fake_config) = &config.fake_config { - let (server, mut fake_server) = lsp::LanguageServer::fake_with_capabilities( - fake_config.capabilities.clone(), - cx, - ); - - if let Some(initalizer) = &fake_config.initializer { - initalizer(&mut fake_server); + if language + .config + .language_server + .as_ref() + .and_then(|config| config.fake_config.as_ref()) + .is_some() + { + let language = language.clone(); + return Some(cx.spawn(|mut cx| async move { + let fake_config = language + .config + .language_server + .as_ref() + .unwrap() + .fake_config + .as_ref() + .unwrap(); + let (server, mut fake_server) = cx.update(|cx| { + lsp::LanguageServer::fake_with_capabilities( + fake_config.capabilities.clone(), + cx, + ) + }); + if let Some(initializer) = &fake_config.initializer { + initializer(&mut fake_server); } let servers_tx = fake_config.servers_tx.clone(); - let initialized = server.capabilities(); cx.background() .spawn(async move { - if initialized.await.is_some() { - servers_tx.unbounded_send(fake_server).ok(); - } + fake_server + .receive_notification::() + .await; + servers_tx.unbounded_send(fake_server).ok(); }) .detach(); - - return Some(Task::ready(Ok(server.clone()))); - } + Ok(server) + })); } let download_dir = self @@ -304,14 +319,13 @@ impl LanguageRegistry { let server_binary_path = server_binary_path.await?; let server_args = adapter.server_args(); - let server = lsp::LanguageServer::new( + lsp::LanguageServer::new( &server_binary_path, server_args, - adapter.initialization_options(), &root_path, + adapter.initialization_options(), background, - )?; - Ok(server) + ) })) } diff --git a/crates/language/src/tests.rs b/crates/language/src/tests.rs index 112073aed6..582410a9be 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}, @@ -76,43 +75,48 @@ fn test_edit_events(cx: &mut gpui::MutableAppContext) { let buffer1 = cx.add_model(|cx| Buffer::new(0, "abcdef", cx)); let buffer2 = cx.add_model(|cx| Buffer::new(1, "abcdef", cx)); - let buffer_ops = buffer1.update(cx, |buffer, cx| { - let buffer_1_events = buffer_1_events.clone(); - cx.subscribe(&buffer1, move |_, _, event, _| { - buffer_1_events.borrow_mut().push(event.clone()) - }) - .detach(); - let buffer_2_events = buffer_2_events.clone(); - cx.subscribe(&buffer2, move |_, _, event, _| { - buffer_2_events.borrow_mut().push(event.clone()) - }) - .detach(); + let buffer1_ops = Rc::new(RefCell::new(Vec::new())); + buffer1.update(cx, { + let buffer1_ops = buffer1_ops.clone(); + |buffer, cx| { + let buffer_1_events = buffer_1_events.clone(); + cx.subscribe(&buffer1, move |_, _, event, _| match event.clone() { + Event::Operation(op) => buffer1_ops.borrow_mut().push(op), + event @ _ => buffer_1_events.borrow_mut().push(event), + }) + .detach(); + let buffer_2_events = buffer_2_events.clone(); + cx.subscribe(&buffer2, move |_, _, event, _| { + buffer_2_events.borrow_mut().push(event.clone()) + }) + .detach(); - // An edit emits an edited event, followed by a dirtied event, - // since the buffer was previously in a clean state. - buffer.edit(Some(2..4), "XYZ", cx); + // An edit emits an edited event, followed by a dirtied event, + // since the buffer was previously in a clean state. + buffer.edit(Some(2..4), "XYZ", cx); - // An empty transaction does not emit any events. - buffer.start_transaction(); - buffer.end_transaction(cx); + // An empty transaction does not emit any events. + buffer.start_transaction(); + buffer.end_transaction(cx); - // A transaction containing two edits emits one edited event. - now += Duration::from_secs(1); - buffer.start_transaction_at(now); - buffer.edit(Some(5..5), "u", cx); - buffer.edit(Some(6..6), "w", cx); - buffer.end_transaction_at(now, cx); + // A transaction containing two edits emits one edited event. + now += Duration::from_secs(1); + buffer.start_transaction_at(now); + buffer.edit(Some(5..5), "u", cx); + buffer.edit(Some(6..6), "w", cx); + buffer.end_transaction_at(now, cx); - // Undoing a transaction emits one edited event. - buffer.undo(cx); - - buffer.operations.clone() + // Undoing a transaction emits one edited event. + buffer.undo(cx); + } }); // Incorporating a set of remote ops emits a single edited event, // followed by a dirtied event. buffer2.update(cx, |buffer, cx| { - buffer.apply_ops(buffer_ops, cx).unwrap(); + buffer + .apply_ops(buffer1_ops.borrow_mut().drain(..), cx) + .unwrap(); }); let buffer_1_events = buffer_1_events.borrow(); @@ -553,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(); @@ -1177,17 +603,26 @@ fn test_random_collaboration(cx: &mut MutableAppContext, mut rng: StdRng) { .collect::(); let mut replica_ids = Vec::new(); let mut buffers = Vec::new(); - let mut network = Network::new(rng.clone()); + let network = Rc::new(RefCell::new(Network::new(rng.clone()))); for i in 0..rng.gen_range(min_peers..=max_peers) { let buffer = cx.add_model(|cx| { let mut buffer = Buffer::new(i as ReplicaId, base_text.as_str(), cx); buffer.set_group_interval(Duration::from_millis(rng.gen_range(0..=200))); + let network = network.clone(); + cx.subscribe(&cx.handle(), move |buffer, _, event, _| { + if let Event::Operation(op) = event { + network + .borrow_mut() + .broadcast(buffer.replica_id(), vec![proto::serialize_operation(&op)]); + } + }) + .detach(); buffer }); buffers.push(buffer); replica_ids.push(i as ReplicaId); - network.add_peer(i as ReplicaId); + network.borrow_mut().add_peer(i as ReplicaId); log::info!("Adding initial peer with replica id {}", i); } @@ -1239,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 { @@ -1249,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; } @@ -1268,10 +705,20 @@ fn test_random_collaboration(cx: &mut MutableAppContext, mut rng: StdRng) { let mut new_buffer = Buffer::from_proto(new_replica_id, old_buffer, None, cx).unwrap(); new_buffer.set_group_interval(Duration::from_millis(rng.gen_range(0..=200))); + let network = network.clone(); + cx.subscribe(&cx.handle(), move |buffer, _, event, _| { + if let Event::Operation(op) = event { + network.borrow_mut().broadcast( + buffer.replica_id(), + vec![proto::serialize_operation(&op)], + ); + } + }) + .detach(); new_buffer })); replica_ids.push(new_replica_id); - network.replicate(replica_id, new_replica_id); + network.borrow_mut().replicate(replica_id, new_replica_id); } 60..=69 if mutation_count != 0 => { buffer.update(cx, |buffer, cx| { @@ -1280,8 +727,9 @@ fn test_random_collaboration(cx: &mut MutableAppContext, mut rng: StdRng) { }); mutation_count -= 1; } - _ if network.has_unreceived(replica_id) => { + _ if network.borrow().has_unreceived(replica_id) => { let ops = network + .borrow_mut() .receive(replica_id) .into_iter() .map(|op| proto::deserialize_operation(op).unwrap()); @@ -1297,14 +745,6 @@ fn test_random_collaboration(cx: &mut MutableAppContext, mut rng: StdRng) { _ => {} } - buffer.update(cx, |buffer, _| { - let ops = buffer - .operations - .drain(..) - .map(|op| proto::serialize_operation(&op)) - .collect(); - network.broadcast(buffer.replica_id(), ops); - }); now += Duration::from_millis(rng.gen_range(0..=200)); buffers.extend(new_buffer); @@ -1312,7 +752,7 @@ fn test_random_collaboration(cx: &mut MutableAppContext, mut rng: StdRng) { buffer.read(cx).check_invariants(); } - if mutation_count == 0 && network.is_idle() { + if mutation_count == 0 && network.borrow().is_idle() { break; } } @@ -1353,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/lsp/src/lsp.rs b/crates/lsp/src/lsp.rs index 5f3eb2caaf..d9024975e4 100644 --- a/crates/lsp/src/lsp.rs +++ b/crates/lsp/src/lsp.rs @@ -3,7 +3,7 @@ use collections::HashMap; use futures::{channel::oneshot, io::BufWriter, AsyncRead, AsyncWrite}; use gpui::{executor, Task}; use parking_lot::{Mutex, RwLock}; -use postage::{barrier, prelude::Stream, watch}; +use postage::{barrier, prelude::Stream}; use serde::{Deserialize, Serialize}; use serde_json::{json, value::RawValue, Value}; use smol::{ @@ -14,6 +14,7 @@ use smol::{ use std::{ future::Future, io::Write, + path::PathBuf, str::FromStr, sync::{ atomic::{AtomicUsize, Ordering::SeqCst}, @@ -34,13 +35,14 @@ type ResponseHandler = Box)>; pub struct LanguageServer { next_id: AtomicUsize, outbound_tx: channel::Sender>, - capabilities: watch::Receiver>, + capabilities: ServerCapabilities, notification_handlers: Arc>>, response_handlers: Arc>>, executor: Arc, io_tasks: Mutex>, Task>)>>, - initialized: barrier::Receiver, output_done_rx: Mutex>, + root_path: PathBuf, + options: Option, } pub struct Subscription { @@ -103,10 +105,10 @@ impl LanguageServer { pub fn new( binary_path: &Path, args: &[&str], - options: Option, root_path: &Path, + options: Option, background: Arc, - ) -> Result> { + ) -> Result { let mut server = Command::new(binary_path) .current_dir(root_path) .args(args) @@ -116,7 +118,9 @@ impl LanguageServer { .spawn()?; let stdin = server.stdin.take().unwrap(); let stdout = server.stdout.take().unwrap(); - Self::new_internal(stdin, stdout, root_path, options, background) + Ok(Self::new_internal( + stdin, stdout, root_path, options, background, + )) } fn new_internal( @@ -125,7 +129,7 @@ impl LanguageServer { root_path: &Path, options: Option, executor: Arc, - ) -> Result> + ) -> Self where Stdin: AsyncWrite + Unpin + Send + 'static, Stdout: AsyncRead + Unpin + Send + 'static, @@ -215,42 +219,24 @@ impl LanguageServer { .log_err() }); - let (initialized_tx, initialized_rx) = barrier::channel(); - let (mut capabilities_tx, capabilities_rx) = watch::channel(); - let this = Arc::new(Self { + Self { notification_handlers, response_handlers, - capabilities: capabilities_rx, + capabilities: Default::default(), next_id: Default::default(), outbound_tx, executor: executor.clone(), io_tasks: Mutex::new(Some((input_task, output_task))), - initialized: initialized_rx, output_done_rx: Mutex::new(Some(output_done_rx)), - }); - - let root_uri = Url::from_file_path(root_path).map_err(|_| anyhow!("invalid root path"))?; - executor - .spawn({ - let this = this.clone(); - async move { - if let Some(capabilities) = this.init(root_uri, options).log_err().await { - *capabilities_tx.borrow_mut() = Some(capabilities); - } - - drop(initialized_tx); - } - }) - .detach(); - - Ok(this) + root_path: root_path.to_path_buf(), + options, + } } - async fn init( - self: Arc, - root_uri: Url, - options: Option, - ) -> Result { + pub async fn initialize(mut self) -> Result> { + let options = self.options.take(); + let mut this = Arc::new(self); + let root_uri = Url::from_file_path(&this.root_path).unwrap(); #[allow(deprecated)] let params = InitializeParams { process_id: Default::default(), @@ -305,19 +291,10 @@ impl LanguageServer { locale: Default::default(), }; - let this = self.clone(); - let request = Self::request_internal::( - &this.next_id, - &this.response_handlers, - &this.outbound_tx, - params, - ); - let response = request.await?; - Self::notify_internal::( - &this.outbound_tx, - InitializedParams {}, - )?; - Ok(response.capabilities) + let response = this.request::(params).await?; + Arc::get_mut(&mut this).unwrap().capabilities = response.capabilities; + this.notify::(InitializedParams {})?; + Ok(this) } pub fn shutdown(&self) -> Option>> { @@ -352,7 +329,7 @@ impl LanguageServer { } } - pub fn on_notification(&self, mut f: F) -> Subscription + pub fn on_notification(&mut self, mut f: F) -> Subscription where T: notification::Notification, F: 'static + Send + Sync + FnMut(T::Params), @@ -378,16 +355,8 @@ impl LanguageServer { } } - pub fn capabilities(&self) -> impl 'static + Future> { - let mut rx = self.capabilities.clone(); - async move { - loop { - let value = rx.recv().await?; - if value.is_some() { - return value; - } - } - } + pub fn capabilities<'a>(self: &'a Arc) -> &'a ServerCapabilities { + &self.capabilities } pub fn request( @@ -397,17 +366,12 @@ impl LanguageServer { where T::Result: 'static + Send, { - let this = self.clone(); - async move { - this.initialized.clone().recv().await; - Self::request_internal::( - &this.next_id, - &this.response_handlers, - &this.outbound_tx, - params, - ) - .await - } + Self::request_internal::( + &self.next_id, + &self.response_handlers, + &self.outbound_tx, + params, + ) } fn request_internal( @@ -452,16 +416,8 @@ impl LanguageServer { } } - pub fn notify( - self: &Arc, - params: T::Params, - ) -> impl Future> { - let this = self.clone(); - async move { - this.initialized.clone().recv().await; - Self::notify_internal::(&this.outbound_tx, params)?; - Ok(()) - } + pub fn notify(&self, params: T::Params) -> Result<()> { + Self::notify_internal::(&self.outbound_tx, params) } fn notify_internal( @@ -530,14 +486,14 @@ impl LanguageServer { } } - pub fn fake(cx: &mut gpui::MutableAppContext) -> (Arc, FakeLanguageServer) { + pub fn fake(cx: &mut gpui::MutableAppContext) -> (Self, FakeLanguageServer) { Self::fake_with_capabilities(Self::full_capabilities(), cx) } pub fn fake_with_capabilities( capabilities: ServerCapabilities, cx: &mut gpui::MutableAppContext, - ) -> (Arc, FakeLanguageServer) { + ) -> (Self, FakeLanguageServer) { let (stdin_writer, stdin_reader) = async_pipe::pipe(); let (stdout_writer, stdout_reader) = async_pipe::pipe(); @@ -550,15 +506,9 @@ impl LanguageServer { } }); - let server = Self::new_internal( - stdin_writer, - stdout_reader, - Path::new("/"), - None, - cx.background().clone(), - ) - .unwrap(); - + let executor = cx.background().clone(); + let server = + Self::new_internal(stdin_writer, stdout_reader, Path::new("/"), None, executor); (server, fake) } } @@ -584,6 +534,7 @@ impl FakeLanguageServer { let mut stdin = smol::io::BufReader::new(stdin); while Self::receive(&mut stdin, &mut buffer).await.is_ok() { cx.background().simulate_random_delay().await; + if let Ok(request) = serde_json::from_slice::(&buffer) { assert_eq!(request.jsonrpc, JSON_RPC_VERSION); @@ -639,7 +590,7 @@ impl FakeLanguageServer { } } - pub async fn notify(&mut self, params: T::Params) { + pub fn notify(&mut self, params: T::Params) { let message = serde_json::to_vec(&Notification { jsonrpc: JSON_RPC_VERSION, method: T::METHOD, @@ -704,16 +655,14 @@ impl FakeLanguageServer { self.notify::(ProgressParams { token: NumberOrString::String(token.into()), value: ProgressParamsValue::WorkDone(WorkDoneProgress::Begin(Default::default())), - }) - .await; + }); } pub async fn end_progress(&mut self, token: impl Into) { self.notify::(ProgressParams { token: NumberOrString::String(token.into()), value: ProgressParamsValue::WorkDone(WorkDoneProgress::End(Default::default())), - }) - .await; + }); } async fn receive( @@ -758,7 +707,7 @@ mod tests { #[gpui::test] async fn test_fake(cx: &mut TestAppContext) { - let (server, mut fake) = cx.update(LanguageServer::fake); + let (mut server, mut fake) = cx.update(LanguageServer::fake); let (message_tx, message_rx) = channel::unbounded(); let (diagnostics_tx, diagnostics_rx) = channel::unbounded(); @@ -773,6 +722,7 @@ mod tests { }) .detach(); + let server = server.initialize().await.unwrap(); server .notify::(DidOpenTextDocumentParams { text_document: TextDocumentItem::new( @@ -782,7 +732,6 @@ mod tests { "".to_string(), ), }) - .await .unwrap(); assert_eq!( fake.receive_notification::() @@ -796,14 +745,12 @@ mod tests { fake.notify::(ShowMessageParams { typ: MessageType::ERROR, message: "ok".to_string(), - }) - .await; + }); fake.notify::(PublishDiagnosticsParams { uri: Url::from_str("file://b/c").unwrap(), version: Some(5), diagnostics: vec![], - }) - .await; + }); assert_eq!(message_rx.recv().await.unwrap().message, "ok"); assert_eq!( diagnostics_rx.recv().await.unwrap().uri.as_str(), @@ -815,16 +762,4 @@ mod tests { drop(server); fake.receive_notification::().await; } - - pub enum ServerStatusNotification {} - - impl notification::Notification for ServerStatusNotification { - type Params = ServerStatusParams; - const METHOD: &'static str = "experimental/serverStatus"; - } - - #[derive(Deserialize, Serialize, PartialEq, Eq, Clone)] - pub struct ServerStatusParams { - pub quiescent: bool, - } } 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..4867ada7cb 100644 --- a/crates/project/src/lsp_command.rs +++ b/crates/project/src/lsp_command.rs @@ -223,21 +223,21 @@ 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.read(cx), 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 +343,16 @@ impl LspCommand for GetDefinition { mut cx: AsyncAppContext, ) -> Result> { let mut definitions = Vec::new(); - let (language, language_server) = buffer - .read_with(&cx, |buffer, _| { - buffer - .language() + let language_server = project + .read_with(&cx, |project, cx| { + project + .language_server_for_buffer(buffer.read(cx), cx) .cloned() - .zip(buffer.language_server().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 +377,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 +521,16 @@ impl LspCommand for GetReferences { mut cx: AsyncAppContext, ) -> Result> { let mut references = Vec::new(); - let (language, language_server) = buffer - .read_with(&cx, |buffer, _| { - buffer - .language() + let language_server = project + .read_with(&cx, |project, cx| { + project + .language_server_for_buffer(buffer.read(cx), cx) .cloned() - .zip(buffer.language_server().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 +538,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 694755274d..9a45e08164 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, 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, } @@ -201,7 +203,6 @@ impl Project { client.add_entity_message_handler(Self::handle_add_collaborator); client.add_entity_message_handler(Self::handle_buffer_reloaded); client.add_entity_message_handler(Self::handle_buffer_saved); - client.add_entity_message_handler(Self::handle_close_buffer); client.add_entity_message_handler(Self::handle_disk_based_diagnostics_updated); client.add_entity_message_handler(Self::handle_disk_based_diagnostics_updating); client.add_entity_message_handler(Self::handle_remove_collaborator); @@ -286,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, @@ -372,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 { @@ -723,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) } @@ -798,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) }) } @@ -839,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>> { @@ -891,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_server(&buffer, cx); }); Ok(()) }) @@ -917,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(); @@ -945,74 +942,226 @@ impl Project { remote_id ))?, } - self.assign_language_to_buffer(&buffer, worktree, cx); + cx.subscribe(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_server(buffer, cx); + Ok(()) } - fn assign_language_to_buffer( + fn register_buffer_with_language_server( &mut self, - buffer: &ModelHandle, - worktree: Option<&ModelHandle>, + buffer_handle: &ModelHandle, cx: &mut ModelContext, - ) -> Option<()> { - let (path, full_path) = { - let file = buffer.read(cx).file()?; - (file.path().clone(), file.full_path(cx)) - }; + ) { + let buffer = buffer_handle.read(cx); + let buffer_id = buffer.remote_id(); + if let Some(file) = File::from_dyn(buffer.file()) { + if file.is_local() { + let uri = lsp::Url::from_file_path(file.abs_path(cx)).unwrap(); + let initial_snapshot = buffer.text_snapshot(); + let language_server = self.language_server_for_buffer(buffer, cx).cloned(); - // 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); - }); + 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(); + } + } - // 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); + if let Some(server) = language_server { + server + .notify::( + lsp::DidOpenTextDocumentParams { + text_document: lsp::TextDocumentItem::new( + uri, + Default::default(), + 0, + initial_snapshot.text(), + ), + } + .clone(), + ) + .log_err(); + buffer_handle.update(cx, |buffer, cx| { + buffer.set_completion_triggers( + server + .capabilities() + .completion_provider + .as_ref() + .and_then(|provider| provider.trigger_characters.clone()) + .unwrap_or(Vec::new()), + cx, + ) + }); + self.buffer_snapshots + .insert(buffer_id, vec![(0, initial_snapshot)]); + } - 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); - }); + cx.observe_release(buffer_handle, |this, buffer, cx| { + if let Some(file) = File::from_dyn(buffer.file()) { + if file.is_local() { + let uri = lsp::Url::from_file_path(file.abs_path(cx)).unwrap(); + if let Some(server) = this.language_server_for_buffer(buffer, cx) { + server + .notify::( + lsp::DidCloseTextDocumentParams { + text_document: lsp::TextDocumentIdentifier::new( + uri.clone(), + ), + }, + ) + .log_err(); + } } } }) .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(); + fn on_buffer_event( + &mut self, + buffer: ModelHandle, + event: &BufferEvent, + cx: &mut ModelContext, + ) -> Option<()> { + match event { + BufferEvent::Operation(operation) => { + 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 language_server = self + .language_server_for_buffer(buffer.read(cx), cx)? + .clone(); + let buffer = buffer.read(cx); + let file = File::from_dyn(buffer.file())?; + 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(); + + buffer_snapshots.push((next_version, next_snapshot)); + + language_server + .notify::( + lsp::DidChangeTextDocumentParams { + text_document: lsp::VersionedTextDocumentIdentifier::new( + uri, + next_version, + ), + content_changes, + }, + ) + .log_err(); + } + BufferEvent::Saved => { + 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(), + }; + + for (_, server) in self.language_servers_for_worktree(worktree_id) { + server + .notify::( + lsp::DidSaveTextDocumentParams { + text_document: text_document.clone(), + text: None, + }, + ) + .log_err(); + } + } + _ => {} } None } + fn language_servers_for_worktree( + &self, + worktree_id: WorktreeId, + ) -> impl Iterator)> { + self.language_servers.iter().filter_map( + move |((language_server_worktree_id, language_name), server)| { + if *language_server_worktree_id == worktree_id { + Some((language_name.as_ref(), server)) + } else { + None + } + }, + ) + } + + fn assign_language_to_buffer( + &mut self, + buffer: &ModelHandle, + cx: &mut ModelContext, + ) -> Option<()> { + // 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); + }); + + 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 + } + fn start_language_server( &mut self, worktree_id: WorktreeId, 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(|| { @@ -1024,12 +1173,8 @@ 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 mut language_server = language_server?.await.log_err()?; + let this = this.upgrade(&cx)?; let disk_based_sources = language .disk_based_diagnostic_sources() @@ -1101,50 +1246,116 @@ impl Project { .detach(); // Process all the LSP events. - cx.spawn(|mut cx| async move { - while let Ok(message) = diagnostics_rx.recv().await { - let this = this.upgrade(&cx)?; - match message { - LspEvent::DiagnosticsStart => { - this.update(&mut cx, |this, cx| { - this.disk_based_diagnostics_started(cx); - if let Some(project_id) = this.remote_id() { - rpc.send(proto::DiskBasedDiagnosticsUpdating { - project_id, - }) + cx.spawn(|mut cx| { + let this = this.downgrade(); + async move { + while let Ok(message) = diagnostics_rx.recv().await { + let this = this.upgrade(&cx)?; + match message { + LspEvent::DiagnosticsStart => { + this.update(&mut cx, |this, cx| { + this.disk_based_diagnostics_started(cx); + if let Some(project_id) = this.remote_id() { + rpc.send(proto::DiskBasedDiagnosticsUpdating { + project_id, + }) + .log_err(); + } + }); + } + LspEvent::DiagnosticsUpdate(mut params) => { + language.process_diagnostics(&mut params); + this.update(&mut cx, |this, cx| { + this.update_diagnostics( + params, + &disk_based_sources, + cx, + ) .log_err(); - } - }); - } - LspEvent::DiagnosticsUpdate(mut params) => { - language.process_diagnostics(&mut params); - this.update(&mut cx, |this, cx| { - this.update_diagnostics(params, &disk_based_sources, cx) - .log_err(); - }); - } - LspEvent::DiagnosticsFinish => { - this.update(&mut cx, |this, cx| { - this.disk_based_diagnostics_finished(cx); - if let Some(project_id) = this.remote_id() { - rpc.send(proto::DiskBasedDiagnosticsUpdated { - project_id, - }) - .log_err(); - } - }); + }); + } + LspEvent::DiagnosticsFinish => { + this.update(&mut cx, |this, cx| { + this.disk_based_diagnostics_finished(cx); + if let Some(project_id) = this.remote_id() { + rpc.send(proto::DiskBasedDiagnosticsUpdated { + project_id, + }) + .log_err(); + } + }); + } } } + Some(()) } - Some(()) }) .detach(); + let language_server = language_server.initialize().await.log_err()?; + this.update(&mut cx, |this, cx| { + this.language_servers + .insert(key.clone(), language_server.clone()); + + // Tell the language server about every open buffer in the worktree that matches the language. + for buffer in this.opened_buffers.values() { + if let Some(buffer_handle) = buffer.upgrade(cx) { + let buffer = buffer_handle.read(cx); + let file = if let Some(file) = File::from_dyn(buffer.file()) { + file + } else { + continue; + }; + let language = if let Some(language) = buffer.language() { + language + } else { + continue; + }; + if (file.worktree.read(cx).id(), language.name()) != key { + continue; + } + + let 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(); + language_server + .notify::( + lsp::DidOpenTextDocumentParams { + text_document: lsp::TextDocumentItem::new( + uri, + Default::default(), + *version, + initial_snapshot.text(), + ), + }, + ) + .log_err()?; + buffer_handle.update(cx, |buffer, cx| { + buffer.set_completion_triggers( + language_server + .capabilities() + .completion_provider + .as_ref() + .and_then(|provider| { + provider.trigger_characters.clone() + }) + .unwrap_or(Vec::new()), + cx, + ) + }); + } + } + + Some(()) + }); + Some(language_server) }) - .shared() - }) - .clone() + }); } pub fn update_diagnostics( @@ -1274,9 +1485,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; } } @@ -1291,6 +1500,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>, @@ -1301,25 +1594,11 @@ impl Project { let mut remote_buffers = None; for buffer_handle in buffers { let buffer = buffer_handle.read(cx); - let worktree; if let Some(file) = File::from_dyn(buffer.file()) { - worktree = file.worktree.clone(); if let Some(buffer_abs_path) = file.as_local().map(|f| f.abs_path(cx)) { - let lang_server; - if let Some(lang) = buffer.language() { - if let Some(server) = self - .language_servers - .get(&(worktree.read(cx).id(), lang.name().to_string())) - { - lang_server = server.clone(); - } else { - return Task::ready(Ok(Default::default())); - }; - } else { - return Task::ready(Ok(Default::default())); + if let Some(server) = self.language_server_for_buffer(buffer, cx) { + local_buffers.push((buffer_handle, buffer_abs_path, server.clone())); } - - local_buffers.push((buffer_handle, buffer_abs_path, lang_server)); } else { remote_buffers.get_or_insert(Vec::new()).push(buffer_handle); } @@ -1353,21 +1632,17 @@ impl Project { .await?; } - for (buffer, buffer_abs_path, lang_server) in local_buffers { - let capabilities = if let Some(capabilities) = lang_server.capabilities().await { - capabilities - } else { - continue; - }; - + for (buffer, buffer_abs_path, language_server) in local_buffers { let text_document = lsp::TextDocumentIdentifier::new( lsp::Url::from_file_path(&buffer_abs_path).unwrap(), ); + let capabilities = &language_server.capabilities(); let lsp_edits = if capabilities .document_formatting_provider - .map_or(false, |provider| provider != lsp::OneOf::Left(false)) + .as_ref() + .map_or(false, |provider| *provider != lsp::OneOf::Left(false)) { - lang_server + language_server .request::(lsp::DocumentFormattingParams { text_document, options: Default::default(), @@ -1376,13 +1651,14 @@ impl Project { .await? } else if capabilities .document_range_formatting_provider - .map_or(false, |provider| provider != lsp::OneOf::Left(false)) + .as_ref() + .map_or(false, |provider| *provider != lsp::OneOf::Left(false)) { let buffer_start = lsp::Position::new(0, 0); let buffer_end = buffer .read_with(&cx, |buffer, _| buffer.max_point_utf16()) .to_lsp_position(); - lang_server + language_server .request::( lsp::DocumentRangeFormattingParams { text_document, @@ -1397,9 +1673,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| { @@ -1564,10 +1840,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!( @@ -1593,7 +1869,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, ) @@ -1637,11 +1913,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, cx) { + server.clone() + } else { + return Task::ready(Ok(Default::default())); + }; cx.spawn(|_, cx| async move { let completions = lang_server @@ -1748,19 +2025,21 @@ 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() + let lang_server = if let Some(server) = self.language_server_for_buffer(buffer, cx) { + server.clone() } else { - return Task::ready(Err(anyhow!("buffer does not have a language server"))); + 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(); @@ -1837,34 +2116,18 @@ impl Project { if worktree.read(cx).as_local().is_some() { let buffer_abs_path = buffer_abs_path.unwrap(); - let lang_name; - let lang_server; - if let Some(lang) = buffer.language() { - lang_name = lang.name().to_string(); - if let Some(server) = self - .language_servers - .get(&(worktree.read(cx).id(), lang_name.clone())) - { - lang_server = server.clone(); - } else { - return Task::ready(Ok(Default::default())); - }; + let lang_server = if let Some(server) = self.language_server_for_buffer(buffer, cx) { + server.clone() } else { return Task::ready(Ok(Default::default())); - } + }; let lsp_range = lsp::Range::new( range.start.to_point_utf16(buffer).to_lsp_position(), range.end.to_point_utf16(buffer).to_lsp_position(), ); cx.foreground().spawn(async move { - if !lang_server - .capabilities() - .await - .map_or(false, |capabilities| { - capabilities.code_action_provider.is_some() - }) - { + if !lang_server.capabilities().code_action_provider.is_some() { return Ok(Default::default()); } @@ -1941,14 +2204,14 @@ 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() + let lang_server = if let Some(server) = self.language_server_for_buffer(buffer, cx) { + server.clone() } else { - return Task::ready(Err(anyhow!("buffer does not have a language server"))); + return Task::ready(Ok(Default::default())); }; let range = action.range.to_point_utf16(buffer); @@ -2022,7 +2285,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 { @@ -2106,13 +2369,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?; @@ -2389,16 +2657,12 @@ 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, cx).cloned()) + { let lsp_params = request.to_lsp(&file.abs_path(cx), cx); return cx.spawn(|this, cx| async move { - if !language_server - .capabilities() - .await - .map_or(false, |capabilities| { - request.check_capabilities(&capabilities) - }) - { + if !request.check_capabilities(&language_server.capabilities()) { return Ok(Default::default()); } @@ -2550,7 +2814,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(); @@ -3389,9 +3653,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) @@ -3429,16 +3691,6 @@ impl Project { }) } - async fn handle_close_buffer( - _: ModelHandle, - _: TypedEnvelope, - _: Arc, - _: AsyncAppContext, - ) -> Result<()> { - // TODO: use this for following - Ok(()) - } - async fn handle_buffer_saved( this: ModelHandle, envelope: TypedEnvelope, @@ -3528,6 +3780,160 @@ 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)).text_snapshot()) + } + } + + fn language_server_for_buffer( + &self, + buffer: &Buffer, + cx: &AppContext, + ) -> Option<&Arc> { + 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 { @@ -3760,7 +4166,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; @@ -3833,7 +4240,229 @@ 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 (mut rust_lsp_config, mut fake_rust_servers) = LanguageServerConfig::fake(); + let (mut json_lsp_config, mut fake_json_servers) = LanguageServerConfig::fake(); + rust_lsp_config.set_fake_capabilities(lsp::ServerCapabilities { + completion_provider: Some(lsp::CompletionOptions { + trigger_characters: Some(vec![".".to_string(), "::".to_string()]), + ..Default::default() + }), + ..Default::default() + }); + json_lsp_config.set_fake_capabilities(lsp::ServerCapabilities { + completion_provider: Some(lsp::CompletionOptions { + trigger_characters: Some(vec![":".to_string()]), + ..Default::default() + }), + ..Default::default() + }); + + let rust_language = Arc::new(Language::new( + LanguageConfig { + name: "Rust".into(), + path_suffixes: vec!["rs".to_string()], + language_server: Some(rust_lsp_config), + ..Default::default() + }, + Some(tree_sitter_rust::language()), + )); + 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;", + "test2.rs": "", + "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 Rust files. + 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/test.rs").unwrap(), + version: 0, + text: "const A: i32 = 1;".to_string(), + language_id: Default::default() + } + ); + + // The buffer is configured based on the language server's capabilities. + rust_buffer.read_with(cx, |buffer, _| { + assert_eq!( + buffer.completion_triggers(), + &[".".to_string(), "::".to_string()] + ); + }); + toml_buffer.read_with(cx, |buffer, _| { + assert!(buffer.completion_triggers().is_empty()); + }); + + // 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/package.json").unwrap(), + version: 0, + text: "{\"a\": 1}".to_string(), + language_id: Default::default() + } + ); + + // This buffer is configured based on the second language server's + // capabilities. + json_buffer.read_with(cx, |buffer, _| { + assert_eq!(buffer.completion_triggers(), &[":".to_string()]); + }); + + // When opening another buffer whose language server is already running, + // it is also configured based on the existing language server's capabilities. + let rust_buffer2 = project + .update(cx, |project, cx| { + project.open_buffer((worktree_id, "test2.rs"), cx) + }) + .await + .unwrap(); + rust_buffer2.read_with(cx, |buffer, _| { + assert_eq!( + buffer.completion_triggers(), + &[".".to_string(), "::".to_string()] + ); + }); + + // Changes are reported only to servers matching the buffer's language. + toml_buffer.update(cx, |buffer, cx| buffer.edit([5..5], "23", cx)); + rust_buffer2.update(cx, |buffer, cx| buffer.edit([0..0], "let x = 1;", cx)); + assert_eq!( + fake_rust_server + .receive_notification::() + .await + .text_document, + lsp::VersionedTextDocumentIdentifier::new( + lsp::Url::from_file_path("/the-root/test2.rs").unwrap(), + 1 + ) + ); + + // Save notifications are reported to all servers. + toml_buffer + .update(cx, |buffer, cx| buffer.save(cx)) + .await + .unwrap(); + assert_eq!( + fake_rust_server + .receive_notification::() + .await + .text_document, + lsp::TextDocumentIdentifier::new( + lsp::Url::from_file_path("/the-root/Cargo.toml").unwrap() + ) + ); + assert_eq!( + fake_json_server + .receive_notification::() + .await + .text_document, + lsp::TextDocumentIdentifier::new( + lsp::Url::from_file_path("/the-root/Cargo.toml").unwrap() + ) + ); + + // Close notifications are reported only to servers matching the buffer's language. + 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, + ); + } + + #[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 @@ -3861,9 +4490,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| { @@ -3897,8 +4524,8 @@ mod tests { fake_server.end_progress(&progress_token).await; fake_server.start_progress(&progress_token).await; - fake_server - .notify::(lsp::PublishDiagnosticsParams { + fake_server.notify::( + lsp::PublishDiagnosticsParams { uri: Url::from_file_path("/dir/a.rs").unwrap(), version: None, diagnostics: vec![lsp::Diagnostic { @@ -3907,8 +4534,8 @@ mod tests { message: "undefined variable 'A'".to_string(), ..Default::default() }], - }) - .await; + }, + ); assert_eq!( events.next().await.unwrap(), Event::DiagnosticsUpdated((worktree_id, Path::new("a.rs")).into()) @@ -3951,6 +4578,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() + }, + ], + }, + ); + + // 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() + }, + ], + }, + ); + + 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() + }, + ], + }, + ); + + 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!({ @@ -4452,7 +5772,10 @@ mod tests { buffer1.update(cx, |buffer, cx| { cx.subscribe(&buffer1, { let events = events.clone(); - move |_, _, event, _| events.borrow_mut().push(event.clone()) + move |_, _, event, _| match event { + BufferEvent::Operation(_) => {} + _ => events.borrow_mut().push(event.clone()), + } }) .detach(); @@ -4655,6 +5978,8 @@ mod tests { #[gpui::test] async fn test_grouped_diagnostics(cx: &mut gpui::TestAppContext) { + cx.foreground().forbid_parking(); + let fs = FakeFs::new(cx.background()); fs.insert_tree( "/the-dir", @@ -4914,6 +6239,8 @@ mod tests { #[gpui::test] async fn test_rename(cx: &mut gpui::TestAppContext) { + cx.foreground().forbid_parking(); + let (language_server_config, mut fake_servers) = LanguageServerConfig::fake(); let language = Arc::new(Language::new( LanguageConfig { diff --git a/crates/project/src/worktree.rs b/crates/project/src/worktree.rs index 78dac23681..1130063c98 100644 --- a/crates/project/src/worktree.rs +++ b/crates/project/src/worktree.rs @@ -19,7 +19,7 @@ use gpui::{ }; use language::{ proto::{deserialize_version, serialize_version}, - Buffer, DiagnosticEntry, Operation, PointUtf16, Rope, + Buffer, DiagnosticEntry, PointUtf16, Rope, }; use lazy_static::lazy_static; use parking_lot::Mutex; @@ -71,7 +71,6 @@ pub struct LocalWorktree { share: Option, diagnostics: HashMap, Vec>>, diagnostic_summaries: TreeMap, - queued_operations: Vec<(u64, Operation)>, client: Arc, fs: Arc, visible: bool, @@ -84,7 +83,6 @@ pub struct RemoteWorktree { client: Arc, updates_tx: UnboundedSender, replica_id: ReplicaId, - queued_operations: Vec<(u64, Operation)>, diagnostic_summaries: TreeMap, visible: bool, } @@ -226,7 +224,6 @@ impl Worktree { snapshot_rx: snapshot_rx.clone(), updates_tx, client: client.clone(), - queued_operations: Default::default(), diagnostic_summaries: TreeMap::from_ordered_entries( worktree.diagnostic_summaries.into_iter().map(|summary| { ( @@ -420,42 +417,6 @@ impl Worktree { cx.notify(); } - - fn send_buffer_update( - &mut self, - buffer_id: u64, - operation: Operation, - cx: &mut ModelContext, - ) { - if let Some((project_id, rpc)) = match self { - Worktree::Local(worktree) => worktree - .share - .as_ref() - .map(|share| (share.project_id, worktree.client.clone())), - Worktree::Remote(worktree) => Some((worktree.project_id, worktree.client.clone())), - } { - cx.spawn(|worktree, mut cx| async move { - if let Err(error) = rpc - .request(proto::UpdateBuffer { - project_id, - buffer_id, - operations: vec![language::proto::serialize_operation(&operation)], - }) - .await - { - worktree.update(&mut cx, |worktree, _| { - log::error!("error sending buffer operation: {}", error); - match worktree { - Worktree::Local(t) => &mut t.queued_operations, - Worktree::Remote(t) => &mut t.queued_operations, - } - .push((buffer_id, operation)); - }); - } - }) - .detach(); - } - } } impl LocalWorktree { @@ -526,7 +487,6 @@ impl LocalWorktree { poll_task: None, diagnostics: Default::default(), diagnostic_summaries: Default::default(), - queued_operations: Default::default(), client, fs, visible, @@ -1455,26 +1415,6 @@ impl language::File for File { }) } - fn buffer_updated(&self, buffer_id: u64, operation: Operation, cx: &mut MutableAppContext) { - self.worktree.update(cx, |worktree, cx| { - worktree.send_buffer_update(buffer_id, operation, cx); - }); - } - - fn buffer_removed(&self, buffer_id: u64, cx: &mut MutableAppContext) { - self.worktree.update(cx, |worktree, _| { - if let Worktree::Remote(worktree) = worktree { - worktree - .client - .send(proto::CloseBuffer { - project_id: worktree.project_id, - buffer_id, - }) - .log_err(); - } - }); - } - fn as_any(&self) -> &dyn Any { self } diff --git a/crates/rpc/proto/zed.proto b/crates/rpc/proto/zed.proto index 377aef6946..7f43aaff1a 100644 --- a/crates/rpc/proto/zed.proto +++ b/crates/rpc/proto/zed.proto @@ -42,7 +42,6 @@ message Envelope { OpenBuffer open_buffer = 35; OpenBufferResponse open_buffer_response = 36; - CloseBuffer close_buffer = 37; UpdateBuffer update_buffer = 38; UpdateBufferFile update_buffer_file = 39; SaveBuffer save_buffer = 40; diff --git a/crates/rpc/src/proto.rs b/crates/rpc/src/proto.rs index ffb74f4939..d252decb3a 100644 --- a/crates/rpc/src/proto.rs +++ b/crates/rpc/src/proto.rs @@ -146,7 +146,6 @@ messages!( (BufferReloaded, Foreground), (BufferSaved, Foreground), (ChannelMessageSent, Foreground), - (CloseBuffer, Foreground), (DiskBasedDiagnosticsUpdated, Background), (DiskBasedDiagnosticsUpdating, Background), (Error, Foreground), @@ -247,7 +246,6 @@ entity_messages!( ApplyCompletionAdditionalEdits, BufferReloaded, BufferSaved, - CloseBuffer, DiskBasedDiagnosticsUpdated, DiskBasedDiagnosticsUpdating, FormatBuffers, 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 d05c9e2401..2b954b5774 100644 --- a/crates/server/src/rpc.rs +++ b/crates/server/src/rpc.rs @@ -102,7 +102,6 @@ impl Server { .add_request_handler(Server::forward_project_request::) .add_request_handler(Server::forward_project_request::) .add_request_handler(Server::forward_project_request::) - .add_message_handler(Server::close_buffer) .add_request_handler(Server::update_buffer) .add_message_handler(Server::update_buffer_file) .add_message_handler(Server::buffer_reloaded) @@ -581,19 +580,6 @@ impl Server { .await?) } - async fn close_buffer( - self: Arc, - request: TypedEnvelope, - ) -> tide::Result<()> { - let host_connection_id = self - .state() - .read_project(request.payload.project_id, request.sender_id)? - .host_connection_id; - self.peer - .forward_send(request.sender_id, host_connection_id, request.payload)?; - Ok(()) - } - async fn save_buffer( self: Arc, request: TypedEnvelope, @@ -1025,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; @@ -1034,6 +1020,7 @@ mod tests { use project::{ fs::{FakeFs, Fs as _}, search::SearchQuery, + worktree::WorktreeHandle, DiagnosticSummary, Project, ProjectPath, }; use rand::prelude::*; @@ -1411,6 +1398,8 @@ mod tests { buffer_b.read_with(cx_b, |buf, _| assert!(!buf.is_dirty())); buffer_c.condition(cx_c, |buf, _| !buf.is_dirty()).await; + worktree_a.flush_fs_events(cx_a).await; + // Make changes on host's file system, see those changes on guest worktrees. fs.rename( "/a/file1".as_ref(), @@ -1844,13 +1833,13 @@ mod tests { // Client A sees that a guest has joined. project_a - .condition(&cx_a, |p, _| p.collaborators().len() == 1) + .condition(cx_a, |p, _| p.collaborators().len() == 1) .await; // Drop client B's connection and ensure client A observes client B leaving the project. client_b.disconnect(&cx_b.to_async()).unwrap(); project_a - .condition(&cx_a, |p, _| p.collaborators().len() == 0) + .condition(cx_a, |p, _| p.collaborators().len() == 0) .await; // Rejoin the project as client B @@ -1867,14 +1856,15 @@ mod tests { // Client A sees that a guest has re-joined. project_a - .condition(&cx_a, |p, _| p.collaborators().len() == 1) + .condition(cx_a, |p, _| p.collaborators().len() == 1) .await; // Simulate connection loss for client B and ensure client A observes client B leaving the project. + client_b.wait_for_current_user(cx_b).await; server.disconnect_client(client_b.current_user_id(cx_b)); cx_a.foreground().advance_clock(Duration::from_secs(3)); project_a - .condition(&cx_a, |p, _| p.collaborators().len() == 0) + .condition(cx_a, |p, _| p.collaborators().len() == 0) .await; } @@ -1956,7 +1946,10 @@ mod tests { // Simulate a language server reporting errors for a file. let mut fake_language_server = fake_language_servers.next().await.unwrap(); fake_language_server - .notify::(lsp::PublishDiagnosticsParams { + .receive_notification::() + .await; + fake_language_server.notify::( + lsp::PublishDiagnosticsParams { uri: lsp::Url::from_file_path("/a/a.rs").unwrap(), version: None, diagnostics: vec![lsp::Diagnostic { @@ -1965,8 +1958,8 @@ mod tests { message: "message 1".to_string(), ..Default::default() }], - }) - .await; + }, + ); // Wait for server to see the diagnostics update. server @@ -2015,8 +2008,8 @@ mod tests { }); // Simulate a language server reporting more errors for a file. - fake_language_server - .notify::(lsp::PublishDiagnosticsParams { + fake_language_server.notify::( + lsp::PublishDiagnosticsParams { uri: lsp::Url::from_file_path("/a/a.rs").unwrap(), version: None, diagnostics: vec![ @@ -2036,8 +2029,8 @@ mod tests { ..Default::default() }, ], - }) - .await; + }, + ); // Client b gets the updated summaries project_b @@ -2381,10 +2374,6 @@ mod tests { .await .unwrap(); - let format = project_b.update(cx_b, |project, cx| { - project.format(HashSet::from_iter([buffer_b.clone()]), true, cx) - }); - let mut fake_language_server = fake_language_servers.next().await.unwrap(); fake_language_server.handle_request::(|_, _| { Some(vec![ @@ -2399,7 +2388,12 @@ mod tests { ]) }); - format.await.unwrap(); + project_b + .update(cx_b, |project, cx| { + project.format(HashSet::from_iter([buffer_b.clone()]), true, cx) + }) + .await + .unwrap(); assert_eq!( buffer_b.read_with(cx_b, |buffer, _| buffer.text()), "let honey = two" @@ -2489,8 +2483,6 @@ mod tests { .unwrap(); // Request the definition of a symbol as the guest. - let definitions_1 = project_b.update(cx_b, |p, cx| p.definition(&buffer_b, 23, cx)); - let mut fake_language_server = fake_language_servers.next().await.unwrap(); fake_language_server.handle_request::(|_, _| { Some(lsp::GotoDefinitionResponse::Scalar(lsp::Location::new( @@ -2499,7 +2491,10 @@ mod tests { ))) }); - let definitions_1 = definitions_1.await.unwrap(); + let definitions_1 = project_b + .update(cx_b, |p, cx| p.definition(&buffer_b, 23, cx)) + .await + .unwrap(); cx_b.read(|cx| { assert_eq!(definitions_1.len(), 1); assert_eq!(project_b.read(cx).worktrees(cx).count(), 2); @@ -2516,7 +2511,6 @@ mod tests { // Try getting more definitions for the same buffer, ensuring the buffer gets reused from // the previous call to `definition`. - let definitions_2 = project_b.update(cx_b, |p, cx| p.definition(&buffer_b, 33, cx)); fake_language_server.handle_request::(|_, _| { Some(lsp::GotoDefinitionResponse::Scalar(lsp::Location::new( lsp::Url::from_file_path("/root-2/b.rs").unwrap(), @@ -2524,7 +2518,10 @@ mod tests { ))) }); - let definitions_2 = definitions_2.await.unwrap(); + let definitions_2 = project_b + .update(cx_b, |p, cx| p.definition(&buffer_b, 33, cx)) + .await + .unwrap(); cx_b.read(|cx| { assert_eq!(definitions_2.len(), 1); assert_eq!(project_b.read(cx).worktrees(cx).count(), 2); @@ -2625,8 +2622,6 @@ mod tests { .unwrap(); // Request references to a symbol as the guest. - let references = project_b.update(cx_b, |p, cx| p.references(&buffer_b, 7, cx)); - let mut fake_language_server = fake_language_servers.next().await.unwrap(); fake_language_server.handle_request::(|params, _| { assert_eq!( @@ -2649,7 +2644,10 @@ mod tests { ]) }); - let references = references.await.unwrap(); + let references = project_b + .update(cx_b, |p, cx| p.references(&buffer_b, 7, cx)) + .await + .unwrap(); cx_b.read(|cx| { assert_eq!(references.len(), 3); assert_eq!(project_b.read(cx).worktrees(cx).count(), 2); @@ -2853,8 +2851,6 @@ mod tests { .unwrap(); // Request document highlights as the guest. - let highlights = project_b.update(cx_b, |p, cx| p.document_highlights(&buffer_b, 34, cx)); - let mut fake_language_server = fake_language_servers.next().await.unwrap(); fake_language_server.handle_request::( |params, _| { @@ -2896,7 +2892,10 @@ mod tests { }, ); - let highlights = highlights.await.unwrap(); + let highlights = project_b + .update(cx_b, |p, cx| p.document_highlights(&buffer_b, 34, cx)) + .await + .unwrap(); buffer_b.read_with(cx_b, |buffer, _| { let snapshot = buffer.snapshot(); @@ -2998,8 +2997,6 @@ mod tests { .await .unwrap(); - // Request the definition of a symbol as the guest. - let symbols = project_b.update(cx_b, |p, cx| p.symbols("two", cx)); let mut fake_language_server = fake_language_servers.next().await.unwrap(); fake_language_server.handle_request::(|_, _| { #[allow(deprecated)] @@ -3016,7 +3013,11 @@ mod tests { }]) }); - let symbols = symbols.await.unwrap(); + // Request the definition of a symbol as the guest. + let symbols = project_b + .update(cx_b, |p, cx| p.symbols("two", cx)) + .await + .unwrap(); assert_eq!(symbols.len(), 1); assert_eq!(symbols[0].name, "TWO"); @@ -3127,6 +3128,14 @@ mod tests { .await .unwrap(); + let mut fake_language_server = fake_language_servers.next().await.unwrap(); + fake_language_server.handle_request::(|_, _| { + Some(lsp::GotoDefinitionResponse::Scalar(lsp::Location::new( + lsp::Url::from_file_path("/root/b.rs").unwrap(), + lsp::Range::new(lsp::Position::new(0, 6), lsp::Position::new(0, 9)), + ))) + }); + let definitions; let buffer_b2; if rng.gen() { @@ -3137,14 +3146,6 @@ mod tests { definitions = project_b.update(cx_b, |p, cx| p.definition(&buffer_b1, 23, cx)); } - let mut fake_language_server = fake_language_servers.next().await.unwrap(); - fake_language_server.handle_request::(|_, _| { - Some(lsp::GotoDefinitionResponse::Scalar(lsp::Location::new( - lsp::Url::from_file_path("/root/b.rs").unwrap(), - lsp::Range::new(lsp::Position::new(0, 6), lsp::Position::new(0, 9)), - ))) - }); - let buffer_b2 = buffer_b2.await.unwrap(); let definitions = definitions.await.unwrap(); assert_eq!(definitions.len(), 1); @@ -4478,17 +4479,16 @@ mod tests { let peer_id = PeerId(connection_id_rx.next().await.unwrap().0); let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http, cx)); - let mut authed_user = - user_store.read_with(cx, |user_store, _| user_store.watch_current_user()); - while authed_user.next().await.unwrap().is_none() {} - TestClient { + let client = TestClient { client, peer_id, user_store, project: Default::default(), buffers: Default::default(), - } + }; + client.wait_for_current_user(cx).await; + client } fn disconnect_client(&self, user_id: UserId) { @@ -4568,6 +4568,13 @@ mod tests { ) } + async fn wait_for_current_user(&self, cx: &TestAppContext) { + let mut authed_user = self + .user_store + .read_with(cx, |user_store, _| user_store.watch_current_user()); + while authed_user.next().await.unwrap().is_none() {} + } + fn simulate_host( mut self, project: ModelHandle, 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) - } }