diff --git a/.zed/debug.json b/.zed/debug.json index 49b8f1a7a6..6f4e936c80 100644 --- a/.zed/debug.json +++ b/.zed/debug.json @@ -5,9 +5,7 @@ "build": { "label": "Build Zed", "command": "cargo", - "args": [ - "build" - ] + "args": ["build"] } }, { @@ -16,9 +14,7 @@ "build": { "label": "Build Zed", "command": "cargo", - "args": [ - "build" - ] + "args": ["build"] } - }, + } ] diff --git a/Cargo.lock b/Cargo.lock index 921eea00f8..57b97cb853 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4324,6 +4324,7 @@ dependencies = [ "futures 0.3.31", "fuzzy", "gpui", + "indoc", "itertools 0.14.0", "language", "log", @@ -4344,6 +4345,7 @@ dependencies = [ "tasks_ui", "telemetry", "terminal_view", + "text", "theme", "tree-sitter", "tree-sitter-go", diff --git a/crates/debugger_ui/Cargo.toml b/crates/debugger_ui/Cargo.toml index ba71e50a08..fc543a47f9 100644 --- a/crates/debugger_ui/Cargo.toml +++ b/crates/debugger_ui/Cargo.toml @@ -40,6 +40,7 @@ file_icons.workspace = true futures.workspace = true fuzzy.workspace = true gpui.workspace = true +indoc.workspace = true itertools.workspace = true language.workspace = true log.workspace = true @@ -60,6 +61,7 @@ task.workspace = true tasks_ui.workspace = true telemetry.workspace = true terminal_view.workspace = true +text.workspace = true theme.workspace = true tree-sitter.workspace = true tree-sitter-json.workspace = true diff --git a/crates/debugger_ui/src/attach_modal.rs b/crates/debugger_ui/src/attach_modal.rs index aa4ca9e868..662a98c820 100644 --- a/crates/debugger_ui/src/attach_modal.rs +++ b/crates/debugger_ui/src/attach_modal.rs @@ -206,7 +206,7 @@ impl PickerDelegate for AttachModalDelegate { }) } - fn confirm(&mut self, _: bool, window: &mut Window, cx: &mut Context>) { + fn confirm(&mut self, secondary: bool, window: &mut Window, cx: &mut Context>) { let candidate = self .matches .get(self.selected_index()) @@ -229,30 +229,44 @@ impl PickerDelegate for AttachModalDelegate { } } + let workspace = self.workspace.clone(); + let Some(panel) = workspace + .update(cx, |workspace, cx| workspace.panel::(cx)) + .ok() + .flatten() + else { + return; + }; + + if secondary { + // let Some(id) = worktree_id else { return }; + // cx.spawn_in(window, async move |_, cx| { + // panel + // .update_in(cx, |debug_panel, window, cx| { + // debug_panel.save_scenario(&debug_scenario, id, window, cx) + // })? + // .await?; + // anyhow::Ok(()) + // }) + // .detach_and_log_err(cx); + } let Some(adapter) = cx.read_global::(|registry, _| { registry.adapter(&self.definition.adapter) }) else { return; }; - let workspace = self.workspace.clone(); let definition = self.definition.clone(); cx.spawn_in(window, async move |this, cx| { let Ok(scenario) = adapter.config_from_zed_format(definition).await else { return; }; - let panel = workspace - .update(cx, |workspace, cx| workspace.panel::(cx)) - .ok() - .flatten(); - if let Some(panel) = panel { - panel - .update_in(cx, |panel, window, cx| { - panel.start_session(scenario, Default::default(), None, None, window, cx); - }) - .ok(); - } + panel + .update_in(cx, |panel, window, cx| { + panel.start_session(scenario, Default::default(), None, None, window, cx); + }) + .ok(); this.update(cx, |_, cx| { cx.emit(DismissEvent); }) diff --git a/crates/debugger_ui/src/debugger_panel.rs b/crates/debugger_ui/src/debugger_panel.rs index 37df989c0b..988f6f4019 100644 --- a/crates/debugger_ui/src/debugger_panel.rs +++ b/crates/debugger_ui/src/debugger_panel.rs @@ -16,16 +16,18 @@ use dap::{ client::SessionId, debugger_settings::DebuggerSettings, }; use dap::{DapRegistry, StartDebuggingRequestArguments}; +use editor::Editor; use gpui::{ Action, App, AsyncWindowContext, ClipboardItem, Context, DismissEvent, Entity, EntityId, EventEmitter, FocusHandle, Focusable, MouseButton, MouseDownEvent, Point, Subscription, Task, WeakEntity, anchored, deferred, }; +use text::ToPoint as _; use itertools::Itertools as _; use language::Buffer; use project::debugger::session::{Session, SessionStateEvent}; -use project::{DebugScenarioContext, Fs, ProjectPath, WorktreeId}; +use project::{DebugScenarioContext, Fs, ProjectPath, TaskSourceKind, WorktreeId}; use project::{Project, debugger::session::ThreadStatus}; use rpc::proto::{self}; use settings::Settings; @@ -35,8 +37,9 @@ use tree_sitter::{Query, StreamingIterator as _}; use ui::{ContextMenu, Divider, PopoverMenuHandle, Tooltip, prelude::*}; use util::{ResultExt, maybe}; use workspace::SplitDirection; +use workspace::item::SaveOptions; use workspace::{ - Pane, Workspace, + Item, Pane, Workspace, dock::{DockPosition, Panel, PanelEvent}, }; use zed_actions::ToggleFocus; @@ -988,13 +991,90 @@ impl DebugPanel { cx.notify(); } - pub(crate) fn save_scenario( + pub(crate) fn go_to_scenario_definition( &self, - scenario: &DebugScenario, + kind: TaskSourceKind, + scenario: DebugScenario, worktree_id: WorktreeId, window: &mut Window, - cx: &mut App, - ) -> Task> { + cx: &mut Context, + ) -> Task> { + let Some(workspace) = self.workspace.upgrade() else { + return Task::ready(Ok(())); + }; + let project_path = match kind { + TaskSourceKind::AbsPath { abs_path, .. } => { + let Some(project_path) = workspace + .read(cx) + .project() + .read(cx) + .project_path_for_absolute_path(&abs_path, cx) + else { + return Task::ready(Err(anyhow!("no abs path"))); + }; + + project_path + } + TaskSourceKind::Worktree { + id, + directory_in_worktree: dir, + .. + } => { + let relative_path = if dir.ends_with(".vscode") { + dir.join("launch.json") + } else { + dir.join("debug.json") + }; + ProjectPath { + worktree_id: id, + path: Arc::from(relative_path), + } + } + _ => return self.save_scenario(scenario, worktree_id, window, cx), + }; + + let editor = workspace.update(cx, |workspace, cx| { + workspace.open_path(project_path, None, true, window, cx) + }); + cx.spawn_in(window, async move |_, cx| { + let editor = editor.await?; + let editor = cx + .update(|_, cx| editor.act_as::(cx))? + .context("expected editor")?; + + // unfortunately debug tasks don't have an easy way to globally + // identify them. to jump to the one that you just created or an + // old one that you're choosing to edit we use a heuristic of searching for a line with `label: ` from the end rather than the start so we bias towards more renctly + editor.update_in(cx, |editor, window, cx| { + let row = editor.text(cx).lines().enumerate().find_map(|(row, text)| { + if text.contains(scenario.label.as_ref()) && text.contains("\"label\": ") { + Some(row) + } else { + None + } + }); + if let Some(row) = row { + editor.go_to_singleton_buffer_point( + text::Point::new(row as u32, 4), + window, + cx, + ); + } + })?; + + Ok(()) + }) + } + + pub(crate) fn save_scenario( + &self, + scenario: DebugScenario, + worktree_id: WorktreeId, + window: &mut Window, + cx: &mut Context, + ) -> Task> { + let this = cx.weak_entity(); + let project = self.project.clone(); self.workspace .update(cx, |workspace, cx| { let Some(mut path) = workspace.absolute_path_of_worktree(worktree_id, cx) else { @@ -1027,47 +1107,7 @@ impl DebugPanel { ) .await?; } - - let mut content = fs.load(path).await?; - let new_scenario = serde_json_lenient::to_string_pretty(&serialized_scenario)? - .lines() - .map(|l| format!(" {l}")) - .join("\n"); - - static ARRAY_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 ARRAY_QUERY") - }); - - let mut parser = tree_sitter::Parser::new(); - parser - .set_language(&tree_sitter_json::LANGUAGE.into()) - .unwrap(); - let mut cursor = tree_sitter::QueryCursor::new(); - let syntax_tree = parser.parse(&content, None).unwrap(); - let mut matches = - cursor.matches(&ARRAY_QUERY, syntax_tree.root_node(), content.as_bytes()); - - // we don't have `.last()` since it's a lending iterator, so loop over - // the whole thing to find the last one - 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(pos) - } - } - - if let Some(pos) = last_offset { - content.insert_str(pos, &new_scenario); - content.insert_str(pos, ",\n"); - } - - fs.write(path, content.as_bytes()).await?; - - workspace.update(cx, |workspace, cx| { + let project_path = workspace.update(cx, |workspace, cx| { workspace .project() .read(cx) @@ -1075,12 +1115,113 @@ impl DebugPanel { .context( "Couldn't get project path for .zed/debug.json in active worktree", ) - })? + })??; + + let editor = this + .update_in(cx, |this, window, cx| { + this.workspace.update(cx, |workspace, cx| { + workspace.open_path(project_path, None, true, window, cx) + }) + })?? + .await?; + let editor = cx + .update(|_, cx| editor.act_as::(cx))? + .context("expected editor")?; + + let new_scenario = serde_json_lenient::to_string_pretty(&serialized_scenario)? + .lines() + .map(|l| format!(" {l}")) + .join("\n"); + + editor + .update_in(cx, |editor, window, cx| { + Self::insert_task_into_editor(editor, new_scenario, project, window, cx) + })?? + .await }) }) .unwrap_or_else(|err| Task::ready(Err(err))) } + pub fn insert_task_into_editor( + editor: &mut Editor, + new_scenario: String, + project: Entity, + 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(pos) + } + } + let mut edits = Vec::new(); + let mut cursor_position = 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((pos..pos, format!("\n{new_scenario}\n"))); + cursor_position = pos + "\n ".len(); + } + } else { + edits.push((0..0, format!("[\n{}\n]", new_scenario))); + cursor_position = "[\n ".len(); + } + } + editor.transact(window, cx, |editor, window, cx| { + editor.edit(edits, cx); + let snapshot = editor + .buffer() + .read(cx) + .as_singleton() + .unwrap() + .read(cx) + .snapshot(); + let point = cursor_position.to_point(&snapshot); + editor.go_to_singleton_buffer_point(point, window, cx); + }); + Ok(editor.save(SaveOptions::default(), project, window, cx)) + } + pub(crate) fn toggle_thread_picker(&mut self, window: &mut Window, cx: &mut Context) { self.thread_picker_menu_handle.toggle(window, cx); } diff --git a/crates/debugger_ui/src/new_process_modal.rs b/crates/debugger_ui/src/new_process_modal.rs index e857e33677..6d7fa244a2 100644 --- a/crates/debugger_ui/src/new_process_modal.rs +++ b/crates/debugger_ui/src/new_process_modal.rs @@ -1,12 +1,10 @@ -use anyhow::bail; +use anyhow::{Context as _, bail}; use collections::{FxHashMap, HashMap}; use language::LanguageRegistry; -use paths::local_debug_file_relative_path; use std::{ borrow::Cow, path::{Path, PathBuf}, sync::Arc, - time::Duration, usize, }; use tasks_ui::{TaskOverrides, TasksModal}; @@ -18,35 +16,27 @@ use editor::{Editor, EditorElement, EditorStyle}; use fuzzy::{StringMatch, StringMatchCandidate}; use gpui::{ Action, App, AppContext, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, - HighlightStyle, InteractiveText, KeyContext, PromptButton, PromptLevel, Render, StyledText, - Subscription, Task, TextStyle, UnderlineStyle, WeakEntity, + KeyContext, Render, Subscription, Task, TextStyle, WeakEntity, }; use itertools::Itertools as _; use picker::{Picker, PickerDelegate, highlighted_match_with_paths::HighlightedMatch}; -use project::{ - DebugScenarioContext, ProjectPath, TaskContexts, TaskSourceKind, task_store::TaskStore, -}; -use settings::{Settings, initial_local_debug_tasks_content}; +use project::{DebugScenarioContext, TaskContexts, TaskSourceKind, task_store::TaskStore}; +use settings::Settings; use task::{DebugScenario, RevealTarget, ZedDebugConfig}; use theme::ThemeSettings; use ui::{ - ActiveTheme, CheckboxWithLabel, Clickable, Context, ContextMenu, Disableable, DropdownMenu, - FluentBuilder, IconWithIndicator, Indicator, IntoElement, KeyBinding, ListItem, - ListItemSpacing, ParentElement, StyledExt, ToggleButton, ToggleState, Toggleable, Tooltip, - Window, div, prelude::*, px, relative, rems, + ActiveTheme, Button, ButtonCommon, ButtonSize, CheckboxWithLabel, Clickable, Color, Context, + ContextMenu, Disableable, DropdownMenu, FluentBuilder, Icon, IconName, IconSize, + IconWithIndicator, Indicator, InteractiveElement, IntoElement, KeyBinding, Label, + LabelCommon as _, LabelSize, ListItem, ListItemSpacing, ParentElement, RenderOnce, + SharedString, Styled, StyledExt, ToggleButton, ToggleState, Toggleable, Tooltip, Window, div, + h_flex, relative, rems, v_flex, }; use util::ResultExt; -use workspace::{ModalView, Workspace, pane}; +use workspace::{ModalView, Workspace, notifications::DetachAndPromptErr, pane}; use crate::{attach_modal::AttachModal, debugger_panel::DebugPanel}; -#[allow(unused)] -enum SaveScenarioState { - Saving, - Saved((ProjectPath, SharedString)), - Failed(SharedString), -} - pub(super) struct NewProcessModal { workspace: WeakEntity, debug_panel: WeakEntity, @@ -56,7 +46,6 @@ pub(super) struct NewProcessModal { configure_mode: Entity, task_mode: TaskMode, debugger: Option, - save_scenario_state: Option, _subscriptions: [Subscription; 3], } @@ -268,7 +257,6 @@ impl NewProcessModal { mode, debug_panel: debug_panel.downgrade(), workspace: workspace_handle, - save_scenario_state: None, _subscriptions, } }); @@ -420,63 +408,29 @@ impl NewProcessModal { self.debug_picker.read(cx).delegate.task_contexts.clone() } - fn save_debug_scenario(&mut self, window: &mut Window, cx: &mut Context) { - let task_contents = self.task_contexts(cx); + pub fn save_debug_scenario(&mut self, window: &mut Window, cx: &mut Context) { + let task_contexts = self.task_contexts(cx); let Some(adapter) = self.debugger.as_ref() else { return; }; let scenario = self.debug_scenario(&adapter, cx); - - self.save_scenario_state = Some(SaveScenarioState::Saving); - cx.spawn_in(window, async move |this, cx| { - let Some((scenario, worktree_id)) = scenario - .await - .zip(task_contents.and_then(|tcx| tcx.worktree())) - else { - this.update(cx, |this, _| { - this.save_scenario_state = Some(SaveScenarioState::Failed( - "Couldn't get scenario or task contents".into(), - )) + let scenario = scenario.await.context("no scenario to save")?; + let worktree_id = task_contexts + .context("no task contexts")? + .worktree() + .context("no active worktree")?; + this.update_in(cx, |this, window, cx| { + this.debug_panel.update(cx, |panel, cx| { + panel.save_scenario(scenario, worktree_id, window, cx) }) - .ok(); - return; - }; - - let Some(save_scenario) = this - .update_in(cx, |this, window, cx| { - this.debug_panel - .update(cx, |panel, cx| { - panel.save_scenario(&scenario, worktree_id, window, cx) - }) - .ok() - }) - .ok() - .flatten() - else { - return; - }; - let res = save_scenario.await; - - this.update(cx, |this, _| match res { - Ok(saved_file) => { - this.save_scenario_state = Some(SaveScenarioState::Saved(( - saved_file, - scenario.label.clone(), - ))) - } - Err(error) => { - this.save_scenario_state = - Some(SaveScenarioState::Failed(error.to_string().into())) - } + })?? + .await?; + this.update_in(cx, |_, _, cx| { + cx.emit(DismissEvent); }) - .ok(); - - cx.background_executor().timer(Duration::from_secs(3)).await; - this.update(cx, |this, _| this.save_scenario_state.take()) - .ok(); }) - .detach(); + .detach_and_prompt_err("Failed to edit debug.json", window, cx, |_, _, _| None); } fn adapter_drop_down_menu( @@ -544,70 +498,6 @@ impl NewProcessModal { }), ) } - - fn open_debug_json(&self, window: &mut Window, cx: &mut Context) { - let this = cx.entity(); - window - .spawn(cx, async move |cx| { - let worktree_id = this.update(cx, |this, cx| { - let tcx = this.task_contexts(cx); - tcx?.worktree() - })?; - - let Some(worktree_id) = worktree_id else { - let _ = cx.prompt( - PromptLevel::Critical, - "Cannot open debug.json", - Some("You must have at least one project open"), - &[PromptButton::ok("Ok")], - ); - return Ok(()); - }; - - let editor = this - .update_in(cx, |this, window, cx| { - this.workspace.update(cx, |workspace, cx| { - workspace.open_path( - ProjectPath { - worktree_id, - path: local_debug_file_relative_path().into(), - }, - None, - true, - window, - cx, - ) - }) - })?? - .await?; - - cx.update(|_window, cx| { - if let Some(editor) = editor.act_as::(cx) { - editor.update(cx, |editor, cx| { - editor.buffer().update(cx, |buffer, cx| { - if let Some(singleton) = buffer.as_singleton() { - singleton.update(cx, |buffer, cx| { - if buffer.is_empty() { - buffer.edit( - [(0..0, initial_local_debug_tasks_content())], - None, - cx, - ); - } - }) - } - }) - }); - } - }) - .ok(); - - this.update(cx, |_, cx| cx.emit(DismissEvent)).ok(); - - anyhow::Ok(()) - }) - .detach(); - } } static SELECT_DEBUGGER_LABEL: SharedString = SharedString::new_static("Select Debugger"); @@ -812,39 +702,21 @@ impl Render for NewProcessModal { NewProcessMode::Launch => el.child( container .child( - h_flex() - .text_ui_sm(cx) - .text_color(Color::Muted.color(cx)) - .child( - InteractiveText::new( - "open-debug-json", - StyledText::new( - "Open .zed/debug.json for advanced configuration.", - ) - .with_highlights([( - 5..20, - HighlightStyle { - underline: Some(UnderlineStyle { - thickness: px(1.0), - color: None, - wavy: false, - }), - ..Default::default() - }, - )]), - ) - .on_click( - vec![5..20], - { - let this = cx.entity(); - move |_, window, cx| { - this.update(cx, |this, cx| { - this.open_debug_json(window, cx); - }) - } - }, + h_flex().child( + Button::new("edit-custom-debug", "Edit in debug.json") + .on_click(cx.listener(|this, _, window, cx| { + this.save_debug_scenario(window, cx); + })) + .disabled( + self.debugger.is_none() + || self + .configure_mode + .read(cx) + .program + .read(cx) + .is_empty(cx), ), - ), + ), ) .child( Button::new("debugger-spawn", "Start") @@ -862,29 +734,48 @@ impl Render for NewProcessModal { ), ), ), - NewProcessMode::Attach => el.child( + NewProcessMode::Attach => el.child({ + let disabled = self.debugger.is_none() + || self + .attach_mode + .read(cx) + .attach_picker + .read(cx) + .picker + .read(cx) + .delegate + .match_count() + == 0; + let secondary_action = menu::SecondaryConfirm.boxed_clone(); container - .child(div().child(self.adapter_drop_down_menu(window, cx))) + .child(div().children( + KeyBinding::for_action(&*secondary_action, window, cx).map( + |keybind| { + Button::new("edit-attach-task", "Edit in debug.json") + .label_size(LabelSize::Small) + .key_binding(keybind) + .on_click(move |_, window, cx| { + window.dispatch_action( + secondary_action.boxed_clone(), + cx, + ) + }) + .disabled(disabled) + }, + ), + )) .child( - Button::new("debugger-spawn", "Start") - .on_click(cx.listener(|this, _, window, cx| { - this.start_new_session(window, cx) - })) - .disabled( - self.debugger.is_none() - || self - .attach_mode - .read(cx) - .attach_picker - .read(cx) - .picker - .read(cx) - .delegate - .match_count() - == 0, + h_flex() + .child(div().child(self.adapter_drop_down_menu(window, cx))) + .child( + Button::new("debugger-spawn", "Start") + .on_click(cx.listener(|this, _, window, cx| { + this.start_new_session(window, cx) + })) + .disabled(disabled), ), - ), - ), + ) + }), NewProcessMode::Debug => el, NewProcessMode::Task => el, } @@ -1048,25 +939,6 @@ impl ConfigureMode { ) .checkbox_position(ui::IconPosition::End), ) - .child( - CheckboxWithLabel::new( - "debugger-save-to-debug-json", - Label::new("Save to debug.json") - .size(LabelSize::Small) - .color(Color::Muted), - self.save_to_debug_json, - { - let this = cx.weak_entity(); - move |state, _, cx| { - this.update(cx, |this, _| { - this.save_to_debug_json = *state; - }) - .ok(); - } - }, - ) - .checkbox_position(ui::IconPosition::End), - ) } } @@ -1329,12 +1201,7 @@ impl PickerDelegate for DebugDelegate { } } - fn confirm_input( - &mut self, - _secondary: bool, - window: &mut Window, - cx: &mut Context>, - ) { + fn confirm_input(&mut self, _: bool, window: &mut Window, cx: &mut Context>) { let text = self.prompt.clone(); let (task_context, worktree_id) = self .task_contexts @@ -1364,7 +1231,7 @@ impl PickerDelegate for DebugDelegate { let args = args.collect::>(); let task = task::TaskTemplate { - label: "one-off".to_owned(), + label: "one-off".to_owned(), // TODO: rename using command as label env, command: program, args, @@ -1405,7 +1272,11 @@ impl PickerDelegate for DebugDelegate { .background_spawn(async move { for locator in locators { if let Some(scenario) = - locator.1.create_scenario(&task, "one-off", &adapter).await + // TODO: use a more informative label than "one-off" + locator + .1 + .create_scenario(&task, &task.label, &adapter) + .await { return Some(scenario); } @@ -1439,13 +1310,18 @@ impl PickerDelegate for DebugDelegate { .detach(); } - fn confirm(&mut self, _: bool, window: &mut Window, cx: &mut Context>) { + fn confirm( + &mut self, + secondary: bool, + window: &mut Window, + cx: &mut Context>, + ) { let debug_scenario = self .matches .get(self.selected_index()) .and_then(|match_candidate| self.candidates.get(match_candidate.candidate_id).cloned()); - let Some((_, debug_scenario, context)) = debug_scenario else { + let Some((kind, debug_scenario, context)) = debug_scenario else { return; }; @@ -1463,24 +1339,38 @@ impl PickerDelegate for DebugDelegate { }); let DebugScenarioContext { task_context, - active_buffer, + active_buffer: _, worktree_id, } = context; - let active_buffer = active_buffer.and_then(|buffer| buffer.upgrade()); - send_telemetry(&debug_scenario, TelemetrySpawnLocation::ScenarioList, cx); - self.debug_panel - .update(cx, |panel, cx| { - panel.start_session( - debug_scenario, - task_context, - active_buffer, - worktree_id, - window, - cx, - ); + if secondary { + let Some(kind) = kind else { return }; + let Some(id) = worktree_id else { return }; + let debug_panel = self.debug_panel.clone(); + cx.spawn_in(window, async move |_, cx| { + debug_panel + .update_in(cx, |debug_panel, window, cx| { + debug_panel.go_to_scenario_definition(kind, debug_scenario, id, window, cx) + })? + .await?; + anyhow::Ok(()) }) - .ok(); + .detach(); + } else { + send_telemetry(&debug_scenario, TelemetrySpawnLocation::ScenarioList, cx); + self.debug_panel + .update(cx, |panel, cx| { + panel.start_session( + debug_scenario, + task_context, + None, + worktree_id, + window, + cx, + ); + }) + .ok(); + } cx.emit(DismissEvent); } @@ -1498,19 +1388,23 @@ impl PickerDelegate for DebugDelegate { let footer = h_flex() .w_full() .p_1p5() - .justify_end() + .justify_between() .border_t_1() .border_color(cx.theme().colors().border_variant) - // .child( - // // TODO: add button to open selected task in debug.json - // h_flex().into_any_element(), - // ) + .children({ + let action = menu::SecondaryConfirm.boxed_clone(); + KeyBinding::for_action(&*action, window, cx).map(|keybind| { + Button::new("edit-debug-task", "Edit in debug.json") + .label_size(LabelSize::Small) + .key_binding(keybind) + .on_click(move |_, window, cx| { + window.dispatch_action(action.boxed_clone(), cx) + }) + }) + }) .map(|this| { if (current_modifiers.alt || self.matches.is_empty()) && !self.prompt.is_empty() { - let action = picker::ConfirmInput { - secondary: current_modifiers.secondary(), - } - .boxed_clone(); + let action = picker::ConfirmInput { secondary: false }.boxed_clone(); this.children(KeyBinding::for_action(&*action, window, cx).map(|keybind| { Button::new("launch-custom", "Launch Custom") .key_binding(keybind) @@ -1607,3 +1501,35 @@ pub(crate) fn resolve_path(path: &mut String) { ); }; } + +#[cfg(test)] +impl NewProcessModal { + pub(crate) fn set_configure( + &mut self, + program: impl AsRef, + cwd: impl AsRef, + stop_on_entry: bool, + window: &mut Window, + cx: &mut Context, + ) { + self.mode = NewProcessMode::Launch; + self.debugger = Some(dap::adapters::DebugAdapterName("fake-adapter".into())); + + self.configure_mode.update(cx, |configure, cx| { + configure.program.update(cx, |editor, cx| { + editor.clear(window, cx); + editor.set_text(program.as_ref(), window, cx); + }); + + configure.cwd.update(cx, |editor, cx| { + editor.clear(window, cx); + editor.set_text(cwd.as_ref(), window, cx); + }); + + configure.stop_on_entry = match stop_on_entry { + true => ToggleState::Selected, + _ => ToggleState::Unselected, + } + }) + } +} diff --git a/crates/debugger_ui/src/tests/new_process_modal.rs b/crates/debugger_ui/src/tests/new_process_modal.rs index 81c5f7b598..a4616eaa3b 100644 --- a/crates/debugger_ui/src/tests/new_process_modal.rs +++ b/crates/debugger_ui/src/tests/new_process_modal.rs @@ -1,13 +1,15 @@ use dap::DapRegistry; +use editor::Editor; use gpui::{BackgroundExecutor, TestAppContext, VisualTestContext}; -use project::{FakeFs, Project}; +use project::{FakeFs, Fs as _, Project}; use serde_json::json; use std::sync::Arc; use std::sync::atomic::{AtomicBool, Ordering}; use task::{DebugRequest, DebugScenario, LaunchRequest, TaskContext, VariableName, ZedDebugConfig}; +use text::Point; use util::path; -// use crate::new_process_modal::NewProcessMode; +use crate::NewProcessMode; use crate::tests::{init_test, init_test_workspace}; #[gpui::test] @@ -159,111 +161,127 @@ async fn test_debug_session_substitutes_variables_and_relativizes_paths( } } -// #[gpui::test] -// async fn test_save_debug_scenario_to_file(executor: BackgroundExecutor, cx: &mut TestAppContext) { -// init_test(cx); +#[gpui::test] +async fn test_save_debug_scenario_to_file(executor: BackgroundExecutor, cx: &mut TestAppContext) { + init_test(cx); -// let fs = FakeFs::new(executor.clone()); -// fs.insert_tree( -// path!("/project"), -// json!({ -// "main.rs": "fn main() {}" -// }), -// ) -// .await; + let fs = FakeFs::new(executor.clone()); + fs.insert_tree( + path!("/project"), + json!({ + "main.rs": "fn main() {}" + }), + ) + .await; -// let project = Project::test(fs.clone(), [path!("/project").as_ref()], cx).await; -// let workspace = init_test_workspace(&project, cx).await; -// let cx = &mut VisualTestContext::from_window(*workspace, cx); + let project = Project::test(fs.clone(), [path!("/project").as_ref()], cx).await; + let workspace = init_test_workspace(&project, cx).await; + let cx = &mut VisualTestContext::from_window(*workspace, cx); -// workspace -// .update(cx, |workspace, window, cx| { -// crate::new_process_modal::NewProcessModal::show( -// workspace, -// window, -// NewProcessMode::Debug, -// None, -// cx, -// ); -// }) -// .unwrap(); + workspace + .update(cx, |workspace, window, cx| { + crate::new_process_modal::NewProcessModal::show( + workspace, + window, + NewProcessMode::Debug, + None, + cx, + ); + }) + .unwrap(); -// cx.run_until_parked(); + cx.run_until_parked(); -// let modal = workspace -// .update(cx, |workspace, _, cx| { -// workspace.active_modal::(cx) -// }) -// .unwrap() -// .expect("Modal should be active"); + let modal = workspace + .update(cx, |workspace, _, cx| { + workspace.active_modal::(cx) + }) + .unwrap() + .expect("Modal should be active"); -// modal.update_in(cx, |modal, window, cx| { -// modal.set_configure("/project/main", "/project", false, window, cx); -// modal.save_scenario(window, cx); -// }); + modal.update_in(cx, |modal, window, cx| { + modal.set_configure("/project/main", "/project", false, window, cx); + modal.save_debug_scenario(window, cx); + }); -// cx.executor().run_until_parked(); + cx.executor().run_until_parked(); -// let debug_json_content = fs -// .load(path!("/project/.zed/debug.json").as_ref()) -// .await -// .expect("debug.json should exist"); + let editor = workspace + .update(cx, |workspace, _window, cx| { + workspace.active_item_as::(cx).unwrap() + }) + .unwrap(); -// let expected_content = vec![ -// "[", -// " {", -// r#" "adapter": "fake-adapter","#, -// r#" "label": "main (fake-adapter)","#, -// r#" "request": "launch","#, -// r#" "program": "/project/main","#, -// r#" "cwd": "/project","#, -// r#" "args": [],"#, -// r#" "env": {}"#, -// " }", -// "]", -// ]; + let debug_json_content = fs + .load(path!("/project/.zed/debug.json").as_ref()) + .await + .expect("debug.json should exist") + .lines() + .filter(|line| !line.starts_with("//")) + .collect::>() + .join("\n"); -// let actual_lines: Vec<&str> = debug_json_content.lines().collect(); -// pretty_assertions::assert_eq!(expected_content, actual_lines); + let expected_content = indoc::indoc! {r#" + [ + { + "adapter": "fake-adapter", + "label": "main (fake-adapter)", + "request": "launch", + "program": "/project/main", + "cwd": "/project", + "args": [], + "env": {} + } + ]"#}; -// modal.update_in(cx, |modal, window, cx| { -// modal.set_configure("/project/other", "/project", true, window, cx); -// modal.save_scenario(window, cx); -// }); + pretty_assertions::assert_eq!(expected_content, debug_json_content); -// cx.executor().run_until_parked(); + editor.update(cx, |editor, cx| { + assert_eq!( + editor.selections.newest::(cx).head(), + Point::new(5, 2) + ) + }); -// let debug_json_content = fs -// .load(path!("/project/.zed/debug.json").as_ref()) -// .await -// .expect("debug.json should exist after second save"); + modal.update_in(cx, |modal, window, cx| { + modal.set_configure("/project/other", "/project", true, window, cx); + modal.save_debug_scenario(window, cx); + }); -// let expected_content = vec![ -// "[", -// " {", -// r#" "adapter": "fake-adapter","#, -// r#" "label": "main (fake-adapter)","#, -// r#" "request": "launch","#, -// r#" "program": "/project/main","#, -// r#" "cwd": "/project","#, -// r#" "args": [],"#, -// r#" "env": {}"#, -// " },", -// " {", -// r#" "adapter": "fake-adapter","#, -// r#" "label": "other (fake-adapter)","#, -// r#" "request": "launch","#, -// r#" "program": "/project/other","#, -// r#" "cwd": "/project","#, -// r#" "args": [],"#, -// r#" "env": {}"#, -// " }", -// "]", -// ]; + cx.executor().run_until_parked(); -// let actual_lines: Vec<&str> = debug_json_content.lines().collect(); -// pretty_assertions::assert_eq!(expected_content, actual_lines); -// } + let expected_content = indoc::indoc! {r#" + [ + { + "adapter": "fake-adapter", + "label": "main (fake-adapter)", + "request": "launch", + "program": "/project/main", + "cwd": "/project", + "args": [], + "env": {} + }, + { + "adapter": "fake-adapter", + "label": "other (fake-adapter)", + "request": "launch", + "program": "/project/other", + "cwd": "/project", + "args": [], + "env": {} + } + ]"#}; + + let debug_json_content = fs + .load(path!("/project/.zed/debug.json").as_ref()) + .await + .expect("debug.json should exist") + .lines() + .filter(|line| !line.starts_with("//")) + .collect::>() + .join("\n"); + pretty_assertions::assert_eq!(expected_content, debug_json_content); +} #[gpui::test] async fn test_dap_adapter_config_conversion_and_validation(cx: &mut TestAppContext) { diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index 49f4fc52ac..d5c8eae99c 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -2829,6 +2829,7 @@ impl EditorElement { ) -> Vec { self.editor.update(cx, |editor, cx| { let active_task_indicator_row = + // TODO: add edit button on the right side of each row in the context menu if let Some(crate::CodeContextMenu::CodeActions(CodeActionsMenu { deployed_from, actions,