debugger: Add comment-preserving debug.json editing (#32896)

Release Notes:

- Re-added "Save to `debug.json`" for custom debug tasks

---------

Co-authored-by: Cole Miller <cole@zed.dev>
This commit is contained in:
Julia Ryan 2025-06-17 15:51:05 -07:00 committed by GitHub
parent 2f1d25d7f3
commit e47c48fd3b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 186 additions and 134 deletions

3
Cargo.lock generated
View file

@ -4293,6 +4293,7 @@ dependencies = [
"rpc", "rpc",
"serde", "serde",
"serde_json", "serde_json",
"serde_json_lenient",
"settings", "settings",
"shlex", "shlex",
"sysinfo", "sysinfo",
@ -4300,6 +4301,8 @@ dependencies = [
"tasks_ui", "tasks_ui",
"terminal_view", "terminal_view",
"theme", "theme",
"tree-sitter",
"tree-sitter-json",
"ui", "ui",
"unindent", "unindent",
"util", "util",

View file

@ -1,10 +1,10 @@
use ::fs::Fs;
use anyhow::{Context as _, Result, anyhow}; use anyhow::{Context as _, Result, anyhow};
use async_compression::futures::bufread::GzipDecoder; use async_compression::futures::bufread::GzipDecoder;
use async_tar::Archive; use async_tar::Archive;
use async_trait::async_trait; use async_trait::async_trait;
use collections::HashMap; use collections::HashMap;
pub use dap_types::{StartDebuggingRequestArguments, StartDebuggingRequestArgumentsRequest}; pub use dap_types::{StartDebuggingRequestArguments, StartDebuggingRequestArgumentsRequest};
use fs::Fs;
use futures::io::BufReader; use futures::io::BufReader;
use gpui::{AsyncApp, SharedString}; use gpui::{AsyncApp, SharedString};
pub use http_client::{HttpClient, github::latest_github_release}; pub use http_client::{HttpClient, github::latest_github_release};

View file

@ -50,7 +50,7 @@ project.workspace = true
rpc.workspace = true rpc.workspace = true
serde.workspace = true serde.workspace = true
serde_json.workspace = true serde_json.workspace = true
# serde_json_lenient.workspace = true serde_json_lenient.workspace = true
settings.workspace = true settings.workspace = true
shlex.workspace = true shlex.workspace = true
sysinfo.workspace = true sysinfo.workspace = true
@ -58,6 +58,8 @@ task.workspace = true
tasks_ui.workspace = true tasks_ui.workspace = true
terminal_view.workspace = true terminal_view.workspace = true
theme.workspace = true theme.workspace = true
tree-sitter.workspace = true
tree-sitter-json.workspace = true
ui.workspace = true ui.workspace = true
util.workspace = true util.workspace = true
workspace.workspace = true workspace.workspace = true

View file

@ -7,7 +7,7 @@ use crate::{
NewProcessModal, NewProcessMode, Pause, Restart, StepInto, StepOut, StepOver, Stop, NewProcessModal, NewProcessMode, Pause, Restart, StepInto, StepOut, StepOver, Stop,
ToggleExpandItem, ToggleSessionPicker, ToggleThreadPicker, persistence, spawn_task_or_modal, ToggleExpandItem, ToggleSessionPicker, ToggleThreadPicker, persistence, spawn_task_or_modal,
}; };
use anyhow::Result; use anyhow::{Context as _, Result, anyhow};
use dap::adapters::DebugAdapterName; use dap::adapters::DebugAdapterName;
use dap::debugger_settings::DebugPanelDockPosition; use dap::debugger_settings::DebugPanelDockPosition;
use dap::{ use dap::{
@ -21,14 +21,16 @@ use gpui::{
WeakEntity, actions, anchored, deferred, WeakEntity, actions, anchored, deferred,
}; };
use itertools::Itertools as _;
use language::Buffer; use language::Buffer;
use project::debugger::session::{Session, SessionStateEvent}; use project::debugger::session::{Session, SessionStateEvent};
use project::{Fs, WorktreeId}; use project::{Fs, ProjectPath, 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;
use std::sync::Arc; use std::sync::{Arc, LazyLock};
use task::{DebugScenario, TaskContext}; use task::{DebugScenario, TaskContext};
use tree_sitter::{Query, StreamingIterator as _};
use ui::{ContextMenu, Divider, PopoverMenuHandle, Tooltip, prelude::*}; use ui::{ContextMenu, Divider, PopoverMenuHandle, Tooltip, prelude::*};
use util::maybe; use util::maybe;
use workspace::SplitDirection; use workspace::SplitDirection;
@ -957,69 +959,98 @@ impl DebugPanel {
cx.notify(); cx.notify();
} }
// TODO: restore once we have proper comment preserving file edits pub(crate) fn save_scenario(
// pub(crate) fn save_scenario( &self,
// &self, scenario: &DebugScenario,
// scenario: &DebugScenario, worktree_id: WorktreeId,
// worktree_id: WorktreeId, window: &mut Window,
// window: &mut Window, cx: &mut App,
// cx: &mut App, ) -> Task<Result<ProjectPath>> {
// ) -> Task<Result<ProjectPath>> { 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 { return Task::ready(Err(anyhow!("Couldn't get worktree path")));
// return Task::ready(Err(anyhow!("Couldn't get worktree path"))); };
// };
// let serialized_scenario = serde_json::to_value(scenario); let serialized_scenario = serde_json::to_value(scenario);
// cx.spawn_in(window, async move |workspace, cx| { cx.spawn_in(window, async move |workspace, cx| {
// let serialized_scenario = serialized_scenario?; let serialized_scenario = serialized_scenario?;
// let fs = let fs =
// workspace.read_with(cx, |workspace, _| workspace.app_state().fs.clone())?; workspace.read_with(cx, |workspace, _| workspace.app_state().fs.clone())?;
// path.push(paths::local_settings_folder_relative_path()); path.push(paths::local_settings_folder_relative_path());
// if !fs.is_dir(path.as_path()).await { if !fs.is_dir(path.as_path()).await {
// fs.create_dir(path.as_path()).await?; fs.create_dir(path.as_path()).await?;
// } }
// path.pop(); path.pop();
// path.push(paths::local_debug_file_relative_path()); path.push(paths::local_debug_file_relative_path());
// let path = path.as_path(); let path = path.as_path();
// if !fs.is_file(path).await { if !fs.is_file(path).await {
// fs.create_file(path, Default::default()).await?; fs.create_file(path, Default::default()).await?;
// fs.write( fs.write(
// path, path,
// initial_local_debug_tasks_content().to_string().as_bytes(), settings::initial_local_debug_tasks_content()
// ) .to_string()
// .await?; .as_bytes(),
// } )
.await?;
}
// let content = fs.load(path).await?; let mut content = fs.load(path).await?;
// let mut values = let new_scenario = serde_json_lenient::to_string_pretty(&serialized_scenario)?
// serde_json_lenient::from_str::<Vec<serde_json::Value>>(&content)?; .lines()
// values.push(serialized_scenario); .map(|l| format!(" {l}"))
// fs.save( .join("\n");
// path,
// &serde_json_lenient::to_string_pretty(&values).map(Into::into)?,
// Default::default(),
// )
// .await?;
// workspace.update(cx, |workspace, cx| { static ARRAY_QUERY: LazyLock<Query> = LazyLock::new(|| {
// workspace Query::new(
// .project() &tree_sitter_json::LANGUAGE.into(),
// .read(cx) "(document (array (object) @object))", // TODO: use "." anchor to only match last object
// .project_path_for_absolute_path(&path, cx) )
// .context( .expect("Failed to create ARRAY_QUERY")
// "Couldn't get project path for .zed/debug.json in active worktree", });
// )
// })? let mut parser = tree_sitter::Parser::new();
// }) parser
// }) .set_language(&tree_sitter_json::LANGUAGE.into())
// .unwrap_or_else(|err| Task::ready(Err(err))) .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
.project()
.read(cx)
.project_path_for_absolute_path(&path, cx)
.context(
"Couldn't get project path for .zed/debug.json in active worktree",
)
})?
})
})
.unwrap_or_else(|err| Task::ready(Err(err)))
}
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

