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:
parent
2f1d25d7f3
commit
e47c48fd3b
5 changed files with 186 additions and 134 deletions
|
@ -1,10 +1,10 @@
|
|||
use ::fs::Fs;
|
||||
use anyhow::{Context as _, Result, anyhow};
|
||||
use async_compression::futures::bufread::GzipDecoder;
|
||||
use async_tar::Archive;
|
||||
use async_trait::async_trait;
|
||||
use collections::HashMap;
|
||||
pub use dap_types::{StartDebuggingRequestArguments, StartDebuggingRequestArgumentsRequest};
|
||||
use fs::Fs;
|
||||
use futures::io::BufReader;
|
||||
use gpui::{AsyncApp, SharedString};
|
||||
pub use http_client::{HttpClient, github::latest_github_release};
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue