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