mago 1.29.0

A comprehensive suite of PHP tooling inspired by Rust’s approach, providing parsing, linting, formatting, and more through a unified CLI and library interface.
//! Item builders for each [`super::Context`].

use mago_bytes::BytesDisplay;
use mago_codex::metadata::CodebaseMetadata;
use mago_codex::metadata::class_like::ClassLikeMetadata;
use mago_codex::metadata::function_like::FunctionLikeKind;
use mago_codex::metadata::function_like::FunctionLikeMetadata;
use mago_codex::ttype::TType;
use mago_database::Database;
use mago_database::DatabaseReader;
use mago_database::file::File as MagoFile;
use mago_database::file::FileType;
use mago_span::Span;
use mago_syntax::token::TokenKind;
use tower_lsp::lsp_types::CompletionItem;
use tower_lsp::lsp_types::CompletionItemKind;
use tower_lsp::lsp_types::Documentation;
use tower_lsp::lsp_types::InsertTextFormat;
use tower_lsp::lsp_types::MarkupContent;
use tower_lsp::lsp_types::MarkupKind;

use crate::language_server::capabilities::lookup;
use crate::language_server::state::ExpressionTypeIndex;

use super::MAX_RESULTS;

pub(super) fn variable_items(
    tokens: &[mago_syntax::token::Token<'_>],
    scope_start: u32,
    offset: u32,
    prefix: &[u8],
) -> Vec<CompletionItem> {
    let mut seen = foldhash::HashSet::default();
    let mut out = Vec::new();
    for token in tokens {
        if !matches!(token.kind, TokenKind::Variable) {
            continue;
        }
        if token.start.offset < scope_start || token.start.offset >= offset {
            continue;
        }
        let name = mago_bytes::trim_start_byte(token.value, b'$');
        if !name.starts_with(prefix) {
            continue;
        }
        if !seen.insert(name) {
            continue;
        }
        let name_str = String::from_utf8_lossy(name).into_owned();
        out.push(CompletionItem {
            label: format!("${name_str}"),
            kind: Some(CompletionItemKind::VARIABLE),
            insert_text: Some(name_str),
            ..CompletionItem::default()
        });
        if out.len() >= MAX_RESULTS {
            break;
        }
    }
    out
}

pub(super) fn instance_member_items(
    codebase: &CodebaseMetadata,
    type_index: Option<&ExpressionTypeIndex>,
    receiver_span: (u32, u32),
    prefix: &[u8],
) -> Vec<CompletionItem> {
    let Some(type_index) = type_index else {
        return Vec::new();
    };
    let Some(class_words) = type_index.by_span.get(&receiver_span) else {
        return Vec::new();
    };

    let mut out: Vec<CompletionItem> = Vec::new();
    let mut seen = foldhash::HashSet::default();
    for class in class_words {
        if let Some(meta) = codebase.get_class_like(class.as_bytes()) {
            push_unique(&mut out, &mut seen, collect_class_members(codebase, meta, prefix, false));
        }
        if out.len() >= MAX_RESULTS {
            break;
        }
    }
    out
}

pub(super) fn static_member_items(codebase: &CodebaseMetadata, class: &[u8], prefix: &[u8]) -> Vec<CompletionItem> {
    if matches!(class, b"self" | b"static" | b"parent") {
        return Vec::new();
    }
    codebase.get_class_like(class).map(|meta| collect_class_members(codebase, meta, prefix, true)).unwrap_or_default()
}

pub(super) fn bare_items(
    database: &Database<'_>,
    codebase: &CodebaseMetadata,
    file: &MagoFile,
    offset: u32,
    prefix: &[u8],
) -> Vec<CompletionItem> {
    let needle = prefix.to_ascii_lowercase();
    let namespace = lookup::namespace_at_offset(file, offset).unwrap_or_default();
    let ns_lc = if namespace.is_empty() { None } else { Some(namespace.to_ascii_lowercase()) };
    let in_scope = |full: &[u8]| -> bool {
        match &ns_lc {
            Some(ns) => full.starts_with(ns) && full.get(ns.len()) == Some(&b'\\') || !full.contains(&b'\\'),
            None => true,
        }
    };
    let mut out = Vec::new();

    for (key, meta) in codebase.class_likes.iter() {
        if out.len() >= MAX_RESULTS {
            break;
        }
        if !is_user_symbol(database, meta.span) {
            continue;
        }
        let display = local_name(meta.original_name.as_bytes());
        if !display.to_ascii_lowercase().starts_with(&needle) || !in_scope(key.as_bytes()) {
            continue;
        }
        out.push(make_class_item(meta, display));
    }

    for ((_, name), meta) in codebase.function_likes.iter() {
        if matches!(meta.kind, FunctionLikeKind::Method) || out.len() >= MAX_RESULTS {
            continue;
        }
        if !is_user_symbol(database, meta.span) {
            continue;
        }
        let display: &[u8] = meta.original_name.as_bytes();
        let local = local_name(display);
        if !local.to_ascii_lowercase().starts_with(&needle) || !in_scope(name.as_bytes()) {
            continue;
        }
        let local_str = String::from_utf8_lossy(local).into_owned();
        out.push(CompletionItem {
            label: local_str.clone(),
            kind: Some(CompletionItemKind::FUNCTION),
            detail: Some(render_signature(meta, local)),
            insert_text: Some(format!("{local_str}($1)")),
            insert_text_format: Some(InsertTextFormat::SNIPPET),
            ..CompletionItem::default()
        });
    }

    for (name, meta) in codebase.constants.iter() {
        if out.len() >= MAX_RESULTS {
            break;
        }
        if !is_user_symbol(database, meta.span) || !name.as_bytes().to_ascii_lowercase().starts_with(&needle) {
            continue;
        }
        if !in_scope(name.as_bytes()) {
            continue;
        }
        out.push(CompletionItem {
            label: String::from_utf8_lossy(local_name(name.as_bytes())).into_owned(),
            kind: Some(CompletionItemKind::CONSTANT),
            ..CompletionItem::default()
        });
    }

    out
}

pub(super) fn qualified_items(
    database: &Database<'_>,
    codebase: &CodebaseMetadata,
    qualifier: &[u8],
    prefix: &[u8],
) -> Vec<CompletionItem> {
    let needle = prefix.to_ascii_lowercase();
    let qual_lc = qualifier.to_ascii_lowercase();
    let mut want_prefix = qual_lc;
    want_prefix.push(b'\\');
    let mut out = Vec::new();

    for (key, meta) in codebase.class_likes.iter() {
        if out.len() >= MAX_RESULTS {
            break;
        }
        if !is_user_symbol(database, meta.span) {
            continue;
        }
        let lc = key.as_bytes();
        if !lc.starts_with(&want_prefix) {
            continue;
        }
        let suffix = &lc[want_prefix.len()..];
        if suffix.contains(&b'\\') || !suffix.starts_with(&needle) {
            continue;
        }
        let display = local_name(meta.original_name.as_bytes());
        out.push(make_class_item(meta, display));
    }

    out
}

fn collect_class_members(
    codebase: &CodebaseMetadata,
    meta: &ClassLikeMetadata,
    prefix: &[u8],
    static_access: bool,
) -> Vec<CompletionItem> {
    let needle = prefix.to_ascii_lowercase();
    let mut out = Vec::new();

    if static_access {
        for name in meta.constants.keys() {
            let s = name.as_bytes();
            if !s.to_ascii_lowercase().starts_with(&needle) {
                continue;
            }
            out.push(CompletionItem {
                label: String::from_utf8_lossy(s).into_owned(),
                kind: Some(CompletionItemKind::CONSTANT),
                ..CompletionItem::default()
            });
        }
        for name in meta.enum_cases.keys() {
            let s = name.as_bytes();
            if !s.to_ascii_lowercase().starts_with(&needle) {
                continue;
            }
            out.push(CompletionItem {
                label: String::from_utf8_lossy(s).into_owned(),
                kind: Some(CompletionItemKind::ENUM_MEMBER),
                ..CompletionItem::default()
            });
        }
    } else {
        for (name, declaring_class) in meta.appearing_property_ids.iter() {
            let visible = mago_bytes::trim_start_byte(name.as_bytes(), b'$');
            if !visible.to_ascii_lowercase().starts_with(&needle) {
                continue;
            }
            let detail = codebase
                .get_class_like(declaring_class.as_bytes())
                .and_then(|c| c.properties.get(name))
                .and_then(|p| p.type_metadata.as_ref())
                .map(|t| String::from_utf8_lossy(t.type_union.get_id().as_bytes()).into_owned());
            out.push(CompletionItem {
                label: String::from_utf8_lossy(visible).into_owned(),
                kind: Some(CompletionItemKind::FIELD),
                detail,
                ..CompletionItem::default()
            });
        }
    }

    for (name, mid) in meta.appearing_method_ids.iter() {
        if !name.as_bytes().to_ascii_lowercase().starts_with(&needle) {
            continue;
        }
        let Some(method) = codebase.get_method(mid.get_class_name().as_bytes(), mid.get_method_name().as_bytes())
        else {
            continue;
        };
        let display: &[u8] = method.original_name.as_bytes();
        let display_str = String::from_utf8_lossy(display).into_owned();
        out.push(CompletionItem {
            label: display_str.clone(),
            kind: Some(CompletionItemKind::METHOD),
            detail: Some(render_signature(method, display)),
            insert_text: Some(format!("{display_str}($1)")),
            insert_text_format: Some(InsertTextFormat::SNIPPET),
            ..CompletionItem::default()
        });
        if out.len() >= MAX_RESULTS {
            break;
        }
    }

    out
}

fn render_signature(meta: &FunctionLikeMetadata, name: &[u8]) -> String {
    use std::fmt::Write;
    let mut sig = String::with_capacity(64);
    let _ = write!(sig, "{}", BytesDisplay(name));
    sig.push('(');
    let mut first = true;
    for p in &meta.parameters {
        if !first {
            sig.push_str(", ");
        }
        first = false;
        if let Some(ty) = &p.type_metadata {
            let _ = write!(sig, "{}", BytesDisplay(ty.type_union.get_id().as_bytes()));
            sig.push(' ');
        }
        let _ = write!(sig, "{}", BytesDisplay(p.name.0.as_bytes()));
    }
    sig.push(')');
    if let Some(rt) = &meta.return_type_metadata {
        sig.push_str(": ");
        let _ = write!(sig, "{}", BytesDisplay(rt.type_union.get_id().as_bytes()));
    }
    sig
}

fn push_unique(out: &mut Vec<CompletionItem>, seen: &mut foldhash::HashSet<String>, items: Vec<CompletionItem>) {
    for item in items {
        if seen.insert(item.label.clone()) {
            out.push(item);
        }
    }
}

fn make_class_item(meta: &ClassLikeMetadata, display: &[u8]) -> CompletionItem {
    use mago_codex::symbol::SymbolKind as M;
    let kind = match meta.kind {
        M::Class => CompletionItemKind::CLASS,
        M::Interface => CompletionItemKind::INTERFACE,
        M::Trait => CompletionItemKind::CLASS,
        M::Enum => CompletionItemKind::ENUM,
    };
    CompletionItem {
        label: String::from_utf8_lossy(display).into_owned(),
        kind: Some(kind),
        documentation: Some(Documentation::MarkupContent(MarkupContent {
            kind: MarkupKind::PlainText,
            value: String::from_utf8_lossy(meta.original_name.as_bytes()).into_owned(),
        })),
        ..CompletionItem::default()
    }
}

fn is_user_symbol(database: &Database<'_>, span: Span) -> bool {
    database.get(&span.file_id).map(|f| !matches!(f.file_type, FileType::Builtin)).unwrap_or(false)
}

fn local_name(full: &[u8]) -> &[u8] {
    match memchr::memrchr(b'\\', full) {
        Some(i) => &full[i + 1..],
        None => full,
    }
}