new_process_modal.rs

  1use dap::DapRegistry;
  2use editor::Editor;
  3use gpui::{BackgroundExecutor, TestAppContext, VisualTestContext};
  4use project::{FakeFs, Fs as _, Project};
  5use serde_json::json;
  6use std::sync::Arc;
  7use std::sync::atomic::{AtomicBool, Ordering};
  8use task::{DebugRequest, DebugScenario, LaunchRequest, TaskContext, VariableName, ZedDebugConfig};
  9use text::Point;
 10use util::path;
 11
 12use crate::NewProcessMode;
 13use crate::new_process_modal::NewProcessModal;
 14use crate::tests::{init_test, init_test_workspace};
 15
 16#[gpui::test]
 17async fn test_debug_session_substitutes_variables_and_relativizes_paths(
 18    executor: BackgroundExecutor,
 19    cx: &mut TestAppContext,
 20) {
 21    init_test(cx);
 22
 23    let fs = FakeFs::new(executor.clone());
 24    fs.insert_tree(
 25        path!("/project"),
 26        json!({
 27            "main.rs": "fn main() {}"
 28        }),
 29    )
 30    .await;
 31
 32    let project = Project::test(fs, [path!("/project").as_ref()], cx).await;
 33    let workspace = init_test_workspace(&project, cx).await;
 34    let cx = &mut VisualTestContext::from_window(*workspace, cx);
 35
 36    let test_variables = vec![(
 37        VariableName::WorktreeRoot,
 38        path!("/test/worktree/path").to_string(),
 39    )]
 40    .into_iter()
 41    .collect();
 42
 43    let task_context = TaskContext {
 44        cwd: None,
 45        task_variables: test_variables,
 46        project_env: Default::default(),
 47    };
 48
 49    let home_dir = paths::home_dir();
 50
 51    let test_cases: Vec<(&'static str, &'static str)> = vec![
 52        // Absolute path - should not be relativized
 53        (
 54            path!("/absolute/path/to/program"),
 55            path!("/absolute/path/to/program"),
 56        ),
 57        // Relative path - should be prefixed with worktree root
 58        (
 59            format!(".{0}src{0}program", std::path::MAIN_SEPARATOR).leak(),
 60            path!("/test/worktree/path/src/program"),
 61        ),
 62        // Home directory path - should be expanded to full home directory path
 63        (
 64            format!("~{0}src{0}program", std::path::MAIN_SEPARATOR).leak(),
 65            home_dir
 66                .join("src")
 67                .join("program")
 68                .to_string_lossy()
 69                .to_string()
 70                .leak(),
 71        ),
 72        // Path with $ZED_WORKTREE_ROOT - should be substituted without double appending
 73        (
 74            format!(
 75                "$ZED_WORKTREE_ROOT{0}src{0}program",
 76                std::path::MAIN_SEPARATOR
 77            )
 78            .leak(),
 79            path!("/test/worktree/path/src/program"),
 80        ),
 81    ];
 82
 83    let called_launch = Arc::new(AtomicBool::new(false));
 84
 85    for (input_path, expected_path) in test_cases {
 86        let _subscription = project::debugger::test::intercept_debug_sessions(cx, {
 87            let called_launch = called_launch.clone();
 88            move |client| {
 89                client.on_request::<dap::requests::Launch, _>({
 90                    let called_launch = called_launch.clone();
 91
 92                    move |_, args| {
 93                        let config = args.raw.as_object().unwrap();
 94
 95                        assert_eq!(
 96                            config["program"].as_str().unwrap(),
 97                            expected_path,
 98                            "Program path was not correctly substituted for input: {}",
 99                            input_path
100                        );
101
102                        assert_eq!(
103                            config["cwd"].as_str().unwrap(),
104                            expected_path,
105                            "CWD path was not correctly substituted for input: {}",
106                            input_path
107                        );
108
109                        let expected_other_field = if input_path.contains("$ZED_WORKTREE_ROOT") {
110                            input_path.replace("$ZED_WORKTREE_ROOT", path!("/test/worktree/path"))
111                        } else {
112                            input_path.to_string()
113                        };
114
115                        assert_eq!(
116                            config["otherField"].as_str().unwrap(),
117                            &expected_other_field,
118                            "Other field was incorrectly modified for input: {}",
119                            input_path
120                        );
121
122                        called_launch.store(true, Ordering::SeqCst);
123
124                        Ok(())
125                    }
126                });
127            }
128        });
129
130        let scenario = DebugScenario {
131            adapter: "fake-adapter".into(),
132            label: "test-debug-session".into(),
133            build: None,
134            config: json!({
135                "request": "launch",
136                "program": input_path,
137                "cwd": input_path,
138                "otherField": input_path
139            }),
140            tcp_connection: None,
141        };
142
143        workspace
144            .update(cx, |workspace, window, cx| {
145                workspace.start_debug_session(
146                    scenario,
147                    task_context.clone(),
148                    None,
149                    None,
150                    window,
151                    cx,
152                )
153            })
154            .unwrap();
155
156        cx.run_until_parked();
157
158        assert!(called_launch.load(Ordering::SeqCst));
159        called_launch.store(false, Ordering::SeqCst);
160    }
161}
162
163#[gpui::test]
164async fn test_save_debug_scenario_to_file(executor: BackgroundExecutor, cx: &mut TestAppContext) {
165    init_test(cx);
166
167    let fs = FakeFs::new(executor.clone());
168    fs.insert_tree(
169        path!("/project"),
170        json!({
171            "main.rs": "fn main() {}"
172        }),
173    )
174    .await;
175
176    let project = Project::test(fs.clone(), [path!("/project").as_ref()], cx).await;
177    let workspace = init_test_workspace(&project, cx).await;
178    let cx = &mut VisualTestContext::from_window(*workspace, cx);
179
180    workspace
181        .update(cx, |workspace, window, cx| {
182            NewProcessModal::show(workspace, window, NewProcessMode::Debug, None, cx);
183        })
184        .unwrap();
185
186    cx.run_until_parked();
187
188    let modal = workspace
189        .update(cx, |workspace, _, cx| {
190            workspace.active_modal::<NewProcessModal>(cx)
191        })
192        .unwrap()
193        .expect("Modal should be active");
194
195    modal.update_in(cx, |modal, window, cx| {
196        modal.set_configure("/project/main", "/project", false, window, cx);
197        modal.save_debug_scenario(window, cx);
198    });
199
200    cx.executor().run_until_parked();
201
202    let editor = workspace
203        .update(cx, |workspace, _window, cx| {
204            workspace.active_item_as::<Editor>(cx).unwrap()
205        })
206        .unwrap();
207
208    let debug_json_content = fs
209        .load(path!("/project/.zed/debug.json").as_ref())
210        .await
211        .expect("debug.json should exist")
212        .lines()
213        .filter(|line| !line.starts_with("//"))
214        .collect::<Vec<_>>()
215        .join("\n");
216
217    let expected_content = indoc::indoc! {r#"
218        [
219          {
220            "adapter": "fake-adapter",
221            "label": "main (fake-adapter)",
222            "request": "launch",
223            "program": "/project/main",
224            "cwd": "/project",
225            "args": [],
226            "env": {}
227          }
228        ]"#};
229
230    pretty_assertions::assert_eq!(expected_content, debug_json_content);
231
232    editor.update(cx, |editor, cx| {
233        assert_eq!(
234            editor
235                .selections
236                .newest::<Point>(&editor.display_snapshot(cx))
237                .head(),
238            Point::new(5, 2)
239        )
240    });
241
242    modal.update_in(cx, |modal, window, cx| {
243        modal.set_configure("/project/other", "/project", true, window, cx);
244        modal.save_debug_scenario(window, cx);
245    });
246
247    cx.executor().run_until_parked();
248
249    let expected_content = indoc::indoc! {r#"
250        [
251          {
252            "adapter": "fake-adapter",
253            "label": "main (fake-adapter)",
254            "request": "launch",
255            "program": "/project/main",
256            "cwd": "/project",
257            "args": [],
258            "env": {}
259          },
260          {
261            "adapter": "fake-adapter",
262            "label": "other (fake-adapter)",
263            "request": "launch",
264            "program": "/project/other",
265            "cwd": "/project",
266            "args": [],
267            "env": {}
268          }
269        ]"#};
270
271    let debug_json_content = fs
272        .load(path!("/project/.zed/debug.json").as_ref())
273        .await
274        .expect("debug.json should exist")
275        .lines()
276        .filter(|line| !line.starts_with("//"))
277        .collect::<Vec<_>>()
278        .join("\n");
279    pretty_assertions::assert_eq!(expected_content, debug_json_content);
280}
281
282#[gpui::test]
283async fn test_debug_modal_subtitles_with_multiple_worktrees(
284    executor: BackgroundExecutor,
285    cx: &mut TestAppContext,
286) {
287    init_test(cx);
288
289    let fs = FakeFs::new(executor.clone());
290
291    fs.insert_tree(
292        path!("/workspace1"),
293        json!({
294            ".zed": {
295                "debug.json": r#"[
296                    {
297                        "adapter": "fake-adapter",
298                        "label": "Debug App 1",
299                        "request": "launch",
300                        "program": "./app1",
301                        "cwd": "."
302                    },
303                    {
304                        "adapter": "fake-adapter",
305                        "label": "Debug Tests 1",
306                        "request": "launch",
307                        "program": "./test1",
308                        "cwd": "."
309                    }
310                ]"#
311            },
312            "main.rs": "fn main() {}"
313        }),
314    )
315    .await;
316
317    let project = Project::test(fs.clone(), [path!("/workspace1").as_ref()], cx).await;
318
319    let workspace = init_test_workspace(&project, cx).await;
320    let cx = &mut VisualTestContext::from_window(*workspace, cx);
321
322    workspace
323        .update(cx, |workspace, window, cx| {
324            NewProcessModal::show(workspace, window, NewProcessMode::Debug, None, cx);
325        })
326        .unwrap();
327
328    cx.run_until_parked();
329
330    let modal = workspace
331        .update(cx, |workspace, _, cx| {
332            workspace.active_modal::<NewProcessModal>(cx)
333        })
334        .unwrap()
335        .expect("Modal should be active");
336
337    cx.executor().run_until_parked();
338
339    let subtitles = modal.update_in(cx, |modal, _, cx| {
340        modal.debug_picker_candidate_subtitles(cx)
341    });
342
343    assert_eq!(
344        subtitles.as_slice(),
345        [path!(".zed/debug.json"), path!(".zed/debug.json")]
346    );
347}
348
349#[gpui::test]
350async fn test_dap_adapter_config_conversion_and_validation(cx: &mut TestAppContext) {
351    init_test(cx);
352
353    let mut expected_adapters = vec![
354        "CodeLLDB",
355        "Debugpy",
356        "JavaScript",
357        "Delve",
358        "GDB",
359        "fake-adapter",
360    ];
361
362    let adapter_names = cx.update(|cx| {
363        let registry = DapRegistry::global(cx);
364        registry.enumerate_adapters::<Vec<_>>()
365    });
366
367    let zed_config = ZedDebugConfig {
368        label: "test_debug_session".into(),
369        adapter: "test_adapter".into(),
370        request: DebugRequest::Launch(LaunchRequest {
371            program: "test_program".into(),
372            cwd: None,
373            args: vec![],
374            env: Default::default(),
375        }),
376        stop_on_entry: Some(true),
377    };
378
379    for adapter_name in adapter_names {
380        let adapter_str = adapter_name.to_string();
381        if let Some(pos) = expected_adapters.iter().position(|&x| x == adapter_str) {
382            expected_adapters.remove(pos);
383        }
384
385        let adapter = cx
386            .update(|cx| {
387                let registry = DapRegistry::global(cx);
388                registry.adapter(adapter_name.as_ref())
389            })
390            .unwrap_or_else(|| panic!("Adapter {} should exist", adapter_name));
391
392        let mut adapter_specific_config = zed_config.clone();
393        adapter_specific_config.adapter = adapter_name.to_string().into();
394
395        let debug_scenario = adapter
396            .config_from_zed_format(adapter_specific_config)
397            .await
398            .unwrap_or_else(|_| {
399                panic!(
400                    "Adapter {} should successfully convert from Zed format",
401                    adapter_name
402                )
403            });
404
405        assert!(
406            debug_scenario.config.is_object(),
407            "Adapter {} should produce a JSON object for config",
408            adapter_name
409        );
410
411        let request_type = adapter
412            .request_kind(&debug_scenario.config)
413            .await
414            .unwrap_or_else(|_| {
415                panic!(
416                    "Adapter {} should validate the config successfully",
417                    adapter_name
418                )
419            });
420
421        match request_type {
422            dap::StartDebuggingRequestArgumentsRequest::Launch => {}
423            dap::StartDebuggingRequestArgumentsRequest::Attach => {
424                panic!(
425                    "Expected Launch request but got Attach for adapter {}",
426                    adapter_name
427                );
428            }
429        }
430    }
431
432    assert!(
433        expected_adapters.is_empty(),
434        "The following expected adapters were not found in the registry: {:?}",
435        expected_adapters
436    );
437}