diff --git a/Cargo.lock b/Cargo.lock index 787d8a8cde..a4528af7ce 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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", diff --git a/assets/keymaps/default-linux.json b/assets/keymaps/default-linux.json index bb7679ab06..834568fbe5 100644 --- a/assets/keymaps/default-linux.json +++ b/assets/keymaps/default-linux.json @@ -1571,6 +1571,7 @@ "bindings": { "ctrl-shift-backspace": "worktree_picker::DeleteWorktree", "ctrl-alt-shift-backspace": "worktree_picker::ForceDeleteWorktree", + "ctrl-shift-c": "zed::OpenWorktreeSetupTasks", }, }, { diff --git a/assets/keymaps/default-macos.json b/assets/keymaps/default-macos.json index ad8d80e162..639ab247ee 100644 --- a/assets/keymaps/default-macos.json +++ b/assets/keymaps/default-macos.json @@ -1626,6 +1626,7 @@ "bindings": { "cmd-shift-backspace": "worktree_picker::DeleteWorktree", "cmd-alt-shift-backspace": "worktree_picker::ForceDeleteWorktree", + "cmd-shift-c": "zed::OpenWorktreeSetupTasks", }, }, { diff --git a/assets/keymaps/default-windows.json b/assets/keymaps/default-windows.json index 3aed4904de..7f31683801 100644 --- a/assets/keymaps/default-windows.json +++ b/assets/keymaps/default-windows.json @@ -1550,6 +1550,7 @@ "bindings": { "ctrl-shift-backspace": "worktree_picker::DeleteWorktree", "ctrl-alt-shift-backspace": "worktree_picker::ForceDeleteWorktree", + "ctrl-shift-,": "zed::OpenWorktreeSetupTasks", }, }, { diff --git a/assets/settings/initial_tasks.json b/assets/settings/initial_tasks.json index bb6c9c04ae..416383e0f7 100644 --- a/assets/settings/initial_tasks.json +++ b/assets/settings/initial_tasks.json @@ -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"] }, ] diff --git a/assets/settings/initial_worktree_setup_tasks.json b/assets/settings/initial_worktree_setup_tasks.json new file mode 100644 index 0000000000..5a3c5a4507 --- /dev/null +++ b/assets/settings/initial_worktree_setup_tasks.json @@ -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"] + // } +] diff --git a/crates/debugger_ui/Cargo.toml b/crates/debugger_ui/Cargo.toml index 8a2364abb6..0a7857152b 100644 --- a/crates/debugger_ui/Cargo.toml +++ b/crates/debugger_ui/Cargo.toml @@ -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 } diff --git a/crates/debugger_ui/src/debugger_panel.rs b/crates/debugger_ui/src/debugger_panel.rs index 8b2af4afbe..37abc04694 100644 --- a/crates/debugger_ui/src/debugger_panel.rs +++ b/crates/debugger_ui/src/debugger_panel.rs @@ -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, ) -> Result>> { - static LAST_ITEM_QUERY: LazyLock = 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 = 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)) } diff --git a/crates/git_ui/src/worktree_picker.rs b/crates/git_ui/src/worktree_picker.rs index df0e899275..9d23880fbc 100644 --- a/crates/git_ui/src/worktree_picker.rs +++ b/crates/git_ui/src/worktree_picker.rs @@ -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>, + ) -> Option { + 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>) -> Option { 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 { diff --git a/crates/settings/src/settings.rs b/crates/settings/src/settings.rs index cec4aac903..538e2aa296 100644 --- a/crates/settings/src/settings.rs +++ b/crates/settings/src/settings.rs @@ -186,6 +186,10 @@ pub fn initial_tasks_content() -> Cow<'static, str> { asset_str::("settings/initial_tasks.json") } +pub fn initial_worktree_setup_tasks_content() -> Cow<'static, str> { + asset_str::("settings/initial_worktree_setup_tasks.json") +} + pub fn initial_debug_tasks_content() -> Cow<'static, str> { asset_str::("settings/initial_debug_tasks.json") } diff --git a/crates/tasks_ui/Cargo.toml b/crates/tasks_ui/Cargo.toml index 2f75a0b57c..c3884252b4 100644 --- a/crates/tasks_ui/Cargo.toml +++ b/crates/tasks_ui/Cargo.toml @@ -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 diff --git a/crates/tasks_ui/src/tasks_ui.rs b/crates/tasks_ui/src/tasks_ui.rs index 072ad29c1b..41b24dee32 100644 --- a/crates/tasks_ui/src/tasks_ui.rs +++ b/crates/tasks_ui/src/tasks_ui.rs @@ -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, +) -> anyhow::Result<()> { + static LAST_ITEM_QUERY: LazyLock = 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 = 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| { diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index 79abe8949e..302215ea53 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -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, ) { - 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, ) { - 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, +) { + // 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, ) { - 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, -) { +) -> Option>>> { 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::() .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::(), cx, |cx| { cx.new(|cx| MessageNotification::new("This project has no folders open.", cx)) - }) + }); + None } } diff --git a/crates/zed/src/zed/app_menus.rs b/crates/zed/src/zed/app_menus.rs index 6e73536b1d..8531ebb7f6 100644 --- a/crates/zed/src/zed/app_menus.rs +++ b/crates/zed/src/zed/app_menus.rs @@ -262,8 +262,8 @@ pub fn app_menus(cx: &mut App) -> Vec { ), 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), diff --git a/crates/zed_actions/src/lib.rs b/crates/zed_actions/src/lib.rs index 12b34def15..1c5d70a0fd 100644 --- a/crates/zed_actions/src/lib.rs +++ b/crates/zed_actions/src/lib.rs @@ -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.