worktree_picker: Improve discoverability of tasks as a creation setup mechanism (#61148)

Sometimes when creating Git worktrees, it's necessary to set things up
so the new worktree is ready. For example, copying env.vars, installing
dependencies, etc. That was already possible in Zed through the tasks
system, but you'd only maybe know about that if you read the
documentation or is super familiar with it already; nothing in the
product in the context of worktrees told you about that. This is what
this PR does.

Now, in the worktree picker, there's a "" button that opens the
`tasks.json` file exposing the `create_worktree` hook, which allows you
to plugin all sorts of things to be run by the time of a worktree
creation. If you don't already have a `tasks.json` file, we will create
a new one for you with a worktree-creation template. Otherwise, we
either append the `create_worktree` hook content to it or just open the
file.

Here's a quick video:


https://github.com/user-attachments/assets/21908be4-306b-4cf3-bed0-50d52cad9d79

Release Notes:

- Git Worktrees: Improved discoverability of the `create_worktree` hook
for setting up things that need to happen by the time of worktree
creation.
This commit is contained in:
Danilo Leal authored and GitHub committed 2026-07-20 11:17:21 +00:00
1 parent a77838bb98
commit e1ee74dcbf
15 files changed
+323 -158

No files matched your search

Generated
+2 -2
View File
@@ -5034,9 +5034,7 @@ dependencies = [
"text",
"theme",
"theme_settings",
"tree-sitter",
"tree-sitter-go",
"tree-sitter-json",
"ui",
"ui_input",
"unindent",
@@ -18443,6 +18441,8 @@ dependencies = [
"serde",
"serde_json",
"task",
"tree-sitter",
"tree-sitter-json",
"tree-sitter-rust",
"tree-sitter-typescript",
"ui",
+1
View File
@@ -1571,6 +1571,7 @@
"bindings": {
"ctrl-shift-backspace": "worktree_picker::DeleteWorktree",
"ctrl-alt-shift-backspace": "worktree_picker::ForceDeleteWorktree",
"ctrl-shift-c": "zed::OpenWorktreeSetupTasks",
},
},
{
+1
View File
@@ -1626,6 +1626,7 @@
"bindings": {
"cmd-shift-backspace": "worktree_picker::DeleteWorktree",
"cmd-alt-shift-backspace": "worktree_picker::ForceDeleteWorktree",
"cmd-shift-c": "zed::OpenWorktreeSetupTasks",
},
},
{
+1
View File
@@ -1550,6 +1550,7 @@
"bindings": {
"ctrl-shift-backspace": "worktree_picker::DeleteWorktree",
"ctrl-alt-shift-backspace": "worktree_picker::ForceDeleteWorktree",
"ctrl-shift-,": "zed::OpenWorktreeSetupTasks",
},
},
{
+3
View File
@@ -55,5 +55,8 @@
"save": "none",
// Represents the tags for inline runnable indicators, or spawning multiple tasks at once.
// "tags": []
// Hooks that cause this task to run automatically on certain events:
// * `create_worktree` — run this task after creating a new git worktree (e.g. to install dependencies or copy untracked config files into it)
// "hooks": ["create_worktree"]
},
]
@@ -0,0 +1,23 @@
// Worktree setup tasks, stored in this project's tasks.json.
// See https://zed.dev/docs/tasks#hooks for the full Tasks Hooks documentation.
//
// Tasks with the `create_worktree` hook run automatically right after Zed
// creates a new git worktree. Use them to get a fresh worktree ready to work
// in — for example, installing dependencies or copying files that git doesn't
// track (like `.env`) from the original repository.
//
// Two variables are available to these tasks:
// * $ZED_WORKTREE_ROOT — the root directory of the newly created worktree
// * $ZED_MAIN_GIT_WORKTREE — the original repository's working directory
//
// Uncomment the example below and adjust the command to get started.
// Note: hooked tasks run as soon as a worktree is created, so only enable
// commands you want to run automatically.
[
// {
// "label": "Set up new worktree",
// "command": "cp -n \"$ZED_MAIN_GIT_WORKTREE/.env\" \"$ZED_WORKTREE_ROOT/\" && npm install",
// "cwd": "$ZED_WORKTREE_ROOT",
// "hooks": ["create_worktree"]
// }
]
-2
View File
@@ -68,8 +68,6 @@ terminal_view.workspace = true
text.workspace = true
theme.workspace = true
theme_settings.workspace = true
tree-sitter-json.workspace = true
tree-sitter = { workspace = true, features = ["wasm"] }
ui.workspace = true
ui_input.workspace = true
unindent = { workspace = true, optional = true }
+4 -73
View File
@@ -14,7 +14,7 @@ use collections::IndexMap;
use dap::adapters::DebugAdapterName;
use dap::{DapRegistry, StartDebuggingRequestArguments};
use dap::{client::SessionId, debugger_settings::DebuggerSettings};
use editor::{Editor, MultiBufferOffset, ToPoint};
use editor::Editor;
use feature_flags::{FeatureFlag, FeatureFlagAppExt as _, PresenceFlag, register_feature_flag};
use gpui::{
Action, Anchor, App, AsyncWindowContext, ClipboardItem, Context, DismissEvent, Entity,
@@ -29,9 +29,9 @@ use project::{DebugScenarioContext, Fs, ProjectPath, TaskSourceKind, WorktreeId}
use project::{Project, debugger::session::ThreadStatus};
use rpc::proto::{self};
use settings::Settings;
use std::sync::{Arc, LazyLock};
use std::sync::Arc;
use task::{DebugScenario, SharedTaskContext};
use tree_sitter::{Query, StreamingIterator as _};
use ui::{
ButtonLike, ContextMenu, Divider, ElevationIndex, PopoverMenu, PopoverMenuHandle, SplitButton,
Tab, TintColor, Tooltip, prelude::*,
@@ -1220,76 +1220,7 @@ impl DebugPanel {
window: &mut Window,
cx: &mut Context<Editor>,
) -> Result<Task<Result<()>>> {
static LAST_ITEM_QUERY: LazyLock<Query> = LazyLock::new(|| {
Query::new(
&tree_sitter_json::LANGUAGE.into(),
"(document (array (object) @object))", // TODO: use "." anchor to only match last object
)
.expect("Failed to create LAST_ITEM_QUERY")
});
static EMPTY_ARRAY_QUERY: LazyLock<Query> = LazyLock::new(|| {
Query::new(
&tree_sitter_json::LANGUAGE.into(),
"(document (array) @array)",
)
.expect("Failed to create EMPTY_ARRAY_QUERY")
});
let content = editor.text(cx);
let mut parser = tree_sitter::Parser::new();
parser.set_language(&tree_sitter_json::LANGUAGE.into())?;
let mut cursor = tree_sitter::QueryCursor::new();
let syntax_tree = parser
.parse(&content, None)
.context("could not parse debug.json")?;
let mut matches = cursor.matches(
&LAST_ITEM_QUERY,
syntax_tree.root_node(),
content.as_bytes(),
);
let mut last_offset = None;
while let Some(mat) = matches.next() {
if let Some(pos) = mat.captures.first().map(|m| m.node.byte_range().end) {
last_offset = Some(MultiBufferOffset(pos))
}
}
let mut edits = Vec::new();
let mut cursor_position = MultiBufferOffset(0);
if let Some(pos) = last_offset {
edits.push((pos..pos, format!(",\n{new_scenario}")));
cursor_position = pos + ",\n ".len();
} else {
let mut matches = cursor.matches(
&EMPTY_ARRAY_QUERY,
syntax_tree.root_node(),
content.as_bytes(),
);
if let Some(mat) = matches.next() {
if let Some(pos) = mat.captures.first().map(|m| m.node.byte_range().end - 1) {
edits.push((
MultiBufferOffset(pos)..MultiBufferOffset(pos),
format!("\n{new_scenario}\n"),
));
cursor_position = MultiBufferOffset(pos) + "\n ".len();
}
} else {
edits.push((
MultiBufferOffset(0)..MultiBufferOffset(0),
format!("[\n{}\n]", new_scenario),
));
cursor_position = MultiBufferOffset("[\n ".len());
}
}
editor.transact(window, cx, |editor, window, cx| {
editor.edit(edits, cx);
let snapshot = editor.buffer().read(cx).read(cx);
let point = cursor_position.to_point(&snapshot);
drop(snapshot);
editor.go_to_singleton_buffer_point(point, window, cx);
});
tasks_ui::insert_task_json_into_editor(editor, new_scenario, window, cx)?;
Ok(editor.save(SaveOptions::default(), project, window, cx))
}
+108 -49
View File
@@ -26,7 +26,8 @@ use workspace::{
use crate::git_panel::show_error_toast;
use crate::worktree_service::{RemoteBranchName, WorktreeCreateTarget, worktree_create_targets};
use zed_actions::{
CreateWorktree, NewWorktreeBranchTarget, OpenWorktreeInNewWindow, SwitchWorktree,
CreateWorktree, NewWorktreeBranchTarget, OpenWorktreeInNewWindow, OpenWorktreeSetupTasks,
SwitchWorktree,
};
actions!(
@@ -246,6 +247,10 @@ impl Render for WorktreePicker {
.on_mouse_down_out(cx.listener(|_, _, _, cx| {
cx.emit(DismissEvent);
}))
.on_action(cx.listener(|_, _: &OpenWorktreeSetupTasks, _, cx| {
cx.emit(DismissEvent);
cx.propagate();
}))
.on_action(cx.listener(|this, _: &DeleteWorktree, window, cx| {
this.picker.update(cx, |picker, cx| {
let ix = picker.delegate.selected_index;
@@ -1348,6 +1353,35 @@ impl PickerDelegate for WorktreePickerDelegate {
}
}
fn searchbar_trailer(
&self,
_window: &mut Window,
_cx: &mut Context<Picker<Self>>,
) -> Option<AnyElement> {
if self.show_footer {
return None;
}
let focus_handle = self.focus_handle.clone();
Some(
IconButton::new("configure-worktree-tasks", IconName::Settings)
.icon_size(IconSize::Small)
.tooltip(move |_window, cx| {
Tooltip::for_action_in(
"Automate Worktree Setup",
&OpenWorktreeSetupTasks,
&focus_handle,
cx,
)
})
.on_click(|_, window, cx| {
window.dispatch_action(OpenWorktreeSetupTasks.boxed_clone(), cx)
})
.into_any_element(),
)
}
fn render_footer(&self, _: &mut Window, cx: &mut Context<Picker<Self>>) -> Option<AnyElement> {
if !self.show_footer {
return None;
@@ -1384,9 +1418,19 @@ impl PickerDelegate for WorktreePickerDelegate {
.w_full()
.p_1p5()
.gap_0p5()
.justify_end()
.justify_between()
.border_t_1()
.border_color(cx.theme().colors().border_variant);
.border_color(cx.theme().colors().border_variant)
.child(
Button::new("configure-worktree-tasks", "Automate Setup")
.key_binding(
KeyBinding::for_action_in(&OpenWorktreeSetupTasks, &focus_handle, cx)
.map(|kb| kb.size(rems_from_px(12.))),
)
.on_click(|_, window, cx| {
window.dispatch_action(OpenWorktreeSetupTasks.boxed_clone(), cx)
}),
);
if is_creating {
Some(
@@ -1406,55 +1450,70 @@ impl PickerDelegate for WorktreePickerDelegate {
} else if is_existing_worktree {
Some(
footer
.when(is_deleting, |this| {
this.child(
Button::new("delete-worktree", "Deleting…")
.loading(true)
.disabled(true),
)
})
.when(!is_deleting && can_delete, |this| {
let focus_handle = focus_handle.clone();
this.child(
Button::new("delete-worktree", "Delete")
.key_binding(
KeyBinding::for_action_in(&DeleteWorktree, &focus_handle, cx)
.map(|kb| kb.size(rems_from_px(12.))),
.child(
h_flex()
.gap_0p5()
.when(is_deleting, |this| {
this.child(
Button::new("delete-worktree", "Deleting…")
.loading(true)
.disabled(true),
)
.on_click(|_, window, cx| {
window.dispatch_action(DeleteWorktree.boxed_clone(), cx)
}),
)
})
.when(!is_deleting && !is_current, |this| {
let focus_handle = focus_handle.clone();
this.child(
Button::new("open-in-new-window", "Open in New Window")
.key_binding(
KeyBinding::for_action_in(
&menu::SecondaryConfirm,
&focus_handle,
cx,
)
.map(|kb| kb.size(rems_from_px(12.))),
})
.when(!is_deleting && can_delete, |this| {
let focus_handle = focus_handle.clone();
this.child(
Button::new("delete-worktree", "Delete")
.key_binding(
KeyBinding::for_action_in(
&DeleteWorktree,
&focus_handle,
cx,
)
.map(|kb| kb.size(rems_from_px(12.))),
)
.on_click(|_, window, cx| {
window.dispatch_action(DeleteWorktree.boxed_clone(), cx)
}),
)
.on_click(|_, window, cx| {
window.dispatch_action(menu::SecondaryConfirm.boxed_clone(), cx)
}),
)
})
.when(!is_deleting, |this| {
this.child(
Button::new("open-worktree", "Open")
.key_binding(
KeyBinding::for_action_in(&menu::Confirm, &focus_handle, cx)
.map(|kb| kb.size(rems_from_px(12.))),
})
.when(!is_deleting && !is_current, |this| {
let focus_handle = focus_handle.clone();
this.child(
Button::new("open-in-new-window", "Open in New Window")
.key_binding(
KeyBinding::for_action_in(
&menu::SecondaryConfirm,
&focus_handle,
cx,
)
.map(|kb| kb.size(rems_from_px(12.))),
)
.on_click(|_, window, cx| {
window.dispatch_action(
menu::SecondaryConfirm.boxed_clone(),
cx,
)
}),
)
.on_click(|_, window, cx| {
window.dispatch_action(menu::Confirm.boxed_clone(), cx)
}),
)
})
})
.when(!is_deleting, |this| {
this.child(
Button::new("open-worktree", "Open")
.key_binding(
KeyBinding::for_action_in(
&menu::Confirm,
&focus_handle,
cx,
)
.map(|kb| kb.size(rems_from_px(12.))),
)
.on_click(|_, window, cx| {
window.dispatch_action(menu::Confirm.boxed_clone(), cx)
}),
)
}),
)
.into_any(),
)
} else {
+4
View File
@@ -186,6 +186,10 @@ pub fn initial_tasks_content() -> Cow<'static, str> {
asset_str::<SettingsAssets>("settings/initial_tasks.json")
}
pub fn initial_worktree_setup_tasks_content() -> Cow<'static, str> {
asset_str::<SettingsAssets>("settings/initial_worktree_setup_tasks.json")
}
pub fn initial_debug_tasks_content() -> Cow<'static, str> {
asset_str::<SettingsAssets>("settings/initial_debug_tasks.json")
}
+2
View File
@@ -24,6 +24,8 @@ picker.workspace = true
project.workspace = true
task.workspace = true
serde.workspace = true
tree-sitter.workspace = true
tree-sitter-json.workspace = true
ui.workspace = true
util.workspace = true
workspace.workspace = true
+89 -2
View File
@@ -1,16 +1,103 @@
use std::{path::Path, sync::Arc};
use std::{
path::Path,
sync::{Arc, LazyLock},
};
use anyhow::Context as _;
use collections::HashMap;
use editor::Editor;
use editor::{Editor, MultiBufferOffset, ToPoint as _};
use gpui::{App, AppContext as _, Context, Entity, Task, TaskExt, Window};
use project::{Location, TaskContexts, TaskSourceKind, Worktree};
use task::{RevealTarget, TaskContext, TaskId, TaskTemplate, TaskVariables, VariableName};
use tree_sitter::{Query, StreamingIterator as _};
use workspace::Workspace;
mod modal;
pub use modal::{Rerun, ShowAttachModal, Spawn, TaskOverrides, TasksModal};
/// Inserts `new_task` (pretty-printed JSON object text) at the end of the top-level JSON
/// array in the editor's buffer, creating the array if the buffer has none, and moves the
/// cursor to the inserted task. The edit is left unsaved so callers decide whether to persist it.
pub fn insert_task_json_into_editor(
editor: &mut Editor,
new_task: String,
window: &mut Window,
cx: &mut Context<Editor>,
) -> anyhow::Result<()> {
static LAST_ITEM_QUERY: LazyLock<Query> = LazyLock::new(|| {
Query::new(
&tree_sitter_json::LANGUAGE.into(),
"(document (array (object) @object))", // TODO: use "." anchor to only match last object
)
.expect("Failed to create LAST_ITEM_QUERY")
});
static EMPTY_ARRAY_QUERY: LazyLock<Query> = LazyLock::new(|| {
Query::new(
&tree_sitter_json::LANGUAGE.into(),
"(document (array) @array)",
)
.expect("Failed to create EMPTY_ARRAY_QUERY")
});
let content = editor.text(cx);
let mut parser = tree_sitter::Parser::new();
parser.set_language(&tree_sitter_json::LANGUAGE.into())?;
let mut cursor = tree_sitter::QueryCursor::new();
let syntax_tree = parser
.parse(&content, None)
.context("could not parse tasks file")?;
let mut matches = cursor.matches(
&LAST_ITEM_QUERY,
syntax_tree.root_node(),
content.as_bytes(),
);
let mut last_offset = None;
while let Some(mat) = matches.next() {
if let Some(pos) = mat.captures.first().map(|m| m.node.byte_range().end) {
last_offset = Some(MultiBufferOffset(pos))
}
}
let mut edits = Vec::new();
let mut cursor_position = MultiBufferOffset(0);
if let Some(pos) = last_offset {
edits.push((pos..pos, format!(",\n{new_task}")));
cursor_position = pos + ",\n ".len();
} else {
let mut matches = cursor.matches(
&EMPTY_ARRAY_QUERY,
syntax_tree.root_node(),
content.as_bytes(),
);
if let Some(mat) = matches.next() {
if let Some(pos) = mat.captures.first().map(|m| m.node.byte_range().end - 1) {
edits.push((
MultiBufferOffset(pos)..MultiBufferOffset(pos),
format!("\n{new_task}\n"),
));
cursor_position = MultiBufferOffset(pos) + "\n ".len();
}
} else {
edits.push((
MultiBufferOffset(0)..MultiBufferOffset(0),
format!("[\n{}\n]", new_task),
));
cursor_position = MultiBufferOffset("[\n ".len());
}
}
editor.transact(window, cx, |editor, window, cx| {
editor.edit(edits, cx);
let snapshot = editor.buffer().read(cx).read(cx);
let point = cursor_position.to_point(&snapshot);
drop(snapshot);
editor.go_to_singleton_buffer_point(point, window, cx);
});
Ok(())
}
pub fn init(cx: &mut App) {
cx.observe_new(
|workspace: &mut Workspace, _: Option<&mut Window>, _: &mut Context<Workspace>| {
+79 -28
View File
@@ -104,8 +104,8 @@ use workspace::{
};
use workspace::{Pane, notifications::DetachAndPromptErr};
use zed_actions::{
About, GetMerch, OpenAccountSettings, OpenBrowser, OpenDocs, OpenServerSettings,
OpenSettingsFile, OpenStatusPage, OpenZedUrl, Quit,
About, GetMerch, OpenAccountSettings, OpenBrowser, OpenDocs, OpenProjectTasks,
OpenServerSettings, OpenSettingsFile, OpenStatusPage, OpenZedUrl, Quit,
};
const DOCS_URL: &str = "https://zed.dev/docs/";
@@ -131,8 +131,6 @@ actions!(
OpenDefaultSettings,
/// Opens project-specific settings file.
OpenProjectSettingsFile,
/// Opens the project tasks configuration.
OpenProjectTasks,
/// Opens the tasks panel.
OpenTasks,
/// Opens debug tasks configuration.
@@ -1229,6 +1227,7 @@ fn register_actions(
})
.register_action(open_project_settings_file)
.register_action(open_project_tasks_file)
.register_action(open_worktree_setup_tasks_file)
.register_action(open_project_debug_tasks_file)
.register_action(
|workspace: &mut Workspace,
@@ -2392,13 +2391,15 @@ fn open_project_settings_file(
window: &mut Window,
cx: &mut Context<Workspace>,
) {
open_local_file(
if let Some(task) = open_local_file(
workspace,
local_settings_file_relative_path(),
initial_project_settings_content(),
window,
cx,
)
) {
task.detach_and_log_err(cx);
}
}
fn open_project_tasks_file(
@@ -2407,13 +2408,64 @@ fn open_project_tasks_file(
window: &mut Window,
cx: &mut Context<Workspace>,
) {
open_local_file(
if let Some(task) = open_local_file(
workspace,
local_tasks_file_relative_path(),
initial_tasks_content(),
window,
cx,
)
) {
task.detach_and_log_err(cx);
}
}
fn open_worktree_setup_tasks_file(
workspace: &mut Workspace,
_: &zed_actions::OpenWorktreeSetupTasks,
window: &mut Window,
cx: &mut Context<Workspace>,
) {
// Kept harmless on purpose: tasks with the `create_worktree` hook run automatically
// when a worktree is created, so the example must be safe to save unedited.
const WORKTREE_SETUP_TASK_EXAMPLE: &str = r#" {
// Runs automatically after Zed creates a new git worktree.
// $ZED_WORKTREE_ROOT is the new worktree's root directory, and
// $ZED_MAIN_GIT_WORKTREE is the original repository's working directory.
"label": "Set up new worktree",
"command": "echo \"Setting up $ZED_WORKTREE_ROOT — edit this command\"",
"cwd": "$ZED_WORKTREE_ROOT",
"hooks": ["create_worktree"]
}"#;
let Some(open_task) = open_local_file(
workspace,
local_tasks_file_relative_path(),
settings::initial_worktree_setup_tasks_content(),
window,
cx,
) else {
return;
};
cx.spawn_in(window, async move |_, cx| {
let editor = open_task.await?;
editor.update_in(cx, |editor, window, cx| {
// Skip insertion if the file already mentions the hook (even in a comment,
// like the seeded template's example — uncommenting it beats duplicating it).
// `create_git_worktree` is a serde alias for the same hook.
let text = editor.text(cx);
if text.contains("create_worktree") || text.contains("create_git_worktree") {
return anyhow::Ok(());
}
tasks_ui::insert_task_json_into_editor(
editor,
WORKTREE_SETUP_TASK_EXAMPLE.to_string(),
window,
cx,
)
})?
})
.detach_and_log_err(cx);
}
fn open_project_debug_tasks_file(
@@ -2422,13 +2474,15 @@ fn open_project_debug_tasks_file(
window: &mut Window,
cx: &mut Context<Workspace>,
) {
open_local_file(
if let Some(task) = open_local_file(
workspace,
local_debug_file_relative_path(),
initial_local_debug_tasks_content(),
window,
cx,
)
) {
task.detach_and_log_err(cx);
}
}
fn open_local_file(
@@ -2437,7 +2491,7 @@ fn open_local_file(
initial_contents: Cow<'static, str>,
window: &mut Window,
cx: &mut Context<Workspace>,
) {
) -> Option<gpui::Task<anyhow::Result<Entity<Editor>>>> {
let project = workspace.project().clone();
let worktree = project
.read(cx)
@@ -2445,7 +2499,7 @@ fn open_local_file(
.find_map(|tree| tree.read(cx).root_entry()?.is_dir().then_some(tree));
if let Some(worktree) = worktree {
let tree_id = worktree.read(cx).id();
cx.spawn_in(window, async move |workspace, cx| {
Some(cx.spawn_in(window, async move |workspace, cx| {
// Check if the file actually exists on disk (even if it's excluded from worktree)
let file_exists = {
let full_path = worktree.read_with(cx, |tree, _| {
@@ -2493,28 +2547,25 @@ fn open_local_file(
.downcast::<Editor>()
.context("unexpected item type: expected editor item")?;
editor
.downgrade()
.update(cx, |editor, cx| {
if let Some(buffer) = editor.buffer().read(cx).as_singleton()
&& buffer.read(cx).is_empty()
{
buffer.update(cx, |buffer, cx| {
buffer.edit([(0..0, initial_contents)], None, cx)
});
}
})
.ok();
editor.update(cx, |editor, cx| {
if let Some(buffer) = editor.buffer().read(cx).as_singleton()
&& buffer.read(cx).is_empty()
{
buffer.update(cx, |buffer, cx| {
buffer.edit([(0..0, initial_contents)], None, cx)
});
}
});
anyhow::Ok(())
})
.detach();
anyhow::Ok(editor)
}))
} else {
struct NoOpenFolders;
workspace.show_notification(NotificationId::unique::<NoOpenFolders>(), cx, |cx| {
cx.new(|cx| MessageNotification::new("This project has no folders open.", cx))
})
});
None
}
}
+2 -2
View File
@@ -262,8 +262,8 @@ pub fn app_menus(cx: &mut App) -> Vec<Menu> {
),
MenuItem::action("Start Debugger", debugger_ui::Start),
MenuItem::separator(),
MenuItem::action("Edit tasks.json...", crate::zed::OpenProjectTasks),
MenuItem::action("Edit debug.json...", zed_actions::OpenProjectDebugTasks),
MenuItem::action("Edit tasks.json…", zed_actions::OpenProjectTasks),
MenuItem::action("Edit debug.json…", zed_actions::OpenProjectDebugTasks),
MenuItem::separator(),
MenuItem::action("Continue", debugger_ui::Continue),
MenuItem::action("Step Over", debugger_ui::StepOver),
+4
View File
@@ -47,6 +47,10 @@ actions!(
/// Opens project-specific settings.
#[action(deprecated_aliases = ["zed_actions::OpenProjectSettings"])]
OpenProjectSettings,
/// Opens the project tasks configuration.
OpenProjectTasks,
/// Opens the project tasks configuration with worktree setup guidance.
OpenWorktreeSetupTasks,
/// Opens the default keymap file.
OpenDefaultKeymap,
/// Opens the user keymap file.