use tower_lsp::lsp_types::Position;
use php_ast::{
ClassMember, ClassMemberKind, EnumMember, EnumMemberKind, ExprKind, NamespaceBody, Stmt,
StmtKind,
};
use crate::document::ast::str_offset;
use super::position::position_to_byte_offset_strict;
fn name_offset_in_member(source: &str, member_span: php_ast::Span, name: &str) -> Option<u32> {
let s = member_span.start as usize;
let e = (member_span.end as usize).min(source.len());
source
.get(s..e)?
.find(name)
.map(|off| member_span.start + off as u32)
}
pub(crate) fn cursor_is_on_method_decl(
source: &str,
stmts: &[Stmt<'_, '_>],
position: Position,
) -> bool {
let Some(cursor) = position_to_byte_offset_strict(source, position) else {
return false;
};
fn check(source: &str, stmts: &[Stmt<'_, '_>], cursor: u32) -> bool {
for stmt in stmts {
match &stmt.kind {
StmtKind::Class(c) => {
for member in c.body.members.iter() {
if let ClassMemberKind::Method(m) = &member.kind {
let name = m.name.to_string();
let start =
name_offset_in_member(source, member.span, &name).unwrap_or(0);
let end = start + name.len() as u32;
if cursor >= start && cursor < end {
return true;
}
}
}
}
StmtKind::Interface(i) => {
for member in i.body.members.iter() {
if let ClassMemberKind::Method(m) = &member.kind {
let name = m.name.to_string();
let start =
name_offset_in_member(source, member.span, &name).unwrap_or(0);
let end = start + name.len() as u32;
if cursor >= start && cursor < end {
return true;
}
}
}
}
StmtKind::Trait(t) => {
for member in t.body.members.iter() {
if let ClassMemberKind::Method(m) = &member.kind {
let name = m.name.to_string();
let start =
name_offset_in_member(source, member.span, &name).unwrap_or(0);
let end = start + name.len() as u32;
if cursor >= start && cursor < end {
return true;
}
}
}
}
StmtKind::Enum(e) => {
for member in e.body.members.iter() {
if let EnumMemberKind::Method(m) = &member.kind {
let name = m.name.to_string();
let start =
name_offset_in_member(source, member.span, &name).unwrap_or(0);
let end = start + name.len() as u32;
if cursor >= start && cursor < end {
return true;
}
}
}
}
StmtKind::Namespace(ns) => {
if let NamespaceBody::Braced(inner) = &ns.body
&& check(source, &inner.stmts, cursor)
{
return true;
}
}
_ => {}
}
}
false
}
check(source, stmts, cursor)
}
pub(crate) fn cursor_is_on_property_decl(
source: &str,
stmts: &[Stmt<'_, '_>],
position: Position,
) -> Option<String> {
let cursor = position_to_byte_offset_strict(source, position)?;
fn check(source: &str, stmts: &[Stmt<'_, '_>], cursor: u32) -> Option<String> {
for stmt in stmts {
match &stmt.kind {
StmtKind::Class(c) => {
for member in c.body.members.iter() {
if let ClassMemberKind::Property(p) = &member.kind {
let name = p.name.to_string();
let start =
name_offset_in_member(source, member.span, &name).unwrap_or(0);
let end = start + name.len() as u32;
if cursor >= start && cursor < end {
return Some(name);
}
}
}
}
StmtKind::Trait(t) => {
for member in t.body.members.iter() {
if let ClassMemberKind::Property(p) = &member.kind {
let name = p.name.to_string();
let start =
name_offset_in_member(source, member.span, &name).unwrap_or(0);
let end = start + name.len() as u32;
if cursor >= start && cursor < end {
return Some(name);
}
}
}
}
StmtKind::Namespace(ns) => {
if let NamespaceBody::Braced(inner) = &ns.body
&& let Some(name) = check(source, &inner.stmts, cursor)
{
return Some(name);
}
}
_ => {}
}
}
None
}
check(source, stmts, cursor)
}
pub(crate) fn cursor_is_on_constant_decl(
source: &str,
stmts: &[Stmt<'_, '_>],
position: Position,
) -> Option<(String, Option<String>)> {
let cursor = position_to_byte_offset_strict(source, position)?;
fn check_members(source: &str, members: &[ClassMember<'_, '_>], cursor: u32) -> Option<String> {
for member in members {
if let ClassMemberKind::ClassConst(c) = &member.kind {
let name = c.name.to_string();
let start = name_offset_in_member(source, member.span, &name).unwrap_or(0);
let end = start + name.len() as u32;
if cursor >= start && cursor < end {
return Some(name);
}
}
}
None
}
fn check_enum_members(
source: &str,
members: &[EnumMember<'_, '_>],
cursor: u32,
) -> Option<String> {
for member in members {
if let EnumMemberKind::ClassConst(c) = &member.kind {
let name = c.name.to_string();
let start = name_offset_in_member(source, member.span, &name).unwrap_or(0);
let end = start + name.len() as u32;
if cursor >= start && cursor < end {
return Some(name);
}
}
}
None
}
fn check(
source: &str,
stmts: &[Stmt<'_, '_>],
cursor: u32,
) -> Option<(String, Option<String>)> {
for stmt in stmts {
match &stmt.kind {
StmtKind::Class(c) => {
if let Some(const_name) = check_members(source, &c.body.members, cursor) {
let owner = c.name.map(|n| n.to_string());
return Some((const_name, owner));
}
}
StmtKind::Interface(i) => {
if let Some(const_name) = check_members(source, &i.body.members, cursor) {
return Some((const_name, Some(i.name.to_string())));
}
}
StmtKind::Trait(t) => {
if let Some(const_name) = check_members(source, &t.body.members, cursor) {
return Some((const_name, Some(t.name.to_string())));
}
}
StmtKind::Enum(e) => {
if let Some(const_name) = check_enum_members(source, &e.body.members, cursor) {
return Some((const_name, Some(e.name.to_string())));
}
}
StmtKind::Const(items) => {
for item in items.iter() {
let name = item.name.to_string();
let s = item.span.start as usize;
let e = (item.span.end as usize).min(source.len());
if let Some(off) = source.get(s..e).and_then(|sl| sl.find(&name)) {
let start = item.span.start + off as u32;
let end = start + name.len() as u32;
if cursor >= start && cursor < end {
return Some((name, None));
}
}
}
}
StmtKind::Expression(expr) => {
if let ExprKind::FunctionCall(f) = &expr.kind
&& let ExprKind::Identifier(id) = &f.name.kind
&& id.as_str() == "define"
&& let Some(first_arg) = f.args.first()
&& let ExprKind::String(s) = &first_arg.value.kind
{
let start = first_arg.value.span.start + 1;
let end = start + s.len() as u32;
if cursor >= start && cursor < end {
return Some((s.to_string(), None));
}
}
}
StmtKind::Namespace(ns) => {
if let NamespaceBody::Braced(inner) = &ns.body
&& let Some(result) = check(source, &inner.stmts, cursor)
{
return Some(result);
}
}
_ => {}
}
}
None
}
check(source, stmts, cursor)
}
pub(crate) fn class_name_at_construct_decl(
source: &str,
stmts: &[Stmt<'_, '_>],
position: Position,
) -> Option<String> {
let cursor = position_to_byte_offset_strict(source, position)?;
fn check(source: &str, stmts: &[Stmt<'_, '_>], cursor: u32, ns_prefix: &str) -> Option<String> {
let mut current_ns = ns_prefix.to_owned();
for stmt in stmts {
match &stmt.kind {
StmtKind::Class(c) => {
for member in c.body.members.iter() {
if let ClassMemberKind::Method(m) = &member.kind
&& m.name == "__construct"
{
let name = m.name.to_string();
let start =
name_offset_in_member(source, member.span, &name).unwrap_or(0);
let end = start + name.len() as u32;
if cursor >= start && cursor < end {
let short = c.name?;
return Some(if current_ns.is_empty() {
short.to_string()
} else {
format!("{}\\{}", current_ns, short)
});
}
}
}
}
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) => {
if let Some(name) = check(source, &inner.stmts, cursor, &ns_name) {
return Some(name);
}
}
NamespaceBody::Simple => {
current_ns = ns_name;
}
}
}
_ => {}
}
}
None
}
check(source, stmts, cursor, "")
}
pub(crate) fn promoted_property_at_cursor(
source: &str,
stmts: &[Stmt<'_, '_>],
position: Position,
) -> Option<String> {
let cursor = position_to_byte_offset_strict(source, position)?;
fn check(source: &str, stmts: &[Stmt<'_, '_>], cursor: u32) -> Option<String> {
for stmt in stmts {
match &stmt.kind {
StmtKind::Class(c) => {
for member in c.body.members.iter() {
if let ClassMemberKind::Method(m) = &member.kind
&& m.name == "__construct"
{
for param in m.params.iter() {
if param.visibility.is_none() {
continue;
}
let name_start =
str_offset(source, ¶m.name.to_string()).unwrap_or(0);
let name_end = name_start + param.name.to_string().len() as u32;
if cursor >= name_start && cursor < name_end {
return Some(
param.name.to_string().trim_start_matches('$').to_string(),
);
}
}
}
}
}
StmtKind::Namespace(ns) => {
if let NamespaceBody::Braced(inner) = &ns.body
&& let Some(name) = check(source, &inner.stmts, cursor)
{
return Some(name);
}
}
_ => {}
}
}
None
}
check(source, stmts, cursor)
}