@ -6,6 +6,7 @@ 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};
@ -39,11 +40,12 @@ use workspace::{ModalView, Workspace, pane};
use crate::{attach_modal::AttachModal, debugger_panel::DebugPanel}; use crate::{attach_modal::AttachModal, debugger_panel::DebugPanel};
// enum SaveScenarioState { #[allow(unused)]
// Saving, enum SaveScenarioState {
// Saved((ProjectPath, SharedString)), Saving,
// Failed(SharedString), Saved((ProjectPath, SharedString)),
// } Failed(SharedString),
}
pub(super) struct NewProcessModal { pub(super) struct NewProcessModal {
workspace: WeakEntity<Workspace>, workspace: WeakEntity<Workspace>,
@ -54,7 +56,7 @@ 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>, save_scenario_state: Option<SaveScenarioState>,
_subscriptions: [Subscription; 3], _subscriptions: [Subscription; 3],
} }
@ -265,7 +267,7 @@ impl NewProcessModal {
mode, mode,
debug_panel: debug_panel.downgrade(), debug_panel: debug_panel.downgrade(),
workspace: workspace_handle, workspace: workspace_handle,
// save_scenario_state: None, save_scenario_state: None,
_subscriptions, _subscriptions,
} }
}); });
@ -352,12 +354,11 @@ impl NewProcessModal {
return; return;
} }
// TODO: Restore once we have proper, comment preserving edits if let NewProcessMode::Launch = &self.mode {
// if let NewProcessMode::Launch = &self.mode { if self.configure_mode.read(cx).save_to_debug_json.selected() {
// if self.launch_mode.read(cx).save_to_debug_json.selected() { self.save_debug_scenario(window, cx);
// self.save_debug_scenario(window, cx); }
// } }
// }
let Some(debugger) = self.debugger.clone() else { let Some(debugger) = self.debugger.clone() else {
return; return;
@ -418,47 +419,64 @@ 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>) { fn save_debug_scenario(&mut self, window: &mut Window, cx: &mut Context<Self>) {
// let Some((save_scenario, scenario_label)) = self let task_contents = self.task_contexts(cx);
// .debugger let Some(adapter) = self.debugger.as_ref() else {
// .as_ref() return;
// .and_then(|debugger| self.debug_scenario(&debugger, cx)) };
// .zip(self.task_contexts(cx).and_then(|tcx| tcx.worktree())) let scenario = self.debug_scenario(&adapter, cx);
// .and_then(|(scenario, worktree_id)| {
// self.debug_panel
// .update(cx, |panel, cx| {
// panel.save_scenario(&scenario, worktree_id, window, cx)
// })
// .ok()
// .zip(Some(scenario.label.clone()))
// })
// else {
// return;
// };
// self.save_scenario_state = Some(SaveScenarioState::Saving); self.save_scenario_state = Some(SaveScenarioState::Saving);
// cx.spawn(async move |this, cx| { cx.spawn_in(window, async move |this, cx| {
// let res = save_scenario.await; 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(),
))
})
.ok();
return;
};
// this.update(cx, |this, _| match res { let Some(save_scenario) = this
// Ok(saved_file) => { .update_in(cx, |this, window, cx| {
// this.save_scenario_state = this.debug_panel
// Some(SaveScenarioState::Saved((saved_file, scenario_label))) .update(cx, |panel, cx| {
// } panel.save_scenario(&scenario, worktree_id, window, cx)
// Err(error) => { })
// this.save_scenario_state = .ok()
// Some(SaveScenarioState::Failed(error.to_string().into())) })
// } .ok()
// }) .flatten()
// .ok(); else {
return;
};
let res = save_scenario.await;
// cx.background_executor().timer(Duration::from_secs(3)).await; this.update(cx, |this, _| match res {
// this.update(cx, |this, _| this.save_scenario_state.take()) Ok(saved_file) => {
// .ok(); this.save_scenario_state = Some(SaveScenarioState::Saved((
// }) saved_file,
// .detach(); 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();
}
fn adapter_drop_down_menu( fn adapter_drop_down_menu(
&mut self, &mut self,
@ -903,7 +921,7 @@ pub(super) struct ConfigureMode {
program: Entity<Editor>, program: Entity<Editor>,
cwd: Entity<Editor>, cwd: Entity<Editor>,
stop_on_entry: ToggleState, stop_on_entry: ToggleState,
// save_to_debug_json: ToggleState, save_to_debug_json: ToggleState,
} }
impl ConfigureMode { impl ConfigureMode {
@ -922,7 +940,7 @@ impl ConfigureMode {
program, program,
cwd, cwd,
stop_on_entry: ToggleState::Unselected, stop_on_entry: ToggleState::Unselected,
// save_to_debug_json: ToggleState::Unselected, save_to_debug_json: ToggleState::Unselected,
}) })
} }
@ -1028,27 +1046,25 @@ impl ConfigureMode {
) )
.checkbox_position(ui::IconPosition::End), .checkbox_position(ui::IconPosition::End),
) )
// TODO: restore once we have proper, comment preserving .child(
// file edits. CheckboxWithLabel::new(
// .child( "debugger-save-to-debug-json",
// CheckboxWithLabel::new( Label::new("Save to debug.json")
// "debugger-save-to-debug-json", .size(ui::LabelSize::Small)
// Label::new("Save to debug.json") .color(Color::Muted),
// .size(ui::LabelSize::Small) self.save_to_debug_json,
// .color(Color::Muted), {
// self.save_to_debug_json, let this = cx.weak_entity();
// { move |state, _, cx| {
// let this = cx.weak_entity(); this.update(cx, |this, _| {
// move |state, _, cx| { this.save_to_debug_json = *state;
// this.update(cx, |this, _| { })
// this.save_to_debug_json = *state; .ok();
// }) }
// .ok(); },
// } )
// }, .checkbox_position(ui::IconPosition::End),
// ) )
// .checkbox_position(ui::IconPosition::End),
// )
} }
} }