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:
Andrew Lygin 2024-03-21 03:43:31 +03:00 committed by GitHub
parent 91ab95ec82
commit 5602c48136
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 260 additions and 31 deletions

View file

@ -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()