Skip to repository content

tenant.openagents/omega

No repository description is available.

OpenAgents Git authority 2026-07-28T04:01:46.425Z 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

command_palette.rs

1358 lines · 45.8 KB · rust
1mod persistence;
2
3use std::{
4    cmp::{self, Reverse},
5    collections::{HashMap, VecDeque},
6    sync::Arc,
7    time::Duration,
8};
9
10use client::parse_zed_link;
11use command_palette_hooks::{
12    CommandInterceptItem, CommandInterceptResult, CommandPaletteFilter,
13    GlobalCommandPaletteInterceptor,
14};
15
16use fuzzy_nucleo::{StringMatch, StringMatchCandidate};
17use gpui::{
18    Action, App, Context, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable,
19    ParentElement, Render, Styled, Task, TaskExt, WeakEntity, Window,
20};
21use persistence::CommandPaletteDB;
22use picker::Direction;
23use picker::{Picker, PickerDelegate};
24use postage::{sink::Sink, stream::Stream};
25use settings::Settings;
26use ui::{HighlightedLabel, KeyBinding, ListItem, ListItemSpacing, prelude::*};
27use util::ResultExt;
28use workspace::{ModalView, Workspace, WorkspaceSettings};
29use zed_actions::{OpenAppUrl, command_palette::Toggle};
30
31pub fn init(cx: &mut App) {
32    command_palette_hooks::init(cx);
33    cx.observe_new(CommandPalette::register).detach();
34}
35
36impl ModalView for CommandPalette {}
37
38pub struct CommandPalette {
39    picker: Entity<Picker<CommandPaletteDelegate>>,
40}
41
42/// Removes subsequent whitespace characters and double colons from the query, and converts
43/// underscores to spaces.
44///
45/// This improves the likelihood of a match by either humanized name or keymap-style name.
46/// Underscores are converted to spaces because `humanize_action_name` converts them to spaces
47/// when building the search candidates (e.g. `terminal_panel::Toggle` -> `terminal panel: toggle`).
48pub fn normalize_action_query(input: &str) -> String {
49    let mut result = String::with_capacity(input.len());
50    let mut last_char = None;
51
52    for char in input.trim().chars() {
53        let normalized_char = if char == '_' { ' ' } else { char };
54        match (last_char, normalized_char) {
55            (Some(':'), ':') => continue,
56            (Some(last_char), c) if last_char.is_whitespace() && c.is_whitespace() => {
57                continue;
58            }
59            _ => {
60                last_char = Some(normalized_char);
61            }
62        }
63        result.push(normalized_char);
64    }
65
66    result
67}
68
69impl CommandPalette {
70    fn register(
71        workspace: &mut Workspace,
72        _window: Option<&mut Window>,
73        _: &mut Context<Workspace>,
74    ) {
75        workspace.register_action(|workspace, _: &Toggle, window, cx| {
76            Self::toggle(workspace, "", window, cx)
77        });
78    }
79
80    pub fn toggle(
81        workspace: &mut Workspace,
82        query: &str,
83        window: &mut Window,
84        cx: &mut Context<Workspace>,
85    ) {
86        if workspace.active_modal::<CommandPalette>(cx).is_some() {
87            workspace.hide_modal(window, cx);
88            return;
89        }
90
91        if workspace.has_active_modal(window, cx) && !workspace.hide_modal(window, cx) {
92            return;
93        }
94
95        let Some(previous_focus_handle) = window.focused(cx) else {
96            return;
97        };
98
99        let entity = cx.weak_entity();
100        workspace.toggle_modal(window, cx, move |window, cx| {
101            CommandPalette::new(previous_focus_handle, query, entity, window, cx)
102        });
103    }
104
105    fn new(
106        previous_focus_handle: FocusHandle,
107        query: &str,
108        entity: WeakEntity<Workspace>,
109        window: &mut Window,
110        cx: &mut Context<Self>,
111    ) -> Self {
112        let filter = CommandPaletteFilter::try_global(cx);
113
114        let commands = window
115            .available_actions(cx)
116            .into_iter()
117            .filter_map(|action| {
118                if filter.is_some_and(|filter| filter.is_hidden(&*action)) {
119                    return None;
120                }
121
122                Some(Command {
123                    name: humanize_action_name(action.name()),
124                    action,
125                })
126            })
127            .collect();
128
129        let delegate = CommandPaletteDelegate::new(
130            cx.entity().downgrade(),
131            entity,
132            commands,
133            previous_focus_handle,
134        );
135
136        let picker = cx.new(|cx| {
137            // One-shot action; there's nothing to reopen.
138            let picker = Picker::uniform_list(delegate, window, cx)
139                .reopenable(false, cx)
140                .show_scrollbar(true);
141            picker.set_query(query, window, cx);
142            picker
143        });
144        Self { picker }
145    }
146
147    pub fn set_query(&mut self, query: &str, window: &mut Window, cx: &mut Context<Self>) {
148        self.picker
149            .update(cx, |picker, cx| picker.set_query(query, window, cx))
150    }
151}
152
153impl EventEmitter<DismissEvent> for CommandPalette {}
154
155impl Focusable for CommandPalette {
156    fn focus_handle(&self, cx: &App) -> FocusHandle {
157        self.picker.focus_handle(cx)
158    }
159}
160
161impl Render for CommandPalette {
162    fn render(&mut self, _window: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
163        v_flex()
164            .key_context("CommandPalette")
165            .child(self.picker.clone())
166    }
167}
168
169pub struct CommandPaletteDelegate {
170    latest_query: String,
171    command_palette: WeakEntity<CommandPalette>,
172    workspace: WeakEntity<Workspace>,
173    all_commands: Vec<Command>,
174    commands: Vec<Command>,
175    matches: Vec<StringMatch>,
176    selected_ix: usize,
177    previous_focus_handle: FocusHandle,
178    updating_matches: Option<(
179        Task<()>,
180        postage::dispatch::Receiver<(Vec<Command>, Vec<StringMatch>, CommandInterceptResult)>,
181    )>,
182    query_history: QueryHistory,
183}
184
185struct Command {
186    name: String,
187    action: Box<dyn Action>,
188}
189
190#[derive(Default)]
191struct QueryHistory {
192    history: Option<VecDeque<String>>,
193    cursor: Option<usize>,
194    prefix: Option<String>,
195}
196
197impl QueryHistory {
198    fn history(&mut self, cx: &App) -> &mut VecDeque<String> {
199        self.history.get_or_insert_with(|| {
200            CommandPaletteDB::global(cx)
201                .list_recent_queries()
202                .unwrap_or_default()
203                .into_iter()
204                .collect()
205        })
206    }
207
208    fn add(&mut self, query: String, cx: &App) {
209        if let Some(pos) = self.history(cx).iter().position(|h| h == &query) {
210            self.history(cx).remove(pos);
211        }
212        self.history(cx).push_back(query);
213        self.cursor = None;
214        self.prefix = None;
215    }
216
217    fn validate_cursor(&mut self, current_query: &str, cx: &App) -> Option<usize> {
218        if let Some(pos) = self.cursor {
219            if self.history(cx).get(pos).map(|s| s.as_str()) != Some(current_query) {
220                self.cursor = None;
221                self.prefix = None;
222            }
223        }
224        self.cursor
225    }
226
227    fn previous(&mut self, current_query: &str, cx: &App) -> Option<&str> {
228        if self.validate_cursor(current_query, cx).is_none() {
229            self.prefix = Some(current_query.to_string());
230        }
231
232        let prefix = self.prefix.clone().unwrap_or_default();
233        let start_index = self.cursor.unwrap_or(self.history(cx).len());
234
235        for i in (0..start_index).rev() {
236            if self
237                .history(cx)
238                .get(i)
239                .is_some_and(|e| e.starts_with(&prefix))
240            {
241                self.cursor = Some(i);
242                return self.history(cx).get(i).map(|s| s.as_str());
243            }
244        }
245        None
246    }
247
248    fn next(&mut self, current_query: &str, cx: &App) -> Option<&str> {
249        let selected = self.validate_cursor(current_query, cx)?;
250        let prefix = self.prefix.clone().unwrap_or_default();
251
252        for i in (selected + 1)..self.history(cx).len() {
253            if self
254                .history(cx)
255                .get(i)
256                .is_some_and(|e| e.starts_with(&prefix))
257            {
258                self.cursor = Some(i);
259                return self.history(cx).get(i).map(|s| s.as_str());
260            }
261        }
262        None
263    }
264
265    fn reset_cursor(&mut self) {
266        self.cursor = None;
267        self.prefix = None;
268    }
269
270    fn is_navigating(&self) -> bool {
271        self.cursor.is_some()
272    }
273}
274
275impl Clone for Command {
276    fn clone(&self) -> Self {
277        Self {
278            name: self.name.clone(),
279            action: self.action.boxed_clone(),
280        }
281    }
282}
283
284impl CommandPaletteDelegate {
285    fn new(
286        command_palette: WeakEntity<CommandPalette>,
287        workspace: WeakEntity<Workspace>,
288        commands: Vec<Command>,
289        previous_focus_handle: FocusHandle,
290    ) -> Self {
291        Self {
292            command_palette,
293            workspace,
294            all_commands: commands.clone(),
295            matches: vec![],
296            commands,
297            selected_ix: 0,
298            previous_focus_handle,
299            latest_query: String::new(),
300            updating_matches: None,
301            query_history: Default::default(),
302        }
303    }
304
305    fn matches_updated(
306        &mut self,
307        query: String,
308        mut commands: Vec<Command>,
309        mut matches: Vec<StringMatch>,
310        intercept_result: CommandInterceptResult,
311        _: &mut Context<Picker<Self>>,
312    ) {
313        self.updating_matches.take();
314        self.latest_query = query;
315
316        let mut new_matches = Vec::new();
317
318        for CommandInterceptItem {
319            action,
320            string,
321            positions,
322        } in intercept_result.results
323        {
324            if let Some(idx) = matches
325                .iter()
326                .position(|m| commands[m.candidate_id].action.partial_eq(&*action))
327            {
328                matches.remove(idx);
329            }
330            commands.push(Command {
331                name: string.clone(),
332                action,
333            });
334            new_matches.push(StringMatch {
335                candidate_id: commands.len() - 1,
336                string: string.into(),
337                positions,
338                score: 0.0,
339            })
340        }
341        if !intercept_result.exclusive {
342            new_matches.append(&mut matches);
343        }
344        self.commands = commands;
345        self.matches = new_matches;
346        if self.matches.is_empty() {
347            self.selected_ix = 0;
348        } else {
349            self.selected_ix = cmp::min(self.selected_ix, self.matches.len() - 1);
350        }
351    }
352
353    /// Hit count for each command in the palette.
354    /// We only account for commands triggered directly via command palette and not by e.g. keystrokes because
355    /// if a user already knows a keystroke for a command, they are unlikely to use a command palette to look for it.
356    fn hit_counts(&self, cx: &App) -> HashMap<String, u16> {
357        if let Ok(commands) = CommandPaletteDB::global(cx).list_commands_used() {
358            commands
359                .into_iter()
360                .map(|command| (command.command_name, command.invocations))
361                .collect()
362        } else {
363            HashMap::new()
364        }
365    }
366
367    fn selected_command(&self) -> Option<&Command> {
368        if self.matches.is_empty() {
369            return None;
370        }
371        let action_ix = self
372            .matches
373            .get(self.selected_ix)
374            .map(|m| m.candidate_id)
375            .unwrap_or(self.selected_ix);
376        // this gets called in headless tests where there are no commands loaded
377        // so we need to return an Option here
378        self.commands.get(action_ix)
379    }
380
381    #[cfg(any(test, feature = "test-support"))]
382    pub fn seed_history(&mut self, queries: &[&str]) {
383        self.query_history.history = Some(queries.iter().map(|s| s.to_string()).collect());
384    }
385}
386
387impl PickerDelegate for CommandPaletteDelegate {
388    type ListItem = ListItem;
389
390    fn name() -> &'static str {
391        "command palette"
392    }
393
394    fn placeholder_text(&self, _window: &mut Window, _cx: &mut App) -> Arc<str> {
395        "Execute a command...".into()
396    }
397
398    fn select_history(
399        &mut self,
400        direction: Direction,
401        query: &str,
402        _window: &mut Window,
403        cx: &mut App,
404    ) -> Option<String> {
405        match direction {
406            Direction::Up => {
407                let should_use_history =
408                    self.selected_ix == 0 || self.query_history.is_navigating();
409                if should_use_history {
410                    if let Some(query) = self
411                        .query_history
412                        .previous(query, cx)
413                        .map(|s| s.to_string())
414                    {
415                        return Some(query);
416                    }
417                }
418            }
419            Direction::Down => {
420                if self.query_history.is_navigating() {
421                    if let Some(query) = self.query_history.next(query, cx).map(|s| s.to_string()) {
422                        return Some(query);
423                    } else {
424                        let prefix = self.query_history.prefix.take().unwrap_or_default();
425                        self.query_history.reset_cursor();
426                        return Some(prefix);
427                    }
428                }
429            }
430        }
431        None
432    }
433
434    fn match_count(&self) -> usize {
435        self.matches.len()
436    }
437
438    fn selected_index(&self) -> usize {
439        self.selected_ix
440    }
441
442    fn set_selected_index(
443        &mut self,
444        ix: usize,
445        _window: &mut Window,
446        _: &mut Context<Picker<Self>>,
447    ) {
448        self.selected_ix = ix;
449    }
450
451    fn update_matches(
452        &mut self,
453        mut query: String,
454        window: &mut Window,
455        cx: &mut Context<Picker<Self>>,
456    ) -> gpui::Task<()> {
457        let settings = WorkspaceSettings::get_global(cx);
458        if let Some(alias) = settings.command_aliases.get(&query) {
459            query = alias.as_ref().to_owned();
460        }
461
462        let workspace = self.workspace.clone();
463
464        let intercept_task = GlobalCommandPaletteInterceptor::intercept(&query, workspace, cx);
465
466        let (mut tx, mut rx) = postage::dispatch::channel(1);
467
468        let query_str = query.as_str();
469        let is_zed_link = parse_zed_link(query_str, cx).is_some();
470
471        let task = cx.background_spawn({
472            let mut commands = self.all_commands.clone();
473            let hit_counts = self.hit_counts(cx);
474            let executor = cx.background_executor().clone();
475            let query = normalize_action_query(query_str);
476            let query_for_link = query_str.to_string();
477            async move {
478                commands.sort_by_key(|action| {
479                    (
480                        Reverse(hit_counts.get(&action.name).cloned()),
481                        action.name.clone(),
482                    )
483                });
484
485                let candidates = commands
486                    .iter()
487                    .enumerate()
488                    .map(|(ix, command)| StringMatchCandidate::new(ix, &command.name))
489                    .collect::<Vec<_>>();
490
491                let matches = fuzzy_nucleo::match_strings_async(
492                    &candidates,
493                    &query,
494                    fuzzy_nucleo::Case::Smart,
495                    fuzzy_nucleo::LengthPenalty::On,
496                    10000,
497                    &Default::default(),
498                    executor,
499                )
500                .await;
501
502                let intercept_result = if is_zed_link {
503                    CommandInterceptResult {
504                        results: vec![CommandInterceptItem {
505                            action: OpenAppUrl {
506                                url: query_for_link.clone().into(),
507                            }
508                            .boxed_clone(),
509                            string: query_for_link,
510                            positions: vec![],
511                        }],
512                        exclusive: false,
513                    }
514                } else if let Some(task) = intercept_task {
515                    task.await
516                } else {
517                    CommandInterceptResult::default()
518                };
519
520                tx.send((commands, matches, intercept_result))
521                    .await
522                    .log_err();
523            }
524        });
525
526        self.updating_matches = Some((task, rx.clone()));
527
528        cx.spawn_in(window, async move |picker, cx| {
529            let Some((commands, matches, intercept_result)) = rx.recv().await else {
530                return;
531            };
532
533            picker
534                .update(cx, |picker, cx| {
535                    picker
536                        .delegate
537                        .matches_updated(query, commands, matches, intercept_result, cx)
538                })
539                .ok();
540        })
541    }
542
543    fn finalize_update_matches(
544        &mut self,
545        query: String,
546        duration: Duration,
547        _: &mut Window,
548        cx: &mut Context<Picker<Self>>,
549    ) -> bool {
550        let Some((task, rx)) = self.updating_matches.take() else {
551            return true;
552        };
553
554        match cx
555            .foreground_executor()
556            .block_with_timeout(duration, rx.clone().recv())
557        {
558            Ok(Some((commands, matches, interceptor_result))) => {
559                self.matches_updated(query, commands, matches, interceptor_result, cx);
560                true
561            }
562            _ => {
563                self.updating_matches = Some((task, rx));
564                false
565            }
566        }
567    }
568
569    fn dismissed(&mut self, _window: &mut Window, cx: &mut Context<Picker<Self>>) {
570        self.command_palette
571            .update(cx, |_, cx| cx.emit(DismissEvent))
572            .ok();
573    }
574
575    fn confirm(&mut self, secondary: bool, window: &mut Window, cx: &mut Context<Picker<Self>>) {
576        if secondary {
577            if self.matches.is_empty() {
578                return;
579            }
580            let Some(selected_command) = self.selected_command() else {
581                return;
582            };
583            let action_name = selected_command.action.name();
584            let open_keymap = Box::new(zed_actions::ChangeKeybinding {
585                action: action_name.to_string(),
586            });
587            window.dispatch_action(open_keymap, cx);
588            self.dismissed(window, cx);
589            return;
590        }
591
592        if self.matches.is_empty() {
593            self.dismissed(window, cx);
594            return;
595        }
596
597        if !self.latest_query.is_empty() {
598            self.query_history.add(self.latest_query.clone(), cx);
599            self.query_history.reset_cursor();
600        }
601
602        let action_ix = self.matches[self.selected_ix].candidate_id;
603        let command = self.commands.swap_remove(action_ix);
604        telemetry::event!(
605            "Action Invoked",
606            source = "command palette",
607            action = command.name
608        );
609        self.matches.clear();
610        self.commands.clear();
611        let command_name = command.name.clone();
612        let latest_query = self.latest_query.clone();
613        let db = CommandPaletteDB::global(cx);
614        cx.background_spawn(async move {
615            db.write_command_invocation(command_name, latest_query)
616                .await
617        })
618        .detach_and_log_err(cx);
619        let action = command.action;
620        window.focus(&self.previous_focus_handle, cx);
621        self.dismissed(window, cx);
622        window.dispatch_action(action, cx);
623    }
624
625    fn render_match(
626        &self,
627        ix: usize,
628        selected: bool,
629        _: &mut Window,
630        cx: &mut Context<Picker<Self>>,
631    ) -> Option<Self::ListItem> {
632        let matching_command = self.matches.get(ix)?;
633        let command = self.commands.get(matching_command.candidate_id)?;
634
635        Some(
636            ListItem::new(ix)
637                .inset(true)
638                .spacing(ListItemSpacing::Sparse)
639                .toggle_state(selected)
640                .child(
641                    h_flex()
642                        .w_full()
643                        .py_px()
644                        .justify_between()
645                        .child(HighlightedLabel::new(
646                            command.name.clone(),
647                            matching_command.positions.clone(),
648                        ))
649                        .child(KeyBinding::for_action_in(
650                            &*command.action,
651                            &self.previous_focus_handle,
652                            cx,
653                        )),
654                ),
655        )
656    }
657
658    fn render_footer(
659        &self,
660        window: &mut Window,
661        cx: &mut Context<Picker<Self>>,
662    ) -> Option<AnyElement> {
663        let selected_command = self.selected_command()?;
664        let keybind =
665            KeyBinding::for_action_in(&*selected_command.action, &self.previous_focus_handle, cx);
666
667        let focus_handle = &self.previous_focus_handle;
668        let keybinding_buttons = if keybind.has_binding(window) {
669            Button::new("change", "Change Keybinding…")
670                .key_binding(
671                    KeyBinding::for_action_in(&menu::SecondaryConfirm, focus_handle, cx)
672                        .map(|kb| kb.size(rems_from_px(12.))),
673                )
674                .on_click(move |_, window, cx| {
675                    window.dispatch_action(menu::SecondaryConfirm.boxed_clone(), cx);
676                })
677        } else {
678            Button::new("add", "Add Keybinding…")
679                .key_binding(
680                    KeyBinding::for_action_in(&menu::SecondaryConfirm, focus_handle, cx)
681                        .map(|kb| kb.size(rems_from_px(12.))),
682                )
683                .on_click(move |_, window, cx| {
684                    window.dispatch_action(menu::SecondaryConfirm.boxed_clone(), cx);
685                })
686        };
687
688        Some(
689            h_flex()
690                .w_full()
691                .p_1p5()
692                .gap_1()
693                .justify_end()
694                .border_t_1()
695                .border_color(cx.theme().colors().border_variant)
696                .child(keybinding_buttons)
697                .child(
698                    Button::new("run-action", "Run")
699                        .key_binding(
700                            KeyBinding::for_action_in(&menu::Confirm, &focus_handle, cx)
701                                .map(|kb| kb.size(rems_from_px(12.))),
702                        )
703                        .on_click(|_, window, cx| {
704                            window.dispatch_action(menu::Confirm.boxed_clone(), cx)
705                        }),
706                )
707                .into_any(),
708        )
709    }
710}
711
712pub fn humanize_action_name(name: &str) -> String {
713    let chars = name.chars().collect::<Vec<_>>();
714    let capacity = name.len() + chars.iter().filter(|c| c.is_uppercase()).count();
715    let mut result = String::with_capacity(capacity);
716    let mut index = 0;
717
718    while index < chars.len() {
719        let char = chars[index];
720        if char == ':' {
721            if result.ends_with(':') {
722                result.push(' ');
723            } else {
724                result.push(':');
725            }
726            index += 1;
727        } else if char == '_' {
728            result.push(' ');
729            index += 1;
730        } else if char.is_uppercase() {
731            let start = index;
732            index += 1;
733            while chars
734                .get(index)
735                .is_some_and(|next_char| next_char.is_uppercase())
736            {
737                index += 1;
738            }
739
740            let uppercase_run = &chars[start..index];
741            if uppercase_run.len() > 1 {
742                let split_before_last = chars
743                    .get(index)
744                    .is_some_and(|next_char| next_char.is_lowercase());
745                let acronym_end = if split_before_last {
746                    uppercase_run.len() - 1
747                } else {
748                    uppercase_run.len()
749                };
750
751                if acronym_end > 0 {
752                    if !result.ends_with(' ') {
753                        result.push(' ');
754                    }
755                    result.extend(&uppercase_run[..acronym_end]);
756                }
757
758                if split_before_last {
759                    if !result.ends_with(' ') {
760                        result.push(' ');
761                    }
762                    result.extend(uppercase_run[acronym_end].to_lowercase());
763                }
764            } else {
765                if !result.ends_with(' ') {
766                    result.push(' ');
767                }
768                result.extend(char.to_lowercase());
769            }
770        } else {
771            result.push(char);
772            index += 1;
773        }
774    }
775
776    result
777}
778
779impl std::fmt::Debug for Command {
780    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
781        f.debug_struct("Command")
782            .field("name", &self.name)
783            .finish_non_exhaustive()
784    }
785}
786
787#[cfg(test)]
788mod tests {
789    use std::sync::Arc;
790
791    use super::*;
792    use editor::Editor;
793    use go_to_line::GoToLine;
794    use gpui::{TestAppContext, VisualTestContext};
795    use language::Point;
796    use project::Project;
797    use settings::KeymapFile;
798    use workspace::{AppState, MultiWorkspace, Workspace};
799
800    #[test]
801    fn test_humanize_action_name() {
802        assert_eq!(
803            humanize_action_name("editor::GoToDefinition"),
804            "editor: go to definition"
805        );
806        assert_eq!(
807            humanize_action_name("editor::Backspace"),
808            "editor: backspace"
809        );
810        assert_eq!(
811            humanize_action_name("go_to_line::Deploy"),
812            "go to line: deploy"
813        );
814        assert_eq!(
815            humanize_action_name("agent::OpenGlobalAGENTS.mdRules"),
816            "agent: open global AGENTS.md rules"
817        );
818        assert_eq!(
819            humanize_action_name("agent::OpenProjectAGENTS.mdRules"),
820            "agent: open project AGENTS.md rules"
821        );
822        assert_eq!(humanize_action_name("editor::OpenURL"), "editor: open URL");
823        assert_eq!(
824            humanize_action_name("editor::OpenURLParser"),
825            "editor: open URL parser"
826        );
827    }
828
829    #[test]
830    fn test_normalize_query() {
831        assert_eq!(
832            normalize_action_query("editor: backspace"),
833            "editor: backspace"
834        );
835        assert_eq!(
836            normalize_action_query("editor:  backspace"),
837            "editor: backspace"
838        );
839        assert_eq!(
840            normalize_action_query("editor:    backspace"),
841            "editor: backspace"
842        );
843        assert_eq!(
844            normalize_action_query("editor::GoToDefinition"),
845            "editor:GoToDefinition"
846        );
847        assert_eq!(
848            normalize_action_query("editor::::GoToDefinition"),
849            "editor:GoToDefinition"
850        );
851        assert_eq!(
852            normalize_action_query("editor: :GoToDefinition"),
853            "editor: :GoToDefinition"
854        );
855        assert_eq!(
856            normalize_action_query("terminal_panel::Toggle"),
857            "terminal panel:Toggle"
858        );
859        assert_eq!(
860            normalize_action_query("project_panel::ToggleFocus"),
861            "project panel:ToggleFocus"
862        );
863    }
864
865    #[gpui::test]
866    async fn test_command_palette(cx: &mut TestAppContext) {
867        let app_state = init_test(cx);
868        let db = cx.update(|cx| persistence::CommandPaletteDB::global(cx));
869        db.clear_all().await.unwrap();
870        let project = Project::test(app_state.fs.clone(), [], cx).await;
871        let (multi_workspace, cx) =
872            cx.add_window_view(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
873        let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
874
875        let editor = cx.new_window_entity(|window, cx| {
876            let mut editor = Editor::single_line(window, cx);
877            editor.set_text("abc", window, cx);
878            editor
879        });
880
881        workspace.update_in(cx, |workspace, window, cx| {
882            workspace.add_item_to_active_pane(Box::new(editor.clone()), None, true, window, cx);
883            editor.update(cx, |editor, cx| window.focus(&editor.focus_handle(cx), cx))
884        });
885
886        cx.simulate_keystrokes("cmd-shift-p");
887
888        let palette = workspace.update(cx, |workspace, cx| {
889            workspace
890                .active_modal::<CommandPalette>(cx)
891                .unwrap()
892                .read(cx)
893                .picker
894                .clone()
895        });
896
897        palette.read_with(cx, |palette, _| {
898            assert!(palette.delegate.commands.len() > 5);
899            let is_sorted =
900                |actions: &[Command]| actions.windows(2).all(|pair| pair[0].name <= pair[1].name);
901            assert!(is_sorted(&palette.delegate.commands));
902        });
903
904        cx.simulate_input("bcksp");
905
906        palette.read_with(cx, |palette, _| {
907            assert_eq!(palette.delegate.matches[0].string, "editor: backspace");
908        });
909
910        cx.simulate_keystrokes("enter");
911
912        workspace.update(cx, |workspace, cx| {
913            assert!(workspace.active_modal::<CommandPalette>(cx).is_none());
914            assert_eq!(editor.read(cx).text(cx), "ab")
915        });
916
917        // Add namespace filter, and redeploy the palette
918        cx.update(|_window, cx| {
919            CommandPaletteFilter::update_global(cx, |filter, _| {
920                filter.hide_namespace("editor");
921            });
922        });
923
924        cx.simulate_keystrokes("cmd-shift-p");
925        cx.simulate_input("bcksp");
926
927        let palette = workspace.update(cx, |workspace, cx| {
928            workspace
929                .active_modal::<CommandPalette>(cx)
930                .unwrap()
931                .read(cx)
932                .picker
933                .clone()
934        });
935        palette.read_with(cx, |palette, _| {
936            assert!(palette.delegate.matches.is_empty())
937        });
938    }
939
940    #[gpui::test]
941    async fn test_selected_command_none_when_no_matches(cx: &mut TestAppContext) {
942        let app_state = init_test(cx);
943        let project = Project::test(app_state.fs.clone(), [], cx).await;
944        let (multi_workspace, cx) =
945            cx.add_window_view(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
946        let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
947
948        cx.simulate_keystrokes("cmd-shift-p");
949        let picker = workspace.update(cx, |workspace, cx| {
950            workspace
951                .active_modal::<CommandPalette>(cx)
952                .unwrap()
953                .read(cx)
954                .picker
955                .clone()
956        });
957
958        cx.simulate_input("definitely-no-command-should-match-this");
959        cx.background_executor.run_until_parked();
960
961        picker.read_with(cx, |picker, _cx| {
962            assert!(picker.delegate.matches.is_empty());
963            assert!(picker.delegate.selected_command().is_none());
964        });
965    }
966    #[gpui::test]
967    async fn test_normalized_matches(cx: &mut TestAppContext) {
968        let app_state = init_test(cx);
969        let project = Project::test(app_state.fs.clone(), [], cx).await;
970        let (multi_workspace, cx) =
971            cx.add_window_view(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
972        let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
973
974        let editor = cx.new_window_entity(|window, cx| {
975            let mut editor = Editor::single_line(window, cx);
976            editor.set_text("abc", window, cx);
977            editor
978        });
979
980        workspace.update_in(cx, |workspace, window, cx| {
981            workspace.add_item_to_active_pane(Box::new(editor.clone()), None, true, window, cx);
982            editor.update(cx, |editor, cx| window.focus(&editor.focus_handle(cx), cx))
983        });
984
985        // Test normalize (trimming whitespace and double colons)
986        cx.simulate_keystrokes("cmd-shift-p");
987
988        let palette = workspace.update(cx, |workspace, cx| {
989            workspace
990                .active_modal::<CommandPalette>(cx)
991                .unwrap()
992                .read(cx)
993                .picker
994                .clone()
995        });
996
997        cx.simulate_input("Editor::    Backspace");
998        palette.read_with(cx, |palette, _| {
999            assert_eq!(palette.delegate.matches[0].string, "editor: backspace");
1000        });
1001    }
1002
1003    #[gpui::test]
1004    async fn test_go_to_line(cx: &mut TestAppContext) {
1005        let app_state = init_test(cx);
1006        let project = Project::test(app_state.fs.clone(), [], cx).await;
1007        let (multi_workspace, cx) =
1008            cx.add_window_view(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
1009        let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
1010
1011        cx.simulate_keystrokes("cmd-n");
1012
1013        let editor = workspace.update(cx, |workspace, cx| {
1014            workspace.active_item_as::<Editor>(cx).unwrap()
1015        });
1016        editor.update_in(cx, |editor, window, cx| {
1017            editor.set_text("1\n2\n3\n4\n5\n6\n", window, cx)
1018        });
1019
1020        cx.simulate_keystrokes("cmd-shift-p");
1021        cx.simulate_input("go to line: Toggle");
1022        cx.simulate_keystrokes("enter");
1023
1024        workspace.update(cx, |workspace, cx| {
1025            assert!(workspace.active_modal::<GoToLine>(cx).is_some())
1026        });
1027
1028        cx.simulate_keystrokes("3 enter");
1029
1030        editor.update_in(cx, |editor, window, cx| {
1031            assert!(editor.focus_handle(cx).is_focused(window));
1032            assert_eq!(
1033                editor
1034                    .selections
1035                    .last::<Point>(&editor.display_snapshot(cx))
1036                    .range()
1037                    .start,
1038                Point::new(2, 0)
1039            );
1040        });
1041    }
1042
1043    #[gpui::test]
1044    async fn test_reopen_command_palette_over_another_modal(cx: &mut TestAppContext) {
1045        let app_state = init_test(cx);
1046        let project = Project::test(app_state.fs.clone(), [], cx).await;
1047        let (multi_workspace, cx) =
1048            cx.add_window_view(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
1049        let workspace =
1050            multi_workspace.read_with(cx, |multi_workspace, _| multi_workspace.workspace().clone());
1051
1052        cx.simulate_keystrokes("cmd-n");
1053
1054        for _ in 0..2 {
1055            cx.simulate_keystrokes("cmd-shift-p");
1056            cx.simulate_input("go to line: Toggle");
1057            cx.simulate_keystrokes("enter");
1058
1059            workspace.update(cx, |workspace, cx| {
1060                assert!(workspace.active_modal::<GoToLine>(cx).is_some());
1061            });
1062        }
1063    }
1064
1065    fn init_test(cx: &mut TestAppContext) -> Arc<AppState> {
1066        cx.update(|cx| {
1067            let app_state = AppState::test(cx);
1068            theme_settings::init(theme::LoadThemes::JustBase, cx);
1069            editor::init(cx);
1070            menu::init();
1071            go_to_line::init(cx);
1072            workspace::init(app_state.clone(), cx);
1073            init(cx);
1074            cx.bind_keys(KeymapFile::load_panic_on_failure(
1075                r#"[
1076                    {
1077                        "bindings": {
1078                            "cmd-n": "workspace::NewFile",
1079                            "enter": "menu::Confirm",
1080                            "cmd-shift-p": "command_palette::Toggle",
1081                            "up": "menu::SelectPrevious",
1082                            "down": "menu::SelectNext"
1083                        }
1084                    }
1085                ]"#,
1086                cx,
1087            ));
1088            app_state
1089        })
1090    }
1091
1092    fn open_palette_with_history(
1093        workspace: &Entity<Workspace>,
1094        history: &[&str],
1095        cx: &mut VisualTestContext,
1096    ) -> Entity<Picker<CommandPaletteDelegate>> {
1097        cx.simulate_keystrokes("cmd-shift-p");
1098        cx.run_until_parked();
1099
1100        let palette = workspace.update(cx, |workspace, cx| {
1101            workspace
1102                .active_modal::<CommandPalette>(cx)
1103                .unwrap()
1104                .read(cx)
1105                .picker
1106                .clone()
1107        });
1108
1109        palette.update(cx, |palette, _cx| {
1110            palette.delegate.seed_history(history);
1111        });
1112
1113        palette
1114    }
1115
1116    #[gpui::test]
1117    async fn test_history_navigation_basic(cx: &mut TestAppContext) {
1118        let app_state = init_test(cx);
1119        let project = Project::test(app_state.fs.clone(), [], cx).await;
1120        let (multi_workspace, cx) =
1121            cx.add_window_view(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
1122        let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
1123
1124        let palette = open_palette_with_history(&workspace, &["backspace", "select all"], cx);
1125
1126        // Query should be empty initially
1127        palette.read_with(cx, |palette, cx| {
1128            assert_eq!(palette.query(cx), "");
1129        });
1130
1131        // Press up - should load most recent query "select all"
1132        cx.simulate_keystrokes("up");
1133        cx.background_executor.run_until_parked();
1134        palette.read_with(cx, |palette, cx| {
1135            assert_eq!(palette.query(cx), "select all");
1136        });
1137
1138        // Press up again - should load "backspace"
1139        cx.simulate_keystrokes("up");
1140        cx.background_executor.run_until_parked();
1141        palette.read_with(cx, |palette, cx| {
1142            assert_eq!(palette.query(cx), "backspace");
1143        });
1144
1145        // Press down - should go back to "select all"
1146        cx.simulate_keystrokes("down");
1147        cx.background_executor.run_until_parked();
1148        palette.read_with(cx, |palette, cx| {
1149            assert_eq!(palette.query(cx), "select all");
1150        });
1151
1152        // Press down again - should clear query (exit history mode)
1153        cx.simulate_keystrokes("down");
1154        cx.background_executor.run_until_parked();
1155        palette.read_with(cx, |palette, cx| {
1156            assert_eq!(palette.query(cx), "");
1157        });
1158    }
1159
1160    #[gpui::test]
1161    async fn test_history_mode_exit_on_typing(cx: &mut TestAppContext) {
1162        let app_state = init_test(cx);
1163        let project = Project::test(app_state.fs.clone(), [], cx).await;
1164        let (multi_workspace, cx) =
1165            cx.add_window_view(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
1166        let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
1167
1168        let palette = open_palette_with_history(&workspace, &["backspace"], cx);
1169
1170        // Press up to enter history mode
1171        cx.simulate_keystrokes("up");
1172        cx.background_executor.run_until_parked();
1173        palette.read_with(cx, |palette, cx| {
1174            assert_eq!(palette.query(cx), "backspace");
1175        });
1176
1177        // Type something - should append to the history query
1178        cx.simulate_input("x");
1179        cx.background_executor.run_until_parked();
1180        palette.read_with(cx, |palette, cx| {
1181            assert_eq!(palette.query(cx), "backspacex");
1182        });
1183    }
1184
1185    #[gpui::test]
1186    async fn test_history_navigation_with_suggestions(cx: &mut TestAppContext) {
1187        let app_state = init_test(cx);
1188        let project = Project::test(app_state.fs.clone(), [], cx).await;
1189        let (multi_workspace, cx) =
1190            cx.add_window_view(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
1191        let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
1192
1193        let palette = open_palette_with_history(&workspace, &["editor: close", "editor: open"], cx);
1194
1195        // Open palette with a query that has multiple matches
1196        cx.simulate_input("editor");
1197        cx.background_executor.run_until_parked();
1198
1199        // Should have multiple matches, selected_ix should be 0
1200        palette.read_with(cx, |palette, _| {
1201            assert!(palette.delegate.matches.len() > 1);
1202            assert_eq!(palette.delegate.selected_ix, 0);
1203        });
1204
1205        // Press down - should navigate to next suggestion (not history)
1206        cx.simulate_keystrokes("down");
1207        cx.background_executor.run_until_parked();
1208        palette.read_with(cx, |palette, _| {
1209            assert_eq!(palette.delegate.selected_ix, 1);
1210        });
1211
1212        // Press up - should go back to first suggestion
1213        cx.simulate_keystrokes("up");
1214        cx.background_executor.run_until_parked();
1215        palette.read_with(cx, |palette, _| {
1216            assert_eq!(palette.delegate.selected_ix, 0);
1217        });
1218
1219        // Press up again at top - should enter history mode and show previous query
1220        // that matches the "editor" prefix
1221        cx.simulate_keystrokes("up");
1222        cx.background_executor.run_until_parked();
1223        palette.read_with(cx, |palette, cx| {
1224            assert_eq!(palette.query(cx), "editor: open");
1225        });
1226    }
1227
1228    #[gpui::test]
1229    async fn test_history_prefix_search(cx: &mut TestAppContext) {
1230        let app_state = init_test(cx);
1231        let project = Project::test(app_state.fs.clone(), [], cx).await;
1232        let (multi_workspace, cx) =
1233            cx.add_window_view(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
1234        let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
1235
1236        let palette = open_palette_with_history(
1237            &workspace,
1238            &["open file", "select all", "select line", "backspace"],
1239            cx,
1240        );
1241
1242        // Type "sel" as a prefix
1243        cx.simulate_input("sel");
1244        cx.background_executor.run_until_parked();
1245
1246        // Press up - should get "select line" (most recent matching "sel")
1247        cx.simulate_keystrokes("up");
1248        cx.background_executor.run_until_parked();
1249        palette.read_with(cx, |palette, cx| {
1250            assert_eq!(palette.query(cx), "select line");
1251        });
1252
1253        // Press up again - should get "select all" (next matching "sel")
1254        cx.simulate_keystrokes("up");
1255        cx.background_executor.run_until_parked();
1256        palette.read_with(cx, |palette, cx| {
1257            assert_eq!(palette.query(cx), "select all");
1258        });
1259
1260        // Press up again - should stay at "select all" (no more matches for "sel")
1261        cx.simulate_keystrokes("up");
1262        cx.background_executor.run_until_parked();
1263        palette.read_with(cx, |palette, cx| {
1264            assert_eq!(palette.query(cx), "select all");
1265        });
1266
1267        // Press down - should go back to "select line"
1268        cx.simulate_keystrokes("down");
1269        cx.background_executor.run_until_parked();
1270        palette.read_with(cx, |palette, cx| {
1271            assert_eq!(palette.query(cx), "select line");
1272        });
1273
1274        // Press down again - should return to original prefix "sel"
1275        cx.simulate_keystrokes("down");
1276        cx.background_executor.run_until_parked();
1277        palette.read_with(cx, |palette, cx| {
1278            assert_eq!(palette.query(cx), "sel");
1279        });
1280    }
1281
1282    #[gpui::test]
1283    async fn test_history_prefix_search_no_matches(cx: &mut TestAppContext) {
1284        let app_state = init_test(cx);
1285        let project = Project::test(app_state.fs.clone(), [], cx).await;
1286        let (multi_workspace, cx) =
1287            cx.add_window_view(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
1288        let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
1289
1290        let palette =
1291            open_palette_with_history(&workspace, &["open file", "backspace", "select all"], cx);
1292
1293        // Type "xyz" as a prefix that doesn't match anything
1294        cx.simulate_input("xyz");
1295        cx.background_executor.run_until_parked();
1296
1297        // Press up - should stay at "xyz" (no matches)
1298        cx.simulate_keystrokes("up");
1299        cx.background_executor.run_until_parked();
1300        palette.read_with(cx, |palette, cx| {
1301            assert_eq!(palette.query(cx), "xyz");
1302        });
1303    }
1304
1305    #[gpui::test]
1306    async fn test_history_empty_prefix_searches_all(cx: &mut TestAppContext) {
1307        let app_state = init_test(cx);
1308        let project = Project::test(app_state.fs.clone(), [], cx).await;
1309        let (multi_workspace, cx) =
1310            cx.add_window_view(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
1311        let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
1312
1313        let palette = open_palette_with_history(&workspace, &["alpha", "beta", "gamma"], cx);
1314
1315        // With empty query, press up - should get "gamma" (most recent)
1316        cx.simulate_keystrokes("up");
1317        cx.background_executor.run_until_parked();
1318        palette.read_with(cx, |palette, cx| {
1319            assert_eq!(palette.query(cx), "gamma");
1320        });
1321
1322        // Press up - should get "beta"
1323        cx.simulate_keystrokes("up");
1324        cx.background_executor.run_until_parked();
1325        palette.read_with(cx, |palette, cx| {
1326            assert_eq!(palette.query(cx), "beta");
1327        });
1328
1329        // Press up - should get "alpha"
1330        cx.simulate_keystrokes("up");
1331        cx.background_executor.run_until_parked();
1332        palette.read_with(cx, |palette, cx| {
1333            assert_eq!(palette.query(cx), "alpha");
1334        });
1335
1336        // Press down - should get "beta"
1337        cx.simulate_keystrokes("down");
1338        cx.background_executor.run_until_parked();
1339        palette.read_with(cx, |palette, cx| {
1340            assert_eq!(palette.query(cx), "beta");
1341        });
1342
1343        // Press down - should get "gamma"
1344        cx.simulate_keystrokes("down");
1345        cx.background_executor.run_until_parked();
1346        palette.read_with(cx, |palette, cx| {
1347            assert_eq!(palette.query(cx), "gamma");
1348        });
1349
1350        // Press down - should return to empty string (exit history mode)
1351        cx.simulate_keystrokes("down");
1352        cx.background_executor.run_until_parked();
1353        palette.read_with(cx, |palette, cx| {
1354            assert_eq!(palette.query(cx), "");
1355        });
1356    }
1357}
1358
Served at tenant.openagents/omega Member data and write actions are omitted.