Detailed changes
@@ -4293,6 +4293,7 @@ dependencies = [
"rpc",
"serde",
"serde_json",
+ "serde_json_lenient",
"settings",
"shlex",
"sysinfo",
@@ -4300,6 +4301,8 @@ dependencies = [
"tasks_ui",
"terminal_view",
"theme",
+ "tree-sitter",
+ "tree-sitter-json",
"ui",
"unindent",
"util",
@@ -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")));
- // };
-
- // 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())?;
-
- // 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();
-
- // 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?;
- // }
-
- // 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?;
-
- // 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 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);
+
+ 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_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,
+ settings::initial_local_debug_tasks_content()
+ .to_string()
+ .as_bytes(),
+ )
+ .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<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;
- // };
-
- // self.save_scenario_state = Some(SaveScenarioState::Saving);
-
- // cx.spawn(async move |this, cx| {
- // 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)))
- // }
- // 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 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);
+
+ 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;
+ };
+
+ 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();
+ }
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),
+ )
}
}