Action release handlers (#8782)
This PR adds support for handling action releases — events that are fired when the user releases all the modifier keys that were part of an action-triggering shortcut. If the user holds modifiers and invokes several actions sequentially via shortcuts (same or different), only the last action is "released" when its modifier keys released. ~The following methods were added to `Div`:~ - ~`capture_action_release()`~ - ~`on_action_release()`~ - ~`on_boxed_action_release()`~ ~They work similarly to `capture_action()`, `on_action()` and `on_boxed_action()`.~ See the implementation details in [this comment](https://github.com/zed-industries/zed/pull/8782#issuecomment-2009154646). Release Notes: - Added a fast-switch mode to the file finder: hit `p` or `shift-p` while holding down `cmd` to select a file immediately. (#8258). Related Issues: - Implements #8757 - Implements #8258 - Part of #7653 Co-authored-by: @ConradIrwin
This commit is contained in:
parent
91ab95ec82
commit
5602c48136
9 changed files with 260 additions and 31 deletions
|
@ -5,8 +5,9 @@ use collections::{HashMap, HashSet};
|
|||
use editor::{scroll::Autoscroll, Bias, Editor};
|
||||
use fuzzy::{CharBag, PathMatch, PathMatchCandidate};
|
||||
use gpui::{
|
||||
actions, rems, AppContext, DismissEvent, EventEmitter, FocusHandle, FocusableView, Model,
|
||||
ParentElement, Render, Styled, Task, View, ViewContext, VisualContext, WeakView,
|
||||
actions, rems, Action, AppContext, DismissEvent, EventEmitter, FocusHandle, FocusableView,
|
||||
Model, Modifiers, ModifiersChangedEvent, ParentElement, Render, Styled, Task, View,
|
||||
ViewContext, VisualContext, WeakView,
|
||||
};
|
||||
use itertools::Itertools;
|
||||
use picker::{Picker, PickerDelegate};
|
||||
|
@ -30,6 +31,7 @@ impl ModalView for FileFinder {}
|
|||
|
||||
pub struct FileFinder {
|
||||
picker: View<Picker<FileFinderDelegate>>,
|
||||
init_modifiers: Option<Modifiers>,
|
||||
}
|
||||
|
||||
pub fn init(cx: &mut AppContext) {
|
||||
|
@ -94,6 +96,23 @@ impl FileFinder {
|
|||
fn new(delegate: FileFinderDelegate, cx: &mut ViewContext<Self>) -> Self {
|
||||
Self {
|
||||
picker: cx.new_view(|cx| Picker::uniform_list(delegate, cx)),
|
||||
init_modifiers: cx.modifiers().modified().then_some(cx.modifiers()),
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_modifiers_changed(
|
||||
&mut self,
|
||||
event: &ModifiersChangedEvent,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) {
|
||||
let Some(init_modifiers) = self.init_modifiers else {
|
||||
return;
|
||||
};
|
||||
if self.picker.read(cx).delegate.has_changed_selected_index {
|
||||
if !event.modified() || !init_modifiers.is_subset_of(&event) {
|
||||
self.init_modifiers = None;
|
||||
cx.dispatch_action(menu::Confirm.boxed_clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -107,8 +126,12 @@ impl FocusableView for FileFinder {
|
|||
}
|
||||
|
||||
impl Render for FileFinder {
|
||||
fn render(&mut self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
v_flex().w(rems(34.)).child(self.picker.clone())
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
v_flex()
|
||||
.key_context("FileFinder")
|
||||
.w(rems(34.))
|
||||
.on_modifiers_changed(cx.listener(Self::handle_modifiers_changed))
|
||||
.child(self.picker.clone())
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -123,6 +146,7 @@ pub struct FileFinderDelegate {
|
|||
currently_opened_path: Option<FoundPath>,
|
||||
matches: Matches,
|
||||
selected_index: usize,
|
||||
has_changed_selected_index: bool,
|
||||
cancel_flag: Arc<AtomicBool>,
|
||||
history_items: Vec<FoundPath>,
|
||||
}
|
||||
|
@ -376,6 +400,7 @@ impl FileFinderDelegate {
|
|||
latest_search_query: None,
|
||||
currently_opened_path,
|
||||
matches: Matches::default(),
|
||||
has_changed_selected_index: false,
|
||||
selected_index: 0,
|
||||
cancel_flag: Arc::new(AtomicBool::new(false)),
|
||||
history_items,
|
||||
|
@ -683,6 +708,7 @@ impl PickerDelegate for FileFinderDelegate {
|
|||
}
|
||||
|
||||
fn set_selected_index(&mut self, ix: usize, cx: &mut ViewContext<Picker<Self>>) {
|
||||
self.has_changed_selected_index = true;
|
||||
self.selected_index = ix;
|
||||
cx.notify();
|
||||
}
|
||||
|
@ -721,7 +747,7 @@ impl PickerDelegate for FileFinderDelegate {
|
|||
}),
|
||||
);
|
||||
|
||||
self.selected_index = self.calculate_selected_index();
|
||||
self.selected_index = 0;
|
||||
cx.notify();
|
||||
Task::ready(())
|
||||
} else {
|
||||
|
|
|
@ -872,7 +872,6 @@ async fn test_toggle_panel_new_selections(cx: &mut gpui::TestAppContext) {
|
|||
|
||||
// generate some history to select from
|
||||
open_close_queried_buffer("fir", 1, "first.rs", &workspace, cx).await;
|
||||
cx.executor().run_until_parked();
|
||||
open_close_queried_buffer("sec", 1, "second.rs", &workspace, cx).await;
|
||||
open_close_queried_buffer("thi", 1, "third.rs", &workspace, cx).await;
|
||||
let current_history = open_close_queried_buffer("sec", 1, "second.rs", &workspace, cx).await;
|
||||
|
@ -1125,12 +1124,12 @@ async fn test_keep_opened_file_on_top_of_search_results_and_select_next_one(
|
|||
let picker = open_file_picker(&workspace, cx);
|
||||
picker.update(cx, |finder, _| {
|
||||
assert_eq!(finder.delegate.matches.len(), 3);
|
||||
assert_match_at_position(finder, 0, "main.rs");
|
||||
assert_match_selection(finder, 1, "lib.rs");
|
||||
assert_match_selection(finder, 0, "main.rs");
|
||||
assert_match_at_position(finder, 1, "lib.rs");
|
||||
assert_match_at_position(finder, 2, "bar.rs");
|
||||
});
|
||||
|
||||
// all files match, main.rs is still on top
|
||||
// all files match, main.rs is still on top, but the second item is selected
|
||||
picker
|
||||
.update(cx, |finder, cx| {
|
||||
finder.delegate.update_matches(".rs".to_string(), cx)
|
||||
|
@ -1173,8 +1172,8 @@ async fn test_keep_opened_file_on_top_of_search_results_and_select_next_one(
|
|||
.await;
|
||||
picker.update(cx, |finder, _| {
|
||||
assert_eq!(finder.delegate.matches.len(), 3);
|
||||
assert_match_at_position(finder, 0, "main.rs");
|
||||
assert_match_selection(finder, 1, "lib.rs");
|
||||
assert_match_selection(finder, 0, "main.rs");
|
||||
assert_match_at_position(finder, 1, "lib.rs");
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -1207,29 +1206,31 @@ async fn test_history_items_shown_in_order_of_open(cx: &mut TestAppContext) {
|
|||
let picker = open_file_picker(&workspace, cx);
|
||||
picker.update(cx, |finder, _| {
|
||||
assert_eq!(finder.delegate.matches.len(), 3);
|
||||
assert_match_at_position(finder, 0, "3.txt");
|
||||
assert_match_selection(finder, 1, "2.txt");
|
||||
assert_match_selection(finder, 0, "3.txt");
|
||||
assert_match_at_position(finder, 1, "2.txt");
|
||||
assert_match_at_position(finder, 2, "1.txt");
|
||||
});
|
||||
|
||||
cx.dispatch_action(SelectNext);
|
||||
cx.dispatch_action(Confirm); // Open 2.txt
|
||||
|
||||
let picker = open_file_picker(&workspace, cx);
|
||||
picker.update(cx, |finder, _| {
|
||||
assert_eq!(finder.delegate.matches.len(), 3);
|
||||
assert_match_at_position(finder, 0, "2.txt");
|
||||
assert_match_selection(finder, 1, "3.txt");
|
||||
assert_match_selection(finder, 0, "2.txt");
|
||||
assert_match_at_position(finder, 1, "3.txt");
|
||||
assert_match_at_position(finder, 2, "1.txt");
|
||||
});
|
||||
|
||||
cx.dispatch_action(SelectNext);
|
||||
cx.dispatch_action(SelectNext);
|
||||
cx.dispatch_action(Confirm); // Open 1.txt
|
||||
|
||||
let picker = open_file_picker(&workspace, cx);
|
||||
picker.update(cx, |finder, _| {
|
||||
assert_eq!(finder.delegate.matches.len(), 3);
|
||||
assert_match_at_position(finder, 0, "1.txt");
|
||||
assert_match_selection(finder, 1, "2.txt");
|
||||
assert_match_selection(finder, 0, "1.txt");
|
||||
assert_match_at_position(finder, 1, "2.txt");
|
||||
assert_match_at_position(finder, 2, "3.txt");
|
||||
});
|
||||
}
|
||||
|
@ -1469,6 +1470,98 @@ async fn test_search_results_refreshed_on_adding_and_removing_worktrees(
|
|||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_keeps_file_finder_open_after_modifier_keys_release(cx: &mut gpui::TestAppContext) {
|
||||
let app_state = init_test(cx);
|
||||
|
||||
app_state
|
||||
.fs
|
||||
.as_fake()
|
||||
.insert_tree(
|
||||
"/test",
|
||||
json!({
|
||||
"1.txt": "// One",
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
|
||||
let project = Project::test(app_state.fs.clone(), ["/test".as_ref()], cx).await;
|
||||
let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
|
||||
|
||||
open_queried_buffer("1", 1, "1.txt", &workspace, cx).await;
|
||||
|
||||
cx.simulate_modifiers_change(Modifiers::command());
|
||||
open_file_picker(&workspace, cx);
|
||||
|
||||
cx.simulate_modifiers_change(Modifiers::none());
|
||||
active_file_picker(&workspace, cx);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_opens_file_on_modifier_keys_release(cx: &mut gpui::TestAppContext) {
|
||||
let app_state = init_test(cx);
|
||||
|
||||
app_state
|
||||
.fs
|
||||
.as_fake()
|
||||
.insert_tree(
|
||||
"/test",
|
||||
json!({
|
||||
"1.txt": "// One",
|
||||
"2.txt": "// Two",
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
|
||||
let project = Project::test(app_state.fs.clone(), ["/test".as_ref()], cx).await;
|
||||
let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
|
||||
|
||||
open_queried_buffer("1", 1, "1.txt", &workspace, cx).await;
|
||||
open_queried_buffer("2", 1, "2.txt", &workspace, cx).await;
|
||||
|
||||
cx.simulate_modifiers_change(Modifiers::command());
|
||||
let picker = open_file_picker(&workspace, cx);
|
||||
picker.update(cx, |finder, _| {
|
||||
assert_eq!(finder.delegate.matches.len(), 2);
|
||||
assert_match_selection(finder, 0, "2.txt");
|
||||
assert_match_at_position(finder, 1, "1.txt");
|
||||
});
|
||||
|
||||
cx.dispatch_action(SelectNext);
|
||||
cx.simulate_modifiers_change(Modifiers::none());
|
||||
cx.read(|cx| {
|
||||
let active_editor = workspace.read(cx).active_item_as::<Editor>(cx).unwrap();
|
||||
assert_eq!(active_editor.read(cx).title(cx), "1.txt");
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_extending_modifiers_does_not_confirm_selection(cx: &mut gpui::TestAppContext) {
|
||||
let app_state = init_test(cx);
|
||||
|
||||
app_state
|
||||
.fs
|
||||
.as_fake()
|
||||
.insert_tree(
|
||||
"/test",
|
||||
json!({
|
||||
"1.txt": "// One",
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
|
||||
let project = Project::test(app_state.fs.clone(), ["/test".as_ref()], cx).await;
|
||||
let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
|
||||
|
||||
open_queried_buffer("1", 1, "1.txt", &workspace, cx).await;
|
||||
|
||||
cx.simulate_modifiers_change(Modifiers::command());
|
||||
open_file_picker(&workspace, cx);
|
||||
|
||||
cx.simulate_modifiers_change(Modifiers::command_shift());
|
||||
active_file_picker(&workspace, cx);
|
||||
}
|
||||
|
||||
async fn open_close_queried_buffer(
|
||||
input: &str,
|
||||
expected_matches: usize,
|
||||
|
@ -1581,7 +1674,7 @@ fn active_file_picker(
|
|||
workspace.update(cx, |workspace, cx| {
|
||||
workspace
|
||||
.active_modal::<FileFinder>(cx)
|
||||
.unwrap()
|
||||
.expect("file finder is not open")
|
||||
.read(cx)
|
||||
.picker
|
||||
.clone()
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue