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, |multi, window, cx| {
149                multi.workspace().update(cx, |workspace, cx| {
150                    workspace.start_debug_session(
151                        scenario,
152                        task_context.clone(),
153                        None,
154                        None,
155                        window,
156                        cx,
157                    );
158                })
159            })
160            .unwrap();
161
162        cx.run_until_parked();
163
164        assert!(called_launch.load(Ordering::SeqCst));
165        called_launch.store(false, Ordering::SeqCst);
166    }
167}
168
169#[gpui::test]
170async fn test_save_debug_scenario_to_file(executor: BackgroundExecutor, cx: &mut TestAppContext) {
171    init_test(cx);
172
173    let fs = FakeFs::new(executor.clone());
174    fs.insert_tree(
175        path!("/project"),
176        json!({
177            "main.rs": "fn main() {}"
178        }),
179    )
180    .await;
181
182    let project = Project::test(fs.clone(), [path!("/project").as_ref()], cx).await;
183    let workspace = init_test_workspace(&project, cx).await;
184    let cx = &mut VisualTestContext::from_window(*workspace, cx);
185
186    workspace
187        .update(cx, |multi, window, cx| {
188            multi.workspace().update(cx, |workspace, cx| {
189                NewProcessModal::show(workspace, window, NewProcessMode::Debug, None, cx);
190            });
191        })
192        .unwrap();
193
194    cx.run_until_parked();
195
196    let modal = workspace
197        .update(cx, |workspace, _, cx| {
198            workspace.active_modal::<NewProcessModal>(cx)
199        })
200        .unwrap()
201        .expect("Modal should be active");
202
203    modal.update_in(cx, |modal, window, cx| {
204        modal.set_configure("/project/main", "/project", false, window, cx);
205        modal.save_debug_scenario(window, cx);
206    });
207
208    cx.executor().run_until_parked();
209
210    let editor = workspace
211        .update(cx, |workspace, _window, cx| {
212            workspace.active_item_as::<Editor>(cx).unwrap()
213        })
214        .unwrap();
215
216    let debug_json_content = fs
217        .load(path!("/project/.zed/debug.json").as_ref())
218        .await
219        .expect("debug.json should exist")
220        .lines()
221        .filter(|line| !line.starts_with("//"))
222        .collect::<Vec<_>>()
223        .join("\n");
224
225    let expected_content = indoc::indoc! {r#"
226        [
227          {
228            "adapter": "fake-adapter",
229            "label": "main (fake-adapter)",
230            "request": "launch",
231            "program": "/project/main",
232            "cwd": "/project",
233            "args": [],
234            "env": {}
235          }
236        ]"#};
237
238    pretty_assertions::assert_eq!(expected_content, debug_json_content);
239
240    editor.update(cx, |editor, cx| {
241        assert_eq!(
242            editor
243                .selections
244                .newest::<Point>(&editor.display_snapshot(cx))
245                .head(),
246            Point::new(5, 2)
247        )
248    });
249
250    modal.update_in(cx, |modal, window, cx| {
251        modal.set_configure("/project/other", "/project", true, window, cx);
252        modal.save_debug_scenario(window, cx);
253    });
254
255    cx.executor().run_until_parked();
256
257    let expected_content = indoc::indoc! {r#"
258        [
259          {
260            "adapter": "fake-adapter",
261            "label": "main (fake-adapter)",
262            "request": "launch",
263            "program": "/project/main",
264            "cwd": "/project",
265            "args": [],
266            "env": {}
267          },
268          {
269            "adapter": "fake-adapter",
270            "label": "other (fake-adapter)",
271            "request": "launch",
272            "program": "/project/other",
273            "cwd": "/project",
274            "args": [],
275            "env": {}
276          }
277        ]"#};
278
279    let debug_json_content = fs
280        .load(path!("/project/.zed/debug.json").as_ref())
281        .await
282        .expect("debug.json should exist")
283        .lines()
284        .filter(|line| !line.starts_with("//"))
285        .collect::<Vec<_>>()
286        .join("\n");
287    pretty_assertions::assert_eq!(expected_content, debug_json_content);
288}
289
290#[gpui::test]
291async fn test_debug_modal_subtitles_with_multiple_worktrees(
292    executor: BackgroundExecutor,
293    cx: &mut TestAppContext,
294) {
295    init_test(cx);
296
297    let fs = FakeFs::new(executor.clone());
298
299    fs.insert_tree(
300        path!("/workspace1"),
301        json!({
302            ".zed": {
303                "debug.json": r#"[
304                    {
305                        "adapter": "fake-adapter",
306                        "label": "Debug App 1",
307                        "request": "launch",
308                        "program": "./app1",
309                        "cwd": "."
310                    },
311                    {
312                        "adapter": "fake-adapter",
313                        "label": "Debug Tests 1",
314                        "request": "launch",
315                        "program": "./test1",
316                        "cwd": "."
317                    }
318                ]"#
319            },
320            "main.rs": "fn main() {}"
321        }),
322    )
323    .await;
324
325    let project = Project::test(fs.clone(), [path!("/workspace1").as_ref()], cx).await;
326
327    let workspace = init_test_workspace(&project, cx).await;
328    let cx = &mut VisualTestContext::from_window(*workspace, cx);
329
330    workspace
331        .update(cx, |multi, window, cx| {
332            multi.workspace().update(cx, |workspace, cx| {
333                NewProcessModal::show(workspace, window, NewProcessMode::Debug, None, cx);
334            });
335        })
336        .unwrap();
337
338    cx.run_until_parked();
339
340    let modal = workspace
341        .update(cx, |workspace, _, cx| {
342            workspace.active_modal::<NewProcessModal>(cx)
343        })
344        .unwrap()
345        .expect("Modal should be active");
346
347    cx.executor().run_until_parked();
348
349    let subtitles = modal.update_in(cx, |modal, _, cx| {
350        modal.debug_picker_candidate_subtitles(cx)
351    });
352
353    assert_eq!(
354        subtitles.as_slice(),
355        [path!(".zed/debug.json"), path!(".zed/debug.json")]
356    );
357}
358
359#[gpui::test]
360async fn test_dap_adapter_config_conversion_and_validation(cx: &mut TestAppContext) {
361    init_test(cx);
362
363    let mut expected_adapters = vec![
364        "CodeLLDB",
365        "Debugpy",
366        "JavaScript",
367        "Delve",
368        "GDB",
369        "fake-adapter",
370    ];
371
372    let adapter_names = cx.update(|cx| {
373        let registry = DapRegistry::global(cx);
374        registry.enumerate_adapters::<Vec<_>>()
375    });
376
377    let zed_config = ZedDebugConfig {
378        label: "test_debug_session".into(),
379        adapter: "test_adapter".into(),
380        request: DebugRequest::Launch(LaunchRequest {
381            program: "test_program".into(),
382            cwd: None,
383            args: vec![],
384            env: Default::default(),
385        }),
386        stop_on_entry: Some(true),
387    };
388
389    for adapter_name in adapter_names {
390        let adapter_str = adapter_name.to_string();
391        if let Some(pos) = expected_adapters.iter().position(|&x| x == adapter_str) {
392            expected_adapters.remove(pos);
393        }
394
395        let adapter = cx
396            .update(|cx| {
397                let registry = DapRegistry::global(cx);
398                registry.adapter(adapter_name.as_ref())
399            })
400            .unwrap_or_else(|| panic!("Adapter {} should exist", adapter_name));
401
402        let mut adapter_specific_config = zed_config.clone();
403        adapter_specific_config.adapter = adapter_name.to_string().into();
404
405        let debug_scenario = adapter
406            .config_from_zed_format(adapter_specific_config)
407            .await
408            .unwrap_or_else(|_| {
409                panic!(
410                    "Adapter {} should successfully convert from Zed format",
411                    adapter_name
412                )
413            });
414
415        assert!(
416            debug_scenario.config.is_object(),
417            "Adapter {} should produce a JSON object for config",
418            adapter_name
419        );
420
421        let request_type = adapter
422            .request_kind(&debug_scenario.config)
423            .await
424            .unwrap_or_else(|_| {
425                panic!(
426                    "Adapter {} should validate the config successfully",
427                    adapter_name
428                )
429            });
430
431        match request_type {
432            dap::StartDebuggingRequestArgumentsRequest::Launch => {}
433            dap::StartDebuggingRequestArgumentsRequest::Attach => {
434                panic!(
435                    "Expected Launch request but got Attach for adapter {}",
436                    adapter_name
437                );
438            }
439        }
440    }
441
442    assert!(
443        expected_adapters.is_empty(),
444        "The following expected adapters were not found in the registry: {:?}",
445        expected_adapters
446    );
447}