New list (used tasks are above the separator line, sorted by the usage recency), then all language tasks, then project-local and global tasks are listed. Note that there are two test tasks (for `test_name_1` and `test_name_2` functions) that are created from the same task template: <img width="563" alt="Screenshot 2024-04-10 at 01 00 46" src="https://github.com/zed-industries/zed/assets/2690773/7455a82f-2af2-47bf-99bd-d9c5a36e64ab"> Tasks are deduplicated by labels, with the used tasks left in case of the conflict with the new tasks from the template: <img width="555" alt="Screenshot 2024-04-10 at 01 01 06" src="https://github.com/zed-industries/zed/assets/2690773/8f5a249e-abec-46ef-a991-08c6d0348648"> Regular recent tasks can be now removed too: <img width="565" alt="Screenshot 2024-04-10 at 01 00 55" src="https://github.com/zed-industries/zed/assets/2690773/0976b8fe-b5d7-4d2a-953d-1d8b1f216192"> When the caret is in the place where no function symbol could be retrieved, no cargo tests for function are listed in tasks: <img width="556" alt="image" src="https://github.com/zed-industries/zed/assets/2690773/df30feba-fe27-4645-8be9-02afc70f02da"> Part of https://github.com/zed-industries/zed/issues/10132 Reworks the task code to simplify it and enable proper task labels. * removes `trait Task`, renames `Definition` into `TaskTemplate` and use that instead of `Arc<dyn Task>` everywhere * implement more generic `TaskId` generation that depends on the `TaskContext` and `TaskTemplate` * remove `TaskId` out of the template and only create it after "resolving" the template into the `ResolvedTask`: this way, task templates, task state (`TaskContext`) and task "result" (resolved state) are clearly separated and are not mixed * implement the logic for filtering out non-related language tasks and tasks that have non-resolved Zed task variables * rework Zed template-vs-resolved-task display in modal: now all reruns and recently used tasks are resolved tasks with "fixed" context (unless configured otherwise in the task json) that are always shown, and Zed can add on top tasks with different context that are derived from the same template as the used, resolved tasks * sort the tasks list better, showing more specific and least recently used tasks higher * shows a separator between used and unused tasks, allow removing the used tasks same as the oneshot ones * remote the Oneshot task source as redundant: all oneshot tasks are now stored in the inventory's history * when reusing the tasks as query in the modal, paste the expanded task label now, show trimmed resolved label in the modal * adjusts Rust and Elixir task labels to be more descriptive and closer to bash scripts Release Notes: - Improved task modal ordering, run and deletion capabilities
483 lines
18 KiB
Rust
483 lines
18 KiB
Rust
use std::{path::PathBuf, sync::Arc};
|
|
|
|
use ::settings::Settings;
|
|
use editor::Editor;
|
|
use gpui::{AppContext, ViewContext, WindowContext};
|
|
use language::{Language, Point};
|
|
use modal::{Spawn, TasksModal};
|
|
use project::{Location, TaskSourceKind, WorktreeId};
|
|
use task::{ResolvedTask, TaskContext, TaskTemplate, TaskVariables, VariableName};
|
|
use util::ResultExt;
|
|
use workspace::Workspace;
|
|
|
|
mod modal;
|
|
mod settings;
|
|
mod status_indicator;
|
|
|
|
pub use status_indicator::TaskStatusIndicator;
|
|
|
|
pub fn init(cx: &mut AppContext) {
|
|
settings::TaskSettings::register(cx);
|
|
cx.observe_new_views(
|
|
|workspace: &mut Workspace, _: &mut ViewContext<Workspace>| {
|
|
workspace
|
|
.register_action(spawn_task_or_modal)
|
|
.register_action(move |workspace, action: &modal::Rerun, cx| {
|
|
if let Some((task_source_kind, last_scheduled_task)) =
|
|
workspace.project().update(cx, |project, cx| {
|
|
project.task_inventory().read(cx).last_scheduled_task()
|
|
})
|
|
{
|
|
if action.reevaluate_context {
|
|
let original_task = last_scheduled_task.original_task;
|
|
let cwd = task_cwd(workspace, cx).log_err().flatten();
|
|
let task_context = task_context(workspace, cwd, cx);
|
|
schedule_task(
|
|
workspace,
|
|
task_source_kind,
|
|
&original_task,
|
|
task_context,
|
|
false,
|
|
cx,
|
|
)
|
|
} else {
|
|
schedule_resolved_task(
|
|
workspace,
|
|
task_source_kind,
|
|
last_scheduled_task,
|
|
false,
|
|
cx,
|
|
);
|
|
}
|
|
};
|
|
});
|
|
},
|
|
)
|
|
.detach();
|
|
}
|
|
|
|
fn spawn_task_or_modal(workspace: &mut Workspace, action: &Spawn, cx: &mut ViewContext<Workspace>) {
|
|
match &action.task_name {
|
|
Some(name) => spawn_task_with_name(name.clone(), cx),
|
|
None => {
|
|
let inventory = workspace.project().read(cx).task_inventory().clone();
|
|
let workspace_handle = workspace.weak_handle();
|
|
let cwd = task_cwd(workspace, cx).log_err().flatten();
|
|
let task_context = task_context(workspace, cwd, cx);
|
|
workspace.toggle_modal(cx, |cx| {
|
|
TasksModal::new(inventory, task_context, workspace_handle, cx)
|
|
})
|
|
}
|
|
}
|
|
}
|
|
|
|
fn spawn_task_with_name(name: String, cx: &mut ViewContext<Workspace>) {
|
|
cx.spawn(|workspace, mut cx| async move {
|
|
let did_spawn = workspace
|
|
.update(&mut cx, |workspace, cx| {
|
|
let (worktree, language) = active_item_selection_properties(workspace, cx);
|
|
let tasks = workspace.project().update(cx, |project, cx| {
|
|
project.task_inventory().update(cx, |inventory, cx| {
|
|
inventory.list_tasks(language, worktree, cx)
|
|
})
|
|
});
|
|
let (task_source_kind, target_task) =
|
|
tasks.into_iter().find(|(_, task)| task.label == name)?;
|
|
let cwd = task_cwd(workspace, cx).log_err().flatten();
|
|
let task_context = task_context(workspace, cwd, cx);
|
|
schedule_task(
|
|
workspace,
|
|
task_source_kind,
|
|
&target_task,
|
|
task_context,
|
|
false,
|
|
cx,
|
|
);
|
|
Some(())
|
|
})
|
|
.ok()
|
|
.flatten()
|
|
.is_some();
|
|
if !did_spawn {
|
|
workspace
|
|
.update(&mut cx, |workspace, cx| {
|
|
spawn_task_or_modal(workspace, &Spawn::default(), cx);
|
|
})
|
|
.ok();
|
|
}
|
|
})
|
|
.detach();
|
|
}
|
|
|
|
fn active_item_selection_properties(
|
|
workspace: &Workspace,
|
|
cx: &mut WindowContext,
|
|
) -> (Option<WorktreeId>, Option<Arc<Language>>) {
|
|
let active_item = workspace.active_item(cx);
|
|
let worktree_id = active_item
|
|
.as_ref()
|
|
.and_then(|item| item.project_path(cx))
|
|
.map(|path| path.worktree_id);
|
|
let language = active_item
|
|
.and_then(|active_item| active_item.act_as::<Editor>(cx))
|
|
.and_then(|editor| {
|
|
editor.update(cx, |editor, cx| {
|
|
let selection = editor.selections.newest::<usize>(cx);
|
|
let (buffer, buffer_position, _) = editor
|
|
.buffer()
|
|
.read(cx)
|
|
.point_to_buffer_offset(selection.start, cx)?;
|
|
buffer.read(cx).language_at(buffer_position)
|
|
})
|
|
});
|
|
(worktree_id, language)
|
|
}
|
|
|
|
fn task_context(
|
|
workspace: &Workspace,
|
|
cwd: Option<PathBuf>,
|
|
cx: &mut WindowContext<'_>,
|
|
) -> TaskContext {
|
|
let current_editor = workspace
|
|
.active_item(cx)
|
|
.and_then(|item| item.act_as::<Editor>(cx));
|
|
if let Some(current_editor) = current_editor {
|
|
(|| {
|
|
let editor = current_editor.read(cx);
|
|
let selection = editor.selections.newest::<usize>(cx);
|
|
let (buffer, _, _) = editor
|
|
.buffer()
|
|
.read(cx)
|
|
.point_to_buffer_offset(selection.start, cx)?;
|
|
|
|
current_editor.update(cx, |editor, cx| {
|
|
let snapshot = editor.snapshot(cx);
|
|
let selection_range = selection.range();
|
|
let start = snapshot
|
|
.display_snapshot
|
|
.buffer_snapshot
|
|
.anchor_after(selection_range.start)
|
|
.text_anchor;
|
|
let end = snapshot
|
|
.display_snapshot
|
|
.buffer_snapshot
|
|
.anchor_after(selection_range.end)
|
|
.text_anchor;
|
|
let Point { row, column } = snapshot
|
|
.display_snapshot
|
|
.buffer_snapshot
|
|
.offset_to_point(selection_range.start);
|
|
let row = row + 1;
|
|
let column = column + 1;
|
|
let location = Location {
|
|
buffer: buffer.clone(),
|
|
range: start..end,
|
|
};
|
|
|
|
let current_file = location
|
|
.buffer
|
|
.read(cx)
|
|
.file()
|
|
.and_then(|file| file.as_local())
|
|
.map(|file| file.abs_path(cx).to_string_lossy().to_string());
|
|
let worktree_id = location
|
|
.buffer
|
|
.read(cx)
|
|
.file()
|
|
.map(|file| WorktreeId::from_usize(file.worktree_id()));
|
|
let context = buffer
|
|
.read(cx)
|
|
.language()
|
|
.and_then(|language| language.context_provider())
|
|
.and_then(|provider| provider.build_context(location, cx).ok());
|
|
|
|
let worktree_path = worktree_id.and_then(|worktree_id| {
|
|
workspace
|
|
.project()
|
|
.read(cx)
|
|
.worktree_for_id(worktree_id, cx)
|
|
.map(|worktree| worktree.read(cx).abs_path().to_string_lossy().to_string())
|
|
});
|
|
|
|
let selected_text = buffer.read(cx).chars_for_range(selection_range).collect();
|
|
|
|
let mut task_variables = TaskVariables::from_iter([
|
|
(VariableName::Row, row.to_string()),
|
|
(VariableName::Column, column.to_string()),
|
|
(VariableName::SelectedText, selected_text),
|
|
]);
|
|
if let Some(path) = current_file {
|
|
task_variables.insert(VariableName::File, path);
|
|
}
|
|
if let Some(worktree_path) = worktree_path {
|
|
task_variables.insert(VariableName::WorktreeRoot, worktree_path);
|
|
}
|
|
if let Some(language_context) = context {
|
|
task_variables.extend(language_context);
|
|
}
|
|
|
|
Some(TaskContext {
|
|
cwd: cwd.clone(),
|
|
task_variables,
|
|
})
|
|
})
|
|
})()
|
|
.unwrap_or_else(|| TaskContext {
|
|
cwd,
|
|
task_variables: Default::default(),
|
|
})
|
|
} else {
|
|
TaskContext {
|
|
cwd,
|
|
task_variables: Default::default(),
|
|
}
|
|
}
|
|
}
|
|
|
|
fn schedule_task(
|
|
workspace: &Workspace,
|
|
task_source_kind: TaskSourceKind,
|
|
task_to_resolve: &TaskTemplate,
|
|
task_cx: TaskContext,
|
|
omit_history: bool,
|
|
cx: &mut ViewContext<'_, Workspace>,
|
|
) {
|
|
if let Some(spawn_in_terminal) =
|
|
task_to_resolve.resolve_task(&task_source_kind.to_id_base(), task_cx)
|
|
{
|
|
schedule_resolved_task(
|
|
workspace,
|
|
task_source_kind,
|
|
spawn_in_terminal,
|
|
omit_history,
|
|
cx,
|
|
);
|
|
}
|
|
}
|
|
|
|
fn schedule_resolved_task(
|
|
workspace: &Workspace,
|
|
task_source_kind: TaskSourceKind,
|
|
mut resolved_task: ResolvedTask,
|
|
omit_history: bool,
|
|
cx: &mut ViewContext<'_, Workspace>,
|
|
) {
|
|
if let Some(spawn_in_terminal) = resolved_task.resolved.take() {
|
|
if !omit_history {
|
|
resolved_task.resolved = Some(spawn_in_terminal.clone());
|
|
workspace.project().update(cx, |project, cx| {
|
|
project.task_inventory().update(cx, |inventory, _| {
|
|
inventory.task_scheduled(task_source_kind, resolved_task);
|
|
})
|
|
});
|
|
}
|
|
cx.emit(workspace::Event::SpawnTask(spawn_in_terminal));
|
|
}
|
|
}
|
|
|
|
fn task_cwd(workspace: &Workspace, cx: &mut WindowContext) -> anyhow::Result<Option<PathBuf>> {
|
|
let project = workspace.project().read(cx);
|
|
let available_worktrees = project
|
|
.worktrees()
|
|
.filter(|worktree| {
|
|
let worktree = worktree.read(cx);
|
|
worktree.is_visible()
|
|
&& worktree.is_local()
|
|
&& worktree.root_entry().map_or(false, |e| e.is_dir())
|
|
})
|
|
.collect::<Vec<_>>();
|
|
let cwd = match available_worktrees.len() {
|
|
0 => None,
|
|
1 => Some(available_worktrees[0].read(cx).abs_path()),
|
|
_ => {
|
|
let cwd_for_active_entry = project.active_entry().and_then(|entry_id| {
|
|
available_worktrees.into_iter().find_map(|worktree| {
|
|
let worktree = worktree.read(cx);
|
|
if worktree.contains_entry(entry_id) {
|
|
Some(worktree.abs_path())
|
|
} else {
|
|
None
|
|
}
|
|
})
|
|
});
|
|
anyhow::ensure!(
|
|
cwd_for_active_entry.is_some(),
|
|
"Cannot determine task cwd for multiple worktrees"
|
|
);
|
|
cwd_for_active_entry
|
|
}
|
|
};
|
|
Ok(cwd.map(|path| path.to_path_buf()))
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use std::sync::Arc;
|
|
|
|
use editor::Editor;
|
|
use gpui::{Entity, TestAppContext};
|
|
use language::{Language, LanguageConfig, SymbolContextProvider};
|
|
use project::{FakeFs, Project};
|
|
use serde_json::json;
|
|
use task::{TaskContext, TaskVariables, VariableName};
|
|
use ui::VisualContext;
|
|
use workspace::{AppState, Workspace};
|
|
|
|
use crate::{task_context, task_cwd};
|
|
|
|
#[gpui::test]
|
|
async fn test_default_language_context(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": "function this_is_a_test() { }",
|
|
"rust": {
|
|
"b.rs": "use std; fn this_is_a_rust_file() { }",
|
|
}
|
|
|
|
}),
|
|
)
|
|
.await;
|
|
|
|
let rust_language = Arc::new(
|
|
Language::new(
|
|
LanguageConfig::default(),
|
|
Some(tree_sitter_rust::language()),
|
|
)
|
|
.with_outline_query(
|
|
r#"(function_item
|
|
"fn" @context
|
|
name: (_) @name) @item"#,
|
|
)
|
|
.unwrap()
|
|
.with_context_provider(Some(Arc::new(SymbolContextProvider))),
|
|
);
|
|
|
|
let typescript_language = Arc::new(
|
|
Language::new(
|
|
LanguageConfig::default(),
|
|
Some(tree_sitter_typescript::language_typescript()),
|
|
)
|
|
.with_outline_query(
|
|
r#"(function_declaration
|
|
"async"? @context
|
|
"function" @context
|
|
name: (_) @name
|
|
parameters: (formal_parameters
|
|
"(" @context
|
|
")" @context)) @item"#,
|
|
)
|
|
.unwrap()
|
|
.with_context_provider(Some(Arc::new(SymbolContextProvider))),
|
|
);
|
|
let project = Project::test(fs, ["/dir".as_ref()], cx).await;
|
|
let worktree_id = project.update(cx, |project, cx| {
|
|
project.worktrees().next().unwrap().read(cx).id()
|
|
});
|
|
let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
|
|
|
|
let buffer1 = workspace
|
|
.update(cx, |this, cx| {
|
|
this.project()
|
|
.update(cx, |this, cx| this.open_buffer((worktree_id, "a.ts"), cx))
|
|
})
|
|
.await
|
|
.unwrap();
|
|
buffer1.update(cx, |this, cx| {
|
|
this.set_language(Some(typescript_language), cx)
|
|
});
|
|
let editor1 = cx.new_view(|cx| Editor::for_buffer(buffer1, Some(project.clone()), cx));
|
|
let buffer2 = workspace
|
|
.update(cx, |this, cx| {
|
|
this.project().update(cx, |this, cx| {
|
|
this.open_buffer((worktree_id, "rust/b.rs"), cx)
|
|
})
|
|
})
|
|
.await
|
|
.unwrap();
|
|
buffer2.update(cx, |this, cx| this.set_language(Some(rust_language), cx));
|
|
let editor2 = cx.new_view(|cx| Editor::for_buffer(buffer2, Some(project), cx));
|
|
workspace.update(cx, |this, cx| {
|
|
this.add_item_to_center(Box::new(editor1.clone()), cx);
|
|
this.add_item_to_center(Box::new(editor2.clone()), cx);
|
|
assert_eq!(this.active_item(cx).unwrap().item_id(), editor2.entity_id());
|
|
assert_eq!(
|
|
task_context(this, task_cwd(this, cx).unwrap(), cx),
|
|
TaskContext {
|
|
cwd: Some("/dir".into()),
|
|
task_variables: TaskVariables::from_iter([
|
|
(VariableName::File, "/dir/rust/b.rs".into()),
|
|
(VariableName::WorktreeRoot, "/dir".into()),
|
|
(VariableName::Row, "1".into()),
|
|
(VariableName::Column, "1".into()),
|
|
(VariableName::SelectedText, "".into())
|
|
])
|
|
}
|
|
);
|
|
// And now, let's select an identifier.
|
|
editor2.update(cx, |this, cx| {
|
|
this.change_selections(None, cx, |selections| selections.select_ranges([14..18]))
|
|
});
|
|
assert_eq!(
|
|
task_context(this, task_cwd(this, cx).unwrap(), cx),
|
|
TaskContext {
|
|
cwd: Some("/dir".into()),
|
|
task_variables: TaskVariables::from_iter([
|
|
(VariableName::File, "/dir/rust/b.rs".into()),
|
|
(VariableName::WorktreeRoot, "/dir".into()),
|
|
(VariableName::Row, "1".into()),
|
|
(VariableName::Column, "15".into()),
|
|
(VariableName::SelectedText, "is_i".into()),
|
|
(VariableName::Symbol, "this_is_a_rust_file".into()),
|
|
])
|
|
}
|
|
);
|
|
|
|
// Now, let's switch the active item to .ts file.
|
|
this.activate_item(&editor1, cx);
|
|
assert_eq!(
|
|
task_context(this, task_cwd(this, cx).unwrap(), cx),
|
|
TaskContext {
|
|
cwd: Some("/dir".into()),
|
|
task_variables: TaskVariables::from_iter([
|
|
(VariableName::File, "/dir/a.ts".into()),
|
|
(VariableName::WorktreeRoot, "/dir".into()),
|
|
(VariableName::Row, "1".into()),
|
|
(VariableName::Column, "1".into()),
|
|
(VariableName::SelectedText, "".into()),
|
|
(VariableName::Symbol, "this_is_a_test".into()),
|
|
])
|
|
}
|
|
);
|
|
});
|
|
}
|
|
|
|
pub(crate) 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
|
|
})
|
|
}
|
|
}
|