From de8dd9bea5b57678e5cb6791adc054dfdc92ad7e Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Thu, 16 Oct 2025 18:13:23 +0300 Subject: [PATCH] Rework editors to register and query buffers on scroll (#40388) Preparation to https://github.com/zed-industries/zed/pull/40183 Moves https://github.com/zed-industries/zed/pull/22958 further: now, instead of selection, scrolling the buffer into view is enough to get registered and, later, be queried for its LSP data such as inlay hints, diagnostics and document colors. This effectively undoes https://github.com/zed-industries/zed/pull/28855 as now we try to register whatever's visible more aggressively, instead of implicitly via inlay hints. Release Notes: - Reworked editors to register and query buffers on scroll --- crates/collab/src/tests/editor_tests.rs | 381 ++++++++++-------- crates/editor/src/editor.rs | 240 ++++++----- crates/editor/src/inlay_hint_cache.rs | 36 +- crates/editor/src/linked_editing_ranges.rs | 2 +- crates/editor/src/lsp_colors.rs | 32 +- crates/editor/src/scroll.rs | 25 +- crates/multi_buffer/src/multi_buffer.rs | 9 - crates/multi_buffer/src/multi_buffer_tests.rs | 3 - crates/project/src/lsp_store.rs | 77 ++-- .../project/src/manifest_tree/server_tree.rs | 2 +- 10 files changed, 421 insertions(+), 386 deletions(-) diff --git a/crates/collab/src/tests/editor_tests.rs b/crates/collab/src/tests/editor_tests.rs index 621e7e100ad9c6da8352d9ecad4061a1ebf7ae51..9a923c3fb3a11e0ecd01706c19c0087de6475a09 100644 --- a/crates/collab/src/tests/editor_tests.rs +++ b/crates/collab/src/tests/editor_tests.rs @@ -2553,6 +2553,27 @@ async fn test_lsp_pull_diagnostics( cx_a.update(editor::init); cx_b.update(editor::init); + let expected_push_diagnostic_main_message = "pushed main diagnostic"; + let expected_push_diagnostic_lib_message = "pushed lib diagnostic"; + let expected_pull_diagnostic_main_message = "pulled main diagnostic"; + let expected_pull_diagnostic_lib_message = "pulled lib diagnostic"; + let expected_workspace_pull_diagnostics_main_message = "pulled workspace main diagnostic"; + let expected_workspace_pull_diagnostics_lib_message = "pulled workspace lib diagnostic"; + + let diagnostics_pulls_result_ids = Arc::new(Mutex::new(BTreeSet::>::new())); + let workspace_diagnostics_pulls_result_ids = Arc::new(Mutex::new(BTreeSet::::new())); + let diagnostics_pulls_made = Arc::new(AtomicUsize::new(0)); + let closure_diagnostics_pulls_made = diagnostics_pulls_made.clone(); + let closure_diagnostics_pulls_result_ids = diagnostics_pulls_result_ids.clone(); + let workspace_diagnostics_pulls_made = Arc::new(AtomicUsize::new(0)); + let closure_workspace_diagnostics_pulls_made = workspace_diagnostics_pulls_made.clone(); + let closure_workspace_diagnostics_pulls_result_ids = + workspace_diagnostics_pulls_result_ids.clone(); + let (workspace_diagnostic_cancel_tx, closure_workspace_diagnostic_cancel_rx) = + smol::channel::bounded::<()>(1); + let (closure_workspace_diagnostic_received_tx, workspace_diagnostic_received_rx) = + smol::channel::bounded::<()>(1); + let capabilities = lsp::ServerCapabilities { diagnostic_provider: Some(lsp::DiagnosticServerCapabilities::Options( lsp::DiagnosticOptions { @@ -2567,13 +2588,195 @@ async fn test_lsp_pull_diagnostics( ..lsp::ServerCapabilities::default() }; client_a.language_registry().add(rust_lang()); + + let pull_diagnostics_handle = Arc::new(parking_lot::Mutex::new(None)); + let workspace_diagnostics_pulls_handle = Arc::new(parking_lot::Mutex::new(None)); + + let closure_pull_diagnostics_handle = pull_diagnostics_handle.clone(); + let closure_workspace_diagnostics_pulls_handle = workspace_diagnostics_pulls_handle.clone(); let mut fake_language_servers = client_a.language_registry().register_fake_lsp( "Rust", FakeLspAdapter { capabilities: capabilities.clone(), + initializer: Some(Box::new(move |fake_language_server| { + let expected_workspace_diagnostic_token = lsp::ProgressToken::String(format!( + "workspace/diagnostic-{}-1", + fake_language_server.server.server_id() + )); + let closure_workspace_diagnostics_pulls_result_ids = closure_workspace_diagnostics_pulls_result_ids.clone(); + let diagnostics_pulls_made = closure_diagnostics_pulls_made.clone(); + let diagnostics_pulls_result_ids = closure_diagnostics_pulls_result_ids.clone(); + let closure_pull_diagnostics_handle = closure_pull_diagnostics_handle.clone(); + let closure_workspace_diagnostics_pulls_handle = closure_workspace_diagnostics_pulls_handle.clone(); + let closure_workspace_diagnostic_cancel_rx = closure_workspace_diagnostic_cancel_rx.clone(); + let closure_workspace_diagnostic_received_tx = closure_workspace_diagnostic_received_tx.clone(); + let pull_diagnostics_handle = fake_language_server + .set_request_handler::( + move |params, _| { + let requests_made = diagnostics_pulls_made.clone(); + let diagnostics_pulls_result_ids = + diagnostics_pulls_result_ids.clone(); + async move { + let message = if lsp::Uri::from_file_path(path!("/a/main.rs")) + .unwrap() + == params.text_document.uri + { + expected_pull_diagnostic_main_message.to_string() + } else if lsp::Uri::from_file_path(path!("/a/lib.rs")).unwrap() + == params.text_document.uri + { + expected_pull_diagnostic_lib_message.to_string() + } else { + panic!("Unexpected document: {}", params.text_document.uri) + }; + { + diagnostics_pulls_result_ids + .lock() + .await + .insert(params.previous_result_id); + } + let new_requests_count = + requests_made.fetch_add(1, atomic::Ordering::Release) + 1; + Ok(lsp::DocumentDiagnosticReportResult::Report( + lsp::DocumentDiagnosticReport::Full( + lsp::RelatedFullDocumentDiagnosticReport { + related_documents: None, + full_document_diagnostic_report: + lsp::FullDocumentDiagnosticReport { + result_id: Some(format!( + "pull-{new_requests_count}" + )), + items: vec![lsp::Diagnostic { + range: lsp::Range { + start: lsp::Position { + line: 0, + character: 0, + }, + end: lsp::Position { + line: 0, + character: 2, + }, + }, + severity: Some( + lsp::DiagnosticSeverity::ERROR, + ), + message, + ..lsp::Diagnostic::default() + }], + }, + }, + ), + )) + } + }, + ); + let _ = closure_pull_diagnostics_handle.lock().insert(pull_diagnostics_handle); + + let closure_workspace_diagnostics_pulls_made = closure_workspace_diagnostics_pulls_made.clone(); + let workspace_diagnostics_pulls_handle = fake_language_server.set_request_handler::( + move |params, _| { + let workspace_requests_made = closure_workspace_diagnostics_pulls_made.clone(); + let workspace_diagnostics_pulls_result_ids = + closure_workspace_diagnostics_pulls_result_ids.clone(); + let workspace_diagnostic_cancel_rx = closure_workspace_diagnostic_cancel_rx.clone(); + let workspace_diagnostic_received_tx = closure_workspace_diagnostic_received_tx.clone(); + let expected_workspace_diagnostic_token = expected_workspace_diagnostic_token.clone(); + async move { + let workspace_request_count = + workspace_requests_made.fetch_add(1, atomic::Ordering::Release) + 1; + { + workspace_diagnostics_pulls_result_ids + .lock() + .await + .extend(params.previous_result_ids.into_iter().map(|id| id.value)); + } + if should_stream_workspace_diagnostic && !workspace_diagnostic_cancel_rx.is_closed() + { + assert_eq!( + params.partial_result_params.partial_result_token, + Some(expected_workspace_diagnostic_token) + ); + workspace_diagnostic_received_tx.send(()).await.unwrap(); + workspace_diagnostic_cancel_rx.recv().await.unwrap(); + workspace_diagnostic_cancel_rx.close(); + // https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#partialResults + // > The final response has to be empty in terms of result values. + return Ok(lsp::WorkspaceDiagnosticReportResult::Report( + lsp::WorkspaceDiagnosticReport { items: Vec::new() }, + )); + } + Ok(lsp::WorkspaceDiagnosticReportResult::Report( + lsp::WorkspaceDiagnosticReport { + items: vec![ + lsp::WorkspaceDocumentDiagnosticReport::Full( + lsp::WorkspaceFullDocumentDiagnosticReport { + uri: lsp::Uri::from_file_path(path!("/a/main.rs")).unwrap(), + version: None, + full_document_diagnostic_report: + lsp::FullDocumentDiagnosticReport { + result_id: Some(format!( + "workspace_{workspace_request_count}" + )), + items: vec![lsp::Diagnostic { + range: lsp::Range { + start: lsp::Position { + line: 0, + character: 1, + }, + end: lsp::Position { + line: 0, + character: 3, + }, + }, + severity: Some(lsp::DiagnosticSeverity::WARNING), + message: + expected_workspace_pull_diagnostics_main_message + .to_string(), + ..lsp::Diagnostic::default() + }], + }, + }, + ), + lsp::WorkspaceDocumentDiagnosticReport::Full( + lsp::WorkspaceFullDocumentDiagnosticReport { + uri: lsp::Uri::from_file_path(path!("/a/lib.rs")).unwrap(), + version: None, + full_document_diagnostic_report: + lsp::FullDocumentDiagnosticReport { + result_id: Some(format!( + "workspace_{workspace_request_count}" + )), + items: vec![lsp::Diagnostic { + range: lsp::Range { + start: lsp::Position { + line: 0, + character: 1, + }, + end: lsp::Position { + line: 0, + character: 3, + }, + }, + severity: Some(lsp::DiagnosticSeverity::WARNING), + message: + expected_workspace_pull_diagnostics_lib_message + .to_string(), + ..lsp::Diagnostic::default() + }], + }, + }, + ), + ], + }, + )) + } + }); + let _ = closure_workspace_diagnostics_pulls_handle.lock().insert(workspace_diagnostics_pulls_handle); + })), ..FakeLspAdapter::default() }, ); + client_b.language_registry().add(rust_lang()); client_b.language_registry().register_fake_lsp_adapter( "Rust", @@ -2631,183 +2834,15 @@ async fn test_lsp_pull_diagnostics( .unwrap(); let fake_language_server = fake_language_servers.next().await.unwrap(); - cx_a.run_until_parked(); - cx_b.run_until_parked(); - let expected_push_diagnostic_main_message = "pushed main diagnostic"; - let expected_push_diagnostic_lib_message = "pushed lib diagnostic"; - let expected_pull_diagnostic_main_message = "pulled main diagnostic"; - let expected_pull_diagnostic_lib_message = "pulled lib diagnostic"; - let expected_workspace_pull_diagnostics_main_message = "pulled workspace main diagnostic"; - let expected_workspace_pull_diagnostics_lib_message = "pulled workspace lib diagnostic"; - - let diagnostics_pulls_result_ids = Arc::new(Mutex::new(BTreeSet::>::new())); - let workspace_diagnostics_pulls_result_ids = Arc::new(Mutex::new(BTreeSet::::new())); - let diagnostics_pulls_made = Arc::new(AtomicUsize::new(0)); - let closure_diagnostics_pulls_made = diagnostics_pulls_made.clone(); - let closure_diagnostics_pulls_result_ids = diagnostics_pulls_result_ids.clone(); - let mut pull_diagnostics_handle = fake_language_server - .set_request_handler::(move |params, _| { - let requests_made = closure_diagnostics_pulls_made.clone(); - let diagnostics_pulls_result_ids = closure_diagnostics_pulls_result_ids.clone(); - async move { - let message = if lsp::Uri::from_file_path(path!("/a/main.rs")).unwrap() - == params.text_document.uri - { - expected_pull_diagnostic_main_message.to_string() - } else if lsp::Uri::from_file_path(path!("/a/lib.rs")).unwrap() - == params.text_document.uri - { - expected_pull_diagnostic_lib_message.to_string() - } else { - panic!("Unexpected document: {}", params.text_document.uri) - }; - { - diagnostics_pulls_result_ids - .lock() - .await - .insert(params.previous_result_id); - } - let new_requests_count = requests_made.fetch_add(1, atomic::Ordering::Release) + 1; - Ok(lsp::DocumentDiagnosticReportResult::Report( - lsp::DocumentDiagnosticReport::Full(lsp::RelatedFullDocumentDiagnosticReport { - related_documents: None, - full_document_diagnostic_report: lsp::FullDocumentDiagnosticReport { - result_id: Some(format!("pull-{new_requests_count}")), - items: vec![lsp::Diagnostic { - range: lsp::Range { - start: lsp::Position { - line: 0, - character: 0, - }, - end: lsp::Position { - line: 0, - character: 2, - }, - }, - severity: Some(lsp::DiagnosticSeverity::ERROR), - message, - ..lsp::Diagnostic::default() - }], - }, - }), - )) - } - }); - - let workspace_diagnostics_pulls_made = Arc::new(AtomicUsize::new(0)); - let closure_workspace_diagnostics_pulls_made = workspace_diagnostics_pulls_made.clone(); - let closure_workspace_diagnostics_pulls_result_ids = - workspace_diagnostics_pulls_result_ids.clone(); - let (workspace_diagnostic_cancel_tx, closure_workspace_diagnostic_cancel_rx) = - smol::channel::bounded::<()>(1); - let (closure_workspace_diagnostic_received_tx, workspace_diagnostic_received_rx) = - smol::channel::bounded::<()>(1); let expected_workspace_diagnostic_token = lsp::ProgressToken::String(format!( "workspace/diagnostic-{}-1", fake_language_server.server.server_id() )); - let closure_expected_workspace_diagnostic_token = expected_workspace_diagnostic_token.clone(); - let mut workspace_diagnostics_pulls_handle = fake_language_server - .set_request_handler::( - move |params, _| { - let workspace_requests_made = closure_workspace_diagnostics_pulls_made.clone(); - let workspace_diagnostics_pulls_result_ids = - closure_workspace_diagnostics_pulls_result_ids.clone(); - let workspace_diagnostic_cancel_rx = closure_workspace_diagnostic_cancel_rx.clone(); - let workspace_diagnostic_received_tx = closure_workspace_diagnostic_received_tx.clone(); - let expected_workspace_diagnostic_token = - closure_expected_workspace_diagnostic_token.clone(); - async move { - let workspace_request_count = - workspace_requests_made.fetch_add(1, atomic::Ordering::Release) + 1; - { - workspace_diagnostics_pulls_result_ids - .lock() - .await - .extend(params.previous_result_ids.into_iter().map(|id| id.value)); - } - if should_stream_workspace_diagnostic && !workspace_diagnostic_cancel_rx.is_closed() - { - assert_eq!( - params.partial_result_params.partial_result_token, - Some(expected_workspace_diagnostic_token) - ); - workspace_diagnostic_received_tx.send(()).await.unwrap(); - workspace_diagnostic_cancel_rx.recv().await.unwrap(); - workspace_diagnostic_cancel_rx.close(); - // https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#partialResults - // > The final response has to be empty in terms of result values. - return Ok(lsp::WorkspaceDiagnosticReportResult::Report( - lsp::WorkspaceDiagnosticReport { items: Vec::new() }, - )); - } - Ok(lsp::WorkspaceDiagnosticReportResult::Report( - lsp::WorkspaceDiagnosticReport { - items: vec![ - lsp::WorkspaceDocumentDiagnosticReport::Full( - lsp::WorkspaceFullDocumentDiagnosticReport { - uri: lsp::Uri::from_file_path(path!("/a/main.rs")).unwrap(), - version: None, - full_document_diagnostic_report: - lsp::FullDocumentDiagnosticReport { - result_id: Some(format!( - "workspace_{workspace_request_count}" - )), - items: vec![lsp::Diagnostic { - range: lsp::Range { - start: lsp::Position { - line: 0, - character: 1, - }, - end: lsp::Position { - line: 0, - character: 3, - }, - }, - severity: Some(lsp::DiagnosticSeverity::WARNING), - message: - expected_workspace_pull_diagnostics_main_message - .to_string(), - ..lsp::Diagnostic::default() - }], - }, - }, - ), - lsp::WorkspaceDocumentDiagnosticReport::Full( - lsp::WorkspaceFullDocumentDiagnosticReport { - uri: lsp::Uri::from_file_path(path!("/a/lib.rs")).unwrap(), - version: None, - full_document_diagnostic_report: - lsp::FullDocumentDiagnosticReport { - result_id: Some(format!( - "workspace_{workspace_request_count}" - )), - items: vec![lsp::Diagnostic { - range: lsp::Range { - start: lsp::Position { - line: 0, - character: 1, - }, - end: lsp::Position { - line: 0, - character: 3, - }, - }, - severity: Some(lsp::DiagnosticSeverity::WARNING), - message: - expected_workspace_pull_diagnostics_lib_message - .to_string(), - ..lsp::Diagnostic::default() - }], - }, - }, - ), - ], - }, - )) - } - }, - ); + cx_a.run_until_parked(); + cx_b.run_until_parked(); + let mut pull_diagnostics_handle = pull_diagnostics_handle.lock().take().unwrap(); + let mut workspace_diagnostics_pulls_handle = + workspace_diagnostics_pulls_handle.lock().take().unwrap(); if should_stream_workspace_diagnostic { workspace_diagnostic_received_rx.recv().await.unwrap(); diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 5b0d4b68a5f1d87c55648943f4798e679d59aacc..6229af77ebb27c4d163a26105517905c5665fe04 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -1190,6 +1190,7 @@ pub struct Editor { inline_value_cache: InlineValueCache, selection_drag_state: SelectionDragState, colors: Option, + post_scroll_update: Task<()>, refresh_colors_task: Task<()>, folding_newlines: Task<()>, pub lookup_key: Option>, @@ -1786,7 +1787,7 @@ impl Editor { fn new_internal( mode: EditorMode, - buffer: Entity, + multi_buffer: Entity, project: Option>, display_map: Option>, window: &mut Window, @@ -1844,7 +1845,7 @@ impl Editor { let display_map = display_map.unwrap_or_else(|| { cx.new(|cx| { DisplayMap::new( - buffer.clone(), + multi_buffer.clone(), style.font(), font_size, None, @@ -1857,7 +1858,7 @@ impl Editor { }) }); - let selections = SelectionsCollection::new(display_map.clone(), buffer.clone()); + let selections = SelectionsCollection::new(display_map.clone(), multi_buffer.clone()); let blink_manager = cx.new(|cx| { let mut blink_manager = BlinkManager::new(CURSOR_BLINK_INTERVAL, cx); @@ -1909,8 +1910,19 @@ impl Editor { } } project::Event::LanguageServerBufferRegistered { buffer_id, .. } => { - if editor.buffer().read(cx).buffer(*buffer_id).is_some() { - editor.update_lsp_data(false, Some(*buffer_id), window, cx); + let buffer_id = *buffer_id; + if editor.buffer().read(cx).buffer(buffer_id).is_some() { + let registered = editor.register_buffer(buffer_id, cx); + if registered { + editor.update_lsp_data(Some(buffer_id), window, cx); + editor.refresh_inlay_hints( + InlayHintRefreshReason::RefreshRequested, + cx, + ); + refresh_linked_ranges(editor, window, cx); + editor.refresh_code_actions(window, cx); + editor.refresh_document_highlights(cx); + } } } @@ -2029,7 +2041,7 @@ impl Editor { })); } - let buffer_snapshot = buffer.read(cx).snapshot(cx); + let buffer_snapshot = multi_buffer.read(cx).snapshot(cx); let inlay_hint_settings = inlay_hint_settings(selections.newest_anchor().head(), &buffer_snapshot, cx); @@ -2066,8 +2078,8 @@ impl Editor { update_uncommitted_diff_for_buffer( cx.entity(), &project, - buffer.read(cx).all_buffers(), - buffer.clone(), + multi_buffer.read(cx).all_buffers(), + multi_buffer.clone(), cx, ) .shared(), @@ -2079,7 +2091,7 @@ impl Editor { focus_handle, show_cursor_when_unfocused: false, last_focused_descendant: None, - buffer: buffer.clone(), + buffer: multi_buffer.clone(), display_map: display_map.clone(), placeholder_display_map: None, selections, @@ -2221,8 +2233,8 @@ impl Editor { _subscriptions: (!is_minimap) .then(|| { vec![ - cx.observe(&buffer, Self::on_buffer_changed), - cx.subscribe_in(&buffer, window, Self::on_buffer_event), + cx.observe(&multi_buffer, Self::on_buffer_changed), + cx.subscribe_in(&multi_buffer, window, Self::on_buffer_event), cx.observe_in(&display_map, window, Self::on_display_map_changed), cx.observe(&blink_manager, |_, _, cx| cx.notify()), cx.observe_global_in::(window, Self::settings_changed), @@ -2248,6 +2260,7 @@ impl Editor { colors: None, refresh_colors_task: Task::ready(()), next_color_inlay_id: 0, + post_scroll_update: Task::ready(()), linked_edit_ranges: Default::default(), in_project_search: false, previous_search_ranges: None, @@ -2370,7 +2383,7 @@ impl Editor { editor.selection_history.mode = SelectionHistoryMode::Normal; editor.scroll_manager.show_scrollbars(window, cx); - jsx_tag_auto_close::refresh_enabled_in_any_buffer(&mut editor, &buffer, cx); + jsx_tag_auto_close::refresh_enabled_in_any_buffer(&mut editor, &multi_buffer, cx); if full_mode { let should_auto_hide_scrollbars = cx.should_auto_hide_scrollbars(); @@ -2382,24 +2395,13 @@ impl Editor { editor.go_to_active_debug_line(window, cx); - if let Some(buffer) = buffer.read(cx).as_singleton() - && let Some(project) = editor.project() - { - let handle = project.update(cx, |project, cx| { - project.register_buffer_with_language_servers(&buffer, cx) - }); - editor - .registered_buffers - .insert(buffer.read(cx).remote_id(), handle); + if let Some(buffer) = multi_buffer.read(cx).as_singleton() { + editor.register_buffer(buffer.read(cx).remote_id(), cx); } editor.minimap = editor.create_minimap(EditorSettings::get_global(cx).minimap, window, cx); editor.colors = Some(LspColorData::new(cx)); - editor.update_lsp_data(false, None, window, cx); - } - - if editor.mode.is_full() { editor.report_editor_event(ReportEditorEvent::EditorOpened, None, cx); } @@ -2899,20 +2901,6 @@ impl Editor { self.collapse_matches = collapse_matches; } - fn register_buffers_with_language_servers(&mut self, cx: &mut Context) { - let buffers = self.buffer.read(cx).all_buffers(); - let Some(project) = self.project.as_ref() else { - return; - }; - project.update(cx, |project, cx| { - for buffer in buffers { - self.registered_buffers - .entry(buffer.read(cx).remote_id()) - .or_insert_with(|| project.register_buffer_with_language_servers(&buffer, cx)); - } - }) - } - pub fn range_for_match(&self, range: &Range) -> Range { if self.collapse_matches { return range.start..range.start; @@ -3112,19 +3100,8 @@ impl Editor { } if local { - if let Some(buffer_id) = new_cursor_position.buffer_id - && !self.registered_buffers.contains_key(&buffer_id) - && let Some(project) = self.project.as_ref() - { - project.update(cx, |project, cx| { - let Some(buffer) = self.buffer.read(cx).buffer(buffer_id) else { - return; - }; - self.registered_buffers.insert( - buffer_id, - project.register_buffer_with_language_servers(&buffer, cx), - ); - }) + if let Some(buffer_id) = new_cursor_position.buffer_id { + self.register_buffer(buffer_id, cx); } let mut context_menu = self.context_menu.borrow_mut(); @@ -3174,11 +3151,12 @@ impl Editor { } self.refresh_code_actions(window, cx); self.refresh_document_highlights(cx); + refresh_linked_ranges(self, window, cx); + self.refresh_selected_text_highlights(false, window, cx); refresh_matching_bracket_highlights(self, cx); self.update_visible_edit_prediction(window, cx); self.edit_prediction_requires_modifier_in_indent_conflict = true; - linked_editing_ranges::refresh_linked_ranges(self, window, cx); self.inline_blame_popover.take(); if self.git_blame_inline_enabled { self.start_inline_blame_timer(window, cx); @@ -4444,7 +4422,7 @@ impl Editor { } } this.trigger_completion_on_input(&text, trigger_in_words, window, cx); - linked_editing_ranges::refresh_linked_ranges(this, window, cx); + refresh_linked_ranges(this, window, cx); this.refresh_edit_prediction(true, false, window, cx); jsx_tag_auto_close::handle_from(this, initial_buffer_versions, window, cx); }); @@ -5305,12 +5283,18 @@ impl Editor { } }; + let mut visible_excerpts = self.visible_excerpts(required_languages.as_ref(), cx); + visible_excerpts.retain(|_, (buffer, _, _)| { + self.registered_buffers + .contains_key(&buffer.read(cx).remote_id()) + }); + if let Some(InlaySplice { to_remove, to_insert, }) = self.inlay_hint_cache.spawn_hint_refresh( reason_description, - self.visible_excerpts(required_languages.as_ref(), cx), + visible_excerpts, invalidate_cache, ignore_debounce, cx, @@ -10107,7 +10091,7 @@ impl Editor { }) } this.refresh_edit_prediction(true, false, window, cx); - linked_editing_ranges::refresh_linked_ranges(this, window, cx); + refresh_linked_ranges(this, window, cx); }); } @@ -16974,40 +16958,31 @@ impl Editor { editor }) }); - editor.update(cx, |editor, cx| { - match multibuffer_selection_mode { - MultibufferSelectionMode::First => { - if let Some(first_range) = ranges.first() { - editor.change_selections( - SelectionEffects::no_scroll(), - window, - cx, - |selections| { - selections.clear_disjoint(); - selections - .select_anchor_ranges(std::iter::once(first_range.clone())); - }, - ); - } - editor.highlight_background::( - &ranges, - |theme| theme.colors().editor_highlighted_line_background, - cx, - ); - } - MultibufferSelectionMode::All => { + editor.update(cx, |editor, cx| match multibuffer_selection_mode { + MultibufferSelectionMode::First => { + if let Some(first_range) = ranges.first() { editor.change_selections( SelectionEffects::no_scroll(), window, cx, |selections| { selections.clear_disjoint(); - selections.select_anchor_ranges(ranges); + selections.select_anchor_ranges(std::iter::once(first_range.clone())); }, ); } + editor.highlight_background::( + &ranges, + |theme| theme.colors().editor_highlighted_line_background, + cx, + ); + } + MultibufferSelectionMode::All => { + editor.change_selections(SelectionEffects::no_scroll(), window, cx, |selections| { + selections.clear_disjoint(); + selections.select_anchor_ranges(ranges); + }); } - editor.register_buffers_with_language_servers(cx); }); let item = Box::new(editor); @@ -17854,7 +17829,7 @@ impl Editor { window: &Window, cx: &mut Context, ) -> Option<()> { - if !self.mode().is_full() { + if self.ignore_lsp_data() { return None; } let pull_diagnostics_settings = ProjectSettings::get_global(cx) @@ -17866,8 +17841,14 @@ impl Editor { let project = self.project()?.downgrade(); let debounce = Duration::from_millis(pull_diagnostics_settings.debounce_ms); let mut buffers = self.buffer.read(cx).all_buffers(); - if let Some(buffer_id) = buffer_id { - buffers.retain(|buffer| buffer.read(cx).remote_id() == buffer_id); + buffers.retain(|buffer| { + let buffer_id_to_retain = buffer.read(cx).remote_id(); + buffer_id.is_none_or(|buffer_id| buffer_id == buffer_id_to_retain) + && self.registered_buffers.contains_key(&buffer_id_to_retain) + }); + if buffers.is_empty() { + self.pull_diagnostics_task = Task::ready(()); + return None; } self.pull_diagnostics_task = cx.spawn_in(window, async move |editor, cx| { @@ -20864,10 +20845,7 @@ impl Editor { cx: &mut Context, ) { match event { - multi_buffer::Event::Edited { - singleton_buffer_edited, - edited_buffer, - } => { + multi_buffer::Event::Edited { edited_buffer } => { self.scrollbar_marker_state.dirty = true; self.active_indent_guides_state.dirty = true; self.refresh_active_diagnostics(cx); @@ -20878,31 +20856,16 @@ impl Editor { if self.has_active_edit_prediction() { self.update_visible_edit_prediction(window, cx); } - if let Some(project) = self.project.as_ref() - && let Some(edited_buffer) = edited_buffer - { - project.update(cx, |project, cx| { - self.registered_buffers - .entry(edited_buffer.read(cx).remote_id()) - .or_insert_with(|| { - project.register_buffer_with_language_servers(edited_buffer, cx) - }); - }); - } - cx.emit(EditorEvent::BufferEdited); - cx.emit(SearchEvent::MatchesInvalidated); - if let Some(buffer) = edited_buffer { - self.update_lsp_data(false, Some(buffer.read(cx).remote_id()), window, cx); - } - - if *singleton_buffer_edited { - if let Some(buffer) = edited_buffer - && buffer.read(cx).file().is_none() - { + if let Some(edited_buffer) = edited_buffer { + if edited_buffer.read(cx).file().is_none() { cx.emit(EditorEvent::TitleChanged); } - if let Some(project) = &self.project { + + let buffer_id = edited_buffer.read(cx).remote_id(); + if let Some(project) = self.project.clone() { + self.register_buffer(buffer_id, cx); + self.update_lsp_data(Some(buffer_id), window, cx); #[allow(clippy::mutable_key_type)] let languages_affected = multibuffer.update(cx, |multibuffer, cx| { multibuffer @@ -20929,6 +20892,9 @@ impl Editor { } } + cx.emit(EditorEvent::BufferEdited); + cx.emit(SearchEvent::MatchesInvalidated); + let Some(project) = &self.project else { return }; let (telemetry, is_via_ssh) = { let project = project.read(cx); @@ -20936,7 +20902,6 @@ impl Editor { let is_via_ssh = project.is_via_remote_server(); (telemetry, is_via_ssh) }; - refresh_linked_ranges(self, window, cx); telemetry.log_edit_event("editor", is_via_ssh); } multi_buffer::Event::ExcerptsAdded { @@ -20958,24 +20923,22 @@ impl Editor { ) .detach(); } - if self.active_diagnostics != ActiveDiagnostic::All { - self.update_lsp_data(false, Some(buffer_id), window, cx); - } + self.update_lsp_data(Some(buffer_id), window, cx); + self.refresh_inlay_hints(InlayHintRefreshReason::NewLinesShown, cx); cx.emit(EditorEvent::ExcerptsAdded { buffer: buffer.clone(), predecessor: *predecessor, excerpts: excerpts.clone(), }); - self.refresh_inlay_hints(InlayHintRefreshReason::NewLinesShown, cx); } multi_buffer::Event::ExcerptsRemoved { ids, removed_buffer_ids, } => { self.refresh_inlay_hints(InlayHintRefreshReason::ExcerptsRemoved(ids.clone()), cx); - let buffer = self.buffer.read(cx); - self.registered_buffers - .retain(|buffer_id, _| buffer.buffer(*buffer_id).is_some()); + for buffer_id in removed_buffer_ids { + self.registered_buffers.remove(buffer_id); + } jsx_tag_auto_close::refresh_enabled_in_any_buffer(self, multibuffer, cx); cx.emit(EditorEvent::ExcerptsRemoved { ids: ids.clone(), @@ -21007,7 +20970,7 @@ impl Editor { self.tasks_update_task = Some(self.refresh_runnables(window, cx)); } multi_buffer::Event::LanguageChanged(buffer_id) => { - linked_editing_ranges::refresh_linked_ranges(self, window, cx); + self.registered_buffers.remove(&buffer_id); jsx_tag_auto_close::refresh_enabled_in_any_buffer(self, multibuffer, cx); cx.emit(EditorEvent::Reparsed(*buffer_id)); cx.notify(); @@ -21148,7 +21111,7 @@ impl Editor { if !inlay_splice.to_insert.is_empty() || !inlay_splice.to_remove.is_empty() { self.splice_inlays(&inlay_splice.to_remove, inlay_splice.to_insert, cx); } - self.refresh_colors(false, None, window, cx); + self.refresh_colors_for_visible_range(None, window, cx); } cx.notify(); @@ -22060,13 +22023,48 @@ impl Editor { fn update_lsp_data( &mut self, - ignore_cache: bool, for_buffer: Option, window: &mut Window, cx: &mut Context<'_, Self>, ) { self.pull_diagnostics(for_buffer, window, cx); - self.refresh_colors(ignore_cache, for_buffer, window, cx); + self.refresh_colors_for_visible_range(for_buffer, window, cx); + } + + fn register_visible_buffers(&mut self, cx: &mut Context) { + // Singletons are registered on editor creation. + if self.ignore_lsp_data() || self.buffer().read(cx).is_singleton() { + return; + } + for (_, (visible_buffer, _, _)) in self.visible_excerpts(None, cx) { + self.register_buffer(visible_buffer.read(cx).remote_id(), cx); + } + } + + fn register_buffer(&mut self, buffer_id: BufferId, cx: &mut Context) -> bool { + if !self.registered_buffers.contains_key(&buffer_id) + && let Some(project) = self.project.as_ref() + { + if let Some(buffer) = self.buffer.read(cx).buffer(buffer_id) { + project.update(cx, |project, cx| { + self.registered_buffers.insert( + buffer_id, + project.register_buffer_with_language_servers(&buffer, cx), + ); + }); + return true; + } else { + self.registered_buffers.remove(&buffer_id); + } + } + + false + } + + fn ignore_lsp_data(&self) -> bool { + // `ActiveDiagnostic::All` is a special mode where editor's diagnostics are managed by the external view, + // skip any LSP updates for it. + self.active_diagnostics == ActiveDiagnostic::All || !self.mode().is_full() } } diff --git a/crates/editor/src/inlay_hint_cache.rs b/crates/editor/src/inlay_hint_cache.rs index fe2ab972ef6920a8530b493d09f7d0e279e147a5..34d737452e905449c259fb41fe03a96e34159b05 100644 --- a/crates/editor/src/inlay_hint_cache.rs +++ b/crates/editor/src/inlay_hint_cache.rs @@ -2394,6 +2394,8 @@ pub mod tests { editor.scroll_screen(&ScrollAmount::Page(1.0), window, cx); }) .unwrap(); + // Wait for the first hints request to fire off + cx.executor().advance_clock(Duration::from_millis(100)); cx.executor().run_until_parked(); editor .update(cx, |editor, window, cx| { @@ -2752,6 +2754,23 @@ pub mod tests { }) .unwrap(); cx.executor().run_until_parked(); + editor + .update(cx, |editor, _window, cx| { + let expected_hints = vec![ + "main hint #0".to_string(), + "main hint #1".to_string(), + "main hint #2".to_string(), + "main hint #3".to_string(), + "main hint #4".to_string(), + "main hint #5".to_string(), + ]; + assert_eq!(expected_hints, sorted_cached_hint_labels(editor), + "New hints are not shown right after scrolling, we need to wait for the buffer to be registered"); + assert_eq!(expected_hints, visible_hint_labels(editor, cx)); + }) + .unwrap(); + cx.executor().advance_clock(Duration::from_millis(100)); + cx.executor().run_until_parked(); editor .update(cx, |editor, _window, cx| { let expected_hints = vec![ @@ -2766,7 +2785,7 @@ pub mod tests { "other hint #2".to_string(), ]; assert_eq!(expected_hints, sorted_cached_hint_labels(editor), - "With more scrolls of the multibuffer, more hints should be added into the cache and nothing invalidated without edits"); + "After scrolling to the new buffer and waiting for it to be registered, new hints should appear"); assert_eq!(expected_hints, visible_hint_labels(editor, cx)); }) .unwrap(); @@ -2853,15 +2872,18 @@ pub mod tests { }) .unwrap(); cx.executor().run_until_parked(); + // Wait again to trigger the inlay hints fetch on scroll + cx.executor().advance_clock(Duration::from_millis(100)); + cx.executor().run_until_parked(); editor .update(cx, |editor, _window, cx| { let expected_hints = vec![ - "main hint #0".to_string(), - "main hint #1".to_string(), - "main hint #2".to_string(), - "main hint #3".to_string(), - "main hint #4".to_string(), - "main hint #5".to_string(), + "main hint(edited) #0".to_string(), + "main hint(edited) #1".to_string(), + "main hint(edited) #2".to_string(), + "main hint(edited) #3".to_string(), + "main hint(edited) #4".to_string(), + "main hint(edited) #5".to_string(), "other hint(edited) #0".to_string(), "other hint(edited) #1".to_string(), ]; diff --git a/crates/editor/src/linked_editing_ranges.rs b/crates/editor/src/linked_editing_ranges.rs index 4f1313797f97b1d482effced60b6843541c9e3a7..a26e24e1cbca4f06e94cde4898e7f9d2a80da8e1 100644 --- a/crates/editor/src/linked_editing_ranges.rs +++ b/crates/editor/src/linked_editing_ranges.rs @@ -48,7 +48,7 @@ pub(super) fn refresh_linked_ranges( window: &mut Window, cx: &mut Context, ) -> Option<()> { - if editor.pending_rename.is_some() { + if editor.ignore_lsp_data() || editor.pending_rename.is_some() { return None; } let project = editor.project()?.downgrade(); diff --git a/crates/editor/src/lsp_colors.rs b/crates/editor/src/lsp_colors.rs index f13ffd69206d3d3d89ba8dd29ff6f73fd93e0bec..e7fdb6aaa1f661b717945b568f23062a2fb41f12 100644 --- a/crates/editor/src/lsp_colors.rs +++ b/crates/editor/src/lsp_colors.rs @@ -2,11 +2,11 @@ use std::{cmp, ops::Range}; use collections::HashMap; use futures::future::join_all; -use gpui::{Hsla, Rgba}; +use gpui::{Hsla, Rgba, Task}; use itertools::Itertools; use language::point_from_lsp; use multi_buffer::Anchor; -use project::{DocumentColor, lsp_store::LspFetchStrategy}; +use project::DocumentColor; use settings::Settings as _; use text::{Bias, BufferId, OffsetRangeExt as _}; use ui::{App, Context, Window}; @@ -143,14 +143,13 @@ impl LspColorData { } impl Editor { - pub(super) fn refresh_colors( + pub(super) fn refresh_colors_for_visible_range( &mut self, - ignore_cache: bool, buffer_id: Option, _: &Window, cx: &mut Context, ) { - if !self.mode().is_full() { + if self.ignore_lsp_data() { return; } let Some(project) = self.project.clone() else { @@ -169,7 +168,9 @@ impl Editor { .into_values() .map(|(buffer, ..)| buffer) .filter(|editor_buffer| { - buffer_id.is_none_or(|buffer_id| buffer_id == editor_buffer.read(cx).remote_id()) + let editor_buffer_id = editor_buffer.read(cx).remote_id(); + buffer_id.is_none_or(|buffer_id| buffer_id == editor_buffer_id) + && self.registered_buffers.contains_key(&editor_buffer_id) }) .unique_by(|buffer| buffer.read(cx).remote_id()) .collect::>(); @@ -179,21 +180,20 @@ impl Editor { .into_iter() .filter_map(|buffer| { let buffer_id = buffer.read(cx).remote_id(); - let fetch_strategy = if ignore_cache { - LspFetchStrategy::IgnoreCache - } else { - LspFetchStrategy::UseCache { - known_cache_version: self.colors.as_ref().and_then(|colors| { - Some(colors.buffer_colors.get(&buffer_id)?.cache_version_used) - }), - } - }; - let colors_task = lsp_store.document_colors(fetch_strategy, buffer, cx)?; + let known_cache_version = self.colors.as_ref().and_then(|colors| { + Some(colors.buffer_colors.get(&buffer_id)?.cache_version_used) + }); + let colors_task = lsp_store.document_colors(known_cache_version, buffer, cx)?; Some(async move { (buffer_id, colors_task.await) }) }) .collect::>() }); + if all_colors_task.is_empty() { + self.refresh_colors_task = Task::ready(()); + return; + } + self.refresh_colors_task = cx.spawn(async move |editor, cx| { cx.background_executor() .timer(FETCH_COLORS_DEBOUNCE_TIMEOUT) diff --git a/crates/editor/src/scroll.rs b/crates/editor/src/scroll.rs index dae668a4b4f9cd718d034f45259d3706e515fafb..001be45ab814e1627dc34abbba342272d3e15750 100644 --- a/crates/editor/src/scroll.rs +++ b/crates/editor/src/scroll.rs @@ -494,15 +494,15 @@ impl Editor { let opened_first_time = self.scroll_manager.visible_line_count.is_none(); self.scroll_manager.visible_line_count = Some(lines); if opened_first_time { - cx.spawn_in(window, async move |editor, cx| { + self.post_scroll_update = cx.spawn_in(window, async move |editor, cx| { editor .update_in(cx, |editor, window, cx| { + editor.register_visible_buffers(cx); editor.refresh_inlay_hints(InlayHintRefreshReason::NewLinesShown, cx); - editor.refresh_colors(false, None, window, cx); + editor.update_lsp_data(None, window, cx); }) - .ok() - }) - .detach() + .ok(); + }); } } @@ -613,8 +613,19 @@ impl Editor { cx, ); - self.refresh_inlay_hints(InlayHintRefreshReason::NewLinesShown, cx); - self.refresh_colors(false, None, window, cx); + self.post_scroll_update = cx.spawn_in(window, async move |editor, cx| { + cx.background_executor() + .timer(Duration::from_millis(50)) + .await; + editor + .update_in(cx, |editor, window, cx| { + editor.register_visible_buffers(cx); + editor.refresh_colors_for_visible_range(None, window, cx); + editor.refresh_inlay_hints(InlayHintRefreshReason::NewLinesShown, cx); + }) + .ok(); + }); + editor_was_scrolled } diff --git a/crates/multi_buffer/src/multi_buffer.rs b/crates/multi_buffer/src/multi_buffer.rs index be01c4b6a1f9f67703f99bcd0b9b331574a6b360..0347757660c584efbfe3c786964d1cad51d4a325 100644 --- a/crates/multi_buffer/src/multi_buffer.rs +++ b/crates/multi_buffer/src/multi_buffer.rs @@ -99,7 +99,6 @@ pub enum Event { }, DiffHunksToggled, Edited { - singleton_buffer_edited: bool, edited_buffer: Option>, }, TransactionUndone { @@ -2033,7 +2032,6 @@ impl MultiBuffer { DiffChangeKind::BufferEdited, ); cx.emit(Event::Edited { - singleton_buffer_edited: false, edited_buffer: None, }); cx.emit(Event::ExcerptsAdded { @@ -2074,7 +2072,6 @@ impl MultiBuffer { DiffChangeKind::BufferEdited, ); cx.emit(Event::Edited { - singleton_buffer_edited: false, edited_buffer: None, }); cx.emit(Event::ExcerptsRemoved { @@ -2361,7 +2358,6 @@ impl MultiBuffer { self.sync_diff_transforms(&mut snapshot, edits, DiffChangeKind::BufferEdited); self.buffer_changed_since_sync.replace(true); cx.emit(Event::Edited { - singleton_buffer_edited: false, edited_buffer: None, }); cx.emit(Event::ExcerptsRemoved { @@ -2429,7 +2425,6 @@ impl MultiBuffer { use language::BufferEvent; cx.emit(match event { BufferEvent::Edited => Event::Edited { - singleton_buffer_edited: true, edited_buffer: Some(buffer), }, BufferEvent::DirtyChanged => Event::DirtyChanged, @@ -2528,7 +2523,6 @@ impl MultiBuffer { }, ); cx.emit(Event::Edited { - singleton_buffer_edited: false, edited_buffer: None, }); } @@ -2808,7 +2802,6 @@ impl MultiBuffer { ); cx.emit(Event::DiffHunksToggled); cx.emit(Event::Edited { - singleton_buffer_edited: false, edited_buffer: None, }); } @@ -2892,7 +2885,6 @@ impl MultiBuffer { self.sync_diff_transforms(&mut snapshot, edits, DiffChangeKind::BufferEdited); cx.emit(Event::Edited { - singleton_buffer_edited: false, edited_buffer: None, }); cx.emit(Event::ExcerptsExpanded { ids: vec![id] }); @@ -2997,7 +2989,6 @@ impl MultiBuffer { self.sync_diff_transforms(&mut snapshot, edits, DiffChangeKind::BufferEdited); cx.emit(Event::Edited { - singleton_buffer_edited: false, edited_buffer: None, }); cx.emit(Event::ExcerptsExpanded { ids }); diff --git a/crates/multi_buffer/src/multi_buffer_tests.rs b/crates/multi_buffer/src/multi_buffer_tests.rs index bad99d5412cd009ecaeabe9c6ad7686f07d30862..2f4bfdfda8d023cf08e31ec6ffc9300446f8a400 100644 --- a/crates/multi_buffer/src/multi_buffer_tests.rs +++ b/crates/multi_buffer/src/multi_buffer_tests.rs @@ -157,15 +157,12 @@ fn test_excerpt_boundaries_and_clipping(cx: &mut App) { events.read().as_slice(), &[ Event::Edited { - singleton_buffer_edited: false, edited_buffer: None, }, Event::Edited { - singleton_buffer_edited: false, edited_buffer: None, }, Event::Edited { - singleton_buffer_edited: false, edited_buffer: None, } ] diff --git a/crates/project/src/lsp_store.rs b/crates/project/src/lsp_store.rs index 68db3ac2b436e77281b4c61296b78ff5e4cf52d1..34ea47dbc3a4b6f1215bc1c0342f208cabfb76f0 100644 --- a/crates/project/src/lsp_store.rs +++ b/crates/project/src/lsp_store.rs @@ -3495,12 +3495,6 @@ struct CodeLensData { update: Option<(Global, CodeLensTask)>, } -#[derive(Debug, PartialEq, Eq, Clone, Copy)] -pub enum LspFetchStrategy { - IgnoreCache, - UseCache { known_cache_version: Option }, -} - #[derive(Debug)] pub enum LspStoreEvent { LanguageServerAdded(LanguageServerId, LanguageServerName, Option), @@ -6498,47 +6492,35 @@ impl LspStore { pub fn document_colors( &mut self, - fetch_strategy: LspFetchStrategy, + known_cache_version: Option, buffer: Entity, cx: &mut Context, ) -> Option { let version_queried_for = buffer.read(cx).version(); let buffer_id = buffer.read(cx).remote_id(); - match fetch_strategy { - LspFetchStrategy::IgnoreCache => {} - LspFetchStrategy::UseCache { - known_cache_version, - } => { - if let Some(cached_data) = self.lsp_document_colors.get(&buffer_id) - && !version_queried_for.changed_since(&cached_data.colors_for_version) - { - let has_different_servers = self.as_local().is_some_and(|local| { - local - .buffers_opened_in_servers - .get(&buffer_id) - .cloned() - .unwrap_or_default() - != cached_data.colors.keys().copied().collect() - }); - if !has_different_servers { - if Some(cached_data.cache_version) == known_cache_version { - return None; - } else { - return Some( - Task::ready(Ok(DocumentColors { - colors: cached_data - .colors - .values() - .flatten() - .cloned() - .collect(), - cache_version: Some(cached_data.cache_version), - })) - .shared(), - ); - } - } + if let Some(cached_data) = self.lsp_document_colors.get(&buffer_id) + && !version_queried_for.changed_since(&cached_data.colors_for_version) + { + let has_different_servers = self.as_local().is_some_and(|local| { + local + .buffers_opened_in_servers + .get(&buffer_id) + .cloned() + .unwrap_or_default() + != cached_data.colors.keys().copied().collect() + }); + if !has_different_servers { + if Some(cached_data.cache_version) == known_cache_version { + return None; + } else { + return Some( + Task::ready(Ok(DocumentColors { + colors: cached_data.colors.values().flatten().cloned().collect(), + cache_version: Some(cached_data.cache_version), + })) + .shared(), + ); } } } @@ -6564,13 +6546,12 @@ impl LspStore { .map_err(Arc::new); let fetched_colors = match fetched_colors { Ok(fetched_colors) => { - if fetch_strategy != LspFetchStrategy::IgnoreCache - && Some(true) - == buffer - .update(cx, |buffer, _| { - buffer.version() != query_version_queried_for - }) - .ok() + if Some(true) + == buffer + .update(cx, |buffer, _| { + buffer.version() != query_version_queried_for + }) + .ok() { return Ok(DocumentColors::default()); } diff --git a/crates/project/src/manifest_tree/server_tree.rs b/crates/project/src/manifest_tree/server_tree.rs index 0d80b845dc63ce00ed7221162b323b8549ad4075..b6828fdb281d51600096bbcad411f94a91c69e54 100644 --- a/crates/project/src/manifest_tree/server_tree.rs +++ b/crates/project/src/manifest_tree/server_tree.rs @@ -47,7 +47,7 @@ pub struct LanguageServerTree { /// A node in language server tree represents either: /// - A language server that has already been initialized/updated for a given project /// - A soon-to-be-initialized language server. -#[derive(Clone)] +#[derive(Clone, Debug)] pub struct LanguageServerTreeNode(Weak); /// Describes a request to launch a language server.