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:
1 parent
a77838bb98
commit
e1ee74dcbf
15 files changed
+323
-158
No files matched your search
Generated
+2
-2
@@ -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",
|
||||
|
||||
@@ -1571,6 +1571,7 @@
|
||||
"bindings": {
|
||||
"ctrl-shift-backspace": "worktree_picker::DeleteWorktree",
|
||||
"ctrl-alt-shift-backspace": "worktree_picker::ForceDeleteWorktree",
|
||||
"ctrl-shift-c": "zed::OpenWorktreeSetupTasks",
|
||||
},
|
||||
},
|
||||
{
|
||||
|
||||
@@ -1626,6 +1626,7 @@
|
||||
"bindings": {
|
||||
"cmd-shift-backspace": "worktree_picker::DeleteWorktree",
|
||||
"cmd-alt-shift-backspace": "worktree_picker::ForceDeleteWorktree",
|
||||
"cmd-shift-c": "zed::OpenWorktreeSetupTasks",
|
||||
},
|
||||
},
|
||||
{
|
||||
|
||||
@@ -1550,6 +1550,7 @@
|
||||
"bindings": {
|
||||
"ctrl-shift-backspace": "worktree_picker::DeleteWorktree",
|
||||
"ctrl-alt-shift-backspace": "worktree_picker::ForceDeleteWorktree",
|
||||
"ctrl-shift-,": "zed::OpenWorktreeSetupTasks",
|
||||
},
|
||||
},
|
||||
{
|
||||
|
||||
@@ -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"]
|
||||
// }
|
||||
]
|
||||
@@ -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 }
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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.
|
||||
|
||||
Reference in new issue
Block a user