mirror of
https://github.com/zed-industries/zed.git
synced 2024-09-19 18:41:56 +03:00
Add spawning of tasks without saving them in the task stack (#9951)
These tasks are not considered for reruns with `task::Rerun`. This PR tears a bunch of stuff up around tasks: - `menu::SecondaryConfirm` for tasks is gonna spawn a task without storing it in history instead of being occupied by oneshot tasks. This is done so that cmd-clicking on the menu item actually does something meaningful. - `menu::UseSelectedQuery` got moved into picker, as tasks are it's only user (and it doesn't really make sense as a menu action). TODO: - [x] add release note - [x] Actually implement the core of this feature, which is spawning a task without saving it in history, lol. Fixes #9804 Release Notes: - Added "fire-and-forget" task spawning; `menu::SecondaryConfirm` in tasks modal now spawns a task without registering it as the last spawned task for the purposes of `task::Rerun`. By default you can spawn a task in this fashion with cmd+enter or by holding cmd and clicking on a task entry in a list. Spawning oneshots has been rebound to `option-enter` (under a `picker::ConfirmInput` name). Fixes #9804 (breaking change) - Moved `menu::UseSelectedQuery` action to `picker` namespace (breaking change).
This commit is contained in:
parent
e7bd91c6c7
commit
cff9ad19f8
1
Cargo.lock
generated
1
Cargo.lock
generated
@ -6884,6 +6884,7 @@ dependencies = [
|
|||||||
"env_logger",
|
"env_logger",
|
||||||
"gpui",
|
"gpui",
|
||||||
"menu",
|
"menu",
|
||||||
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"ui",
|
"ui",
|
||||||
"workspace",
|
"workspace",
|
||||||
|
@ -16,7 +16,9 @@
|
|||||||
"escape": "menu::Cancel",
|
"escape": "menu::Cancel",
|
||||||
"ctrl-escape": "menu::Cancel",
|
"ctrl-escape": "menu::Cancel",
|
||||||
"ctrl-c": "menu::Cancel",
|
"ctrl-c": "menu::Cancel",
|
||||||
"shift-enter": "menu::UseSelectedQuery",
|
"shift-enter": "picker::UseSelectedQuery",
|
||||||
|
"alt-enter": ["picker::ConfirmInput", { "secondary": false }],
|
||||||
|
"ctrl-alt-enter": ["picker::ConfirmInput", { "secondary": true }],
|
||||||
"ctrl-shift-w": "workspace::CloseWindow",
|
"ctrl-shift-w": "workspace::CloseWindow",
|
||||||
"shift-escape": "workspace::ToggleZoom",
|
"shift-escape": "workspace::ToggleZoom",
|
||||||
"ctrl-o": "workspace::Open",
|
"ctrl-o": "workspace::Open",
|
||||||
|
@ -19,7 +19,9 @@
|
|||||||
"cmd-escape": "menu::Cancel",
|
"cmd-escape": "menu::Cancel",
|
||||||
"ctrl-escape": "menu::Cancel",
|
"ctrl-escape": "menu::Cancel",
|
||||||
"ctrl-c": "menu::Cancel",
|
"ctrl-c": "menu::Cancel",
|
||||||
"shift-enter": "menu::UseSelectedQuery",
|
"shift-enter": "picker::UseSelectedQuery",
|
||||||
|
"alt-enter": ["picker::ConfirmInput", { "secondary": false }],
|
||||||
|
"cmd-alt-enter": ["picker::ConfirmInput", { "secondary": true }],
|
||||||
"cmd-shift-w": "workspace::CloseWindow",
|
"cmd-shift-w": "workspace::CloseWindow",
|
||||||
"shift-escape": "workspace::ToggleZoom",
|
"shift-escape": "workspace::ToggleZoom",
|
||||||
"cmd-o": "workspace::Open",
|
"cmd-o": "workspace::Open",
|
||||||
|
@ -17,6 +17,7 @@ anyhow.workspace = true
|
|||||||
editor.workspace = true
|
editor.workspace = true
|
||||||
gpui.workspace = true
|
gpui.workspace = true
|
||||||
menu.workspace = true
|
menu.workspace = true
|
||||||
|
serde.workspace = true
|
||||||
ui.workspace = true
|
ui.workspace = true
|
||||||
workspace.workspace = true
|
workspace.workspace = true
|
||||||
|
|
||||||
|
@ -1,11 +1,12 @@
|
|||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use editor::{scroll::Autoscroll, Editor};
|
use editor::{scroll::Autoscroll, Editor};
|
||||||
use gpui::{
|
use gpui::{
|
||||||
div, list, prelude::*, uniform_list, AnyElement, AppContext, ClickEvent, DismissEvent,
|
actions, div, impl_actions, list, prelude::*, uniform_list, AnyElement, AppContext, ClickEvent,
|
||||||
EventEmitter, FocusHandle, FocusableView, Length, ListState, MouseButton, MouseUpEvent, Render,
|
DismissEvent, EventEmitter, FocusHandle, FocusableView, Length, ListState, MouseButton,
|
||||||
Task, UniformListScrollHandle, View, ViewContext, WindowContext,
|
MouseUpEvent, Render, Task, UniformListScrollHandle, View, ViewContext, WindowContext,
|
||||||
};
|
};
|
||||||
use head::Head;
|
use head::Head;
|
||||||
|
use serde::Deserialize;
|
||||||
use std::{sync::Arc, time::Duration};
|
use std::{sync::Arc, time::Duration};
|
||||||
use ui::{prelude::*, v_flex, Color, Divider, Label, ListItem, ListItemSpacing};
|
use ui::{prelude::*, v_flex, Color, Divider, Label, ListItem, ListItemSpacing};
|
||||||
use workspace::ModalView;
|
use workspace::ModalView;
|
||||||
@ -18,6 +19,17 @@ enum ElementContainer {
|
|||||||
UniformList(UniformListScrollHandle),
|
UniformList(UniformListScrollHandle),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
actions!(picker, [UseSelectedQuery]);
|
||||||
|
|
||||||
|
/// ConfirmInput is an alternative editor action which - instead of selecting active picker entry - treats pickers editor input literally,
|
||||||
|
/// performing some kind of action on it.
|
||||||
|
#[derive(PartialEq, Clone, Deserialize, Default)]
|
||||||
|
pub struct ConfirmInput {
|
||||||
|
pub secondary: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl_actions!(picker, [ConfirmInput]);
|
||||||
|
|
||||||
struct PendingUpdateMatches {
|
struct PendingUpdateMatches {
|
||||||
delegate_update_matches: Option<Task<()>>,
|
delegate_update_matches: Option<Task<()>>,
|
||||||
_task: Task<Result<()>>,
|
_task: Task<Result<()>>,
|
||||||
@ -65,6 +77,9 @@ pub trait PickerDelegate: Sized + 'static {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn confirm(&mut self, secondary: bool, cx: &mut ViewContext<Picker<Self>>);
|
fn confirm(&mut self, secondary: bool, cx: &mut ViewContext<Picker<Self>>);
|
||||||
|
/// Instead of interacting with currently selected entry, treats editor input literally,
|
||||||
|
/// performing some kind of action on it.
|
||||||
|
fn confirm_input(&mut self, _secondary: bool, _: &mut ViewContext<Picker<Self>>) {}
|
||||||
fn dismissed(&mut self, cx: &mut ViewContext<Picker<Self>>);
|
fn dismissed(&mut self, cx: &mut ViewContext<Picker<Self>>);
|
||||||
fn selected_as_query(&self) -> Option<String> {
|
fn selected_as_query(&self) -> Option<String> {
|
||||||
None
|
None
|
||||||
@ -278,7 +293,11 @@ impl<D: PickerDelegate> Picker<D> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn use_selected_query(&mut self, _: &menu::UseSelectedQuery, cx: &mut ViewContext<Self>) {
|
fn confirm_input(&mut self, input: &ConfirmInput, cx: &mut ViewContext<Self>) {
|
||||||
|
self.delegate.confirm_input(input.secondary, cx);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn use_selected_query(&mut self, _: &UseSelectedQuery, cx: &mut ViewContext<Self>) {
|
||||||
if let Some(new_query) = self.delegate.selected_as_query() {
|
if let Some(new_query) = self.delegate.selected_as_query() {
|
||||||
self.set_query(new_query, cx);
|
self.set_query(new_query, cx);
|
||||||
cx.stop_propagation();
|
cx.stop_propagation();
|
||||||
@ -472,6 +491,7 @@ impl<D: PickerDelegate> Render for Picker<D> {
|
|||||||
.on_action(cx.listener(Self::confirm))
|
.on_action(cx.listener(Self::confirm))
|
||||||
.on_action(cx.listener(Self::secondary_confirm))
|
.on_action(cx.listener(Self::secondary_confirm))
|
||||||
.on_action(cx.listener(Self::use_selected_query))
|
.on_action(cx.listener(Self::use_selected_query))
|
||||||
|
.on_action(cx.listener(Self::confirm_input))
|
||||||
.child(match &self.head {
|
.child(match &self.head {
|
||||||
Head::Editor(editor) => v_flex()
|
Head::Editor(editor) => v_flex()
|
||||||
.child(
|
.child(
|
||||||
|
@ -31,7 +31,7 @@ pub fn init(cx: &mut AppContext) {
|
|||||||
old_context
|
old_context
|
||||||
};
|
};
|
||||||
|
|
||||||
schedule_task(workspace, task.as_ref(), task_context, cx)
|
schedule_task(workspace, task.as_ref(), task_context, false, cx)
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
@ -70,7 +70,7 @@ fn spawn_task_with_name(name: String, cx: &mut ViewContext<Workspace>) {
|
|||||||
let (_, target_task) = tasks.into_iter().find(|(_, task)| task.name() == name)?;
|
let (_, target_task) = tasks.into_iter().find(|(_, task)| task.name() == name)?;
|
||||||
let cwd = task_cwd(this, cx).log_err().flatten();
|
let cwd = task_cwd(this, cx).log_err().flatten();
|
||||||
let task_context = task_context(this, cwd, cx);
|
let task_context = task_context(this, cwd, cx);
|
||||||
schedule_task(this, target_task.as_ref(), task_context, cx);
|
schedule_task(this, target_task.as_ref(), task_context, false, cx);
|
||||||
Some(())
|
Some(())
|
||||||
})
|
})
|
||||||
.ok()
|
.ok()
|
||||||
@ -195,15 +195,18 @@ fn schedule_task(
|
|||||||
workspace: &Workspace,
|
workspace: &Workspace,
|
||||||
task: &dyn Task,
|
task: &dyn Task,
|
||||||
task_cx: TaskContext,
|
task_cx: TaskContext,
|
||||||
|
omit_history: bool,
|
||||||
cx: &mut ViewContext<'_, Workspace>,
|
cx: &mut ViewContext<'_, Workspace>,
|
||||||
) {
|
) {
|
||||||
let spawn_in_terminal = task.exec(task_cx.clone());
|
let spawn_in_terminal = task.exec(task_cx.clone());
|
||||||
if let Some(spawn_in_terminal) = spawn_in_terminal {
|
if let Some(spawn_in_terminal) = spawn_in_terminal {
|
||||||
|
if !omit_history {
|
||||||
workspace.project().update(cx, |project, cx| {
|
workspace.project().update(cx, |project, cx| {
|
||||||
project.task_inventory().update(cx, |inventory, _| {
|
project.task_inventory().update(cx, |inventory, _| {
|
||||||
inventory.task_scheduled(task.id().clone(), task_cx);
|
inventory.task_scheduled(task.id().clone(), task_cx);
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
|
}
|
||||||
cx.emit(workspace::Event::SpawnTask(spawn_in_terminal));
|
cx.emit(workspace::Event::SpawnTask(spawn_in_terminal));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -169,8 +169,8 @@ impl PickerDelegate for TasksModalDelegate {
|
|||||||
fn placeholder_text(&self, cx: &mut WindowContext) -> Arc<str> {
|
fn placeholder_text(&self, cx: &mut WindowContext) -> Arc<str> {
|
||||||
Arc::from(format!(
|
Arc::from(format!(
|
||||||
"{} use task name as prompt, {} spawns a bash-like task from the prompt, {} runs the selected task",
|
"{} use task name as prompt, {} spawns a bash-like task from the prompt, {} runs the selected task",
|
||||||
cx.keystroke_text_for(&menu::UseSelectedQuery),
|
cx.keystroke_text_for(&picker::UseSelectedQuery),
|
||||||
cx.keystroke_text_for(&menu::SecondaryConfirm),
|
cx.keystroke_text_for(&picker::ConfirmInput {secondary: false}),
|
||||||
cx.keystroke_text_for(&menu::Confirm),
|
cx.keystroke_text_for(&menu::Confirm),
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
@ -236,32 +236,30 @@ impl PickerDelegate for TasksModalDelegate {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn confirm(&mut self, secondary: bool, cx: &mut ViewContext<picker::Picker<Self>>) {
|
fn confirm(&mut self, omit_history_entry: bool, cx: &mut ViewContext<picker::Picker<Self>>) {
|
||||||
let current_match_index = self.selected_index();
|
let current_match_index = self.selected_index();
|
||||||
let task = if secondary {
|
let task = self
|
||||||
if !self.prompt.trim().is_empty() {
|
.matches
|
||||||
self.spawn_oneshot(cx)
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
self.matches
|
|
||||||
.get(current_match_index)
|
.get(current_match_index)
|
||||||
.and_then(|current_match| {
|
.and_then(|current_match| {
|
||||||
let ix = current_match.candidate_id;
|
let ix = current_match.candidate_id;
|
||||||
self.candidates
|
self.candidates
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.map(|candidates| candidates[ix].1.clone())
|
.map(|candidates| candidates[ix].1.clone())
|
||||||
})
|
});
|
||||||
};
|
|
||||||
|
|
||||||
let Some(task) = task else {
|
let Some(task) = task else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
self.workspace
|
self.workspace
|
||||||
.update(cx, |workspace, cx| {
|
.update(cx, |workspace, cx| {
|
||||||
schedule_task(workspace, task.as_ref(), self.task_context.clone(), cx);
|
schedule_task(
|
||||||
|
workspace,
|
||||||
|
task.as_ref(),
|
||||||
|
self.task_context.clone(),
|
||||||
|
omit_history_entry,
|
||||||
|
cx,
|
||||||
|
);
|
||||||
})
|
})
|
||||||
.ok();
|
.ok();
|
||||||
cx.emit(DismissEvent);
|
cx.emit(DismissEvent);
|
||||||
@ -325,6 +323,23 @@ impl PickerDelegate for TasksModalDelegate {
|
|||||||
}
|
}
|
||||||
Some(spawn_prompt.command)
|
Some(spawn_prompt.command)
|
||||||
}
|
}
|
||||||
|
fn confirm_input(&mut self, omit_history_entry: bool, cx: &mut ViewContext<Picker<Self>>) {
|
||||||
|
let Some(task) = self.spawn_oneshot(cx) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
self.workspace
|
||||||
|
.update(cx, |workspace, cx| {
|
||||||
|
schedule_task(
|
||||||
|
workspace,
|
||||||
|
task.as_ref(),
|
||||||
|
self.task_context.clone(),
|
||||||
|
omit_history_entry,
|
||||||
|
cx,
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.ok();
|
||||||
|
cx.emit(DismissEvent);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
@ -391,7 +406,7 @@ mod tests {
|
|||||||
"Only one task should match the query {query_str}"
|
"Only one task should match the query {query_str}"
|
||||||
);
|
);
|
||||||
|
|
||||||
cx.dispatch_action(menu::UseSelectedQuery);
|
cx.dispatch_action(picker::UseSelectedQuery);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
query(&tasks_picker, cx),
|
query(&tasks_picker, cx),
|
||||||
"echo 4",
|
"echo 4",
|
||||||
@ -402,7 +417,7 @@ mod tests {
|
|||||||
Vec::<String>::new(),
|
Vec::<String>::new(),
|
||||||
"No task should be listed"
|
"No task should be listed"
|
||||||
);
|
);
|
||||||
cx.dispatch_action(menu::SecondaryConfirm);
|
cx.dispatch_action(picker::ConfirmInput { secondary: false });
|
||||||
|
|
||||||
let tasks_picker = open_spawn_tasks(&workspace, cx);
|
let tasks_picker = open_spawn_tasks(&workspace, cx);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
@ -425,7 +440,7 @@ mod tests {
|
|||||||
"New oneshot should match custom command query"
|
"New oneshot should match custom command query"
|
||||||
);
|
);
|
||||||
|
|
||||||
cx.dispatch_action(menu::SecondaryConfirm);
|
cx.dispatch_action(picker::ConfirmInput { secondary: false });
|
||||||
let tasks_picker = open_spawn_tasks(&workspace, cx);
|
let tasks_picker = open_spawn_tasks(&workspace, cx);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
query(&tasks_picker, cx),
|
query(&tasks_picker, cx),
|
||||||
@ -438,7 +453,7 @@ mod tests {
|
|||||||
"Last recently used one show task should be listed first"
|
"Last recently used one show task should be listed first"
|
||||||
);
|
);
|
||||||
|
|
||||||
cx.dispatch_action(menu::UseSelectedQuery);
|
cx.dispatch_action(picker::UseSelectedQuery);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
query(&tasks_picker, cx),
|
query(&tasks_picker, cx),
|
||||||
query_str,
|
query_str,
|
||||||
@ -449,6 +464,28 @@ mod tests {
|
|||||||
vec![query_str],
|
vec![query_str],
|
||||||
"Only custom task should be listed"
|
"Only custom task should be listed"
|
||||||
);
|
);
|
||||||
|
|
||||||
|
let query_str = "0";
|
||||||
|
cx.simulate_input(query_str);
|
||||||
|
assert_eq!(query(&tasks_picker, cx), "echo 40");
|
||||||
|
assert_eq!(
|
||||||
|
task_names(&tasks_picker, cx),
|
||||||
|
Vec::<String>::new(),
|
||||||
|
"New oneshot should not match any command query"
|
||||||
|
);
|
||||||
|
|
||||||
|
cx.dispatch_action(picker::ConfirmInput { secondary: true });
|
||||||
|
let tasks_picker = open_spawn_tasks(&workspace, cx);
|
||||||
|
assert_eq!(
|
||||||
|
query(&tasks_picker, cx),
|
||||||
|
"",
|
||||||
|
"Query should be reset after confirming"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
task_names(&tasks_picker, cx),
|
||||||
|
vec!["echo 4", "another one", "example task", "echo 40"],
|
||||||
|
"Last recently used one show task should be listed last, as it is a fire-and-forget task"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn open_spawn_tasks(
|
fn open_spawn_tasks(
|
||||||
|
Loading…
Reference in New Issue
Block a user