new_process_modal.rs

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