logiguard fork v3: full patch set on verified 8c74db0 tree

Includes prior-session patches (carry forward so the app compiles):
  - crates/gpui/build.rs: cross-compile manifest fix
  - crates/gpui/src/platform.rs: PlatformWindow::activate_with_token trait method
  - crates/gpui/src/window.rs: Window::activate_with_token public API
  - crates/gpui_linux/src/linux/wayland/window.rs: WaylandWindow::activate_with_token + activate() keyboard-serial fix

Plus the focus-serial fix:
  - serial.rs: SerialKind::KeyboardEnter
  - client.rs: store wl_keyboard.enter serial; latest_serial_of()

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Mohamad Khani
2026-07-14 01:52:12 +03:30
commit b9819977a5
3984 changed files with 1487015 additions and 0 deletions

View 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);
});
}

View 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]);
}
}

View 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);
}
}

File diff suppressed because it is too large Load Diff

View 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);
}
}

View 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");
}

View 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"
);
}

File diff suppressed because it is too large Load Diff

View 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());
}

View 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());
}

View 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"
));
}

View 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);
}
}

View 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
));
}

File diff suppressed because it is too large Load Diff

View 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]);
}

View 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);
}

View 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())]
)
);
}

View 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()
}

View 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"
);
}

View 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);
}
}