use crate::common::create_test_backend;
use tower_lsp::LanguageServer;
use tower_lsp::lsp_types::*;
async fn complete_at(
backend: &phpantom_lsp::Backend,
uri: &Url,
text: &str,
line: u32,
character: u32,
) -> Vec<CompletionItem> {
backend
.did_open(DidOpenTextDocumentParams {
text_document: TextDocumentItem {
uri: uri.clone(),
language_id: "php".to_string(),
version: 1,
text: text.to_string(),
},
})
.await;
let result = backend
.completion(CompletionParams {
text_document_position: TextDocumentPositionParams {
text_document: TextDocumentIdentifier { uri: uri.clone() },
position: Position { line, character },
},
work_done_progress_params: WorkDoneProgressParams::default(),
partial_result_params: PartialResultParams::default(),
context: None,
})
.await
.unwrap();
match result {
Some(CompletionResponse::Array(items)) => items,
Some(CompletionResponse::List(list)) => list.items,
None => vec![],
}
}
fn labels(items: &[CompletionItem]) -> Vec<&str> {
items.iter().map(|i| i.label.as_str()).collect()
}
#[tokio::test]
async fn test_class_declaration_offers_filename() {
let backend = create_test_backend();
let uri = Url::parse("file:///Test.php").unwrap();
let items = complete_at(&backend, &uri, "<?php\n\nclass T", 2, 7).await;
assert_eq!(labels(&items), vec!["Test"]);
assert_eq!(items[0].kind, Some(CompletionItemKind::CLASS));
}
#[tokio::test]
async fn test_class_declaration_no_partial() {
let backend = create_test_backend();
let uri = Url::parse("file:///MyService.php").unwrap();
let items = complete_at(&backend, &uri, "<?php\n\nclass ", 2, 6).await;
assert_eq!(labels(&items), vec!["MyService"]);
}
#[tokio::test]
async fn test_interface_declaration_offers_filename() {
let backend = create_test_backend();
let uri = Url::parse("file:///Printable.php").unwrap();
let items = complete_at(&backend, &uri, "<?php\n\ninterface P", 2, 11).await;
assert_eq!(labels(&items), vec!["Printable"]);
}
#[tokio::test]
async fn test_trait_declaration_offers_filename() {
let backend = create_test_backend();
let uri = Url::parse("file:///HasTimestamps.php").unwrap();
let items = complete_at(&backend, &uri, "<?php\n\ntrait H", 2, 7).await;
assert_eq!(labels(&items), vec!["HasTimestamps"]);
}
#[tokio::test]
async fn test_enum_declaration_offers_filename() {
let backend = create_test_backend();
let uri = Url::parse("file:///Status.php").unwrap();
let items = complete_at(&backend, &uri, "<?php\n\nenum S", 2, 6).await;
assert_eq!(labels(&items), vec!["Status"]);
}
#[tokio::test]
async fn test_abstract_class_declaration_offers_filename() {
let backend = create_test_backend();
let uri = Url::parse("file:///BaseController.php").unwrap();
let items = complete_at(&backend, &uri, "<?php\n\nabstract class B", 2, 16).await;
assert_eq!(labels(&items), vec!["BaseController"]);
}
#[tokio::test]
async fn test_final_class_declaration_offers_filename() {
let backend = create_test_backend();
let uri = Url::parse("file:///Singleton.php").unwrap();
let items = complete_at(&backend, &uri, "<?php\n\nfinal class S", 2, 13).await;
assert_eq!(labels(&items), vec!["Singleton"]);
}
#[tokio::test]
async fn test_readonly_class_declaration_offers_filename() {
let backend = create_test_backend();
let uri = Url::parse("file:///ValueObject.php").unwrap();
let items = complete_at(&backend, &uri, "<?php\n\nreadonly class V", 2, 16).await;
assert_eq!(labels(&items), vec!["ValueObject"]);
}
#[tokio::test]
async fn test_final_readonly_class_declaration_offers_filename() {
let backend = create_test_backend();
let uri = Url::parse("file:///Money.php").unwrap();
let items = complete_at(&backend, &uri, "<?php\n\nfinal readonly class M", 2, 22).await;
assert_eq!(labels(&items), vec!["Money"]);
}
#[tokio::test]
async fn test_anonymous_class_not_triggered() {
let backend = create_test_backend();
let uri = Url::parse("file:///Foo.php").unwrap();
let items = complete_at(&backend, &uri, "<?php\n\n$x = new class ", 2, 16).await;
let has_foo = items.iter().any(|i| i.label == "Foo");
assert!(
!has_foo,
"anonymous class context should not offer filename"
);
}
#[tokio::test]
async fn test_extends_not_triggered() {
let backend = create_test_backend();
let uri = Url::parse("file:///Child.php").unwrap();
let items = complete_at(&backend, &uri, "<?php\n\nclass Child extends B", 2, 21).await;
let is_declaration_only = items.len() == 1
&& items[0].label == "Child"
&& items[0].detail.as_deref() == Some("Match filename");
assert!(
!is_declaration_only,
"extends context should not trigger class declaration completion"
);
}
#[tokio::test]
async fn test_implements_not_triggered() {
let backend = create_test_backend();
let uri = Url::parse("file:///MyClass.php").unwrap();
let items = complete_at(&backend, &uri, "<?php\n\nclass MyClass implements S", 2, 26).await;
let is_declaration_only = items.len() == 1
&& items[0].label == "MyClass"
&& items[0].detail.as_deref() == Some("Match filename");
assert!(
!is_declaration_only,
"implements context should not trigger class declaration completion"
);
}
#[tokio::test]
async fn test_only_one_item_returned() {
let backend = create_test_backend();
let uri = Url::parse("file:///Test.php").unwrap();
let items = complete_at(&backend, &uri, "<?php\n\nclass T", 2, 7).await;
assert_eq!(items.len(), 1, "should return exactly one completion item");
}
#[tokio::test]
async fn test_completion_item_detail() {
let backend = create_test_backend();
let uri = Url::parse("file:///UserRepository.php").unwrap();
let items = complete_at(&backend, &uri, "<?php\n\nclass U", 2, 7).await;
assert_eq!(items.len(), 1);
assert_eq!(items[0].detail.as_deref(), Some("Match filename"));
}
#[tokio::test]
async fn test_nested_path_uses_filename_only() {
let backend = create_test_backend();
let uri = Url::parse("file:///home/user/project/src/Models/User.php").unwrap();
let items = complete_at(&backend, &uri, "<?php\n\nclass U", 2, 7).await;
assert_eq!(labels(&items), vec!["User"]);
}
#[tokio::test]
async fn test_class_declaration_with_namespace() {
let backend = create_test_backend();
let uri = Url::parse("file:///OrderService.php").unwrap();
let items = complete_at(
&backend,
&uri,
"<?php\n\nnamespace App\\Services;\n\nclass O",
4,
7,
)
.await;
assert_eq!(labels(&items), vec!["OrderService"]);
}