use php_ast::{NamespaceBody, Stmt, StmtKind};
use tower_lsp::lsp_types::{CompletionItem, CompletionItemKind, Position};
pub(super) fn collect_classes_with_ns(
stmts: &[Stmt<'_, '_>],
ns_prefix: &str,
items: &mut Vec<(String, CompletionItemKind, String)>,
) {
let mut cur_ns = ns_prefix.to_string();
let fqn_for = |short: &str, ns: &str| -> String {
if ns.is_empty() {
short.to_string()
} else {
format!("{}\\{}", ns, short)
}
};
for stmt in stmts {
match &stmt.kind {
StmtKind::Class(c) => {
let short = c.name.unwrap_or("");
if !short.is_empty() {
items.push((
short.to_string(),
CompletionItemKind::CLASS,
fqn_for(short, &cur_ns),
));
}
}
StmtKind::Interface(i) => {
items.push((
i.name.to_string(),
CompletionItemKind::INTERFACE,
fqn_for(i.name, &cur_ns),
));
}
StmtKind::Trait(t) => {
items.push((
t.name.to_string(),
CompletionItemKind::CLASS,
fqn_for(t.name, &cur_ns),
));
}
StmtKind::Enum(e) => {
items.push((
e.name.to_string(),
CompletionItemKind::ENUM,
fqn_for(e.name, &cur_ns),
));
}
StmtKind::Namespace(ns) => {
let ns_name = ns
.name
.as_ref()
.map(|n| n.to_string_repr().to_string())
.unwrap_or_default();
match &ns.body {
NamespaceBody::Braced(inner) => {
collect_classes_with_ns(inner, &ns_name, items);
}
NamespaceBody::Simple => {
cur_ns = ns_name;
}
}
}
_ => {}
}
}
}
pub(super) fn use_insert_position(source: &str) -> Position {
let mut last_use_line: Option<u32> = None;
let mut anchor_line: u32 = 0;
for (i, line) in source.lines().enumerate() {
let trimmed = line.trim();
if trimmed.starts_with("<?") || trimmed.starts_with("namespace ") {
anchor_line = i as u32;
}
if trimmed.starts_with("use ") && !trimmed.starts_with("use function ") {
last_use_line = Some(i as u32);
}
}
Position {
line: last_use_line.unwrap_or(anchor_line) + 1,
character: 0,
}
}
pub(super) fn current_file_namespace(stmts: &[Stmt<'_, '_>]) -> String {
for stmt in stmts {
if let StmtKind::Namespace(ns) = &stmt.kind {
return ns
.name
.as_ref()
.map(|n| n.to_string_repr().to_string())
.unwrap_or_default();
}
}
String::new()
}
pub(super) fn collect_fqns_with_prefix(
stmts: &[Stmt<'_, '_>],
ns: &str,
prefix: &str,
out: &mut Vec<CompletionItem>,
) {
for stmt in stmts {
match &stmt.kind {
StmtKind::Class(c) => {
if let Some(name) = c.name {
let fqn = if ns.is_empty() {
name.to_string()
} else {
format!("{ns}\\{name}")
};
if fqn.to_lowercase().contains(&prefix.to_lowercase()) || prefix.is_empty() {
out.push(CompletionItem {
label: fqn.clone(),
kind: Some(CompletionItemKind::CLASS),
insert_text: Some(fqn),
..Default::default()
});
}
}
}
StmtKind::Interface(i) => {
let fqn = if ns.is_empty() {
i.name.to_string()
} else {
format!("{ns}\\{}", i.name)
};
if fqn.to_lowercase().contains(&prefix.to_lowercase()) || prefix.is_empty() {
out.push(CompletionItem {
label: fqn.clone(),
kind: Some(CompletionItemKind::INTERFACE),
insert_text: Some(fqn),
..Default::default()
});
}
}
StmtKind::Namespace(ns_stmt) => {
let ns_name = ns_stmt
.name
.as_ref()
.map(|n| {
if ns.is_empty() {
n.to_string_repr().to_string()
} else {
format!("{ns}\\{}", n.to_string_repr())
}
})
.unwrap_or_else(|| ns.to_string());
if let NamespaceBody::Braced(inner) = &ns_stmt.body {
collect_fqns_with_prefix(inner, &ns_name, prefix, out);
}
}
_ => {}
}
}
}
pub(super) fn use_completion_prefix(source: &str, position: Position) -> Option<String> {
let line = source.lines().nth(position.line as usize)?;
let col = crate::util::utf16_offset_to_byte(line, position.character as usize);
let before = line[..col].trim_start();
let prefix = before.strip_prefix("use ")?;
Some(prefix.trim_start_matches('\\').to_string())
}
pub(super) fn typed_prefix(source: Option<&str>, position: Option<Position>) -> Option<String> {
let src = source?;
let pos = position?;
let line = src.lines().nth(pos.line as usize)?;
let col = crate::util::utf16_offset_to_byte(line, pos.character as usize);
let before = &line[..col];
let prefix: String = before
.chars()
.rev()
.take_while(|&c| c.is_alphanumeric() || c == '_' || c == '\\')
.collect::<String>()
.chars()
.rev()
.collect();
if prefix.is_empty() {
None
} else {
Some(prefix)
}
}