diff --git a/crates/debugger_ui/src/new_session_modal.rs b/crates/debugger_ui/src/new_session_modal.rs index 928a63cd0e425da3669d05d097a9472eb41c3380..b27af9f8760ca3336e0e1f0b42341e85beb0bcc9 100644 --- a/crates/debugger_ui/src/new_session_modal.rs +++ b/crates/debugger_ui/src/new_session_modal.rs @@ -1,5 +1,5 @@ use collections::FxHashMap; -use language::LanguageRegistry; +use language::{LanguageRegistry, Point, Selection}; use std::{ borrow::Cow, ops::Not, @@ -12,7 +12,7 @@ use std::{ use dap::{ DapRegistry, DebugRequest, TelemetrySpawnLocation, adapters::DebugAdapterName, send_telemetry, }; -use editor::{Editor, EditorElement, EditorStyle}; +use editor::{Anchor, Editor, EditorElement, EditorStyle, scroll::Autoscroll}; use fuzzy::{StringMatch, StringMatchCandidate}; use gpui::{ Animation, AnimationExt as _, App, AppContext, DismissEvent, Entity, EventEmitter, FocusHandle, @@ -37,7 +37,7 @@ use crate::{attach_modal::AttachModal, debugger_panel::DebugPanel}; enum SaveScenarioState { Saving, - Saved(ProjectPath), + Saved((ProjectPath, SharedString)), Failed(SharedString), } @@ -284,6 +284,177 @@ impl NewSessionModal { self.launch_picker.read(cx).delegate.task_contexts.clone() } + fn save_debug_scenario(&mut self, window: &mut Window, cx: &mut Context) { + 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 render_save_state(&self, cx: &mut Context) -> impl IntoElement { + let this_entity = cx.weak_entity().clone(); + + div().when_some(self.save_scenario_state.as_ref(), { + let this_entity = this_entity.clone(); + + move |this, save_state| match save_state { + SaveScenarioState::Saved((saved_path, scenario_label)) => this.child( + IconButton::new("new-session-modal-go-to-file", IconName::ArrowUpRight) + .icon_size(IconSize::Small) + .icon_color(Color::Muted) + .on_click({ + let this_entity = this_entity.clone(); + let saved_path = saved_path.clone(); + let scenario_label = scenario_label.clone(); + move |_, window, cx| { + window + .spawn(cx, { + let this_entity = this_entity.clone(); + let saved_path = saved_path.clone(); + let scenario_label = scenario_label.clone(); + + async move |cx| { + let editor = this_entity + .update_in(cx, |this, window, cx| { + this.workspace.update(cx, |workspace, cx| { + workspace.open_path( + saved_path.clone(), + None, + true, + window, + cx, + ) + }) + })?? + .await?; + + cx.update(|window, cx| { + if let Some(editor) = editor.act_as::(cx) { + editor.update(cx, |editor, cx| { + let row = editor + .text(cx) + .lines() + .enumerate() + .find_map(|(row, text)| { + if text.contains( + scenario_label.as_ref(), + ) { + Some(row) + } else { + None + } + })?; + + let buffer = editor.buffer().read(cx); + let excerpt_id = + *buffer.excerpt_ids().first()?; + + let snapshot = buffer + .as_singleton()? + .read(cx) + .snapshot(); + + let anchor = snapshot.anchor_before( + Point::new(row as u32, 0), + ); + + let anchor = Anchor { + buffer_id: anchor.buffer_id, + excerpt_id, + text_anchor: anchor, + diff_base_anchor: None, + }; + + editor.change_selections( + Some(Autoscroll::center()), + window, + cx, + |selections| { + let id = + selections.new_selection_id(); + selections.select_anchors( + vec![Selection { + id, + start: anchor, + end: anchor, + reversed: false, + goal: language::SelectionGoal::None + }], + ); + }, + ); + + Some(()) + }); + } + })?; + + this_entity + .update(cx, |_, cx| cx.emit(DismissEvent)) + .ok(); + + anyhow::Ok(()) + } + }) + .detach(); + } + }), + ), + SaveScenarioState::Saving => this.child( + Icon::new(IconName::Spinner) + .size(IconSize::Small) + .color(Color::Muted) + .with_animation( + "Spinner", + Animation::new(Duration::from_secs(3)).repeat(), + |icon, delta| icon.transform(Transformation::rotate(percentage(delta))), + ), + ), + SaveScenarioState::Failed(error_msg) => this.child( + IconButton::new("Failed Scenario Saved", IconName::X) + .icon_size(IconSize::Small) + .icon_color(Color::Error) + .tooltip(ui::Tooltip::text(error_msg.clone())), + ), + } + }) + } + fn adapter_drop_down_menu( &mut self, window: &mut Window, @@ -355,7 +526,7 @@ impl NewSessionModal { static SELECT_DEBUGGER_LABEL: SharedString = SharedString::new_static("Select Debugger"); #[derive(Clone)] -enum NewSessionMode { +pub(crate) enum NewSessionMode { Custom, Attach, Launch, @@ -423,8 +594,6 @@ impl Render for NewSessionModal { window: &mut ui::Window, cx: &mut ui::Context, ) -> impl ui::IntoElement { - let this = cx.weak_entity().clone(); - v_flex() .size_full() .w(rems(34.)) @@ -534,58 +703,7 @@ impl Render for NewSessionModal { .child( Button::new("new-session-modal-back", "Save to .zed/debug.json...") .on_click(cx.listener(|this, _, window, cx| { - let Some(save_scenario) = this - .debugger - .as_ref() - .and_then(|debugger| this.debug_scenario(&debugger, cx)) - .zip( - this.task_contexts(cx) - .and_then(|tcx| tcx.worktree()), - ) - .and_then(|(scenario, worktree_id)| { - this.debug_panel - .update(cx, |panel, cx| { - panel.save_scenario( - &scenario, - worktree_id, - window, - cx, - ) - }) - .ok() - }) - else { - return; - }; - - this.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)) - } - Err(error) => { - this.save_scenario_state = - Some(SaveScenarioState::Failed( - error.to_string().into(), - )) - } - }) - .ok(); - - cx.background_executor() - .timer(Duration::from_secs(2)) - .await; - this.update(cx, |this, _| { - this.save_scenario_state.take() - }) - .ok(); - }) - .detach(); + this.save_debug_scenario(window, cx); })) .disabled( self.debugger.is_none() @@ -598,83 +716,7 @@ impl Render for NewSessionModal { || self.save_scenario_state.is_some(), ), ) - .when_some(self.save_scenario_state.as_ref(), { - let this_entity = this.clone(); - - move |this, save_state| match save_state { - SaveScenarioState::Saved(saved_path) => this.child( - IconButton::new( - "new-session-modal-go-to-file", - IconName::ArrowUpRight, - ) - .icon_size(IconSize::Small) - .icon_color(Color::Muted) - .on_click({ - let this_entity = this_entity.clone(); - let saved_path = saved_path.clone(); - move |_, window, cx| { - window - .spawn(cx, { - let this_entity = this_entity.clone(); - let saved_path = saved_path.clone(); - - async move |cx| { - this_entity - .update_in( - cx, - |this, window, cx| { - this.workspace.update( - cx, - |workspace, cx| { - workspace.open_path( - saved_path - .clone(), - None, - true, - window, - cx, - ) - }, - ) - }, - )?? - .await?; - - this_entity - .update(cx, |_, cx| { - cx.emit(DismissEvent) - }) - .ok(); - - anyhow::Ok(()) - } - }) - .detach(); - } - }), - ), - SaveScenarioState::Saving => this.child( - Icon::new(IconName::Spinner) - .size(IconSize::Small) - .color(Color::Muted) - .with_animation( - "Spinner", - Animation::new(Duration::from_secs(3)).repeat(), - |icon, delta| { - icon.transform(Transformation::rotate( - percentage(delta), - )) - }, - ), - ), - SaveScenarioState::Failed(error_msg) => this.child( - IconButton::new("Failed Scenario Saved", IconName::X) - .icon_size(IconSize::Small) - .icon_color(Color::Error) - .tooltip(ui::Tooltip::text(error_msg.clone())), - ), - } - }), + .child(self.render_save_state(cx)), }) .child( Button::new("debugger-spawn", "Start") @@ -1162,6 +1204,42 @@ pub(crate) fn resolve_path(path: &mut String) { }; } +#[cfg(test)] +impl NewSessionModal { + pub(crate) fn set_custom( + &mut self, + program: impl AsRef, + cwd: impl AsRef, + stop_on_entry: bool, + window: &mut Window, + cx: &mut Context, + ) { + self.mode = NewSessionMode::Custom; + self.debugger = Some(dap::adapters::DebugAdapterName("fake-adapter".into())); + + self.custom_mode.update(cx, |custom, cx| { + custom.program.update(cx, |editor, cx| { + editor.clear(window, cx); + editor.set_text(program.as_ref(), window, cx); + }); + + custom.cwd.update(cx, |editor, cx| { + editor.clear(window, cx); + editor.set_text(cwd.as_ref(), window, cx); + }); + + custom.stop_on_entry = match stop_on_entry { + true => ToggleState::Selected, + _ => ToggleState::Unselected, + } + }) + } + + pub(crate) fn save_scenario(&mut self, window: &mut Window, cx: &mut Context) { + self.save_debug_scenario(window, cx); + } +} + #[cfg(test)] mod tests { use paths::home_dir; diff --git a/crates/debugger_ui/src/tests/new_session_modal.rs b/crates/debugger_ui/src/tests/new_session_modal.rs index 4088248a6fd33584bac4543c8c7891bebd589dbb..ffdce0dbc45cec662eadb35c96adee134dc1b436 100644 --- a/crates/debugger_ui/src/tests/new_session_modal.rs +++ b/crates/debugger_ui/src/tests/new_session_modal.rs @@ -1,6 +1,6 @@ use dap::DapRegistry; use gpui::{BackgroundExecutor, TestAppContext, VisualTestContext}; -use project::{FakeFs, Project}; +use project::{FakeFs, Fs, Project}; use serde_json::json; use std::sync::Arc; use std::sync::atomic::{AtomicBool, Ordering}; @@ -151,6 +151,106 @@ 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_session_modal::NewSessionModal::show(workspace, window, cx); + }) + .unwrap(); + + cx.run_until_parked(); + + let modal = workspace + .update(cx, |workspace, _, cx| { + workspace.active_modal::(cx) + }) + .unwrap() + .expect("Modal should be active"); + + modal.update_in(cx, |modal, window, cx| { + modal.set_custom("/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_custom("/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_dap_adapter_config_conversion_and_validation(cx: &mut TestAppContext) { init_test(cx); diff --git a/crates/tasks_ui/src/tasks_ui.rs b/crates/tasks_ui/src/tasks_ui.rs index 7e976184c0a92055ac53d19f2d30e782f540722b..2857eef481161c2fdcf576846eccf17c39acbb57 100644 --- a/crates/tasks_ui/src/tasks_ui.rs +++ b/crates/tasks_ui/src/tasks_ui.rs @@ -270,7 +270,11 @@ pub fn task_contexts( .read(cx) .worktree_for_id(*worktree_id, cx) .map_or(false, |worktree| is_visible_directory(&worktree, cx)) - }); + }) + .or(workspace + .visible_worktrees(cx) + .next() + .map(|tree| tree.read(cx).id())); let active_editor = active_item.and_then(|item| item.act_as::(cx));