Detailed changes
@@ -5,9 +5,7 @@
"build": {
"label": "Build Zed",
"command": "cargo",
- "args": [
- "build"
- ]
+ "args": ["build"]
}
},
{
@@ -16,9 +14,7 @@
"build": {
"label": "Build Zed",
"command": "cargo",
- "args": [
- "build"
- ]
+ "args": ["build"]
}
- },
+ }
]
@@ -4324,6 +4324,7 @@ dependencies = [
"futures 0.3.31",
"fuzzy",
"gpui",
+ "indoc",
"itertools 0.14.0",
"language",
"log",
@@ -4344,6 +4345,7 @@ dependencies = [
"tasks_ui",
"telemetry",
"terminal_view",
+ "text",
"theme",
"tree-sitter",
"tree-sitter-go",
@@ -40,6 +40,7 @@ file_icons.workspace = true
futures.workspace = true
fuzzy.workspace = true
gpui.workspace = true
+indoc.workspace = true
itertools.workspace = true
language.workspace = true
log.workspace = true
@@ -60,6 +61,7 @@ task.workspace = true
tasks_ui.workspace = true
telemetry.workspace = true
terminal_view.workspace = true
+text.workspace = true
theme.workspace = true
tree-sitter.workspace = true
tree-sitter-json.workspace = true
@@ -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
.matches
.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, _| {
registry.adapter(&self.definition.adapter)
}) else {
return;
};
- let workspace = self.workspace.clone();
let definition = self.definition.clone();
cx.spawn_in(window, async move |this, cx| {
let Ok(scenario) = adapter.config_from_zed_format(definition).await else {
return;
};
- let panel = workspace
- .update(cx, |workspace, cx| workspace.panel::<DebugPanel>(cx))
- .ok()
- .flatten();
- if let Some(panel) = panel {
- panel
- .update_in(cx, |panel, window, cx| {
- panel.start_session(scenario, Default::default(), None, None, window, cx);
- })
- .ok();
- }
+ panel
+ .update_in(cx, |panel, window, cx| {
+ panel.start_session(scenario, Default::default(), None, None, window, cx);
+ })
+ .ok();
this.update(cx, |_, cx| {
cx.emit(DismissEvent);
})
@@ -16,16 +16,18 @@ use dap::{
client::SessionId, debugger_settings::DebuggerSettings,
};
use dap::{DapRegistry, StartDebuggingRequestArguments};
+use editor::Editor;
use gpui::{
Action, App, AsyncWindowContext, ClipboardItem, Context, DismissEvent, Entity, EntityId,
EventEmitter, FocusHandle, Focusable, MouseButton, MouseDownEvent, Point, Subscription, Task,
WeakEntity, anchored, deferred,
};
+use text::ToPoint as _;
use itertools::Itertools as _;
use language::Buffer;
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 rpc::proto::{self};
use settings::Settings;
@@ -35,8 +37,9 @@ use tree_sitter::{Query, StreamingIterator as _};
use ui::{ContextMenu, Divider, PopoverMenuHandle, Tooltip, prelude::*};
use util::{ResultExt, maybe};
use workspace::SplitDirection;
+use workspace::item::SaveOptions;
use workspace::{
- Pane, Workspace,
+ Item, Pane, Workspace,
dock::{DockPosition, Panel, PanelEvent},
};
use zed_actions::ToggleFocus;
@@ -988,13 +991,90 @@ impl DebugPanel {
cx.notify();
}
+ pub(crate) fn go_to_scenario_definition(
+ &self,
+ kind: TaskSourceKind,
+ scenario: DebugScenario,
+ worktree_id: WorktreeId,
+ window: &mut Window,
+ cx: &mut Context<Self>,
+ ) -> 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,
+ scenario: DebugScenario,
worktree_id: WorktreeId,
window: &mut Window,
- cx: &mut App,
- ) -> Task<Result<ProjectPath>> {
+ cx: &mut Context<Self>,
+ ) -> Task<Result<()>> {
+ let this = cx.weak_entity();
+ let project = self.project.clone();
self.workspace
.update(cx, |workspace, cx| {
let Some(mut path) = workspace.absolute_path_of_worktree(worktree_id, cx) else {
@@ -1027,47 +1107,7 @@ impl DebugPanel {
)
.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| {
+ let project_path = workspace.update(cx, |workspace, cx| {
workspace
.project()
.read(cx)
@@ -1075,12 +1115,113 @@ impl DebugPanel {
.context(
"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)))
}
+ 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>) {
self.thread_picker_menu_handle.toggle(window, cx);
}
@@ -1,12 +1,10 @@
-use anyhow::bail;
+use anyhow::{Context as _, bail};
use collections::{FxHashMap, HashMap};
use language::LanguageRegistry;
-use paths::local_debug_file_relative_path;
use std::{
borrow::Cow,
path::{Path, PathBuf},
sync::Arc,
- time::Duration,
usize,
};
use tasks_ui::{TaskOverrides, TasksModal};
@@ -18,35 +16,27 @@ use editor::{Editor, EditorElement, EditorStyle};
use fuzzy::{StringMatch, StringMatchCandidate};
use gpui::{
Action, App, AppContext, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable,
- HighlightStyle, InteractiveText, KeyContext, PromptButton, PromptLevel, Render, StyledText,
- Subscription, Task, TextStyle, UnderlineStyle, WeakEntity,
+ KeyContext, Render, Subscription, Task, TextStyle, WeakEntity,
};
use itertools::Itertools as _;
use picker::{Picker, PickerDelegate, highlighted_match_with_paths::HighlightedMatch};
-use project::{
- DebugScenarioContext, ProjectPath, TaskContexts, TaskSourceKind, task_store::TaskStore,
-};
-use settings::{Settings, initial_local_debug_tasks_content};
+use project::{DebugScenarioContext, TaskContexts, TaskSourceKind, task_store::TaskStore};
+use settings::Settings;
use task::{DebugScenario, RevealTarget, ZedDebugConfig};
use theme::ThemeSettings;
use ui::{
- ActiveTheme, CheckboxWithLabel, Clickable, Context, ContextMenu, Disableable, DropdownMenu,
- FluentBuilder, IconWithIndicator, Indicator, IntoElement, KeyBinding, ListItem,
- ListItemSpacing, ParentElement, StyledExt, ToggleButton, ToggleState, Toggleable, Tooltip,
- Window, div, prelude::*, px, relative, rems,
+ ActiveTheme, Button, ButtonCommon, ButtonSize, CheckboxWithLabel, Clickable, Color, Context,
+ ContextMenu, Disableable, DropdownMenu, FluentBuilder, Icon, IconName, IconSize,
+ IconWithIndicator, Indicator, InteractiveElement, IntoElement, KeyBinding, Label,
+ 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 workspace::{ModalView, Workspace, pane};
+use workspace::{ModalView, Workspace, notifications::DetachAndPromptErr, pane};
use crate::{attach_modal::AttachModal, debugger_panel::DebugPanel};
-#[allow(unused)]
-enum SaveScenarioState {
- Saving,
- Saved((ProjectPath, SharedString)),
- Failed(SharedString),
-}
-
pub(super) struct NewProcessModal {
workspace: WeakEntity<Workspace>,
debug_panel: WeakEntity<DebugPanel>,
@@ -56,7 +46,6 @@ pub(super) struct NewProcessModal {
configure_mode: Entity<ConfigureMode>,
task_mode: TaskMode,
debugger: Option<DebugAdapterName>,
- save_scenario_state: Option<SaveScenarioState>,
_subscriptions: [Subscription; 3],
}
@@ -268,7 +257,6 @@ impl NewProcessModal {
mode,
debug_panel: debug_panel.downgrade(),
workspace: workspace_handle,
- save_scenario_state: None,
_subscriptions,
}
});
@@ -420,63 +408,29 @@ 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 task_contents = self.task_contexts(cx);
+ pub fn save_debug_scenario(&mut self, window: &mut Window, cx: &mut Context<Self>) {
+ let task_contexts = 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()
+ let scenario = scenario.await.context("no scenario to save")?;
+ let worktree_id = task_contexts
+ .context("no task contexts")?
+ .worktree()
+ .context("no active worktree")?;
+ this.update_in(cx, |this, window, cx| {
+ this.debug_panel.update(cx, |panel, cx| {
+ panel.save_scenario(scenario, worktree_id, window, cx)
})
- .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()))
- }
+ })??
+ .await?;
+ this.update_in(cx, |_, _, cx| {
+ cx.emit(DismissEvent);
})
- .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(
@@ -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");
@@ -812,39 +702,21 @@ impl Render for NewProcessModal {
NewProcessMode::Launch => el.child(
container
.child(
- h_flex()
- .text_ui_sm(cx)
- .text_color(Color::Muted.color(cx))
- .child(
- InteractiveText::new(
- "open-debug-json",
- StyledText::new(
- "Open .zed/debug.json for advanced configuration.",
- )
- .with_highlights([(
- 5..20,
- HighlightStyle {
- underline: Some(UnderlineStyle {
- 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);
- })
- }
- },
+ h_flex().child(
+ Button::new("edit-custom-debug", "Edit in debug.json")
+ .on_click(cx.listener(|this, _, window, cx| {
+ this.save_debug_scenario(window, cx);
+ }))
+ .disabled(
+ self.debugger.is_none()
+ || self
+ .configure_mode
+ .read(cx)
+ .program
+ .read(cx)
+ .is_empty(cx),
),
- ),
+ ),
)
.child(
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
- .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(
- Button::new("debugger-spawn", "Start")
- .on_click(cx.listener(|this, _, window, cx| {
- this.start_new_session(window, cx)
- }))
- .disabled(
- self.debugger.is_none()
- || self
- .attach_mode
- .read(cx)
- .attach_picker
- .read(cx)
- .picker
- .read(cx)
- .delegate
- .match_count()
- == 0,
+ h_flex()
+ .child(div().child(self.adapter_drop_down_menu(window, cx)))
+ .child(
+ Button::new("debugger-spawn", "Start")
+ .on_click(cx.listener(|this, _, window, cx| {
+ this.start_new_session(window, cx)
+ }))
+ .disabled(disabled),
),
- ),
- ),
+ )
+ }),
NewProcessMode::Debug => el,
NewProcessMode::Task => el,
}
@@ -1048,25 +939,6 @@ impl ConfigureMode {
)
.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(
- &mut self,
- _secondary: bool,
- window: &mut Window,
- cx: &mut Context<Picker<Self>>,
- ) {
+ fn confirm_input(&mut self, _: bool, window: &mut Window, cx: &mut Context<Picker<Self>>) {
let text = self.prompt.clone();
let (task_context, worktree_id) = self
.task_contexts
@@ -1364,7 +1231,7 @@ impl PickerDelegate for DebugDelegate {
let args = args.collect::<Vec<_>>();
let task = task::TaskTemplate {
- label: "one-off".to_owned(),
+ label: "one-off".to_owned(), // TODO: rename using command as label
env,
command: program,
args,
@@ -1405,7 +1272,11 @@ impl PickerDelegate for DebugDelegate {
.background_spawn(async move {
for locator in locators {
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);
}
@@ -1439,13 +1310,18 @@ impl PickerDelegate for DebugDelegate {
.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
.matches
.get(self.selected_index())
.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;
};
@@ -1463,24 +1339,38 @@ impl PickerDelegate for DebugDelegate {
});
let DebugScenarioContext {
task_context,
- active_buffer,
+ active_buffer: _,
worktree_id,
} = context;
- let active_buffer = active_buffer.and_then(|buffer| buffer.upgrade());
-
- send_telemetry(&debug_scenario, TelemetrySpawnLocation::ScenarioList, cx);
- self.debug_panel
- .update(cx, |panel, cx| {
- panel.start_session(
- debug_scenario,
- task_context,
- active_buffer,
- worktree_id,
- window,
- cx,
- );
+
+ if secondary {
+ let Some(kind) = kind else { return };
+ let Some(id) = worktree_id else { return };
+ let debug_panel = self.debug_panel.clone();
+ cx.spawn_in(window, async move |_, cx| {
+ debug_panel
+ .update_in(cx, |debug_panel, window, cx| {
+ debug_panel.go_to_scenario_definition(kind, debug_scenario, id, 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);
}
@@ -1498,19 +1388,23 @@ impl PickerDelegate for DebugDelegate {
let footer = h_flex()
.w_full()
.p_1p5()
- .justify_end()
+ .justify_between()
.border_t_1()
.border_color(cx.theme().colors().border_variant)
- // .child(
- // // TODO: add button to open selected task in debug.json
- // h_flex().into_any_element(),
- // )
+ .children({
+ let action = menu::SecondaryConfirm.boxed_clone();
+ 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| {
if (current_modifiers.alt || self.matches.is_empty()) && !self.prompt.is_empty() {
- let action = picker::ConfirmInput {
- secondary: current_modifiers.secondary(),
- }
- .boxed_clone();
+ let action = picker::ConfirmInput { secondary: false }.boxed_clone();
this.children(KeyBinding::for_action(&*action, window, cx).map(|keybind| {
Button::new("launch-custom", "Launch Custom")
.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,
+ }
+ })
+ }
+}
@@ -1,13 +1,15 @@
use dap::DapRegistry;
+use editor::Editor;
use gpui::{BackgroundExecutor, TestAppContext, VisualTestContext};
-use project::{FakeFs, Project};
+use project::{FakeFs, Fs as _, Project};
use serde_json::json;
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};
use task::{DebugRequest, DebugScenario, LaunchRequest, TaskContext, VariableName, ZedDebugConfig};
+use text::Point;
use util::path;
-// use crate::new_process_modal::NewProcessMode;
+use crate::NewProcessMode;
use crate::tests::{init_test, init_test_workspace};
#[gpui::test]
@@ -159,111 +161,127 @@ async fn test_debug_session_substitutes_variables_and_relativizes_paths(
}
}
-// #[gpui::test]
-// async fn test_save_debug_scenario_to_file(executor: BackgroundExecutor, cx: &mut TestAppContext) {
-// init_test(cx);
-
-// let fs = FakeFs::new(executor.clone());
-// fs.insert_tree(
-// path!("/project"),
-// json!({
-// "main.rs": "fn main() {}"
-// }),
-// )
-// .await;
-
-// let project = Project::test(fs.clone(), [path!("/project").as_ref()], cx).await;
-// let workspace = init_test_workspace(&project, cx).await;
-// let cx = &mut VisualTestContext::from_window(*workspace, cx);
-
-// workspace
-// .update(cx, |workspace, window, cx| {
-// crate::new_process_modal::NewProcessModal::show(
-// workspace,
-// window,
-// NewProcessMode::Debug,
-// None,
-// cx,
-// );
-// })
-// .unwrap();
-
-// cx.run_until_parked();
-
-// let modal = workspace
-// .update(cx, |workspace, _, cx| {
-// workspace.active_modal::<crate::new_process_modal::NewProcessModal>(cx)
-// })
-// .unwrap()
-// .expect("Modal should be active");
-
-// modal.update_in(cx, |modal, window, cx| {
-// modal.set_configure("/project/main", "/project", false, window, cx);
-// modal.save_scenario(window, cx);
-// });
-
-// cx.executor().run_until_parked();
-
-// let debug_json_content = fs
-// .load(path!("/project/.zed/debug.json").as_ref())
-// .await
-// .expect("debug.json should exist");
-
-// let expected_content = vec![
-// "[",
-// " {",
-// r#" "adapter": "fake-adapter","#,
-// r#" "label": "main (fake-adapter)","#,
-// r#" "request": "launch","#,
-// r#" "program": "/project/main","#,
-// r#" "cwd": "/project","#,
-// r#" "args": [],"#,
-// r#" "env": {}"#,
-// " }",
-// "]",
-// ];
-
-// let actual_lines: Vec<&str> = debug_json_content.lines().collect();
-// pretty_assertions::assert_eq!(expected_content, actual_lines);
-
-// modal.update_in(cx, |modal, window, cx| {
-// modal.set_configure("/project/other", "/project", true, window, cx);
-// modal.save_scenario(window, cx);
-// });
-
-// cx.executor().run_until_parked();
-
-// let debug_json_content = fs
-// .load(path!("/project/.zed/debug.json").as_ref())
-// .await
-// .expect("debug.json should exist after second save");
-
-// let expected_content = vec![
-// "[",
-// " {",
-// 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();
-// pretty_assertions::assert_eq!(expected_content, actual_lines);
-// }
+#[gpui::test]
+async fn test_save_debug_scenario_to_file(executor: BackgroundExecutor, cx: &mut TestAppContext) {
+ init_test(cx);
+
+ let fs = FakeFs::new(executor.clone());
+ fs.insert_tree(
+ path!("/project"),
+ json!({
+ "main.rs": "fn main() {}"
+ }),
+ )
+ .await;
+
+ let project = Project::test(fs.clone(), [path!("/project").as_ref()], cx).await;
+ let workspace = init_test_workspace(&project, cx).await;
+ let cx = &mut VisualTestContext::from_window(*workspace, cx);
+
+ workspace
+ .update(cx, |workspace, window, cx| {
+ crate::new_process_modal::NewProcessModal::show(
+ workspace,
+ window,
+ NewProcessMode::Debug,
+ None,
+ cx,
+ );
+ })
+ .unwrap();
+
+ cx.run_until_parked();
+
+ let modal = workspace
+ .update(cx, |workspace, _, cx| {
+ workspace.active_modal::<crate::new_process_modal::NewProcessModal>(cx)
+ })
+ .unwrap()
+ .expect("Modal should be active");
+
+ modal.update_in(cx, |modal, window, cx| {
+ modal.set_configure("/project/main", "/project", false, window, cx);
+ modal.save_debug_scenario(window, cx);
+ });
+
+ cx.executor().run_until_parked();
+
+ let editor = workspace
+ .update(cx, |workspace, _window, cx| {
+ workspace.active_item_as::<Editor>(cx).unwrap()
+ })
+ .unwrap();
+
+ 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");
+
+ let expected_content = indoc::indoc! {r#"
+ [
+ {
+ "adapter": "fake-adapter",
+ "label": "main (fake-adapter)",
+ "request": "launch",
+ "program": "/project/main",
+ "cwd": "/project",
+ "args": [],
+ "env": {}
+ }
+ ]"#};
+
+ pretty_assertions::assert_eq!(expected_content, debug_json_content);
+
+ editor.update(cx, |editor, cx| {
+ assert_eq!(
+ editor.selections.newest::<Point>(cx).head(),
+ Point::new(5, 2)
+ )
+ });
+
+ modal.update_in(cx, |modal, window, cx| {
+ modal.set_configure("/project/other", "/project", true, window, cx);
+ modal.save_debug_scenario(window, cx);
+ });
+
+ cx.executor().run_until_parked();
+
+ let expected_content = indoc::indoc! {r#"
+ [
+ {
+ "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]
async fn test_dap_adapter_config_conversion_and_validation(cx: &mut TestAppContext) {
@@ -2829,6 +2829,7 @@ impl EditorElement {
) -> Vec<AnyElement> {
self.editor.update(cx, |editor, cx| {
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 {
deployed_from,
actions,