From 336679c8e147a248537a9ebf97a7cca268266e08 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Thu, 23 Jul 2026 11:35:26 +0000 Subject: [PATCH] Fall back to file names in breadcumbs with LSP symbols (#61511) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes https://github.com/zed-industries/zed/issues/60905 Also makes the breadcrumbs to update on settings change — before, one needed to focus them. Release Notes: - Improved breadcrumbs for `"document_symbols": "on"` case when no LSP data is sent --- crates/editor/src/document_symbols.rs | 149 ++++++++++++++++++++++++-- crates/editor/src/editor.rs | 40 +++---- crates/editor/src/items.rs | 2 +- 3 files changed, 161 insertions(+), 30 deletions(-) diff --git a/crates/editor/src/document_symbols.rs b/crates/editor/src/document_symbols.rs index 58483e6dab..5d3d73f405 100644 --- a/crates/editor/src/document_symbols.rs +++ b/crates/editor/src/document_symbols.rs @@ -333,12 +333,13 @@ mod tests { }; use futures::StreamExt as _; - use gpui::TestAppContext; + use gpui::{App, TestAppContext}; use multi_buffer::ToPoint; use settings::{DocumentSymbols, SettingsStore}; use text::Point; use util::path; - use zed_actions::editor::{MoveDown, MoveUp}; + use workspace::item::{Item, ItemEvent}; + use zed_actions::editor::MoveDown; use crate::{ Editor, LSP_REQUEST_DEBOUNCE_TIMEOUT, @@ -357,6 +358,16 @@ mod tests { .collect() } + fn breadcrumb_texts(editor: &Editor, cx: &App) -> Vec { + editor + .breadcrumbs(cx) + .expect("Should have breadcrumbs") + .0 + .into_iter() + .map(|segment| segment.text.to_string()) + .collect() + } + fn lsp_range(start_line: u32, start_char: u32, end_line: u32, end_char: u32) -> lsp::Range { lsp::Range { start: lsp::Position::new(start_line, start_char), @@ -542,18 +553,13 @@ mod tests { ); }); - // Step 3: Switch back to tree-sitter + // Step 3: Switch back to tree-sitter, the symbols should refresh + // without any extra selection changes update_test_language_settings(&mut cx.cx.cx, &|settings| { settings.defaults.document_symbols = Some(DocumentSymbols::Off); }); cx.run_until_parked(); - // Force another selection change - cx.update_editor(|editor, window, cx| { - editor.move_up(&MoveUp, window, cx); - }); - cx.run_until_parked(); - cx.update_editor(|editor, _window, _cx| { assert_eq!( outline_symbol_names(editor), @@ -1022,4 +1028,129 @@ mod tests { ); }); } + + #[gpui::test] + async fn test_breadcrumbs_keep_file_name_without_lsp_symbols(cx: &mut TestAppContext) { + init_test(cx, |_| {}); + + update_test_language_settings(cx, &|settings| { + settings.defaults.document_symbols = Some(DocumentSymbols::On); + }); + + let mut cx = EditorLspTestContext::new_rust( + lsp::ServerCapabilities { + document_symbol_provider: Some(lsp::OneOf::Left(true)), + ..lsp::ServerCapabilities::default() + }, + cx, + ) + .await; + let mut symbol_request = cx + .set_request_handler::( + move |_, _, _| async move { + Ok(Some(lsp::DocumentSymbolResponse::Nested(Vec::new()))) + }, + ); + + cx.set_state("fn maˇin() {\n let x = 1;\n}\n"); + assert!(symbol_request.next().await.is_some()); + cx.run_until_parked(); + + cx.update_editor(|editor, _window, cx| { + assert_eq!( + breadcrumb_texts(editor, cx), + vec![path!("dir/file.rs").to_string()], + "Breadcrumbs should fall back to the file name when the language server returns no symbols" + ); + + editor.set_breadcrumb_header("Last 1000 lines in the log".to_string()); + assert_eq!( + breadcrumb_texts(editor, cx), + vec!["Last 1000 lines in the log".to_string()], + "A custom breadcrumb header should never disappear" + ); + }); + } + + #[gpui::test] + async fn test_breadcrumbs_refresh_on_document_symbols_setting_change(cx: &mut TestAppContext) { + init_test(cx, |_| {}); + + let mut cx = EditorLspTestContext::new_rust( + lsp::ServerCapabilities { + document_symbol_provider: Some(lsp::OneOf::Left(true)), + ..lsp::ServerCapabilities::default() + }, + cx, + ) + .await; + let mut symbol_request = cx + .set_request_handler::( + move |_, _, _| async move { + Ok(Some(lsp::DocumentSymbolResponse::Nested(Vec::new()))) + }, + ); + + cx.set_state("fn maˇin() {\n let x = 1;\n}\n"); + cx.run_until_parked(); + + cx.update_editor(|editor, _window, cx| { + assert_eq!( + breadcrumb_texts(editor, cx), + vec![path!("dir/file.rs").to_string(), "fn main".to_string()], + "With tree-sitter symbols, breadcrumbs should show the file name and the symbol" + ); + }); + + let breadcrumb_updates = Arc::new(atomic::AtomicUsize::new(0)); + let editor = cx.editor.clone(); + let _subscription = cx.update(|_, cx| { + cx.subscribe(&editor, { + let breadcrumb_updates = breadcrumb_updates.clone(); + move |_, event, _| { + Editor::to_item_events(event, &mut |item_event| { + if item_event == ItemEvent::UpdateBreadcrumbs { + breadcrumb_updates.fetch_add(1, atomic::Ordering::AcqRel); + } + }); + } + }) + }); + + update_test_language_settings(&mut cx.cx.cx, &|settings| { + settings.defaults.document_symbols = Some(DocumentSymbols::On); + }); + assert!(symbol_request.next().await.is_some()); + cx.run_until_parked(); + + cx.update_editor(|editor, _window, cx| { + assert_eq!( + breadcrumb_texts(editor, cx), + vec![path!("dir/file.rs").to_string()], + "After enabling LSP symbols that return nothing, breadcrumbs should keep the file name" + ); + }); + assert!( + breadcrumb_updates.load(atomic::Ordering::Acquire) > 0, + "Breadcrumbs should refresh on the setting change, without extra selection changes" + ); + + breadcrumb_updates.store(0, atomic::Ordering::Release); + update_test_language_settings(&mut cx.cx.cx, &|settings| { + settings.defaults.document_symbols = Some(DocumentSymbols::Off); + }); + cx.run_until_parked(); + + cx.update_editor(|editor, _window, cx| { + assert_eq!( + breadcrumb_texts(editor, cx), + vec![path!("dir/file.rs").to_string(), "fn main".to_string()], + "After disabling LSP symbols, tree-sitter breadcrumbs should return" + ); + }); + assert!( + breadcrumb_updates.load(atomic::Ordering::Acquire) > 0, + "Breadcrumbs should refresh on the setting change, without extra selection changes" + ); + } } diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index fbd6ad44d8..1756d19d49 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -9887,6 +9887,7 @@ impl Editor { if language_settings_changed { self.clear_disabled_lsp_folding_ranges(window, cx); self.refresh_document_symbols(None, cx); + self.refresh_outline_symbols_at_cursor(cx); } if let Some(inlay_splice) = self.colors.as_mut().and_then(|colors| { @@ -10943,16 +10944,12 @@ impl Editor { } fn breadcrumbs_inner(&self, cx: &App) -> Option> { - let multibuffer = self.buffer().read(cx); - let is_singleton = multibuffer.is_singleton(); - let (buffer_id, symbols) = self.outline_symbols_at_cursor.as_ref()?; - let buffer = multibuffer.buffer(*buffer_id)?; - - let buffer = buffer.read(cx); + let multi_buffer = self.buffer().read(cx); // In a multi-buffer layout, we don't want to include the filename in the breadcrumbs - let mut breadcrumbs = if is_singleton { + let mut breadcrumbs = if let Some(buffer) = multi_buffer.as_singleton() { let text = self.breadcrumb_header.clone().unwrap_or_else(|| { buffer + .read(cx) .snapshot() .resolve_file_path( self.project @@ -10961,27 +10958,30 @@ impl Editor { .unwrap_or_default(), cx, ) - .unwrap_or_else(|| { - if multibuffer.is_singleton() { - multibuffer.title(cx).to_string() - } else { - MultiBuffer::DEFAULT_TITLE.to_string() - } - }) + .unwrap_or_else(|| multi_buffer.title(cx).to_string()) }); vec![HighlightedText { text: text.into(), highlights: vec![], }] } else { - vec![] + Vec::new() }; - breadcrumbs.extend(symbols.iter().map(|symbol| HighlightedText { - text: symbol.text.clone(), - highlights: symbol.highlight_ranges.clone(), - })); - Some(breadcrumbs) + if let Some((buffer_id, symbols)) = self.outline_symbols_at_cursor.as_ref() + && multi_buffer.buffer(*buffer_id).is_some() + { + breadcrumbs.extend(symbols.iter().map(|symbol| HighlightedText { + text: symbol.text.clone(), + highlights: symbol.highlight_ranges.clone(), + })); + } + + if breadcrumbs.is_empty() { + None + } else { + Some(breadcrumbs) + } } fn disable_lsp_data(&mut self) { diff --git a/crates/editor/src/items.rs b/crates/editor/src/items.rs index cb2b16a48d..f515ed19fa 100644 --- a/crates/editor/src/items.rs +++ b/crates/editor/src/items.rs @@ -1169,7 +1169,7 @@ impl Item for Editor { f(ItemEvent::UpdateBreadcrumbs); } - EditorEvent::BreadcrumbsChanged => { + EditorEvent::BreadcrumbsChanged | EditorEvent::OutlineSymbolsChanged => { f(ItemEvent::UpdateBreadcrumbs); }