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.selections.newest::<Point>(cx).head(),
235            Point::new(5, 2)
236        )
237    });
238
239    modal.update_in(cx, |modal, window, cx| {
240        modal.set_configure("/project/other", "/project", true, window, cx);
241        modal.save_debug_scenario(window, cx);
242    });
243
244    cx.executor().run_until_parked();
245
246    let expected_content = indoc::indoc! {r#"
247        [
248          {
249            "adapter": "fake-adapter",
250            "label": "main (fake-adapter)",
251            "request": "launch",
252            "program": "/project/main",
253            "cwd": "/project",
254            "args": [],
255            "env": {}
256          },
257          {
258            "adapter": "fake-adapter",
259            "label": "other (fake-adapter)",
260            "request": "launch",
261            "program": "/project/other",
262            "cwd": "/project",
263            "args": [],
264            "env": {}
265          }
266        ]"#};
267
268    let debug_json_content = fs
269        .load(path!("/project/.zed/debug.json").as_ref())
270        .await
271        .expect("debug.json should exist")
272        .lines()
273        .filter(|line| !line.starts_with("//"))
274        .collect::<Vec<_>>()
275        .join("\n");
276    pretty_assertions::assert_eq!(expected_content, debug_json_content);
277}
278
279#[gpui::test]
280async fn test_debug_modal_subtitles_with_multiple_worktrees(
281    executor: BackgroundExecutor,
282    cx: &mut TestAppContext,
283) {
284    init_test(cx);
285
286    let fs = FakeFs::new(executor.clone());
287
288    fs.insert_tree(
289        path!("/workspace1"),
290        json!({
291            ".zed": {
292                "debug.json": r#"[
293                    {
294                        "adapter": "fake-adapter",
295                        "label": "Debug App 1",
296                        "request": "launch",
297                        "program": "./app1",
298                        "cwd": "."
299                    },
300                    {
301                        "adapter": "fake-adapter",
302                        "label": "Debug Tests 1",
303                        "request": "launch",
304                        "program": "./test1",
305                        "cwd": "."
306                    }
307                ]"#
308            },
309            "main.rs": "fn main() {}"
310        }),
311    )
312    .await;
313
314    let project = Project::test(fs.clone(), [path!("/workspace1").as_ref()], cx).await;
315
316    let workspace = init_test_workspace(&project, cx).await;
317    let cx = &mut VisualTestContext::from_window(*workspace, cx);
318
319    workspace
320        .update(cx, |workspace, window, cx| {
321            NewProcessModal::show(workspace, window, NewProcessMode::Debug, None, cx);
322        })
323        .unwrap();
324
325    cx.run_until_parked();
326
327    let modal = workspace
328        .update(cx, |workspace, _, cx| {
329            workspace.active_modal::<NewProcessModal>(cx)
330        })
331        .unwrap()
332        .expect("Modal should be active");
333
334    cx.executor().run_until_parked();
335
336    let subtitles = modal.update_in(cx, |modal, _, cx| {
337        modal.debug_picker_candidate_subtitles(cx)
338    });
339
340    assert_eq!(
341        subtitles.as_slice(),
342        [path!(".zed/debug.json"), path!(".zed/debug.json")]
343    );
344}
345
346#[gpui::test]
347async fn test_dap_adapter_config_conversion_and_validation(cx: &mut TestAppContext) {
348    init_test(cx);
349
350    let mut expected_adapters = vec![
351        "CodeLLDB",
352        "Debugpy",
353        "JavaScript",
354        "Delve",
355        "GDB",
356        "fake-adapter",
357    ];
358
359    let adapter_names = cx.update(|cx| {
360        let registry = DapRegistry::global(cx);
361        registry.enumerate_adapters::<Vec<_>>()
362    });
363
364    let zed_config = ZedDebugConfig {
365        label: "test_debug_session".into(),
366        adapter: "test_adapter".into(),
367        request: DebugRequest::Launch(LaunchRequest {
368            program: "test_program".into(),
369            cwd: None,
370            args: vec![],
371            env: Default::default(),
372        }),
373        stop_on_entry: Some(true),
374    };
375
376    for adapter_name in adapter_names {
377        let adapter_str = adapter_name.to_string();
378        if let Some(pos) = expected_adapters.iter().position(|&x| x == adapter_str) {
379            expected_adapters.remove(pos);
380        }
381
382        let adapter = cx
383            .update(|cx| {
384                let registry = DapRegistry::global(cx);
385                registry.adapter(adapter_name.as_ref())
386            })
387            .unwrap_or_else(|| panic!("Adapter {} should exist", adapter_name));
388
389        let mut adapter_specific_config = zed_config.clone();
390        adapter_specific_config.adapter = adapter_name.to_string().into();
391
392        let debug_scenario = adapter
393            .config_from_zed_format(adapter_specific_config)
394            .await
395            .unwrap_or_else(|_| {
396                panic!(
397                    "Adapter {} should successfully convert from Zed format",
398                    adapter_name
399                )
400            });
401
402        assert!(
403            debug_scenario.config.is_object(),
404            "Adapter {} should produce a JSON object for config",
405            adapter_name
406        );
407
408        let request_type = adapter
409            .request_kind(&debug_scenario.config)
410            .await
411            .unwrap_or_else(|_| {
412                panic!(
413                    "Adapter {} should validate the config successfully",
414                    adapter_name
415                )
416            });
417
418        match request_type {
419            dap::StartDebuggingRequestArgumentsRequest::Launch => {}
420            dap::StartDebuggingRequestArgumentsRequest::Attach => {
421                panic!(
422                    "Expected Launch request but got Attach for adapter {}",
423                    adapter_name
424                );
425            }
426        }
427    }
428
429    assert!(
430        expected_adapters.is_empty(),
431        "The following expected adapters were not found in the registry: {:?}",
432        expected_adapters
433    );
434}