diff --git a/Cargo.lock b/Cargo.lock index 3b5e12d6044..d6312937bb7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -12404,7 +12404,6 @@ dependencies = [ "serde_json", "settings", "theme", - "theme_settings", "ui", "util", "workspace", diff --git a/crates/call_hierarchy/src/call_hierarchy.rs b/crates/call_hierarchy/src/call_hierarchy.rs index fc4043e8e64..bc20c57baf5 100644 --- a/crates/call_hierarchy/src/call_hierarchy.rs +++ b/crates/call_hierarchy/src/call_hierarchy.rs @@ -10,9 +10,9 @@ use editor::{ }; use fuzzy::{StringMatch, StringMatchCandidate}; use gpui::{ - AbsoluteLength, Action, AnyView, App, AsyncWindowContext, Context, DismissEvent, Entity, - EventEmitter, FocusHandle, Focusable, HighlightStyle, ParentElement, Point, Render, Styled, - StyledText, Subscription, Task, TextRun, TextStyle, WeakEntity, Window, + Action, AnyView, App, AsyncWindowContext, Context, DismissEvent, Entity, EventEmitter, + FocusHandle, Focusable, HighlightStyle, ParentElement, Point, Render, Styled, StyledText, + Subscription, Task, TextRun, WeakEntity, Window, }; use language::{ Buffer, CodeLabel, File as _, Language, Location, Rope, ToOffset, ToPoint, lsp_to_symbol_kind, @@ -20,9 +20,10 @@ use language::{ use picker::{Picker, PickerDelegate}; use project::{CallHierarchyItem, LspStoreEvent, Project}; use settings::Settings; -use theme::SyntaxTheme; use theme_settings::ThemeSettings; -use ui::{KeyBinding, ListItem, ListItemSpacing, prelude::*, tooltip_container}; +use ui::{ + KeyBinding, ListItem, ListItemSpacing, prelude::*, tooltip_container, utils::buffer_text_style, +}; use util::{ResultExt, paths::PathExt}; use workspace::{DismissDecision, ModalView, Workspace}; pub use zed_actions::{ShowIncomingCalls, ShowOutgoingCalls, ToggleDirection}; @@ -806,7 +807,7 @@ impl Render for CallSignatureTooltip { Some((label, label_text)) => StyledText::new(label_text.clone()) .with_default_highlights( &signature_style, - label_syntax_runs(label, cx.theme().syntax()), + cx.theme().syntax().resolve_runs(&label.runs), ) .into_any_element(), None => div().child(self.full_signature.clone()).into_any_element(), @@ -1170,29 +1171,6 @@ fn shaped_width( .width } -fn buffer_text_style(cx: &App) -> TextStyle { - let settings = ThemeSettings::get_global(cx); - TextStyle { - color: cx.theme().colors().text, - font_family: settings.buffer_font.family.clone(), - font_features: settings.buffer_font.features.clone(), - font_fallbacks: settings.buffer_font.fallbacks.clone(), - font_size: AbsoluteLength::from(settings.buffer_font_size(cx)), - font_weight: settings.buffer_font.weight, - line_height: relative(1.), - ..TextStyle::default() - } -} - -fn label_syntax_runs<'a>( - label: &'a CodeLabel, - syntax_theme: &'a SyntaxTheme, -) -> impl Iterator, HighlightStyle)> + 'a { - label.runs.iter().filter_map(|(range, highlight_id)| { - Some((range.clone(), *syntax_theme.get(*highlight_id)?)) - }) -} - fn render_item( call_item: &Call, match_ranges: impl IntoIterator>, @@ -1225,7 +1203,7 @@ fn render_item( .as_ref() .zip(call_item.display.label_text.clone()) { - let syntax_runs = label_syntax_runs(label, cx.theme().syntax()); + let syntax_runs = cx.theme().syntax().resolve_runs(&label.runs); let custom_highlights = match_ranges.into_iter().map(|range| { let start = label.filter_range.start + range.start; let end = label.filter_range.start + range.end; diff --git a/crates/editor/src/document_symbols.rs b/crates/editor/src/document_symbols.rs index 3a2f488db8d..c66c1f7cc76 100644 --- a/crates/editor/src/document_symbols.rs +++ b/crates/editor/src/document_symbols.rs @@ -6,7 +6,7 @@ use futures::future::join_all; use gpui::{App, Context, HighlightStyle, Task}; use itertools::Itertools as _; use language::language_settings::LanguageSettings; -use language::{Buffer, OutlineItem}; +use language::{Buffer, OutlineItem, highlight_ranges_from_text}; use multi_buffer::{ Anchor, AnchorRangeExt as _, MultiBufferOffset, MultiBufferRow, MultiBufferSnapshot, ToOffset as _, @@ -219,12 +219,20 @@ impl Editor { let display_snapshot = editor.display_map.update(cx, |map, cx| map.snapshot(cx)); let mut highlighted_results = results; - for items in highlighted_results.values_mut() { + for (buffer_id, items) in highlighted_results.iter_mut() { + let language = editor + .buffer + .read(cx) + .buffer(*buffer_id) + .and_then(|buffer| buffer.read(cx).language().cloned()); for item in items { if let Some(highlights) = highlights_from_buffer(&display_snapshot, &item, &syntax) { item.highlight_ranges = highlights; + } else if let Some(language) = &language { + item.highlight_ranges = + highlight_ranges_from_text(&item.text, language, &syntax); } } } @@ -334,6 +342,7 @@ mod tests { use futures::StreamExt as _; use gpui::{App, TestAppContext}; + use language::highlight_ranges_from_text; use multi_buffer::ToPoint; use settings::{DocumentSymbols, SettingsStore}; use text::Point; @@ -813,6 +822,82 @@ mod tests { }); } + #[gpui::test] + async fn test_lsp_document_symbols_fall_back_to_reparsed_text_highlights( + cx: &mut TestAppContext, + ) { + use ui::ActiveTheme as _; + + 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; + cx.update_editor(|editor, _window, cx| { + editor + .project + .as_ref() + .expect("editor should have a project") + .read(cx) + .languages() + .set_theme(cx.theme().clone()); + }); + let mut symbol_request = cx + .set_request_handler::( + move |_, _, _| async move { + Ok(Some(lsp::DocumentSymbolResponse::Nested(vec![ + nested_symbol( + "impl ZzzMissing", + lsp::SymbolKind::OBJECT, + lsp_range(0, 0, 0, 12), + lsp_range(0, 3, 0, 7), + Vec::new(), + ), + ]))) + }, + ); + + cx.set_state("fn teˇst() {}\n"); + assert!(symbol_request.next().await.is_some()); + cx.run_until_parked(); + + cx.update_editor(|editor, _window, cx| { + let (_, symbols) = editor + .outline_symbols_at_cursor + .as_ref() + .expect("Should have outline symbols"); + assert_eq!(symbols.len(), 1); + let symbol = &symbols[0]; + assert_eq!(symbol.text, "impl ZzzMissing"); + + let language = editor + .buffer + .read(cx) + .as_singleton() + .expect("singleton buffer") + .read(cx) + .language() + .cloned() + .expect("buffer language"); + let expected = highlight_ranges_from_text(&symbol.text, &language, cx.theme().syntax()); + assert_eq!( + expected.first().map(|(range, _)| range.clone()), + Some(0..4), + "reparsing the symbol text should highlight the `impl` keyword" + ); + assert_eq!(symbol.highlight_ranges, expected); + }); + } + #[gpui::test] async fn test_lsp_document_symbols_empty_response(cx: &mut TestAppContext) { init_test(cx, |_| {}); diff --git a/crates/language/src/outline.rs b/crates/language/src/outline.rs index c85b7be78fe..e3e9ec51018 100644 --- a/crates/language/src/outline.rs +++ b/crates/language/src/outline.rs @@ -1,7 +1,9 @@ -use crate::{BufferSnapshot, Point, ToPoint, ToTreeSitterPoint}; +use crate::{BufferSnapshot, Language, Point, ToPoint, ToTreeSitterPoint}; use fuzzy_nucleo::{Case, LengthPenalty, StringMatch, StringMatchCandidate}; use gpui::{BackgroundExecutor, HighlightStyle, SharedString}; -use std::ops::Range; +use std::{ops::Range, sync::Arc}; +use text::Rope; +use theme::SyntaxTheme; /// An outline of all the symbols contained in a buffer. #[derive(Debug)] @@ -262,6 +264,16 @@ impl Outline { } } +pub fn highlight_ranges_from_text( + text: &str, + language: &Arc, + syntax_theme: &SyntaxTheme, +) -> Vec<(Range, HighlightStyle)> { + let rope = Rope::from(text); + let runs = language.highlight_text(&rope, 0..text.len()); + syntax_theme.resolve_runs(&runs).collect() +} + /// Interleaves synthetic [`OutlineSearchEntry::Ancestor`] rows before each match so callers /// can render the parent chain as tree context above the match. /// @@ -414,4 +426,38 @@ mod tests { assert_eq!(outline.find_most_similar("struct User"), None); assert_eq!(outline.find_most_similar("struct"), None); } + + #[test] + fn test_highlight_ranges_from_text() { + let language = rust_lang(); + let keyword = HighlightStyle::color(gpui::Hsla::from(gpui::rgba(0x100000ff))); + let type_style = HighlightStyle::color(gpui::Hsla::from(gpui::rgba(0x200000ff))); + let theme = SyntaxTheme::new([ + ("keyword".to_string(), keyword), + ("type".to_string(), type_style), + ]); + language.set_theme(&theme); + + let text = "impl LspCommand for GetIncomingCalls"; + assert_eq!( + highlight_ranges_from_text(text, &language, &theme), + vec![ + (0..4, keyword), + (5..15, type_style), + (16..19, keyword), + (20..36, type_style), + ] + ); + + let text = "pub struct OutlineItem"; + assert_eq!( + highlight_ranges_from_text(text, &language, &theme), + vec![(0..3, keyword), (4..10, keyword), (11..22, type_style)] + ); + + assert_eq!( + highlight_ranges_from_text("", &language, &theme), + Vec::new() + ); + } } diff --git a/crates/outline/Cargo.toml b/crates/outline/Cargo.toml index ba661b36f98..49408501f6f 100644 --- a/crates/outline/Cargo.toml +++ b/crates/outline/Cargo.toml @@ -21,7 +21,6 @@ picker.workspace = true picker_preview.workspace = true settings.workspace = true theme.workspace = true -theme_settings.workspace = true ui.workspace = true util.workspace = true workspace.workspace = true diff --git a/crates/outline/src/outline.rs b/crates/outline/src/outline.rs index ab6570eb7b7..d11a710892d 100644 --- a/crates/outline/src/outline.rs +++ b/crates/outline/src/outline.rs @@ -7,15 +7,12 @@ use editor::{MultiBufferOffset, RowHighlightOptions, SelectionEffects}; use fuzzy_nucleo::StringMatch; use gpui::{ App, Context, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, HighlightStyle, - ParentElement, Point, Rems, Render, Styled, StyledText, Task, TextStyle, WeakEntity, Window, - div, rems, + ParentElement, Point, Rems, Render, Styled, StyledText, Task, WeakEntity, Window, div, rems, }; use language::{OffsetRangeExt, Outline, OutlineItem, OutlineSearchEntry}; use picker::{MatchLocation, Picker, PickerDelegate, PreviewUpdate}; -use settings::Settings; use theme::ActiveTheme; -use theme_settings::ThemeSettings; -use ui::{ListItem, ListItemSpacing, prelude::*}; +use ui::{ListItem, ListItemSpacing, prelude::*, utils::buffer_text_style}; use workspace::{DismissDecision, ModalView}; pub fn init(cx: &mut App) { @@ -463,11 +460,11 @@ impl PickerDelegate for OutlineViewDelegate { } } -pub fn render_item( +pub fn render_item>>( outline_item: &OutlineItem, - match_ranges: impl IntoIterator>, + match_ranges: M, cx: &App, -) -> StyledText { +) -> impl IntoElement + use { let highlight_style = HighlightStyle { background_color: Some(cx.theme().colors().text_accent.alpha(0.3)), ..Default::default() @@ -476,27 +473,16 @@ pub fn render_item( .into_iter() .map(|range| (range, highlight_style)); - let settings = ThemeSettings::get_global(cx); - - // TODO: We probably shouldn't need to build a whole new text style here - // but I'm not sure how to get the current one and modify it. - // Before this change TextStyle::default() was used here, which was giving us the wrong font and text color. - let text_style = TextStyle { - color: cx.theme().colors().text, - font_family: settings.buffer_font.family.clone(), - font_features: settings.buffer_font.features.clone(), - font_fallbacks: settings.buffer_font.fallbacks.clone(), - font_size: settings.buffer_font_size(cx).into(), - font_weight: settings.buffer_font.weight, - line_height: relative(1.), - ..Default::default() - }; + let text_style = buffer_text_style(cx); + let buffer_font_size = text_style.font_size; let highlights = gpui::combine_highlights( custom_highlights, outline_item.highlight_ranges.iter().cloned(), ); - StyledText::new(outline_item.text.clone()).with_default_highlights(&text_style, highlights) + div().text_size(buffer_font_size).child( + StyledText::new(outline_item.text.clone()).with_default_highlights(&text_style, highlights), + ) } #[cfg(test)] diff --git a/crates/syntax_theme/src/syntax_theme.rs b/crates/syntax_theme/src/syntax_theme.rs index edd02130b6e..25de3bc0d7a 100644 --- a/crates/syntax_theme/src/syntax_theme.rs +++ b/crates/syntax_theme/src/syntax_theme.rs @@ -2,6 +2,7 @@ use std::{ collections::{BTreeMap, btree_map::Entry}, + ops::Range, sync::Arc, }; @@ -61,6 +62,14 @@ impl SyntaxTheme { self.highlights.get(highlight_index.into()) } + pub fn resolve_runs<'a, Id: Into + Copy + 'a>( + &'a self, + runs: impl IntoIterator, Id)> + 'a, + ) -> impl Iterator, HighlightStyle)> + 'a { + runs.into_iter() + .filter_map(|(range, highlight_id)| Some((range.clone(), *self.get(*highlight_id)?))) + } + pub fn style_for_name(&self, name: &str) -> Option { self.capture_name_map .get(name) diff --git a/crates/ui/src/utils.rs b/crates/ui/src/utils.rs index 42ed0b62405..2138ec00c23 100644 --- a/crates/ui/src/utils.rs +++ b/crates/ui/src/utils.rs @@ -26,6 +26,21 @@ pub fn is_light(cx: &mut App) -> bool { cx.theme().appearance.is_light() } +pub fn buffer_text_style(cx: &App) -> gpui::TextStyle { + let settings = theme::theme_settings(cx); + let buffer_font = settings.buffer_font(cx); + gpui::TextStyle { + color: cx.theme().colors().text, + font_family: buffer_font.family.clone(), + font_features: buffer_font.features.clone(), + font_fallbacks: buffer_font.fallbacks.clone(), + font_size: gpui::AbsoluteLength::from(settings.buffer_font_size(cx)), + font_weight: buffer_font.weight, + line_height: gpui::relative(1.), + ..gpui::TextStyle::default() + } +} + /// Returns the platform-appropriate label for the "reveal in file manager" action. pub fn reveal_in_file_manager_label(is_remote: bool) -> &'static str { if cfg!(target_os = "macos") && !is_remote {