1//! Provides hooks for customizing the behavior of the command palette.
  2
  3#![deny(missing_docs)]
  4
  5use std::{any::TypeId, rc::Rc};
  6
  7use collections::HashSet;
  8use derive_more::{Deref, DerefMut};
  9use gpui::{Action, App, BorrowAppContext, Global, Task, WeakEntity};
 10use workspace::Workspace;
 11
 12/// Initializes the command palette hooks.
 13pub fn init(cx: &mut App) {
 14    cx.set_global(GlobalCommandPaletteFilter::default());
 15}
 16
 17/// A filter for the command palette.
 18#[derive(Default)]
 19pub struct CommandPaletteFilter {
 20    hidden_namespaces: HashSet<&'static str>,
 21    hidden_action_types: HashSet<TypeId>,
 22    /// Actions that have explicitly been shown. These should be shown even if
 23    /// they are in a hidden namespace.
 24    shown_action_types: HashSet<TypeId>,
 25}
 26
 27#[derive(Deref, DerefMut, Default)]
 28struct GlobalCommandPaletteFilter(CommandPaletteFilter);
 29
 30impl Global for GlobalCommandPaletteFilter {}
 31
 32impl CommandPaletteFilter {
 33    /// Returns the global [`CommandPaletteFilter`], if one is set.
 34    pub fn try_global(cx: &App) -> Option<&CommandPaletteFilter> {
 35        cx.try_global::<GlobalCommandPaletteFilter>()
 36            .map(|filter| &filter.0)
 37    }
 38
 39    /// Returns a mutable reference to the global [`CommandPaletteFilter`].
 40    pub fn global_mut(cx: &mut App) -> &mut Self {
 41        cx.global_mut::<GlobalCommandPaletteFilter>()
 42    }
 43
 44    /// Updates the global [`CommandPaletteFilter`] using the given closure.
 45    pub fn update_global<F>(cx: &mut App, update: F)
 46    where
 47        F: FnOnce(&mut Self, &mut App),
 48    {
 49        if cx.has_global::<GlobalCommandPaletteFilter>() {
 50            cx.update_global(|this: &mut GlobalCommandPaletteFilter, cx| update(&mut this.0, cx))
 51        }
 52    }
 53
 54    /// Returns whether the given [`Action`] is hidden by the filter.
 55    pub fn is_hidden(&self, action: &dyn Action) -> bool {
 56        let name = action.name();
 57        let namespace = name.split("::").next().unwrap_or("malformed action name");
 58
 59        // If this action has specifically been shown then it should be visible.
 60        if self.shown_action_types.contains(&action.type_id()) {
 61            return false;
 62        }
 63
 64        self.hidden_namespaces.contains(namespace)
 65            || self.hidden_action_types.contains(&action.type_id())
 66    }
 67
 68    /// Hides all actions in the given namespace.
 69    pub fn hide_namespace(&mut self, namespace: &'static str) {
 70        self.hidden_namespaces.insert(namespace);
 71    }
 72
 73    /// Shows all actions in the given namespace.
 74    pub fn show_namespace(&mut self, namespace: &'static str) {
 75        self.hidden_namespaces.remove(namespace);
 76    }
 77
 78    /// Hides all actions with the given types.
 79    pub fn hide_action_types<'a>(&mut self, action_types: impl IntoIterator<Item = &'a TypeId>) {
 80        for action_type in action_types {
 81            self.hidden_action_types.insert(*action_type);
 82            self.shown_action_types.remove(action_type);
 83        }
 84    }
 85
 86    /// Shows all actions with the given types.
 87    pub fn show_action_types<'a>(&mut self, action_types: impl IntoIterator<Item = &'a TypeId>) {
 88        for action_type in action_types {
 89            self.shown_action_types.insert(*action_type);
 90            self.hidden_action_types.remove(action_type);
 91        }
 92    }
 93}
 94
 95/// The result of intercepting a command palette command.
 96#[derive(Debug)]
 97pub struct CommandInterceptItem {
 98    /// The action produced as a result of the interception.
 99    pub action: Box<dyn Action>,
100    /// The display string to show in the command palette for this result.
101    pub string: String,
102    /// The character positions in the string that match the query.
103    /// Used for highlighting matched characters in the command palette UI.
104    pub positions: Vec<usize>,
105}
106
107/// The result of intercepting a command palette command.
108#[derive(Default, Debug)]
109pub struct CommandInterceptResult {
110    /// The items
111    pub results: Vec<CommandInterceptItem>,
112    /// Whether or not to continue to show the normal matches
113    pub exclusive: bool,
114}
115
116/// An interceptor for the command palette.
117#[derive(Clone)]
118pub struct GlobalCommandPaletteInterceptor(
119    Rc<dyn Fn(&str, WeakEntity<Workspace>, &mut App) -> Task<CommandInterceptResult>>,
120);
121
122impl Global for GlobalCommandPaletteInterceptor {}
123
124impl GlobalCommandPaletteInterceptor {
125    /// Sets the global interceptor.
126    ///
127    /// This will override the previous interceptor, if it exists.
128    pub fn set(
129        cx: &mut App,
130        interceptor: impl Fn(&str, WeakEntity<Workspace>, &mut App) -> Task<CommandInterceptResult>
131        + 'static,
132    ) {
133        cx.set_global(Self(Rc::new(interceptor)));
134    }
135
136    /// Clears the global interceptor.
137    pub fn clear(cx: &mut App) {
138        if cx.has_global::<Self>() {
139            cx.remove_global::<Self>();
140        }
141    }
142
143    /// Intercepts the given query from the command palette.
144    pub fn intercept(
145        query: &str,
146        workspace: WeakEntity<Workspace>,
147        cx: &mut App,
148    ) -> Option<Task<CommandInterceptResult>> {
149        let interceptor = cx.try_global::<Self>()?;
150        let handler = interceptor.0.clone();
151        Some(handler(query, workspace, cx))
152    }
153}