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::*, keymap, AppContext, Axis, Entity, ModelHandle, MutableAppContext,
7 RenderContext, Task, View, ViewContext, ViewHandle, WeakViewHandle,
8};
9use ordered_float::OrderedFloat;
10use project::{Project, Symbol};
11use settings::Settings;
12use std::{
13 borrow::Cow,
14 cmp::{self, Reverse},
15};
16use util::ResultExt;
17use workspace::{
18 menu::{Confirm, SelectFirst, SelectLast, SelectNext, SelectPrev},
19 Workspace,
20};
21
22actions!(project_symbols, [Toggle]);
23
24pub fn init(cx: &mut MutableAppContext) {
25 cx.add_action(ProjectSymbolsView::toggle);
26 cx.add_action(ProjectSymbolsView::confirm);
27 cx.add_action(ProjectSymbolsView::select_prev);
28 cx.add_action(ProjectSymbolsView::select_next);
29 cx.add_action(ProjectSymbolsView::select_first);
30 cx.add_action(ProjectSymbolsView::select_last);
31}
32
33pub struct ProjectSymbolsView {
34 handle: WeakViewHandle<Self>,
35 project: ModelHandle<Project>,
36 selected_match_index: usize,
37 list_state: UniformListState,
38 symbols: Vec<Symbol>,
39 match_candidates: Vec<StringMatchCandidate>,
40 matches: Vec<StringMatch>,
41 pending_symbols_task: Task<Option<()>>,
42 query_editor: ViewHandle<Editor>,
43}
44
45pub enum Event {
46 Dismissed,
47 Selected(Symbol),
48}
49
50impl Entity for ProjectSymbolsView {
51 type Event = Event;
52}
53
54impl View for ProjectSymbolsView {
55 fn ui_name() -> &'static str {
56 "ProjectSymbolsView"
57 }
58
59 fn keymap_context(&self, _: &AppContext) -> keymap::Context {
60 let mut cx = Self::default_keymap_context();
61 cx.set.insert("menu".into());
62 cx
63 }
64
65 fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox {
66 let settings = cx.global::<Settings>();
67 Flex::new(Axis::Vertical)
68 .with_child(
69 Container::new(ChildView::new(&self.query_editor).boxed())
70 .with_style(settings.theme.selector.input_editor.container)
71 .boxed(),
72 )
73 .with_child(
74 FlexItem::new(self.render_matches(cx))
75 .flex(1., false)
76 .boxed(),
77 )
78 .contained()
79 .with_style(settings.theme.selector.container)
80 .constrained()
81 .with_max_width(500.0)
82 .with_max_height(420.0)
83 .aligned()
84 .top()
85 .named("project symbols view")
86 }
87
88 fn on_focus(&mut self, cx: &mut ViewContext<Self>) {
89 cx.focus(&self.query_editor);
90 }
91}
92
93impl ProjectSymbolsView {
94 fn new(project: ModelHandle<Project>, cx: &mut ViewContext<Self>) -> Self {
95 let query_editor = cx.add_view(|cx| {
96 Editor::single_line(Some(|theme| theme.selector.input_editor.clone()), cx)
97 });
98 cx.subscribe(&query_editor, Self::on_query_editor_event)
99 .detach();
100 let mut this = Self {
101 handle: cx.weak_handle(),
102 project,
103 selected_match_index: 0,
104 list_state: Default::default(),
105 symbols: Default::default(),
106 match_candidates: Default::default(),
107 matches: Default::default(),
108 pending_symbols_task: Task::ready(None),
109 query_editor,
110 };
111 this.update_matches(cx);
112 this
113 }
114
115 fn toggle(workspace: &mut Workspace, _: &Toggle, cx: &mut ViewContext<Workspace>) {
116 workspace.toggle_modal(cx, |cx, workspace| {
117 let project = workspace.project().clone();
118 let symbols = cx.add_view(|cx| Self::new(project, cx));
119 cx.subscribe(&symbols, Self::on_event).detach();
120 symbols
121 });
122 }
123
124 fn select_prev(&mut self, _: &SelectPrev, cx: &mut ViewContext<Self>) {
125 if self.selected_match_index > 0 {
126 self.select(self.selected_match_index - 1, cx);
127 }
128 }
129
130 fn select_next(&mut self, _: &SelectNext, cx: &mut ViewContext<Self>) {
131 if self.selected_match_index + 1 < self.matches.len() {
132 self.select(self.selected_match_index + 1, cx);
133 }
134 }
135
136 fn select_first(&mut self, _: &SelectFirst, cx: &mut ViewContext<Self>) {
137 self.select(0, cx);
138 }
139
140 fn select_last(&mut self, _: &SelectLast, cx: &mut ViewContext<Self>) {
141 self.select(self.matches.len().saturating_sub(1), cx);
142 }
143
144 fn select(&mut self, index: usize, cx: &mut ViewContext<Self>) {
145 self.selected_match_index = index;
146 self.list_state.scroll_to(ScrollTarget::Show(index));
147 cx.notify();
148 }
149
150 fn confirm(&mut self, _: &Confirm, cx: &mut ViewContext<Self>) {
151 if let Some(symbol) = self
152 .matches
153 .get(self.selected_match_index)
154 .map(|mat| self.symbols[mat.candidate_id].clone())
155 {
156 cx.emit(Event::Selected(symbol));
157 }
158 }
159
160 fn update_matches(&mut self, cx: &mut ViewContext<Self>) {
161 self.filter(cx);
162 let query = self.query_editor.read(cx).text(cx);
163 let symbols = self
164 .project
165 .update(cx, |project, cx| project.symbols(&query, cx));
166 self.pending_symbols_task = cx.spawn_weak(|this, mut cx| async move {
167 let symbols = symbols.await.log_err()?;
168 if let Some(this) = this.upgrade(&cx) {
169 this.update(&mut cx, |this, cx| {
170 this.match_candidates = symbols
171 .iter()
172 .enumerate()
173 .map(|(id, symbol)| {
174 StringMatchCandidate::new(
175 id,
176 symbol.label.text[symbol.label.filter_range.clone()].to_string(),
177 )
178 })
179 .collect();
180 this.symbols = symbols;
181 this.filter(cx);
182 });
183 }
184 None
185 });
186 }
187
188 fn filter(&mut self, cx: &mut ViewContext<Self>) {
189 let query = self.query_editor.read(cx).text(cx);
190 let mut matches = if query.is_empty() {
191 self.match_candidates
192 .iter()
193 .enumerate()
194 .map(|(candidate_id, candidate)| StringMatch {
195 candidate_id,
196 score: Default::default(),
197 positions: Default::default(),
198 string: candidate.string.clone(),
199 })
200 .collect()
201 } else {
202 smol::block_on(fuzzy::match_strings(
203 &self.match_candidates,
204 &query,
205 false,
206 100,
207 &Default::default(),
208 cx.background().clone(),
209 ))
210 };
211
212 matches.sort_unstable_by_key(|mat| {
213 let label = &self.symbols[mat.candidate_id].label;
214 (
215 Reverse(OrderedFloat(mat.score)),
216 &label.text[label.filter_range.clone()],
217 )
218 });
219
220 for mat in &mut matches {
221 let filter_start = self.symbols[mat.candidate_id].label.filter_range.start;
222 for position in &mut mat.positions {
223 *position += filter_start;
224 }
225 }
226
227 self.matches = matches;
228 self.select_first(&SelectFirst, cx);
229 cx.notify();
230 }
231
232 fn render_matches(&self, cx: &AppContext) -> ElementBox {
233 if self.matches.is_empty() {
234 let settings = cx.global::<Settings>();
235 return Container::new(
236 Label::new(
237 "No matches".into(),
238 settings.theme.selector.empty.label.clone(),
239 )
240 .boxed(),
241 )
242 .with_style(settings.theme.selector.empty.container)
243 .named("empty matches");
244 }
245
246 let handle = self.handle.clone();
247 let list = UniformList::new(
248 self.list_state.clone(),
249 self.matches.len(),
250 move |mut range, items, cx| {
251 let cx = cx.as_ref();
252 let view = handle.upgrade(cx).unwrap();
253 let view = view.read(cx);
254 let start = range.start;
255 range.end = cmp::min(range.end, view.matches.len());
256
257 let show_worktree_root_name =
258 view.project.read(cx).visible_worktrees(cx).count() > 1;
259 items.extend(view.matches[range].iter().enumerate().map(move |(ix, m)| {
260 view.render_match(m, start + ix, show_worktree_root_name, cx)
261 }));
262 },
263 );
264
265 Container::new(list.boxed())
266 .with_margin_top(6.0)
267 .named("matches")
268 }
269
270 fn render_match(
271 &self,
272 string_match: &StringMatch,
273 index: usize,
274 show_worktree_root_name: bool,
275 cx: &AppContext,
276 ) -> ElementBox {
277 let settings = cx.global::<Settings>();
278 let style = if index == self.selected_match_index {
279 &settings.theme.selector.active_item
280 } else {
281 &settings.theme.selector.item
282 };
283 let symbol = &self.symbols[string_match.candidate_id];
284 let syntax_runs = styled_runs_for_code_label(&symbol.label, &settings.theme.editor.syntax);
285
286 let mut path = symbol.path.to_string_lossy();
287 if show_worktree_root_name {
288 let project = self.project.read(cx);
289 if let Some(worktree) = project.worktree_for_id(symbol.worktree_id, cx) {
290 path = Cow::Owned(format!(
291 "{}{}{}",
292 worktree.read(cx).root_name(),
293 std::path::MAIN_SEPARATOR,
294 path.as_ref()
295 ));
296 }
297 }
298
299 Flex::column()
300 .with_child(
301 Text::new(symbol.label.text.clone(), style.label.text.clone())
302 .with_soft_wrap(false)
303 .with_highlights(combine_syntax_and_fuzzy_match_highlights(
304 &symbol.label.text,
305 style.label.text.clone().into(),
306 syntax_runs,
307 &string_match.positions,
308 ))
309 .boxed(),
310 )
311 .with_child(
312 // Avoid styling the path differently when it is selected, since
313 // the symbol's syntax highlighting doesn't change when selected.
314 Label::new(path.to_string(), settings.theme.selector.item.label.clone()).boxed(),
315 )
316 .contained()
317 .with_style(style.container)
318 .boxed()
319 }
320
321 fn on_query_editor_event(
322 &mut self,
323 _: ViewHandle<Editor>,
324 event: &editor::Event,
325 cx: &mut ViewContext<Self>,
326 ) {
327 match event {
328 editor::Event::Blurred => cx.emit(Event::Dismissed),
329 editor::Event::BufferEdited { .. } => self.update_matches(cx),
330 _ => {}
331 }
332 }
333
334 fn on_event(
335 workspace: &mut Workspace,
336 _: ViewHandle<Self>,
337 event: &Event,
338 cx: &mut ViewContext<Workspace>,
339 ) {
340 match event {
341 Event::Dismissed => workspace.dismiss_modal(cx),
342 Event::Selected(symbol) => {
343 let buffer = workspace
344 .project()
345 .update(cx, |project, cx| project.open_buffer_for_symbol(symbol, cx));
346
347 let symbol = symbol.clone();
348 cx.spawn(|workspace, mut cx| async move {
349 let buffer = buffer.await?;
350 workspace.update(&mut cx, |workspace, cx| {
351 let position = buffer
352 .read(cx)
353 .clip_point_utf16(symbol.range.start, Bias::Left);
354
355 let editor = workspace.open_project_item::<Editor>(buffer, cx);
356 editor.update(cx, |editor, cx| {
357 editor.select_ranges(
358 [position..position],
359 Some(Autoscroll::Center),
360 cx,
361 );
362 });
363 });
364 Ok::<_, anyhow::Error>(())
365 })
366 .detach_and_log_err(cx);
367 workspace.dismiss_modal(cx);
368 }
369 }
370 }
371}