mirror of
https://github.com/zed-industries/zed.git
synced 2024-12-25 22:18:14 +03:00
Added menu::UseSelectedQuery
command that populates task modal query with the selected task name (#8572)
This commit is contained in:
parent
9bd5ebb74b
commit
b7429bf29d
3
Cargo.lock
generated
3
Cargo.lock
generated
@ -9140,12 +9140,15 @@ name = "tasks_ui"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"editor",
|
||||
"fuzzy",
|
||||
"gpui",
|
||||
"language",
|
||||
"menu",
|
||||
"picker",
|
||||
"project",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"task",
|
||||
"ui",
|
||||
"util",
|
||||
|
@ -16,6 +16,7 @@
|
||||
"ctrl-enter": "menu::SecondaryConfirm",
|
||||
"escape": "menu::Cancel",
|
||||
"ctrl-c": "menu::Cancel",
|
||||
"shift-enter": "menu::UseSelectedQuery",
|
||||
"ctrl-shift-w": "workspace::CloseWindow",
|
||||
"shift-escape": "workspace::ToggleZoom",
|
||||
"ctrl-o": "workspace::Open",
|
||||
|
@ -17,6 +17,7 @@
|
||||
"cmd-enter": "menu::SecondaryConfirm",
|
||||
"escape": "menu::Cancel",
|
||||
"ctrl-c": "menu::Cancel",
|
||||
"shift-enter": "menu::UseSelectedQuery",
|
||||
"cmd-shift-w": "workspace::CloseWindow",
|
||||
"shift-escape": "workspace::ToggleZoom",
|
||||
"cmd-o": "workspace::Open",
|
||||
|
@ -19,6 +19,7 @@ actions!(
|
||||
SelectNext,
|
||||
SelectFirst,
|
||||
SelectLast,
|
||||
ShowContextMenu
|
||||
ShowContextMenu,
|
||||
UseSelectedQuery,
|
||||
]
|
||||
);
|
||||
|
@ -32,6 +32,7 @@ pub struct Picker<D: PickerDelegate> {
|
||||
|
||||
pub trait PickerDelegate: Sized + 'static {
|
||||
type ListItem: IntoElement;
|
||||
|
||||
fn match_count(&self) -> usize;
|
||||
fn selected_index(&self) -> usize;
|
||||
fn separators_after_indices(&self) -> Vec<usize> {
|
||||
@ -57,6 +58,9 @@ pub trait PickerDelegate: Sized + 'static {
|
||||
|
||||
fn confirm(&mut self, secondary: bool, cx: &mut ViewContext<Picker<Self>>);
|
||||
fn dismissed(&mut self, cx: &mut ViewContext<Picker<Self>>);
|
||||
fn selected_as_query(&self) -> Option<String> {
|
||||
None
|
||||
}
|
||||
|
||||
fn render_match(
|
||||
&self,
|
||||
@ -239,6 +243,13 @@ impl<D: PickerDelegate> Picker<D> {
|
||||
}
|
||||
}
|
||||
|
||||
fn use_selected_query(&mut self, _: &menu::UseSelectedQuery, cx: &mut ViewContext<Self>) {
|
||||
if let Some(new_query) = self.delegate.selected_as_query() {
|
||||
self.set_query(new_query, cx);
|
||||
cx.stop_propagation();
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_click(&mut self, ix: usize, secondary: bool, cx: &mut ViewContext<Self>) {
|
||||
cx.stop_propagation();
|
||||
cx.prevent_default();
|
||||
@ -384,6 +395,7 @@ impl<D: PickerDelegate> Render for Picker<D> {
|
||||
.on_action(cx.listener(Self::cancel))
|
||||
.on_action(cx.listener(Self::confirm))
|
||||
.on_action(cx.listener(Self::secondary_confirm))
|
||||
.on_action(cx.listener(Self::use_selected_query))
|
||||
.child(picker_editor)
|
||||
.child(Divider::horizontal())
|
||||
.when(self.delegate.match_count() > 0, |el| {
|
||||
|
@ -99,6 +99,8 @@ pub use language::Location;
|
||||
pub use prettier::FORMAT_SUFFIX as TEST_PRETTIER_FORMAT_SUFFIX;
|
||||
pub use project_core::project_settings;
|
||||
pub use project_core::worktree::{self, *};
|
||||
#[cfg(feature = "test-support")]
|
||||
pub use task_inventory::test_inventory::*;
|
||||
pub use task_inventory::{Inventory, TaskSourceKind};
|
||||
|
||||
const MAX_SERVER_REINSTALL_ATTEMPT_COUNT: u64 = 4;
|
||||
|
@ -54,7 +54,7 @@ impl TaskSourceKind {
|
||||
}
|
||||
|
||||
impl Inventory {
|
||||
pub(crate) fn new(cx: &mut AppContext) -> Model<Self> {
|
||||
pub fn new(cx: &mut AppContext) -> Model<Self> {
|
||||
cx.new_model(|_| Self {
|
||||
sources: Vec::new(),
|
||||
last_scheduled_tasks: VecDeque::new(),
|
||||
@ -219,12 +219,140 @@ impl Inventory {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "test-support")]
|
||||
pub mod test_inventory {
|
||||
use std::{
|
||||
path::{Path, PathBuf},
|
||||
sync::Arc,
|
||||
};
|
||||
|
||||
use gpui::{AppContext, Context as _, Model, ModelContext, TestAppContext};
|
||||
use project_core::worktree::WorktreeId;
|
||||
use task::{Task, TaskId, TaskSource};
|
||||
|
||||
use crate::Inventory;
|
||||
|
||||
use super::TaskSourceKind;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct TestTask {
|
||||
pub id: task::TaskId,
|
||||
pub name: String,
|
||||
}
|
||||
|
||||
impl Task for TestTask {
|
||||
fn id(&self) -> &TaskId {
|
||||
&self.id
|
||||
}
|
||||
|
||||
fn name(&self) -> &str {
|
||||
&self.name
|
||||
}
|
||||
|
||||
fn cwd(&self) -> Option<&Path> {
|
||||
None
|
||||
}
|
||||
|
||||
fn exec(&self, _cwd: Option<PathBuf>) -> Option<task::SpawnInTerminal> {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub struct StaticTestSource {
|
||||
pub tasks: Vec<TestTask>,
|
||||
}
|
||||
|
||||
impl StaticTestSource {
|
||||
pub fn new(
|
||||
task_names: impl IntoIterator<Item = String>,
|
||||
cx: &mut AppContext,
|
||||
) -> Model<Box<dyn TaskSource>> {
|
||||
cx.new_model(|_| {
|
||||
Box::new(Self {
|
||||
tasks: task_names
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
.map(|(i, name)| TestTask {
|
||||
id: TaskId(format!("task_{i}_{name}")),
|
||||
name,
|
||||
})
|
||||
.collect(),
|
||||
}) as Box<dyn TaskSource>
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl TaskSource for StaticTestSource {
|
||||
fn tasks_for_path(
|
||||
&mut self,
|
||||
_path: Option<&Path>,
|
||||
_cx: &mut ModelContext<Box<dyn TaskSource>>,
|
||||
) -> Vec<Arc<dyn Task>> {
|
||||
self.tasks
|
||||
.clone()
|
||||
.into_iter()
|
||||
.map(|task| Arc::new(task) as Arc<dyn Task>)
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn as_any(&mut self) -> &mut dyn std::any::Any {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
pub fn list_task_names(
|
||||
inventory: &Model<Inventory>,
|
||||
path: Option<&Path>,
|
||||
worktree: Option<WorktreeId>,
|
||||
lru: bool,
|
||||
cx: &mut TestAppContext,
|
||||
) -> Vec<String> {
|
||||
inventory.update(cx, |inventory, cx| {
|
||||
inventory
|
||||
.list_tasks(path, worktree, lru, cx)
|
||||
.into_iter()
|
||||
.map(|(_, task)| task.name().to_string())
|
||||
.collect()
|
||||
})
|
||||
}
|
||||
|
||||
pub fn register_task_used(
|
||||
inventory: &Model<Inventory>,
|
||||
task_name: &str,
|
||||
cx: &mut TestAppContext,
|
||||
) {
|
||||
inventory.update(cx, |inventory, cx| {
|
||||
let task = inventory
|
||||
.list_tasks(None, None, false, cx)
|
||||
.into_iter()
|
||||
.find(|(_, task)| task.name() == task_name)
|
||||
.unwrap_or_else(|| panic!("Failed to find task with name {task_name}"));
|
||||
inventory.task_scheduled(task.1.id().clone());
|
||||
});
|
||||
}
|
||||
|
||||
pub fn list_tasks(
|
||||
inventory: &Model<Inventory>,
|
||||
path: Option<&Path>,
|
||||
worktree: Option<WorktreeId>,
|
||||
lru: bool,
|
||||
cx: &mut TestAppContext,
|
||||
) -> Vec<(TaskSourceKind, String)> {
|
||||
inventory.update(cx, |inventory, cx| {
|
||||
inventory
|
||||
.list_tasks(path, worktree, lru, cx)
|
||||
.into_iter()
|
||||
.map(|(source_kind, task)| (source_kind, task.name().to_string()))
|
||||
.collect()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::path::PathBuf;
|
||||
|
||||
use gpui::TestAppContext;
|
||||
|
||||
use super::test_inventory::*;
|
||||
use super::*;
|
||||
|
||||
#[gpui::test]
|
||||
@ -532,114 +660,4 @@ mod tests {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
struct TestTask {
|
||||
id: TaskId,
|
||||
name: String,
|
||||
}
|
||||
|
||||
impl Task for TestTask {
|
||||
fn id(&self) -> &TaskId {
|
||||
&self.id
|
||||
}
|
||||
|
||||
fn name(&self) -> &str {
|
||||
&self.name
|
||||
}
|
||||
|
||||
fn cwd(&self) -> Option<&Path> {
|
||||
None
|
||||
}
|
||||
|
||||
fn exec(&self, _cwd: Option<PathBuf>) -> Option<task::SpawnInTerminal> {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
struct StaticTestSource {
|
||||
tasks: Vec<TestTask>,
|
||||
}
|
||||
|
||||
impl StaticTestSource {
|
||||
fn new(
|
||||
task_names: impl IntoIterator<Item = String>,
|
||||
cx: &mut AppContext,
|
||||
) -> Model<Box<dyn TaskSource>> {
|
||||
cx.new_model(|_| {
|
||||
Box::new(Self {
|
||||
tasks: task_names
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
.map(|(i, name)| TestTask {
|
||||
id: TaskId(format!("task_{i}_{name}")),
|
||||
name,
|
||||
})
|
||||
.collect(),
|
||||
}) as Box<dyn TaskSource>
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl TaskSource for StaticTestSource {
|
||||
fn tasks_for_path(
|
||||
&mut self,
|
||||
// static task source does not depend on path input
|
||||
_: Option<&Path>,
|
||||
_cx: &mut ModelContext<Box<dyn TaskSource>>,
|
||||
) -> Vec<Arc<dyn Task>> {
|
||||
self.tasks
|
||||
.clone()
|
||||
.into_iter()
|
||||
.map(|task| Arc::new(task) as Arc<dyn Task>)
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn as_any(&mut self) -> &mut dyn std::any::Any {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
fn list_task_names(
|
||||
inventory: &Model<Inventory>,
|
||||
path: Option<&Path>,
|
||||
worktree: Option<WorktreeId>,
|
||||
lru: bool,
|
||||
cx: &mut TestAppContext,
|
||||
) -> Vec<String> {
|
||||
inventory.update(cx, |inventory, cx| {
|
||||
inventory
|
||||
.list_tasks(path, worktree, lru, cx)
|
||||
.into_iter()
|
||||
.map(|(_, task)| task.name().to_string())
|
||||
.collect()
|
||||
})
|
||||
}
|
||||
|
||||
fn list_tasks(
|
||||
inventory: &Model<Inventory>,
|
||||
path: Option<&Path>,
|
||||
worktree: Option<WorktreeId>,
|
||||
lru: bool,
|
||||
cx: &mut TestAppContext,
|
||||
) -> Vec<(TaskSourceKind, String)> {
|
||||
inventory.update(cx, |inventory, cx| {
|
||||
inventory
|
||||
.list_tasks(path, worktree, lru, cx)
|
||||
.into_iter()
|
||||
.map(|(source_kind, task)| (source_kind, task.name().to_string()))
|
||||
.collect()
|
||||
})
|
||||
}
|
||||
|
||||
fn register_task_used(inventory: &Model<Inventory>, task_name: &str, cx: &mut TestAppContext) {
|
||||
inventory.update(cx, |inventory, cx| {
|
||||
let (_, task) = inventory
|
||||
.list_tasks(None, None, false, cx)
|
||||
.into_iter()
|
||||
.find(|(_, task)| task.name() == task_name)
|
||||
.unwrap_or_else(|| panic!("Failed to find task with name {task_name}"));
|
||||
inventory.task_scheduled(task.id().clone());
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -17,3 +17,10 @@ serde.workspace = true
|
||||
ui.workspace = true
|
||||
util.workspace = true
|
||||
workspace.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
editor = { workspace = true, features = ["test-support"] }
|
||||
language = { workspace = true, features = ["test-support"] }
|
||||
project = { workspace = true, features = ["test-support"] }
|
||||
serde_json.workspace = true
|
||||
workspace = { workspace = true, features = ["test-support"] }
|
||||
|
@ -97,6 +97,7 @@ impl TasksModal {
|
||||
impl Render for TasksModal {
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl gpui::prelude::IntoElement {
|
||||
v_flex()
|
||||
.key_context("TasksModal")
|
||||
.w(rems(34.))
|
||||
.child(self.picker.clone())
|
||||
.on_mouse_down_out(cx.listener(|modal, _, cx| {
|
||||
@ -134,9 +135,10 @@ impl PickerDelegate for TasksModalDelegate {
|
||||
|
||||
fn placeholder_text(&self, cx: &mut WindowContext) -> Arc<str> {
|
||||
Arc::from(format!(
|
||||
"{} runs the selected task, {} spawns a bash-like task from the prompt",
|
||||
cx.keystroke_text_for(&menu::Confirm),
|
||||
"{} 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(&menu::SecondaryConfirm),
|
||||
cx.keystroke_text_for(&menu::Confirm),
|
||||
))
|
||||
}
|
||||
|
||||
@ -266,4 +268,179 @@ impl PickerDelegate for TasksModalDelegate {
|
||||
.child(highlighted_location.render(cx)),
|
||||
)
|
||||
}
|
||||
|
||||
fn selected_as_query(&self) -> Option<String> {
|
||||
Some(self.matches.get(self.selected_index())?.string.clone())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use gpui::{TestAppContext, VisualTestContext};
|
||||
use project::{FakeFs, Project};
|
||||
use serde_json::json;
|
||||
use workspace::AppState;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_name(cx: &mut TestAppContext) {
|
||||
init_test(cx);
|
||||
let fs = FakeFs::new(cx.executor());
|
||||
fs.insert_tree(
|
||||
"/dir",
|
||||
json!({
|
||||
".zed": {
|
||||
"tasks.json": r#"[
|
||||
{
|
||||
"label": "example task",
|
||||
"command": "echo",
|
||||
"args": ["4"]
|
||||
},
|
||||
{
|
||||
"label": "another one",
|
||||
"command": "echo",
|
||||
"args": ["55"]
|
||||
},
|
||||
]"#,
|
||||
},
|
||||
"a.ts": "a"
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
|
||||
let project = Project::test(fs, ["/dir".as_ref()], cx).await;
|
||||
project.update(cx, |project, cx| {
|
||||
project.task_inventory().update(cx, |inventory, cx| {
|
||||
inventory.add_source(TaskSourceKind::UserInput, |cx| OneshotSource::new(cx), cx)
|
||||
})
|
||||
});
|
||||
|
||||
let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
|
||||
|
||||
let tasks_picker = open_spawn_tasks(&workspace, cx);
|
||||
assert_eq!(
|
||||
query(&tasks_picker, cx),
|
||||
"",
|
||||
"Initial query should be empty"
|
||||
);
|
||||
assert_eq!(
|
||||
task_names(&tasks_picker, cx),
|
||||
vec!["another one", "example task"],
|
||||
"Initial tasks should be listed in alphabetical order"
|
||||
);
|
||||
|
||||
let query_str = "tas";
|
||||
cx.simulate_input(query_str);
|
||||
assert_eq!(query(&tasks_picker, cx), query_str);
|
||||
assert_eq!(
|
||||
task_names(&tasks_picker, cx),
|
||||
vec!["example task"],
|
||||
"Only one task should match the query {query_str}"
|
||||
);
|
||||
|
||||
cx.dispatch_action(menu::UseSelectedQuery);
|
||||
assert_eq!(
|
||||
query(&tasks_picker, cx),
|
||||
"example task",
|
||||
"Query should be set to the selected task's name"
|
||||
);
|
||||
assert_eq!(
|
||||
task_names(&tasks_picker, cx),
|
||||
vec!["example task"],
|
||||
"No other tasks should be listed"
|
||||
);
|
||||
cx.dispatch_action(menu::Confirm);
|
||||
|
||||
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!["example task", "another one"],
|
||||
"Last recently used task should be listed first"
|
||||
);
|
||||
|
||||
let query_str = "echo 4";
|
||||
cx.simulate_input(query_str);
|
||||
assert_eq!(query(&tasks_picker, cx), query_str);
|
||||
assert_eq!(
|
||||
task_names(&tasks_picker, cx),
|
||||
Vec::<String>::new(),
|
||||
"No tasks should match custom command query"
|
||||
);
|
||||
|
||||
cx.dispatch_action(menu::SecondaryConfirm);
|
||||
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![query_str, "example task", "another one"],
|
||||
"Last recently used one show task should be listed first"
|
||||
);
|
||||
|
||||
cx.dispatch_action(menu::UseSelectedQuery);
|
||||
assert_eq!(
|
||||
query(&tasks_picker, cx),
|
||||
query_str,
|
||||
"Query should be set to the custom task's name"
|
||||
);
|
||||
assert_eq!(
|
||||
task_names(&tasks_picker, cx),
|
||||
vec![query_str],
|
||||
"Only custom task should be listed"
|
||||
);
|
||||
}
|
||||
|
||||
fn open_spawn_tasks(
|
||||
workspace: &View<Workspace>,
|
||||
cx: &mut VisualTestContext,
|
||||
) -> View<Picker<TasksModalDelegate>> {
|
||||
cx.dispatch_action(crate::modal::Spawn);
|
||||
workspace.update(cx, |workspace, cx| {
|
||||
workspace
|
||||
.active_modal::<TasksModal>(cx)
|
||||
.unwrap()
|
||||
.read(cx)
|
||||
.picker
|
||||
.clone()
|
||||
})
|
||||
}
|
||||
|
||||
fn query(spawn_tasks: &View<Picker<TasksModalDelegate>>, cx: &mut VisualTestContext) -> String {
|
||||
spawn_tasks.update(cx, |spawn_tasks, cx| spawn_tasks.query(cx))
|
||||
}
|
||||
|
||||
fn task_names(
|
||||
spawn_tasks: &View<Picker<TasksModalDelegate>>,
|
||||
cx: &mut VisualTestContext,
|
||||
) -> Vec<String> {
|
||||
spawn_tasks.update(cx, |spawn_tasks, _| {
|
||||
spawn_tasks
|
||||
.delegate
|
||||
.matches
|
||||
.iter()
|
||||
.map(|hit| hit.string.clone())
|
||||
.collect::<Vec<_>>()
|
||||
})
|
||||
}
|
||||
|
||||
fn init_test(cx: &mut TestAppContext) -> Arc<AppState> {
|
||||
cx.update(|cx| {
|
||||
let state = AppState::test(cx);
|
||||
language::init(cx);
|
||||
crate::init(cx);
|
||||
editor::init(cx);
|
||||
workspace::init_settings(cx);
|
||||
Project::init_settings(cx);
|
||||
state
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -4,6 +4,8 @@ Zed supports ways to spawn (and rerun) commands using its integrated terminal to
|
||||
|
||||
Currently, two kinds of tasks are supported, but more will be added in the future.
|
||||
|
||||
All tasks are are sorted in LRU order and their names can be used (with `menu::UseSelectedQuery`, `shift-enter` by default) as an input text for quicker oneshot task edit-spawn cycle.
|
||||
|
||||
## Static tasks
|
||||
|
||||
Tasks, defined in a config file (`tasks.json` in the Zed config directory) that do not depend on the current editor or its content.
|
||||
|
Loading…
Reference in New Issue
Block a user