logiguard fork: GPUI xdg-activation keyboard-focus serial fix
Some checks failed
Update All Top Ranking Issues / update_top_ranking_issues (push) Has been cancelled
Triage Project Sync (#84) / Sync triage project (push) Has been cancelled
release_nightly / notify_on_failure (push) Has been cancelled
release_nightly / check_style (push) Has been cancelled
release_nightly / run_tests_windows (push) Has been cancelled
release_nightly / clippy_windows (push) Has been cancelled
release_nightly / bundle_linux_aarch64 (push) Has been cancelled
release_nightly / bundle_linux_x86_64 (push) Has been cancelled
release_nightly / bundle_mac_aarch64 (push) Has been cancelled
release_nightly / bundle_mac_x86_64 (push) Has been cancelled
release_nightly / bundle_windows_aarch64 (push) Has been cancelled
release_nightly / bundle_windows_x86_64 (push) Has been cancelled
release_nightly / build_nix_linux_x86_64 (push) Has been cancelled
release_nightly / build_nix_mac_aarch64 (push) Has been cancelled
release_nightly / update_nightly_tag (push) Has been cancelled
Hotfix Review Monitor / check-hotfix-reviews (push) Has been cancelled
Stale PR Review Reminder / check-stale-prs (push) Has been cancelled
Update Weekly Top Ranking Issues / update_top_ranking_issues (push) Has been cancelled
Bump collab-staging Tag / update-collab-staging-tag (push) Has been cancelled
compliance_check / scheduled_compliance_check (push) Has been cancelled

Single-commit orphan branch: full zed-industries/zed @ 8c74db0 source tree
with a 3-file patch applied (no upstream history).

Patch (crates/gpui_linux/src/linux/wayland/):
  - serial.rs: add SerialKind::KeyboardEnter
  - client.rs: store wl_keyboard.enter serial; add latest_serial_of()
  - window.rs: activate() uses keyboard-enter serial (Mutter focus gate)

Mutter honors window activation only when the token carries the keyboard-
focus serial from wl_keyboard.enter; GPUI used a stale mouse-press serial.
See docs/tray-window-focus-wayland.md in logiguard.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Mohamad Khani
2026-07-14 02:22:17 +03:30
commit b72a46db68
3984 changed files with 1583326 additions and 0 deletions

View File

@@ -0,0 +1,320 @@
#![expect(clippy::result_large_err)]
use crate::{
attach_modal::{Candidate, ModalIntent},
tests::start_debug_session_with,
*,
};
use attach_modal::AttachModal;
use dap::{FakeAdapter, adapters::DebugTaskDefinition};
use gpui::{BackgroundExecutor, TestAppContext, VisualTestContext};
use menu::Confirm;
use project::{FakeFs, Project};
use serde_json::json;
use task::{AttachRequest, SharedTaskContext};
use tests::{init_test, init_test_workspace};
use util::path;
#[gpui::test]
async fn test_direct_attach_to_process(executor: BackgroundExecutor, cx: &mut TestAppContext) {
init_test(cx);
let fs = FakeFs::new(executor.clone());
fs.insert_tree(
path!("/project"),
json!({
"main.rs": "First line\nSecond line\nThird line\nFourth line",
}),
)
.await;
let project = Project::test(fs, [path!("/project").as_ref()], cx).await;
let workspace = init_test_workspace(&project, cx).await;
let cx = &mut VisualTestContext::from_window(*workspace, cx);
let _session = start_debug_session_with(
&workspace,
cx,
DebugTaskDefinition {
adapter: "fake-adapter".into(),
label: "label".into(),
config: json!({
"request": "attach",
"process_id": 10,
}),
tcp_connection: None,
},
|client| {
client.on_request::<dap::requests::Attach, _>(move |_, args| {
let raw = &args.raw;
assert_eq!(raw["request"], "attach");
assert_eq!(raw["process_id"], 10);
Ok(())
});
},
)
.unwrap();
cx.run_until_parked();
// assert we didn't show the attach modal
workspace
.update(cx, |workspace, _window, cx| {
assert!(
workspace
.workspace()
.read(cx)
.active_modal::<AttachModal>(cx)
.is_none()
);
})
.unwrap();
}
#[gpui::test]
async fn test_show_attach_modal_and_select_process(
executor: BackgroundExecutor,
cx: &mut TestAppContext,
) {
init_test(cx);
let fs = FakeFs::new(executor.clone());
fs.insert_tree(
path!("/project"),
json!({
"main.rs": "First line\nSecond line\nThird line\nFourth line",
}),
)
.await;
let project = Project::test(fs, [path!("/project").as_ref()], cx).await;
let workspace = init_test_workspace(&project, cx).await;
let cx = &mut VisualTestContext::from_window(*workspace, cx);
// Set up handlers for sessions spawned via modal.
let _initialize_subscription =
project::debugger::test::intercept_debug_sessions(cx, |client| {
client.on_request::<dap::requests::Attach, _>(move |_, args| {
let raw = &args.raw;
assert_eq!(raw["request"], "attach");
assert_eq!(raw["process_id"], 1);
Ok(())
});
});
let attach_modal = workspace
.update(cx, |multi, window, cx| {
let workspace_handle = multi.workspace().downgrade();
multi.toggle_modal(window, cx, |window, cx| {
AttachModal::with_processes(
workspace_handle,
vec![
Candidate {
pid: 0,
name: "fake-binary-1".into(),
command: vec![],
},
Candidate {
pid: 3,
name: "real-binary-1".into(),
command: vec![],
},
Candidate {
pid: 1,
name: "fake-binary-2".into(),
command: vec![],
},
]
.into_iter()
.collect(),
true,
ModalIntent::AttachToProcess(task::ZedDebugConfig {
adapter: FakeAdapter::ADAPTER_NAME.into(),
request: dap::DebugRequest::Attach(AttachRequest::default()),
label: "attach example".into(),
stop_on_entry: None,
}),
window,
cx,
)
});
multi.active_modal::<AttachModal>(cx).unwrap()
})
.unwrap();
cx.run_until_parked();
// assert we got the expected processes
workspace
.update(cx, |_, window, cx| {
let names = attach_modal.update(cx, |modal, cx| attach_modal::process_names(modal, cx));
// Initially all processes are visible.
assert_eq!(3, names.len());
attach_modal.update(cx, |this, cx| {
this.picker.update(cx, |this, cx| {
this.set_query("fakb", window, cx);
})
})
})
.unwrap();
cx.run_until_parked();
// assert we got the expected processes
workspace
.update(cx, |_, _, cx| {
let names = attach_modal.update(cx, |modal, cx| attach_modal::process_names(modal, cx));
// Initially all processes are visible.
assert_eq!(2, names.len());
})
.unwrap();
// select the only existing process
cx.dispatch_action(Confirm);
cx.run_until_parked();
// assert attach modal was dismissed
workspace
.update(cx, |workspace, _window, cx| {
assert!(workspace.active_modal::<AttachModal>(cx).is_none());
})
.unwrap();
}
#[gpui::test]
async fn test_attach_with_pick_pid_variable(executor: BackgroundExecutor, cx: &mut TestAppContext) {
init_test(cx);
let fs = FakeFs::new(executor.clone());
fs.insert_tree(
path!("/project"),
json!({
"main.rs": "First line\nSecond line\nThird line\nFourth line",
}),
)
.await;
let project = Project::test(fs, [path!("/project").as_ref()], cx).await;
let workspace = init_test_workspace(&project, cx).await;
let cx = &mut VisualTestContext::from_window(*workspace, cx);
let _initialize_subscription =
project::debugger::test::intercept_debug_sessions(cx, |client| {
client.on_request::<dap::requests::Attach, _>(move |_, args| {
let raw = &args.raw;
assert_eq!(raw["request"], "attach");
assert_eq!(
raw["process_id"], "42",
"verify process id has been replaced"
);
Ok(())
});
});
let pick_pid_placeholder = task::VariableName::PickProcessId.template_value();
workspace
.update(cx, |multi, window, cx| {
multi.workspace().update(cx, |workspace, cx| {
workspace.start_debug_session(
DebugTaskDefinition {
adapter: FakeAdapter::ADAPTER_NAME.into(),
label: "attach with picker".into(),
config: json!({
"request": "attach",
"process_id": pick_pid_placeholder,
}),
tcp_connection: None,
}
.to_scenario(),
SharedTaskContext::default(),
None,
None,
window,
cx,
);
})
})
.unwrap();
cx.run_until_parked();
let attach_modal = workspace
.update(cx, |workspace, _window, cx| {
workspace.active_modal::<AttachModal>(cx)
})
.unwrap();
assert!(
attach_modal.is_some(),
"Attach modal should open when config contains ZED_PICK_PID"
);
let attach_modal = attach_modal.unwrap();
workspace
.update(cx, |_, window, cx| {
attach_modal.update(cx, |modal, cx| {
attach_modal::set_candidates(
modal,
vec![
Candidate {
pid: 10,
name: "process-1".into(),
command: vec![],
},
Candidate {
pid: 42,
name: "target-process".into(),
command: vec![],
},
Candidate {
pid: 99,
name: "process-3".into(),
command: vec![],
},
]
.into_iter()
.collect(),
window,
cx,
)
})
})
.unwrap();
cx.run_until_parked();
workspace
.update(cx, |_, window, cx| {
attach_modal.update(cx, |modal, cx| {
modal.picker.update(cx, |picker, cx| {
picker.set_query("target", window, cx);
})
})
})
.unwrap();
cx.run_until_parked();
workspace
.update(cx, |_, _, cx| {
let names = attach_modal.update(cx, |modal, cx| attach_modal::process_names(modal, cx));
assert_eq!(names.len(), 1);
assert_eq!(names[0], " 42 target-process");
})
.unwrap();
cx.dispatch_action(Confirm);
cx.run_until_parked();
workspace
.update(cx, |workspace, _window, cx| {
assert!(
workspace.active_modal::<AttachModal>(cx).is_none(),
"Attach modal should be dismissed after selection"
);
})
.unwrap();
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,118 @@
#![expect(clippy::result_large_err)]
use crate::tests::{init_test, init_test_workspace, start_debug_session};
use dap::requests::{StackTrace, Threads};
use debugger_tools::LogStore;
use gpui::{BackgroundExecutor, TestAppContext, VisualTestContext};
use project::Project;
use serde_json::json;
use std::cell::OnceCell;
use util::path;
#[gpui::test]
async fn test_dap_logger_captures_all_session_rpc_messages(
executor: BackgroundExecutor,
cx: &mut TestAppContext,
) {
let log_store_cell = std::rc::Rc::new(OnceCell::new());
cx.update(|cx| {
let log_store_cell = log_store_cell.clone();
cx.observe_new::<LogStore>(move |_, _, cx| {
log_store_cell.set(cx.entity()).unwrap();
})
.detach();
debugger_tools::init(cx);
});
init_test(cx);
let log_store = log_store_cell.get().unwrap().clone();
// Create a filesystem with a simple project
let fs = project::FakeFs::new(executor.clone());
fs.insert_tree(
path!("/project"),
json!({
"main.rs": "fn main() {\n println!(\"Hello, world!\");\n}"
}),
)
.await;
assert!(
log_store.read_with(cx, |log_store, _| !log_store.has_projects()),
"log_store shouldn't contain any projects before any projects were created"
);
let project = Project::test(fs, [path!("/project").as_ref()], cx).await;
let workspace = init_test_workspace(&project, cx).await;
assert!(
log_store.read_with(cx, |log_store, _| log_store.has_projects()),
"log_store shouldn't contain any projects before any projects were created"
);
assert!(
log_store.read_with(cx, |log_store, _| log_store
.contained_session_ids(&project.downgrade())
.is_empty()),
"log_store shouldn't contain any projects before any projects were created"
);
let cx = &mut VisualTestContext::from_window(*workspace, cx);
// Start a debug session
let session = start_debug_session(&workspace, cx, |_| {}).unwrap();
let session_id = session.read_with(cx, |session, _| session.session_id());
let client = session.update(cx, |session, _| session.adapter_client().unwrap());
assert_eq!(
log_store.read_with(cx, |log_store, _| log_store
.contained_session_ids(&project.downgrade())
.len()),
1,
);
assert!(
log_store.read_with(cx, |log_store, _| log_store
.contained_session_ids(&project.downgrade())
.contains(&session_id)),
"log_store should contain the session IDs of the started session"
);
assert!(
!log_store.read_with(cx, |log_store, _| log_store
.rpc_messages_for_session_id(&project.downgrade(), session_id)
.is_empty()),
"We should have the initialization sequence in the log store"
);
// Set up basic responses for common requests
client.on_request::<Threads, _>(move |_, _| {
Ok(dap::ThreadsResponse {
threads: vec![dap::Thread {
id: 1,
name: "Thread 1".into(),
}],
})
});
client.on_request::<StackTrace, _>(move |_, _| {
Ok(dap::StackTraceResponse {
stack_frames: Vec::default(),
total_frames: None,
})
});
// Run until all pending tasks are executed
cx.run_until_parked();
// Simulate a stopped event to generate more DAP messages
client
.fake_event(dap::messages::Events::Stopped(dap::StoppedEvent {
reason: dap::StoppedEventReason::Pause,
description: None,
thread_id: Some(1),
preserve_focus_hint: None,
text: None,
all_threads_stopped: None,
hit_breakpoint_ids: None,
}))
.await;
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,217 @@
#![expect(clippy::result_large_err)]
use crate::{
debugger_panel::DebugPanel,
persistence::DebuggerPaneItem,
tests::{active_debug_session_panel, init_test, init_test_workspace, start_debug_session},
};
use dap::{
StoppedEvent,
requests::{Initialize, Modules},
};
use gpui::{BackgroundExecutor, TestAppContext, VisualTestContext};
use project::{FakeFs, Project};
use std::sync::{
Arc,
atomic::{AtomicBool, AtomicI32, Ordering},
};
use util::path;
#[gpui::test]
async fn test_module_list(executor: BackgroundExecutor, cx: &mut TestAppContext) {
init_test(cx);
let fs = FakeFs::new(executor.clone());
let project = Project::test(fs, [path!("/project").as_ref()], cx).await;
let workspace = init_test_workspace(&project, cx).await;
workspace
.update(cx, |workspace, window, cx| {
workspace.focus_panel::<DebugPanel>(window, cx);
})
.unwrap();
let cx = &mut VisualTestContext::from_window(*workspace, cx);
let session = start_debug_session(&workspace, cx, |client| {
client.on_request::<Initialize, _>(move |_, _| {
Ok(dap::Capabilities {
supports_modules_request: Some(true),
..Default::default()
})
});
})
.unwrap();
let client = session.update(cx, |session, _| session.adapter_client().unwrap());
let called_modules = Arc::new(AtomicBool::new(false));
let modules = vec![
dap::Module {
id: dap::ModuleId::Number(1),
name: "First Module".into(),
address_range: None,
date_time_stamp: None,
path: None,
symbol_file_path: None,
symbol_status: None,
version: None,
is_optimized: None,
is_user_code: None,
},
dap::Module {
id: dap::ModuleId::Number(2),
name: "Second Module".into(),
address_range: None,
date_time_stamp: None,
path: None,
symbol_file_path: None,
symbol_status: None,
version: None,
is_optimized: None,
is_user_code: None,
},
];
client.on_request::<Modules, _>({
let called_modules = called_modules.clone();
let modules_request_count = AtomicI32::new(0);
let modules = modules.clone();
move |_, _| {
modules_request_count.fetch_add(1, Ordering::SeqCst);
assert_eq!(
1,
modules_request_count.load(Ordering::SeqCst),
"This request should only be called once from the host"
);
called_modules.store(true, Ordering::SeqCst);
Ok(dap::ModulesResponse {
modules: modules.clone(),
total_modules: Some(2u64),
})
}
});
client
.fake_event(dap::messages::Events::Stopped(StoppedEvent {
reason: dap::StoppedEventReason::Pause,
description: None,
thread_id: Some(1),
preserve_focus_hint: None,
text: None,
all_threads_stopped: None,
hit_breakpoint_ids: None,
}))
.await;
cx.run_until_parked();
let running_state =
active_debug_session_panel(workspace, cx).update_in(cx, |item, window, cx| {
cx.focus_self(window);
item.running_state().clone()
});
running_state.update_in(cx, |this, window, cx| {
this.activate_item(DebuggerPaneItem::Modules, window, cx);
cx.refresh_windows();
});
cx.run_until_parked();
assert!(
called_modules.load(std::sync::atomic::Ordering::SeqCst),
"Request Modules should be called because a user clicked on the module list"
);
active_debug_session_panel(workspace, cx).update(cx, |_, cx| {
let actual_modules = running_state.update(cx, |state, cx| {
state.module_list().update(cx, |list, cx| list.modules(cx))
});
assert_eq!(modules, actual_modules);
});
// Test all module events now
// New Module
// Changed
// Removed
let new_module = dap::Module {
id: dap::ModuleId::Number(3),
name: "Third Module".into(),
address_range: None,
date_time_stamp: None,
path: None,
symbol_file_path: None,
symbol_status: None,
version: None,
is_optimized: None,
is_user_code: None,
};
client
.fake_event(dap::messages::Events::Module(dap::ModuleEvent {
reason: dap::ModuleEventReason::New,
module: new_module.clone(),
}))
.await;
cx.run_until_parked();
active_debug_session_panel(workspace, cx).update(cx, |_, cx| {
let actual_modules = running_state.update(cx, |state, cx| {
state.module_list().update(cx, |list, cx| list.modules(cx))
});
assert_eq!(actual_modules.len(), 3);
assert!(actual_modules.contains(&new_module));
});
let changed_module = dap::Module {
id: dap::ModuleId::Number(2),
name: "Modified Second Module".into(),
address_range: None,
date_time_stamp: None,
path: None,
symbol_file_path: None,
symbol_status: None,
version: None,
is_optimized: None,
is_user_code: None,
};
client
.fake_event(dap::messages::Events::Module(dap::ModuleEvent {
reason: dap::ModuleEventReason::Changed,
module: changed_module.clone(),
}))
.await;
cx.run_until_parked();
active_debug_session_panel(workspace, cx).update(cx, |_, cx| {
let actual_modules = running_state.update(cx, |state, cx| {
state.module_list().update(cx, |list, cx| list.modules(cx))
});
assert_eq!(actual_modules.len(), 3);
assert!(actual_modules.contains(&changed_module));
});
client
.fake_event(dap::messages::Events::Module(dap::ModuleEvent {
reason: dap::ModuleEventReason::Removed,
module: changed_module.clone(),
}))
.await;
cx.run_until_parked();
active_debug_session_panel(workspace, cx).update(cx, |_, cx| {
let actual_modules = running_state.update(cx, |state, cx| {
state.module_list().update(cx, |list, cx| list.modules(cx))
});
assert_eq!(actual_modules.len(), 2);
assert!(!actual_modules.contains(&changed_module));
});
}

View File

@@ -0,0 +1,448 @@
#![expect(clippy::result_large_err)]
use dap::DapRegistry;
use editor::Editor;
use gpui::{BackgroundExecutor, TestAppContext, VisualTestContext};
use project::{FakeFs, Fs as _, Project};
use serde_json::json;
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};
use task::{
DebugRequest, DebugScenario, LaunchRequest, SharedTaskContext, TaskContext, VariableName,
ZedDebugConfig,
};
use text::Point;
use util::path;
use crate::NewProcessMode;
use crate::new_process_modal::NewProcessModal;
use crate::tests::{init_test, init_test_workspace};
#[gpui::test]
async fn test_debug_session_substitutes_variables_and_relativizes_paths(
executor: BackgroundExecutor,
cx: &mut TestAppContext,
) {
init_test(cx);
let fs = FakeFs::new(executor.clone());
fs.insert_tree(
path!("/project"),
json!({
"main.rs": "fn main() {}"
}),
)
.await;
let project = Project::test(fs, [path!("/project").as_ref()], cx).await;
let workspace = init_test_workspace(&project, cx).await;
let cx = &mut VisualTestContext::from_window(*workspace, cx);
let test_variables = vec![(
VariableName::WorktreeRoot,
path!("/test/worktree/path").to_string(),
)]
.into_iter()
.collect();
let task_context: SharedTaskContext = TaskContext {
cwd: None,
task_variables: test_variables,
project_env: Default::default(),
}
.into();
let home_dir = paths::home_dir();
let test_cases: Vec<(&'static str, &'static str)> = vec![
// Absolute path - should not be relativized
(
path!("/absolute/path/to/program"),
path!("/absolute/path/to/program"),
),
// Relative path - should be prefixed with worktree root
(
format!(".{0}src{0}program", std::path::MAIN_SEPARATOR).leak(),
path!("/test/worktree/path/src/program"),
),
// Home directory path - should be expanded to full home directory path
(
format!("~{0}src{0}program", std::path::MAIN_SEPARATOR).leak(),
home_dir
.join("src")
.join("program")
.to_string_lossy()
.to_string()
.leak(),
),
// Path with $ZED_WORKTREE_ROOT - should be substituted without double appending
(
format!(
"$ZED_WORKTREE_ROOT{0}src{0}program",
std::path::MAIN_SEPARATOR
)
.leak(),
path!("/test/worktree/path/src/program"),
),
];
let called_launch = Arc::new(AtomicBool::new(false));
for (input_path, expected_path) in test_cases {
let _subscription = project::debugger::test::intercept_debug_sessions(cx, {
let called_launch = called_launch.clone();
move |client| {
client.on_request::<dap::requests::Launch, _>({
let called_launch = called_launch.clone();
move |_, args| {
let config = args.raw.as_object().unwrap();
assert_eq!(
config["program"].as_str().unwrap(),
expected_path,
"Program path was not correctly substituted for input: {}",
input_path
);
assert_eq!(
config["cwd"].as_str().unwrap(),
expected_path,
"CWD path was not correctly substituted for input: {}",
input_path
);
let expected_other_field = if input_path.contains("$ZED_WORKTREE_ROOT") {
input_path.replace("$ZED_WORKTREE_ROOT", path!("/test/worktree/path"))
} else {
input_path.to_string()
};
assert_eq!(
config["otherField"].as_str().unwrap(),
&expected_other_field,
"Other field was incorrectly modified for input: {}",
input_path
);
called_launch.store(true, Ordering::SeqCst);
Ok(())
}
});
}
});
let scenario = DebugScenario {
adapter: "fake-adapter".into(),
label: "test-debug-session".into(),
build: None,
config: json!({
"request": "launch",
"program": input_path,
"cwd": input_path,
"otherField": input_path
}),
tcp_connection: None,
};
workspace
.update(cx, |multi, window, cx| {
multi.workspace().update(cx, |workspace, cx| {
workspace.start_debug_session(
scenario,
task_context.clone(),
None,
None,
window,
cx,
);
})
})
.unwrap();
cx.run_until_parked();
assert!(called_launch.load(Ordering::SeqCst));
called_launch.store(false, Ordering::SeqCst);
}
}
#[gpui::test]
async fn test_save_debug_scenario_to_file(executor: BackgroundExecutor, cx: &mut TestAppContext) {
init_test(cx);
let fs = FakeFs::new(executor.clone());
fs.insert_tree(
path!("/project"),
json!({
"main.rs": "fn main() {}"
}),
)
.await;
let project = Project::test(fs.clone(), [path!("/project").as_ref()], cx).await;
let workspace = init_test_workspace(&project, cx).await;
let cx = &mut VisualTestContext::from_window(*workspace, cx);
workspace
.update(cx, |multi, window, cx| {
multi.workspace().update(cx, |workspace, cx| {
NewProcessModal::show(workspace, window, NewProcessMode::Debug, None, cx);
});
})
.unwrap();
cx.run_until_parked();
let modal = workspace
.update(cx, |workspace, _, cx| {
workspace.active_modal::<NewProcessModal>(cx)
})
.unwrap()
.expect("Modal should be active");
modal.update_in(cx, |modal, window, cx| {
modal.set_configure("/project/main", "/project", false, window, cx);
modal.save_debug_scenario(window, cx);
});
cx.executor().run_until_parked();
let editor = workspace
.update(cx, |workspace, _window, cx| {
workspace.active_item_as::<Editor>(cx).unwrap()
})
.unwrap();
let debug_json_content = fs
.load(path!("/project/.zed/debug.json").as_ref())
.await
.expect("debug.json should exist")
.lines()
.filter(|line| !line.starts_with("//"))
.collect::<Vec<_>>()
.join("\n");
let expected_content = indoc::indoc! {r#"
[
{
"adapter": "fake-adapter",
"label": "main (fake-adapter)",
"request": "launch",
"program": "/project/main",
"cwd": "/project",
"args": [],
"env": {}
}
]"#};
pretty_assertions::assert_eq!(expected_content, debug_json_content);
editor.update(cx, |editor, cx| {
assert_eq!(
editor
.selections
.newest::<Point>(&editor.display_snapshot(cx))
.head(),
Point::new(5, 2)
)
});
modal.update_in(cx, |modal, window, cx| {
modal.set_configure("/project/other", "/project", true, window, cx);
modal.save_debug_scenario(window, cx);
});
cx.executor().run_until_parked();
let expected_content = indoc::indoc! {r#"
[
{
"adapter": "fake-adapter",
"label": "main (fake-adapter)",
"request": "launch",
"program": "/project/main",
"cwd": "/project",
"args": [],
"env": {}
},
{
"adapter": "fake-adapter",
"label": "other (fake-adapter)",
"request": "launch",
"program": "/project/other",
"cwd": "/project",
"args": [],
"env": {}
}
]"#};
let debug_json_content = fs
.load(path!("/project/.zed/debug.json").as_ref())
.await
.expect("debug.json should exist")
.lines()
.filter(|line| !line.starts_with("//"))
.collect::<Vec<_>>()
.join("\n");
pretty_assertions::assert_eq!(expected_content, debug_json_content);
}
#[gpui::test]
async fn test_debug_modal_subtitles_with_multiple_worktrees(
executor: BackgroundExecutor,
cx: &mut TestAppContext,
) {
init_test(cx);
let fs = FakeFs::new(executor.clone());
fs.insert_tree(
path!("/workspace1"),
json!({
".zed": {
"debug.json": r#"[
{
"adapter": "fake-adapter",
"label": "Debug App 1",
"request": "launch",
"program": "./app1",
"cwd": "."
},
{
"adapter": "fake-adapter",
"label": "Debug Tests 1",
"request": "launch",
"program": "./test1",
"cwd": "."
}
]"#
},
"main.rs": "fn main() {}"
}),
)
.await;
let project = Project::test(fs.clone(), [path!("/workspace1").as_ref()], cx).await;
let workspace = init_test_workspace(&project, cx).await;
let cx = &mut VisualTestContext::from_window(*workspace, cx);
workspace
.update(cx, |multi, window, cx| {
multi.workspace().update(cx, |workspace, cx| {
NewProcessModal::show(workspace, window, NewProcessMode::Debug, None, cx);
});
})
.unwrap();
cx.run_until_parked();
let modal = workspace
.update(cx, |workspace, _, cx| {
workspace.active_modal::<NewProcessModal>(cx)
})
.unwrap()
.expect("Modal should be active");
cx.executor().run_until_parked();
let subtitles = modal.update_in(cx, |modal, _, cx| {
modal.debug_picker_candidate_subtitles(cx)
});
assert_eq!(
subtitles.as_slice(),
[path!(".zed/debug.json"), path!(".zed/debug.json")]
);
}
#[gpui::test]
async fn test_dap_adapter_config_conversion_and_validation(cx: &mut TestAppContext) {
init_test(cx);
let mut expected_adapters = vec![
"CodeLLDB",
"Debugpy",
"JavaScript",
"Delve",
"GDB",
"fake-adapter",
];
let adapter_names = cx.update(|cx| {
let registry = DapRegistry::global(cx);
registry.enumerate_adapters::<Vec<_>>()
});
let zed_config = ZedDebugConfig {
label: "test_debug_session".into(),
adapter: "test_adapter".into(),
request: DebugRequest::Launch(LaunchRequest {
program: "test_program".into(),
cwd: None,
args: vec![],
env: Default::default(),
}),
stop_on_entry: Some(true),
};
for adapter_name in adapter_names {
let adapter_str = adapter_name.to_string();
if let Some(pos) = expected_adapters.iter().position(|&x| x == adapter_str) {
expected_adapters.remove(pos);
}
let adapter = cx
.update(|cx| {
let registry = DapRegistry::global(cx);
registry.adapter(adapter_name.as_ref())
})
.unwrap_or_else(|| panic!("Adapter {} should exist", adapter_name));
let mut adapter_specific_config = zed_config.clone();
adapter_specific_config.adapter = adapter_name.to_string().into();
let debug_scenario = adapter
.config_from_zed_format(adapter_specific_config)
.await
.unwrap_or_else(|_| {
panic!(
"Adapter {} should successfully convert from Zed format",
adapter_name
)
});
assert!(
debug_scenario.config.is_object(),
"Adapter {} should produce a JSON object for config",
adapter_name
);
let request_type = adapter
.request_kind(&debug_scenario.config)
.await
.unwrap_or_else(|_| {
panic!(
"Adapter {} should validate the config successfully",
adapter_name
)
});
match request_type {
dap::StartDebuggingRequestArgumentsRequest::Launch => {}
dap::StartDebuggingRequestArgumentsRequest::Attach => {
panic!(
"Expected Launch request but got Attach for adapter {}",
adapter_name
);
}
}
}
assert!(
expected_adapters.is_empty(),
"The following expected adapters were not found in the registry: {:?}",
expected_adapters
);
}

View File

@@ -0,0 +1,132 @@
#![expect(clippy::result_large_err)]
use std::iter::zip;
use crate::{
debugger_panel::DebugPanel,
persistence::SerializedPaneLayout,
tests::{init_test, init_test_workspace, start_debug_session},
};
use dap::{StoppedEvent, StoppedEventReason, messages::Events};
use gpui::{BackgroundExecutor, TestAppContext, VisualTestContext};
use project::{FakeFs, Project};
use serde_json::json;
use util::path;
use workspace::{Panel, dock::DockPosition};
#[gpui::test]
async fn test_invert_axis_on_panel_position_change(
executor: BackgroundExecutor,
cx: &mut TestAppContext,
) {
init_test(cx);
let fs = FakeFs::new(executor.clone());
fs.insert_tree(
path!("/project"),
json!({
"main.rs": "fn main() {\n println!(\"Hello, world!\");\n}",
}),
)
.await;
let project = Project::test(fs, [path!("/project").as_ref()], cx).await;
let workspace = init_test_workspace(&project, cx).await;
let cx = &mut VisualTestContext::from_window(*workspace, cx);
// Start a debug session
let session = start_debug_session(&workspace, cx, |_| {}).unwrap();
let client = session.update(cx, |session, _| session.adapter_client().unwrap());
// Setup thread response
client.on_request::<dap::requests::Threads, _>(move |_, _| {
Ok(dap::ThreadsResponse { threads: vec![] })
});
cx.run_until_parked();
client
.fake_event(Events::Stopped(StoppedEvent {
reason: StoppedEventReason::Pause,
description: None,
thread_id: Some(1),
preserve_focus_hint: None,
text: None,
all_threads_stopped: None,
hit_breakpoint_ids: None,
}))
.await;
cx.run_until_parked();
let (debug_panel, dock_position) = workspace
.update(cx, |workspace, window, cx| {
let debug_panel = workspace.panel::<DebugPanel>(cx).unwrap();
let dock_position = debug_panel.read(cx).position(window, cx);
(debug_panel, dock_position)
})
.unwrap();
assert_eq!(
dock_position,
DockPosition::Bottom,
"Default dock position should be bottom for debug panel"
);
let pre_serialized_layout = debug_panel
.read_with(cx, |panel, cx| {
panel
.active_session()
.unwrap()
.read(cx)
.running_state()
.read(cx)
.serialized_layout(cx)
})
.panes;
let post_serialized_layout = debug_panel
.update_in(cx, |panel, window, cx| {
panel.set_position(DockPosition::Right, window, cx);
panel
.active_session()
.unwrap()
.read(cx)
.running_state()
.read(cx)
.serialized_layout(cx)
})
.panes;
let pre_panes = pre_serialized_layout.in_order();
let post_panes = post_serialized_layout.in_order();
assert_eq!(pre_panes.len(), post_panes.len());
for (pre, post) in zip(pre_panes, post_panes) {
match (pre, post) {
(
SerializedPaneLayout::Group {
axis: pre_axis,
flexes: pre_flexes,
children: _,
},
SerializedPaneLayout::Group {
axis: post_axis,
flexes: post_flexes,
children: _,
},
) => {
assert_ne!(pre_axis, post_axis);
assert_eq!(pre_flexes, post_flexes);
}
(SerializedPaneLayout::Pane(pre_pane), SerializedPaneLayout::Pane(post_pane)) => {
assert_eq!(pre_pane.children, post_pane.children);
assert_eq!(pre_pane.active_item, post_pane.active_item);
}
_ => {
panic!("Variants don't match")
}
}
}
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff