1use editor::{
2 combine_syntax_and_fuzzy_match_highlights, styled_runs_for_code_label, Autoscroll, Bias, Editor,
3};
4use fuzzy::{StringMatch, StringMatchCandidate};
5use gpui::{
6 actions, elements::*, AppContext, Entity, ModelHandle, MutableAppContext, RenderContext, Task,
7 View, ViewContext, ViewHandle,
8};
9use ordered_float::OrderedFloat;
10use picker::{Picker, PickerDelegate};
11use project::{Project, Symbol};
12use settings::Settings;
13use std::{borrow::Cow, cmp::Reverse};
14use util::ResultExt;
15use workspace::Workspace;
16
17actions!(project_symbols, [Toggle]);
18
19pub fn init(cx: &mut MutableAppContext) {
20 cx.add_action(ProjectSymbolsView::toggle);
21 Picker::<ProjectSymbolsView>::init(cx);
22}
23
24pub struct ProjectSymbolsView {
25 picker: ViewHandle<Picker<Self>>,
26 project: ModelHandle<Project>,
27 selected_match_index: usize,
28 symbols: Vec<Symbol>,
29 match_candidates: Vec<StringMatchCandidate>,
30 show_worktree_root_name: bool,
31 matches: Vec<StringMatch>,
32}
33
34pub enum Event {
35 Dismissed,
36 Selected(Symbol),
37}
38
39impl Entity for ProjectSymbolsView {
40 type Event = Event;
41}
42
43impl View for ProjectSymbolsView {
44 fn ui_name() -> &'static str {
45 "ProjectSymbolsView"
46 }
47
48 fn render(&mut self, _: &mut RenderContext<Self>) -> ElementBox {
49 ChildView::new(self.picker.clone()).boxed()
50 }
51
52 fn on_focus(&mut self, cx: &mut ViewContext<Self>) {
53 cx.focus(&self.picker);
54 }
55}
56
57impl ProjectSymbolsView {
58 fn new(project: ModelHandle<Project>, cx: &mut ViewContext<Self>) -> Self {
59 let handle = cx.weak_handle();
60 Self {
61 project,
62 picker: cx.add_view(|cx| Picker::new(handle, cx)),
63 selected_match_index: 0,
64 symbols: Default::default(),
65 match_candidates: Default::default(),
66 matches: Default::default(),
67 show_worktree_root_name: false,
68 }
69 }
70
71 fn toggle(workspace: &mut Workspace, _: &Toggle, cx: &mut ViewContext<Workspace>) {
72 workspace.toggle_modal(cx, |cx, workspace| {
73 let project = workspace.project().clone();
74 let symbols = cx.add_view(|cx| Self::new(project, cx));
75 cx.subscribe(&symbols, Self::on_event).detach();
76 symbols
77 });
78 }
79
80 fn filter(&mut self, query: &str, cx: &mut ViewContext<Self>) {
81 let mut matches = if query.is_empty() {
82 self.match_candidates
83 .iter()
84 .enumerate()
85 .map(|(candidate_id, candidate)| StringMatch {
86 candidate_id,
87 score: Default::default(),
88 positions: Default::default(),
89 string: candidate.string.clone(),
90 })
91 .collect()
92 } else {
93 smol::block_on(fuzzy::match_strings(
94 &self.match_candidates,
95 query,
96 false,
97 100,
98 &Default::default(),
99 cx.background().clone(),
100 ))
101 };
102
103 matches.sort_unstable_by_key(|mat| {
104 let label = &self.symbols[mat.candidate_id].label;
105 (
106 Reverse(OrderedFloat(mat.score)),
107 &label.text[label.filter_range.clone()],
108 )
109 });
110
111 for mat in &mut matches {
112 let filter_start = self.symbols[mat.candidate_id].label.filter_range.start;
113 for position in &mut mat.positions {
114 *position += filter_start;
115 }
116 }
117
118 self.matches = matches;
119 self.set_selected_index(0, cx);
120 cx.notify();
121 }
122
123 fn on_event(
124 workspace: &mut Workspace,
125 _: ViewHandle<Self>,
126 event: &Event,
127 cx: &mut ViewContext<Workspace>,
128 ) {
129 match event {
130 Event::Dismissed => workspace.dismiss_modal(cx),
131 Event::Selected(symbol) => {
132 let buffer = workspace
133 .project()
134 .update(cx, |project, cx| project.open_buffer_for_symbol(symbol, cx));
135
136 let symbol = symbol.clone();
137 cx.spawn(|workspace, mut cx| async move {
138 let buffer = buffer.await?;
139 workspace.update(&mut cx, |workspace, cx| {
140 let position = buffer
141 .read(cx)
142 .clip_point_utf16(symbol.range.start, Bias::Left);
143
144 let editor = workspace.open_project_item::<Editor>(buffer, cx);
145 editor.update(cx, |editor, cx| {
146 editor.select_ranges(
147 [position..position],
148 Some(Autoscroll::Center),
149 cx,
150 );
151 });
152 });
153 Ok::<_, anyhow::Error>(())
154 })
155 .detach_and_log_err(cx);
156 workspace.dismiss_modal(cx);
157 }
158 }
159 }
160}
161
162impl PickerDelegate for ProjectSymbolsView {
163 fn confirm(&mut self, cx: &mut ViewContext<Self>) {
164 if let Some(symbol) = self
165 .matches
166 .get(self.selected_match_index)
167 .map(|mat| self.symbols[mat.candidate_id].clone())
168 {
169 cx.emit(Event::Selected(symbol));
170 }
171 }
172
173 fn dismiss(&mut self, cx: &mut ViewContext<Self>) {
174 cx.emit(Event::Dismissed);
175 }
176
177 fn match_count(&self) -> usize {
178 self.matches.len()
179 }
180
181 fn selected_index(&self) -> usize {
182 self.selected_match_index
183 }
184
185 fn set_selected_index(&mut self, ix: usize, cx: &mut ViewContext<Self>) {
186 self.selected_match_index = ix;
187 cx.notify();
188 }
189
190 fn update_matches(&mut self, query: String, cx: &mut ViewContext<Self>) -> Task<()> {
191 self.filter(&query, cx);
192 self.show_worktree_root_name = self.project.read(cx).visible_worktrees(cx).count() > 1;
193 let symbols = self
194 .project
195 .update(cx, |project, cx| project.symbols(&query, cx));
196 cx.spawn_weak(|this, mut cx| async move {
197 let symbols = symbols.await.log_err();
198 if let Some(this) = this.upgrade(&cx) {
199 if let Some(symbols) = symbols {
200 this.update(&mut cx, |this, cx| {
201 this.match_candidates = symbols
202 .iter()
203 .enumerate()
204 .map(|(id, symbol)| {
205 StringMatchCandidate::new(
206 id,
207 symbol.label.text[symbol.label.filter_range.clone()]
208 .to_string(),
209 )
210 })
211 .collect();
212 this.symbols = symbols;
213 this.filter(&query, cx);
214 });
215 }
216 }
217 })
218 }
219
220 fn render_match(&self, ix: usize, selected: bool, cx: &AppContext) -> ElementBox {
221 let string_match = &self.matches[ix];
222 let settings = cx.global::<Settings>();
223 let style = if selected {
224 &settings.theme.selector.active_item
225 } else {
226 &settings.theme.selector.item
227 };
228 let symbol = &self.symbols[string_match.candidate_id];
229 let syntax_runs = styled_runs_for_code_label(&symbol.label, &settings.theme.editor.syntax);
230
231 let mut path = symbol.path.to_string_lossy();
232 if self.show_worktree_root_name {
233 let project = self.project.read(cx);
234 if let Some(worktree) = project.worktree_for_id(symbol.worktree_id, cx) {
235 path = Cow::Owned(format!(
236 "{}{}{}",
237 worktree.read(cx).root_name(),
238 std::path::MAIN_SEPARATOR,
239 path.as_ref()
240 ));
241 }
242 }
243
244 Flex::column()
245 .with_child(
246 Text::new(symbol.label.text.clone(), style.label.text.clone())
247 .with_soft_wrap(false)
248 .with_highlights(combine_syntax_and_fuzzy_match_highlights(
249 &symbol.label.text,
250 style.label.text.clone().into(),
251 syntax_runs,
252 &string_match.positions,
253 ))
254 .boxed(),
255 )
256 .with_child(
257 // Avoid styling the path differently when it is selected, since
258 // the symbol's syntax highlighting doesn't change when selected.
259 Label::new(path.to_string(), settings.theme.selector.item.label.clone()).boxed(),
260 )
261 .contained()
262 .with_style(style.container)
263 .boxed()
264 }
265}