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
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:
115
crates/project/tests/integration/agent_registry_store.rs
Normal file
115
crates/project/tests/integration/agent_registry_store.rs
Normal file
@@ -0,0 +1,115 @@
|
||||
use std::{future, sync::Arc, time::Duration};
|
||||
|
||||
use fs::FakeFs;
|
||||
use gpui::TestAppContext;
|
||||
use http_client::{AsyncBody, FakeHttpClient, HttpClient, Response};
|
||||
use project::AgentRegistryStore;
|
||||
use serde_json::json;
|
||||
|
||||
use crate::init_test;
|
||||
|
||||
#[gpui::test]
|
||||
async fn registry_refresh_times_out_when_fetch_never_completes(cx: &mut TestAppContext) {
|
||||
init_test(cx);
|
||||
|
||||
let fs = FakeFs::new(cx.executor());
|
||||
let http_client =
|
||||
FakeHttpClient::create(|_| future::pending::<anyhow::Result<Response<AsyncBody>>>())
|
||||
as Arc<dyn HttpClient>;
|
||||
|
||||
let registry_store =
|
||||
cx.update(|cx| AgentRegistryStore::init_global(cx, fs.clone(), http_client));
|
||||
cx.run_until_parked();
|
||||
|
||||
cx.executor().advance_clock(Duration::from_secs(31));
|
||||
cx.run_until_parked();
|
||||
|
||||
registry_store.update(cx, |store, _| {
|
||||
assert!(!store.is_fetching());
|
||||
assert!(
|
||||
store
|
||||
.fetch_error()
|
||||
.is_some_and(|error| error.contains("timed out after 30s")),
|
||||
"expected registry fetch timeout error, got {:?}",
|
||||
store.fetch_error()
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn registry_refresh_does_not_block_sequentially_on_hung_icon_downloads(
|
||||
cx: &mut TestAppContext,
|
||||
) {
|
||||
init_test(cx);
|
||||
|
||||
let fs = FakeFs::new(cx.executor());
|
||||
let http_client = FakeHttpClient::create(|request| async move {
|
||||
if request.uri().to_string().contains("registry.json") {
|
||||
Ok(Response::builder()
|
||||
.status(200)
|
||||
.body(AsyncBody::from(
|
||||
serde_json::to_string(&json!({
|
||||
"version": "1",
|
||||
"agents": [
|
||||
{
|
||||
"id": "slow-icon-a",
|
||||
"name": "Slow Icon A",
|
||||
"version": "1.0.0",
|
||||
"description": "An agent with a slow icon.",
|
||||
"icon": "https://example.test/slow-icon-a.svg",
|
||||
"distribution": {
|
||||
"npx": {
|
||||
"package": "slow-icon-a"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "slow-icon-b",
|
||||
"name": "Slow Icon B",
|
||||
"version": "1.0.0",
|
||||
"description": "Another agent with a slow icon.",
|
||||
"icon": "https://example.test/slow-icon-b.svg",
|
||||
"distribution": {
|
||||
"npx": {
|
||||
"package": "slow-icon-b"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "slow-icon-c",
|
||||
"name": "Slow Icon C",
|
||||
"version": "1.0.0",
|
||||
"description": "A third agent with a slow icon.",
|
||||
"icon": "https://example.test/slow-icon-c.svg",
|
||||
"distribution": {
|
||||
"npx": {
|
||||
"package": "slow-icon-c"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}))
|
||||
.unwrap(),
|
||||
))
|
||||
.unwrap())
|
||||
} else {
|
||||
future::pending::<anyhow::Result<Response<AsyncBody>>>().await
|
||||
}
|
||||
}) as Arc<dyn HttpClient>;
|
||||
|
||||
let registry_store =
|
||||
cx.update(|cx| AgentRegistryStore::init_global(cx, fs.clone(), http_client));
|
||||
cx.run_until_parked();
|
||||
|
||||
cx.executor().advance_clock(Duration::from_secs(11));
|
||||
cx.run_until_parked();
|
||||
|
||||
registry_store.update(cx, |store, _| {
|
||||
assert!(!store.is_fetching());
|
||||
assert_eq!(store.agents().len(), 3);
|
||||
assert_eq!(store.agents()[0].id().as_ref(), "slow-icon-a");
|
||||
assert_eq!(store.agents()[1].id().as_ref(), "slow-icon-b");
|
||||
assert_eq!(store.agents()[2].id().as_ref(), "slow-icon-c");
|
||||
assert_eq!(store.fetch_error(), None);
|
||||
});
|
||||
}
|
||||
685
crates/project/tests/integration/bookmark_store.rs
Normal file
685
crates/project/tests/integration/bookmark_store.rs
Normal file
@@ -0,0 +1,685 @@
|
||||
use std::{path::Path, sync::Arc};
|
||||
|
||||
use collections::BTreeMap;
|
||||
use gpui::{Entity, TestAppContext};
|
||||
use language::Buffer;
|
||||
use project::{Project, bookmark_store::SerializedBookmark};
|
||||
use serde_json::json;
|
||||
use util::path;
|
||||
|
||||
mod integration {
|
||||
use super::*;
|
||||
use fs::Fs as _;
|
||||
|
||||
fn init_test(cx: &mut TestAppContext) {
|
||||
cx.update(|cx| {
|
||||
let settings_store = settings::SettingsStore::test(cx);
|
||||
cx.set_global(settings_store);
|
||||
release_channel::init(semver::Version::new(0, 0, 0), cx);
|
||||
});
|
||||
}
|
||||
|
||||
fn project_path(path: &str) -> Arc<Path> {
|
||||
Arc::from(Path::new(path))
|
||||
}
|
||||
|
||||
async fn open_buffer(
|
||||
project: &Entity<Project>,
|
||||
path: &str,
|
||||
cx: &mut TestAppContext,
|
||||
) -> Entity<Buffer> {
|
||||
project
|
||||
.update(cx, |project, cx| {
|
||||
project.open_local_buffer(Path::new(path), cx)
|
||||
})
|
||||
.await
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
fn add_bookmarks(
|
||||
project: &Entity<Project>,
|
||||
buffer: &Entity<Buffer>,
|
||||
rows: &[u32],
|
||||
cx: &mut TestAppContext,
|
||||
) {
|
||||
let buffer = buffer.clone();
|
||||
project.update(cx, |project, cx| {
|
||||
let bookmark_store = project.bookmark_store();
|
||||
let snapshot = buffer.read(cx).snapshot();
|
||||
for &row in rows {
|
||||
let anchor = snapshot.anchor_after(text::Point::new(row, 0));
|
||||
bookmark_store.update(cx, |store, cx| {
|
||||
store.toggle_bookmark(buffer.clone(), anchor, cx);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
fn get_all_bookmarks(
|
||||
project: &Entity<Project>,
|
||||
cx: &mut TestAppContext,
|
||||
) -> BTreeMap<Arc<Path>, Vec<SerializedBookmark>> {
|
||||
project.read_with(cx, |project, cx| {
|
||||
project
|
||||
.bookmark_store()
|
||||
.read(cx)
|
||||
.all_serialized_bookmarks(cx)
|
||||
})
|
||||
}
|
||||
|
||||
fn build_serialized(
|
||||
entries: &[(&str, &[u32])],
|
||||
) -> BTreeMap<Arc<Path>, Vec<SerializedBookmark>> {
|
||||
let mut map = BTreeMap::new();
|
||||
for &(path_str, rows) in entries {
|
||||
let path = project_path(path_str);
|
||||
map.insert(
|
||||
path.clone(),
|
||||
rows.iter().map(|&row| SerializedBookmark(row)).collect(),
|
||||
);
|
||||
}
|
||||
map
|
||||
}
|
||||
|
||||
async fn restore_bookmarks(
|
||||
project: &Entity<Project>,
|
||||
serialized: BTreeMap<Arc<Path>, Vec<SerializedBookmark>>,
|
||||
cx: &mut TestAppContext,
|
||||
) {
|
||||
project
|
||||
.update(cx, |project, cx| {
|
||||
project.bookmark_store().update(cx, |store, cx| {
|
||||
store.load_serialized_bookmarks(serialized, cx)
|
||||
})
|
||||
})
|
||||
.await
|
||||
.expect("with_serialized_bookmarks should succeed");
|
||||
}
|
||||
|
||||
fn clear_bookmarks(project: &Entity<Project>, cx: &mut TestAppContext) {
|
||||
project.update(cx, |project, cx| {
|
||||
project.bookmark_store().update(cx, |store, cx| {
|
||||
store.clear_bookmarks(cx);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
fn assert_bookmark_rows(
|
||||
bookmarks: &BTreeMap<Arc<Path>, Vec<SerializedBookmark>>,
|
||||
path: &str,
|
||||
expected_rows: &[u32],
|
||||
) {
|
||||
let path = project_path(path);
|
||||
let file_bookmarks = bookmarks
|
||||
.get(&path)
|
||||
.unwrap_or_else(|| panic!("Expected bookmarks for {}", path.display()));
|
||||
let rows: Vec<u32> = file_bookmarks.iter().map(|b| b.0).collect();
|
||||
assert_eq!(rows, expected_rows, "Bookmark rows for {}", path.display());
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_all_serialized_bookmarks_empty(cx: &mut TestAppContext) {
|
||||
init_test(cx);
|
||||
cx.executor().allow_parking();
|
||||
|
||||
let fs = fs::FakeFs::new(cx.executor());
|
||||
fs.insert_tree(path!("/project"), json!({"file1.rs": "line1\nline2\n"}))
|
||||
.await;
|
||||
|
||||
let project = Project::test(fs, [path!("/project").as_ref()], cx).await;
|
||||
assert!(get_all_bookmarks(&project, cx).is_empty());
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_all_serialized_bookmarks_single_file(cx: &mut TestAppContext) {
|
||||
init_test(cx);
|
||||
cx.executor().allow_parking();
|
||||
|
||||
let fs = fs::FakeFs::new(cx.executor());
|
||||
fs.insert_tree(
|
||||
path!("/project"),
|
||||
json!({"file1.rs": "line1\nline2\nline3\nline4\nline5\n"}),
|
||||
)
|
||||
.await;
|
||||
|
||||
let project = Project::test(fs, [path!("/project").as_ref()], cx).await;
|
||||
let buffer = open_buffer(&project, path!("/project/file1.rs"), cx).await;
|
||||
|
||||
add_bookmarks(&project, &buffer, &[0, 2], cx);
|
||||
|
||||
let bookmarks = get_all_bookmarks(&project, cx);
|
||||
assert_eq!(bookmarks.len(), 1);
|
||||
assert_bookmark_rows(&bookmarks, path!("/project/file1.rs"), &[0, 2]);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_all_serialized_bookmarks_multiple_files(cx: &mut TestAppContext) {
|
||||
init_test(cx);
|
||||
cx.executor().allow_parking();
|
||||
|
||||
let fs = fs::FakeFs::new(cx.executor());
|
||||
fs.insert_tree(
|
||||
path!("/project"),
|
||||
json!({
|
||||
"file1.rs": "line1\nline2\nline3\n",
|
||||
"file2.rs": "lineA\nlineB\nlineC\nlineD\n",
|
||||
"file3.rs": "single line"
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
|
||||
let project = Project::test(fs, [path!("/project").as_ref()], cx).await;
|
||||
let buffer1 = open_buffer(&project, path!("/project/file1.rs"), cx).await;
|
||||
let buffer2 = open_buffer(&project, path!("/project/file2.rs"), cx).await;
|
||||
let _buffer3 = open_buffer(&project, path!("/project/file3.rs"), cx).await;
|
||||
|
||||
add_bookmarks(&project, &buffer1, &[1], cx);
|
||||
add_bookmarks(&project, &buffer2, &[0, 3], cx);
|
||||
|
||||
let bookmarks = get_all_bookmarks(&project, cx);
|
||||
assert_eq!(bookmarks.len(), 2);
|
||||
assert_bookmark_rows(&bookmarks, path!("/project/file1.rs"), &[1]);
|
||||
assert_bookmark_rows(&bookmarks, path!("/project/file2.rs"), &[0, 3]);
|
||||
assert!(
|
||||
!bookmarks.contains_key(&project_path(path!("/project/file3.rs"))),
|
||||
"file3.rs should have no bookmarks"
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_all_serialized_bookmarks_after_toggle_off(cx: &mut TestAppContext) {
|
||||
init_test(cx);
|
||||
cx.executor().allow_parking();
|
||||
|
||||
let fs = fs::FakeFs::new(cx.executor());
|
||||
fs.insert_tree(
|
||||
path!("/project"),
|
||||
json!({"file1.rs": "line1\nline2\nline3\n"}),
|
||||
)
|
||||
.await;
|
||||
|
||||
let project = Project::test(fs, [path!("/project").as_ref()], cx).await;
|
||||
let buffer = open_buffer(&project, path!("/project/file1.rs"), cx).await;
|
||||
|
||||
add_bookmarks(&project, &buffer, &[1], cx);
|
||||
assert_eq!(get_all_bookmarks(&project, cx).len(), 1);
|
||||
|
||||
// Toggle same row again to remove it
|
||||
add_bookmarks(&project, &buffer, &[1], cx);
|
||||
assert!(get_all_bookmarks(&project, cx).is_empty());
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_all_serialized_bookmarks_with_clear(cx: &mut TestAppContext) {
|
||||
init_test(cx);
|
||||
cx.executor().allow_parking();
|
||||
|
||||
let fs = fs::FakeFs::new(cx.executor());
|
||||
fs.insert_tree(
|
||||
path!("/project"),
|
||||
json!({
|
||||
"file1.rs": "line1\nline2\nline3\n",
|
||||
"file2.rs": "lineA\nlineB\n"
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
|
||||
let project = Project::test(fs, [path!("/project").as_ref()], cx).await;
|
||||
let buffer1 = open_buffer(&project, path!("/project/file1.rs"), cx).await;
|
||||
let buffer2 = open_buffer(&project, path!("/project/file2.rs"), cx).await;
|
||||
|
||||
add_bookmarks(&project, &buffer1, &[0], cx);
|
||||
add_bookmarks(&project, &buffer2, &[1], cx);
|
||||
assert_eq!(get_all_bookmarks(&project, cx).len(), 2);
|
||||
|
||||
clear_bookmarks(&project, cx);
|
||||
assert!(get_all_bookmarks(&project, cx).is_empty());
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_all_serialized_bookmarks_returns_sorted_by_path(cx: &mut TestAppContext) {
|
||||
init_test(cx);
|
||||
cx.executor().allow_parking();
|
||||
|
||||
let fs = fs::FakeFs::new(cx.executor());
|
||||
fs.insert_tree(
|
||||
path!("/project"),
|
||||
json!({"b.rs": "line1\n", "a.rs": "line1\n", "c.rs": "line1\n"}),
|
||||
)
|
||||
.await;
|
||||
|
||||
let project = Project::test(fs, [path!("/project").as_ref()], cx).await;
|
||||
let buffer_b = open_buffer(&project, path!("/project/b.rs"), cx).await;
|
||||
let buffer_a = open_buffer(&project, path!("/project/a.rs"), cx).await;
|
||||
let buffer_c = open_buffer(&project, path!("/project/c.rs"), cx).await;
|
||||
|
||||
add_bookmarks(&project, &buffer_b, &[0], cx);
|
||||
add_bookmarks(&project, &buffer_a, &[0], cx);
|
||||
add_bookmarks(&project, &buffer_c, &[0], cx);
|
||||
|
||||
let paths: Vec<_> = get_all_bookmarks(&project, cx).keys().cloned().collect();
|
||||
assert_eq!(
|
||||
paths,
|
||||
[
|
||||
project_path(path!("/project/a.rs")),
|
||||
project_path(path!("/project/b.rs")),
|
||||
project_path(path!("/project/c.rs")),
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_all_serialized_bookmarks_deduplicates_same_row(cx: &mut TestAppContext) {
|
||||
init_test(cx);
|
||||
cx.executor().allow_parking();
|
||||
|
||||
let fs = fs::FakeFs::new(cx.executor());
|
||||
fs.insert_tree(
|
||||
path!("/project"),
|
||||
json!({"file1.rs": "line1\nline2\nline3\nline4\n"}),
|
||||
)
|
||||
.await;
|
||||
|
||||
let project = Project::test(fs, [path!("/project").as_ref()], cx).await;
|
||||
let buffer = open_buffer(&project, path!("/project/file1.rs"), cx).await;
|
||||
|
||||
add_bookmarks(&project, &buffer, &[1, 2], cx);
|
||||
|
||||
let bookmarks = get_all_bookmarks(&project, cx);
|
||||
assert_bookmark_rows(&bookmarks, path!("/project/file1.rs"), &[1, 2]);
|
||||
|
||||
// Verify no duplicates
|
||||
let rows: Vec<u32> = bookmarks
|
||||
.get(&project_path(path!("/project/file1.rs")))
|
||||
.unwrap()
|
||||
.iter()
|
||||
.map(|b| b.0)
|
||||
.collect();
|
||||
let mut deduped = rows.clone();
|
||||
deduped.dedup();
|
||||
assert_eq!(rows, deduped);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_with_serialized_bookmarks_restores_bookmarks(cx: &mut TestAppContext) {
|
||||
init_test(cx);
|
||||
cx.executor().allow_parking();
|
||||
|
||||
let fs = fs::FakeFs::new(cx.executor());
|
||||
fs.insert_tree(
|
||||
path!("/project"),
|
||||
json!({
|
||||
"file1.rs": "line1\nline2\nline3\nline4\nline5\n",
|
||||
"file2.rs": "aaa\nbbb\nccc\n"
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
|
||||
let project = Project::test(fs, [path!("/project").as_ref()], cx).await;
|
||||
|
||||
let serialized = build_serialized(&[
|
||||
(path!("/project/file1.rs"), &[0, 3]),
|
||||
(path!("/project/file2.rs"), &[1]),
|
||||
]);
|
||||
|
||||
restore_bookmarks(&project, serialized, cx).await;
|
||||
|
||||
let restored = get_all_bookmarks(&project, cx);
|
||||
assert_eq!(restored.len(), 2);
|
||||
assert_bookmark_rows(&restored, path!("/project/file1.rs"), &[0, 3]);
|
||||
assert_bookmark_rows(&restored, path!("/project/file2.rs"), &[1]);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_with_serialized_bookmarks_skips_out_of_range_rows(cx: &mut TestAppContext) {
|
||||
init_test(cx);
|
||||
cx.executor().allow_parking();
|
||||
|
||||
let fs = fs::FakeFs::new(cx.executor());
|
||||
// 3 lines: rows 0, 1, 2
|
||||
fs.insert_tree(
|
||||
path!("/project"),
|
||||
json!({"file1.rs": "line1\nline2\nline3"}),
|
||||
)
|
||||
.await;
|
||||
|
||||
let project = Project::test(fs, [path!("/project").as_ref()], cx).await;
|
||||
|
||||
let serialized = build_serialized(&[(path!("/project/file1.rs"), &[1, 100, 2])]);
|
||||
restore_bookmarks(&project, serialized, cx).await;
|
||||
|
||||
// Before resolution, unloaded bookmarks are stored as-is
|
||||
let unresolved = get_all_bookmarks(&project, cx);
|
||||
assert_bookmark_rows(&unresolved, path!("/project/file1.rs"), &[1, 2, 100]);
|
||||
|
||||
// Open the buffer to trigger lazy resolution
|
||||
let buffer = open_buffer(&project, path!("/project/file1.rs"), cx).await;
|
||||
project.update(cx, |project, cx| {
|
||||
let buffer_snapshot = buffer.read(cx).snapshot();
|
||||
project.bookmark_store().update(cx, |store, cx| {
|
||||
store.bookmarks_for_buffer(
|
||||
buffer.clone(),
|
||||
buffer_snapshot.anchor_before(0)
|
||||
..buffer_snapshot.anchor_after(buffer_snapshot.len()),
|
||||
&buffer_snapshot,
|
||||
cx,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
// After resolution, out-of-range rows are filtered
|
||||
let restored = get_all_bookmarks(&project, cx);
|
||||
assert_bookmark_rows(&restored, path!("/project/file1.rs"), &[1, 2]);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_with_serialized_bookmarks_skips_empty_entries(cx: &mut TestAppContext) {
|
||||
init_test(cx);
|
||||
cx.executor().allow_parking();
|
||||
|
||||
let fs = fs::FakeFs::new(cx.executor());
|
||||
fs.insert_tree(
|
||||
path!("/project"),
|
||||
json!({"file1.rs": "line1\nline2\n", "file2.rs": "aaa\nbbb\n"}),
|
||||
)
|
||||
.await;
|
||||
|
||||
let project = Project::test(fs, [path!("/project").as_ref()], cx).await;
|
||||
|
||||
let mut serialized = build_serialized(&[(path!("/project/file1.rs"), &[0])]);
|
||||
serialized.insert(project_path(path!("/project/file2.rs")), vec![]);
|
||||
|
||||
restore_bookmarks(&project, serialized, cx).await;
|
||||
|
||||
let restored = get_all_bookmarks(&project, cx);
|
||||
assert_eq!(restored.len(), 1);
|
||||
assert!(restored.contains_key(&project_path(path!("/project/file1.rs"))));
|
||||
assert!(!restored.contains_key(&project_path(path!("/project/file2.rs"))));
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_with_serialized_bookmarks_all_out_of_range_produces_no_entry(
|
||||
cx: &mut TestAppContext,
|
||||
) {
|
||||
init_test(cx);
|
||||
cx.executor().allow_parking();
|
||||
|
||||
let fs = fs::FakeFs::new(cx.executor());
|
||||
fs.insert_tree(path!("/project"), json!({"tiny.rs": "x"}))
|
||||
.await;
|
||||
|
||||
let project = Project::test(fs, [path!("/project").as_ref()], cx).await;
|
||||
|
||||
let serialized = build_serialized(&[(path!("/project/tiny.rs"), &[5, 10])]);
|
||||
restore_bookmarks(&project, serialized, cx).await;
|
||||
|
||||
// Before resolution, unloaded bookmarks are stored as-is
|
||||
let unresolved = get_all_bookmarks(&project, cx);
|
||||
assert_eq!(unresolved.len(), 1);
|
||||
|
||||
// Open the buffer to trigger lazy resolution
|
||||
let buffer = open_buffer(&project, path!("/project/tiny.rs"), cx).await;
|
||||
project.update(cx, |project, cx| {
|
||||
let buffer_snapshot = buffer.read(cx).snapshot();
|
||||
project.bookmark_store().update(cx, |store, cx| {
|
||||
store.bookmarks_for_buffer(
|
||||
buffer.clone(),
|
||||
buffer_snapshot.anchor_before(0)
|
||||
..buffer_snapshot.anchor_after(buffer_snapshot.len()),
|
||||
&buffer_snapshot,
|
||||
cx,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
// After resolution, all out-of-range rows are filtered away
|
||||
assert!(get_all_bookmarks(&project, cx).is_empty());
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_with_serialized_bookmarks_replaces_existing(cx: &mut TestAppContext) {
|
||||
init_test(cx);
|
||||
cx.executor().allow_parking();
|
||||
|
||||
let fs = fs::FakeFs::new(cx.executor());
|
||||
fs.insert_tree(
|
||||
path!("/project"),
|
||||
json!({"file1.rs": "aaa\nbbb\nccc\nddd\n"}),
|
||||
)
|
||||
.await;
|
||||
|
||||
let project = Project::test(fs, [path!("/project").as_ref()], cx).await;
|
||||
let buffer = open_buffer(&project, path!("/project/file1.rs"), cx).await;
|
||||
|
||||
add_bookmarks(&project, &buffer, &[0], cx);
|
||||
assert_bookmark_rows(
|
||||
&get_all_bookmarks(&project, cx),
|
||||
path!("/project/file1.rs"),
|
||||
&[0],
|
||||
);
|
||||
|
||||
// Restoring different bookmarks should replace, not merge
|
||||
let serialized = build_serialized(&[(path!("/project/file1.rs"), &[2, 3])]);
|
||||
restore_bookmarks(&project, serialized, cx).await;
|
||||
|
||||
let after = get_all_bookmarks(&project, cx);
|
||||
assert_eq!(after.len(), 1);
|
||||
assert_bookmark_rows(&after, path!("/project/file1.rs"), &[2, 3]);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_serialize_deserialize_round_trip(cx: &mut TestAppContext) {
|
||||
init_test(cx);
|
||||
cx.executor().allow_parking();
|
||||
|
||||
let fs = fs::FakeFs::new(cx.executor());
|
||||
fs.insert_tree(
|
||||
path!("/project"),
|
||||
json!({
|
||||
"alpha.rs": "fn main() {\n println!(\"hello\");\n return;\n}\n",
|
||||
"beta.rs": "use std::io;\nfn read() {}\nfn write() {}\n"
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
|
||||
let project = Project::test(fs, [path!("/project").as_ref()], cx).await;
|
||||
let buffer_alpha = open_buffer(&project, path!("/project/alpha.rs"), cx).await;
|
||||
let buffer_beta = open_buffer(&project, path!("/project/beta.rs"), cx).await;
|
||||
|
||||
add_bookmarks(&project, &buffer_alpha, &[0, 2, 3], cx);
|
||||
add_bookmarks(&project, &buffer_beta, &[1], cx);
|
||||
|
||||
// Serialize
|
||||
let serialized = get_all_bookmarks(&project, cx);
|
||||
assert_eq!(serialized.len(), 2);
|
||||
assert_bookmark_rows(&serialized, path!("/project/alpha.rs"), &[0, 2, 3]);
|
||||
assert_bookmark_rows(&serialized, path!("/project/beta.rs"), &[1]);
|
||||
|
||||
// Clear and restore
|
||||
clear_bookmarks(&project, cx);
|
||||
assert!(get_all_bookmarks(&project, cx).is_empty());
|
||||
|
||||
restore_bookmarks(&project, serialized, cx).await;
|
||||
|
||||
let restored = get_all_bookmarks(&project, cx);
|
||||
assert_eq!(restored.len(), 2);
|
||||
assert_bookmark_rows(&restored, path!("/project/alpha.rs"), &[0, 2, 3]);
|
||||
assert_bookmark_rows(&restored, path!("/project/beta.rs"), &[1]);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_round_trip_preserves_bookmarks_after_file_edit(cx: &mut TestAppContext) {
|
||||
init_test(cx);
|
||||
cx.executor().allow_parking();
|
||||
|
||||
let fs = fs::FakeFs::new(cx.executor());
|
||||
fs.insert_tree(
|
||||
path!("/project"),
|
||||
json!({"file.rs": "aaa\nbbb\nccc\nddd\neee\n"}),
|
||||
)
|
||||
.await;
|
||||
|
||||
let project = Project::test(fs, [path!("/project").as_ref()], cx).await;
|
||||
let buffer = open_buffer(&project, path!("/project/file.rs"), cx).await;
|
||||
|
||||
add_bookmarks(&project, &buffer, &[1, 3], cx);
|
||||
|
||||
// Insert a line at the beginning, shifting bookmarks down by 1
|
||||
buffer.update(cx, |buffer, cx| {
|
||||
buffer.edit([(0..0, "new_first_line\n")], None, cx);
|
||||
});
|
||||
|
||||
let serialized = get_all_bookmarks(&project, cx);
|
||||
assert_bookmark_rows(&serialized, path!("/project/file.rs"), &[2, 4]);
|
||||
|
||||
// Clear and restore
|
||||
clear_bookmarks(&project, cx);
|
||||
restore_bookmarks(&project, serialized, cx).await;
|
||||
|
||||
let restored = get_all_bookmarks(&project, cx);
|
||||
assert_bookmark_rows(&restored, path!("/project/file.rs"), &[2, 4]);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_file_deletion_removes_bookmarks(cx: &mut TestAppContext) {
|
||||
init_test(cx);
|
||||
cx.executor().allow_parking();
|
||||
|
||||
let fs = fs::FakeFs::new(cx.executor());
|
||||
fs.insert_tree(
|
||||
path!("/project"),
|
||||
json!({
|
||||
"file1.rs": "aaa\nbbb\nccc\n",
|
||||
"file2.rs": "ddd\neee\nfff\n"
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
|
||||
let project = Project::test(fs.clone(), [path!("/project").as_ref()], cx).await;
|
||||
let buffer1 = open_buffer(&project, path!("/project/file1.rs"), cx).await;
|
||||
let buffer2 = open_buffer(&project, path!("/project/file2.rs"), cx).await;
|
||||
|
||||
add_bookmarks(&project, &buffer1, &[0, 2], cx);
|
||||
add_bookmarks(&project, &buffer2, &[1], cx);
|
||||
assert_eq!(get_all_bookmarks(&project, cx).len(), 2);
|
||||
|
||||
// Delete file1.rs
|
||||
fs.remove_file(path!("/project/file1.rs").as_ref(), Default::default())
|
||||
.await
|
||||
.unwrap();
|
||||
cx.executor().run_until_parked();
|
||||
|
||||
// file1.rs bookmarks should be gone, file2.rs bookmarks preserved
|
||||
let bookmarks = get_all_bookmarks(&project, cx);
|
||||
assert_eq!(bookmarks.len(), 1);
|
||||
assert!(!bookmarks.contains_key(&project_path(path!("/project/file1.rs"))));
|
||||
assert_bookmark_rows(&bookmarks, path!("/project/file2.rs"), &[1]);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_deleting_all_bookmarked_files_clears_store(cx: &mut TestAppContext) {
|
||||
init_test(cx);
|
||||
cx.executor().allow_parking();
|
||||
|
||||
let fs = fs::FakeFs::new(cx.executor());
|
||||
fs.insert_tree(
|
||||
path!("/project"),
|
||||
json!({
|
||||
"file1.rs": "aaa\nbbb\n",
|
||||
"file2.rs": "ccc\nddd\n"
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
|
||||
let project = Project::test(fs.clone(), [path!("/project").as_ref()], cx).await;
|
||||
let buffer1 = open_buffer(&project, path!("/project/file1.rs"), cx).await;
|
||||
let buffer2 = open_buffer(&project, path!("/project/file2.rs"), cx).await;
|
||||
|
||||
add_bookmarks(&project, &buffer1, &[0], cx);
|
||||
add_bookmarks(&project, &buffer2, &[1], cx);
|
||||
assert_eq!(get_all_bookmarks(&project, cx).len(), 2);
|
||||
|
||||
// Delete both files
|
||||
fs.remove_file(path!("/project/file1.rs").as_ref(), Default::default())
|
||||
.await
|
||||
.unwrap();
|
||||
fs.remove_file(path!("/project/file2.rs").as_ref(), Default::default())
|
||||
.await
|
||||
.unwrap();
|
||||
cx.executor().run_until_parked();
|
||||
|
||||
assert!(get_all_bookmarks(&project, cx).is_empty());
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_file_rename_re_keys_bookmarks(cx: &mut TestAppContext) {
|
||||
init_test(cx);
|
||||
cx.executor().allow_parking();
|
||||
|
||||
let fs = fs::FakeFs::new(cx.executor());
|
||||
fs.insert_tree(path!("/project"), json!({"old_name.rs": "aaa\nbbb\nccc\n"}))
|
||||
.await;
|
||||
|
||||
let project = Project::test(fs.clone(), [path!("/project").as_ref()], cx).await;
|
||||
let buffer = open_buffer(&project, path!("/project/old_name.rs"), cx).await;
|
||||
|
||||
add_bookmarks(&project, &buffer, &[0, 2], cx);
|
||||
assert_bookmark_rows(
|
||||
&get_all_bookmarks(&project, cx),
|
||||
path!("/project/old_name.rs"),
|
||||
&[0, 2],
|
||||
);
|
||||
|
||||
// Rename the file
|
||||
fs.rename(
|
||||
path!("/project/old_name.rs").as_ref(),
|
||||
path!("/project/new_name.rs").as_ref(),
|
||||
Default::default(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
cx.executor().run_until_parked();
|
||||
|
||||
let bookmarks = get_all_bookmarks(&project, cx);
|
||||
assert_eq!(bookmarks.len(), 1);
|
||||
assert!(!bookmarks.contains_key(&project_path(path!("/project/old_name.rs"))));
|
||||
assert_bookmark_rows(&bookmarks, path!("/project/new_name.rs"), &[0, 2]);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_file_rename_preserves_other_bookmarks(cx: &mut TestAppContext) {
|
||||
init_test(cx);
|
||||
cx.executor().allow_parking();
|
||||
|
||||
let fs = fs::FakeFs::new(cx.executor());
|
||||
fs.insert_tree(
|
||||
path!("/project"),
|
||||
json!({
|
||||
"rename_me.rs": "aaa\nbbb\n",
|
||||
"untouched.rs": "ccc\nddd\neee\n"
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
|
||||
let project = Project::test(fs.clone(), [path!("/project").as_ref()], cx).await;
|
||||
let buffer_rename = open_buffer(&project, path!("/project/rename_me.rs"), cx).await;
|
||||
let buffer_other = open_buffer(&project, path!("/project/untouched.rs"), cx).await;
|
||||
|
||||
add_bookmarks(&project, &buffer_rename, &[1], cx);
|
||||
add_bookmarks(&project, &buffer_other, &[0, 2], cx);
|
||||
|
||||
fs.rename(
|
||||
path!("/project/rename_me.rs").as_ref(),
|
||||
path!("/project/renamed.rs").as_ref(),
|
||||
Default::default(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
cx.executor().run_until_parked();
|
||||
|
||||
let bookmarks = get_all_bookmarks(&project, cx);
|
||||
assert_eq!(bookmarks.len(), 2);
|
||||
assert_bookmark_rows(&bookmarks, path!("/project/renamed.rs"), &[1]);
|
||||
assert_bookmark_rows(&bookmarks, path!("/project/untouched.rs"), &[0, 2]);
|
||||
}
|
||||
}
|
||||
155
crates/project/tests/integration/color_extractor.rs
Normal file
155
crates/project/tests/integration/color_extractor.rs
Normal file
@@ -0,0 +1,155 @@
|
||||
use gpui::{Hsla, rgba};
|
||||
use lsp::{CompletionItem, CompletionItemKind, Documentation};
|
||||
use project::color_extractor::*;
|
||||
|
||||
pub const COLOR_TABLE: &[(&str, Option<u32>)] = &[
|
||||
// -- Invalid --
|
||||
// Invalid hex
|
||||
("f0f", None),
|
||||
("#fof", None),
|
||||
// Extra field
|
||||
("rgb(255, 0, 0, 0.0)", None),
|
||||
("hsl(120, 0, 0, 0.0)", None),
|
||||
// Missing field
|
||||
("rgba(255, 0, 0)", None),
|
||||
("hsla(120, 0, 0)", None),
|
||||
// No decimal after zero
|
||||
("rgba(255, 0, 0, 0)", None),
|
||||
("hsla(120, 0, 0, 0)", None),
|
||||
// Decimal after one
|
||||
("rgba(255, 0, 0, 1.0)", None),
|
||||
("hsla(120, 0, 0, 1.0)", None),
|
||||
// HEX (sRGB)
|
||||
("#f0f", Some(0xFF00FFFF)),
|
||||
("#ff0000", Some(0xFF0000FF)),
|
||||
// RGB / RGBA (sRGB)
|
||||
("rgb(255, 0, 0)", Some(0xFF0000FF)),
|
||||
("rgba(255, 0, 0, 0.4)", Some(0xFF000066)),
|
||||
("rgba(255, 0, 0, 1)", Some(0xFF0000FF)),
|
||||
("rgb(20%, 0%, 0%)", Some(0x330000FF)),
|
||||
("rgba(20%, 0%, 0%, 1)", Some(0x330000FF)),
|
||||
("rgb(0%, 20%, 0%)", Some(0x003300FF)),
|
||||
("rgba(0%, 20%, 0%, 1)", Some(0x003300FF)),
|
||||
("rgb(0%, 0%, 20%)", Some(0x000033FF)),
|
||||
("rgba(0%, 0%, 20%, 1)", Some(0x000033FF)),
|
||||
// HSL / HSLA (sRGB)
|
||||
("hsl(0, 100%, 50%)", Some(0xFF0000FF)),
|
||||
("hsl(120, 100%, 50%)", Some(0x00FF00FF)),
|
||||
("hsla(0, 100%, 50%, 0.0)", Some(0xFF000000)),
|
||||
("hsla(0, 100%, 50%, 0.4)", Some(0xFF000066)),
|
||||
("hsla(0, 100%, 50%, 1)", Some(0xFF0000FF)),
|
||||
("hsla(120, 100%, 50%, 0.0)", Some(0x00FF0000)),
|
||||
("hsla(120, 100%, 50%, 0.4)", Some(0x00FF0066)),
|
||||
("hsla(120, 100%, 50%, 1)", Some(0x00FF00FF)),
|
||||
];
|
||||
|
||||
#[test]
|
||||
fn can_extract_from_label() {
|
||||
for (color_str, color_val) in COLOR_TABLE.iter() {
|
||||
let color = extract_color(&CompletionItem {
|
||||
kind: Some(CompletionItemKind::COLOR),
|
||||
label: color_str.to_string(),
|
||||
detail: None,
|
||||
documentation: None,
|
||||
..Default::default()
|
||||
});
|
||||
|
||||
assert_eq!(color, color_val.map(|v| Hsla::from(rgba(v))));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn only_whole_label_matches_are_allowed() {
|
||||
for (color_str, _) in COLOR_TABLE.iter() {
|
||||
let color = extract_color(&CompletionItem {
|
||||
kind: Some(CompletionItemKind::COLOR),
|
||||
label: format!("{} foo", color_str).to_string(),
|
||||
detail: None,
|
||||
documentation: None,
|
||||
..Default::default()
|
||||
});
|
||||
|
||||
assert_eq!(color, None);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn can_extract_from_detail() {
|
||||
for (color_str, color_val) in COLOR_TABLE.iter() {
|
||||
let color = extract_color(&CompletionItem {
|
||||
kind: Some(CompletionItemKind::COLOR),
|
||||
label: "".to_string(),
|
||||
detail: Some(color_str.to_string()),
|
||||
documentation: None,
|
||||
..Default::default()
|
||||
});
|
||||
|
||||
assert_eq!(color, color_val.map(|v| Hsla::from(rgba(v))));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn only_whole_detail_matches_are_allowed() {
|
||||
for (color_str, _) in COLOR_TABLE.iter() {
|
||||
let color = extract_color(&CompletionItem {
|
||||
kind: Some(CompletionItemKind::COLOR),
|
||||
label: "".to_string(),
|
||||
detail: Some(format!("{} foo", color_str).to_string()),
|
||||
documentation: None,
|
||||
..Default::default()
|
||||
});
|
||||
|
||||
assert_eq!(color, None);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn can_extract_from_documentation_start() {
|
||||
for (color_str, color_val) in COLOR_TABLE.iter() {
|
||||
let color = extract_color(&CompletionItem {
|
||||
kind: Some(CompletionItemKind::COLOR),
|
||||
label: "".to_string(),
|
||||
detail: None,
|
||||
documentation: Some(Documentation::String(
|
||||
format!("{} foo", color_str).to_string(),
|
||||
)),
|
||||
..Default::default()
|
||||
});
|
||||
|
||||
assert_eq!(color, color_val.map(|v| Hsla::from(rgba(v))));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn can_extract_from_documentation_end() {
|
||||
for (color_str, color_val) in COLOR_TABLE.iter() {
|
||||
let color = extract_color(&CompletionItem {
|
||||
kind: Some(CompletionItemKind::COLOR),
|
||||
label: "".to_string(),
|
||||
detail: None,
|
||||
documentation: Some(Documentation::String(
|
||||
format!("foo {}", color_str).to_string(),
|
||||
)),
|
||||
..Default::default()
|
||||
});
|
||||
|
||||
assert_eq!(color, color_val.map(|v| Hsla::from(rgba(v))));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cannot_extract_from_documentation_middle() {
|
||||
for (color_str, _) in COLOR_TABLE.iter() {
|
||||
let color = extract_color(&CompletionItem {
|
||||
kind: Some(CompletionItemKind::COLOR),
|
||||
label: "".to_string(),
|
||||
detail: None,
|
||||
documentation: Some(Documentation::String(
|
||||
format!("foo {} foo", color_str).to_string(),
|
||||
)),
|
||||
..Default::default()
|
||||
});
|
||||
|
||||
assert_eq!(color, None);
|
||||
}
|
||||
}
|
||||
1078
crates/project/tests/integration/context_server_store.rs
Normal file
1078
crates/project/tests/integration/context_server_store.rs
Normal file
File diff suppressed because it is too large
Load Diff
301
crates/project/tests/integration/debugger.rs
Normal file
301
crates/project/tests/integration/debugger.rs
Normal file
@@ -0,0 +1,301 @@
|
||||
mod go_locator {
|
||||
use collections::HashMap;
|
||||
use dap::{DapLocator, adapters::DebugAdapterName};
|
||||
use gpui::TestAppContext;
|
||||
use project::debugger::locators::go::{DelveLaunchRequest, GoLocator};
|
||||
use task::{HideStrategy, RevealStrategy, RevealTarget, SaveStrategy, Shell, TaskTemplate};
|
||||
#[gpui::test]
|
||||
async fn test_create_scenario_for_go_build(_: &mut TestAppContext) {
|
||||
let locator = GoLocator;
|
||||
let task = TaskTemplate {
|
||||
label: "go build".into(),
|
||||
command: "go".into(),
|
||||
args: vec!["build".into(), ".".into()],
|
||||
env: Default::default(),
|
||||
cwd: Some("${ZED_WORKTREE_ROOT}".into()),
|
||||
use_new_terminal: false,
|
||||
allow_concurrent_runs: false,
|
||||
reveal: RevealStrategy::Always,
|
||||
reveal_target: RevealTarget::Dock,
|
||||
hide: HideStrategy::Never,
|
||||
shell: Shell::System,
|
||||
tags: vec![],
|
||||
show_summary: true,
|
||||
show_command: true,
|
||||
save: SaveStrategy::default(),
|
||||
hooks: Default::default(),
|
||||
};
|
||||
|
||||
let scenario = locator
|
||||
.create_scenario(&task, "test label", &DebugAdapterName("Delve".into()))
|
||||
.await;
|
||||
|
||||
assert!(scenario.is_none());
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_skip_non_go_commands_with_non_delve_adapter(_: &mut TestAppContext) {
|
||||
let locator = GoLocator;
|
||||
let task = TaskTemplate {
|
||||
label: "cargo build".into(),
|
||||
command: "cargo".into(),
|
||||
args: vec!["build".into()],
|
||||
env: Default::default(),
|
||||
cwd: Some("${ZED_WORKTREE_ROOT}".into()),
|
||||
use_new_terminal: false,
|
||||
allow_concurrent_runs: false,
|
||||
reveal: RevealStrategy::Always,
|
||||
reveal_target: RevealTarget::Dock,
|
||||
hide: HideStrategy::Never,
|
||||
shell: Shell::System,
|
||||
tags: vec![],
|
||||
show_summary: true,
|
||||
show_command: true,
|
||||
save: SaveStrategy::default(),
|
||||
hooks: Default::default(),
|
||||
};
|
||||
|
||||
let scenario = locator
|
||||
.create_scenario(
|
||||
&task,
|
||||
"test label",
|
||||
&DebugAdapterName("SomeOtherAdapter".into()),
|
||||
)
|
||||
.await;
|
||||
assert!(scenario.is_none());
|
||||
|
||||
let scenario = locator
|
||||
.create_scenario(&task, "test label", &DebugAdapterName("Delve".into()))
|
||||
.await;
|
||||
assert!(scenario.is_none());
|
||||
}
|
||||
#[gpui::test]
|
||||
async fn test_go_locator_run(_: &mut TestAppContext) {
|
||||
let locator = GoLocator;
|
||||
let delve = DebugAdapterName("Delve".into());
|
||||
|
||||
let task = TaskTemplate {
|
||||
label: "go run with flags".into(),
|
||||
command: "go".into(),
|
||||
args: vec![
|
||||
"run".to_string(),
|
||||
"-race".to_string(),
|
||||
"-ldflags".to_string(),
|
||||
"-X main.version=1.0".to_string(),
|
||||
"./cmd/myapp".to_string(),
|
||||
"--config".to_string(),
|
||||
"production.yaml".to_string(),
|
||||
"--verbose".to_string(),
|
||||
],
|
||||
env: {
|
||||
let mut env = HashMap::default();
|
||||
env.insert("GO_ENV".to_string(), "production".to_string());
|
||||
env
|
||||
},
|
||||
cwd: Some("/project/root".into()),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let scenario = locator
|
||||
.create_scenario(&task, "test run label", &delve)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let config: DelveLaunchRequest = serde_json::from_value(scenario.config).unwrap();
|
||||
|
||||
assert_eq!(
|
||||
config,
|
||||
DelveLaunchRequest {
|
||||
request: "launch".to_string(),
|
||||
mode: "debug".to_string(),
|
||||
program: "./cmd/myapp".to_string(),
|
||||
build_flags: vec![
|
||||
"-race".to_string(),
|
||||
"-ldflags".to_string(),
|
||||
"-X main.version=1.0".to_string()
|
||||
],
|
||||
args: vec![
|
||||
"--config".to_string(),
|
||||
"production.yaml".to_string(),
|
||||
"--verbose".to_string(),
|
||||
],
|
||||
env: {
|
||||
let mut env = HashMap::default();
|
||||
env.insert("GO_ENV".to_string(), "production".to_string());
|
||||
env
|
||||
},
|
||||
cwd: Some("/project/root".to_string()),
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_go_locator_test(_: &mut TestAppContext) {
|
||||
let locator = GoLocator;
|
||||
let delve = DebugAdapterName("Delve".into());
|
||||
|
||||
// Test with tags and run flag
|
||||
let task_with_tags = TaskTemplate {
|
||||
label: "test".into(),
|
||||
command: "go".into(),
|
||||
args: vec![
|
||||
"test".to_string(),
|
||||
"-tags".to_string(),
|
||||
"integration,unit".to_string(),
|
||||
"-run".to_string(),
|
||||
"Foo".to_string(),
|
||||
".".to_string(),
|
||||
],
|
||||
..Default::default()
|
||||
};
|
||||
let result = locator
|
||||
.create_scenario(&task_with_tags, "", &delve)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let config: DelveLaunchRequest = serde_json::from_value(result.config).unwrap();
|
||||
|
||||
assert_eq!(
|
||||
config,
|
||||
DelveLaunchRequest {
|
||||
request: "launch".to_string(),
|
||||
mode: "test".to_string(),
|
||||
program: ".".to_string(),
|
||||
build_flags: vec!["-tags".to_string(), "integration,unit".to_string(),],
|
||||
args: vec![
|
||||
"-test.run".to_string(),
|
||||
"Foo".to_string(),
|
||||
"-test.v".to_string()
|
||||
],
|
||||
env: Default::default(),
|
||||
cwd: None,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_skip_unsupported_go_commands(_: &mut TestAppContext) {
|
||||
let locator = GoLocator;
|
||||
let task = TaskTemplate {
|
||||
label: "go clean".into(),
|
||||
command: "go".into(),
|
||||
args: vec!["clean".into()],
|
||||
env: Default::default(),
|
||||
cwd: Some("${ZED_WORKTREE_ROOT}".into()),
|
||||
use_new_terminal: false,
|
||||
allow_concurrent_runs: false,
|
||||
reveal: RevealStrategy::Always,
|
||||
reveal_target: RevealTarget::Dock,
|
||||
hide: HideStrategy::Never,
|
||||
shell: Shell::System,
|
||||
tags: vec![],
|
||||
show_summary: true,
|
||||
show_command: true,
|
||||
save: SaveStrategy::default(),
|
||||
hooks: Default::default(),
|
||||
};
|
||||
|
||||
let scenario = locator
|
||||
.create_scenario(&task, "test label", &DebugAdapterName("Delve".into()))
|
||||
.await;
|
||||
assert!(scenario.is_none());
|
||||
}
|
||||
}
|
||||
|
||||
mod python_locator {
|
||||
use dap::{DapLocator, adapters::DebugAdapterName};
|
||||
use serde_json::json;
|
||||
|
||||
use project::debugger::locators::python::*;
|
||||
use task::{DebugScenario, TaskTemplate};
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_python_locator() {
|
||||
let adapter = DebugAdapterName("Debugpy".into());
|
||||
let build_task = TaskTemplate {
|
||||
label: "run module '$ZED_FILE'".into(),
|
||||
command: "$ZED_CUSTOM_PYTHON_ACTIVE_ZED_TOOLCHAIN".into(),
|
||||
args: vec!["-m".into(), "$ZED_CUSTOM_PYTHON_MODULE_NAME".into()],
|
||||
env: Default::default(),
|
||||
cwd: Some("$ZED_WORKTREE_ROOT".into()),
|
||||
use_new_terminal: false,
|
||||
allow_concurrent_runs: false,
|
||||
reveal: task::RevealStrategy::Always,
|
||||
reveal_target: task::RevealTarget::Dock,
|
||||
hide: task::HideStrategy::Never,
|
||||
tags: vec!["python-module-main-method".into()],
|
||||
shell: task::Shell::System,
|
||||
show_summary: false,
|
||||
show_command: false,
|
||||
save: task::SaveStrategy::default(),
|
||||
hooks: Default::default(),
|
||||
};
|
||||
|
||||
let expected_scenario = DebugScenario {
|
||||
adapter: "Debugpy".into(),
|
||||
label: "run module 'main.py'".into(),
|
||||
build: None,
|
||||
config: json!({
|
||||
"request": "launch",
|
||||
"python": "$ZED_CUSTOM_PYTHON_ACTIVE_ZED_TOOLCHAIN",
|
||||
"args": [],
|
||||
"cwd": "$ZED_WORKTREE_ROOT",
|
||||
"module": "$ZED_CUSTOM_PYTHON_MODULE_NAME",
|
||||
}),
|
||||
tcp_connection: None,
|
||||
};
|
||||
|
||||
assert_eq!(
|
||||
PythonLocator
|
||||
.create_scenario(&build_task, "run module 'main.py'", &adapter)
|
||||
.await
|
||||
.expect("Failed to create a scenario"),
|
||||
expected_scenario
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
mod memory {
|
||||
use project::debugger::{
|
||||
MemoryCell,
|
||||
memory::{MemoryIterator, PageAddress, PageContents},
|
||||
};
|
||||
|
||||
#[test]
|
||||
fn iterate_over_unmapped_memory() {
|
||||
let empty_iterator = MemoryIterator::new(0..=127, Default::default());
|
||||
let actual = empty_iterator.collect::<Vec<_>>();
|
||||
let expected = vec![MemoryCell(None); 128];
|
||||
assert_eq!(actual.len(), expected.len());
|
||||
assert_eq!(actual, expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn iterate_over_partially_mapped_memory() {
|
||||
let it = MemoryIterator::new(
|
||||
0..=127,
|
||||
vec![(PageAddress(5), PageContents::mapped(vec![1]))].into_iter(),
|
||||
);
|
||||
let actual = it.collect::<Vec<_>>();
|
||||
let expected = std::iter::repeat_n(MemoryCell(None), 5)
|
||||
.chain(std::iter::once(MemoryCell(Some(1))))
|
||||
.chain(std::iter::repeat_n(MemoryCell(None), 122))
|
||||
.collect::<Vec<_>>();
|
||||
assert_eq!(actual.len(), expected.len());
|
||||
assert_eq!(actual, expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reads_from_the_middle_of_a_page() {
|
||||
let partial_iter = MemoryIterator::new(
|
||||
20..=30,
|
||||
vec![(PageAddress(0), PageContents::mapped((0..255).collect()))].into_iter(),
|
||||
);
|
||||
let actual = partial_iter.collect::<Vec<_>>();
|
||||
let expected = (20..=30)
|
||||
.map(|val| MemoryCell(Some(val)))
|
||||
.collect::<Vec<_>>();
|
||||
assert_eq!(actual.len(), expected.len());
|
||||
assert_eq!(actual, expected);
|
||||
}
|
||||
}
|
||||
224
crates/project/tests/integration/ext_agent_tests.rs
Normal file
224
crates/project/tests/integration/ext_agent_tests.rs
Normal file
@@ -0,0 +1,224 @@
|
||||
use anyhow::Result;
|
||||
use collections::HashMap;
|
||||
use gpui::{AsyncApp, SharedString, Task};
|
||||
use project::agent_server_store::*;
|
||||
use std::{any::Any, collections::HashSet, fmt::Write as _, path::PathBuf};
|
||||
// A simple fake that implements ExternalAgentServer without needing async plumbing.
|
||||
struct NoopExternalAgent;
|
||||
|
||||
impl ExternalAgentServer for NoopExternalAgent {
|
||||
fn get_command(
|
||||
&self,
|
||||
_extra_args: Vec<String>,
|
||||
_extra_env: HashMap<String, String>,
|
||||
_cx: &mut AsyncApp,
|
||||
) -> Task<Result<AgentServerCommand>> {
|
||||
Task::ready(Ok(AgentServerCommand {
|
||||
path: PathBuf::from("noop"),
|
||||
args: Vec::new(),
|
||||
env: None,
|
||||
}))
|
||||
}
|
||||
|
||||
fn as_any(&self) -> &dyn Any {
|
||||
self
|
||||
}
|
||||
|
||||
fn as_any_mut(&mut self) -> &mut dyn Any {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn external_agent_server_name_display() {
|
||||
let name = AgentId(SharedString::from("Ext: Tool"));
|
||||
let mut s = String::new();
|
||||
write!(&mut s, "{name}").unwrap();
|
||||
assert_eq!(s, "Ext: Tool");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sync_extension_agents_removes_previous_extension_entries() {
|
||||
let mut store = AgentServerStore::collab();
|
||||
|
||||
// Seed with a couple of agents that will be replaced by extensions
|
||||
store.external_agents.insert(
|
||||
AgentId(SharedString::from("foo-agent")),
|
||||
ExternalAgentEntry::new(
|
||||
Box::new(NoopExternalAgent) as Box<dyn ExternalAgentServer>,
|
||||
ExternalAgentSource::Custom,
|
||||
None,
|
||||
None,
|
||||
),
|
||||
);
|
||||
store.external_agents.insert(
|
||||
AgentId(SharedString::from("bar-agent")),
|
||||
ExternalAgentEntry::new(
|
||||
Box::new(NoopExternalAgent) as Box<dyn ExternalAgentServer>,
|
||||
ExternalAgentSource::Custom,
|
||||
None,
|
||||
None,
|
||||
),
|
||||
);
|
||||
store.external_agents.insert(
|
||||
AgentId(SharedString::from("custom")),
|
||||
ExternalAgentEntry::new(
|
||||
Box::new(NoopExternalAgent) as Box<dyn ExternalAgentServer>,
|
||||
ExternalAgentSource::Custom,
|
||||
None,
|
||||
None,
|
||||
),
|
||||
);
|
||||
|
||||
// Simulate the removal phase: if we're syncing extensions that provide
|
||||
// "foo-agent" and "bar-agent", those should be removed first
|
||||
let extension_agent_names: HashSet<String> = ["foo-agent".to_string(), "bar-agent".to_string()]
|
||||
.into_iter()
|
||||
.collect();
|
||||
|
||||
let keys_to_remove: Vec<_> = store
|
||||
.external_agents
|
||||
.keys()
|
||||
.filter(|name| extension_agent_names.contains(name.0.as_ref()))
|
||||
.cloned()
|
||||
.collect();
|
||||
|
||||
for key in keys_to_remove {
|
||||
store.external_agents.remove(&key);
|
||||
}
|
||||
|
||||
// Only the custom entry should remain.
|
||||
let remaining: Vec<_> = store
|
||||
.external_agents
|
||||
.keys()
|
||||
.map(|k| k.0.to_string())
|
||||
.collect();
|
||||
assert_eq!(remaining, vec!["custom".to_string()]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolve_extension_icon_path_allows_valid_paths() {
|
||||
// Create a temporary directory structure for testing
|
||||
let temp_dir = tempfile::tempdir().unwrap();
|
||||
let extensions_dir = temp_dir.path();
|
||||
let ext_dir = extensions_dir.join("my-extension");
|
||||
std::fs::create_dir_all(&ext_dir).unwrap();
|
||||
|
||||
// Create a valid icon file
|
||||
let icon_path = ext_dir.join("icon.svg");
|
||||
std::fs::write(&icon_path, "<svg></svg>").unwrap();
|
||||
|
||||
// Test that a valid relative path works
|
||||
let result = project::agent_server_store::resolve_extension_icon_path(
|
||||
extensions_dir,
|
||||
"my-extension",
|
||||
"icon.svg",
|
||||
);
|
||||
assert!(result.is_some());
|
||||
assert!(result.unwrap().ends_with("icon.svg"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolve_extension_icon_path_allows_nested_paths() {
|
||||
let temp_dir = tempfile::tempdir().unwrap();
|
||||
let extensions_dir = temp_dir.path();
|
||||
let ext_dir = extensions_dir.join("my-extension");
|
||||
let icons_dir = ext_dir.join("assets").join("icons");
|
||||
std::fs::create_dir_all(&icons_dir).unwrap();
|
||||
|
||||
let icon_path = icons_dir.join("logo.svg");
|
||||
std::fs::write(&icon_path, "<svg></svg>").unwrap();
|
||||
|
||||
let result = project::agent_server_store::resolve_extension_icon_path(
|
||||
extensions_dir,
|
||||
"my-extension",
|
||||
"assets/icons/logo.svg",
|
||||
);
|
||||
assert!(result.is_some());
|
||||
assert!(result.unwrap().ends_with("logo.svg"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolve_extension_icon_path_blocks_path_traversal() {
|
||||
let temp_dir = tempfile::tempdir().unwrap();
|
||||
let extensions_dir = temp_dir.path();
|
||||
|
||||
// Create two extension directories
|
||||
let ext1_dir = extensions_dir.join("extension1");
|
||||
let ext2_dir = extensions_dir.join("extension2");
|
||||
std::fs::create_dir_all(&ext1_dir).unwrap();
|
||||
std::fs::create_dir_all(&ext2_dir).unwrap();
|
||||
|
||||
// Create a file in extension2
|
||||
let secret_file = ext2_dir.join("secret.svg");
|
||||
std::fs::write(&secret_file, "<svg>secret</svg>").unwrap();
|
||||
|
||||
// Try to access extension2's file from extension1 using path traversal
|
||||
let result = project::agent_server_store::resolve_extension_icon_path(
|
||||
extensions_dir,
|
||||
"extension1",
|
||||
"../extension2/secret.svg",
|
||||
);
|
||||
assert!(
|
||||
result.is_none(),
|
||||
"Path traversal to sibling extension should be blocked"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolve_extension_icon_path_blocks_absolute_escape() {
|
||||
let temp_dir = tempfile::tempdir().unwrap();
|
||||
let extensions_dir = temp_dir.path();
|
||||
let ext_dir = extensions_dir.join("my-extension");
|
||||
std::fs::create_dir_all(&ext_dir).unwrap();
|
||||
|
||||
// Create a file outside the extensions directory
|
||||
let outside_file = temp_dir.path().join("outside.svg");
|
||||
std::fs::write(&outside_file, "<svg>outside</svg>").unwrap();
|
||||
|
||||
// Try to escape to parent directory
|
||||
let result = project::agent_server_store::resolve_extension_icon_path(
|
||||
extensions_dir,
|
||||
"my-extension",
|
||||
"../outside.svg",
|
||||
);
|
||||
assert!(
|
||||
result.is_none(),
|
||||
"Path traversal to parent directory should be blocked"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolve_extension_icon_path_blocks_deep_traversal() {
|
||||
let temp_dir = tempfile::tempdir().unwrap();
|
||||
let extensions_dir = temp_dir.path();
|
||||
let ext_dir = extensions_dir.join("my-extension");
|
||||
std::fs::create_dir_all(&ext_dir).unwrap();
|
||||
|
||||
// Try deep path traversal
|
||||
let result = project::agent_server_store::resolve_extension_icon_path(
|
||||
extensions_dir,
|
||||
"my-extension",
|
||||
"../../../../../../etc/passwd",
|
||||
);
|
||||
assert!(
|
||||
result.is_none(),
|
||||
"Deep path traversal should be blocked (file doesn't exist)"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolve_extension_icon_path_returns_none_for_nonexistent() {
|
||||
let temp_dir = tempfile::tempdir().unwrap();
|
||||
let extensions_dir = temp_dir.path();
|
||||
let ext_dir = extensions_dir.join("my-extension");
|
||||
std::fs::create_dir_all(&ext_dir).unwrap();
|
||||
|
||||
// Try to access a file that doesn't exist
|
||||
let result = project::agent_server_store::resolve_extension_icon_path(
|
||||
extensions_dir,
|
||||
"my-extension",
|
||||
"nonexistent.svg",
|
||||
);
|
||||
assert!(result.is_none(), "Nonexistent file should return None");
|
||||
}
|
||||
332
crates/project/tests/integration/extension_agent_tests.rs
Normal file
332
crates/project/tests/integration/extension_agent_tests.rs
Normal file
@@ -0,0 +1,332 @@
|
||||
use anyhow::Result;
|
||||
use collections::HashMap;
|
||||
use gpui::{AppContext, AsyncApp, SharedString, Task, TestAppContext};
|
||||
use node_runtime::NodeRuntime;
|
||||
use project::worktree_store::WorktreeStore;
|
||||
use project::{agent_server_store::*, worktree_store::WorktreeIdCounter};
|
||||
use std::{any::Any, path::PathBuf, sync::Arc};
|
||||
|
||||
#[test]
|
||||
fn extension_agent_constructs_proper_display_names() {
|
||||
// Verify the display name format for extension-provided agents
|
||||
let name1 = AgentId(SharedString::from("Extension: Agent"));
|
||||
assert!(name1.0.contains(": "));
|
||||
|
||||
let name2 = AgentId(SharedString::from("MyExt: MyAgent"));
|
||||
assert_eq!(name2.0, "MyExt: MyAgent");
|
||||
|
||||
// Non-extension agents shouldn't have the separator
|
||||
let custom = AgentId(SharedString::from("custom"));
|
||||
assert!(!custom.0.contains(": "));
|
||||
}
|
||||
|
||||
struct NoopExternalAgent;
|
||||
|
||||
impl ExternalAgentServer for NoopExternalAgent {
|
||||
fn get_command(
|
||||
&self,
|
||||
_extra_args: Vec<String>,
|
||||
_extra_env: HashMap<String, String>,
|
||||
_cx: &mut AsyncApp,
|
||||
) -> Task<Result<AgentServerCommand>> {
|
||||
Task::ready(Ok(AgentServerCommand {
|
||||
path: PathBuf::from("noop"),
|
||||
args: Vec::new(),
|
||||
env: None,
|
||||
}))
|
||||
}
|
||||
|
||||
fn as_any(&self) -> &dyn Any {
|
||||
self
|
||||
}
|
||||
|
||||
fn as_any_mut(&mut self) -> &mut dyn Any {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sync_removes_only_extension_provided_agents() {
|
||||
let mut store = AgentServerStore::collab();
|
||||
|
||||
// Seed with extension agents (contain ": ") and custom agents (don't contain ": ")
|
||||
store.external_agents.insert(
|
||||
AgentId(SharedString::from("Ext1: Agent1")),
|
||||
ExternalAgentEntry::new(
|
||||
Box::new(NoopExternalAgent) as Box<dyn ExternalAgentServer>,
|
||||
ExternalAgentSource::Extension,
|
||||
None,
|
||||
None,
|
||||
),
|
||||
);
|
||||
store.external_agents.insert(
|
||||
AgentId(SharedString::from("Ext2: Agent2")),
|
||||
ExternalAgentEntry::new(
|
||||
Box::new(NoopExternalAgent) as Box<dyn ExternalAgentServer>,
|
||||
ExternalAgentSource::Extension,
|
||||
None,
|
||||
None,
|
||||
),
|
||||
);
|
||||
store.external_agents.insert(
|
||||
AgentId(SharedString::from("custom-agent")),
|
||||
ExternalAgentEntry::new(
|
||||
Box::new(NoopExternalAgent) as Box<dyn ExternalAgentServer>,
|
||||
ExternalAgentSource::Custom,
|
||||
None,
|
||||
None,
|
||||
),
|
||||
);
|
||||
|
||||
// Simulate removal phase
|
||||
store
|
||||
.external_agents
|
||||
.retain(|_, entry| entry.source != ExternalAgentSource::Extension);
|
||||
|
||||
// Only custom-agent should remain
|
||||
assert_eq!(store.external_agents.len(), 1);
|
||||
assert!(
|
||||
store
|
||||
.external_agents
|
||||
.contains_key(&AgentId(SharedString::from("custom-agent")))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn archive_launcher_constructs_with_all_fields() {
|
||||
use extension::AgentServerManifestEntry;
|
||||
|
||||
let mut env = HashMap::default();
|
||||
env.insert("GITHUB_TOKEN".into(), "secret".into());
|
||||
|
||||
let mut targets = HashMap::default();
|
||||
targets.insert(
|
||||
"darwin-aarch64".to_string(),
|
||||
extension::TargetConfig {
|
||||
archive:
|
||||
"https://github.com/owner/repo/releases/download/v1.0.0/agent-darwin-arm64.zip"
|
||||
.into(),
|
||||
cmd: "./agent".into(),
|
||||
args: vec![],
|
||||
sha256: None,
|
||||
env: Default::default(),
|
||||
},
|
||||
);
|
||||
|
||||
let _entry = AgentServerManifestEntry {
|
||||
name: "GitHub Agent".into(),
|
||||
targets,
|
||||
env,
|
||||
icon: None,
|
||||
};
|
||||
|
||||
// Verify display name construction
|
||||
let expected_name = AgentId(SharedString::from("GitHub Agent"));
|
||||
assert_eq!(expected_name.0, "GitHub Agent");
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn archive_agent_uses_extension_and_agent_id_for_cache_key(cx: &mut TestAppContext) {
|
||||
let fs = fs::FakeFs::new(cx.background_executor.clone());
|
||||
let http_client = http_client::FakeHttpClient::with_404_response();
|
||||
let worktree_store =
|
||||
cx.new(|cx| WorktreeStore::local(false, fs.clone(), WorktreeIdCounter::get(cx)));
|
||||
let project_environment = cx.new(|cx| {
|
||||
crate::ProjectEnvironment::new(None, worktree_store.downgrade(), None, false, cx)
|
||||
});
|
||||
|
||||
let agent = LocalExtensionArchiveAgent {
|
||||
fs,
|
||||
http_client,
|
||||
node_runtime: node_runtime::NodeRuntime::unavailable(),
|
||||
project_environment,
|
||||
extension_id: Arc::from("my-extension"),
|
||||
agent_id: Arc::from("my-agent"),
|
||||
version: Some(SharedString::from("1.0.0")),
|
||||
targets: {
|
||||
let mut map = HashMap::default();
|
||||
map.insert(
|
||||
"darwin-aarch64".to_string(),
|
||||
extension::TargetConfig {
|
||||
archive: "https://example.com/my-agent-darwin-arm64.zip".into(),
|
||||
cmd: "./my-agent".into(),
|
||||
args: vec!["--serve".into()],
|
||||
sha256: None,
|
||||
env: Default::default(),
|
||||
},
|
||||
);
|
||||
map
|
||||
},
|
||||
env: {
|
||||
let mut map = HashMap::default();
|
||||
map.insert("PORT".into(), "8080".into());
|
||||
map
|
||||
},
|
||||
new_version_available_tx: None,
|
||||
};
|
||||
|
||||
// Verify agent is properly constructed
|
||||
assert_eq!(agent.extension_id.as_ref(), "my-extension");
|
||||
assert_eq!(agent.agent_id.as_ref(), "my-agent");
|
||||
assert_eq!(agent.env.get("PORT"), Some(&"8080".to_string()));
|
||||
assert!(agent.targets.contains_key("darwin-aarch64"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sync_extension_agents_registers_archive_launcher() {
|
||||
use extension::AgentServerManifestEntry;
|
||||
|
||||
let expected_name = AgentId(SharedString::from("Release Agent"));
|
||||
assert_eq!(expected_name.0, "Release Agent");
|
||||
|
||||
// Verify the manifest entry structure for archive-based installation
|
||||
let mut env = HashMap::default();
|
||||
env.insert("API_KEY".into(), "secret".into());
|
||||
|
||||
let mut targets = HashMap::default();
|
||||
targets.insert(
|
||||
"linux-x86_64".to_string(),
|
||||
extension::TargetConfig {
|
||||
archive: "https://github.com/org/project/releases/download/v2.1.0/release-agent-linux-x64.tar.gz".into(),
|
||||
cmd: "./release-agent".into(),
|
||||
args: vec!["serve".into()],
|
||||
sha256: None,
|
||||
env: Default::default(),
|
||||
},
|
||||
);
|
||||
|
||||
let manifest_entry = AgentServerManifestEntry {
|
||||
name: "Release Agent".into(),
|
||||
targets: targets.clone(),
|
||||
env,
|
||||
icon: None,
|
||||
};
|
||||
|
||||
// Verify target config is present
|
||||
assert!(manifest_entry.targets.contains_key("linux-x86_64"));
|
||||
let target = manifest_entry.targets.get("linux-x86_64").unwrap();
|
||||
assert_eq!(target.cmd, "./release-agent");
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_node_command_uses_managed_runtime(cx: &mut TestAppContext) {
|
||||
let fs = fs::FakeFs::new(cx.background_executor.clone());
|
||||
let http_client = http_client::FakeHttpClient::with_404_response();
|
||||
let node_runtime = NodeRuntime::unavailable();
|
||||
let worktree_store =
|
||||
cx.new(|cx| WorktreeStore::local(false, fs.clone(), WorktreeIdCounter::get(cx)));
|
||||
let project_environment = cx.new(|cx| {
|
||||
crate::ProjectEnvironment::new(None, worktree_store.downgrade(), None, false, cx)
|
||||
});
|
||||
|
||||
let agent = LocalExtensionArchiveAgent {
|
||||
fs: fs.clone(),
|
||||
http_client,
|
||||
node_runtime,
|
||||
project_environment,
|
||||
extension_id: Arc::from("node-extension"),
|
||||
agent_id: Arc::from("node-agent"),
|
||||
version: Some(SharedString::from("1.0.0")),
|
||||
targets: {
|
||||
let mut map = HashMap::default();
|
||||
map.insert(
|
||||
"darwin-aarch64".to_string(),
|
||||
extension::TargetConfig {
|
||||
archive: "https://example.com/node-agent.zip".into(),
|
||||
cmd: "node".into(),
|
||||
args: vec!["index.js".into()],
|
||||
sha256: None,
|
||||
env: Default::default(),
|
||||
},
|
||||
);
|
||||
map
|
||||
},
|
||||
env: HashMap::default(),
|
||||
new_version_available_tx: None,
|
||||
};
|
||||
|
||||
// Verify that when cmd is "node", it attempts to use the node runtime
|
||||
assert_eq!(agent.extension_id.as_ref(), "node-extension");
|
||||
assert_eq!(agent.agent_id.as_ref(), "node-agent");
|
||||
|
||||
let target = agent.targets.get("darwin-aarch64").unwrap();
|
||||
assert_eq!(target.cmd, "node");
|
||||
assert_eq!(target.args, vec!["index.js"]);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_commands_run_in_extraction_directory(cx: &mut TestAppContext) {
|
||||
let fs = fs::FakeFs::new(cx.background_executor.clone());
|
||||
let http_client = http_client::FakeHttpClient::with_404_response();
|
||||
let node_runtime = NodeRuntime::unavailable();
|
||||
let worktree_store =
|
||||
cx.new(|cx| WorktreeStore::local(false, fs.clone(), WorktreeIdCounter::get(cx)));
|
||||
let project_environment = cx.new(|cx| {
|
||||
crate::ProjectEnvironment::new(None, worktree_store.downgrade(), None, false, cx)
|
||||
});
|
||||
|
||||
let agent = LocalExtensionArchiveAgent {
|
||||
fs: fs.clone(),
|
||||
http_client,
|
||||
node_runtime,
|
||||
project_environment,
|
||||
extension_id: Arc::from("test-ext"),
|
||||
agent_id: Arc::from("test-agent"),
|
||||
version: Some(SharedString::from("1.0.0")),
|
||||
targets: {
|
||||
let mut map = HashMap::default();
|
||||
map.insert(
|
||||
"darwin-aarch64".to_string(),
|
||||
extension::TargetConfig {
|
||||
archive: "https://example.com/test.zip".into(),
|
||||
cmd: "node".into(),
|
||||
args: vec![
|
||||
"server.js".into(),
|
||||
"--config".into(),
|
||||
"./config.json".into(),
|
||||
],
|
||||
sha256: None,
|
||||
env: Default::default(),
|
||||
},
|
||||
);
|
||||
map
|
||||
},
|
||||
env: Default::default(),
|
||||
new_version_available_tx: None,
|
||||
};
|
||||
|
||||
// Verify the agent is configured with relative paths in args
|
||||
let target = agent.targets.get("darwin-aarch64").unwrap();
|
||||
assert_eq!(target.args[0], "server.js");
|
||||
assert_eq!(target.args[2], "./config.json");
|
||||
// These relative paths will resolve relative to the extraction directory
|
||||
// when the command is executed
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_tilde_expansion_in_settings() {
|
||||
let settings = settings::CustomAgentServerSettings::Custom {
|
||||
path: PathBuf::from("~/custom/agent"),
|
||||
args: vec!["serve".into()],
|
||||
env: Default::default(),
|
||||
default_mode: None,
|
||||
default_model: None,
|
||||
favorite_models: vec![],
|
||||
default_config_options: Default::default(),
|
||||
favorite_config_option_values: Default::default(),
|
||||
};
|
||||
|
||||
let converted: CustomAgentServerSettings = settings.into();
|
||||
let CustomAgentServerSettings::Custom {
|
||||
command: AgentServerCommand { path, .. },
|
||||
..
|
||||
} = converted
|
||||
else {
|
||||
panic!("Expected Custom variant");
|
||||
};
|
||||
|
||||
assert!(
|
||||
!path.to_string_lossy().starts_with("~"),
|
||||
"Tilde should be expanded for custom agent path"
|
||||
);
|
||||
}
|
||||
1788
crates/project/tests/integration/git_store.rs
Normal file
1788
crates/project/tests/integration/git_store.rs
Normal file
File diff suppressed because it is too large
Load Diff
78
crates/project/tests/integration/image_store.rs
Normal file
78
crates/project/tests/integration/image_store.rs
Normal file
@@ -0,0 +1,78 @@
|
||||
use fs::FakeFs;
|
||||
use gpui::TestAppContext;
|
||||
use project::Project;
|
||||
use project::ProjectPath;
|
||||
use project::image_store::*;
|
||||
use serde_json::json;
|
||||
use settings::SettingsStore;
|
||||
use util::rel_path::rel_path;
|
||||
|
||||
pub fn init_test(cx: &mut TestAppContext) {
|
||||
zlog::init_test();
|
||||
|
||||
cx.update(|cx| {
|
||||
let settings_store = SettingsStore::test(cx);
|
||||
cx.set_global(settings_store);
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_image_not_loaded_twice(cx: &mut TestAppContext) {
|
||||
init_test(cx);
|
||||
let fs = FakeFs::new(cx.executor());
|
||||
|
||||
fs.insert_tree("/root", json!({})).await;
|
||||
// Create a png file that consists of a single white pixel
|
||||
fs.insert_file(
|
||||
"/root/image_1.png",
|
||||
vec![
|
||||
0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A, 0x00, 0x00, 0x00, 0x0D, 0x49, 0x48,
|
||||
0x44, 0x52, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x08, 0x06, 0x00, 0x00,
|
||||
0x00, 0x1F, 0x15, 0xC4, 0x89, 0x00, 0x00, 0x00, 0x0A, 0x49, 0x44, 0x41, 0x54, 0x78,
|
||||
0x9C, 0x63, 0x00, 0x01, 0x00, 0x00, 0x05, 0x00, 0x01, 0x0D, 0x0A, 0x2D, 0xB4, 0x00,
|
||||
0x00, 0x00, 0x00, 0x49, 0x45, 0x4E, 0x44, 0xAE, 0x42, 0x60, 0x82,
|
||||
],
|
||||
)
|
||||
.await;
|
||||
|
||||
let project = Project::test(fs, ["/root".as_ref()], cx).await;
|
||||
|
||||
let worktree_id = cx.update(|cx| project.read(cx).worktrees(cx).next().unwrap().read(cx).id());
|
||||
|
||||
let project_path = ProjectPath {
|
||||
worktree_id,
|
||||
path: rel_path("image_1.png").into(),
|
||||
};
|
||||
|
||||
let (task1, task2) = project.update(cx, |project, cx| {
|
||||
(
|
||||
project.open_image(project_path.clone(), cx),
|
||||
project.open_image(project_path.clone(), cx),
|
||||
)
|
||||
});
|
||||
|
||||
let image1 = task1.await.unwrap();
|
||||
let image2 = task2.await.unwrap();
|
||||
|
||||
assert_eq!(image1, image2);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn test_compute_metadata_from_bytes() {
|
||||
// Single white pixel PNG
|
||||
let png_bytes = vec![
|
||||
0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A, 0x00, 0x00, 0x00, 0x0D, 0x49, 0x48, 0x44,
|
||||
0x52, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x08, 0x06, 0x00, 0x00, 0x00, 0x1F,
|
||||
0x15, 0xC4, 0x89, 0x00, 0x00, 0x00, 0x0A, 0x49, 0x44, 0x41, 0x54, 0x78, 0x9C, 0x63, 0x00,
|
||||
0x01, 0x00, 0x00, 0x05, 0x00, 0x01, 0x0D, 0x0A, 0x2D, 0xB4, 0x00, 0x00, 0x00, 0x00, 0x49,
|
||||
0x45, 0x4E, 0x44, 0xAE, 0x42, 0x60, 0x82,
|
||||
];
|
||||
|
||||
let metadata = ImageItem::compute_metadata_from_bytes(&png_bytes).unwrap();
|
||||
|
||||
assert_eq!(metadata.width, 1);
|
||||
assert_eq!(metadata.height, 1);
|
||||
assert_eq!(metadata.file_size, png_bytes.len() as u64);
|
||||
assert_eq!(metadata.format, image::ImageFormat::Png);
|
||||
assert!(metadata.colors.is_some());
|
||||
}
|
||||
128
crates/project/tests/integration/lsp_command.rs
Normal file
128
crates/project/tests/integration/lsp_command.rs
Normal file
@@ -0,0 +1,128 @@
|
||||
use std::str::FromStr;
|
||||
|
||||
use lsp::{DiagnosticSeverity, DiagnosticTag};
|
||||
use project::lsp_command::*;
|
||||
use rpc::proto::{self};
|
||||
use serde_json::json;
|
||||
|
||||
#[test]
|
||||
fn test_serialize_lsp_diagnostic() {
|
||||
let lsp_diagnostic = lsp::Diagnostic {
|
||||
range: lsp::Range {
|
||||
start: lsp::Position::new(0, 1),
|
||||
end: lsp::Position::new(2, 3),
|
||||
},
|
||||
severity: Some(DiagnosticSeverity::ERROR),
|
||||
code: Some(lsp::NumberOrString::String("E001".to_string())),
|
||||
source: Some("test-source".to_string()),
|
||||
message: "Test error message".to_string(),
|
||||
related_information: None,
|
||||
tags: Some(vec![DiagnosticTag::DEPRECATED]),
|
||||
code_description: None,
|
||||
data: Some(json!({"detail": "test detail"})),
|
||||
};
|
||||
|
||||
let proto_diagnostic = GetDocumentDiagnostics::serialize_lsp_diagnostic(lsp_diagnostic)
|
||||
.expect("Failed to serialize diagnostic");
|
||||
|
||||
let start = proto_diagnostic.start.unwrap();
|
||||
let end = proto_diagnostic.end.unwrap();
|
||||
assert_eq!(start.row, 0);
|
||||
assert_eq!(start.column, 1);
|
||||
assert_eq!(end.row, 2);
|
||||
assert_eq!(end.column, 3);
|
||||
assert_eq!(
|
||||
proto_diagnostic.severity,
|
||||
proto::lsp_diagnostic::Severity::Error as i32
|
||||
);
|
||||
assert_eq!(proto_diagnostic.code, Some("E001".to_string()));
|
||||
assert_eq!(proto_diagnostic.source, Some("test-source".to_string()));
|
||||
assert_eq!(proto_diagnostic.message, "Test error message");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_deserialize_lsp_diagnostic() {
|
||||
let proto_diagnostic = proto::LspDiagnostic {
|
||||
start: Some(proto::PointUtf16 { row: 0, column: 1 }),
|
||||
end: Some(proto::PointUtf16 { row: 2, column: 3 }),
|
||||
severity: proto::lsp_diagnostic::Severity::Warning as i32,
|
||||
code: Some("ERR".to_string()),
|
||||
source: Some("Prism".to_string()),
|
||||
message: "assigned but unused variable - a".to_string(),
|
||||
related_information: vec![],
|
||||
tags: vec![],
|
||||
code_description: None,
|
||||
data: None,
|
||||
};
|
||||
|
||||
let lsp_diagnostic = GetDocumentDiagnostics::deserialize_lsp_diagnostic(proto_diagnostic)
|
||||
.expect("Failed to deserialize diagnostic");
|
||||
|
||||
assert_eq!(lsp_diagnostic.range.start.line, 0);
|
||||
assert_eq!(lsp_diagnostic.range.start.character, 1);
|
||||
assert_eq!(lsp_diagnostic.range.end.line, 2);
|
||||
assert_eq!(lsp_diagnostic.range.end.character, 3);
|
||||
assert_eq!(lsp_diagnostic.severity, Some(DiagnosticSeverity::WARNING));
|
||||
assert_eq!(
|
||||
lsp_diagnostic.code,
|
||||
Some(lsp::NumberOrString::String("ERR".to_string()))
|
||||
);
|
||||
assert_eq!(lsp_diagnostic.source, Some("Prism".to_string()));
|
||||
assert_eq!(lsp_diagnostic.message, "assigned but unused variable - a");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_related_information() {
|
||||
let related_info = lsp::DiagnosticRelatedInformation {
|
||||
location: lsp::Location {
|
||||
uri: lsp::Uri::from_str("file:///test.rs").unwrap(),
|
||||
range: lsp::Range {
|
||||
start: lsp::Position::new(1, 1),
|
||||
end: lsp::Position::new(1, 5),
|
||||
},
|
||||
},
|
||||
message: "Related info message".to_string(),
|
||||
};
|
||||
|
||||
let lsp_diagnostic = lsp::Diagnostic {
|
||||
range: lsp::Range {
|
||||
start: lsp::Position::new(0, 0),
|
||||
end: lsp::Position::new(0, 1),
|
||||
},
|
||||
severity: Some(DiagnosticSeverity::INFORMATION),
|
||||
code: None,
|
||||
source: Some("Prism".to_string()),
|
||||
message: "assigned but unused variable - a".to_string(),
|
||||
related_information: Some(vec![related_info]),
|
||||
tags: None,
|
||||
code_description: None,
|
||||
data: None,
|
||||
};
|
||||
|
||||
let proto_diagnostic = GetDocumentDiagnostics::serialize_lsp_diagnostic(lsp_diagnostic)
|
||||
.expect("Failed to serialize diagnostic");
|
||||
|
||||
assert_eq!(proto_diagnostic.related_information.len(), 1);
|
||||
let related = &proto_diagnostic.related_information[0];
|
||||
assert_eq!(related.location_url, Some("file:///test.rs".to_string()));
|
||||
assert_eq!(related.message, "Related info message");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_invalid_ranges() {
|
||||
let proto_diagnostic = proto::LspDiagnostic {
|
||||
start: None,
|
||||
end: Some(proto::PointUtf16 { row: 2, column: 3 }),
|
||||
severity: proto::lsp_diagnostic::Severity::Error as i32,
|
||||
code: None,
|
||||
source: None,
|
||||
message: "Test message".to_string(),
|
||||
related_information: vec![],
|
||||
tags: vec![],
|
||||
code_description: None,
|
||||
data: None,
|
||||
};
|
||||
|
||||
let result = GetDocumentDiagnostics::deserialize_lsp_diagnostic(proto_diagnostic);
|
||||
assert!(result.is_err());
|
||||
}
|
||||
74
crates/project/tests/integration/lsp_store.rs
Normal file
74
crates/project/tests/integration/lsp_store.rs
Normal file
@@ -0,0 +1,74 @@
|
||||
use std::path::Path;
|
||||
|
||||
use language::{CodeLabel, HighlightId};
|
||||
|
||||
use project::lsp_store::*;
|
||||
|
||||
#[test]
|
||||
fn test_glob_literal_prefix() {
|
||||
assert_eq!(glob_literal_prefix(Path::new("**/*.js")), Path::new(""));
|
||||
assert_eq!(
|
||||
glob_literal_prefix(Path::new("node_modules/**/*.js")),
|
||||
Path::new("node_modules")
|
||||
);
|
||||
assert_eq!(
|
||||
glob_literal_prefix(Path::new("foo/{bar,baz}.js")),
|
||||
Path::new("foo")
|
||||
);
|
||||
assert_eq!(
|
||||
glob_literal_prefix(Path::new("foo/bar/baz.js")),
|
||||
Path::new("foo/bar/baz.js")
|
||||
);
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
assert_eq!(glob_literal_prefix(Path::new("**\\*.js")), Path::new(""));
|
||||
assert_eq!(
|
||||
glob_literal_prefix(Path::new("node_modules\\**/*.js")),
|
||||
Path::new("node_modules")
|
||||
);
|
||||
assert_eq!(
|
||||
glob_literal_prefix(Path::new("foo/{bar,baz}.js")),
|
||||
Path::new("foo")
|
||||
);
|
||||
assert_eq!(
|
||||
glob_literal_prefix(Path::new("foo\\bar\\baz.js")),
|
||||
Path::new("foo/bar/baz.js")
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_multi_len_chars_normalization() {
|
||||
let mut label = CodeLabel::new(
|
||||
"myElˇ (parameter) myElˇ: {\n foo: string;\n}".to_string(),
|
||||
0..6,
|
||||
vec![(0..6, HighlightId::new(1))],
|
||||
);
|
||||
ensure_uniform_list_compatible_label(&mut label);
|
||||
assert_eq!(
|
||||
label,
|
||||
CodeLabel::new(
|
||||
"myElˇ (parameter) myElˇ: { foo: string; }".to_string(),
|
||||
0..6,
|
||||
vec![(0..6, HighlightId::new(1))],
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_trailing_newline_in_completion_documentation() {
|
||||
let doc =
|
||||
lsp::Documentation::String("Inappropriate argument value (of correct type).\n".to_string());
|
||||
let completion_doc: CompletionDocumentation = doc.into();
|
||||
assert!(
|
||||
matches!(completion_doc, CompletionDocumentation::SingleLine(s) if s == "Inappropriate argument value (of correct type).")
|
||||
);
|
||||
|
||||
let doc = lsp::Documentation::String(" some value \n".to_string());
|
||||
let completion_doc: CompletionDocumentation = doc.into();
|
||||
assert!(matches!(
|
||||
completion_doc,
|
||||
CompletionDocumentation::SingleLine(s) if s == "some value"
|
||||
));
|
||||
}
|
||||
124
crates/project/tests/integration/manifest_tree.rs
Normal file
124
crates/project/tests/integration/manifest_tree.rs
Normal file
@@ -0,0 +1,124 @@
|
||||
mod path_trie {
|
||||
use std::{collections::BTreeSet, ops::ControlFlow};
|
||||
|
||||
use util::rel_path::rel_path;
|
||||
|
||||
use project::manifest_tree::path_trie::*;
|
||||
|
||||
#[test]
|
||||
fn test_insert_and_lookup() {
|
||||
let mut trie = RootPathTrie::<()>::new();
|
||||
trie.insert(
|
||||
&TriePath::new(rel_path("a/b/c")),
|
||||
(),
|
||||
LabelPresence::Present,
|
||||
);
|
||||
|
||||
trie.walk(&TriePath::new(rel_path("a/b/c")), &mut |path, nodes| {
|
||||
assert_eq!(nodes.get(&()), Some(&LabelPresence::Present));
|
||||
assert_eq!(path.as_unix_str(), "a/b/c");
|
||||
ControlFlow::Continue(())
|
||||
});
|
||||
// Now let's annotate a parent with "Known missing" node.
|
||||
trie.insert(
|
||||
&TriePath::new(rel_path("a")),
|
||||
(),
|
||||
LabelPresence::KnownAbsent,
|
||||
);
|
||||
|
||||
// Ensure that we walk from the root to the leaf.
|
||||
let mut visited_paths = BTreeSet::new();
|
||||
trie.walk(&TriePath::new(rel_path("a/b/c")), &mut |path, nodes| {
|
||||
if path.as_unix_str() == "a/b/c" {
|
||||
assert_eq!(visited_paths, BTreeSet::from_iter([rel_path("a").into()]));
|
||||
assert_eq!(nodes.get(&()), Some(&LabelPresence::Present));
|
||||
} else if path.as_unix_str() == "a" {
|
||||
assert!(visited_paths.is_empty());
|
||||
assert_eq!(nodes.get(&()), Some(&LabelPresence::KnownAbsent));
|
||||
} else {
|
||||
panic!("Unknown path");
|
||||
}
|
||||
// Assert that we only ever visit a path once.
|
||||
assert!(visited_paths.insert(path.clone()));
|
||||
ControlFlow::Continue(())
|
||||
});
|
||||
|
||||
// One can also pass a path whose prefix is in the tree, but not that path itself.
|
||||
let mut visited_paths = BTreeSet::new();
|
||||
trie.walk(
|
||||
&TriePath::new(rel_path("a/b/c/d/e/f/g")),
|
||||
&mut |path, nodes| {
|
||||
if path.as_unix_str() == "a/b/c" {
|
||||
assert_eq!(visited_paths, BTreeSet::from_iter([rel_path("a").into()]));
|
||||
assert_eq!(nodes.get(&()), Some(&LabelPresence::Present));
|
||||
} else if path.as_unix_str() == "a" {
|
||||
assert!(visited_paths.is_empty());
|
||||
assert_eq!(nodes.get(&()), Some(&LabelPresence::KnownAbsent));
|
||||
} else {
|
||||
panic!("Unknown path");
|
||||
}
|
||||
// Assert that we only ever visit a path once.
|
||||
assert!(visited_paths.insert(path.clone()));
|
||||
ControlFlow::Continue(())
|
||||
},
|
||||
);
|
||||
|
||||
// Test breaking from the tree-walk.
|
||||
let mut visited_paths = BTreeSet::new();
|
||||
trie.walk(&TriePath::new(rel_path("a/b/c")), &mut |path, nodes| {
|
||||
if path.as_unix_str() == "a" {
|
||||
assert!(visited_paths.is_empty());
|
||||
assert_eq!(nodes.get(&()), Some(&LabelPresence::KnownAbsent));
|
||||
} else {
|
||||
panic!("Unknown path");
|
||||
}
|
||||
// Assert that we only ever visit a path once.
|
||||
assert!(visited_paths.insert(path.clone()));
|
||||
ControlFlow::Break(())
|
||||
});
|
||||
assert_eq!(visited_paths.len(), 1);
|
||||
|
||||
// Entry removal.
|
||||
trie.insert(
|
||||
&TriePath::new(rel_path("a/b")),
|
||||
(),
|
||||
LabelPresence::KnownAbsent,
|
||||
);
|
||||
let mut visited_paths = BTreeSet::new();
|
||||
trie.walk(&TriePath::new(rel_path("a/b/c")), &mut |path, _nodes| {
|
||||
// Assert that we only ever visit a path once.
|
||||
assert!(visited_paths.insert(path.clone()));
|
||||
ControlFlow::Continue(())
|
||||
});
|
||||
assert_eq!(visited_paths.len(), 3);
|
||||
trie.remove(&TriePath::new(rel_path("a/b")));
|
||||
let mut visited_paths = BTreeSet::new();
|
||||
trie.walk(&TriePath::new(rel_path("a/b/c")), &mut |path, _nodes| {
|
||||
// Assert that we only ever visit a path once.
|
||||
assert!(visited_paths.insert(path.clone()));
|
||||
ControlFlow::Continue(())
|
||||
});
|
||||
assert_eq!(visited_paths.len(), 1);
|
||||
assert_eq!(
|
||||
visited_paths.into_iter().next().unwrap(),
|
||||
rel_path("a").into()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn path_to_a_root_can_contain_multiple_known_nodes() {
|
||||
let mut trie = RootPathTrie::<()>::new();
|
||||
trie.insert(&TriePath::new(rel_path("a/b")), (), LabelPresence::Present);
|
||||
trie.insert(&TriePath::new(rel_path("a")), (), LabelPresence::Present);
|
||||
let mut visited_paths = BTreeSet::new();
|
||||
trie.walk(&TriePath::new(rel_path("a/b/c")), &mut |path, nodes| {
|
||||
assert_eq!(nodes.get(&()), Some(&LabelPresence::Present));
|
||||
if path.as_unix_str() != "a" && path.as_unix_str() != "a/b" {
|
||||
panic!("Unexpected path: {}", path.as_unix_str());
|
||||
}
|
||||
assert!(visited_paths.insert(path.clone()));
|
||||
ControlFlow::Continue(())
|
||||
});
|
||||
assert_eq!(visited_paths.len(), 2);
|
||||
}
|
||||
}
|
||||
114
crates/project/tests/integration/project_search.rs
Normal file
114
crates/project/tests/integration/project_search.rs
Normal file
@@ -0,0 +1,114 @@
|
||||
use std::{path::Path, sync::Arc};
|
||||
|
||||
use crate::init_test;
|
||||
use fs::FakeFs;
|
||||
use project::{Project, ProjectEntryId, project_search::PathInclusionMatcher, search::SearchQuery};
|
||||
use serde_json::json;
|
||||
use settings::Settings;
|
||||
use util::{
|
||||
path,
|
||||
paths::{PathMatcher, PathStyle},
|
||||
rel_path::RelPath,
|
||||
};
|
||||
use worktree::{Entry, EntryKind, WorktreeSettings};
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_path_inclusion_matcher(cx: &mut gpui::TestAppContext) {
|
||||
init_test(cx);
|
||||
|
||||
let fs = FakeFs::new(cx.background_executor.clone());
|
||||
fs.insert_tree(
|
||||
"/root",
|
||||
json!({
|
||||
".gitignore": "src/data/\n",
|
||||
"src": {
|
||||
"data": {
|
||||
"main.csv": "field_1,field_2,field_3",
|
||||
},
|
||||
"lib": {
|
||||
"main.txt": "Are you familiar with fields?",
|
||||
},
|
||||
},
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
|
||||
let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
|
||||
let worktree = project.update(cx, |project, cx| project.worktrees(cx).next().unwrap());
|
||||
let (worktree_settings, worktree_snapshot) = worktree.update(cx, |worktree, cx| {
|
||||
let settings_location = worktree.settings_location(cx);
|
||||
return (
|
||||
WorktreeSettings::get(Some(settings_location), cx).clone(),
|
||||
worktree.snapshot(),
|
||||
);
|
||||
});
|
||||
|
||||
// Manually create a test entry for the gitignored directory since it won't
|
||||
// be loaded by the worktree
|
||||
let entry = Entry {
|
||||
id: ProjectEntryId::from_proto(1),
|
||||
kind: EntryKind::UnloadedDir,
|
||||
path: Arc::from(RelPath::unix(Path::new("src/data")).unwrap()),
|
||||
inode: 0,
|
||||
mtime: None,
|
||||
canonical_path: None,
|
||||
is_ignored: true,
|
||||
is_hidden: false,
|
||||
is_always_included: false,
|
||||
is_external: false,
|
||||
is_private: false,
|
||||
size: 0,
|
||||
char_bag: Default::default(),
|
||||
is_fifo: false,
|
||||
};
|
||||
|
||||
// 1. Test searching for `field`, including ignored files without any
|
||||
// inclusion and exclusion filters.
|
||||
let include_ignored = true;
|
||||
let files_to_include = PathMatcher::default();
|
||||
let files_to_exclude = PathMatcher::default();
|
||||
let match_full_paths = false;
|
||||
let search_query = SearchQuery::text(
|
||||
"field",
|
||||
false,
|
||||
false,
|
||||
include_ignored,
|
||||
files_to_include,
|
||||
files_to_exclude,
|
||||
match_full_paths,
|
||||
None,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let path_matcher = PathInclusionMatcher::new(Arc::new(search_query));
|
||||
assert!(path_matcher.should_scan_gitignored_dir(
|
||||
&entry,
|
||||
&worktree_snapshot,
|
||||
&worktree_settings
|
||||
));
|
||||
|
||||
// 2. Test searching for `field`, including ignored files but updating
|
||||
// `files_to_include` to only include files under `src/lib`.
|
||||
let include_ignored = true;
|
||||
let files_to_include = PathMatcher::new(vec!["src/lib"], PathStyle::Posix).unwrap();
|
||||
let files_to_exclude = PathMatcher::default();
|
||||
let match_full_paths = false;
|
||||
let search_query = SearchQuery::text(
|
||||
"field",
|
||||
false,
|
||||
false,
|
||||
include_ignored,
|
||||
files_to_include,
|
||||
files_to_exclude,
|
||||
match_full_paths,
|
||||
None,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let path_matcher = PathInclusionMatcher::new(Arc::new(search_query));
|
||||
assert!(!path_matcher.should_scan_gitignored_dir(
|
||||
&entry,
|
||||
&worktree_snapshot,
|
||||
&worktree_settings
|
||||
));
|
||||
}
|
||||
13481
crates/project/tests/integration/project_tests.rs
Normal file
13481
crates/project/tests/integration/project_tests.rs
Normal file
File diff suppressed because it is too large
Load Diff
156
crates/project/tests/integration/search.rs
Normal file
156
crates/project/tests/integration/search.rs
Normal file
@@ -0,0 +1,156 @@
|
||||
use project::search::SearchQuery;
|
||||
use text::Rope;
|
||||
use util::{
|
||||
paths::{PathMatcher, PathStyle},
|
||||
rel_path::RelPath,
|
||||
};
|
||||
|
||||
#[test]
|
||||
fn path_matcher_creation_for_valid_paths() {
|
||||
for valid_path in [
|
||||
"file",
|
||||
"Cargo.toml",
|
||||
".DS_Store",
|
||||
"~/dir/another_dir/",
|
||||
"./dir/file",
|
||||
"dir/[a-z].txt",
|
||||
] {
|
||||
let path_matcher = PathMatcher::new(&[valid_path.to_owned()], PathStyle::local())
|
||||
.unwrap_or_else(|e| panic!("Valid path {valid_path} should be accepted, but got: {e}"));
|
||||
assert!(
|
||||
path_matcher.is_match(&RelPath::new(valid_path.as_ref(), PathStyle::local()).unwrap()),
|
||||
"Path matcher for valid path {valid_path} should match itself"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn path_matcher_creation_for_globs() {
|
||||
for invalid_glob in ["dir/[].txt", "dir/[a-z.txt", "dir/{file"] {
|
||||
match PathMatcher::new(&[invalid_glob.to_owned()], PathStyle::local()) {
|
||||
Ok(_) => panic!("Invalid glob {invalid_glob} should not be accepted"),
|
||||
Err(_expected) => {}
|
||||
}
|
||||
}
|
||||
|
||||
for valid_glob in [
|
||||
"dir/?ile",
|
||||
"dir/*.txt",
|
||||
"dir/**/file",
|
||||
"dir/[a-z].txt",
|
||||
"{dir,file}",
|
||||
] {
|
||||
match PathMatcher::new(&[valid_glob.to_owned()], PathStyle::local()) {
|
||||
Ok(_expected) => {}
|
||||
Err(e) => panic!("Valid glob should be accepted, but got: {e}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_case_sensitive_pattern_items() {
|
||||
let case_sensitive = false;
|
||||
let search_query = SearchQuery::regex(
|
||||
"test\\C",
|
||||
false,
|
||||
case_sensitive,
|
||||
false,
|
||||
false,
|
||||
Default::default(),
|
||||
Default::default(),
|
||||
false,
|
||||
None,
|
||||
)
|
||||
.expect("Should be able to create a regex SearchQuery");
|
||||
|
||||
assert_eq!(
|
||||
search_query.case_sensitive(),
|
||||
true,
|
||||
"Case sensitivity should be enabled when \\C pattern item is present in the query."
|
||||
);
|
||||
|
||||
let case_sensitive = true;
|
||||
let search_query = SearchQuery::regex(
|
||||
"test\\c",
|
||||
true,
|
||||
case_sensitive,
|
||||
false,
|
||||
false,
|
||||
Default::default(),
|
||||
Default::default(),
|
||||
false,
|
||||
None,
|
||||
)
|
||||
.expect("Should be able to create a regex SearchQuery");
|
||||
|
||||
assert_eq!(
|
||||
search_query.case_sensitive(),
|
||||
false,
|
||||
"Case sensitivity should be disabled when \\c pattern item is present, even if initially set to true."
|
||||
);
|
||||
|
||||
let case_sensitive = false;
|
||||
let search_query = SearchQuery::regex(
|
||||
"test\\c\\C",
|
||||
false,
|
||||
case_sensitive,
|
||||
false,
|
||||
false,
|
||||
Default::default(),
|
||||
Default::default(),
|
||||
false,
|
||||
None,
|
||||
)
|
||||
.expect("Should be able to create a regex SearchQuery");
|
||||
|
||||
assert_eq!(
|
||||
search_query.case_sensitive(),
|
||||
true,
|
||||
"Case sensitivity should be enabled when \\C is the last pattern item, even after a \\c."
|
||||
);
|
||||
|
||||
let case_sensitive = false;
|
||||
let search_query = SearchQuery::regex(
|
||||
"tests\\\\C",
|
||||
false,
|
||||
case_sensitive,
|
||||
false,
|
||||
false,
|
||||
Default::default(),
|
||||
Default::default(),
|
||||
false,
|
||||
None,
|
||||
)
|
||||
.expect("Should be able to create a regex SearchQuery");
|
||||
|
||||
assert_eq!(
|
||||
search_query.case_sensitive(),
|
||||
false,
|
||||
"Case sensitivity should not be enabled when \\C pattern item is preceded by a backslash."
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_multiline_regex(cx: &mut gpui::TestAppContext) {
|
||||
let search_query = SearchQuery::regex(
|
||||
"^hello$\n",
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
Default::default(),
|
||||
Default::default(),
|
||||
false,
|
||||
None,
|
||||
)
|
||||
.expect("Should be able to create a regex SearchQuery");
|
||||
|
||||
use language::Buffer;
|
||||
let text = Rope::from("hello\nworld\nhello\nworld");
|
||||
let snapshot = cx
|
||||
.update(|app| Buffer::build_snapshot(text, None, None, None, app))
|
||||
.await;
|
||||
|
||||
let results = search_query.search(&snapshot, None).await;
|
||||
assert_eq!(results, vec![0..6, 12..18]);
|
||||
}
|
||||
151
crates/project/tests/integration/search_history.rs
Normal file
151
crates/project/tests/integration/search_history.rs
Normal file
@@ -0,0 +1,151 @@
|
||||
use project::search_history::{QueryInsertionBehavior, SearchHistory, SearchHistoryCursor};
|
||||
|
||||
#[test]
|
||||
fn test_add() {
|
||||
const MAX_HISTORY_LEN: usize = 20;
|
||||
let mut search_history = SearchHistory::new(
|
||||
Some(MAX_HISTORY_LEN),
|
||||
QueryInsertionBehavior::ReplacePreviousIfContains,
|
||||
);
|
||||
let mut cursor = SearchHistoryCursor::default();
|
||||
|
||||
assert_eq!(
|
||||
search_history.current(&cursor),
|
||||
None,
|
||||
"No current selection should be set for the default search history"
|
||||
);
|
||||
|
||||
search_history.add(&mut cursor, "rust".to_string());
|
||||
assert_eq!(
|
||||
search_history.current(&cursor),
|
||||
Some("rust"),
|
||||
"Newly added item should be selected"
|
||||
);
|
||||
|
||||
// check if duplicates are not added
|
||||
search_history.add(&mut cursor, "rust".to_string());
|
||||
assert_eq!(search_history.len(), 1, "Should not add a duplicate");
|
||||
assert_eq!(search_history.current(&cursor), Some("rust"));
|
||||
|
||||
// check if new string containing the previous string replaces it
|
||||
search_history.add(&mut cursor, "rustlang".to_string());
|
||||
assert_eq!(
|
||||
search_history.len(),
|
||||
1,
|
||||
"Should replace previous item if it's a substring"
|
||||
);
|
||||
assert_eq!(search_history.current(&cursor), Some("rustlang"));
|
||||
|
||||
// add item when it equals to current item if it's not the last one
|
||||
search_history.add(&mut cursor, "php".to_string());
|
||||
search_history.previous(&mut cursor, "");
|
||||
assert_eq!(search_history.current(&cursor), Some("rustlang"));
|
||||
search_history.add(&mut cursor, "rustlang".to_string());
|
||||
assert_eq!(search_history.len(), 3, "Should add item");
|
||||
assert_eq!(search_history.current(&cursor), Some("rustlang"));
|
||||
|
||||
// push enough items to test SEARCH_HISTORY_LIMIT
|
||||
for i in 0..MAX_HISTORY_LEN * 2 {
|
||||
search_history.add(&mut cursor, format!("item{i}"));
|
||||
}
|
||||
assert!(search_history.len() <= MAX_HISTORY_LEN);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_next_and_previous() {
|
||||
let mut search_history = SearchHistory::new(None, QueryInsertionBehavior::AlwaysInsert);
|
||||
let mut cursor = SearchHistoryCursor::default();
|
||||
|
||||
assert_eq!(
|
||||
search_history.next(&mut cursor),
|
||||
None,
|
||||
"Default search history should not have a next item"
|
||||
);
|
||||
|
||||
search_history.add(&mut cursor, "Rust".to_string());
|
||||
assert_eq!(search_history.next(&mut cursor), None);
|
||||
search_history.add(&mut cursor, "JavaScript".to_string());
|
||||
assert_eq!(search_history.next(&mut cursor), None);
|
||||
search_history.add(&mut cursor, "TypeScript".to_string());
|
||||
assert_eq!(search_history.next(&mut cursor), None);
|
||||
|
||||
assert_eq!(search_history.current(&cursor), Some("TypeScript"));
|
||||
|
||||
assert_eq!(search_history.previous(&mut cursor, ""), Some("JavaScript"));
|
||||
assert_eq!(search_history.current(&cursor), Some("JavaScript"));
|
||||
|
||||
assert_eq!(search_history.previous(&mut cursor, ""), Some("Rust"));
|
||||
assert_eq!(search_history.current(&cursor), Some("Rust"));
|
||||
|
||||
assert_eq!(search_history.previous(&mut cursor, ""), None);
|
||||
assert_eq!(search_history.current(&cursor), Some("Rust"));
|
||||
|
||||
assert_eq!(search_history.next(&mut cursor), Some("JavaScript"));
|
||||
assert_eq!(search_history.current(&cursor), Some("JavaScript"));
|
||||
|
||||
assert_eq!(search_history.next(&mut cursor), Some("TypeScript"));
|
||||
assert_eq!(search_history.current(&cursor), Some("TypeScript"));
|
||||
|
||||
assert_eq!(search_history.next(&mut cursor), None);
|
||||
assert_eq!(search_history.current(&cursor), Some("TypeScript"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_reset_selection() {
|
||||
let mut search_history = SearchHistory::new(None, QueryInsertionBehavior::AlwaysInsert);
|
||||
let mut cursor = SearchHistoryCursor::default();
|
||||
|
||||
search_history.add(&mut cursor, "Rust".to_string());
|
||||
search_history.add(&mut cursor, "JavaScript".to_string());
|
||||
search_history.add(&mut cursor, "TypeScript".to_string());
|
||||
|
||||
assert_eq!(search_history.current(&cursor), Some("TypeScript"));
|
||||
cursor.reset();
|
||||
assert_eq!(search_history.current(&cursor), None);
|
||||
assert_eq!(
|
||||
search_history.previous(&mut cursor, ""),
|
||||
Some("TypeScript"),
|
||||
"Should start from the end after reset on previous item query"
|
||||
);
|
||||
|
||||
search_history.previous(&mut cursor, "");
|
||||
assert_eq!(search_history.current(&cursor), Some("JavaScript"));
|
||||
search_history.previous(&mut cursor, "");
|
||||
assert_eq!(search_history.current(&cursor), Some("Rust"));
|
||||
|
||||
cursor.reset();
|
||||
assert_eq!(search_history.current(&cursor), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_multiple_cursors() {
|
||||
let mut search_history = SearchHistory::new(None, QueryInsertionBehavior::AlwaysInsert);
|
||||
let mut cursor1 = SearchHistoryCursor::default();
|
||||
let mut cursor2 = SearchHistoryCursor::default();
|
||||
|
||||
search_history.add(&mut cursor1, "Rust".to_string());
|
||||
search_history.add(&mut cursor1, "JavaScript".to_string());
|
||||
search_history.add(&mut cursor1, "TypeScript".to_string());
|
||||
|
||||
search_history.add(&mut cursor2, "Python".to_string());
|
||||
search_history.add(&mut cursor2, "Java".to_string());
|
||||
search_history.add(&mut cursor2, "C++".to_string());
|
||||
|
||||
assert_eq!(search_history.current(&cursor1), Some("TypeScript"));
|
||||
assert_eq!(search_history.current(&cursor2), Some("C++"));
|
||||
|
||||
assert_eq!(
|
||||
search_history.previous(&mut cursor1, ""),
|
||||
Some("JavaScript")
|
||||
);
|
||||
assert_eq!(search_history.previous(&mut cursor2, ""), Some("Java"));
|
||||
|
||||
assert_eq!(search_history.next(&mut cursor1), Some("TypeScript"));
|
||||
assert_eq!(search_history.next(&mut cursor1), Some("Python"));
|
||||
|
||||
cursor1.reset();
|
||||
cursor2.reset();
|
||||
|
||||
assert_eq!(search_history.current(&cursor1), None);
|
||||
assert_eq!(search_history.current(&cursor2), None);
|
||||
}
|
||||
517
crates/project/tests/integration/signature_help.rs
Normal file
517
crates/project/tests/integration/signature_help.rs
Normal file
@@ -0,0 +1,517 @@
|
||||
use gpui::{FontWeight, HighlightStyle, SharedString, TestAppContext};
|
||||
use lsp::{Documentation, MarkupContent, MarkupKind};
|
||||
|
||||
use project::lsp_command::signature_help::SignatureHelp;
|
||||
|
||||
fn current_parameter() -> HighlightStyle {
|
||||
HighlightStyle {
|
||||
font_weight: Some(FontWeight::EXTRA_BOLD),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn test_create_signature_help_markdown_string_1(cx: &mut TestAppContext) {
|
||||
let signature_help = lsp::SignatureHelp {
|
||||
signatures: vec![lsp::SignatureInformation {
|
||||
label: "fn test(foo: u8, bar: &str)".to_string(),
|
||||
documentation: Some(Documentation::String(
|
||||
"This is a test documentation".to_string(),
|
||||
)),
|
||||
parameters: Some(vec![
|
||||
lsp::ParameterInformation {
|
||||
label: lsp::ParameterLabel::Simple("foo: u8".to_string()),
|
||||
documentation: None,
|
||||
},
|
||||
lsp::ParameterInformation {
|
||||
label: lsp::ParameterLabel::Simple("bar: &str".to_string()),
|
||||
documentation: None,
|
||||
},
|
||||
]),
|
||||
active_parameter: None,
|
||||
}],
|
||||
active_signature: Some(0),
|
||||
active_parameter: Some(0),
|
||||
};
|
||||
let maybe_markdown = cx.update(|cx| SignatureHelp::new(signature_help, None, None, cx));
|
||||
assert!(maybe_markdown.is_some());
|
||||
|
||||
let markdown = maybe_markdown.unwrap();
|
||||
let signature = markdown.signatures[markdown.active_signature].clone();
|
||||
let markdown = (signature.label, signature.highlights);
|
||||
assert_eq!(
|
||||
markdown,
|
||||
(
|
||||
SharedString::new_static("fn test(foo: u8, bar: &str)"),
|
||||
vec![(8..15, current_parameter())]
|
||||
)
|
||||
);
|
||||
assert_eq!(
|
||||
signature
|
||||
.documentation
|
||||
.unwrap()
|
||||
.update(cx, |documentation, _| documentation.source().to_owned()),
|
||||
"This is a test documentation",
|
||||
)
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn test_create_signature_help_markdown_string_2(cx: &mut TestAppContext) {
|
||||
let signature_help = lsp::SignatureHelp {
|
||||
signatures: vec![lsp::SignatureInformation {
|
||||
label: "fn test(foo: u8, bar: &str)".to_string(),
|
||||
documentation: Some(Documentation::MarkupContent(MarkupContent {
|
||||
kind: MarkupKind::Markdown,
|
||||
value: "This is a test documentation".to_string(),
|
||||
})),
|
||||
parameters: Some(vec![
|
||||
lsp::ParameterInformation {
|
||||
label: lsp::ParameterLabel::Simple("foo: u8".to_string()),
|
||||
documentation: None,
|
||||
},
|
||||
lsp::ParameterInformation {
|
||||
label: lsp::ParameterLabel::Simple("bar: &str".to_string()),
|
||||
documentation: None,
|
||||
},
|
||||
]),
|
||||
active_parameter: None,
|
||||
}],
|
||||
active_signature: Some(0),
|
||||
active_parameter: Some(1),
|
||||
};
|
||||
let maybe_markdown = cx.update(|cx| SignatureHelp::new(signature_help, None, None, cx));
|
||||
assert!(maybe_markdown.is_some());
|
||||
|
||||
let markdown = maybe_markdown.unwrap();
|
||||
let signature = markdown.signatures[markdown.active_signature].clone();
|
||||
let markdown = (signature.label, signature.highlights);
|
||||
assert_eq!(
|
||||
markdown,
|
||||
(
|
||||
SharedString::new_static("fn test(foo: u8, bar: &str)"),
|
||||
vec![(17..26, current_parameter())]
|
||||
)
|
||||
);
|
||||
assert_eq!(
|
||||
signature
|
||||
.documentation
|
||||
.unwrap()
|
||||
.update(cx, |documentation, _| documentation.source().to_owned()),
|
||||
"This is a test documentation",
|
||||
)
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn test_create_signature_help_markdown_string_3(cx: &mut TestAppContext) {
|
||||
let signature_help = lsp::SignatureHelp {
|
||||
signatures: vec![
|
||||
lsp::SignatureInformation {
|
||||
label: "fn test1(foo: u8, bar: &str)".to_string(),
|
||||
documentation: None,
|
||||
parameters: Some(vec![
|
||||
lsp::ParameterInformation {
|
||||
label: lsp::ParameterLabel::Simple("foo: u8".to_string()),
|
||||
documentation: None,
|
||||
},
|
||||
lsp::ParameterInformation {
|
||||
label: lsp::ParameterLabel::Simple("bar: &str".to_string()),
|
||||
documentation: None,
|
||||
},
|
||||
]),
|
||||
active_parameter: None,
|
||||
},
|
||||
lsp::SignatureInformation {
|
||||
label: "fn test2(hoge: String, fuga: bool)".to_string(),
|
||||
documentation: None,
|
||||
parameters: Some(vec![
|
||||
lsp::ParameterInformation {
|
||||
label: lsp::ParameterLabel::Simple("hoge: String".to_string()),
|
||||
documentation: None,
|
||||
},
|
||||
lsp::ParameterInformation {
|
||||
label: lsp::ParameterLabel::Simple("fuga: bool".to_string()),
|
||||
documentation: None,
|
||||
},
|
||||
]),
|
||||
active_parameter: None,
|
||||
},
|
||||
],
|
||||
active_signature: Some(0),
|
||||
active_parameter: Some(0),
|
||||
};
|
||||
let maybe_markdown = cx.update(|cx| SignatureHelp::new(signature_help, None, None, cx));
|
||||
assert!(maybe_markdown.is_some());
|
||||
|
||||
let markdown = maybe_markdown.unwrap();
|
||||
let signature = markdown.signatures[markdown.active_signature].clone();
|
||||
let markdown = (signature.label, signature.highlights);
|
||||
assert_eq!(
|
||||
markdown,
|
||||
(
|
||||
SharedString::new_static("fn test1(foo: u8, bar: &str)"),
|
||||
vec![(9..16, current_parameter())]
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn test_create_signature_help_markdown_string_4(cx: &mut TestAppContext) {
|
||||
let signature_help = lsp::SignatureHelp {
|
||||
signatures: vec![
|
||||
lsp::SignatureInformation {
|
||||
label: "fn test1(foo: u8, bar: &str)".to_string(),
|
||||
documentation: None,
|
||||
parameters: Some(vec![
|
||||
lsp::ParameterInformation {
|
||||
label: lsp::ParameterLabel::Simple("foo: u8".to_string()),
|
||||
documentation: None,
|
||||
},
|
||||
lsp::ParameterInformation {
|
||||
label: lsp::ParameterLabel::Simple("bar: &str".to_string()),
|
||||
documentation: None,
|
||||
},
|
||||
]),
|
||||
active_parameter: None,
|
||||
},
|
||||
lsp::SignatureInformation {
|
||||
label: "fn test2(hoge: String, fuga: bool)".to_string(),
|
||||
documentation: None,
|
||||
parameters: Some(vec![
|
||||
lsp::ParameterInformation {
|
||||
label: lsp::ParameterLabel::Simple("hoge: String".to_string()),
|
||||
documentation: None,
|
||||
},
|
||||
lsp::ParameterInformation {
|
||||
label: lsp::ParameterLabel::Simple("fuga: bool".to_string()),
|
||||
documentation: None,
|
||||
},
|
||||
]),
|
||||
active_parameter: None,
|
||||
},
|
||||
],
|
||||
active_signature: Some(1),
|
||||
active_parameter: Some(0),
|
||||
};
|
||||
let maybe_markdown = cx.update(|cx| SignatureHelp::new(signature_help, None, None, cx));
|
||||
assert!(maybe_markdown.is_some());
|
||||
|
||||
let markdown = maybe_markdown.unwrap();
|
||||
let signature = markdown.signatures[markdown.active_signature].clone();
|
||||
let markdown = (signature.label, signature.highlights);
|
||||
assert_eq!(
|
||||
markdown,
|
||||
(
|
||||
SharedString::new_static("fn test2(hoge: String, fuga: bool)"),
|
||||
vec![(9..21, current_parameter())]
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn test_create_signature_help_markdown_string_5(cx: &mut TestAppContext) {
|
||||
let signature_help = lsp::SignatureHelp {
|
||||
signatures: vec![
|
||||
lsp::SignatureInformation {
|
||||
label: "fn test1(foo: u8, bar: &str)".to_string(),
|
||||
documentation: None,
|
||||
parameters: Some(vec![
|
||||
lsp::ParameterInformation {
|
||||
label: lsp::ParameterLabel::Simple("foo: u8".to_string()),
|
||||
documentation: None,
|
||||
},
|
||||
lsp::ParameterInformation {
|
||||
label: lsp::ParameterLabel::Simple("bar: &str".to_string()),
|
||||
documentation: None,
|
||||
},
|
||||
]),
|
||||
active_parameter: None,
|
||||
},
|
||||
lsp::SignatureInformation {
|
||||
label: "fn test2(hoge: String, fuga: bool)".to_string(),
|
||||
documentation: None,
|
||||
parameters: Some(vec![
|
||||
lsp::ParameterInformation {
|
||||
label: lsp::ParameterLabel::Simple("hoge: String".to_string()),
|
||||
documentation: None,
|
||||
},
|
||||
lsp::ParameterInformation {
|
||||
label: lsp::ParameterLabel::Simple("fuga: bool".to_string()),
|
||||
documentation: None,
|
||||
},
|
||||
]),
|
||||
active_parameter: None,
|
||||
},
|
||||
],
|
||||
active_signature: Some(1),
|
||||
active_parameter: Some(1),
|
||||
};
|
||||
let maybe_markdown = cx.update(|cx| SignatureHelp::new(signature_help, None, None, cx));
|
||||
assert!(maybe_markdown.is_some());
|
||||
|
||||
let markdown = maybe_markdown.unwrap();
|
||||
let signature = markdown.signatures[markdown.active_signature].clone();
|
||||
let markdown = (signature.label, signature.highlights);
|
||||
assert_eq!(
|
||||
markdown,
|
||||
(
|
||||
SharedString::new_static("fn test2(hoge: String, fuga: bool)"),
|
||||
vec![(23..33, current_parameter())]
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn test_create_signature_help_markdown_string_6(cx: &mut TestAppContext) {
|
||||
let signature_help = lsp::SignatureHelp {
|
||||
signatures: vec![
|
||||
lsp::SignatureInformation {
|
||||
label: "fn test1(foo: u8, bar: &str)".to_string(),
|
||||
documentation: None,
|
||||
parameters: Some(vec![
|
||||
lsp::ParameterInformation {
|
||||
label: lsp::ParameterLabel::Simple("foo: u8".to_string()),
|
||||
documentation: None,
|
||||
},
|
||||
lsp::ParameterInformation {
|
||||
label: lsp::ParameterLabel::Simple("bar: &str".to_string()),
|
||||
documentation: None,
|
||||
},
|
||||
]),
|
||||
active_parameter: None,
|
||||
},
|
||||
lsp::SignatureInformation {
|
||||
label: "fn test2(hoge: String, fuga: bool)".to_string(),
|
||||
documentation: None,
|
||||
parameters: Some(vec![
|
||||
lsp::ParameterInformation {
|
||||
label: lsp::ParameterLabel::Simple("hoge: String".to_string()),
|
||||
documentation: None,
|
||||
},
|
||||
lsp::ParameterInformation {
|
||||
label: lsp::ParameterLabel::Simple("fuga: bool".to_string()),
|
||||
documentation: None,
|
||||
},
|
||||
]),
|
||||
active_parameter: None,
|
||||
},
|
||||
],
|
||||
active_signature: Some(1),
|
||||
active_parameter: None,
|
||||
};
|
||||
let maybe_markdown = cx.update(|cx| SignatureHelp::new(signature_help, None, None, cx));
|
||||
assert!(maybe_markdown.is_some());
|
||||
|
||||
let markdown = maybe_markdown.unwrap();
|
||||
let signature = markdown.signatures[markdown.active_signature].clone();
|
||||
let markdown = (signature.label, signature.highlights);
|
||||
assert_eq!(
|
||||
markdown,
|
||||
(
|
||||
SharedString::new_static("fn test2(hoge: String, fuga: bool)"),
|
||||
vec![(9..21, current_parameter())]
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn test_create_signature_help_markdown_string_7(cx: &mut TestAppContext) {
|
||||
let signature_help = lsp::SignatureHelp {
|
||||
signatures: vec![
|
||||
lsp::SignatureInformation {
|
||||
label: "fn test1(foo: u8, bar: &str)".to_string(),
|
||||
documentation: None,
|
||||
parameters: Some(vec![
|
||||
lsp::ParameterInformation {
|
||||
label: lsp::ParameterLabel::Simple("foo: u8".to_string()),
|
||||
documentation: None,
|
||||
},
|
||||
lsp::ParameterInformation {
|
||||
label: lsp::ParameterLabel::Simple("bar: &str".to_string()),
|
||||
documentation: None,
|
||||
},
|
||||
]),
|
||||
active_parameter: None,
|
||||
},
|
||||
lsp::SignatureInformation {
|
||||
label: "fn test2(hoge: String, fuga: bool)".to_string(),
|
||||
documentation: None,
|
||||
parameters: Some(vec![
|
||||
lsp::ParameterInformation {
|
||||
label: lsp::ParameterLabel::Simple("hoge: String".to_string()),
|
||||
documentation: None,
|
||||
},
|
||||
lsp::ParameterInformation {
|
||||
label: lsp::ParameterLabel::Simple("fuga: bool".to_string()),
|
||||
documentation: None,
|
||||
},
|
||||
]),
|
||||
active_parameter: None,
|
||||
},
|
||||
lsp::SignatureInformation {
|
||||
label: "fn test3(one: usize, two: u32)".to_string(),
|
||||
documentation: None,
|
||||
parameters: Some(vec![
|
||||
lsp::ParameterInformation {
|
||||
label: lsp::ParameterLabel::Simple("one: usize".to_string()),
|
||||
documentation: None,
|
||||
},
|
||||
lsp::ParameterInformation {
|
||||
label: lsp::ParameterLabel::Simple("two: u32".to_string()),
|
||||
documentation: None,
|
||||
},
|
||||
]),
|
||||
active_parameter: None,
|
||||
},
|
||||
],
|
||||
active_signature: Some(2),
|
||||
active_parameter: Some(1),
|
||||
};
|
||||
let maybe_markdown = cx.update(|cx| SignatureHelp::new(signature_help, None, None, cx));
|
||||
assert!(maybe_markdown.is_some());
|
||||
|
||||
let markdown = maybe_markdown.unwrap();
|
||||
let signature = markdown.signatures[markdown.active_signature].clone();
|
||||
let markdown = (signature.label, signature.highlights);
|
||||
assert_eq!(
|
||||
markdown,
|
||||
(
|
||||
SharedString::new_static("fn test3(one: usize, two: u32)"),
|
||||
vec![(21..29, current_parameter())]
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn test_create_signature_help_markdown_string_8(cx: &mut TestAppContext) {
|
||||
let signature_help = lsp::SignatureHelp {
|
||||
signatures: vec![],
|
||||
active_signature: None,
|
||||
active_parameter: None,
|
||||
};
|
||||
let maybe_markdown = cx.update(|cx| SignatureHelp::new(signature_help, None, None, cx));
|
||||
assert!(maybe_markdown.is_none());
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn test_create_signature_help_markdown_string_9(cx: &mut TestAppContext) {
|
||||
let signature_help = lsp::SignatureHelp {
|
||||
signatures: vec![lsp::SignatureInformation {
|
||||
label: "fn test(foo: u8, bar: &str)".to_string(),
|
||||
documentation: None,
|
||||
parameters: Some(vec![
|
||||
lsp::ParameterInformation {
|
||||
label: lsp::ParameterLabel::LabelOffsets([8, 15]),
|
||||
documentation: None,
|
||||
},
|
||||
lsp::ParameterInformation {
|
||||
label: lsp::ParameterLabel::LabelOffsets([17, 26]),
|
||||
documentation: None,
|
||||
},
|
||||
]),
|
||||
active_parameter: None,
|
||||
}],
|
||||
active_signature: Some(0),
|
||||
active_parameter: Some(0),
|
||||
};
|
||||
let maybe_markdown = cx.update(|cx| SignatureHelp::new(signature_help, None, None, cx));
|
||||
assert!(maybe_markdown.is_some());
|
||||
|
||||
let markdown = maybe_markdown.unwrap();
|
||||
let signature = markdown.signatures[markdown.active_signature].clone();
|
||||
let markdown = (signature.label, signature.highlights);
|
||||
assert_eq!(
|
||||
markdown,
|
||||
(
|
||||
SharedString::new_static("fn test(foo: u8, bar: &str)"),
|
||||
vec![(8..15, current_parameter())]
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn test_parameter_documentation(cx: &mut TestAppContext) {
|
||||
let signature_help = lsp::SignatureHelp {
|
||||
signatures: vec![lsp::SignatureInformation {
|
||||
label: "fn test(foo: u8, bar: &str)".to_string(),
|
||||
documentation: Some(Documentation::String(
|
||||
"This is a test documentation".to_string(),
|
||||
)),
|
||||
parameters: Some(vec![
|
||||
lsp::ParameterInformation {
|
||||
label: lsp::ParameterLabel::Simple("foo: u8".to_string()),
|
||||
documentation: Some(Documentation::String("The foo parameter".to_string())),
|
||||
},
|
||||
lsp::ParameterInformation {
|
||||
label: lsp::ParameterLabel::Simple("bar: &str".to_string()),
|
||||
documentation: Some(Documentation::String("The bar parameter".to_string())),
|
||||
},
|
||||
]),
|
||||
active_parameter: None,
|
||||
}],
|
||||
active_signature: Some(0),
|
||||
active_parameter: Some(0),
|
||||
};
|
||||
let maybe_signature_help = cx.update(|cx| SignatureHelp::new(signature_help, None, None, cx));
|
||||
assert!(maybe_signature_help.is_some());
|
||||
|
||||
let signature_help = maybe_signature_help.unwrap();
|
||||
let signature = &signature_help.signatures[signature_help.active_signature];
|
||||
|
||||
// Check that parameter documentation is extracted
|
||||
assert_eq!(signature.parameters.len(), 2);
|
||||
assert_eq!(
|
||||
signature.parameters[0]
|
||||
.documentation
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.update(cx, |documentation, _| documentation.source().to_owned()),
|
||||
"The foo parameter",
|
||||
);
|
||||
assert_eq!(
|
||||
signature.parameters[1]
|
||||
.documentation
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.update(cx, |documentation, _| documentation.source().to_owned()),
|
||||
"The bar parameter",
|
||||
);
|
||||
|
||||
// Check that the active parameter is correct
|
||||
assert_eq!(signature.active_parameter, Some(0));
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn test_create_signature_help_implements_utf16_spec(cx: &mut TestAppContext) {
|
||||
let signature_help = lsp::SignatureHelp {
|
||||
signatures: vec![lsp::SignatureInformation {
|
||||
label: "fn test(🦀: u8, 🦀: &str)".to_string(),
|
||||
documentation: None,
|
||||
parameters: Some(vec![
|
||||
lsp::ParameterInformation {
|
||||
label: lsp::ParameterLabel::LabelOffsets([8, 10]),
|
||||
documentation: None,
|
||||
},
|
||||
lsp::ParameterInformation {
|
||||
label: lsp::ParameterLabel::LabelOffsets([16, 18]),
|
||||
documentation: None,
|
||||
},
|
||||
]),
|
||||
active_parameter: None,
|
||||
}],
|
||||
active_signature: Some(0),
|
||||
active_parameter: Some(0),
|
||||
};
|
||||
let signature_help = cx.update(|cx| SignatureHelp::new(signature_help, None, None, cx));
|
||||
assert!(signature_help.is_some());
|
||||
|
||||
let markdown = signature_help.unwrap();
|
||||
let signature = markdown.signatures[markdown.active_signature].clone();
|
||||
let markdown = (signature.label, signature.highlights);
|
||||
assert_eq!(
|
||||
markdown,
|
||||
(
|
||||
SharedString::new_static("fn test(🦀: u8, 🦀: &str)"),
|
||||
vec![(8..12, current_parameter())]
|
||||
)
|
||||
);
|
||||
}
|
||||
674
crates/project/tests/integration/task_inventory.rs
Normal file
674
crates/project/tests/integration/task_inventory.rs
Normal file
@@ -0,0 +1,674 @@
|
||||
use gpui::{AppContext, Entity, Task, TestAppContext};
|
||||
use itertools::Itertools;
|
||||
use paths::tasks_file;
|
||||
use pretty_assertions::assert_eq;
|
||||
use serde_json::json;
|
||||
use settings::SettingsLocation;
|
||||
use std::path::Path;
|
||||
use std::sync::Arc;
|
||||
use util::rel_path::rel_path;
|
||||
|
||||
use project::task_store::{TaskSettingsLocation, TaskStore};
|
||||
|
||||
use project::{WorktreeId, task_inventory::*};
|
||||
use test_inventory::*;
|
||||
|
||||
mod test_inventory {
|
||||
use gpui::{AppContext as _, Entity, Task, TestAppContext};
|
||||
use itertools::Itertools;
|
||||
use task::TaskContext;
|
||||
use worktree::WorktreeId;
|
||||
|
||||
use crate::Inventory;
|
||||
|
||||
use super::TaskSourceKind;
|
||||
|
||||
pub(super) fn task_template_names(
|
||||
inventory: &Entity<Inventory>,
|
||||
worktree: Option<WorktreeId>,
|
||||
cx: &mut TestAppContext,
|
||||
) -> Task<Vec<String>> {
|
||||
let new_tasks = inventory.update(cx, |inventory, cx| {
|
||||
inventory.list_tasks(None, None, worktree, cx)
|
||||
});
|
||||
cx.background_spawn(async move {
|
||||
new_tasks
|
||||
.await
|
||||
.into_iter()
|
||||
.map(|(_, task)| task.label)
|
||||
.sorted()
|
||||
.collect()
|
||||
})
|
||||
}
|
||||
|
||||
pub(super) fn register_task_used(
|
||||
inventory: &Entity<Inventory>,
|
||||
task_name: &str,
|
||||
cx: &mut TestAppContext,
|
||||
) -> Task<()> {
|
||||
let tasks = inventory.update(cx, |inventory, cx| {
|
||||
inventory.list_tasks(None, None, None, cx)
|
||||
});
|
||||
|
||||
let task_name = task_name.to_owned();
|
||||
let inventory = inventory.clone();
|
||||
cx.spawn(|mut cx| async move {
|
||||
let (task_source_kind, task) = tasks
|
||||
.await
|
||||
.into_iter()
|
||||
.find(|(_, task)| task.label == task_name)
|
||||
.unwrap_or_else(|| panic!("Failed to find task with name {task_name}"));
|
||||
|
||||
let id_base = task_source_kind.to_id_base();
|
||||
inventory.update(&mut cx, |inventory, _| {
|
||||
inventory.task_scheduled(
|
||||
task_source_kind.clone(),
|
||||
task.resolve_task(&id_base, &TaskContext::default())
|
||||
.unwrap_or_else(|| panic!("Failed to resolve task with name {task_name}")),
|
||||
)
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
pub(super) fn register_worktree_task_used(
|
||||
inventory: &Entity<Inventory>,
|
||||
worktree_id: WorktreeId,
|
||||
task_name: &str,
|
||||
cx: &mut TestAppContext,
|
||||
) -> Task<()> {
|
||||
let tasks = inventory.update(cx, |inventory, cx| {
|
||||
inventory.list_tasks(None, None, Some(worktree_id), cx)
|
||||
});
|
||||
|
||||
let inventory = inventory.clone();
|
||||
let task_name = task_name.to_owned();
|
||||
cx.spawn(|mut cx| async move {
|
||||
let (task_source_kind, task) = tasks
|
||||
.await
|
||||
.into_iter()
|
||||
.find(|(_, task)| task.label == task_name)
|
||||
.unwrap_or_else(|| panic!("Failed to find task with name {task_name}"));
|
||||
let id_base = task_source_kind.to_id_base();
|
||||
inventory.update(&mut cx, |inventory, _| {
|
||||
inventory.task_scheduled(
|
||||
task_source_kind.clone(),
|
||||
task.resolve_task(&id_base, &TaskContext::default())
|
||||
.unwrap_or_else(|| panic!("Failed to resolve task with name {task_name}")),
|
||||
);
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
pub(super) async fn list_tasks(
|
||||
inventory: &Entity<Inventory>,
|
||||
worktree: Option<WorktreeId>,
|
||||
cx: &mut TestAppContext,
|
||||
) -> Vec<(TaskSourceKind, String)> {
|
||||
let task_context = &TaskContext::default();
|
||||
inventory
|
||||
.update(cx, |inventory, cx| {
|
||||
inventory.list_tasks(None, None, worktree, cx)
|
||||
})
|
||||
.await
|
||||
.into_iter()
|
||||
.filter_map(|(source_kind, task)| {
|
||||
let id_base = source_kind.to_id_base();
|
||||
Some((source_kind, task.resolve_task(&id_base, task_context)?))
|
||||
})
|
||||
.map(|(source_kind, resolved_task)| (source_kind, resolved_task.resolved_label))
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_task_list_sorting(cx: &mut TestAppContext) {
|
||||
init_test(cx);
|
||||
let inventory = cx.update(|cx| Inventory::new(cx));
|
||||
let initial_tasks = resolved_task_names(&inventory, None, cx).await;
|
||||
assert!(
|
||||
initial_tasks.is_empty(),
|
||||
"No tasks expected for empty inventory, but got {initial_tasks:?}"
|
||||
);
|
||||
let initial_tasks = task_template_names(&inventory, None, cx).await;
|
||||
assert!(
|
||||
initial_tasks.is_empty(),
|
||||
"No tasks expected for empty inventory, but got {initial_tasks:?}"
|
||||
);
|
||||
cx.run_until_parked();
|
||||
let expected_initial_state = [
|
||||
"1_a_task".to_string(),
|
||||
"1_task".to_string(),
|
||||
"2_task".to_string(),
|
||||
"3_task".to_string(),
|
||||
];
|
||||
|
||||
inventory.update(cx, |inventory, _| {
|
||||
inventory
|
||||
.update_file_based_tasks(
|
||||
TaskSettingsLocation::Global(tasks_file()),
|
||||
Some(&mock_tasks_from_names(
|
||||
expected_initial_state.iter().map(|name| name.as_str()),
|
||||
)),
|
||||
)
|
||||
.unwrap();
|
||||
});
|
||||
assert_eq!(
|
||||
task_template_names(&inventory, None, cx).await,
|
||||
&expected_initial_state,
|
||||
);
|
||||
assert_eq!(
|
||||
resolved_task_names(&inventory, None, cx).await,
|
||||
&expected_initial_state,
|
||||
"Tasks with equal amount of usages should be sorted alphanumerically"
|
||||
);
|
||||
|
||||
register_task_used(&inventory, "2_task", cx).await;
|
||||
assert_eq!(
|
||||
task_template_names(&inventory, None, cx).await,
|
||||
&expected_initial_state,
|
||||
);
|
||||
assert_eq!(
|
||||
resolved_task_names(&inventory, None, cx).await,
|
||||
vec![
|
||||
"2_task".to_string(),
|
||||
"1_a_task".to_string(),
|
||||
"1_task".to_string(),
|
||||
"3_task".to_string()
|
||||
],
|
||||
);
|
||||
|
||||
register_task_used(&inventory, "1_task", cx).await;
|
||||
register_task_used(&inventory, "1_task", cx).await;
|
||||
register_task_used(&inventory, "1_task", cx).await;
|
||||
register_task_used(&inventory, "3_task", cx).await;
|
||||
assert_eq!(
|
||||
task_template_names(&inventory, None, cx).await,
|
||||
&expected_initial_state,
|
||||
);
|
||||
assert_eq!(
|
||||
resolved_task_names(&inventory, None, cx).await,
|
||||
vec![
|
||||
"3_task".to_string(),
|
||||
"1_task".to_string(),
|
||||
"2_task".to_string(),
|
||||
"1_a_task".to_string(),
|
||||
],
|
||||
"Most recently used task should be at the top"
|
||||
);
|
||||
|
||||
let worktree_id = WorktreeId::from_usize(0);
|
||||
let local_worktree_location = SettingsLocation {
|
||||
worktree_id,
|
||||
path: rel_path("foo"),
|
||||
};
|
||||
inventory.update(cx, |inventory, _| {
|
||||
inventory
|
||||
.update_file_based_tasks(
|
||||
TaskSettingsLocation::Worktree(local_worktree_location),
|
||||
Some(&mock_tasks_from_names(["worktree_task_1"])),
|
||||
)
|
||||
.unwrap();
|
||||
});
|
||||
assert_eq!(
|
||||
resolved_task_names(&inventory, None, cx).await,
|
||||
vec![
|
||||
"3_task".to_string(),
|
||||
"1_task".to_string(),
|
||||
"2_task".to_string(),
|
||||
"1_a_task".to_string(),
|
||||
],
|
||||
"Most recently used task should be at the top"
|
||||
);
|
||||
assert_eq!(
|
||||
resolved_task_names(&inventory, Some(worktree_id), cx).await,
|
||||
vec![
|
||||
"3_task".to_string(),
|
||||
"1_task".to_string(),
|
||||
"2_task".to_string(),
|
||||
"worktree_task_1".to_string(),
|
||||
"1_a_task".to_string(),
|
||||
],
|
||||
);
|
||||
register_worktree_task_used(&inventory, worktree_id, "worktree_task_1", cx).await;
|
||||
assert_eq!(
|
||||
resolved_task_names(&inventory, Some(worktree_id), cx).await,
|
||||
vec![
|
||||
"worktree_task_1".to_string(),
|
||||
"3_task".to_string(),
|
||||
"1_task".to_string(),
|
||||
"2_task".to_string(),
|
||||
"1_a_task".to_string(),
|
||||
],
|
||||
"Most recently used worktree task should be at the top"
|
||||
);
|
||||
|
||||
inventory.update(cx, |inventory, _| {
|
||||
inventory
|
||||
.update_file_based_tasks(
|
||||
TaskSettingsLocation::Global(tasks_file()),
|
||||
Some(&mock_tasks_from_names(
|
||||
["10_hello", "11_hello"]
|
||||
.into_iter()
|
||||
.chain(expected_initial_state.iter().map(|name| name.as_str())),
|
||||
)),
|
||||
)
|
||||
.unwrap();
|
||||
});
|
||||
cx.run_until_parked();
|
||||
let expected_updated_state = [
|
||||
"10_hello".to_string(),
|
||||
"11_hello".to_string(),
|
||||
"1_a_task".to_string(),
|
||||
"1_task".to_string(),
|
||||
"2_task".to_string(),
|
||||
"3_task".to_string(),
|
||||
];
|
||||
assert_eq!(
|
||||
task_template_names(&inventory, None, cx).await,
|
||||
&expected_updated_state,
|
||||
);
|
||||
assert_eq!(
|
||||
resolved_task_names(&inventory, None, cx).await,
|
||||
vec![
|
||||
"worktree_task_1".to_string(),
|
||||
"1_a_task".to_string(),
|
||||
"1_task".to_string(),
|
||||
"2_task".to_string(),
|
||||
"3_task".to_string(),
|
||||
"10_hello".to_string(),
|
||||
"11_hello".to_string(),
|
||||
],
|
||||
"After global tasks update, worktree task usage is not erased and it's the first still; global task is back to regular order as its file was updated"
|
||||
);
|
||||
|
||||
register_task_used(&inventory, "11_hello", cx).await;
|
||||
assert_eq!(
|
||||
task_template_names(&inventory, None, cx).await,
|
||||
&expected_updated_state,
|
||||
);
|
||||
assert_eq!(
|
||||
resolved_task_names(&inventory, None, cx).await,
|
||||
vec![
|
||||
"11_hello".to_string(),
|
||||
"worktree_task_1".to_string(),
|
||||
"1_a_task".to_string(),
|
||||
"1_task".to_string(),
|
||||
"2_task".to_string(),
|
||||
"3_task".to_string(),
|
||||
"10_hello".to_string(),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_reloading_debug_scenarios(cx: &mut TestAppContext) {
|
||||
init_test(cx);
|
||||
let inventory = cx.update(|cx| Inventory::new(cx));
|
||||
inventory.update(cx, |inventory, _| {
|
||||
inventory
|
||||
.update_file_based_scenarios(
|
||||
TaskSettingsLocation::Global(Path::new("")),
|
||||
Some(
|
||||
r#"
|
||||
[{
|
||||
"label": "test scenario",
|
||||
"adapter": "CodeLLDB",
|
||||
"request": "launch",
|
||||
"program": "wowzer",
|
||||
}]
|
||||
"#,
|
||||
),
|
||||
)
|
||||
.unwrap();
|
||||
});
|
||||
|
||||
let (_, scenario) = inventory
|
||||
.update(cx, |this, cx| {
|
||||
this.list_debug_scenarios(&TaskContexts::default(), vec![], vec![], false, cx)
|
||||
})
|
||||
.await
|
||||
.1
|
||||
.first()
|
||||
.unwrap()
|
||||
.clone();
|
||||
|
||||
inventory.update(cx, |this, _| {
|
||||
this.scenario_scheduled(scenario.clone(), Default::default(), None, None);
|
||||
});
|
||||
|
||||
assert_eq!(
|
||||
inventory
|
||||
.update(cx, |this, cx| {
|
||||
this.list_debug_scenarios(&Default::default(), vec![], vec![], false, cx)
|
||||
})
|
||||
.await
|
||||
.0
|
||||
.first()
|
||||
.unwrap()
|
||||
.clone()
|
||||
.0,
|
||||
scenario
|
||||
);
|
||||
|
||||
inventory.update(cx, |this, _| {
|
||||
this.update_file_based_scenarios(
|
||||
TaskSettingsLocation::Global(Path::new("")),
|
||||
Some(
|
||||
r#"
|
||||
[{
|
||||
"label": "test scenario",
|
||||
"adapter": "Delve",
|
||||
"request": "launch",
|
||||
"program": "wowzer",
|
||||
}]
|
||||
"#,
|
||||
),
|
||||
)
|
||||
.unwrap();
|
||||
});
|
||||
|
||||
assert_eq!(
|
||||
inventory
|
||||
.update(cx, |this, cx| {
|
||||
this.list_debug_scenarios(&Default::default(), vec![], vec![], false, cx)
|
||||
})
|
||||
.await
|
||||
.0
|
||||
.first()
|
||||
.unwrap()
|
||||
.0
|
||||
.adapter,
|
||||
"Delve",
|
||||
);
|
||||
|
||||
inventory.update(cx, |this, _| {
|
||||
this.update_file_based_scenarios(
|
||||
TaskSettingsLocation::Global(Path::new("")),
|
||||
Some(
|
||||
r#"
|
||||
[{
|
||||
"label": "testing scenario",
|
||||
"adapter": "Delve",
|
||||
"request": "launch",
|
||||
"program": "wowzer",
|
||||
}]
|
||||
"#,
|
||||
),
|
||||
)
|
||||
.unwrap();
|
||||
});
|
||||
|
||||
assert!(
|
||||
inventory
|
||||
.update(cx, |this, cx| {
|
||||
this.list_debug_scenarios(&TaskContexts::default(), vec![], vec![], false, cx)
|
||||
})
|
||||
.await
|
||||
.0
|
||||
.is_empty(),
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_inventory_static_task_filters(cx: &mut TestAppContext) {
|
||||
init_test(cx);
|
||||
let inventory = cx.update(|cx| Inventory::new(cx));
|
||||
let common_name = "common_task_name";
|
||||
let worktree_1 = WorktreeId::from_usize(1);
|
||||
let worktree_2 = WorktreeId::from_usize(2);
|
||||
|
||||
cx.run_until_parked();
|
||||
let worktree_independent_tasks = vec![
|
||||
(
|
||||
TaskSourceKind::AbsPath {
|
||||
id_base: "global tasks.json".into(),
|
||||
abs_path: paths::tasks_file().clone(),
|
||||
},
|
||||
common_name.to_string(),
|
||||
),
|
||||
(
|
||||
TaskSourceKind::AbsPath {
|
||||
id_base: "global tasks.json".into(),
|
||||
abs_path: paths::tasks_file().clone(),
|
||||
},
|
||||
"static_source_1".to_string(),
|
||||
),
|
||||
(
|
||||
TaskSourceKind::AbsPath {
|
||||
id_base: "global tasks.json".into(),
|
||||
abs_path: paths::tasks_file().clone(),
|
||||
},
|
||||
"static_source_2".to_string(),
|
||||
),
|
||||
];
|
||||
let worktree_1_tasks = [
|
||||
(
|
||||
TaskSourceKind::Worktree {
|
||||
id: worktree_1,
|
||||
directory_in_worktree: rel_path(".zed").into(),
|
||||
id_base: "local worktree tasks from directory \".zed\"".into(),
|
||||
},
|
||||
common_name.to_string(),
|
||||
),
|
||||
(
|
||||
TaskSourceKind::Worktree {
|
||||
id: worktree_1,
|
||||
directory_in_worktree: rel_path(".zed").into(),
|
||||
id_base: "local worktree tasks from directory \".zed\"".into(),
|
||||
},
|
||||
"worktree_1".to_string(),
|
||||
),
|
||||
];
|
||||
let worktree_2_tasks = [
|
||||
(
|
||||
TaskSourceKind::Worktree {
|
||||
id: worktree_2,
|
||||
directory_in_worktree: rel_path(".zed").into(),
|
||||
id_base: "local worktree tasks from directory \".zed\"".into(),
|
||||
},
|
||||
common_name.to_string(),
|
||||
),
|
||||
(
|
||||
TaskSourceKind::Worktree {
|
||||
id: worktree_2,
|
||||
directory_in_worktree: rel_path(".zed").into(),
|
||||
id_base: "local worktree tasks from directory \".zed\"".into(),
|
||||
},
|
||||
"worktree_2".to_string(),
|
||||
),
|
||||
];
|
||||
|
||||
inventory.update(cx, |inventory, _| {
|
||||
inventory
|
||||
.update_file_based_tasks(
|
||||
TaskSettingsLocation::Global(tasks_file()),
|
||||
Some(&mock_tasks_from_names(
|
||||
worktree_independent_tasks
|
||||
.iter()
|
||||
.map(|(_, name)| name.as_str()),
|
||||
)),
|
||||
)
|
||||
.unwrap();
|
||||
inventory
|
||||
.update_file_based_tasks(
|
||||
TaskSettingsLocation::Worktree(SettingsLocation {
|
||||
worktree_id: worktree_1,
|
||||
path: rel_path(".zed"),
|
||||
}),
|
||||
Some(&mock_tasks_from_names(
|
||||
worktree_1_tasks.iter().map(|(_, name)| name.as_str()),
|
||||
)),
|
||||
)
|
||||
.unwrap();
|
||||
inventory
|
||||
.update_file_based_tasks(
|
||||
TaskSettingsLocation::Worktree(SettingsLocation {
|
||||
worktree_id: worktree_2,
|
||||
path: rel_path(".zed"),
|
||||
}),
|
||||
Some(&mock_tasks_from_names(
|
||||
worktree_2_tasks.iter().map(|(_, name)| name.as_str()),
|
||||
)),
|
||||
)
|
||||
.unwrap();
|
||||
});
|
||||
|
||||
assert_eq!(
|
||||
list_tasks_sorted_by_last_used(&inventory, None, cx).await,
|
||||
worktree_independent_tasks,
|
||||
"Without a worktree, only worktree-independent tasks should be listed"
|
||||
);
|
||||
assert_eq!(
|
||||
list_tasks_sorted_by_last_used(&inventory, Some(worktree_1), cx).await,
|
||||
worktree_1_tasks
|
||||
.iter()
|
||||
.chain(worktree_independent_tasks.iter())
|
||||
.cloned()
|
||||
.sorted_by_key(|(kind, label)| (task_source_kind_preference(kind), label.clone()))
|
||||
.collect::<Vec<_>>(),
|
||||
);
|
||||
assert_eq!(
|
||||
list_tasks_sorted_by_last_used(&inventory, Some(worktree_2), cx).await,
|
||||
worktree_2_tasks
|
||||
.iter()
|
||||
.chain(worktree_independent_tasks.iter())
|
||||
.cloned()
|
||||
.sorted_by_key(|(kind, label)| (task_source_kind_preference(kind), label.clone()))
|
||||
.collect::<Vec<_>>(),
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
list_tasks(&inventory, None, cx).await,
|
||||
worktree_independent_tasks,
|
||||
"Without a worktree, only worktree-independent tasks should be listed"
|
||||
);
|
||||
assert_eq!(
|
||||
list_tasks(&inventory, Some(worktree_1), cx).await,
|
||||
worktree_1_tasks
|
||||
.iter()
|
||||
.chain(worktree_independent_tasks.iter())
|
||||
.cloned()
|
||||
.collect::<Vec<_>>(),
|
||||
);
|
||||
assert_eq!(
|
||||
list_tasks(&inventory, Some(worktree_2), cx).await,
|
||||
worktree_2_tasks
|
||||
.iter()
|
||||
.chain(worktree_independent_tasks.iter())
|
||||
.cloned()
|
||||
.collect::<Vec<_>>(),
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_zed_tasks_take_precedence_over_vscode(cx: &mut TestAppContext) {
|
||||
init_test(cx);
|
||||
let inventory = cx.update(|cx| Inventory::new(cx));
|
||||
let worktree_id = WorktreeId::from_usize(0);
|
||||
|
||||
inventory.update(cx, |inventory, _| {
|
||||
inventory
|
||||
.update_file_based_tasks(
|
||||
TaskSettingsLocation::Worktree(SettingsLocation {
|
||||
worktree_id,
|
||||
path: rel_path(".vscode"),
|
||||
}),
|
||||
Some(&mock_tasks_from_names(["vscode_task"])),
|
||||
)
|
||||
.unwrap();
|
||||
});
|
||||
assert_eq!(
|
||||
task_template_names(&inventory, Some(worktree_id), cx).await,
|
||||
vec!["vscode_task"],
|
||||
"With only .vscode tasks, they should appear"
|
||||
);
|
||||
|
||||
inventory.update(cx, |inventory, _| {
|
||||
inventory
|
||||
.update_file_based_tasks(
|
||||
TaskSettingsLocation::Worktree(SettingsLocation {
|
||||
worktree_id,
|
||||
path: rel_path(".zed"),
|
||||
}),
|
||||
Some(&mock_tasks_from_names(["zed_task"])),
|
||||
)
|
||||
.unwrap();
|
||||
});
|
||||
assert_eq!(
|
||||
task_template_names(&inventory, Some(worktree_id), cx).await,
|
||||
vec!["zed_task"],
|
||||
"With both .zed and .vscode tasks, only .zed tasks should appear"
|
||||
);
|
||||
|
||||
register_worktree_task_used(&inventory, worktree_id, "zed_task", cx).await;
|
||||
let resolved = resolved_task_names(&inventory, Some(worktree_id), cx).await;
|
||||
assert!(
|
||||
!resolved.iter().any(|name| name == "vscode_task"),
|
||||
"Previously used .vscode tasks should not appear when .zed tasks exist, got: {resolved:?}"
|
||||
);
|
||||
}
|
||||
|
||||
fn init_test(_cx: &mut TestAppContext) {
|
||||
zlog::init_test();
|
||||
TaskStore::init(None);
|
||||
}
|
||||
|
||||
fn resolved_task_names(
|
||||
inventory: &Entity<Inventory>,
|
||||
worktree: Option<WorktreeId>,
|
||||
cx: &mut TestAppContext,
|
||||
) -> Task<Vec<String>> {
|
||||
let tasks = inventory.update(cx, |inventory, cx| {
|
||||
let mut task_contexts = TaskContexts::default();
|
||||
task_contexts.active_worktree_context =
|
||||
worktree.map(|worktree| (worktree, Default::default()));
|
||||
|
||||
inventory.used_and_current_resolved_tasks(Arc::new(task_contexts), cx)
|
||||
});
|
||||
|
||||
cx.background_spawn(async move {
|
||||
let (used, current) = tasks.await;
|
||||
used.into_iter()
|
||||
.chain(current)
|
||||
.map(|(_, task)| task.original_task().label.clone())
|
||||
.collect()
|
||||
})
|
||||
}
|
||||
|
||||
fn mock_tasks_from_names<'a>(task_names: impl IntoIterator<Item = &'a str> + 'a) -> String {
|
||||
serde_json::to_string(&serde_json::Value::Array(
|
||||
task_names
|
||||
.into_iter()
|
||||
.map(|task_name| {
|
||||
json!({
|
||||
"label": task_name,
|
||||
"command": "echo",
|
||||
"args": vec![task_name],
|
||||
})
|
||||
})
|
||||
.collect::<Vec<_>>(),
|
||||
))
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
async fn list_tasks_sorted_by_last_used(
|
||||
inventory: &Entity<Inventory>,
|
||||
worktree: Option<WorktreeId>,
|
||||
cx: &mut TestAppContext,
|
||||
) -> Vec<(TaskSourceKind, String)> {
|
||||
let (used, current) = inventory
|
||||
.update(cx, |inventory, cx| {
|
||||
let mut task_contexts = TaskContexts::default();
|
||||
task_contexts.active_worktree_context =
|
||||
worktree.map(|worktree| (worktree, Default::default()));
|
||||
|
||||
inventory.used_and_current_resolved_tasks(Arc::new(task_contexts), cx)
|
||||
})
|
||||
.await;
|
||||
let mut all = used;
|
||||
all.extend(current);
|
||||
all.into_iter()
|
||||
.map(|(source_kind, task)| (source_kind, task.resolved_label))
|
||||
.sorted_by_key(|(kind, label)| (task_source_kind_preference(kind), label.clone()))
|
||||
.collect()
|
||||
}
|
||||
957
crates/project/tests/integration/trusted_worktrees.rs
Normal file
957
crates/project/tests/integration/trusted_worktrees.rs
Normal file
@@ -0,0 +1,957 @@
|
||||
use std::{cell::RefCell, path::PathBuf, rc::Rc};
|
||||
|
||||
use collections::HashSet;
|
||||
use gpui::{Entity, TestAppContext};
|
||||
use serde_json::json;
|
||||
use settings::SettingsStore;
|
||||
use util::path;
|
||||
|
||||
use crate::{FakeFs, Project};
|
||||
|
||||
use project::{trusted_worktrees::*, worktree_store::WorktreeStore};
|
||||
|
||||
fn init_test(cx: &mut TestAppContext) {
|
||||
cx.update(|cx| {
|
||||
if cx.try_global::<SettingsStore>().is_none() {
|
||||
let settings_store = SettingsStore::test(cx);
|
||||
cx.set_global(settings_store);
|
||||
}
|
||||
if cx.try_global::<TrustedWorktrees>().is_some() {
|
||||
cx.remove_global::<TrustedWorktrees>();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
fn init_trust_global(
|
||||
worktree_store: Entity<WorktreeStore>,
|
||||
cx: &mut TestAppContext,
|
||||
) -> Entity<TrustedWorktreesStore> {
|
||||
cx.update(|cx| {
|
||||
init(DbTrustedPaths::default(), cx);
|
||||
track_worktree_trust(worktree_store, None, None, None, cx);
|
||||
TrustedWorktrees::try_get_global(cx).expect("global should be set")
|
||||
})
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_single_worktree_trust(cx: &mut TestAppContext) {
|
||||
init_test(cx);
|
||||
|
||||
let fs = FakeFs::new(cx.executor());
|
||||
fs.insert_tree(path!("/root"), json!({ "main.rs": "fn main() {}" }))
|
||||
.await;
|
||||
|
||||
let project = Project::test(fs, [path!("/root").as_ref()], cx).await;
|
||||
let worktree_store = project.read_with(cx, |project, _| project.worktree_store());
|
||||
let worktree_id = worktree_store.read_with(cx, |store, cx| {
|
||||
store.worktrees().next().unwrap().read(cx).id()
|
||||
});
|
||||
|
||||
let trusted_worktrees = init_trust_global(worktree_store.clone(), cx);
|
||||
|
||||
let events: Rc<RefCell<Vec<TrustedWorktreesEvent>>> = Rc::default();
|
||||
cx.update({
|
||||
let events = events.clone();
|
||||
|cx| {
|
||||
cx.subscribe(&trusted_worktrees, move |_, event, _| {
|
||||
events.borrow_mut().push(match event {
|
||||
TrustedWorktreesEvent::Trusted(host, paths) => {
|
||||
TrustedWorktreesEvent::Trusted(host.clone(), paths.clone())
|
||||
}
|
||||
TrustedWorktreesEvent::Restricted(host, paths) => {
|
||||
TrustedWorktreesEvent::Restricted(host.clone(), paths.clone())
|
||||
}
|
||||
});
|
||||
})
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
|
||||
let can_trust = trusted_worktrees.update(cx, |store, cx| {
|
||||
store.can_trust(&worktree_store, worktree_id, cx)
|
||||
});
|
||||
assert!(!can_trust, "worktree should be restricted by default");
|
||||
|
||||
{
|
||||
let events = events.borrow();
|
||||
assert_eq!(events.len(), 1);
|
||||
match &events[0] {
|
||||
TrustedWorktreesEvent::Restricted(event_worktree_store, paths) => {
|
||||
assert_eq!(event_worktree_store, &worktree_store.downgrade());
|
||||
assert!(paths.contains(&PathTrust::Worktree(worktree_id)));
|
||||
}
|
||||
_ => panic!("expected Restricted event"),
|
||||
}
|
||||
}
|
||||
|
||||
let has_restricted = trusted_worktrees.read_with(cx, |store, cx| {
|
||||
store.has_restricted_worktrees(&worktree_store, cx)
|
||||
});
|
||||
assert!(has_restricted, "should have restricted worktrees");
|
||||
|
||||
let restricted = trusted_worktrees.read_with(cx, |trusted_worktrees, cx| {
|
||||
trusted_worktrees.restricted_worktrees(&worktree_store, cx)
|
||||
});
|
||||
assert!(restricted.iter().any(|(id, _)| *id == worktree_id));
|
||||
|
||||
events.borrow_mut().clear();
|
||||
|
||||
let can_trust_again = trusted_worktrees.update(cx, |store, cx| {
|
||||
store.can_trust(&worktree_store, worktree_id, cx)
|
||||
});
|
||||
assert!(!can_trust_again, "worktree should still be restricted");
|
||||
assert!(
|
||||
events.borrow().is_empty(),
|
||||
"no duplicate Restricted event on repeated can_trust"
|
||||
);
|
||||
|
||||
trusted_worktrees.update(cx, |store, cx| {
|
||||
store.trust(
|
||||
&worktree_store,
|
||||
HashSet::from_iter([PathTrust::Worktree(worktree_id)]),
|
||||
cx,
|
||||
);
|
||||
});
|
||||
|
||||
{
|
||||
let events = events.borrow();
|
||||
assert_eq!(events.len(), 1);
|
||||
match &events[0] {
|
||||
TrustedWorktreesEvent::Trusted(event_worktree_store, paths) => {
|
||||
assert_eq!(event_worktree_store, &worktree_store.downgrade());
|
||||
assert!(paths.contains(&PathTrust::Worktree(worktree_id)));
|
||||
}
|
||||
_ => panic!("expected Trusted event"),
|
||||
}
|
||||
}
|
||||
|
||||
let can_trust_after = trusted_worktrees.update(cx, |store, cx| {
|
||||
store.can_trust(&worktree_store, worktree_id, cx)
|
||||
});
|
||||
assert!(can_trust_after, "worktree should be trusted after trust()");
|
||||
|
||||
let has_restricted_after = trusted_worktrees.read_with(cx, |store, cx| {
|
||||
store.has_restricted_worktrees(&worktree_store, cx)
|
||||
});
|
||||
assert!(
|
||||
!has_restricted_after,
|
||||
"should have no restricted worktrees after trust"
|
||||
);
|
||||
|
||||
let restricted_after = trusted_worktrees.read_with(cx, |trusted_worktrees, cx| {
|
||||
trusted_worktrees.restricted_worktrees(&worktree_store, cx)
|
||||
});
|
||||
assert!(
|
||||
restricted_after.is_empty(),
|
||||
"restricted set should be empty"
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_single_file_worktree_trust(cx: &mut TestAppContext) {
|
||||
init_test(cx);
|
||||
|
||||
let fs = FakeFs::new(cx.executor());
|
||||
fs.insert_tree(path!("/root"), json!({ "foo.rs": "fn foo() {}" }))
|
||||
.await;
|
||||
|
||||
let project = Project::test(fs, [path!("/root/foo.rs").as_ref()], cx).await;
|
||||
let worktree_store = project.read_with(cx, |project, _| project.worktree_store());
|
||||
let worktree_id = worktree_store.read_with(cx, |store, cx| {
|
||||
let worktree = store.worktrees().next().unwrap();
|
||||
let worktree = worktree.read(cx);
|
||||
assert!(worktree.is_single_file(), "expected single-file worktree");
|
||||
worktree.id()
|
||||
});
|
||||
|
||||
let trusted_worktrees = init_trust_global(worktree_store.clone(), cx);
|
||||
|
||||
let events: Rc<RefCell<Vec<TrustedWorktreesEvent>>> = Rc::default();
|
||||
cx.update({
|
||||
let events = events.clone();
|
||||
|cx| {
|
||||
cx.subscribe(&trusted_worktrees, move |_, event, _| {
|
||||
events.borrow_mut().push(match event {
|
||||
TrustedWorktreesEvent::Trusted(host, paths) => {
|
||||
TrustedWorktreesEvent::Trusted(host.clone(), paths.clone())
|
||||
}
|
||||
TrustedWorktreesEvent::Restricted(host, paths) => {
|
||||
TrustedWorktreesEvent::Restricted(host.clone(), paths.clone())
|
||||
}
|
||||
});
|
||||
})
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
|
||||
let can_trust = trusted_worktrees.update(cx, |store, cx| {
|
||||
store.can_trust(&worktree_store, worktree_id, cx)
|
||||
});
|
||||
assert!(
|
||||
!can_trust,
|
||||
"single-file worktree should be restricted by default"
|
||||
);
|
||||
|
||||
{
|
||||
let events = events.borrow();
|
||||
assert_eq!(events.len(), 1);
|
||||
match &events[0] {
|
||||
TrustedWorktreesEvent::Restricted(event_worktree_store, paths) => {
|
||||
assert_eq!(event_worktree_store, &worktree_store.downgrade());
|
||||
assert!(paths.contains(&PathTrust::Worktree(worktree_id)));
|
||||
}
|
||||
_ => panic!("expected Restricted event"),
|
||||
}
|
||||
}
|
||||
|
||||
events.borrow_mut().clear();
|
||||
|
||||
trusted_worktrees.update(cx, |store, cx| {
|
||||
store.trust(
|
||||
&worktree_store,
|
||||
HashSet::from_iter([PathTrust::Worktree(worktree_id)]),
|
||||
cx,
|
||||
);
|
||||
});
|
||||
|
||||
{
|
||||
let events = events.borrow();
|
||||
assert_eq!(events.len(), 1);
|
||||
match &events[0] {
|
||||
TrustedWorktreesEvent::Trusted(event_worktree_store, paths) => {
|
||||
assert_eq!(event_worktree_store, &worktree_store.downgrade());
|
||||
assert!(paths.contains(&PathTrust::Worktree(worktree_id)));
|
||||
}
|
||||
_ => panic!("expected Trusted event"),
|
||||
}
|
||||
}
|
||||
|
||||
let can_trust_after = trusted_worktrees.update(cx, |store, cx| {
|
||||
store.can_trust(&worktree_store, worktree_id, cx)
|
||||
});
|
||||
assert!(
|
||||
can_trust_after,
|
||||
"single-file worktree should be trusted after trust()"
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_multiple_single_file_worktrees_trust_one(cx: &mut TestAppContext) {
|
||||
init_test(cx);
|
||||
|
||||
let fs = FakeFs::new(cx.executor());
|
||||
fs.insert_tree(
|
||||
path!("/root"),
|
||||
json!({
|
||||
"a.rs": "fn a() {}",
|
||||
"b.rs": "fn b() {}",
|
||||
"c.rs": "fn c() {}"
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
|
||||
let project = Project::test(
|
||||
fs,
|
||||
[
|
||||
path!("/root/a.rs").as_ref(),
|
||||
path!("/root/b.rs").as_ref(),
|
||||
path!("/root/c.rs").as_ref(),
|
||||
],
|
||||
cx,
|
||||
)
|
||||
.await;
|
||||
let worktree_store = project.read_with(cx, |project, _| project.worktree_store());
|
||||
let worktree_ids: Vec<_> = worktree_store.read_with(cx, |store, cx| {
|
||||
store
|
||||
.worktrees()
|
||||
.map(|worktree| {
|
||||
let worktree = worktree.read(cx);
|
||||
assert!(worktree.is_single_file());
|
||||
worktree.id()
|
||||
})
|
||||
.collect()
|
||||
});
|
||||
assert_eq!(worktree_ids.len(), 3);
|
||||
|
||||
let trusted_worktrees = init_trust_global(worktree_store.clone(), cx);
|
||||
|
||||
for &worktree_id in &worktree_ids {
|
||||
let can_trust = trusted_worktrees.update(cx, |store, cx| {
|
||||
store.can_trust(&worktree_store, worktree_id, cx)
|
||||
});
|
||||
assert!(
|
||||
!can_trust,
|
||||
"worktree {worktree_id:?} should be restricted initially"
|
||||
);
|
||||
}
|
||||
|
||||
trusted_worktrees.update(cx, |store, cx| {
|
||||
store.trust(
|
||||
&worktree_store,
|
||||
HashSet::from_iter([PathTrust::Worktree(worktree_ids[1])]),
|
||||
cx,
|
||||
);
|
||||
});
|
||||
|
||||
let can_trust_0 = trusted_worktrees.update(cx, |store, cx| {
|
||||
store.can_trust(&worktree_store, worktree_ids[0], cx)
|
||||
});
|
||||
let can_trust_1 = trusted_worktrees.update(cx, |store, cx| {
|
||||
store.can_trust(&worktree_store, worktree_ids[1], cx)
|
||||
});
|
||||
let can_trust_2 = trusted_worktrees.update(cx, |store, cx| {
|
||||
store.can_trust(&worktree_store, worktree_ids[2], cx)
|
||||
});
|
||||
|
||||
assert!(!can_trust_0, "worktree 0 should still be restricted");
|
||||
assert!(can_trust_1, "worktree 1 should be trusted");
|
||||
assert!(!can_trust_2, "worktree 2 should still be restricted");
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_two_directory_worktrees_separate_trust(cx: &mut TestAppContext) {
|
||||
init_test(cx);
|
||||
|
||||
let fs = FakeFs::new(cx.executor());
|
||||
fs.insert_tree(
|
||||
path!("/projects"),
|
||||
json!({
|
||||
"project_a": { "main.rs": "fn main() {}" },
|
||||
"project_b": { "lib.rs": "pub fn lib() {}" }
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
|
||||
let project = Project::test(
|
||||
fs,
|
||||
[
|
||||
path!("/projects/project_a").as_ref(),
|
||||
path!("/projects/project_b").as_ref(),
|
||||
],
|
||||
cx,
|
||||
)
|
||||
.await;
|
||||
let worktree_store = project.read_with(cx, |project, _| project.worktree_store());
|
||||
let worktree_ids: Vec<_> = worktree_store.read_with(cx, |store, cx| {
|
||||
store
|
||||
.worktrees()
|
||||
.map(|worktree| {
|
||||
let worktree = worktree.read(cx);
|
||||
assert!(!worktree.is_single_file());
|
||||
worktree.id()
|
||||
})
|
||||
.collect()
|
||||
});
|
||||
assert_eq!(worktree_ids.len(), 2);
|
||||
|
||||
let trusted_worktrees = init_trust_global(worktree_store.clone(), cx);
|
||||
|
||||
let can_trust_a = trusted_worktrees.update(cx, |store, cx| {
|
||||
store.can_trust(&worktree_store, worktree_ids[0], cx)
|
||||
});
|
||||
let can_trust_b = trusted_worktrees.update(cx, |store, cx| {
|
||||
store.can_trust(&worktree_store, worktree_ids[1], cx)
|
||||
});
|
||||
assert!(!can_trust_a, "project_a should be restricted initially");
|
||||
assert!(!can_trust_b, "project_b should be restricted initially");
|
||||
|
||||
trusted_worktrees.update(cx, |store, cx| {
|
||||
store.trust(
|
||||
&worktree_store,
|
||||
HashSet::from_iter([PathTrust::Worktree(worktree_ids[0])]),
|
||||
cx,
|
||||
);
|
||||
});
|
||||
|
||||
let can_trust_a = trusted_worktrees.update(cx, |store, cx| {
|
||||
store.can_trust(&worktree_store, worktree_ids[0], cx)
|
||||
});
|
||||
let can_trust_b = trusted_worktrees.update(cx, |store, cx| {
|
||||
store.can_trust(&worktree_store, worktree_ids[1], cx)
|
||||
});
|
||||
assert!(can_trust_a, "project_a should be trusted after trust()");
|
||||
assert!(!can_trust_b, "project_b should still be restricted");
|
||||
|
||||
trusted_worktrees.update(cx, |store, cx| {
|
||||
store.trust(
|
||||
&worktree_store,
|
||||
HashSet::from_iter([PathTrust::Worktree(worktree_ids[1])]),
|
||||
cx,
|
||||
);
|
||||
});
|
||||
|
||||
let can_trust_a = trusted_worktrees.update(cx, |store, cx| {
|
||||
store.can_trust(&worktree_store, worktree_ids[0], cx)
|
||||
});
|
||||
let can_trust_b = trusted_worktrees.update(cx, |store, cx| {
|
||||
store.can_trust(&worktree_store, worktree_ids[1], cx)
|
||||
});
|
||||
assert!(can_trust_a, "project_a should remain trusted");
|
||||
assert!(can_trust_b, "project_b should now be trusted");
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_directory_worktree_trust_enables_single_file(cx: &mut TestAppContext) {
|
||||
init_test(cx);
|
||||
|
||||
let fs = FakeFs::new(cx.executor());
|
||||
fs.insert_tree(
|
||||
path!("/"),
|
||||
json!({
|
||||
"project": { "main.rs": "fn main() {}" },
|
||||
"standalone.rs": "fn standalone() {}"
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
|
||||
let project = Project::test(
|
||||
fs,
|
||||
[path!("/project").as_ref(), path!("/standalone.rs").as_ref()],
|
||||
cx,
|
||||
)
|
||||
.await;
|
||||
let worktree_store = project.read_with(cx, |project, _| project.worktree_store());
|
||||
let (dir_worktree_id, file_worktree_id) = worktree_store.read_with(cx, |store, cx| {
|
||||
let worktrees: Vec<_> = store.worktrees().collect();
|
||||
assert_eq!(worktrees.len(), 2);
|
||||
let (dir_worktree, file_worktree) = if worktrees[0].read(cx).is_single_file() {
|
||||
(&worktrees[1], &worktrees[0])
|
||||
} else {
|
||||
(&worktrees[0], &worktrees[1])
|
||||
};
|
||||
assert!(!dir_worktree.read(cx).is_single_file());
|
||||
assert!(file_worktree.read(cx).is_single_file());
|
||||
(dir_worktree.read(cx).id(), file_worktree.read(cx).id())
|
||||
});
|
||||
|
||||
let trusted_worktrees = init_trust_global(worktree_store.clone(), cx);
|
||||
|
||||
let can_trust_file = trusted_worktrees.update(cx, |store, cx| {
|
||||
store.can_trust(&worktree_store, file_worktree_id, cx)
|
||||
});
|
||||
assert!(
|
||||
!can_trust_file,
|
||||
"single-file worktree should be restricted initially"
|
||||
);
|
||||
|
||||
let can_trust_directory = trusted_worktrees.update(cx, |store, cx| {
|
||||
store.can_trust(&worktree_store, dir_worktree_id, cx)
|
||||
});
|
||||
assert!(
|
||||
!can_trust_directory,
|
||||
"directory worktree should be restricted initially"
|
||||
);
|
||||
|
||||
trusted_worktrees.update(cx, |store, cx| {
|
||||
store.trust(
|
||||
&worktree_store,
|
||||
HashSet::from_iter([PathTrust::Worktree(dir_worktree_id)]),
|
||||
cx,
|
||||
);
|
||||
});
|
||||
|
||||
let can_trust_dir = trusted_worktrees.update(cx, |store, cx| {
|
||||
store.can_trust(&worktree_store, dir_worktree_id, cx)
|
||||
});
|
||||
let can_trust_file_after = trusted_worktrees.update(cx, |store, cx| {
|
||||
store.can_trust(&worktree_store, file_worktree_id, cx)
|
||||
});
|
||||
assert!(can_trust_dir, "directory worktree should be trusted");
|
||||
assert!(
|
||||
can_trust_file_after,
|
||||
"single-file worktree should be trusted after directory worktree trust"
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_parent_path_trust_enables_single_file(cx: &mut TestAppContext) {
|
||||
init_test(cx);
|
||||
|
||||
let fs = FakeFs::new(cx.executor());
|
||||
fs.insert_tree(
|
||||
path!("/"),
|
||||
json!({
|
||||
"project": { "main.rs": "fn main() {}" },
|
||||
"standalone.rs": "fn standalone() {}"
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
|
||||
let project = Project::test(
|
||||
fs,
|
||||
[path!("/project").as_ref(), path!("/standalone.rs").as_ref()],
|
||||
cx,
|
||||
)
|
||||
.await;
|
||||
let worktree_store = project.read_with(cx, |project, _| project.worktree_store());
|
||||
let (dir_worktree_id, file_worktree_id) = worktree_store.read_with(cx, |store, cx| {
|
||||
let worktrees: Vec<_> = store.worktrees().collect();
|
||||
assert_eq!(worktrees.len(), 2);
|
||||
let (dir_worktree, file_worktree) = if worktrees[0].read(cx).is_single_file() {
|
||||
(&worktrees[1], &worktrees[0])
|
||||
} else {
|
||||
(&worktrees[0], &worktrees[1])
|
||||
};
|
||||
assert!(!dir_worktree.read(cx).is_single_file());
|
||||
assert!(file_worktree.read(cx).is_single_file());
|
||||
(dir_worktree.read(cx).id(), file_worktree.read(cx).id())
|
||||
});
|
||||
|
||||
let trusted_worktrees = init_trust_global(worktree_store.clone(), cx);
|
||||
|
||||
let can_trust_file = trusted_worktrees.update(cx, |store, cx| {
|
||||
store.can_trust(&worktree_store, file_worktree_id, cx)
|
||||
});
|
||||
assert!(
|
||||
!can_trust_file,
|
||||
"single-file worktree should be restricted initially"
|
||||
);
|
||||
|
||||
let can_trust_directory = trusted_worktrees.update(cx, |store, cx| {
|
||||
store.can_trust(&worktree_store, dir_worktree_id, cx)
|
||||
});
|
||||
assert!(
|
||||
!can_trust_directory,
|
||||
"directory worktree should be restricted initially"
|
||||
);
|
||||
|
||||
trusted_worktrees.update(cx, |store, cx| {
|
||||
store.trust(
|
||||
&worktree_store,
|
||||
HashSet::from_iter([PathTrust::AbsPath(PathBuf::from(path!("/project")))]),
|
||||
cx,
|
||||
);
|
||||
});
|
||||
|
||||
let can_trust_dir = trusted_worktrees.update(cx, |store, cx| {
|
||||
store.can_trust(&worktree_store, dir_worktree_id, cx)
|
||||
});
|
||||
let can_trust_file_after = trusted_worktrees.update(cx, |store, cx| {
|
||||
store.can_trust(&worktree_store, file_worktree_id, cx)
|
||||
});
|
||||
assert!(
|
||||
can_trust_dir,
|
||||
"directory worktree should be trusted after its parent is trusted"
|
||||
);
|
||||
assert!(
|
||||
can_trust_file_after,
|
||||
"single-file worktree should be trusted after directory worktree trust via its parent directory trust"
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_abs_path_trust_covers_multiple_worktrees(cx: &mut TestAppContext) {
|
||||
init_test(cx);
|
||||
|
||||
let fs = FakeFs::new(cx.executor());
|
||||
fs.insert_tree(
|
||||
path!("/root"),
|
||||
json!({
|
||||
"project_a": { "main.rs": "fn main() {}" },
|
||||
"project_b": { "lib.rs": "pub fn lib() {}" }
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
|
||||
let project = Project::test(
|
||||
fs,
|
||||
[
|
||||
path!("/root/project_a").as_ref(),
|
||||
path!("/root/project_b").as_ref(),
|
||||
],
|
||||
cx,
|
||||
)
|
||||
.await;
|
||||
let worktree_store = project.read_with(cx, |project, _| project.worktree_store());
|
||||
let worktree_ids: Vec<_> = worktree_store.read_with(cx, |store, cx| {
|
||||
store
|
||||
.worktrees()
|
||||
.map(|worktree| worktree.read(cx).id())
|
||||
.collect()
|
||||
});
|
||||
assert_eq!(worktree_ids.len(), 2);
|
||||
|
||||
let trusted_worktrees = init_trust_global(worktree_store.clone(), cx);
|
||||
|
||||
for &worktree_id in &worktree_ids {
|
||||
let can_trust = trusted_worktrees.update(cx, |store, cx| {
|
||||
store.can_trust(&worktree_store, worktree_id, cx)
|
||||
});
|
||||
assert!(!can_trust, "worktree should be restricted initially");
|
||||
}
|
||||
|
||||
trusted_worktrees.update(cx, |store, cx| {
|
||||
store.trust(
|
||||
&worktree_store,
|
||||
HashSet::from_iter([PathTrust::AbsPath(PathBuf::from(path!("/root")))]),
|
||||
cx,
|
||||
);
|
||||
});
|
||||
|
||||
for &worktree_id in &worktree_ids {
|
||||
let can_trust = trusted_worktrees.update(cx, |store, cx| {
|
||||
store.can_trust(&worktree_store, worktree_id, cx)
|
||||
});
|
||||
assert!(
|
||||
can_trust,
|
||||
"worktree should be trusted after parent path trust"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_auto_trust_all(cx: &mut TestAppContext) {
|
||||
init_test(cx);
|
||||
|
||||
let fs = FakeFs::new(cx.executor());
|
||||
fs.insert_tree(
|
||||
path!("/"),
|
||||
json!({
|
||||
"project_a": { "main.rs": "fn main() {}" },
|
||||
"project_b": { "lib.rs": "pub fn lib() {}" },
|
||||
"single.rs": "fn single() {}"
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
|
||||
let project = Project::test(
|
||||
fs,
|
||||
[
|
||||
path!("/project_a").as_ref(),
|
||||
path!("/project_b").as_ref(),
|
||||
path!("/single.rs").as_ref(),
|
||||
],
|
||||
cx,
|
||||
)
|
||||
.await;
|
||||
let worktree_store = project.read_with(cx, |project, _| project.worktree_store());
|
||||
let worktree_ids: Vec<_> = worktree_store.read_with(cx, |store, cx| {
|
||||
store
|
||||
.worktrees()
|
||||
.map(|worktree| worktree.read(cx).id())
|
||||
.collect()
|
||||
});
|
||||
assert_eq!(worktree_ids.len(), 3);
|
||||
|
||||
let trusted_worktrees = init_trust_global(worktree_store.clone(), cx);
|
||||
|
||||
let events: Rc<RefCell<Vec<TrustedWorktreesEvent>>> = Rc::default();
|
||||
cx.update({
|
||||
let events = events.clone();
|
||||
|cx| {
|
||||
cx.subscribe(&trusted_worktrees, move |_, event, _| {
|
||||
events.borrow_mut().push(match event {
|
||||
TrustedWorktreesEvent::Trusted(host, paths) => {
|
||||
TrustedWorktreesEvent::Trusted(host.clone(), paths.clone())
|
||||
}
|
||||
TrustedWorktreesEvent::Restricted(host, paths) => {
|
||||
TrustedWorktreesEvent::Restricted(host.clone(), paths.clone())
|
||||
}
|
||||
});
|
||||
})
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
|
||||
for &worktree_id in &worktree_ids {
|
||||
let can_trust = trusted_worktrees.update(cx, |store, cx| {
|
||||
store.can_trust(&worktree_store, worktree_id, cx)
|
||||
});
|
||||
assert!(!can_trust, "worktree should be restricted initially");
|
||||
}
|
||||
|
||||
let has_restricted = trusted_worktrees.read_with(cx, |store, cx| {
|
||||
store.has_restricted_worktrees(&worktree_store, cx)
|
||||
});
|
||||
assert!(has_restricted, "should have restricted worktrees");
|
||||
|
||||
events.borrow_mut().clear();
|
||||
|
||||
trusted_worktrees.update(cx, |store, cx| {
|
||||
store.auto_trust_all(cx);
|
||||
});
|
||||
|
||||
for &worktree_id in &worktree_ids {
|
||||
let can_trust = trusted_worktrees.update(cx, |store, cx| {
|
||||
store.can_trust(&worktree_store, worktree_id, cx)
|
||||
});
|
||||
assert!(
|
||||
can_trust,
|
||||
"worktree {worktree_id:?} should be trusted after auto_trust_all"
|
||||
);
|
||||
}
|
||||
|
||||
let has_restricted_after = trusted_worktrees.read_with(cx, |store, cx| {
|
||||
store.has_restricted_worktrees(&worktree_store, cx)
|
||||
});
|
||||
assert!(
|
||||
!has_restricted_after,
|
||||
"should have no restricted worktrees after auto_trust_all"
|
||||
);
|
||||
|
||||
let trusted_event_count = events
|
||||
.borrow()
|
||||
.iter()
|
||||
.filter(|e| matches!(e, TrustedWorktreesEvent::Trusted(..)))
|
||||
.count();
|
||||
assert!(
|
||||
trusted_event_count > 0,
|
||||
"should have emitted Trusted events"
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_trust_restrict_trust_cycle(cx: &mut TestAppContext) {
|
||||
init_test(cx);
|
||||
|
||||
let fs = FakeFs::new(cx.executor());
|
||||
fs.insert_tree(path!("/root"), json!({ "main.rs": "fn main() {}" }))
|
||||
.await;
|
||||
|
||||
let project = Project::test(fs, [path!("/root").as_ref()], cx).await;
|
||||
let worktree_store = project.read_with(cx, |project, _| project.worktree_store());
|
||||
let worktree_id = worktree_store.read_with(cx, |store, cx| {
|
||||
store.worktrees().next().unwrap().read(cx).id()
|
||||
});
|
||||
|
||||
let trusted_worktrees = init_trust_global(worktree_store.clone(), cx);
|
||||
|
||||
let events: Rc<RefCell<Vec<TrustedWorktreesEvent>>> = Rc::default();
|
||||
cx.update({
|
||||
let events = events.clone();
|
||||
|cx| {
|
||||
cx.subscribe(&trusted_worktrees, move |_, event, _| {
|
||||
events.borrow_mut().push(match event {
|
||||
TrustedWorktreesEvent::Trusted(host, paths) => {
|
||||
TrustedWorktreesEvent::Trusted(host.clone(), paths.clone())
|
||||
}
|
||||
TrustedWorktreesEvent::Restricted(host, paths) => {
|
||||
TrustedWorktreesEvent::Restricted(host.clone(), paths.clone())
|
||||
}
|
||||
});
|
||||
})
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
|
||||
let can_trust = trusted_worktrees.update(cx, |store, cx| {
|
||||
store.can_trust(&worktree_store, worktree_id, cx)
|
||||
});
|
||||
assert!(!can_trust, "should be restricted initially");
|
||||
assert_eq!(events.borrow().len(), 1);
|
||||
events.borrow_mut().clear();
|
||||
|
||||
trusted_worktrees.update(cx, |store, cx| {
|
||||
store.trust(
|
||||
&worktree_store,
|
||||
HashSet::from_iter([PathTrust::Worktree(worktree_id)]),
|
||||
cx,
|
||||
);
|
||||
});
|
||||
let can_trust = trusted_worktrees.update(cx, |store, cx| {
|
||||
store.can_trust(&worktree_store, worktree_id, cx)
|
||||
});
|
||||
assert!(can_trust, "should be trusted after trust()");
|
||||
assert_eq!(events.borrow().len(), 1);
|
||||
assert!(matches!(
|
||||
&events.borrow()[0],
|
||||
TrustedWorktreesEvent::Trusted(..)
|
||||
));
|
||||
events.borrow_mut().clear();
|
||||
|
||||
trusted_worktrees.update(cx, |store, cx| {
|
||||
store.restrict(
|
||||
worktree_store.downgrade(),
|
||||
HashSet::from_iter([PathTrust::Worktree(worktree_id)]),
|
||||
cx,
|
||||
);
|
||||
});
|
||||
let can_trust = trusted_worktrees.update(cx, |store, cx| {
|
||||
store.can_trust(&worktree_store, worktree_id, cx)
|
||||
});
|
||||
assert!(!can_trust, "should be restricted after restrict()");
|
||||
assert_eq!(events.borrow().len(), 1);
|
||||
assert!(matches!(
|
||||
&events.borrow()[0],
|
||||
TrustedWorktreesEvent::Restricted(..)
|
||||
));
|
||||
|
||||
let has_restricted = trusted_worktrees.read_with(cx, |store, cx| {
|
||||
store.has_restricted_worktrees(&worktree_store, cx)
|
||||
});
|
||||
assert!(has_restricted);
|
||||
events.borrow_mut().clear();
|
||||
|
||||
trusted_worktrees.update(cx, |store, cx| {
|
||||
store.trust(
|
||||
&worktree_store,
|
||||
HashSet::from_iter([PathTrust::Worktree(worktree_id)]),
|
||||
cx,
|
||||
);
|
||||
});
|
||||
let can_trust = trusted_worktrees.update(cx, |store, cx| {
|
||||
store.can_trust(&worktree_store, worktree_id, cx)
|
||||
});
|
||||
assert!(can_trust, "should be trusted again after second trust()");
|
||||
assert_eq!(events.borrow().len(), 1);
|
||||
assert!(matches!(
|
||||
&events.borrow()[0],
|
||||
TrustedWorktreesEvent::Trusted(..)
|
||||
));
|
||||
|
||||
let has_restricted = trusted_worktrees.read_with(cx, |store, cx| {
|
||||
store.has_restricted_worktrees(&worktree_store, cx)
|
||||
});
|
||||
assert!(!has_restricted);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_multi_host_trust_isolation(cx: &mut TestAppContext) {
|
||||
init_test(cx);
|
||||
|
||||
let fs = FakeFs::new(cx.executor());
|
||||
fs.insert_tree(
|
||||
path!("/"),
|
||||
json!({
|
||||
"local_project": { "main.rs": "fn main() {}" },
|
||||
"remote_project": { "lib.rs": "pub fn lib() {}" }
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
|
||||
let project = Project::test(
|
||||
fs,
|
||||
[
|
||||
path!("/local_project").as_ref(),
|
||||
path!("/remote_project").as_ref(),
|
||||
],
|
||||
cx,
|
||||
)
|
||||
.await;
|
||||
let worktree_store = project.read_with(cx, |project, _| project.worktree_store());
|
||||
let worktree_ids: Vec<_> = worktree_store.read_with(cx, |store, cx| {
|
||||
store
|
||||
.worktrees()
|
||||
.map(|worktree| worktree.read(cx).id())
|
||||
.collect()
|
||||
});
|
||||
assert_eq!(worktree_ids.len(), 2);
|
||||
let local_worktree = worktree_ids[0];
|
||||
let _remote_worktree = worktree_ids[1];
|
||||
|
||||
let trusted_worktrees = init_trust_global(worktree_store.clone(), cx);
|
||||
|
||||
let can_trust_local = trusted_worktrees.update(cx, |store, cx| {
|
||||
store.can_trust(&worktree_store, local_worktree, cx)
|
||||
});
|
||||
assert!(!can_trust_local, "local worktree restricted on host_a");
|
||||
|
||||
trusted_worktrees.update(cx, |store, cx| {
|
||||
store.trust(
|
||||
&worktree_store,
|
||||
HashSet::from_iter([PathTrust::Worktree(local_worktree)]),
|
||||
cx,
|
||||
);
|
||||
});
|
||||
|
||||
let can_trust_local_after = trusted_worktrees.update(cx, |store, cx| {
|
||||
store.can_trust(&worktree_store, local_worktree, cx)
|
||||
});
|
||||
assert!(
|
||||
can_trust_local_after,
|
||||
"local worktree should be trusted on local host"
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_invisible_worktree_stores_do_not_affect_trust(cx: &mut TestAppContext) {
|
||||
init_test(cx);
|
||||
|
||||
let fs = FakeFs::new(cx.executor());
|
||||
fs.insert_tree(
|
||||
path!("/"),
|
||||
json!({
|
||||
"visible": { "main.rs": "fn main() {}" },
|
||||
"other": { "a.rs": "fn other() {}" },
|
||||
"invisible": { "b.rs": "fn invisible() {}" }
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
|
||||
let project = Project::test(fs, [path!("/visible").as_ref()], cx).await;
|
||||
let worktree_store = project.read_with(cx, |project, _| project.worktree_store());
|
||||
let visible_worktree_id = worktree_store.read_with(cx, |store, cx| {
|
||||
store
|
||||
.worktrees()
|
||||
.find(|worktree| worktree.read(cx).root_dir().unwrap().ends_with("visible"))
|
||||
.expect("visible worktree")
|
||||
.read(cx)
|
||||
.id()
|
||||
});
|
||||
let trusted_worktrees = init_trust_global(worktree_store.clone(), cx);
|
||||
|
||||
let events: Rc<RefCell<Vec<TrustedWorktreesEvent>>> = Rc::default();
|
||||
cx.update({
|
||||
let events = events.clone();
|
||||
|cx| {
|
||||
cx.subscribe(&trusted_worktrees, move |_, event, _| {
|
||||
events.borrow_mut().push(match event {
|
||||
TrustedWorktreesEvent::Trusted(host, paths) => {
|
||||
TrustedWorktreesEvent::Trusted(host.clone(), paths.clone())
|
||||
}
|
||||
TrustedWorktreesEvent::Restricted(host, paths) => {
|
||||
TrustedWorktreesEvent::Restricted(host.clone(), paths.clone())
|
||||
}
|
||||
});
|
||||
})
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
|
||||
assert!(
|
||||
!trusted_worktrees.update(cx, |store, cx| {
|
||||
store.can_trust(&worktree_store, visible_worktree_id, cx)
|
||||
}),
|
||||
"visible worktree should be restricted initially"
|
||||
);
|
||||
assert_eq!(
|
||||
HashSet::from_iter([(visible_worktree_id)]),
|
||||
trusted_worktrees.read_with(cx, |store, _| {
|
||||
store.restricted_worktrees_for_store(&worktree_store)
|
||||
}),
|
||||
"only visible worktree should be restricted",
|
||||
);
|
||||
|
||||
let (new_visible_worktree, new_invisible_worktree) =
|
||||
worktree_store.update(cx, |worktree_store, cx| {
|
||||
let new_visible_worktree = worktree_store.create_worktree("/other", true, cx);
|
||||
let new_invisible_worktree = worktree_store.create_worktree("/invisible", false, cx);
|
||||
(new_visible_worktree, new_invisible_worktree)
|
||||
});
|
||||
let (new_visible_worktree, new_invisible_worktree) = (
|
||||
new_visible_worktree.await.unwrap(),
|
||||
new_invisible_worktree.await.unwrap(),
|
||||
);
|
||||
|
||||
let new_visible_worktree_id =
|
||||
new_visible_worktree.read_with(cx, |new_visible_worktree, _| new_visible_worktree.id());
|
||||
assert!(
|
||||
!trusted_worktrees.update(cx, |store, cx| {
|
||||
store.can_trust(&worktree_store, new_visible_worktree_id, cx)
|
||||
}),
|
||||
"new visible worktree should be restricted initially",
|
||||
);
|
||||
assert!(
|
||||
trusted_worktrees.update(cx, |store, cx| {
|
||||
store.can_trust(&worktree_store, new_invisible_worktree.read(cx).id(), cx)
|
||||
}),
|
||||
"invisible worktree should be skipped",
|
||||
);
|
||||
assert_eq!(
|
||||
HashSet::from_iter([visible_worktree_id, new_visible_worktree_id]),
|
||||
trusted_worktrees.read_with(cx, |store, _| {
|
||||
store.restricted_worktrees_for_store(&worktree_store)
|
||||
}),
|
||||
"only visible worktrees should be restricted"
|
||||
);
|
||||
}
|
||||
37
crates/project/tests/integration/yarn.rs
Normal file
37
crates/project/tests/integration/yarn.rs
Normal file
@@ -0,0 +1,37 @@
|
||||
use project::yarn::*;
|
||||
use std::path::Path;
|
||||
|
||||
#[test]
|
||||
fn test_resolve_virtual() {
|
||||
let test_cases = vec![
|
||||
(
|
||||
"/path/to/some/folder/__virtual__/a0b1c2d3/0/subpath/to/file.dat",
|
||||
Some(Path::new("/path/to/some/folder/subpath/to/file.dat")),
|
||||
),
|
||||
(
|
||||
"/path/to/some/folder/__virtual__/e4f5a0b1/0/subpath/to/file.dat",
|
||||
Some(Path::new("/path/to/some/folder/subpath/to/file.dat")),
|
||||
),
|
||||
(
|
||||
"/path/to/some/folder/__virtual__/a0b1c2d3/1/subpath/to/file.dat",
|
||||
Some(Path::new("/path/to/some/subpath/to/file.dat")),
|
||||
),
|
||||
(
|
||||
"/path/to/some/folder/__virtual__/a0b1c2d3/3/subpath/to/file.dat",
|
||||
Some(Path::new("/path/subpath/to/file.dat")),
|
||||
),
|
||||
("/path/to/nonvirtual/", None),
|
||||
("/path/to/malformed/__virtual__", None),
|
||||
("/path/to/malformed/__virtual__/a0b1c2d3", None),
|
||||
(
|
||||
"/path/to/malformed/__virtual__/a0b1c2d3/this-should-be-a-number",
|
||||
None,
|
||||
),
|
||||
];
|
||||
|
||||
for (input, expected) in test_cases {
|
||||
let input_path = Path::new(input);
|
||||
let resolved_path = resolve_virtual(input_path);
|
||||
assert_eq!(resolved_path.as_deref(), expected);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user