use std::collections::HashSet;
use tower_lsp::lsp_types::*;
use crate::Backend;
use crate::util::{short_name, strip_fqn_prefix};
use crate::completion::builder::{
analyze_use_block, build_callable_label, build_callable_snippet, deprecation_tag,
};
use crate::completion::resolve::CompletionItemData;
use crate::completion::use_edit::build_use_function_edit;
struct FunctionItemBuilder {
label: String,
insert_text: String,
filter_text: String,
sort_text: String,
fqn: String,
uri: String,
return_type: Option<String>,
insert_text_format: Option<InsertTextFormat>,
is_deprecated: bool,
additional_text_edits: Option<Vec<TextEdit>>,
}
impl FunctionItemBuilder {
fn new(
label: String,
insert_text: String,
filter_text: String,
sort_text: String,
fqn: String,
uri: String,
) -> Self {
Self {
label,
insert_text,
filter_text,
sort_text,
fqn,
uri,
return_type: None,
insert_text_format: None,
is_deprecated: false,
additional_text_edits: None,
}
}
fn return_type(mut self, rt: Option<String>) -> Self {
self.return_type = rt;
self
}
fn snippet(mut self) -> Self {
self.insert_text_format = Some(InsertTextFormat::SNIPPET);
self
}
fn deprecated(mut self, is_deprecated: bool) -> Self {
self.is_deprecated = is_deprecated;
self
}
fn additional_edits(mut self, edits: Option<Vec<TextEdit>>) -> Self {
self.additional_text_edits = edits;
self
}
fn build(self) -> CompletionItem {
let detail = self.return_type;
let data = serde_json::to_value(CompletionItemData {
class_name: String::new(),
member_name: self.fqn,
kind: "function".to_string(),
uri: self.uri,
extra_class_names: vec![],
})
.ok();
CompletionItem {
label: self.label,
kind: Some(CompletionItemKind::FUNCTION),
detail,
insert_text: Some(self.insert_text),
insert_text_format: self.insert_text_format,
filter_text: Some(self.filter_text),
sort_text: Some(self.sort_text),
tags: deprecation_tag(self.is_deprecated),
additional_text_edits: self.additional_text_edits,
data,
..CompletionItem::default()
}
}
}
fn build_use_import_item(
label: String,
fqn: &str,
sort_prefix: &str,
is_deprecated: bool,
uri: &str,
) -> CompletionItem {
let data = serde_json::to_value(CompletionItemData {
class_name: String::new(),
member_name: fqn.to_string(),
kind: "function".to_string(),
uri: uri.to_string(),
extra_class_names: vec![],
})
.ok();
CompletionItem {
label,
kind: Some(CompletionItemKind::FUNCTION),
insert_text: Some(fqn.to_string()),
filter_text: Some(fqn.to_string()),
sort_text: Some(format!("{}_{}", sort_prefix, fqn.to_lowercase())),
tags: deprecation_tag(is_deprecated),
data,
..CompletionItem::default()
}
}
impl Backend {
const MAX_FUNCTION_COMPLETIONS: usize = 100;
pub(crate) fn build_function_completions(
&self,
prefix: &str,
for_use_import: bool,
content: Option<&str>,
file_namespace: &Option<String>,
uri: &str,
) -> (Vec<CompletionItem>, bool) {
let prefix_lower = strip_fqn_prefix(prefix).to_lowercase();
let mut seen: HashSet<String> = HashSet::new();
let mut items: Vec<CompletionItem> = Vec::new();
let use_block = content.map(analyze_use_block);
{
let fmap = self.global_functions.read();
for (key, (_uri, info)) in fmap.iter() {
if !key.to_lowercase().contains(&prefix_lower)
&& !info.name.to_lowercase().contains(&prefix_lower)
{
continue;
}
if !seen.insert(key.clone()) {
continue;
}
let is_namespaced = info.namespace.is_some();
let fqn = key.clone();
let is_deprecated = info.deprecation_message.is_some();
let return_type = info
.return_type
.as_ref()
.or(info.native_return_type.as_ref())
.map(crate::hover::shorten_php_type);
if for_use_import {
let label = if is_namespaced {
fqn.clone()
} else {
build_callable_label(&info.name, &info.parameters)
};
items.push(build_use_import_item(label, &fqn, "4", is_deprecated, uri));
} else {
let label = build_callable_label(&info.name, &info.parameters);
let same_ns = file_namespace
.as_ref()
.zip(info.namespace.as_ref())
.is_some_and(|(file_ns, func_ns)| file_ns.eq_ignore_ascii_case(func_ns));
let additional_text_edits = if is_namespaced && !same_ns {
use_block
.as_ref()
.and_then(|ub| build_use_function_edit(&fqn, ub))
} else {
None
};
items.push(
FunctionItemBuilder::new(
label,
build_callable_snippet(&info.name, &info.parameters),
info.name.clone(),
format!("4_{}", info.name.to_lowercase()),
fqn.clone(),
uri.to_string(),
)
.return_type(return_type)
.snippet()
.deprecated(is_deprecated)
.additional_edits(additional_text_edits)
.build(),
);
}
}
}
{
let idx = self.autoload_function_index.read();
for (fqn, _path) in idx.iter() {
if !fqn.to_lowercase().contains(&prefix_lower) {
continue;
}
if !seen.insert(fqn.clone()) {
continue;
}
let is_namespaced = fqn.contains('\\');
let sn = if is_namespaced {
short_name(fqn)
} else {
fqn.as_str()
};
if for_use_import {
items.push(build_use_import_item(fqn.clone(), fqn, "4", false, uri));
} else {
let additional_text_edits = if is_namespaced {
use_block
.as_ref()
.and_then(|ub| build_use_function_edit(fqn, ub))
} else {
None
};
items.push(
FunctionItemBuilder::new(
sn.to_string(),
format!("{sn}()$0"),
sn.to_string(),
format!("4_{}", sn.to_lowercase()),
fqn.clone(),
uri.to_string(),
)
.snippet()
.additional_edits(additional_text_edits)
.build(),
);
}
}
}
let stub_fn_idx = self.stub_function_index.read();
for &name in stub_fn_idx.keys() {
if !name.to_lowercase().contains(&prefix_lower) {
continue;
}
if !seen.insert(name.to_string()) {
continue;
}
let is_namespaced = name.contains('\\');
let sn = if is_namespaced {
short_name(name)
} else {
name
};
if for_use_import {
items.push(build_use_import_item(
name.to_string(),
name,
"5",
false,
uri,
));
} else {
let additional_text_edits = if is_namespaced {
use_block
.as_ref()
.and_then(|ub| build_use_function_edit(name, ub))
} else {
None
};
items.push(
FunctionItemBuilder::new(
sn.to_string(),
format!("{sn}()$0"),
sn.to_string(),
format!("5_{}", sn.to_lowercase()),
name.to_string(),
uri.to_string(),
)
.snippet()
.additional_edits(additional_text_edits)
.build(),
);
}
}
let is_incomplete = items.len() > Self::MAX_FUNCTION_COMPLETIONS;
if is_incomplete {
items.sort_by(|a, b| a.sort_text.cmp(&b.sort_text));
items.truncate(Self::MAX_FUNCTION_COMPLETIONS);
}
(items, is_incomplete)
}
}