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

View file

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

View file

@ -7,7 +7,7 @@ use crate::{
NewProcessModal, NewProcessMode, Pause, Restart, StepInto, StepOut, StepOver, Stop,
ToggleExpandItem, ToggleSessionPicker, ToggleThreadPicker, persistence, spawn_task_or_modal,
};
use anyhow::Result;
use anyhow::{Context as _, Result, anyhow};
use dap::adapters::DebugAdapterName;
use dap::debugger_settings::DebugPanelDockPosition;
use dap::{
@ -21,14 +21,16 @@ use gpui::{
WeakEntity, actions, anchored, deferred,
};
use itertools::Itertools as _;
use language::Buffer;
use project::debugger::session::{Session, SessionStateEvent};
use project::{Fs, WorktreeId};
use project::{Fs, ProjectPath, WorktreeId};
use project::{Project, debugger::session::ThreadStatus};
use rpc::proto::{self};
use settings::Settings;
use std::sync::Arc;
use std::sync::{Arc, LazyLock};
use task::{DebugScenario, TaskContext};
use tree_sitter::{Query, StreamingIterator as _};
use ui::{ContextMenu, Divider, PopoverMenuHandle, Tooltip, prelude::*};
use util::maybe;
use workspace::SplitDirection;
@ -957,69 +959,98 @@ impl DebugPanel {
cx.notify();
}
// TODO: restore once we have proper comment preserving file edits
// pub(crate) fn save_scenario(
// &self,
// scenario: &DebugScenario,
// worktree_id: WorktreeId,
// window: &mut Window,
// cx: &mut App,
// ) -> Task<Result<ProjectPath>> {
// self.workspace
// .update(cx, |workspace, cx| {
// let Some(mut path) = workspace.absolute_path_of_worktree(worktree_id, cx) else {
// return Task::ready(Err(anyhow!("Couldn't get worktree path")));
// };
pub(crate) fn save_scenario(
&self,
scenario: &DebugScenario,
worktree_id: WorktreeId,
window: &mut Window,
cx: &mut App,
) -> Task<Result<ProjectPath>> {
self.workspace
.update(cx, |workspace, cx| {
let Some(mut path) = workspace.absolute_path_of_worktree(worktree_id, cx) else {
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| {
// let serialized_scenario = serialized_scenario?;
// let fs =
// workspace.read_with(cx, |workspace, _| workspace.app_state().fs.clone())?;
cx.spawn_in(window, async move |workspace, cx| {
let serialized_scenario = serialized_scenario?;
let fs =
workspace.read_with(cx, |workspace, _| workspace.app_state().fs.clone())?;
// path.push(paths::local_settings_folder_relative_path());
// if !fs.is_dir(path.as_path()).await {
// fs.create_dir(path.as_path()).await?;
// }
// path.pop();
path.push(paths::local_settings_folder_relative_path());
if !fs.is_dir(path.as_path()).await {
fs.create_dir(path.as_path()).await?;
}
path.pop();
// path.push(paths::local_debug_file_relative_path());
// let path = path.as_path();
path.push(paths::local_debug_file_relative_path());
let path = path.as_path();
// if !fs.is_file(path).await {
// fs.create_file(path, Default::default()).await?;
// fs.write(
// path,
// initial_local_debug_tasks_content().to_string().as_bytes(),
// )
// .await?;
// }
if !fs.is_file(path).await {
fs.create_file(path, Default::default()).await?;
fs.write(
path,
settings::initial_local_debug_tasks_content()
.to_string()
.as_bytes(),
)
.await?;
}
// let content = fs.load(path).await?;
// let mut values =
// serde_json_lenient::from_str::<Vec<serde_json::Value>>(&content)?;
// values.push(serialized_scenario);
// fs.save(
// path,
// &serde_json_lenient::to_string_pretty(&values).map(Into::into)?,
// Default::default(),
// )
// .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");
// 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)))
// }
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
.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>) {
self.thread_picker_menu_handle.toggle(window, cx);

View file

@ -6,6 +6,7 @@ use std::{
borrow::Cow,
path::{Path, PathBuf},
sync::Arc,
time::Duration,
usize,
};
use tasks_ui::{TaskOverrides, TasksModal};
@ -39,11 +40,12 @@ use workspace::{ModalView, Workspace, pane};
use crate::{attach_modal::AttachModal, debugger_panel::DebugPanel};
// enum SaveScenarioState {
// Saving,
// Saved((ProjectPath, SharedString)),
// Failed(SharedString),
// }
#[allow(unused)]
enum SaveScenarioState {
Saving,
Saved((ProjectPath, SharedString)),
Failed(SharedString),
}
pub(super) struct NewProcessModal {
workspace: WeakEntity<Workspace>,
@ -54,7 +56,7 @@ pub(super) struct NewProcessModal {
configure_mode: Entity<ConfigureMode>,
task_mode: TaskMode,
debugger: Option<DebugAdapterName>,
// save_scenario_state: Option<SaveScenarioState>,
save_scenario_state: Option<SaveScenarioState>,
_subscriptions: [Subscription; 3],
}
@ -265,7 +267,7 @@ impl NewProcessModal {
mode,
debug_panel: debug_panel.downgrade(),
workspace: workspace_handle,
// save_scenario_state: None,
save_scenario_state: None,
_subscriptions,
}
});
@ -352,12 +354,11 @@ impl NewProcessModal {
return;
}
// TODO: Restore once we have proper, comment preserving edits
// if let NewProcessMode::Launch = &self.mode {
// if self.launch_mode.read(cx).save_to_debug_json.selected() {
// self.save_debug_scenario(window, cx);
// }
// }
if let NewProcessMode::Launch = &self.mode {
if self.configure_mode.read(cx).save_to_debug_json.selected() {
self.save_debug_scenario(window, cx);
}
}
let Some(debugger) = self.debugger.clone() else {
return;
@ -418,47 +419,64 @@ impl NewProcessModal {
self.debug_picker.read(cx).delegate.task_contexts.clone()
}
// fn save_debug_scenario(&mut self, window: &mut Window, cx: &mut Context<Self>) {
// let Some((save_scenario, scenario_label)) = self
// .debugger
// .as_ref()
// .and_then(|debugger| self.debug_scenario(&debugger, cx))
// .zip(self.task_contexts(cx).and_then(|tcx| tcx.worktree()))
// .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;
// };
fn save_debug_scenario(&mut self, window: &mut Window, cx: &mut Context<Self>) {
let task_contents = 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);
self.save_scenario_state = Some(SaveScenarioState::Saving);
// cx.spawn(async move |this, cx| {
// let res = save_scenario.await;
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(),
))
})
.ok();
return;
};
// this.update(cx, |this, _| match res {
// Ok(saved_file) => {
// this.save_scenario_state =
// Some(SaveScenarioState::Saved((saved_file, scenario_label)))
// }
// Err(error) => {
// this.save_scenario_state =
// Some(SaveScenarioState::Failed(error.to_string().into()))
// }
// })
// .ok();
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;
// cx.background_executor().timer(Duration::from_secs(3)).await;
// this.update(cx, |this, _| this.save_scenario_state.take())
// .ok();
// })
// .detach();
// }
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();
}
fn adapter_drop_down_menu(
&mut self,
@ -903,7 +921,7 @@ pub(super) struct ConfigureMode {
program: Entity<Editor>,
cwd: Entity<Editor>,
stop_on_entry: ToggleState,
// save_to_debug_json: ToggleState,
save_to_debug_json: ToggleState,
}
impl ConfigureMode {
@ -922,7 +940,7 @@ impl ConfigureMode {
program,
cwd,
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),
)
// TODO: restore once we have proper, comment preserving
// file edits.
// .child(
// CheckboxWithLabel::new(
// "debugger-save-to-debug-json",
// Label::new("Save to debug.json")
// .size(ui::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),
// )
.child(
CheckboxWithLabel::new(
"debugger-save-to-debug-json",
Label::new("Save to debug.json")
.size(ui::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),
)
}
}