Skip to content

Commit 0cbab31

Browse files
AidanVConradIrwin
andauthored
vim: Add vim command filename autocomplete (#36332)
Release Notes: - Adds filename autocomplete for vim commands: - write - edit - split - vsplit - tabedit - tabnew - Makes command palette interceptor async <img width="1382" height="634" alt="image" src="https://github.com/user-attachments/assets/e7bf01c5-e9cd-4a7d-b38c-12fc3df5069f" /> --------- Co-authored-by: Conrad Irwin <conrad.irwin@gmail.com>
1 parent 908ae95 commit 0cbab31

7 files changed

Lines changed: 424 additions & 182 deletions

File tree

Cargo.lock

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/command_palette/src/command_palette.rs

Lines changed: 61 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,8 @@ use std::{
99

1010
use client::parse_zed_link;
1111
use command_palette_hooks::{
12-
CommandInterceptResult, CommandPaletteFilter, CommandPaletteInterceptor,
12+
CommandInterceptItem, CommandInterceptResult, CommandPaletteFilter,
13+
GlobalCommandPaletteInterceptor,
1314
};
1415

1516
use fuzzy::{StringMatch, StringMatchCandidate};
@@ -81,14 +82,17 @@ impl CommandPalette {
8182
let Some(previous_focus_handle) = window.focused(cx) else {
8283
return;
8384
};
85+
86+
let entity = cx.weak_entity();
8487
workspace.toggle_modal(window, cx, move |window, cx| {
85-
CommandPalette::new(previous_focus_handle, query, window, cx)
88+
CommandPalette::new(previous_focus_handle, query, entity, window, cx)
8689
});
8790
}
8891

8992
fn new(
9093
previous_focus_handle: FocusHandle,
9194
query: &str,
95+
entity: WeakEntity<Workspace>,
9296
window: &mut Window,
9397
cx: &mut Context<Self>,
9498
) -> Self {
@@ -109,8 +113,12 @@ impl CommandPalette {
109113
})
110114
.collect();
111115

112-
let delegate =
113-
CommandPaletteDelegate::new(cx.entity().downgrade(), commands, previous_focus_handle);
116+
let delegate = CommandPaletteDelegate::new(
117+
cx.entity().downgrade(),
118+
entity,
119+
commands,
120+
previous_focus_handle,
121+
);
114122

115123
let picker = cx.new(|cx| {
116124
let picker = Picker::uniform_list(delegate, window, cx);
@@ -146,14 +154,15 @@ impl Render for CommandPalette {
146154
pub struct CommandPaletteDelegate {
147155
latest_query: String,
148156
command_palette: WeakEntity<CommandPalette>,
157+
workspace: WeakEntity<Workspace>,
149158
all_commands: Vec<Command>,
150159
commands: Vec<Command>,
151160
matches: Vec<StringMatch>,
152161
selected_ix: usize,
153162
previous_focus_handle: FocusHandle,
154163
updating_matches: Option<(
155164
Task<()>,
156-
postage::dispatch::Receiver<(Vec<Command>, Vec<StringMatch>)>,
165+
postage::dispatch::Receiver<(Vec<Command>, Vec<StringMatch>, CommandInterceptResult)>,
157166
)>,
158167
}
159168

@@ -174,11 +183,13 @@ impl Clone for Command {
174183
impl CommandPaletteDelegate {
175184
fn new(
176185
command_palette: WeakEntity<CommandPalette>,
186+
workspace: WeakEntity<Workspace>,
177187
commands: Vec<Command>,
178188
previous_focus_handle: FocusHandle,
179189
) -> Self {
180190
Self {
181191
command_palette,
192+
workspace,
182193
all_commands: commands.clone(),
183194
matches: vec![],
184195
commands,
@@ -194,30 +205,19 @@ impl CommandPaletteDelegate {
194205
query: String,
195206
mut commands: Vec<Command>,
196207
mut matches: Vec<StringMatch>,
197-
cx: &mut Context<Picker<Self>>,
208+
intercept_result: CommandInterceptResult,
209+
_: &mut Context<Picker<Self>>,
198210
) {
199211
self.updating_matches.take();
200-
self.latest_query = query.clone();
201-
202-
let mut intercept_results = CommandPaletteInterceptor::try_global(cx)
203-
.map(|interceptor| interceptor.intercept(&query, cx))
204-
.unwrap_or_default();
205-
206-
if parse_zed_link(&query, cx).is_some() {
207-
intercept_results = vec![CommandInterceptResult {
208-
action: OpenZedUrl { url: query.clone() }.boxed_clone(),
209-
string: query,
210-
positions: vec![],
211-
}]
212-
}
212+
self.latest_query = query;
213213

214214
let mut new_matches = Vec::new();
215215

216-
for CommandInterceptResult {
216+
for CommandInterceptItem {
217217
action,
218218
string,
219219
positions,
220-
} in intercept_results
220+
} in intercept_result.results
221221
{
222222
if let Some(idx) = matches
223223
.iter()
@@ -236,7 +236,9 @@ impl CommandPaletteDelegate {
236236
score: 0.0,
237237
})
238238
}
239-
new_matches.append(&mut matches);
239+
if !intercept_result.exclusive {
240+
new_matches.append(&mut matches);
241+
}
240242
self.commands = commands;
241243
self.matches = new_matches;
242244
if self.matches.is_empty() {
@@ -295,12 +297,22 @@ impl PickerDelegate for CommandPaletteDelegate {
295297
if let Some(alias) = settings.command_aliases.get(&query) {
296298
query = alias.to_string();
297299
}
300+
301+
let workspace = self.workspace.clone();
302+
303+
let intercept_task = GlobalCommandPaletteInterceptor::intercept(&query, workspace, cx);
304+
298305
let (mut tx, mut rx) = postage::dispatch::channel(1);
306+
307+
let query_str = query.as_str();
308+
let is_zed_link = parse_zed_link(query_str, cx).is_some();
309+
299310
let task = cx.background_spawn({
300311
let mut commands = self.all_commands.clone();
301312
let hit_counts = self.hit_counts();
302313
let executor = cx.background_executor().clone();
303-
let query = normalize_action_query(query.as_str());
314+
let query = normalize_action_query(query_str);
315+
let query_for_link = query_str.to_string();
304316
async move {
305317
commands.sort_by_key(|action| {
306318
(
@@ -326,21 +338,42 @@ impl PickerDelegate for CommandPaletteDelegate {
326338
)
327339
.await;
328340

329-
tx.send((commands, matches)).await.log_err();
341+
let intercept_result = if is_zed_link {
342+
CommandInterceptResult {
343+
results: vec![CommandInterceptItem {
344+
action: OpenZedUrl {
345+
url: query_for_link.clone(),
346+
}
347+
.boxed_clone(),
348+
string: query_for_link,
349+
positions: vec![],
350+
}],
351+
exclusive: false,
352+
}
353+
} else if let Some(task) = intercept_task {
354+
task.await
355+
} else {
356+
CommandInterceptResult::default()
357+
};
358+
359+
tx.send((commands, matches, intercept_result))
360+
.await
361+
.log_err();
330362
}
331363
});
364+
332365
self.updating_matches = Some((task, rx.clone()));
333366

334367
cx.spawn_in(window, async move |picker, cx| {
335-
let Some((commands, matches)) = rx.recv().await else {
368+
let Some((commands, matches, intercept_result)) = rx.recv().await else {
336369
return;
337370
};
338371

339372
picker
340373
.update(cx, |picker, cx| {
341374
picker
342375
.delegate
343-
.matches_updated(query, commands, matches, cx)
376+
.matches_updated(query, commands, matches, intercept_result, cx)
344377
})
345378
.log_err();
346379
})
@@ -361,8 +394,8 @@ impl PickerDelegate for CommandPaletteDelegate {
361394
.background_executor()
362395
.block_with_timeout(duration, rx.clone().recv())
363396
{
364-
Ok(Some((commands, matches))) => {
365-
self.matches_updated(query, commands, matches, cx);
397+
Ok(Some((commands, matches, interceptor_result))) => {
398+
self.matches_updated(query, commands, matches, interceptor_result, cx);
366399
true
367400
}
368401
_ => {

crates/command_palette_hooks/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,3 +17,4 @@ collections.workspace = true
1717
derive_more.workspace = true
1818
gpui.workspace = true
1919
workspace-hack.workspace = true
20+
workspace.workspace = true

crates/command_palette_hooks/src/command_palette_hooks.rs

Lines changed: 39 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,16 @@
22
33
#![deny(missing_docs)]
44

5-
use std::any::TypeId;
5+
use std::{any::TypeId, rc::Rc};
66

77
use collections::HashSet;
88
use derive_more::{Deref, DerefMut};
9-
use gpui::{Action, App, BorrowAppContext, Global};
9+
use gpui::{Action, App, BorrowAppContext, Global, Task, WeakEntity};
10+
use workspace::Workspace;
1011

1112
/// Initializes the command palette hooks.
1213
pub fn init(cx: &mut App) {
1314
cx.set_global(GlobalCommandPaletteFilter::default());
14-
cx.set_global(GlobalCommandPaletteInterceptor::default());
1515
}
1616

1717
/// A filter for the command palette.
@@ -94,7 +94,7 @@ impl CommandPaletteFilter {
9494

9595
/// The result of intercepting a command palette command.
9696
#[derive(Debug)]
97-
pub struct CommandInterceptResult {
97+
pub struct CommandInterceptItem {
9898
/// The action produced as a result of the interception.
9999
pub action: Box<dyn Action>,
100100
/// The display string to show in the command palette for this result.
@@ -104,50 +104,50 @@ pub struct CommandInterceptResult {
104104
pub positions: Vec<usize>,
105105
}
106106

107+
/// The result of intercepting a command palette command.
108+
#[derive(Default, Debug)]
109+
pub 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+
107116
/// An interceptor for the command palette.
108-
#[derive(Default)]
109-
pub struct CommandPaletteInterceptor(
110-
Option<Box<dyn Fn(&str, &App) -> Vec<CommandInterceptResult>>>,
117+
#[derive(Clone)]
118+
pub struct GlobalCommandPaletteInterceptor(
119+
Rc<dyn Fn(&str, WeakEntity<Workspace>, &mut App) -> Task<CommandInterceptResult>>,
111120
);
112121

113-
#[derive(Default)]
114-
struct GlobalCommandPaletteInterceptor(CommandPaletteInterceptor);
115-
116122
impl Global for GlobalCommandPaletteInterceptor {}
117123

118-
impl CommandPaletteInterceptor {
119-
/// Returns the global [`CommandPaletteInterceptor`], if one is set.
120-
pub fn try_global(cx: &App) -> Option<&CommandPaletteInterceptor> {
121-
cx.try_global::<GlobalCommandPaletteInterceptor>()
122-
.map(|interceptor| &interceptor.0)
123-
}
124-
125-
/// Updates the global [`CommandPaletteInterceptor`] using the given closure.
126-
pub fn update_global<F, R>(cx: &mut App, update: F) -> R
127-
where
128-
F: FnOnce(&mut Self, &mut App) -> R,
129-
{
130-
cx.update_global(|this: &mut GlobalCommandPaletteInterceptor, cx| update(&mut this.0, cx))
131-
}
132-
133-
/// Intercepts the given query from the command palette.
134-
pub fn intercept(&self, query: &str, cx: &App) -> Vec<CommandInterceptResult> {
135-
if let Some(handler) = self.0.as_ref() {
136-
(handler)(query, cx)
137-
} else {
138-
Vec::new()
139-
}
124+
impl 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)));
140134
}
141135

142136
/// Clears the global interceptor.
143-
pub fn clear(&mut self) {
144-
self.0 = None;
137+
pub fn clear(cx: &mut App) {
138+
if cx.has_global::<Self>() {
139+
cx.remove_global::<Self>();
140+
}
145141
}
146142

147-
/// Sets the global interceptor.
148-
///
149-
/// This will override the previous interceptor, if it exists.
150-
pub fn set(&mut self, handler: Box<dyn Fn(&str, &App) -> Vec<CommandInterceptResult>>) {
151-
self.0 = Some(handler);
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))
152152
}
153153
}

crates/vim/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ db.workspace = true
2626
editor.workspace = true
2727
env_logger.workspace = true
2828
futures.workspace = true
29+
fuzzy.workspace = true
2930
gpui.workspace = true
3031
itertools.workspace = true
3132
language.workspace = true

0 commit comments

Comments
 (0)