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