use super::*;
use expect_test::expect;
use serde_json::json;
use std::time::Duration;
const CREATED: u32 = 1;
const CHANGED: u32 = 2;
const DELETED: u32 = 3;
#[tokio::test]
async fn index_ready_fires_after_scan_and_workspace_symbols_work() {
let mut s = TestServer::with_fixture("psr4-mini").await;
s.wait_for_index_ready().await;
let out = s.snapshot_workspace_symbols("User").await;
expect![[r#"
Class User @ src/Model/User.php:4
Property $users @ src/Service/Registry.php:9"#]]
.assert_eq(&out);
}
#[tokio::test]
async fn workspace_scan_emits_work_done_progress_reports() {
let mut s = TestServer::with_fixture("psr4-mini").await;
let notifications = s.client().collect_until_index_ready().await;
let has_report = notifications.iter().any(|n| {
n["method"].as_str() == Some("$/progress")
&& n["params"]["value"]["kind"].as_str() == Some("report")
&& n["params"]["value"]["percentage"].as_u64().is_some()
});
assert!(
has_report,
"expected at least one $/progress notification with kind='report' and 'percentage';\n\
got: {notifications:#?}"
);
}
#[serial_test::serial]
#[tokio::test]
async fn created_file_becomes_discoverable_via_workspace_symbols() {
let mut server = TestServer::with_fixture("psr4-mini").await;
server.wait_for_index_ready().await;
let pre = server.snapshot_workspace_symbols("Widget").await;
expect![[r#"<no symbols>"#]].assert_eq(&pre);
server.write_file(
"src/Service/Widget.php",
"<?php\nnamespace App\\Service;\n\nclass Widget {}\n",
);
let uri = server.uri("src/Service/Widget.php");
server.did_change_watched_files(vec![(uri, CREATED)]).await;
server
.wait_until_symbol_present("Widget", Duration::from_secs(3))
.await;
let post = server.snapshot_workspace_symbols("Widget").await;
expect![[r#"Class Widget @ src/Service/Widget.php:3"#]].assert_eq(&post);
}
#[serial_test::serial]
#[tokio::test]
async fn created_file_in_new_subdirectory_is_indexed() {
let mut server = TestServer::with_fixture("psr4-mini").await;
server.wait_for_index_ready().await;
server.write_file(
"src/Queue/Job.php",
"<?php\nnamespace App\\Queue;\n\nclass Job {}\n",
);
let uri = server.uri("src/Queue/Job.php");
server.did_change_watched_files(vec![(uri, CREATED)]).await;
server
.wait_until_symbol_present("Job", Duration::from_secs(3))
.await;
let out = server.snapshot_workspace_symbols("Job").await;
expect![[r#"Class Job @ src/Queue/Job.php:3"#]].assert_eq(&out);
}
#[serial_test::serial]
#[tokio::test]
async fn changed_file_updates_workspace_index() {
let mut server = TestServer::with_fixture("psr4-mini").await;
server.wait_for_index_ready().await;
let pre = server.snapshot_workspace_symbols("Greeter").await;
expect![[r#"Class Greeter @ src/Service/Greeter.php:6"#]].assert_eq(&pre);
server.write_file(
"src/Service/Greeter.php",
"<?php\nnamespace App\\Service;\n\nclass GreeterUpdated {}\n",
);
let uri = server.uri("src/Service/Greeter.php");
server.did_change_watched_files(vec![(uri, CHANGED)]).await;
server
.wait_until_symbol_present("GreeterUpdated", Duration::from_secs(3))
.await;
let post = server.snapshot_workspace_symbols("GreeterUpdated").await;
expect![[r#"Class GreeterUpdated @ src/Service/Greeter.php:3"#]].assert_eq(&post);
let gone = server.snapshot_workspace_symbols("Greeter").await;
expect![[r#"Class GreeterUpdated @ src/Service/Greeter.php:3"#]].assert_eq(&gone);
}
#[serial_test::serial]
#[tokio::test]
async fn deleted_file_symbols_removed_from_index() {
let mut server = TestServer::with_fixture("psr4-mini").await;
server.wait_for_index_ready().await;
let pre = server.snapshot_workspace_symbols("Registry").await;
expect![[r#"Class Registry @ src/Service/Registry.php:6"#]].assert_eq(&pre);
server.remove_file("src/Service/Registry.php");
let uri = server.uri("src/Service/Registry.php");
server.did_change_watched_files(vec![(uri, DELETED)]).await;
server
.wait_until_symbol_absent("Registry", Duration::from_secs(3))
.await;
let post = server.snapshot_workspace_symbols("Registry").await;
expect![[r#"<no symbols>"#]].assert_eq(&post);
}
#[serial_test::serial]
#[tokio::test]
async fn exclude_paths_honored_by_workspace_scan() {
let mut server = TestServer::with_fixture_and_options(
"psr4-mini",
json!({
"diagnostics": { "enabled": true },
"excludePaths": ["src/Service/*"],
}),
)
.await;
server.wait_for_index_ready().await;
let resp = server.workspace_symbols("Greeter").await;
let symbols = resp["result"].as_array().cloned().unwrap_or_default();
assert!(
!symbols.iter().any(|s| {
s["location"]["uri"]
.as_str()
.map(|u| u.ends_with("src/Service/Greeter.php"))
.unwrap_or(false)
}),
"Greeter is in excluded src/Service — must not be indexed, got: {symbols:?}"
);
let resp = server.workspace_symbols("User").await;
let symbols = resp["result"].as_array().cloned().unwrap_or_default();
assert!(
symbols.iter().any(|s| {
s["location"]["uri"]
.as_str()
.map(|u| u.ends_with("src/Model/User.php"))
.unwrap_or(false)
}),
"User is NOT excluded — must still appear in workspace symbols, got: {symbols:?}"
);
}
#[serial_test::serial]
#[tokio::test]
async fn php_lsp_json_exclude_paths_honored() {
let manifest_dir = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"));
let source = manifest_dir.join("tests/fixtures/psr4-mini");
let tmp = tempfile::tempdir().expect("create TempDir");
fn copy_dir(src: &std::path::Path, dst: &std::path::Path) -> std::io::Result<()> {
std::fs::create_dir_all(dst)?;
for e in std::fs::read_dir(src)? {
let e = e?;
let to = dst.join(e.file_name());
if e.file_type()?.is_dir() {
copy_dir(&e.path(), &to)?;
} else {
std::fs::copy(e.path(), to)?;
}
}
Ok(())
}
copy_dir(&source, tmp.path()).unwrap();
std::fs::write(
tmp.path().join(".php-lsp.json"),
r#"{"excludePaths": ["src/Service/*"]}"#,
)
.unwrap();
let mut server = TestServer::with_root(tmp.path()).await;
server.wait_for_index_ready().await;
let resp = server.workspace_symbols("Greeter").await;
let symbols = resp["result"].as_array().cloned().unwrap_or_default();
assert!(
!symbols.iter().any(|s| s["location"]["uri"]
.as_str()
.map(|u| u.ends_with("src/Service/Greeter.php"))
.unwrap_or(false)),
"Greeter is excluded via .php-lsp.json — must not be indexed, got: {symbols:?}"
);
let resp = server.workspace_symbols("User").await;
let symbols = resp["result"].as_array().cloned().unwrap_or_default();
assert!(
symbols.iter().any(|s| s["location"]["uri"]
.as_str()
.map(|u| u.ends_with("src/Model/User.php"))
.unwrap_or(false)),
"User is not excluded — must still be indexed, got: {symbols:?}"
);
}
#[serial_test::serial]
#[tokio::test]
async fn hidden_directories_are_excluded_from_scan() {
let manifest_dir = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"));
let source = manifest_dir.join("tests/fixtures/psr4-mini");
let tmp = tempfile::tempdir().expect("create TempDir");
fn copy_dir(src: &std::path::Path, dst: &std::path::Path) -> std::io::Result<()> {
std::fs::create_dir_all(dst)?;
for e in std::fs::read_dir(src)? {
let e = e?;
let to = dst.join(e.file_name());
if e.file_type()?.is_dir() {
copy_dir(&e.path(), &to)?;
} else {
std::fs::copy(e.path(), to)?;
}
}
Ok(())
}
copy_dir(&source, tmp.path()).unwrap();
let hidden_dir = tmp.path().join(".git/objects");
std::fs::create_dir_all(&hidden_dir).unwrap();
std::fs::write(
hidden_dir.join("ClassInGit.php"),
"<?php\nclass ClassInGit {}\n",
)
.unwrap();
let vscode_dir = tmp.path().join(".vscode");
std::fs::create_dir_all(&vscode_dir).unwrap();
std::fs::write(
vscode_dir.join("settings.php"),
"<?php\nclass VscodeSetting {}\n",
)
.unwrap();
let mut server = TestServer::with_root(tmp.path()).await;
server.wait_for_index_ready().await;
expect!["Class Greeter @ src/Service/Greeter.php:6"]
.assert_eq(&server.snapshot_workspace_symbols("Greeter").await);
expect!["<no symbols>"].assert_eq(&server.snapshot_workspace_symbols("ClassInGit").await);
expect!["<no symbols>"].assert_eq(&server.snapshot_workspace_symbols("VscodeSetting").await);
}
#[serial_test::serial]
#[tokio::test]
async fn vendor_directory_indexed_by_default() {
let tmp = tempfile::tempdir().expect("create TempDir");
let mut server = TestServer::with_root(tmp.path()).await;
server.write_file(
"vendor/symfony/http-kernel/HttpKernel.php",
"<?php\nnamespace Symfony\\Component\\HttpKernel;\n\nclass HttpKernel {}\n",
);
server.wait_for_index_ready().await;
let out = server.snapshot_workspace_symbols("HttpKernel").await;
expect![[r#"Class HttpKernel @ vendor/symfony/http-kernel/HttpKernel.php:3"#]]
.assert_eq(&out);
}
#[serial_test::serial]
#[tokio::test]
async fn vendor_directory_excluded_with_index_vendor_false() {
let tmp = tempfile::tempdir().expect("create TempDir");
let mut server =
TestServer::with_root_and_options(tmp.path(), json!({ "indexVendor": false })).await;
server.write_file(
"vendor/symfony/http-kernel/HttpKernel.php",
"<?php\nnamespace Symfony\\Component\\HttpKernel;\n\nclass HttpKernel {}\n",
);
server.wait_for_index_ready().await;
let out = server.snapshot_workspace_symbols("HttpKernel").await;
expect![[r#"<no symbols>"#]].assert_eq(&out);
}
#[serial_test::serial]
#[tokio::test]
async fn vendor_directory_indexed_when_index_vendor_true() {
let tmp = tempfile::tempdir().expect("create TempDir");
let mut server = TestServer::with_root_and_options(
tmp.path(),
json!({
"diagnostics": { "enabled": true },
"indexVendor": true,
}),
)
.await;
server.write_file(
"vendor/symfony/http-kernel/HttpKernel.php",
"<?php\nnamespace Symfony\\Component\\HttpKernel;\n\nclass HttpKernel {}\n",
);
server.wait_for_index_ready().await;
let out = server.snapshot_workspace_symbols("HttpKernel").await;
expect![[r#"Class HttpKernel @ vendor/symfony/http-kernel/HttpKernel.php:3"#]]
.assert_eq(&out);
}
#[serial_test::serial]
#[tokio::test]
async fn vendor_directory_excluded_when_configured() {
let manifest_dir = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"));
let source = manifest_dir.join("tests/fixtures/psr4-mini");
let tmp = tempfile::tempdir().expect("create TempDir");
fn copy_dir(src: &std::path::Path, dst: &std::path::Path) -> std::io::Result<()> {
std::fs::create_dir_all(dst)?;
for e in std::fs::read_dir(src)? {
let e = e?;
let to = dst.join(e.file_name());
if e.file_type()?.is_dir() {
copy_dir(&e.path(), &to)?;
} else {
std::fs::copy(e.path(), to)?;
}
}
Ok(())
}
copy_dir(&source, tmp.path()).unwrap();
let mut server = TestServer::with_fixture_and_options(
"psr4-mini",
json!({
"diagnostics": { "enabled": true },
"excludePaths": ["vendor/"],
}),
)
.await;
server.write_file(
"vendor/symfony/http-kernel/HttpKernel.php",
"<?php\nnamespace Symfony\\Component\\HttpKernel;\n\nclass HttpKernel {}\n",
);
server.wait_for_index_ready().await;
expect!["<no symbols>"].assert_eq(&server.snapshot_workspace_symbols("HttpKernel").await);
}
#[serial_test::serial]
#[tokio::test]
async fn max_indexed_files_cap_is_enforced() {
let tmp = tempfile::tempdir().expect("create TempDir");
let mut server = TestServer::with_root_and_options(
tmp.path(),
json!({
"diagnostics": { "enabled": true },
"maxIndexedFiles": 5,
}),
)
.await;
for i in 0..10 {
server.write_file(
&format!("Class{}.php", i),
&format!("<?php\nclass Class{} {{}}\n", i),
);
}
server.wait_for_index_ready().await;
let mut indexed_count = 0;
for i in 0..10 {
let resp = server.workspace_symbols(&format!("Class{}", i)).await;
if !resp["result"]
.as_array()
.map(|a| a.is_empty())
.unwrap_or(true)
{
indexed_count += 1;
}
}
assert_eq!(
indexed_count, 5,
"expected exactly 5 indexed files with maxIndexedFiles=5, got {}",
indexed_count
);
}
#[serial_test::serial]
#[tokio::test]
async fn custom_max_indexed_files_via_init_options() {
let tmp = tempfile::tempdir().expect("create TempDir");
let mut server = TestServer::with_root_and_options(
tmp.path(),
json!({
"diagnostics": { "enabled": true },
"maxIndexedFiles": 3,
}),
)
.await;
for i in 0..5 {
server.write_file(
&format!("src/Class{}.php", i),
&format!("<?php\nclass Class{} {{}}\n", i),
);
}
server.wait_for_index_ready().await;
let mut indexed = Vec::new();
for i in 0..5 {
let resp = server.workspace_symbols(&format!("Class{}", i)).await;
if !resp["result"]
.as_array()
.map(|a| a.is_empty())
.unwrap_or(true)
{
indexed.push(i);
}
}
assert_eq!(
indexed.len(),
3,
"expected exactly 3 indexed files with maxIndexedFiles=3, got {} (files: {:?})",
indexed.len(),
indexed
);
}
#[serial_test::serial]
#[tokio::test]
async fn deeply_nested_directory_structure_is_indexed() {
let tmp = tempfile::tempdir().expect("create TempDir");
let mut server = TestServer::with_root(tmp.path()).await;
server.write_file(
"src/Level1/Level2/Level3/Level4/Level5/DeepClass.php",
"<?php\nnamespace App\\Level1\\Level2\\Level3\\Level4\\Level5;\n\nclass DeepClass {}\n",
);
server.write_file(
"src/Shallow/ShallowClass.php",
"<?php\nnamespace App\\Shallow;\n\nclass ShallowClass {}\n",
);
server.wait_for_index_ready().await;
expect!["Class DeepClass @ src/Level1/Level2/Level3/Level4/Level5/DeepClass.php:3"]
.assert_eq(&server.snapshot_workspace_symbols("DeepClass").await);
expect!["Class ShallowClass @ src/Shallow/ShallowClass.php:3"]
.assert_eq(&server.snapshot_workspace_symbols("ShallowClass").await);
}
#[serial_test::serial]
#[tokio::test]
async fn multiple_top_level_directories_with_different_patterns() {
let tmp = tempfile::tempdir().expect("create TempDir");
let mut server = TestServer::with_root_and_options(
tmp.path(),
json!({
"diagnostics": { "enabled": true },
"indexVendor": true,
}),
)
.await;
server.write_file(
"packages/api/src/ApiService.php",
"<?php\nnamespace App\\Api;\n\nclass ApiService {}\n",
);
server.write_file(
"packages/web/src/WebService.php",
"<?php\nnamespace App\\Web;\n\nclass WebService {}\n",
);
server.write_file(
"packages/shared/src/SharedUtil.php",
"<?php\nnamespace App\\Shared;\n\nclass SharedUtil {}\n",
);
server.write_file(
"packages/api/vendor/external/External.php",
"<?php\nclass External {}\n",
);
server.wait_for_index_ready().await;
expect!["Class ApiService @ packages/api/src/ApiService.php:3"]
.assert_eq(&server.snapshot_workspace_symbols("ApiService").await);
expect!["Class WebService @ packages/web/src/WebService.php:3"]
.assert_eq(&server.snapshot_workspace_symbols("WebService").await);
expect!["Class SharedUtil @ packages/shared/src/SharedUtil.php:3"]
.assert_eq(&server.snapshot_workspace_symbols("SharedUtil").await);
expect!["Class External @ packages/api/vendor/external/External.php:1"]
.assert_eq(&server.snapshot_workspace_symbols("External").await);
}
#[tokio::test]
async fn exclude_specific_package_in_monorepo_structure() {
let mut server = TestServer::with_fixture_and_options(
"psr4-mini",
json!({
"diagnostics": { "enabled": true },
"excludePaths": ["vendor/", "src/Service/*"],
}),
)
.await;
server.wait_for_index_ready().await;
expect!["<no symbols>"].assert_eq(&server.snapshot_workspace_symbols("Greeter").await);
expect!["Class User @ src/Model/User.php:4"]
.assert_eq(&server.snapshot_workspace_symbols("User").await);
}
#[serial_test::serial]
#[tokio::test]
async fn exclude_paths_substring_match_edge_case() {
let tmp = tempfile::tempdir().expect("create TempDir");
let mut server = TestServer::with_root_and_options(
tmp.path(),
json!({
"diagnostics": { "enabled": true },
"excludePaths": ["src/"],
}),
)
.await;
server.write_file("src/ClassInSrc.php", "<?php\nclass ClassInSrc {}\n");
server.write_file(
"tests/source/ClassInTestSource.php",
"<?php\nclass ClassInTestSource {}\n",
);
server.wait_for_index_ready().await;
let resp = server.workspace_symbols("ClassInSrc").await;
let symbols = resp["result"].as_array().cloned().unwrap_or_default();
assert!(
!symbols.iter().any(|s| {
s["name"]
.as_str()
.map(|n| n == "ClassInSrc")
.unwrap_or(false)
}),
"ClassInSrc in src/ should be excluded"
);
let resp = server.workspace_symbols("ClassInTestSource").await;
let symbols = resp["result"].as_array().cloned().unwrap_or_default();
assert!(
symbols.iter().any(|s| {
s["name"]
.as_str()
.map(|n| n == "ClassInTestSource")
.unwrap_or(false)
}),
"ClassInTestSource in tests/source/ should be indexed (pattern is 'src/', not 'src')"
);
}
#[serial_test::serial]
#[tokio::test]
async fn exclude_paths_does_not_substring_match_intermediate_dirs() {
let tmp = tempfile::tempdir().expect("create TempDir");
let mut server = TestServer::with_root_and_options(
tmp.path(),
json!({
"diagnostics": { "enabled": true },
"excludePaths": ["src/"],
}),
)
.await;
server.write_file(
"test_src/ClassInTestSrc.php",
"<?php\nclass ClassInTestSrc {}\n",
);
server.wait_for_index_ready().await;
expect!["Class ClassInTestSrc @ test_src/ClassInTestSrc.php:1"]
.assert_eq(&server.snapshot_workspace_symbols("ClassInTestSrc").await);
}
#[tokio::test]
async fn empty_workspace_returns_zero_files() {
let tmp = tempfile::tempdir().expect("create TempDir");
let mut server = TestServer::with_root(tmp.path()).await;
server.wait_for_index_ready().await;
expect!["<no symbols>"].assert_eq(&server.snapshot_workspace_symbols("NonExistent").await);
}
#[serial_test::serial]
#[tokio::test]
async fn exclude_all_paths_leaves_nothing_indexed() {
let mut server = TestServer::with_fixture_and_options(
"psr4-mini",
json!({
"diagnostics": { "enabled": true },
"excludePaths": ["src/", "vendor/"],
}),
)
.await;
server.wait_for_index_ready().await;
expect!["<no symbols>"].assert_eq(&server.snapshot_workspace_symbols("Greeter").await);
}
#[serial_test::serial]
#[tokio::test]
async fn exclude_paths_glob_with_double_star() {
let tmp = tempfile::tempdir().expect("create TempDir");
let mut server = TestServer::with_root_and_options(
tmp.path(),
json!({
"diagnostics": { "enabled": false },
"excludePaths": ["**/var/**", "src/**/Generated", "*Test.php"],
}),
)
.await;
server.write_file("var/cache/InVar.php", "<?php\nclass InVar {}\n");
server.write_file("app/var/InNestedVar.php", "<?php\nclass InNestedVar {}\n");
server.write_file(
"src/A/B/Generated/InGenerated.php",
"<?php\nclass InGenerated {}\n",
);
server.write_file("tests/FooTest.php", "<?php\nclass FooTest {}\n");
server.write_file("src/A/Kept.php", "<?php\nclass Kept {}\n");
server.wait_for_index_ready().await;
for name in ["InVar", "InNestedVar", "InGenerated", "FooTest"] {
assert_eq!(
server.snapshot_workspace_symbols(name).await,
"<no symbols>",
"{name} should be excluded"
);
}
expect!["Class Kept @ src/A/Kept.php:1"]
.assert_eq(&server.snapshot_workspace_symbols("Kept").await);
}
#[serial_test::serial]
#[tokio::test]
async fn include_paths_double_star_reaches_into_excluded_dir() {
let tmp = tempfile::tempdir().expect("create TempDir");
let mut server = TestServer::with_root_and_options(
tmp.path(),
json!({
"diagnostics": { "enabled": false },
"excludePaths": ["src/"],
"includePaths": ["**/Model"],
}),
)
.await;
server.write_file("src/Model/User.php", "<?php\nclass User {}\n");
server.write_file("src/Service/Greeter.php", "<?php\nclass Greeter {}\n");
server.wait_for_index_ready().await;
expect!["Class User @ src/Model/User.php:1"]
.assert_eq(&server.snapshot_workspace_symbols("User").await);
expect!["<no symbols>"].assert_eq(&server.snapshot_workspace_symbols("Greeter").await);
}
#[serial_test::serial]
#[tokio::test]
async fn include_paths_override_exclude_paths() {
let mut server = TestServer::with_fixture_and_options(
"yii-demo",
json!({
"diagnostics": { "enabled": false },
"excludePaths": ["vendor/*"],
"includePaths": ["vendor/yiisoft"],
}),
)
.await;
server.wait_for_index_ready().await;
expect!["<no symbols>"].assert_eq(&server.snapshot_workspace_symbols("Fish").await);
expect!["Class Translator @ vendor/yiisoft/Translator.php:4"]
.assert_eq(&server.snapshot_workspace_symbols("Translator").await);
}
#[serial_test::serial]
#[tokio::test]
async fn include_paths_only_affects_matched_entries() {
let mut server = TestServer::with_fixture_and_options(
"psr4-mini",
json!({
"diagnostics": { "enabled": false },
"excludePaths": ["src/Service/*"],
"includePaths": ["Greeter"],
}),
)
.await;
server.wait_for_index_ready().await;
expect!["Class Greeter @ src/Service/Greeter.php:6"]
.assert_eq(&server.snapshot_workspace_symbols("Greeter").await);
expect!["<no symbols>"].assert_eq(&server.snapshot_workspace_symbols("Registry").await);
expect!["Class User @ src/Model/User.php:4"]
.assert_eq(&server.snapshot_workspace_symbols("User").await);
}
#[serial_test::serial]
#[tokio::test]
async fn include_paths_from_php_lsp_json() {
let manifest_dir = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"));
let source = manifest_dir.join("tests/fixtures/psr4-mini");
let tmp = tempfile::tempdir().expect("create TempDir");
fn copy_dir(src: &std::path::Path, dst: &std::path::Path) -> std::io::Result<()> {
std::fs::create_dir_all(dst)?;
for e in std::fs::read_dir(src)? {
let e = e?;
let to = dst.join(e.file_name());
if e.file_type()?.is_dir() {
copy_dir(&e.path(), &to)?;
} else {
std::fs::copy(e.path(), to)?;
}
}
Ok(())
}
copy_dir(&source, tmp.path()).unwrap();
std::fs::write(
tmp.path().join(".php-lsp.json"),
r#"{"excludePaths": ["src/Service/*"], "includePaths": ["Greeter"]}"#,
)
.unwrap();
let mut server = TestServer::with_root(tmp.path()).await;
server.wait_for_index_ready().await;
expect!["Class Greeter @ src/Service/Greeter.php:6"]
.assert_eq(&server.snapshot_workspace_symbols("Greeter").await);
expect!["<no symbols>"].assert_eq(&server.snapshot_workspace_symbols("Registry").await);
}
#[serial_test::serial]
#[tokio::test]
async fn all_scanned_files_appear_in_workspace_index_after_index_ready() {
let tmp = tempfile::tempdir().expect("create TempDir");
let mut server = TestServer::with_root(tmp.path()).await;
let classes = [
("src/Alpha.php", "Alpha"),
("src/Beta.php", "Beta"),
("src/Gamma.php", "Gamma"),
("src/Sub/Delta.php", "Delta"),
("src/Sub/Epsilon.php", "Epsilon"),
];
for (path, name) in &classes {
server.write_file(
path,
&format!("<?php\nnamespace App;\n\nclass {name} {{}}\n"),
);
}
server.wait_for_index_ready().await;
let mut results = Vec::new();
for (_, name) in &classes {
results.push(server.snapshot_workspace_symbols(name).await);
}
expect![[r#"
Class Alpha @ src/Alpha.php:3
Class Beta @ src/Beta.php:3
Class Gamma @ src/Gamma.php:3
Class Delta @ src/Sub/Delta.php:3
Class Epsilon @ src/Sub/Epsilon.php:3"#]]
.assert_eq(&results.join("\n"));
}
#[tokio::test]
async fn workspace_symbols_substring_match() {
let mut s = TestServer::with_fixture("psr4-mini").await;
s.wait_for_index_ready().await;
let out = s.snapshot_workspace_symbols("reeter").await;
expect![[r#"Class Greeter @ src/Service/Greeter.php:6"#]].assert_eq(&out);
}
#[serial_test::serial]
#[tokio::test]
async fn unreadable_file_does_not_truncate_workspace_index() {
let tmp = tempfile::tempdir().unwrap();
let dir = tmp.path().join("src");
std::fs::create_dir_all(&dir).unwrap();
std::fs::write(dir.join("Binary.php"), [0xFF, 0xFE, 0x00]).unwrap();
let big_padding = "// ".to_string() + &"x".repeat(8 * 1024 * 1024);
std::fs::write(
dir.join("BigKlass.php"),
format!("<?php\nnamespace App;\n{big_padding}\nclass BigKlass {{}}\n"),
)
.unwrap();
let mut s = TestServer::with_root(tmp.path()).await;
s.validate_syntax(false);
s.wait_for_index_ready().await;
expect!["Class BigKlass @ src/BigKlass.php:3"]
.assert_eq(&s.snapshot_workspace_symbols("BigKlass").await);
}
#[tokio::test]
async fn non_utf8_files_are_indexed() {
let tmp = tempfile::tempdir().unwrap();
std::fs::write(
tmp.path().join("Legacy.php"),
b"<?php\n// caf\xe9\nclass Legacy {}\n",
)
.unwrap();
let mut s = TestServer::with_root(tmp.path()).await;
s.wait_for_index_ready().await;
expect!["Class Legacy @ Legacy.php:2"]
.assert_eq(&s.snapshot_workspace_symbols("Legacy").await);
std::fs::write(
tmp.path().join("Added.php"),
b"<?php\n// na\xefve\nclass Added {}\n",
)
.unwrap();
let uri = s.uri("Added.php");
s.did_change_watched_files(vec![(uri, 1)]).await;
s.wait_until_symbol_present("Added", std::time::Duration::from_secs(5))
.await;
}
#[cfg(unix)]
#[tokio::test]
async fn symlinked_directories_are_followed_once() {
use std::os::unix::fs::symlink;
let tmp = tempfile::tempdir().unwrap();
let package = tempfile::tempdir().unwrap();
std::fs::write(package.path().join("Pkg.php"), "<?php\nclass Pkg {}\n").unwrap();
std::fs::create_dir_all(tmp.path().join("vendor/acme")).unwrap();
symlink(package.path(), tmp.path().join("vendor/acme/pkg")).unwrap();
std::fs::create_dir(tmp.path().join("src")).unwrap();
std::fs::write(tmp.path().join("src/Own.php"), "<?php\nclass Own {}\n").unwrap();
symlink(tmp.path().join("src"), tmp.path().join("alias")).unwrap();
symlink(tmp.path(), tmp.path().join("src/loop")).unwrap();
let mut s = TestServer::with_root(tmp.path()).await;
s.wait_for_index_ready().await;
expect!["Class Pkg @ vendor/acme/pkg/Pkg.php:1"]
.assert_eq(&s.snapshot_workspace_symbols("Pkg").await);
expect!["Class Own @ src/Own.php:1"]
.assert_eq(&s.snapshot_workspace_symbols("Own").await);
}