Edit debug tasks (#32908)

Release Notes:

- Added the ability to edit LSP provided debug tasks

---------

Co-authored-by: Conrad Irwin <conrad.irwin@gmail.com>
This commit is contained in:
Julia Ryan 2025-07-07 14:04:21 -07:00 committed by GitHub
parent d549993c73
commit a9107dfaeb
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 500 additions and 400 deletions

View file

@ -5,9 +5,7 @@
"build": { "build": {
"label": "Build Zed", "label": "Build Zed",
"command": "cargo", "command": "cargo",
"args": [ "args": ["build"]
"build"
]
} }
}, },
{ {
@ -16,9 +14,7 @@
"build": { "build": {
"label": "Build Zed", "label": "Build Zed",
"command": "cargo", "command": "cargo",
"args": [ "args": ["build"]
"build"
]
} }
}, }
] ]

2
Cargo.lock generated
View file

@ -4324,6 +4324,7 @@ dependencies = [
"futures 0.3.31", "futures 0.3.31",
"fuzzy", "fuzzy",
"gpui", "gpui",
"indoc",
"itertools 0.14.0", "itertools 0.14.0",
"language", "language",
"log", "log",
@ -4344,6 +4345,7 @@ dependencies = [
"tasks_ui", "tasks_ui",
"telemetry", "telemetry",
"terminal_view", "terminal_view",
"text",
"theme", "theme",
"tree-sitter", "tree-sitter",
"tree-sitter-go", "tree-sitter-go",

View file

@ -40,6 +40,7 @@ file_icons.workspace = true
futures.workspace = true futures.workspace = true
fuzzy.workspace = true fuzzy.workspace = true
gpui.workspace = true gpui.workspace = true
indoc.workspace = true
itertools.workspace = true itertools.workspace = true
language.workspace = true language.workspace = true
log.workspace = true log.workspace = true
@ -60,6 +61,7 @@ task.workspace = true
tasks_ui.workspace = true tasks_ui.workspace = true
telemetry.workspace = true telemetry.workspace = true
terminal_view.workspace = true terminal_view.workspace = true
text.workspace = true
theme.workspace = true theme.workspace = true
tree-sitter.workspace = true tree-sitter.workspace = true
tree-sitter-json.workspace = true tree-sitter-json.workspace = true

View file

@ -206,7 +206,7 @@ impl PickerDelegate for AttachModalDelegate {
}) })
} }
fn confirm(&mut self, _: bool, window: &mut Window, cx: &mut Context<Picker<Self>>) { fn confirm(&mut self, secondary: bool, window: &mut Window, cx: &mut Context<Picker<Self>>) {
let candidate = self let candidate = self
.matches .matches
.get(self.selected_index()) .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::<DebugPanel>(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::<DapRegistry, _>(|registry, _| { let Some(adapter) = cx.read_global::<DapRegistry, _>(|registry, _| {
registry.adapter(&self.definition.adapter) registry.adapter(&self.definition.adapter)
}) else { }) else {
return; return;
}; };
let workspace = self.workspace.clone();
let definition = self.definition.clone(); let definition = self.definition.clone();
cx.spawn_in(window, async move |this, cx| { cx.spawn_in(window, async move |this, cx| {
let Ok(scenario) = adapter.config_from_zed_format(definition).await else { let Ok(scenario) = adapter.config_from_zed_format(definition).await else {
return; return;
}; };
let panel = workspace panel
.update(cx, |workspace, cx| workspace.panel::<DebugPanel>(cx)) .update_in(cx, |panel, window, cx| {
.ok() panel.start_session(scenario, Default::default(), None, None, window, cx);
.flatten(); })
if let Some(panel) = panel { .ok();
panel
.update_in(cx, |panel, window, cx| {
panel.start_session(scenario, Default::default(), None, None, window, cx);
})
.ok();
}
this.update(cx, |_, cx| { this.update(cx, |_, cx| {
cx.emit(DismissEvent); cx.emit(DismissEvent);
}) })

View file

@ -16,16 +16,18 @@ use dap::{
client::SessionId, debugger_settings::DebuggerSettings, client::SessionId, debugger_settings::DebuggerSettings,
}; };
use dap::{DapRegistry, StartDebuggingRequestArguments}; use dap::{DapRegistry, StartDebuggingRequestArguments};
use editor::Editor;
use gpui::{ use gpui::{
Action, App, AsyncWindowContext, ClipboardItem, Context, DismissEvent, Entity, EntityId, Action, App, AsyncWindowContext, ClipboardItem, Context, DismissEvent, Entity, EntityId,
EventEmitter, FocusHandle, Focusable, MouseButton, MouseDownEvent, Point, Subscription, Task, EventEmitter, FocusHandle, Focusable, MouseButton, MouseDownEvent, Point, Subscription, Task,
WeakEntity, anchored, deferred, WeakEntity, anchored, deferred,
}; };
use text::ToPoint as _;
use itertools::Itertools as _; use itertools::Itertools as _;
use language::Buffer; use language::Buffer;
use project::debugger::session::{Session, SessionStateEvent}; 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 project::{Project, debugger::session::ThreadStatus};
use rpc::proto::{self}; use rpc::proto::{self};
use settings::Settings; use settings::Settings;
@ -35,8 +37,9 @@ use tree_sitter::{Query, StreamingIterator as _};
use ui::{ContextMenu, Divider, PopoverMenuHandle, Tooltip, prelude::*}; use ui::{ContextMenu, Divider, PopoverMenuHandle, Tooltip, prelude::*};
use util::{ResultExt, maybe}; use util::{ResultExt, maybe};
use workspace::SplitDirection; use workspace::SplitDirection;
use workspace::item::SaveOptions;
use workspace::{ use workspace::{
Pane, Workspace, Item, Pane, Workspace,
dock::{DockPosition, Panel, PanelEvent}, dock::{DockPosition, Panel, PanelEvent},
}; };
use zed_actions::ToggleFocus; use zed_actions::ToggleFocus;
@ -988,13 +991,90 @@ impl DebugPanel {
cx.notify(); cx.notify();
} }
pub(crate) fn save_scenario( pub(crate) fn go_to_scenario_definition(
&self, &self,
scenario: &DebugScenario, kind: TaskSourceKind,
scenario: DebugScenario,
worktree_id: WorktreeId, worktree_id: WorktreeId,
window: &mut Window, window: &mut Window,
cx: &mut App, cx: &mut Context<Self>,
) -> Task<Result<ProjectPath>> { ) -> Task<Result<()>> {
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::<Editor>(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: <your 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<Self>,
) -> Task<Result<()>> {
let this = cx.weak_entity();
let project = self.project.clone();
self.workspace self.workspace
.update(cx, |workspace, cx| { .update(cx, |workspace, cx| {
let Some(mut path) = workspace.absolute_path_of_worktree(worktree_id, cx) else { let Some(mut path) = workspace.absolute_path_of_worktree(worktree_id, cx) else {
@ -1027,47 +1107,7 @@ impl DebugPanel {
) )
.await?; .await?;
} }
let project_path = workspace.update(cx, |workspace, cx| {
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<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 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| {
workspace workspace
.project() .project()
.read(cx) .read(cx)
@ -1075,12 +1115,113 @@ impl DebugPanel {
.context( .context(
"Couldn't get project path for .zed/debug.json in active worktree", "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::<Editor>(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))) .unwrap_or_else(|err| Task::ready(Err(err)))
} }
pub fn insert_task_into_editor(
editor: &mut Editor,
new_scenario: String,
project: Entity<Project>,
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(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>) { pub(crate) fn toggle_thread_picker(&mut self, window: &mut Window, cx: &mut Context<Self>) {
self.thread_picker_menu_handle.toggle(window, cx); self.thread_picker_menu_handle.toggle(window, cx);
} }

View file

@ -1,12 +1,10 @@
use anyhow::bail; use anyhow::{Context as _, bail};
use collections::{FxHashMap, HashMap}; use collections::{FxHashMap, HashMap};
use language::LanguageRegistry; use language::LanguageRegistry;
use paths::local_debug_file_relative_path;
use std::{ use std::{
borrow::Cow, borrow::Cow,
path::{Path, PathBuf}, path::{Path, PathBuf},
sync::Arc, sync::Arc,
time::Duration,
usize, usize,
}; };
use tasks_ui::{TaskOverrides, TasksModal}; use tasks_ui::{TaskOverrides, TasksModal};
@ -18,35 +16,27 @@ use editor::{Editor, EditorElement, EditorStyle};
use fuzzy::{StringMatch, StringMatchCandidate}; use fuzzy::{StringMatch, StringMatchCandidate};
use gpui::{ use gpui::{
Action, App, AppContext, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, Action, App, AppContext, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable,
HighlightStyle, InteractiveText, KeyContext, PromptButton, PromptLevel, Render, StyledText, KeyContext, Render, Subscription, Task, TextStyle, WeakEntity,
Subscription, Task, TextStyle, UnderlineStyle, WeakEntity,
}; };
use itertools::Itertools as _; use itertools::Itertools as _;
use picker::{Picker, PickerDelegate, highlighted_match_with_paths::HighlightedMatch}; use picker::{Picker, PickerDelegate, highlighted_match_with_paths::HighlightedMatch};
use project::{ use project::{DebugScenarioContext, TaskContexts, TaskSourceKind, task_store::TaskStore};
DebugScenarioContext, ProjectPath, TaskContexts, TaskSourceKind, task_store::TaskStore, use settings::Settings;
};
use settings::{Settings, initial_local_debug_tasks_content};
use task::{DebugScenario, RevealTarget, ZedDebugConfig}; use task::{DebugScenario, RevealTarget, ZedDebugConfig};
use theme::ThemeSettings; use theme::ThemeSettings;
use ui::{ use ui::{
ActiveTheme, CheckboxWithLabel, Clickable, Context, ContextMenu, Disableable, DropdownMenu, ActiveTheme, Button, ButtonCommon, ButtonSize, CheckboxWithLabel, Clickable, Color, Context,
FluentBuilder, IconWithIndicator, Indicator, IntoElement, KeyBinding, ListItem, ContextMenu, Disableable, DropdownMenu, FluentBuilder, Icon, IconName, IconSize,
ListItemSpacing, ParentElement, StyledExt, ToggleButton, ToggleState, Toggleable, Tooltip, IconWithIndicator, Indicator, InteractiveElement, IntoElement, KeyBinding, Label,
Window, div, prelude::*, px, relative, rems, 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 util::ResultExt;
use workspace::{ModalView, Workspace, pane}; use workspace::{ModalView, Workspace, notifications::DetachAndPromptErr, pane};
use crate::{attach_modal::AttachModal, debugger_panel::DebugPanel}; use crate::{attach_modal::AttachModal, debugger_panel::DebugPanel};
#[allow(unused)]
enum SaveScenarioState {
Saving,
Saved((ProjectPath, SharedString)),
Failed(SharedString),
}
pub(super) struct NewProcessModal { pub(super) struct NewProcessModal {
workspace: WeakEntity<Workspace>, workspace: WeakEntity<Workspace>,
debug_panel: WeakEntity<DebugPanel>, debug_panel: WeakEntity<DebugPanel>,
@ -56,7 +46,6 @@ pub(super) struct NewProcessModal {
configure_mode: Entity<ConfigureMode>, configure_mode: Entity<ConfigureMode>,
task_mode: TaskMode, task_mode: TaskMode,
debugger: Option<DebugAdapterName>, debugger: Option<DebugAdapterName>,
save_scenario_state: Option<SaveScenarioState>,
_subscriptions: [Subscription; 3], _subscriptions: [Subscription; 3],
} }
@ -268,7 +257,6 @@ impl NewProcessModal {
mode, mode,
debug_panel: debug_panel.downgrade(), debug_panel: debug_panel.downgrade(),
workspace: workspace_handle, workspace: workspace_handle,
save_scenario_state: None,
_subscriptions, _subscriptions,
} }
}); });
@ -420,63 +408,29 @@ impl NewProcessModal {
self.debug_picker.read(cx).delegate.task_contexts.clone() self.debug_picker.read(cx).delegate.task_contexts.clone()
} }
fn save_debug_scenario(&mut self, window: &mut Window, cx: &mut Context<Self>) { pub fn save_debug_scenario(&mut self, window: &mut Window, cx: &mut Context<Self>) {
let task_contents = self.task_contexts(cx); let task_contexts = self.task_contexts(cx);
let Some(adapter) = self.debugger.as_ref() else { let Some(adapter) = self.debugger.as_ref() else {
return; return;
}; };
let scenario = self.debug_scenario(&adapter, cx); let scenario = self.debug_scenario(&adapter, cx);
self.save_scenario_state = Some(SaveScenarioState::Saving);
cx.spawn_in(window, async move |this, cx| { cx.spawn_in(window, async move |this, cx| {
let Some((scenario, worktree_id)) = scenario let scenario = scenario.await.context("no scenario to save")?;
.await let worktree_id = task_contexts
.zip(task_contents.and_then(|tcx| tcx.worktree())) .context("no task contexts")?
else { .worktree()
this.update(cx, |this, _| { .context("no active worktree")?;
this.save_scenario_state = Some(SaveScenarioState::Failed( this.update_in(cx, |this, window, cx| {
"Couldn't get scenario or task contents".into(), this.debug_panel.update(cx, |panel, cx| {
)) panel.save_scenario(scenario, worktree_id, window, cx)
}) })
.ok(); })??
return; .await?;
}; this.update_in(cx, |_, _, cx| {
cx.emit(DismissEvent);
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()))
}
}) })
.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( fn adapter_drop_down_menu(
@ -544,70 +498,6 @@ impl NewProcessModal {
}), }),
) )
} }
fn open_debug_json(&self, window: &mut Window, cx: &mut Context<NewProcessModal>) {
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::<Editor>(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"); static SELECT_DEBUGGER_LABEL: SharedString = SharedString::new_static("Select Debugger");
@ -812,39 +702,21 @@ impl Render for NewProcessModal {
NewProcessMode::Launch => el.child( NewProcessMode::Launch => el.child(
container container
.child( .child(
h_flex() h_flex().child(
.text_ui_sm(cx) Button::new("edit-custom-debug", "Edit in debug.json")
.text_color(Color::Muted.color(cx)) .on_click(cx.listener(|this, _, window, cx| {
.child( this.save_debug_scenario(window, cx);
InteractiveText::new( }))
"open-debug-json", .disabled(
StyledText::new( self.debugger.is_none()
"Open .zed/debug.json for advanced configuration.", || self
) .configure_mode
.with_highlights([( .read(cx)
5..20, .program
HighlightStyle { .read(cx)
underline: Some(UnderlineStyle { .is_empty(cx),
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);
})
}
},
), ),
), ),
) )
.child( .child(
Button::new("debugger-spawn", "Start") 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 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( .child(
Button::new("debugger-spawn", "Start") h_flex()
.on_click(cx.listener(|this, _, window, cx| { .child(div().child(self.adapter_drop_down_menu(window, cx)))
this.start_new_session(window, cx) .child(
})) Button::new("debugger-spawn", "Start")
.disabled( .on_click(cx.listener(|this, _, window, cx| {
self.debugger.is_none() this.start_new_session(window, cx)
|| self }))
.attach_mode .disabled(disabled),
.read(cx)
.attach_picker
.read(cx)
.picker
.read(cx)
.delegate
.match_count()
== 0,
), ),
), )
), }),
NewProcessMode::Debug => el, NewProcessMode::Debug => el,
NewProcessMode::Task => el, NewProcessMode::Task => el,
} }
@ -1048,25 +939,6 @@ impl ConfigureMode {
) )
.checkbox_position(ui::IconPosition::End), .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( fn confirm_input(&mut self, _: bool, window: &mut Window, cx: &mut Context<Picker<Self>>) {
&mut self,
_secondary: bool,
window: &mut Window,
cx: &mut Context<Picker<Self>>,
) {
let text = self.prompt.clone(); let text = self.prompt.clone();
let (task_context, worktree_id) = self let (task_context, worktree_id) = self
.task_contexts .task_contexts
@ -1364,7 +1231,7 @@ impl PickerDelegate for DebugDelegate {
let args = args.collect::<Vec<_>>(); let args = args.collect::<Vec<_>>();
let task = task::TaskTemplate { let task = task::TaskTemplate {
label: "one-off".to_owned(), label: "one-off".to_owned(), // TODO: rename using command as label
env, env,
command: program, command: program,
args, args,
@ -1405,7 +1272,11 @@ impl PickerDelegate for DebugDelegate {
.background_spawn(async move { .background_spawn(async move {
for locator in locators { for locator in locators {
if let Some(scenario) = 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); return Some(scenario);
} }
@ -1439,13 +1310,18 @@ impl PickerDelegate for DebugDelegate {
.detach(); .detach();
} }
fn confirm(&mut self, _: bool, window: &mut Window, cx: &mut Context<picker::Picker<Self>>) { fn confirm(
&mut self,
secondary: bool,
window: &mut Window,
cx: &mut Context<picker::Picker<Self>>,
) {
let debug_scenario = self let debug_scenario = self
.matches .matches
.get(self.selected_index()) .get(self.selected_index())
.and_then(|match_candidate| self.candidates.get(match_candidate.candidate_id).cloned()); .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; return;
}; };
@ -1463,24 +1339,38 @@ impl PickerDelegate for DebugDelegate {
}); });
let DebugScenarioContext { let DebugScenarioContext {
task_context, task_context,
active_buffer, active_buffer: _,
worktree_id, worktree_id,
} = context; } = context;
let active_buffer = active_buffer.and_then(|buffer| buffer.upgrade());
send_telemetry(&debug_scenario, TelemetrySpawnLocation::ScenarioList, cx); if secondary {
self.debug_panel let Some(kind) = kind else { return };
.update(cx, |panel, cx| { let Some(id) = worktree_id else { return };
panel.start_session( let debug_panel = self.debug_panel.clone();
debug_scenario, cx.spawn_in(window, async move |_, cx| {
task_context, debug_panel
active_buffer, .update_in(cx, |debug_panel, window, cx| {
worktree_id, debug_panel.go_to_scenario_definition(kind, debug_scenario, id, window, cx)
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); cx.emit(DismissEvent);
} }
@ -1498,19 +1388,23 @@ impl PickerDelegate for DebugDelegate {
let footer = h_flex() let footer = h_flex()
.w_full() .w_full()
.p_1p5() .p_1p5()
.justify_end() .justify_between()
.border_t_1() .border_t_1()
.border_color(cx.theme().colors().border_variant) .border_color(cx.theme().colors().border_variant)
// .child( .children({
// // TODO: add button to open selected task in debug.json let action = menu::SecondaryConfirm.boxed_clone();
// h_flex().into_any_element(), 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| { .map(|this| {
if (current_modifiers.alt || self.matches.is_empty()) && !self.prompt.is_empty() { if (current_modifiers.alt || self.matches.is_empty()) && !self.prompt.is_empty() {
let action = picker::ConfirmInput { let action = picker::ConfirmInput { secondary: false }.boxed_clone();
secondary: current_modifiers.secondary(),
}
.boxed_clone();
this.children(KeyBinding::for_action(&*action, window, cx).map(|keybind| { this.children(KeyBinding::for_action(&*action, window, cx).map(|keybind| {
Button::new("launch-custom", "Launch Custom") Button::new("launch-custom", "Launch Custom")
.key_binding(keybind) .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<str>,
cwd: impl AsRef<str>,
stop_on_entry: bool,
window: &mut Window,
cx: &mut Context<Self>,
) {
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,
}
})
}
}

View file

@ -1,13 +1,15 @@
use dap::DapRegistry; use dap::DapRegistry;
use editor::Editor;
use gpui::{BackgroundExecutor, TestAppContext, VisualTestContext}; use gpui::{BackgroundExecutor, TestAppContext, VisualTestContext};
use project::{FakeFs, Project}; use project::{FakeFs, Fs as _, Project};
use serde_json::json; use serde_json::json;
use std::sync::Arc; use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::atomic::{AtomicBool, Ordering};
use task::{DebugRequest, DebugScenario, LaunchRequest, TaskContext, VariableName, ZedDebugConfig}; use task::{DebugRequest, DebugScenario, LaunchRequest, TaskContext, VariableName, ZedDebugConfig};
use text::Point;
use util::path; use util::path;
// use crate::new_process_modal::NewProcessMode; use crate::NewProcessMode;
use crate::tests::{init_test, init_test_workspace}; use crate::tests::{init_test, init_test_workspace};
#[gpui::test] #[gpui::test]
@ -159,111 +161,127 @@ async fn test_debug_session_substitutes_variables_and_relativizes_paths(
} }
} }
// #[gpui::test] #[gpui::test]
// async fn test_save_debug_scenario_to_file(executor: BackgroundExecutor, cx: &mut TestAppContext) { async fn test_save_debug_scenario_to_file(executor: BackgroundExecutor, cx: &mut TestAppContext) {
// init_test(cx); init_test(cx);
// let fs = FakeFs::new(executor.clone()); let fs = FakeFs::new(executor.clone());
// fs.insert_tree( fs.insert_tree(
// path!("/project"), path!("/project"),
// json!({ json!({
// "main.rs": "fn main() {}" "main.rs": "fn main() {}"
// }), }),
// ) )
// .await; .await;
// let project = Project::test(fs.clone(), [path!("/project").as_ref()], cx).await; let project = Project::test(fs.clone(), [path!("/project").as_ref()], cx).await;
// let workspace = init_test_workspace(&project, cx).await; let workspace = init_test_workspace(&project, cx).await;
// let cx = &mut VisualTestContext::from_window(*workspace, cx); let cx = &mut VisualTestContext::from_window(*workspace, cx);
// workspace workspace
// .update(cx, |workspace, window, cx| { .update(cx, |workspace, window, cx| {
// crate::new_process_modal::NewProcessModal::show( crate::new_process_modal::NewProcessModal::show(
// workspace, workspace,
// window, window,
// NewProcessMode::Debug, NewProcessMode::Debug,
// None, None,
// cx, cx,
// ); );
// }) })
// .unwrap(); .unwrap();
// cx.run_until_parked(); cx.run_until_parked();
// let modal = workspace let modal = workspace
// .update(cx, |workspace, _, cx| { .update(cx, |workspace, _, cx| {
// workspace.active_modal::<crate::new_process_modal::NewProcessModal>(cx) workspace.active_modal::<crate::new_process_modal::NewProcessModal>(cx)
// }) })
// .unwrap() .unwrap()
// .expect("Modal should be active"); .expect("Modal should be active");
// modal.update_in(cx, |modal, window, cx| { modal.update_in(cx, |modal, window, cx| {
// modal.set_configure("/project/main", "/project", false, window, cx); modal.set_configure("/project/main", "/project", false, window, cx);
// modal.save_scenario(window, cx); modal.save_debug_scenario(window, cx);
// }); });
// cx.executor().run_until_parked(); cx.executor().run_until_parked();
// let debug_json_content = fs let editor = workspace
// .load(path!("/project/.zed/debug.json").as_ref()) .update(cx, |workspace, _window, cx| {
// .await workspace.active_item_as::<Editor>(cx).unwrap()
// .expect("debug.json should exist"); })
.unwrap();
// let expected_content = vec![ let debug_json_content = fs
// "[", .load(path!("/project/.zed/debug.json").as_ref())
// " {", .await
// r#" "adapter": "fake-adapter","#, .expect("debug.json should exist")
// r#" "label": "main (fake-adapter)","#, .lines()
// r#" "request": "launch","#, .filter(|line| !line.starts_with("//"))
// r#" "program": "/project/main","#, .collect::<Vec<_>>()
// r#" "cwd": "/project","#, .join("\n");
// r#" "args": [],"#,
// r#" "env": {}"#,
// " }",
// "]",
// ];
// let actual_lines: Vec<&str> = debug_json_content.lines().collect(); let expected_content = indoc::indoc! {r#"
// pretty_assertions::assert_eq!(expected_content, actual_lines); [
{
"adapter": "fake-adapter",
"label": "main (fake-adapter)",
"request": "launch",
"program": "/project/main",
"cwd": "/project",
"args": [],
"env": {}
}
]"#};
// modal.update_in(cx, |modal, window, cx| { pretty_assertions::assert_eq!(expected_content, debug_json_content);
// modal.set_configure("/project/other", "/project", true, window, cx);
// modal.save_scenario(window, cx);
// });
// cx.executor().run_until_parked(); editor.update(cx, |editor, cx| {
assert_eq!(
editor.selections.newest::<Point>(cx).head(),
Point::new(5, 2)
)
});
// let debug_json_content = fs modal.update_in(cx, |modal, window, cx| {
// .load(path!("/project/.zed/debug.json").as_ref()) modal.set_configure("/project/other", "/project", true, window, cx);
// .await modal.save_debug_scenario(window, cx);
// .expect("debug.json should exist after second save"); });
// let expected_content = vec![ cx.executor().run_until_parked();
// "[",
// " {",
// 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": {}"#,
// " }",
// "]",
// ];
// let actual_lines: Vec<&str> = debug_json_content.lines().collect(); let expected_content = indoc::indoc! {r#"
// pretty_assertions::assert_eq!(expected_content, actual_lines); [
// } {
"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::<Vec<_>>()
.join("\n");
pretty_assertions::assert_eq!(expected_content, debug_json_content);
}
#[gpui::test] #[gpui::test]
async fn test_dap_adapter_config_conversion_and_validation(cx: &mut TestAppContext) { async fn test_dap_adapter_config_conversion_and_validation(cx: &mut TestAppContext) {

View file

@ -2829,6 +2829,7 @@ impl EditorElement {
) -> Vec<AnyElement> { ) -> Vec<AnyElement> {
self.editor.update(cx, |editor, cx| { self.editor.update(cx, |editor, cx| {
let active_task_indicator_row = 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 { if let Some(crate::CodeContextMenu::CodeActions(CodeActionsMenu {
deployed_from, deployed_from,
actions, actions,