use std::sync::Arc;
use tower_lsp::lsp_types::*;
use crate::completion::class_completion::ClassNameContext;
use crate::symbol_map::SymbolMap;
use crate::types::{ClassInfo, ClassLikeKind};
use crate::util::find_class_at_offset;
#[derive(Debug, Clone, Copy)]
pub(crate) struct KeywordContext {
pub in_function_like: bool,
pub in_breakable: bool,
pub in_loop: bool,
pub in_switch: bool,
pub in_top_level: bool,
pub in_extends_declaration_header: bool,
pub in_implements_declaration_header: bool,
pub class_body_kind: Option<ClassLikeKind>,
pub after_member_modifier_chain: bool,
}
const PHP_KEYWORDS: &[&str] = &[
"abstract",
"as",
"break",
"case",
"catch",
"class",
"clone",
"const",
"continue",
"declare",
"default",
"die",
"do",
"echo",
"else",
"elseif",
"empty",
"enum",
"eval",
"exit",
"extends",
"final",
"finally",
"fn",
"for",
"foreach",
"function",
"global",
"goto",
"if",
"implements",
"include",
"include_once",
"instanceof",
"interface",
"isset",
"list",
"match",
"namespace",
"new",
"print",
"private",
"protected",
"public",
"readonly",
"require",
"require_once",
"return",
"static",
"switch",
"throw",
"trait",
"try",
"unset",
"use",
"while",
"yield",
];
const BACKED_ENUM_TYPES: &[&str] = &["string", "int"];
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum DeclarationHeaderKind {
Class,
Interface,
Enum,
}
fn statement_segment_start(chars: &[char], offset: usize) -> usize {
for i in (0..offset).rev() {
if matches!(chars[i], '{' | '}' | ';' | '\n' | '\r') {
return i + 1;
}
}
0
}
fn collect_ascii_words(chars: &[char], start: usize, end: usize) -> Vec<String> {
let mut words: Vec<String> = Vec::new();
let mut i = start;
while i < end {
if chars[i].is_ascii_alphanumeric() || chars[i] == '_' {
let j = i;
i += 1;
while i < end && (chars[i].is_ascii_alphanumeric() || chars[i] == '_') {
i += 1;
}
words.push(chars[j..i].iter().collect::<String>().to_ascii_lowercase());
continue;
}
i += 1;
}
words
}
fn declaration_header_kind(content: &str, position: Position) -> Option<DeclarationHeaderKind> {
let chars: Vec<char> = content.chars().collect();
let offset = crate::util::position_to_char_offset(&chars, position)?;
let start = statement_segment_start(&chars, offset);
let words = collect_ascii_words(&chars, start, offset);
if words.is_empty() {
return None;
}
let mut decl_idx_kind: Option<(usize, DeclarationHeaderKind)> = None;
for (idx, w) in words.iter().enumerate() {
let kind = match w.as_str() {
"class" => Some(DeclarationHeaderKind::Class),
"interface" => Some(DeclarationHeaderKind::Interface),
"enum" => Some(DeclarationHeaderKind::Enum),
_ => None,
};
if let Some(kind) = kind {
decl_idx_kind = Some((idx, kind));
break;
}
}
let (decl_idx, kind) = decl_idx_kind?;
let valid_prefix = words[..decl_idx]
.iter()
.all(|w| matches!(w.as_str(), "abstract" | "final" | "readonly"));
if !valid_prefix {
return None;
}
let name_token = words.get(decl_idx + 1)?;
if name_token.is_empty() {
return None;
}
if kind == DeclarationHeaderKind::Class
&& decl_idx > 0
&& words.get(decl_idx - 1).is_some_and(|w| w == "new")
{
return None;
}
Some(kind)
}
fn is_after_member_modifier_chain(content: &str, position: Position) -> bool {
let chars: Vec<char> = content.chars().collect();
let Some(offset) = crate::util::position_to_char_offset(&chars, position) else {
return false;
};
if offset == 0 || !chars[offset - 1].is_ascii_whitespace() {
return false;
}
let start = statement_segment_start(&chars, offset);
let words = collect_ascii_words(&chars, start, offset);
if words.is_empty() {
return false;
}
words.iter().all(|w| {
matches!(
w.as_str(),
"public" | "protected" | "private" | "static" | "abstract" | "final" | "readonly"
)
})
}
pub(crate) fn build_keyword_context(
content: &str,
position: Position,
cursor_offset: u32,
map: Option<&SymbolMap>,
classes: &[Arc<ClassInfo>],
) -> KeywordContext {
let decl_kind = declaration_header_kind(content, position);
let in_function_like = map.is_some_and(|m| m.is_inside_function_like_scope(cursor_offset));
let in_breakable = map.is_some_and(|m| m.is_inside_breakable_scope(cursor_offset));
let in_loop = map.is_some_and(|m| m.is_inside_loop_scope(cursor_offset));
let in_switch = map.is_some_and(|m| m.is_inside_switch_scope(cursor_offset));
let class_at_cursor = find_class_at_offset(classes, cursor_offset);
let in_class_like = class_at_cursor.is_some();
let class_body_kind = if in_function_like {
None
} else {
class_at_cursor.map(|c| c.kind)
};
let in_top_level = !in_function_like && !in_class_like && !in_breakable;
let after_member_modifier_chain =
class_body_kind.is_some() && is_after_member_modifier_chain(content, position);
KeywordContext {
in_function_like,
in_breakable,
in_loop,
in_switch,
in_top_level,
in_extends_declaration_header: decl_kind.is_some(),
in_implements_declaration_header: matches!(
decl_kind,
Some(DeclarationHeaderKind::Class | DeclarationHeaderKind::Enum)
),
class_body_kind,
after_member_modifier_chain,
}
}
pub(crate) fn enum_backing_type_partial(content: &str, position: Position) -> Option<String> {
let chars: Vec<char> = content.chars().collect();
let offset = crate::util::position_to_char_offset(&chars, position)?;
let mut partial_start = offset;
while partial_start > 0
&& (chars[partial_start - 1].is_ascii_alphanumeric() || chars[partial_start - 1] == '_')
{
partial_start -= 1;
}
if partial_start > 0 && chars[partial_start - 1] == '$' {
return None;
}
if partial_start >= 2 && chars[partial_start - 2] == '-' && chars[partial_start - 1] == '>' {
return None;
}
if partial_start >= 2 && chars[partial_start - 2] == ':' && chars[partial_start - 1] == ':' {
return None;
}
let partial: String = chars[partial_start..offset].iter().collect();
let mut i = partial_start;
while i > 0 && chars[i - 1].is_ascii_whitespace() {
i -= 1;
}
if i == 0 || chars[i - 1] != ':' {
return None;
}
if !matches!(
declaration_header_kind(content, position),
Some(DeclarationHeaderKind::Enum)
) {
return None;
}
Some(partial)
}
pub(crate) fn build_keyword_completions(
prefix: &str,
class_ctx: ClassNameContext,
ctx: KeywordContext,
) -> Vec<CompletionItem> {
if !matches!(class_ctx, ClassNameContext::Any) {
return Vec::new();
}
let prefix_lower = prefix.to_lowercase();
PHP_KEYWORDS
.iter()
.enumerate()
.filter(|(_, keyword)| keyword.starts_with(&prefix_lower))
.filter(|(_, keyword)| keyword_allowed(keyword, ctx))
.map(|(idx, keyword)| CompletionItem {
label: (*keyword).to_string(),
kind: Some(CompletionItemKind::KEYWORD),
detail: Some("PHP keyword".to_string()),
insert_text: Some((*keyword).to_string()),
filter_text: Some((*keyword).to_string()),
sort_text: Some(format!("3_{idx:03}_{keyword}")),
..CompletionItem::default()
})
.collect()
}
pub(crate) fn build_backed_enum_type_completions(prefix: &str) -> Vec<CompletionItem> {
let prefix_lower = prefix.to_ascii_lowercase();
BACKED_ENUM_TYPES
.iter()
.enumerate()
.filter(|(_, ty)| ty.starts_with(&prefix_lower))
.map(|(idx, ty)| CompletionItem {
label: (*ty).to_string(),
kind: Some(CompletionItemKind::KEYWORD),
detail: Some("Enum backing type".to_string()),
insert_text: Some((*ty).to_string()),
filter_text: Some((*ty).to_string()),
sort_text: Some(format!("0_enum_backed_{idx:03}_{ty}")),
..CompletionItem::default()
})
.collect()
}
fn keyword_allowed(keyword: &&str, ctx: KeywordContext) -> bool {
if let Some(kind) = ctx.class_body_kind {
return keyword_allowed_in_class_body(keyword, kind);
}
match *keyword {
"return" | "yield" => ctx.in_function_like,
"break" => ctx.in_breakable,
"continue" => ctx.in_loop,
"case" | "default" => ctx.in_switch,
"namespace" => ctx.in_top_level,
"extends" => ctx.in_extends_declaration_header,
"implements" => ctx.in_implements_declaration_header,
_ => true,
}
}
fn keyword_allowed_in_class_body(keyword: &&str, kind: ClassLikeKind) -> bool {
match kind {
ClassLikeKind::Class | ClassLikeKind::Trait => matches!(
*keyword,
"public"
| "protected"
| "private"
| "static"
| "final"
| "abstract"
| "readonly"
| "function"
| "const"
| "use"
),
ClassLikeKind::Interface => matches!(*keyword, "public" | "function" | "const"),
ClassLikeKind::Enum => matches!(
*keyword,
"public" | "protected" | "private" | "static" | "function" | "const" | "use" | "case"
),
}
}