Skip to repository content

tenant.openagents/omega

No repository description is available.

OpenAgents Git authority 2026-07-28T02:03:20.546Z Public web read
NIP-34 coordinate30617:7649603503856e5148d571eac2766b288a8ff1e9e35d380337a1d2b0015b4f92:omega
MaintainersHidden in public view
References2 branches · 1 tag
Read-only clonegit clone https://openagents.com/git/tenant.openagents/omega.git
Browse files

lsp_locations.rs

973 lines · 34.4 KB · rust
1use std::ops::Range;
2use std::sync::Arc;
3
4use collections::HashMap;
5use editor::actions::{FindAllReferences, GoToDefinition, GoToImplementation};
6use editor::{Editor, EditorSettings, GotoDefinitionKind, OpenResultsIn};
7use file_icons::FileIcons;
8use fuzzy::StringMatchCandidate;
9use gpui::{
10    AnyElement, App, AppContext, AsyncWindowContext, Context, DismissEvent, Entity, EventEmitter,
11    FocusHandle, Focusable, HighlightStyle, StyledText, Subscription, Task, TextStyle, WeakEntity,
12    prelude::*,
13};
14use language::{Buffer, HighlightId, LanguageAwareStyling};
15use picker::{Picker, PickerDelegate};
16use project::{Location, Project, ProjectPath};
17use settings::{GoToDefinitionFallback, Settings as _};
18use text::{Anchor, Point};
19use theme_settings::ThemeSettings;
20use ui::{Divider, FluentBuilder};
21use ui::{ListItem, ListItemSpacing, prelude::*};
22use util::ResultExt as _;
23use workspace::item::ItemSettings;
24use workspace::notifications::NotificationId;
25use workspace::{ModalView, Toast, Workspace};
26
27pub fn init(cx: &mut App) {
28    cx.observe_new(register).detach();
29}
30
31/// Registers handlers for the navigation actions on each full editor. When the
32/// action resolves to [`OpenResultsIn::Picker`], we open the filterable picker;
33/// otherwise we `cx.propagate()` so the editor's own handler runs and builds a
34/// multibuffer.
35fn register(editor: &mut Editor, _window: Option<&mut Window>, cx: &mut Context<Editor>) {
36    if !editor.mode().is_full() {
37        return;
38    }
39    let handle = cx.entity().downgrade();
40    editor
41        .register_action({
42            let handle = handle.clone();
43            move |action: &GoToDefinition, window, cx| {
44                handle_nav_action(
45                    action.open_results_in,
46                    LspPickerKind::Definition,
47                    &handle,
48                    window,
49                    cx,
50                );
51            }
52        })
53        .detach();
54    editor
55        .register_action({
56            let handle = handle.clone();
57            move |action: &GoToImplementation, window, cx| {
58                handle_nav_action(
59                    action.open_results_in,
60                    LspPickerKind::Implementation,
61                    &handle,
62                    window,
63                    cx,
64                );
65            }
66        })
67        .detach();
68    editor
69        .register_action(move |action: &FindAllReferences, window, cx| {
70            handle_nav_action(
71                action.open_results_in,
72                LspPickerKind::References,
73                &handle,
74                window,
75                cx,
76            );
77        })
78        .detach();
79}
80
81/// Either opens the picker for the editor, or propagates the action so the
82/// editor's built-in (multibuffer) handler runs. A `None` argument falls back to
83/// the `lsp_results_location` setting.
84fn handle_nav_action(
85    open_results_in: Option<OpenResultsIn>,
86    kind: LspPickerKind,
87    editor: &WeakEntity<Editor>,
88    window: &mut Window,
89    cx: &mut App,
90) {
91    let open_results_in =
92        open_results_in.unwrap_or_else(|| EditorSettings::get_global(cx).lsp_results_location);
93    if open_results_in != OpenResultsIn::Picker {
94        cx.propagate();
95        return;
96    }
97    LspLocationsPicker::open_for_editor(kind, editor.clone(), window, cx);
98}
99
100/// Runs the LSP query for `kind` and returns the raw locations. Returns `None`
101/// (and reports any error) when there is nothing to query, so the caller stops
102/// without an empty-results toast. Deduplication and dropping fileless results
103/// happen later in [`build_location_matches`].
104async fn run_picker_query(
105    kind: LspPickerKind,
106    editor: &WeakEntity<Editor>,
107    workspace: &WeakEntity<Workspace>,
108    project: &Entity<Project>,
109    cx: &mut AsyncWindowContext,
110) -> Option<Vec<Location>> {
111    let query = editor
112        .update(cx, |editor, cx| kind.run_query(editor, project, cx))
113        .ok()
114        .flatten()?;
115    match query.await {
116        Ok(locations) => Some(locations),
117        Err(error) => {
118            log::error!("LSP {kind:?} query failed: {error:#}");
119            workspace
120                .update(cx, |workspace, cx| workspace.show_error(error, cx))
121                .log_err();
122            None
123        }
124    }
125}
126
127/// Runs the query for `kind` and builds the displayable, deduped matches.
128async fn run_picker_matches(
129    kind: LspPickerKind,
130    editor: &WeakEntity<Editor>,
131    workspace: &WeakEntity<Workspace>,
132    project: &Entity<Project>,
133    cx: &mut AsyncWindowContext,
134) -> Option<Vec<LocationMatch>> {
135    let locations = run_picker_query(kind, editor, workspace, project, cx).await?;
136    editor
137        .update(cx, |_, cx| build_location_matches(&locations, cx))
138        .ok()
139}
140
141fn show_no_results_toast(
142    workspace: &WeakEntity<Workspace>,
143    kind: LspPickerKind,
144    cx: &mut AsyncWindowContext,
145) {
146    workspace
147        .update(cx, |workspace, cx| {
148            struct NoLspResults;
149            workspace.show_toast(
150                Toast::new(
151                    NotificationId::unique::<NoLspResults>(),
152                    kind.empty_message(),
153                )
154                .autohide(),
155                cx,
156            );
157        })
158        .log_err();
159}
160
161#[derive(Clone, Copy, Debug, PartialEq, Eq)]
162pub enum LspPickerKind {
163    References,
164    Definition,
165    Implementation,
166}
167
168impl LspPickerKind {
169    fn placeholder(self) -> &'static str {
170        match self {
171            LspPickerKind::References => "Filter references…",
172            LspPickerKind::Definition => "Filter definitions…",
173            LspPickerKind::Implementation => "Filter implementations…",
174        }
175    }
176
177    /// Message shown when the query produces no results, so the command does not
178    /// appear to silently do nothing.
179    fn empty_message(self) -> &'static str {
180        match self {
181            LspPickerKind::References => "No references found",
182            LspPickerKind::Definition => "No definitions found",
183            LspPickerKind::Implementation => "No implementations found",
184        }
185    }
186
187    /// Runs the query for this kind against the active editor, returning the raw
188    /// locations to populate the picker.
189    fn run_query(
190        self,
191        editor: &mut Editor,
192        project: &Entity<Project>,
193        cx: &mut Context<Editor>,
194    ) -> Option<Task<anyhow::Result<Vec<Location>>>> {
195        match self {
196            LspPickerKind::References => editor.find_all_references_locations(project, cx),
197            LspPickerKind::Definition => {
198                editor.definition_locations_of_kind(GotoDefinitionKind::Symbol, cx)
199            }
200            LspPickerKind::Implementation => {
201                editor.definition_locations_of_kind(GotoDefinitionKind::Implementation, cx)
202            }
203        }
204    }
205}
206
207pub struct LspLocationsPicker {
208    picker: Entity<Picker<LspLocationsDelegate>>,
209    _subscription: Subscription,
210}
211
212impl LspLocationsPicker {
213    fn open_for_editor(
214        kind: LspPickerKind,
215        editor: WeakEntity<Editor>,
216        window: &mut Window,
217        cx: &mut App,
218    ) {
219        let Some(editor) = editor.upgrade() else {
220            return;
221        };
222        let Some(workspace) = editor.read(cx).workspace() else {
223            return;
224        };
225        workspace.update(cx, |workspace, cx| {
226            Self::open(kind, editor, workspace, window, cx);
227        });
228    }
229
230    /// Opens the picker for `kind`: runs a fresh LSP query and shows the
231    /// results. An empty definitions query falls back to references when the
232    /// `go_to_definition_fallback` setting calls for it, matching
233    /// [`Editor::go_to_definition`].
234    fn open(
235        kind: LspPickerKind,
236        editor: Entity<Editor>,
237        workspace: &mut Workspace,
238        window: &mut Window,
239        cx: &mut Context<Workspace>,
240    ) {
241        let project = workspace.project().clone();
242        let fallback = EditorSettings::get_global(cx).go_to_definition_fallback;
243        let editor = editor.downgrade();
244        cx.spawn_in(window, async move |workspace, cx| {
245            // The kind the user invoked, kept for user-facing messages even if
246            // the query below falls back to references.
247            let invoked_kind = kind;
248            let mut kind = kind;
249
250            // Count on the built matches (not raw locations): they are deduped by
251            // range and exclude fileless results, so a single distinct result
252            // jumps directly and a fileless-only result reports "no results"
253            // instead of opening a blank picker.
254            let Some(mut matches) =
255                run_picker_matches(kind, &editor, &workspace, &project, cx).await
256            else {
257                return;
258            };
259
260            if matches.is_empty()
261                && kind == LspPickerKind::Definition
262                && fallback == GoToDefinitionFallback::FindAllReferences
263            {
264                kind = LspPickerKind::References;
265                let Some(references) =
266                    run_picker_matches(kind, &editor, &workspace, &project, cx).await
267                else {
268                    return;
269                };
270                matches = references;
271            }
272
273            if matches.is_empty() {
274                show_no_results_toast(&workspace, invoked_kind, cx);
275                return;
276            }
277
278            if matches.len() == 1 {
279                if let Some(location_match) = matches.into_iter().next() {
280                    let location = Location {
281                        buffer: location_match.buffer,
282                        range: location_match.anchor_range,
283                    };
284                    if let Ok(task) = editor.update_in(cx, |editor, window, cx| {
285                        editor.open_location(location, false, window, cx)
286                    }) {
287                        task.await.log_err();
288                    }
289                }
290                return;
291            }
292
293            workspace
294                .update_in(cx, |workspace, window, cx| {
295                    workspace.toggle_modal(window, cx, |window, cx| {
296                        Self::new(kind, matches, project, editor, window, cx)
297                    });
298                })
299                .log_err();
300        })
301        .detach();
302    }
303
304    fn new(
305        kind: LspPickerKind,
306        matches: Vec<LocationMatch>,
307        project: Entity<Project>,
308        editor: WeakEntity<Editor>,
309        window: &mut Window,
310        cx: &mut Context<Self>,
311    ) -> Self {
312        let preview = picker_preview::editor_preview(project.clone(), window, cx);
313        let delegate = LspLocationsDelegate::new(kind, matches, project, editor);
314        let picker = cx.new(|cx| Picker::list_with_preview(delegate, preview, window, cx));
315        let subscription = cx.subscribe(&picker, |_, _, _: &DismissEvent, cx| {
316            cx.emit(DismissEvent);
317        });
318        Self {
319            picker,
320            _subscription: subscription,
321        }
322    }
323}
324
325impl ModalView for LspLocationsPicker {}
326
327impl EventEmitter<DismissEvent> for LspLocationsPicker {}
328
329impl Focusable for LspLocationsPicker {
330    fn focus_handle(&self, cx: &App) -> FocusHandle {
331        self.picker.focus_handle(cx)
332    }
333}
334
335impl Render for LspLocationsPicker {
336    fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
337        v_flex().child(self.picker.clone())
338    }
339}
340
341struct LocationMatch {
342    path: ProjectPath,
343    buffer: Entity<Buffer>,
344    anchor_range: Range<Anchor>,
345    range: Range<usize>,
346    display_text: String,
347    syntax_highlights: Vec<(Range<usize>, HighlightId)>,
348    match_range: Range<usize>,
349    line_number: u32,
350}
351
352/// A row in the grouped display list: a non-selectable file header, a match, or
353/// a separator between file groups. `selected_index` indexes into this list.
354enum Entry {
355    Header(ProjectPath),
356    Match(usize),
357    Separator,
358}
359
360struct LspLocationsDelegate {
361    kind: LspPickerKind,
362    project: Entity<Project>,
363    editor: WeakEntity<Editor>,
364    all_matches: Vec<LocationMatch>,
365    candidates: Arc<[StringMatchCandidate]>,
366    matches: Vec<usize>,
367    entries: Vec<Entry>,
368    selected_index: usize,
369    max_line_number: u32,
370}
371
372impl LspLocationsDelegate {
373    fn new(
374        kind: LspPickerKind,
375        all_matches: Vec<LocationMatch>,
376        project: Entity<Project>,
377        editor: WeakEntity<Editor>,
378    ) -> Self {
379        // Match against the line text and the file path, mirroring the fuzzy
380        // matching every other Zed picker uses.
381        let candidates = all_matches
382            .iter()
383            .enumerate()
384            .map(|(index, location_match)| {
385                StringMatchCandidate::new(
386                    index,
387                    &format!(
388                        "{} {}",
389                        location_match.display_text,
390                        location_match.path.path.as_unix_str()
391                    ),
392                )
393            })
394            .collect();
395        let matches = (0..all_matches.len()).collect();
396        let mut this = Self {
397            kind,
398            project,
399            editor,
400            all_matches,
401            candidates,
402            matches,
403            entries: Vec::new(),
404            selected_index: 0,
405            max_line_number: 0,
406        };
407        this.rebuild_entries();
408        this
409    }
410
411    /// Rebuilds the grouped [`Self::entries`] from the filtered [`Self::matches`]:
412    /// one header per file, its matches, and a separator before every group
413    /// after the first. Selection snaps to the first selectable row.
414    fn rebuild_entries(&mut self) {
415        let mut entries = Vec::with_capacity(self.matches.len());
416        let mut last_path: Option<&ProjectPath> = None;
417        let mut max_line_number = 0;
418        for &match_index in &self.matches {
419            let location_match = &self.all_matches[match_index];
420            if last_path != Some(&location_match.path) {
421                if last_path.is_some() {
422                    entries.push(Entry::Separator);
423                }
424                entries.push(Entry::Header(location_match.path.clone()));
425                last_path = Some(&location_match.path);
426            }
427            max_line_number = max_line_number.max(location_match.line_number);
428            entries.push(Entry::Match(match_index));
429        }
430        self.entries = entries;
431        self.max_line_number = max_line_number;
432        self.selected_index = self.first_selectable_index().unwrap_or(0);
433    }
434
435    fn first_selectable_index(&self) -> Option<usize> {
436        self.entries
437            .iter()
438            .position(|entry| matches!(entry, Entry::Match(_)))
439    }
440
441    fn selected_location_match(&self) -> Option<&LocationMatch> {
442        match self.entries.get(self.selected_index)? {
443            Entry::Match(match_index) => self.all_matches.get(*match_index),
444            Entry::Header(_) | Entry::Separator => None,
445        }
446    }
447
448    fn open_selected(&mut self, split: bool, window: &mut Window, cx: &mut Context<Picker<Self>>) {
449        let Some(location_match) = self.selected_location_match() else {
450            return;
451        };
452        let location = Location {
453            buffer: location_match.buffer.clone(),
454            range: location_match.anchor_range.clone(),
455        };
456        let Some(editor) = self.editor.upgrade() else {
457            return;
458        };
459        editor
460            .update(cx, |editor, cx| {
461                editor.open_location(location, split, window, cx)
462            })
463            .detach_and_log_err(cx);
464        cx.emit(DismissEvent);
465    }
466}
467
468fn build_location_matches(locations: &[Location], cx: &App) -> Vec<LocationMatch> {
469    use gpui::EntityId;
470    let mut snapshots: HashMap<EntityId, language::BufferSnapshot> = HashMap::default();
471    let mut matches = Vec::with_capacity(locations.len());
472
473    for location in locations {
474        let snapshot = snapshots
475            .entry(location.buffer.entity_id())
476            .or_insert_with(|| location.buffer.read(cx).snapshot());
477
478        let Some(file) = snapshot.file() else {
479            continue;
480        };
481        let path = ProjectPath {
482            worktree_id: file.worktree_id(cx),
483            path: file.path().clone(),
484        };
485
486        let start_offset: usize = snapshot.summary_for_anchor(&location.range.start);
487        let end_offset: usize = snapshot.summary_for_anchor(&location.range.end);
488        let row = snapshot.offset_to_point(start_offset).row;
489        let line_start = snapshot.point_to_offset(Point::new(row, 0));
490        let line_end = snapshot.point_to_offset(Point::new(row, snapshot.line_len(row)));
491        let full_line: String = snapshot.text_for_range(line_start..line_end).collect();
492
493        // The row shows the line with leading indentation trimmed. Offsets below
494        // are relative to that displayed text.
495        let display_text = full_line.trim_start().to_string();
496        let visible_start = line_end.saturating_sub(display_text.len());
497        let visible_end = line_end;
498
499        // Precompute syntax highlights for the displayed text so rendering a row
500        // never re-snapshots the buffer or re-runs highlighting.
501        let mut syntax_highlights = Vec::new();
502        let mut offset = 0;
503        for chunk in snapshot.chunks(
504            visible_start..visible_end,
505            LanguageAwareStyling {
506                tree_sitter: true,
507                diagnostics: false,
508            },
509        ) {
510            let chunk_len = chunk.text.len();
511            if let Some(id) = chunk.syntax_highlight_id {
512                syntax_highlights.push((offset..offset + chunk_len, id));
513            }
514            offset += chunk_len;
515        }
516
517        // The match span, clamped into the displayed text. `clamp` bounds each
518        // endpoint to the line; `min`/`max` then keep the range well-ordered even
519        // for a malformed/inverted LSP range (clamping alone preserves bounds but
520        // not `start <= end`).
521        let clamped_start = start_offset.clamp(visible_start, visible_end) - visible_start;
522        let clamped_end = end_offset.clamp(visible_start, visible_end) - visible_start;
523        let match_range = clamped_start.min(clamped_end)..clamped_start.max(clamped_end);
524
525        matches.push(LocationMatch {
526            path,
527            buffer: location.buffer.clone(),
528            anchor_range: location.range.clone(),
529            range: start_offset..end_offset,
530            display_text,
531            syntax_highlights,
532            match_range,
533            line_number: row + 1,
534        });
535    }
536
537    // Group by file and order by position so the grouped display list is stable,
538    // then drop exact-duplicate ranges a server may report more than once.
539    matches.sort_by(|a, b| a.path.cmp(&b.path).then(a.range.start.cmp(&b.range.start)));
540    matches.dedup_by(|a, b| a.path == b.path && a.range == b.range);
541    matches
542}
543
544impl PickerDelegate for LspLocationsDelegate {
545    type ListItem = AnyElement;
546
547    fn name() -> &'static str {
548        "lsp locations picker"
549    }
550
551    fn placeholder_text(&self, _window: &mut Window, _cx: &mut App) -> std::sync::Arc<str> {
552        self.kind.placeholder().into()
553    }
554
555    fn match_count(&self) -> usize {
556        self.entries.len()
557    }
558
559    fn can_select(&self, ix: usize, _window: &mut Window, _cx: &mut Context<Picker<Self>>) -> bool {
560        matches!(self.entries.get(ix), Some(Entry::Match(_)))
561    }
562
563    fn selected_index(&self) -> usize {
564        self.selected_index
565    }
566
567    fn select_on_hover(&self) -> bool {
568        false
569    }
570
571    fn set_selected_index(
572        &mut self,
573        ix: usize,
574        _window: &mut Window,
575        _cx: &mut Context<Picker<Self>>,
576    ) {
577        self.selected_index = ix;
578    }
579
580    fn update_matches(
581        &mut self,
582        query: String,
583        _window: &mut Window,
584        cx: &mut Context<Picker<Self>>,
585    ) -> Task<()> {
586        let query = query.trim().to_owned();
587        let candidates = self.candidates.clone();
588        cx.spawn(async move |picker, cx| {
589            let matches = if query.is_empty() {
590                (0..candidates.len()).collect()
591            } else {
592                let string_matches = fuzzy::match_strings(
593                    &candidates,
594                    &query,
595                    false,
596                    true,
597                    candidates.len(),
598                    &Default::default(),
599                    cx.background_executor().clone(),
600                )
601                .await;
602                let mut indices = string_matches
603                    .into_iter()
604                    .map(|string_match| string_match.candidate_id)
605                    .collect::<Vec<_>>();
606                // Restore the file-grouped, positional order (fuzzy returns by score).
607                indices.sort_unstable();
608                indices
609            };
610            picker
611                .update(cx, |picker, cx| {
612                    picker.delegate.matches = matches;
613                    picker.delegate.rebuild_entries();
614                    cx.notify();
615                })
616                .ok();
617        })
618    }
619
620    fn confirm(&mut self, secondary: bool, window: &mut Window, cx: &mut Context<Picker<Self>>) {
621        self.open_selected(secondary, window, cx);
622    }
623
624    fn dismissed(&mut self, _window: &mut Window, cx: &mut Context<Picker<Self>>) {
625        cx.emit(DismissEvent);
626    }
627
628    fn try_get_preview_data_for_match(&self, _cx: &App) -> Option<picker::PreviewUpdate> {
629        let location_match = self.selected_location_match()?;
630        Some(picker::PreviewUpdate::from_buffer(
631            location_match.buffer.clone(),
632            picker::MatchLocation {
633                anchor_range: location_match.anchor_range.clone(),
634                range: location_match.range.clone(),
635            },
636        ))
637    }
638
639    fn render_match(
640        &self,
641        ix: usize,
642        selected: bool,
643        _window: &mut Window,
644        cx: &mut Context<Picker<Self>>,
645    ) -> Option<Self::ListItem> {
646        match self.entries.get(ix)? {
647            Entry::Separator => Some(
648                div()
649                    .py(DynamicSpacing::Base04.rems(cx))
650                    .child(Divider::horizontal())
651                    .into_any_element(),
652            ),
653            Entry::Header(path) => {
654                let path_style = self.project.read(cx).path_style(cx);
655                let file_name = path
656                    .path
657                    .file_name()
658                    .map(|name| name.to_string())
659                    .unwrap_or_default();
660                let directory = path
661                    .path
662                    .parent()
663                    .map(|parent| parent.display(path_style))
664                    .map(SharedString::new)
665                    .unwrap_or_default();
666                let file_icon = ItemSettings::get_global(cx)
667                    .file_icons
668                    .then(|| FileIcons::get_icon(path.path.as_std_path(), cx))
669                    .flatten()
670                    .map(|icon| {
671                        Icon::from_path(icon)
672                            .color(Color::Muted)
673                            .size(IconSize::Small)
674                    });
675                Some(
676                    h_flex()
677                        .w_full()
678                        .min_w_0()
679                        .px(DynamicSpacing::Base06.rems(cx))
680                        .py_1()
681                        .gap_1p5()
682                        .children(file_icon)
683                        .child(
684                            h_flex()
685                                .gap_1()
686                                .child(Label::new(file_name).size(LabelSize::Small))
687                                .when(!directory.is_empty(), |this| {
688                                    this.child(
689                                        Label::new(directory)
690                                            .size(LabelSize::Small)
691                                            .color(Color::Muted)
692                                            .truncate_start(),
693                                    )
694                                }),
695                        )
696                        .into_any_element(),
697                )
698            }
699            Entry::Match(match_index) => {
700                let location_match = self.all_matches.get(*match_index)?;
701                Some(
702                    ListItem::new(ix)
703                        .spacing(ListItemSpacing::Sparse)
704                        .inset(true)
705                        .toggle_state(selected)
706                        .child(
707                            h_flex()
708                                .w_full()
709                                .min_w_0()
710                                .gap_2p5()
711                                .text_sm()
712                                .child(
713                                    h_flex()
714                                        .w(rems(
715                                            (self.max_line_number.max(1).ilog10() + 1) as f32 * 0.5,
716                                        ))
717                                        .justify_end()
718                                        .child(
719                                            Label::new(location_match.line_number.to_string())
720                                                .color(Color::Custom(
721                                                    cx.theme().colors().text_muted.opacity(0.5),
722                                                )),
723                                        ),
724                                )
725                                .child(
726                                    div()
727                                        .flex_1()
728                                        .min_w_0()
729                                        .truncate()
730                                        .child(render_matched_line(location_match, cx)),
731                                ),
732                        )
733                        .into_any_element(),
734                )
735            }
736        }
737    }
738}
739
740/// Renders the precomputed displayed line, resolving the stored syntax highlight
741/// ids against the current theme and overlaying the match with a highlighted
742/// background and bold weight.
743fn render_matched_line(location_match: &LocationMatch, cx: &App) -> StyledText {
744    let settings = ThemeSettings::get_global(cx);
745    let text_style = TextStyle {
746        color: cx.theme().colors().text,
747        font_family: settings.buffer_font.family.clone(),
748        font_features: settings.buffer_font.features.clone(),
749        font_fallbacks: settings.buffer_font.fallbacks.clone(),
750        font_size: settings.buffer_font_size(cx).into(),
751        font_weight: settings.buffer_font.weight,
752        line_height: relative(1.),
753        ..Default::default()
754    };
755
756    let syntax_theme = cx.theme().syntax();
757    let syntax_highlights = location_match
758        .syntax_highlights
759        .iter()
760        .filter_map(|(range, id)| Some((range.clone(), syntax_theme.get(*id).copied()?)))
761        .collect::<Vec<_>>();
762
763    let match_style = HighlightStyle {
764        background_color: Some(cx.theme().colors().search_match_background),
765        font_weight: Some(gpui::FontWeight::BOLD),
766        ..Default::default()
767    };
768    let match_highlight = (location_match.match_range.clone(), match_style);
769
770    let highlights = gpui::combine_highlights(syntax_highlights, [match_highlight]);
771    StyledText::new(location_match.display_text.clone())
772        .with_default_highlights(&text_style, highlights)
773}
774
775#[cfg(test)]
776mod tests {
777    use super::*;
778    use editor::test::editor_lsp_test_context::EditorLspTestContext;
779    use gpui::TestAppContext;
780    use indoc::indoc;
781
782    async fn rust_cx(
783        capabilities: lsp::ServerCapabilities,
784        cx: &mut TestAppContext,
785    ) -> EditorLspTestContext {
786        EditorLspTestContext::new_rust(capabilities, cx).await
787    }
788
789    fn open(cx: &mut EditorLspTestContext, kind: LspPickerKind) {
790        let editor = cx.editor.clone();
791        let workspace = cx.workspace.clone();
792        cx.update(|window, cx| {
793            workspace.update(cx, |workspace, cx| {
794                LspLocationsPicker::open(kind, editor, workspace, window, cx);
795            });
796        });
797        cx.run_until_parked();
798    }
799
800    fn active_picker(cx: &mut EditorLspTestContext) -> Option<Entity<LspLocationsPicker>> {
801        let workspace = cx.workspace.clone();
802        cx.update(|_window, cx| workspace.read(cx).active_modal::<LspLocationsPicker>(cx))
803    }
804
805    fn references(uri: lsp::Uri, ranges: &[(u32, u32, u32)]) -> Vec<lsp::Location> {
806        ranges
807            .iter()
808            .map(|&(row, start, end)| lsp::Location {
809                uri: uri.clone(),
810                range: lsp::Range::new(
811                    lsp::Position::new(row, start),
812                    lsp::Position::new(row, end),
813                ),
814            })
815            .collect()
816    }
817
818    const SOURCE: &str = indoc! {r#"
819        fn main() {
820            let aˇbc = 123;
821            let xyz = abc;
822        }
823    "#};
824
825    #[gpui::test]
826    async fn test_multiple_references_open_picker(cx: &mut TestAppContext) {
827        let mut cx = rust_cx(
828            lsp::ServerCapabilities {
829                references_provider: Some(lsp::OneOf::Left(true)),
830                ..Default::default()
831            },
832            cx,
833        )
834        .await;
835        cx.set_state(SOURCE);
836        cx.lsp
837            .set_request_handler::<lsp::request::References, _, _>(async move |params, _| {
838                let uri = params.text_document_position.text_document.uri;
839                Ok(Some(references(uri, &[(1, 8, 11), (2, 14, 17)])))
840            });
841
842        open(&mut cx, LspPickerKind::References);
843
844        assert!(
845            active_picker(&mut cx).is_some(),
846            "multiple references should open the picker"
847        );
848    }
849
850    #[gpui::test]
851    async fn test_single_result_jumps_without_picker(cx: &mut TestAppContext) {
852        let mut cx = rust_cx(
853            lsp::ServerCapabilities {
854                references_provider: Some(lsp::OneOf::Left(true)),
855                ..Default::default()
856            },
857            cx,
858        )
859        .await;
860        cx.set_state(SOURCE);
861        cx.lsp
862            .set_request_handler::<lsp::request::References, _, _>(async move |params, _| {
863                let uri = params.text_document_position.text_document.uri;
864                Ok(Some(references(uri, &[(2, 14, 17)])))
865            });
866
867        open(&mut cx, LspPickerKind::References);
868
869        assert!(
870            active_picker(&mut cx).is_none(),
871            "a single result should jump directly instead of opening the picker"
872        );
873        // The lone result at row 2 should be selected directly, moving the
874        // cursor off its starting position on row 1.
875        cx.assert_editor_state(indoc! {r#"
876            fn main() {
877                let abc = 123;
878                let xyz = «abcˇ»;
879            }
880        "#});
881    }
882
883    #[gpui::test]
884    async fn test_no_results_does_not_open_picker(cx: &mut TestAppContext) {
885        let mut cx = rust_cx(
886            lsp::ServerCapabilities {
887                references_provider: Some(lsp::OneOf::Left(true)),
888                ..Default::default()
889            },
890            cx,
891        )
892        .await;
893        cx.set_state(SOURCE);
894        cx.lsp
895            .set_request_handler::<lsp::request::References, _, _>(async move |_params, _| {
896                Ok(Some(Vec::new()))
897            });
898
899        open(&mut cx, LspPickerKind::References);
900
901        assert!(
902            active_picker(&mut cx).is_none(),
903            "an empty result should not open the picker"
904        );
905    }
906
907    #[gpui::test]
908    async fn test_definition_falls_back_to_references_picker(cx: &mut TestAppContext) {
909        let mut cx = rust_cx(
910            lsp::ServerCapabilities {
911                definition_provider: Some(lsp::OneOf::Left(true)),
912                references_provider: Some(lsp::OneOf::Left(true)),
913                ..Default::default()
914            },
915            cx,
916        )
917        .await;
918        cx.set_state(SOURCE);
919        cx.lsp
920            .set_request_handler::<lsp::request::GotoDefinition, _, _>(async move |_params, _| {
921                Ok(None)
922            });
923        cx.lsp
924            .set_request_handler::<lsp::request::References, _, _>(async move |params, _| {
925                let uri = params.text_document_position.text_document.uri;
926                Ok(Some(references(uri, &[(1, 8, 11), (2, 14, 17)])))
927            });
928
929        open(&mut cx, LspPickerKind::Definition);
930
931        assert!(
932            active_picker(&mut cx).is_some(),
933            "an empty definition query should fall back to the references picker"
934        );
935    }
936
937    #[gpui::test]
938    async fn test_fuzzy_filter_matches_subsequence(cx: &mut TestAppContext) {
939        let mut cx = rust_cx(
940            lsp::ServerCapabilities {
941                references_provider: Some(lsp::OneOf::Left(true)),
942                ..Default::default()
943            },
944            cx,
945        )
946        .await;
947        cx.set_state(SOURCE);
948        cx.lsp
949            .set_request_handler::<lsp::request::References, _, _>(async move |params, _| {
950                let uri = params.text_document_position.text_document.uri;
951                Ok(Some(references(uri, &[(1, 8, 11), (2, 14, 17)])))
952            });
953
954        open(&mut cx, LspPickerKind::References);
955        let modal = active_picker(&mut cx).expect("multiple references should open the picker");
956        let picker = cx.update(|_window, cx| modal.read(cx).picker.clone());
957
958        let matches = |cx: &mut EditorLspTestContext, query: &str| -> usize {
959            cx.update(|window, cx| {
960                picker.update(cx, |picker, cx| picker.set_query(query, window, cx));
961            });
962            cx.run_until_parked();
963            cx.update(|_window, cx| picker.read(cx).delegate.matches.len())
964        };
965
966        // "lx" is a subsequence of "let xyz" but not a substring of either line,
967        // so it only matches with fuzzy matching.
968        assert_eq!(matches(&mut cx, "lx"), 1);
969        assert_eq!(matches(&mut cx, "zzzz"), 0);
970        assert_eq!(matches(&mut cx, ""), 2);
971    }
972}
973
Served at tenant.openagents/omega Member data and write actions are omitted.