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