Fall back to file names in breadcumbs with LSP symbols (#61511)
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
This commit is contained in:
1 parent
146a568c79
commit
336679c8e1
3 files changed
+161
-30
No files matched your search
@@ -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<String> {
|
||||
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::<lsp::request::DocumentSymbolRequest, _, _>(
|
||||
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::<lsp::request::DocumentSymbolRequest, _, _>(
|
||||
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"
|
||||
);
|
||||
}
|
||||
}
|
||||
+20
-20
@@ -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<Vec<HighlightedText>> {
|
||||
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) {
|
||||
|
||||
@@ -1169,7 +1169,7 @@ impl Item for Editor {
|
||||
f(ItemEvent::UpdateBreadcrumbs);
|
||||
}
|
||||
|
||||
EditorEvent::BreadcrumbsChanged => {
|
||||
EditorEvent::BreadcrumbsChanged | EditorEvent::OutlineSymbolsChanged => {
|
||||
f(ItemEvent::UpdateBreadcrumbs);
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user