use std::collections::HashMap;
use tower_lsp::lsp_types::*;
use tree_sitter::{Point, Tree};
use crate::cursor_context::{self, CursorContext};
use crate::file_analysis::{
contains_point, format_inferred_type, CompletionCandidate, FileAnalysis, FoldKind,
HandlerOwner, InferredType, OutlineSymbol, ParamInfo, RefKind, Span,
SymKind as FaSymKind, SymbolDetail, PRIORITY_AUTO_ADD_QW, PRIORITY_BARE_IMPORT,
PRIORITY_EXPLICIT_IMPORT, PRIORITY_UNIMPORTED,
};
use crate::module_index::{CachedModule, ModuleIndex, SubInfo};
use std::sync::Arc;
fn point_to_position(p: Point) -> Position {
Position {
line: p.row as u32,
character: p.column as u32,
}
}
pub fn position_to_point(pos: Position) -> Point {
Point::new(pos.line as usize, pos.character as usize)
}
pub fn span_to_range(span: Span) -> Range {
Range {
start: point_to_position(span.start),
end: point_to_position(span.end),
}
}
fn fa_sym_kind_to_lsp(kind: &FaSymKind) -> SymbolKind {
match kind {
FaSymKind::Sub => SymbolKind::FUNCTION,
FaSymKind::Method => SymbolKind::METHOD,
FaSymKind::Variable | FaSymKind::Field => SymbolKind::VARIABLE,
FaSymKind::Package => SymbolKind::NAMESPACE,
FaSymKind::Class => SymbolKind::CLASS,
FaSymKind::Module => SymbolKind::MODULE,
FaSymKind::HashKeyDef => SymbolKind::KEY,
FaSymKind::Handler => SymbolKind::EVENT,
FaSymKind::Namespace => SymbolKind::NAMESPACE,
}
}
fn handler_display_to_symbol_kind(d: &crate::file_analysis::HandlerDisplay) -> SymbolKind {
use crate::file_analysis::HandlerDisplay as H;
match d {
H::Event => SymbolKind::EVENT,
H::Method => SymbolKind::METHOD,
H::Function => SymbolKind::FUNCTION,
H::Field => SymbolKind::FIELD,
H::Property => SymbolKind::PROPERTY,
H::Constant => SymbolKind::CONSTANT,
H::Helper | H::Route | H::Task | H::Action => SymbolKind::FUNCTION,
}
}
fn handler_display_to_completion_kind(d: &crate::file_analysis::HandlerDisplay) -> CompletionItemKind {
use crate::file_analysis::HandlerDisplay as H;
match d {
H::Event => CompletionItemKind::EVENT,
H::Method => CompletionItemKind::METHOD,
H::Function => CompletionItemKind::FUNCTION,
H::Field => CompletionItemKind::FIELD,
H::Property => CompletionItemKind::PROPERTY,
H::Constant => CompletionItemKind::CONSTANT,
H::Helper | H::Route | H::Task | H::Action => CompletionItemKind::FUNCTION,
}
}
pub fn outline_lsp_kind(s: &OutlineSymbol) -> SymbolKind {
match s.handler_display {
Some(ref d) => handler_display_to_symbol_kind(d),
None => fa_sym_kind_to_lsp(&s.kind),
}
}
#[allow(deprecated)]
fn outline_to_document_symbol(s: &OutlineSymbol) -> DocumentSymbol {
let children: Vec<DocumentSymbol> = s.children.iter().map(outline_to_document_symbol).collect();
let kind = outline_lsp_kind(s);
DocumentSymbol {
name: s.name.clone(),
detail: s.detail.clone(),
kind,
tags: None,
deprecated: None,
range: span_to_range(s.span),
selection_range: span_to_range(s.selection_span),
children: if children.is_empty() {
None
} else {
Some(children)
},
}
}
#[allow(deprecated)]
pub fn extract_symbols(analysis: &FileAnalysis) -> Vec<DocumentSymbol> {
analysis.document_symbols()
.iter()
.map(outline_to_document_symbol)
.collect()
}
#[allow(deprecated)]
#[allow(deprecated)]
pub fn plugin_namespace_to_workspace_info(
ns: &crate::file_analysis::PluginNamespace,
uri: Url,
) -> SymbolInformation {
SymbolInformation {
name: format!("[{}] {}", ns.kind, ns.id),
kind: SymbolKind::NAMESPACE,
tags: None,
deprecated: None,
location: Location {
uri,
range: span_to_range(ns.decl_span),
},
container_name: Some(ns.plugin_id.clone()),
}
}
#[allow(deprecated)]
pub fn symbol_to_workspace_info(sym: &crate::file_analysis::Symbol, uri: Url) -> Option<SymbolInformation> {
use crate::file_analysis::SymKind as FaSymKind;
match sym.kind {
FaSymKind::Sub | FaSymKind::Method | FaSymKind::Package | FaSymKind::Class => {}
_ => return None,
}
Some(SymbolInformation {
name: sym.name.clone(),
kind: fa_sym_kind_to_lsp(&sym.kind),
tags: None,
deprecated: None,
location: Location {
uri,
range: span_to_range(sym.selection_span),
},
container_name: sym.package.clone(),
})
}
pub fn find_definition(
analysis: &FileAnalysis,
pos: Position,
uri: &Url,
module_index: &ModuleIndex,
tree: &Tree,
source: &str,
) -> Option<GotoDefinitionResponse> {
let point = position_to_point(pos);
if let Some(span) = analysis.find_definition(point, Some(tree), Some(source.as_bytes()), Some(module_index)) {
return Some(GotoDefinitionResponse::Scalar(Location {
uri: uri.clone(),
range: span_to_range(span),
}));
}
if let Some(r) = analysis.ref_at(point) {
if matches!(r.kind, RefKind::FunctionCall { .. }) {
if let Some((import, module_path, remote_name)) =
resolve_imported_function(analysis, &r.target_name, module_index)
{
if let Ok(module_uri) = Url::from_file_path(&module_path) {
let def_line = module_index
.get_cached(&import.module_name)
.and_then(|cached| cached.sub_info(&remote_name).map(|s| s.def_line()));
let def_range = match def_line {
Some(line) => Range {
start: Position { line, character: 0 },
end: Position { line, character: 0 },
},
None => Range::default(),
};
let pm_location = Location {
uri: module_uri,
range: def_range,
};
if contains_point(&import.span, point) {
return Some(GotoDefinitionResponse::Scalar(pm_location));
}
return Some(GotoDefinitionResponse::Array(vec![
Location {
uri: uri.clone(),
range: span_to_range(import.span),
},
pm_location,
]));
}
return Some(GotoDefinitionResponse::Scalar(Location {
uri: uri.clone(),
range: span_to_range(import.span),
}));
}
}
if matches!(r.kind, RefKind::PackageRef) {
if let Some(path) = module_index.module_path_cached(&r.target_name) {
if let Ok(module_uri) = Url::from_file_path(&path) {
return Some(GotoDefinitionResponse::Scalar(Location {
uri: module_uri,
range: Range::default(),
}));
}
}
}
if let RefKind::DispatchCall { owner: Some(owner), .. } = &r.kind {
use crate::file_analysis::SymbolDetail;
let mut locs: Vec<Location> = Vec::new();
for module_name in module_index.modules_with_symbol(&r.target_name) {
let Some(cached) = module_index.get_cached(&module_name) else { continue };
for sym in &cached.analysis.symbols {
if sym.name != r.target_name { continue; }
if let SymbolDetail::Handler { owner: o, .. } = &sym.detail {
if o == owner {
if let Ok(module_uri) = Url::from_file_path(&cached.path) {
locs.push(Location {
uri: module_uri,
range: span_to_range(sym.selection_span),
});
}
}
}
}
}
if !locs.is_empty() {
return Some(if locs.len() == 1 {
GotoDefinitionResponse::Scalar(locs.into_iter().next().unwrap())
} else {
GotoDefinitionResponse::Array(locs)
});
}
}
if matches!(r.kind, RefKind::MethodCall { .. }) {
use crate::file_analysis::MethodResolution;
let class_name = analysis.method_call_invocant_class(r, Some(module_index));
if let Some(ref cn) = class_name {
if let Some(MethodResolution::CrossFile { ref class }) = analysis.resolve_method_in_ancestors(cn, &r.target_name, Some(module_index)) {
if let Some(cached) = module_index.get_cached(class) {
if let Some(sub_info) = cached.sub_info(&r.target_name) {
if let Ok(module_uri) = Url::from_file_path(&cached.path) {
let line = sub_info.def_line();
let def_range = Range {
start: Position { line, character: 0 },
end: Position { line, character: 0 },
};
return Some(GotoDefinitionResponse::Scalar(Location {
uri: module_uri,
range: def_range,
}));
}
}
}
}
}
}
}
None
}
pub fn find_references(analysis: &FileAnalysis, pos: Position, uri: &Url, tree: &Tree, source: &str, module_index: Option<&ModuleIndex>) -> Vec<Location> {
analysis.find_references(position_to_point(pos), Some(tree), Some(source.as_bytes()), module_index)
.into_iter()
.map(|span| Location {
uri: uri.clone(),
range: span_to_range(span),
})
.collect()
}
pub fn rename(
analysis: &FileAnalysis,
pos: Position,
uri: &Url,
new_name: &str,
tree: Option<&tree_sitter::Tree>,
source: Option<&str>,
) -> Option<WorkspaceEdit> {
let edits = analysis.rename_at(
position_to_point(pos), new_name, tree, source.map(|s| s.as_bytes()),
)?;
let text_edits: Vec<TextEdit> = edits
.into_iter()
.map(|(span, new_text)| TextEdit {
range: span_to_range(span),
new_text,
})
.collect();
let mut changes = HashMap::new();
changes.insert(uri.clone(), text_edits);
Some(WorkspaceEdit {
changes: Some(changes),
..Default::default()
})
}
fn fa_completion_kind(kind: &FaSymKind) -> CompletionItemKind {
match kind {
FaSymKind::Sub => CompletionItemKind::FUNCTION,
FaSymKind::Method => CompletionItemKind::METHOD,
FaSymKind::Variable | FaSymKind::Field => CompletionItemKind::VARIABLE,
FaSymKind::Package => CompletionItemKind::CLASS,
FaSymKind::Class => CompletionItemKind::CLASS,
FaSymKind::Module => CompletionItemKind::MODULE,
FaSymKind::HashKeyDef => CompletionItemKind::PROPERTY,
FaSymKind::Handler => CompletionItemKind::EVENT,
FaSymKind::Namespace => CompletionItemKind::MODULE,
}
}
fn candidate_to_completion_item(c: CompletionCandidate) -> CompletionItem {
let additional_text_edits = if c.additional_edits.is_empty() {
None
} else {
Some(
c.additional_edits
.iter()
.map(|(span, text)| TextEdit {
range: span_to_range(*span),
new_text: text.clone(),
})
.collect(),
)
};
let filter_text = Some(c.label.clone());
let sort_text = if matches!(c.kind, FaSymKind::Handler) {
Some(format!(" {:03}{}", c.sort_priority, c.label))
} else {
Some(format!("{:03}", c.sort_priority))
};
let kind = if let Some(ref d) = c.display_override {
handler_display_to_completion_kind(d)
} else {
fa_completion_kind(&c.kind)
};
CompletionItem {
label: c.label,
kind: Some(kind),
detail: c.detail,
insert_text: c.insert_text,
filter_text,
sort_text,
additional_text_edits,
..Default::default()
}
}
pub fn completion_items(
analysis: &FileAnalysis,
tree: &Tree,
source: &str,
pos: Position,
module_index: &ModuleIndex,
stable_packages: Option<&[(String, usize)]>,
) -> Vec<CompletionItem> {
let point = position_to_point(pos);
if let Some(qctx) = cursor_context::build_plugin_query_context(analysis, tree, source.as_bytes(), point) {
let registry = crate::builder::default_plugin_registry();
let (uses, parents) = analysis.trigger_view_at(point);
let query = crate::plugin::TriggerQuery {
package_uses: &uses,
package_parents: &parents,
};
let mut plugin_items: Vec<CompletionItem> = Vec::new();
let mut exclusive = false;
for p in registry.applicable(&query) {
if let Some(answer) = p.on_completion(&qctx) {
if answer.exclusive { exclusive = true; }
for c in answer.items {
plugin_items.push(plugin_completion_to_item(c));
}
if let Some(req) = answer.dispatch_targets_for {
plugin_items.extend(dispatch_target_items_for(
analysis, module_index, &req.owner_class, &req.dispatcher_names,
));
}
}
}
if exclusive {
return plugin_items;
}
if !plugin_items.is_empty() {
let native = completion_items_native(analysis, tree, source, pos, module_index, stable_packages);
let mut out = plugin_items;
out.extend(native);
return out;
}
}
completion_items_native(analysis, tree, source, pos, module_index, stable_packages)
}
fn completion_items_native(
analysis: &FileAnalysis,
tree: &Tree,
source: &str,
pos: Position,
module_index: &ModuleIndex,
stable_packages: Option<&[(String, usize)]>,
) -> Vec<CompletionItem> {
let point = position_to_point(pos);
let ctx = cursor_context::detect_cursor_context_tree_with_index(
tree, source.as_bytes(), point, analysis, Some(module_index),
)
.unwrap_or_else(|| cursor_context::detect_cursor_context(source, point, Some(analysis)));
if let Some(refs) = refs_at_point_matching(analysis, point, |r|
matches!(r.kind, RefKind::MethodCall { .. })
) {
for r in &refs {
if let RefKind::MethodCall { invocant, .. } = &r.kind {
let early = mid_string_methodref_completions(
analysis, module_index, invocant, source, point, r.span,
);
if !early.is_empty() {
return early;
}
}
}
}
let mut dispatch_items: Vec<CompletionItem> = Vec::new();
let mut candidates: Vec<CompletionCandidate> = Vec::new();
let mut suppress_firehose = false;
if let Some(call_ctx) = cursor_context::find_call_context(tree, source.as_bytes(), point) {
if call_ctx.is_method {
let dispatch_class = analysis.invocant_text_to_class(call_ctx.invocant.as_deref(), point);
let has_any_handlers = dispatch_class.as_ref().is_some_and(|c|
class_has_dispatch_handlers(analysis, module_index, c, &call_ctx.name)
);
log::debug!(
"completion dispatch: method={:?} invocant={:?} class={:?} active_param={} has_handlers={}",
call_ctx.name, call_ctx.invocant, dispatch_class,
call_ctx.active_param, has_any_handlers,
);
if call_ctx.active_param == 0 && has_any_handlers {
let dispatch_cands = dispatch_target_completions(
analysis,
module_index,
call_ctx.invocant.as_deref(),
&call_ctx.name,
point,
tree,
);
dispatch_items.extend(
dispatch_cands.into_iter().map(candidate_to_completion_item),
);
if let Some(span) = string_content_span_at(tree, point) {
retarget_items_to_span(&mut dispatch_items, span);
}
suppress_firehose = true;
} else if call_ctx.active_param > 0 && has_any_handlers
&& !matches!(ctx, CursorContext::HashKey { .. })
{
let vars_only: Vec<CompletionCandidate> = analysis.complete_general(point)
.into_iter()
.filter(|c| matches!(c.kind, FaSymKind::Variable | FaSymKind::Field))
.collect();
candidates.extend(vars_only);
return candidates.drain(..).map(candidate_to_completion_item).collect();
}
}
}
candidates.extend::<Vec<CompletionCandidate>>(match ctx {
CursorContext::Variable { sigil } => analysis.complete_variables(point, sigil),
CursorContext::Method { ref invocant_type, ref invocant_text } => {
if let Some(ref ty) = invocant_type {
if let Some(cn) = ty.class_name() {
analysis.complete_methods_for_class(cn, Some(module_index))
} else {
Vec::new()
}
} else {
analysis.complete_methods(invocant_text, point)
}
}
CursorContext::HashKey { ref owner_type, ref var_text, ref source_sub } => {
let used = cursor_context::used_keys_in_enclosing_hash(tree, source.as_bytes(), point);
let class_name = owner_type.as_ref().and_then(|t| t.class_name());
let candidates = if let Some(cn) = class_name {
analysis.complete_hash_keys_for_class(cn, point)
} else if let Some(ref sub_name) = source_sub {
analysis.complete_hash_keys_for_sub(sub_name, point)
} else {
analysis.complete_hash_keys(var_text, point)
};
candidates.into_iter().filter(|c| !used.contains(&c.label)).collect()
}
CursorContext::UseStatement { ref module_prefix, in_import_list, ref module_name } => {
if in_import_list {
if let Some(ref name) = module_name {
return complete_import_list(name, module_index);
}
} else {
return complete_module_names(module_prefix, module_index);
}
Vec::new()
}
CursorContext::QualifiedPath { ref package } => {
return qualified_path_completions(analysis, module_index, package);
}
CursorContext::General => {
let mut items = Vec::new();
if let Some(call_ctx) =
cursor_context::find_call_context(tree, source.as_bytes(), point)
{
if call_ctx.at_key_position {
items.extend(analysis.complete_keyval_args(
&call_ctx.name,
call_ctx.is_method,
call_ctx.invocant.as_deref(),
point,
&call_ctx.used_keys,
Some(module_index),
));
}
}
items.extend(analysis.complete_general(point));
if !suppress_firehose {
items.extend(imported_function_completions(analysis, module_index));
items.extend(unimported_function_completions(analysis, module_index, point, stable_packages));
}
items
}
});
let mut items: Vec<CompletionItem> = candidates
.drain(..)
.map(candidate_to_completion_item)
.collect();
if !dispatch_items.is_empty() {
let mut with_dispatch = dispatch_items;
with_dispatch.extend(items);
items = with_dispatch;
}
if let CursorContext::Method { ref invocant_type, .. } = ctx {
if let Some(ref ty) = invocant_type {
if !ty.is_object() {
items.extend(ref_type_snippet_completions(ty));
}
}
}
items
}
fn complete_module_names(prefix: &str, module_index: &ModuleIndex) -> Vec<CompletionItem> {
let modules = module_index.complete_module_names(prefix);
modules.into_iter().map(|(name, is_resolved)| {
let (detail, priority) = if is_resolved {
(Some("indexed".to_string()), 10u8)
} else {
(Some("available".to_string()), 50u8)
};
CompletionItem {
label: name.clone(),
kind: Some(CompletionItemKind::MODULE),
detail,
sort_text: Some(format!("{:03}{}", priority, name)),
..Default::default()
}
}).collect()
}
fn complete_import_list(module_name: &str, module_index: &ModuleIndex) -> Vec<CompletionItem> {
let cached: Arc<CachedModule> = match module_index.get_cached(module_name) {
Some(c) => c,
None => return vec![CompletionItem {
label: format!("loading {}...", module_name),
kind: Some(CompletionItemKind::TEXT),
detail: Some("Module is being indexed".to_string()),
insert_text: Some(String::new()),
sort_text: Some("999".to_string()),
..Default::default()
}],
};
let mut items = Vec::new();
let mut seen = std::collections::HashSet::new();
for name in &cached.analysis.export {
if seen.insert(name.clone()) {
let detail = cached.sub_info(name)
.and_then(|s| s.return_type())
.map(|rt| format!("@EXPORT → {}", format_inferred_type(&rt)))
.or(Some("@EXPORT".to_string()));
items.push(CompletionItem {
label: name.clone(),
kind: Some(CompletionItemKind::FUNCTION),
detail,
sort_text: Some(format!("010{}", name)),
..Default::default()
});
}
}
for name in &cached.analysis.export_ok {
if seen.insert(name.clone()) {
let detail = cached.sub_info(name)
.and_then(|s| s.return_type())
.map(|rt| format!("→ {}", format_inferred_type(&rt)));
items.push(CompletionItem {
label: name.clone(),
kind: Some(CompletionItemKind::FUNCTION),
detail,
sort_text: Some(format!("020{}", name)),
..Default::default()
});
}
}
items
}
fn qualified_path_completions(
analysis: &FileAnalysis,
module_index: &ModuleIndex,
package: &str,
) -> Vec<CompletionItem> {
let mut seen: std::collections::HashSet<String> = std::collections::HashSet::new();
let mut items: Vec<CompletionItem> = Vec::new();
for c in analysis.complete_methods_for_class(package, Some(module_index)) {
if !seen.insert(c.label.clone()) {
continue;
}
items.push(CompletionItem {
label: c.label.clone(),
kind: Some(CompletionItemKind::FUNCTION),
detail: c.detail.clone().or_else(|| Some(format!("from {}", package))),
sort_text: Some(format!("010{}", c.label)),
insert_text: Some(c.label),
..Default::default()
});
}
let prefix = format!("{}::", package);
let mut subpaths: Vec<(String, &'static str)> = Vec::new();
for (name, is_resolved) in module_index.complete_module_names(&prefix) {
let hint = if is_resolved { "indexed" } else { "available" };
subpaths.push((name, hint));
}
for sym in &analysis.symbols {
if !matches!(sym.kind, FaSymKind::Package | FaSymKind::Class) {
continue;
}
if sym.name.starts_with(&prefix) && sym.name != package {
subpaths.push((sym.name.clone(), "in-file"));
}
}
for (name, hint) in subpaths {
let suffix = match name.strip_prefix(&prefix) {
Some(s) if !s.is_empty() => s.to_string(),
_ => continue,
};
if !seen.insert(suffix.clone()) {
continue;
}
items.push(CompletionItem {
label: suffix.clone(),
kind: Some(CompletionItemKind::MODULE),
detail: Some(hint.to_string()),
sort_text: Some(format!("020{}", suffix)),
insert_text: Some(suffix),
..Default::default()
});
}
items
}
fn ref_type_snippet_completions(ty: &InferredType) -> Vec<CompletionItem> {
match ty {
InferredType::ArrayRef => vec![CompletionItem {
label: "[index]".to_string(),
kind: Some(CompletionItemKind::SNIPPET),
detail: Some("array dereference".to_string()),
insert_text: Some("[$0]".to_string()),
insert_text_format: Some(InsertTextFormat::SNIPPET),
sort_text: Some("000".to_string()),
..Default::default()
}],
InferredType::CodeRef { .. } => vec![CompletionItem {
label: "(args)".to_string(),
kind: Some(CompletionItemKind::SNIPPET),
detail: Some("code dereference".to_string()),
insert_text: Some("($0)".to_string()),
insert_text_format: Some(InsertTextFormat::SNIPPET),
sort_text: Some("000".to_string()),
..Default::default()
}],
InferredType::HashRef => vec![CompletionItem {
label: "{key}".to_string(),
kind: Some(CompletionItemKind::SNIPPET),
detail: Some("hash dereference".to_string()),
insert_text: Some("{$0}".to_string()),
insert_text_format: Some(InsertTextFormat::SNIPPET),
sort_text: Some("000".to_string()),
..Default::default()
}],
_ => Vec::new(),
}
}
pub fn hover_info(
analysis: &FileAnalysis,
source: &str,
pos: Position,
module_index: &ModuleIndex,
tree: &Tree,
) -> Option<Hover> {
let point = position_to_point(pos);
if let Some(markdown) = analysis.hover_info(point, source, Some(tree), Some(module_index)) {
return Some(Hover {
contents: HoverContents::Markup(MarkupContent {
kind: MarkupKind::Markdown,
value: markdown,
}),
range: None,
});
}
if let Some(r) = analysis.ref_at(point) {
if matches!(r.kind, RefKind::FunctionCall { .. }) {
if is_perl_builtin(&r.target_name) {
if let Some(markdown) = module_index.builtin_doc(&r.target_name) {
return Some(Hover {
contents: HoverContents::Markup(MarkupContent {
kind: MarkupKind::Markdown,
value: markdown,
}),
range: None,
});
}
}
if let Some((import, _path, remote_name)) =
resolve_imported_function(analysis, &r.target_name, module_index)
{
let mut parts = Vec::new();
if let Some(cached) = module_index.get_cached(&import.module_name) {
if let Some(sub_info) = cached.sub_info(&remote_name) {
let sig = format_imported_signature(&r.target_name, &sub_info);
parts.push(format!("```perl\n{}\n```", sig));
if let Some(doc) = sub_info.doc() {
parts.push(doc.to_string());
}
}
}
if remote_name != r.target_name {
parts.push(format!(
"*imported from `{}` (as `{}`)*",
import.module_name, remote_name
));
} else {
parts.push(format!("*imported from `{}`*", import.module_name));
}
let markdown = parts.join("\n\n");
return Some(Hover {
contents: HoverContents::Markup(MarkupContent {
kind: MarkupKind::Markdown,
value: markdown,
}),
range: None,
});
}
}
}
None
}
pub fn document_highlights(analysis: &FileAnalysis, pos: Position, tree: &tree_sitter::Tree, source: &str, module_index: Option<&ModuleIndex>) -> Vec<DocumentHighlight> {
use crate::file_analysis::AccessKind;
analysis.find_highlights(position_to_point(pos), Some(tree), Some(source.as_bytes()), module_index)
.into_iter()
.map(|(span, access)| DocumentHighlight {
range: span_to_range(span),
kind: Some(match access {
AccessKind::Write => DocumentHighlightKind::WRITE,
_ => DocumentHighlightKind::READ,
}),
})
.collect()
}
pub fn selection_ranges(tree: &Tree, pos: Position) -> SelectionRange {
let spans = cursor_context::selection_ranges(tree, position_to_point(pos));
let mut result: Option<SelectionRange> = None;
for span in spans.into_iter().rev() {
result = Some(SelectionRange {
range: span_to_range(span),
parent: result.map(Box::new),
});
}
result.unwrap_or(SelectionRange {
range: Range::default(),
parent: None,
})
}
pub fn folding_ranges(analysis: &FileAnalysis) -> Vec<FoldingRange> {
analysis.fold_ranges
.iter()
.map(|f| FoldingRange {
start_line: f.start_line as u32,
start_character: None,
end_line: f.end_line as u32,
end_character: None,
kind: Some(match f.kind {
FoldKind::Region => FoldingRangeKind::Region,
FoldKind::Comment => FoldingRangeKind::Comment,
}),
collapsed_text: None,
})
.collect()
}
fn class_has_dispatch_handlers(
analysis: &FileAnalysis,
module_index: &ModuleIndex,
class: &str,
dispatcher: &str,
) -> bool {
let mut found = false;
analysis.for_each_dispatch_handler_on_class(
class,
dispatcher,
Some(module_index),
|_sym, _prov| { found = true; },
);
found
}
fn string_content_span_at(tree: &Tree, point: Point) -> Option<Span> {
let mut node = tree.root_node().descendant_for_point_range(point, point)?;
for _ in 0..4 {
match node.kind() {
"string_content" => {
return Some(Span {
start: node.start_position(),
end: node.end_position(),
});
}
"string_literal" | "interpolated_string_literal" => {
let mut walker = node.walk();
for child in node.named_children(&mut walker) {
if child.kind() == "string_content" {
return Some(Span {
start: child.start_position(),
end: child.end_position(),
});
}
}
return Some(Span { start: point, end: point });
}
_ => {}
}
let Some(p) = node.parent() else { break };
node = p;
}
None
}
fn retarget_items_to_span(items: &mut [CompletionItem], span: Span) {
let range = span_to_range(span);
for item in items {
item.text_edit = Some(tower_lsp::lsp_types::CompletionTextEdit::Edit(
tower_lsp::lsp_types::TextEdit { range, new_text: item.label.clone() },
));
item.insert_text = None;
}
}
fn cursor_in_string_or_autoquote(tree: &Tree, point: Point) -> bool {
let Some(mut node) = tree.root_node().descendant_for_point_range(point, point) else {
return false;
};
for _ in 0..4 {
match node.kind() {
"string_literal" | "interpolated_string_literal"
| "string_content" | "autoquoted_bareword" => return true,
_ => {}
}
let Some(p) = node.parent() else { break };
node = p;
}
false
}
fn dispatch_target_completions(
analysis: &FileAnalysis,
module_index: &ModuleIndex,
invocant: Option<&str>,
method_name: &str,
point: Point,
tree: &Tree,
) -> Vec<CompletionCandidate> {
let class = match analysis.invocant_text_to_class(invocant, point) {
Some(c) => c,
None => return Vec::new(),
};
let needs_quotes = !cursor_in_string_or_autoquote(tree, point);
let mut acc: std::collections::BTreeMap<String, (Vec<String>, String)> =
std::collections::BTreeMap::new();
analysis.for_each_dispatch_handler_on_class(
&class, method_name, Some(module_index),
|sym, provenance| {
let SymbolDetail::Handler { params, .. } = &sym.detail else { return };
let display: Vec<String> = params
.iter()
.filter(|p| !p.is_invocant)
.map(|p| p.name.clone())
.collect();
acc.entry(sym.name.clone()).or_insert((display, provenance.to_string()));
},
);
acc.into_iter().map(|(name, (params, provenance))| {
let detail = if params.is_empty() {
format!("handler on {} ({})", class, provenance)
} else {
format!("handler on {} ({}) — {}", class, params.join(", "), provenance)
};
CompletionCandidate {
label: name.clone(),
kind: FaSymKind::Handler,
detail: Some(detail),
insert_text: Some(if needs_quotes {
format!("'{}'", name)
} else {
name.clone()
}),
sort_priority: 0,
additional_edits: Vec::new(),
display_override: None,
}
}).collect()
}
fn refs_at_point_matching<'a>(
analysis: &'a FileAnalysis,
point: Point,
pred: impl Fn(&crate::file_analysis::Ref) -> bool,
) -> Option<Vec<&'a crate::file_analysis::Ref>> {
let out: Vec<&crate::file_analysis::Ref> = analysis.refs.iter()
.filter(|r| span_contains_point(&r.span, point) && pred(r))
.collect();
if out.is_empty() { None } else { Some(out) }
}
fn span_contains_point(span: &crate::file_analysis::Span, p: Point) -> bool {
let a = (span.start.row, span.start.column);
let b = (span.end.row, span.end.column);
let pp = (p.row, p.column);
a <= pp && pp <= b
}
fn mid_string_methodref_completions(
analysis: &FileAnalysis,
module_index: &ModuleIndex,
invocant_class: &str,
source: &str,
point: Point,
ref_span: crate::file_analysis::Span,
) -> Vec<CompletionItem> {
let lines: Vec<&str> = source.lines().collect();
if ref_span.start.row >= lines.len() || point.row >= lines.len() {
return Vec::new();
}
let typed = if ref_span.start.row == point.row {
let line = lines[point.row];
let start = ref_span.start.column.min(line.len());
let end = point.column.min(line.len());
&line[start..end]
} else {
let line = lines[point.row];
&line[..point.column.min(line.len())]
};
let candidates = analysis.complete_methods_for_class(invocant_class, Some(module_index));
let mut items: Vec<CompletionItem> = candidates
.into_iter()
.filter(|c| typed.is_empty() || c.label.starts_with(typed))
.map(|c| {
let mut item = candidate_to_completion_item(c);
item.sort_text = Some(format!("000{}", item.label));
item
})
.collect();
retarget_items_to_span(&mut items, ref_span);
items
}
fn dispatch_target_items_for(
analysis: &FileAnalysis,
module_index: &ModuleIndex,
owner_class: &str,
dispatcher_names: &[String],
) -> Vec<CompletionItem> {
use crate::file_analysis::SymbolDetail;
let mut seen: std::collections::HashSet<String> = std::collections::HashSet::new();
let mut out: Vec<CompletionItem> = Vec::new();
let mut emit = |sym: &crate::file_analysis::Symbol| {
let SymbolDetail::Handler { display, .. } = &sym.detail else { return };
if !seen.insert(sym.name.clone()) { return; }
let detail = display.outline_word().map(|s| s.to_string());
out.push(CompletionItem {
label: sym.name.clone(),
kind: Some(handler_display_to_completion_kind(display)),
detail,
filter_text: Some(sym.name.clone()),
sort_text: Some(format!(" 000{}", sym.name)),
insert_text: Some(format!("'{}'", sym.name)),
..Default::default()
});
};
for sym in analysis.handlers_for_owner(owner_class, dispatcher_names) {
emit(sym);
}
module_index.for_each_cached(|_, cached| {
for sym in cached.analysis.handlers_for_owner(owner_class, dispatcher_names) {
emit(sym);
}
});
out
}
fn plugin_sig_to_lsp(p: crate::plugin::PluginSignatureHelp) -> SignatureHelp {
let parameters: Vec<ParameterInformation> = p.params.iter().cloned()
.map(|label| ParameterInformation {
label: ParameterLabel::Simple(label),
documentation: None,
})
.collect();
SignatureHelp {
signatures: vec![SignatureInformation {
label: p.label,
documentation: None,
parameters: Some(parameters),
active_parameter: Some(p.active_param as u32),
}],
active_signature: Some(0),
active_parameter: Some(p.active_param as u32),
}
}
fn plugin_completion_kind_hint(h: &crate::plugin::CompletionKindHint) -> CompletionItemKind {
use crate::plugin::CompletionKindHint as K;
match h {
K::Function | K::Task | K::Helper | K::Route => CompletionItemKind::FUNCTION,
K::Method => CompletionItemKind::METHOD,
K::Field => CompletionItemKind::FIELD,
K::Property => CompletionItemKind::PROPERTY,
K::Value => CompletionItemKind::VALUE,
K::Event => CompletionItemKind::EVENT,
K::Operator => CompletionItemKind::OPERATOR,
K::Keyword => CompletionItemKind::KEYWORD,
}
}
fn plugin_completion_to_item(p: crate::plugin::PluginCompletion) -> CompletionItem {
let filter_text = Some(p.label.clone());
let kind = plugin_completion_kind_hint(&p.kind);
let detail = p.detail.or_else(|| match p.kind {
crate::plugin::CompletionKindHint::Task => Some("task".into()),
crate::plugin::CompletionKindHint::Helper => Some("helper".into()),
crate::plugin::CompletionKindHint::Route => Some("route".into()),
_ => None,
});
CompletionItem {
label: p.label,
kind: Some(kind),
detail,
insert_text: p.insert_text,
filter_text,
sort_text: Some(" 000".into()), ..Default::default()
}
}
fn dispatch_info_for_enclosing_call(
analysis: &FileAnalysis,
tree: &Tree,
_source: &[u8],
point: Point,
) -> Option<(String, String, String)> {
let mut node = tree.root_node().descendant_for_point_range(point, point)?;
let call = loop {
if node.kind() == "method_call_expression" {
break node;
}
node = node.parent()?;
};
let call_start = crate::file_analysis::Span {
start: call.start_position(),
end: call.end_position(),
};
for r in &analysis.refs {
let RefKind::DispatchCall { dispatcher, owner } = &r.kind else { continue };
if !span_contains_span(&call_start, &r.span) { continue; }
let Some(HandlerOwner::Class(class)) = owner.clone() else { continue };
return Some((r.target_name.clone(), class, dispatcher.clone()));
}
None
}
fn span_contains_span(outer: &crate::file_analysis::Span, inner: &crate::file_analysis::Span) -> bool {
let o_start = (outer.start.row, outer.start.column);
let o_end = (outer.end.row, outer.end.column);
let i_start = (inner.start.row, inner.start.column);
let i_end = (inner.end.row, inner.end.column);
o_start <= i_start && i_end <= o_end
}
fn string_dispatch_signature_for(
analysis: &FileAnalysis,
module_index: Option<&ModuleIndex>,
class: &str,
dispatcher: &str,
handler_name: &str,
active_param: usize,
) -> Option<SignatureHelp> {
let mut signatures: Vec<SignatureInformation> = Vec::new();
let push_sig = |signatures: &mut Vec<SignatureInformation>,
sym: &crate::file_analysis::Symbol,
provenance: Option<&str>| {
let SymbolDetail::Handler { owner, dispatchers, params, .. } = &sym.detail else { return };
let HandlerOwner::Class(n) = owner;
if n != class { return; }
let dispatcher_ok = dispatchers.is_empty()
|| dispatchers.iter().any(|d| d == dispatcher);
if !dispatcher_ok || params.is_empty() { return; }
let display: Vec<&ParamInfo> = params
.iter()
.filter(|p| !p.is_invocant)
.collect();
let labels: Vec<String> = display.iter()
.map(|p| match &p.default {
Some(d) => format!("{} = {}", p.name, d),
None => p.name.clone(),
})
.collect();
let parameters: Vec<ParameterInformation> = labels.iter()
.map(|l| ParameterInformation {
label: ParameterLabel::Simple(l.clone()),
documentation: None,
})
.collect();
let doc = match provenance {
Some(p) => format!(
"{} handler on `{}`, registered at {} line {}",
handler_name, class, p, sym.selection_span.start.row + 1,
),
None => format!(
"{} handler on `{}`, registered at line {}",
handler_name, class, sym.selection_span.start.row + 1,
),
};
signatures.push(SignatureInformation {
label: format!("{}('{}', {})", dispatcher, handler_name, labels.join(", ")),
documentation: Some(Documentation::String(doc)),
parameters: Some(parameters),
active_parameter: None,
});
};
for sym in &analysis.symbols {
if sym.name != handler_name { continue; }
push_sig(&mut signatures, sym, None);
}
if let Some(idx) = module_index {
for module_name in idx.modules_with_symbol(handler_name) {
let Some(cached) = idx.get_cached(&module_name) else { continue };
for sym in &cached.analysis.symbols {
if sym.name != handler_name { continue; }
push_sig(&mut signatures, sym, Some(module_name.as_str()));
}
}
}
if signatures.is_empty() { return None; }
Some(SignatureHelp {
signatures,
active_signature: Some(0),
active_parameter: Some(active_param.saturating_sub(1) as u32),
})
}
fn string_dispatch_signature(
analysis: &FileAnalysis,
module_index: Option<&ModuleIndex>,
invocant: Option<&str>,
dispatcher: &str,
handler_name: &str,
active_param: usize,
point: Point,
) -> Option<SignatureHelp> {
let class = analysis.invocant_text_to_class(invocant, point)?;
string_dispatch_signature_for(analysis, module_index, &class, dispatcher, handler_name, active_param)
}
pub fn signature_help(
analysis: &FileAnalysis,
tree: &Tree,
text: &str,
pos: Position,
module_index: &ModuleIndex,
) -> Option<SignatureHelp> {
let point = position_to_point(pos);
let mut skip_string_dispatch = false;
if let Some(qctx) = cursor_context::build_plugin_query_context(analysis, tree, text.as_bytes(), point) {
let registry = crate::builder::default_plugin_registry();
let (uses, parents) = analysis.trigger_view_at(point);
let query = crate::plugin::TriggerQuery {
package_uses: &uses,
package_parents: &parents,
};
for p in registry.applicable(&query) {
match p.on_signature_help(&qctx) {
Some(crate::plugin::PluginSigHelpAnswer::Show(psig)) => {
return Some(plugin_sig_to_lsp(psig));
}
Some(crate::plugin::PluginSigHelpAnswer::Silent) => {
return None;
}
Some(crate::plugin::PluginSigHelpAnswer::ShowHandler {
owner_class, dispatcher, handler_name, active_param,
}) => {
if let Some(sig) = string_dispatch_signature_for(
analysis, Some(module_index),
&owner_class, &dispatcher, &handler_name,
active_param + 1,
) {
return Some(sig);
}
return None;
}
Some(crate::plugin::PluginSigHelpAnswer::ShowCallSig) => {
skip_string_dispatch = true;
break;
}
None => {}
}
}
}
let call_ctx = cursor_context::find_call_context(tree, text.as_bytes(), point)?;
if !skip_string_dispatch && call_ctx.is_method && call_ctx.active_param >= 1 {
if let Some((handler_name, owner_class, dispatcher)) =
dispatch_info_for_enclosing_call(analysis, tree, text.as_bytes(), point)
{
if let Some(sig) = string_dispatch_signature_for(
analysis,
Some(module_index),
&owner_class,
&dispatcher,
&handler_name,
call_ctx.active_param,
) {
return Some(sig);
}
}
if let Some(ref name) = call_ctx.first_arg_string {
if let Some(sig) = string_dispatch_signature(
analysis,
Some(module_index),
call_ctx.invocant.as_deref(),
&call_ctx.name,
name,
call_ctx.active_param,
point,
) {
return Some(sig);
}
}
}
if let Some(sig_info) = analysis.signature_for_call(
&call_ctx.name,
call_ctx.is_method,
call_ctx.invocant.as_deref(),
point,
Some(module_index),
) {
let param_labels: Vec<String> = sig_info
.params
.iter()
.enumerate()
.map(|(i, p)| {
let base = if let Some(ref default) = p.default {
format!("{} = {}", p.name, default)
} else {
p.name.clone()
};
if p.name == "$self" || p.name == "$class" {
return base;
}
if let Some(ref types) = sig_info.param_types {
if let Some(Some(ref type_tag)) = types.get(i) {
return format!("{}: {}", base, type_tag);
}
return base;
}
if let Some(ty) = analysis.inferred_type_via_bag(&p.name, sig_info.body_end) {
format!("{}: {}", base, format_inferred_type(&ty))
} else {
base
}
})
.collect();
let params: Vec<ParameterInformation> = param_labels
.iter()
.map(|label| ParameterInformation {
label: ParameterLabel::Simple(label.clone()),
documentation: None,
})
.collect();
let label = format!("{}({})", sig_info.name, param_labels.join(", "));
return Some(SignatureHelp {
signatures: vec![SignatureInformation {
label,
documentation: None,
parameters: Some(params),
active_parameter: Some(call_ctx.active_param as u32),
}],
active_signature: Some(0),
active_parameter: Some(call_ctx.active_param as u32),
});
}
None
}
pub fn semantic_token_types() -> Vec<SemanticTokenType> {
vec![
SemanticTokenType::VARIABLE, SemanticTokenType::PARAMETER, SemanticTokenType::FUNCTION, SemanticTokenType::METHOD, SemanticTokenType::MACRO, SemanticTokenType::PROPERTY, SemanticTokenType::NAMESPACE, SemanticTokenType::REGEXP, SemanticTokenType::ENUM_MEMBER, SemanticTokenType::KEYWORD, ]
}
pub fn semantic_token_modifiers() -> Vec<SemanticTokenModifier> {
vec![
SemanticTokenModifier::DECLARATION, SemanticTokenModifier::READONLY, SemanticTokenModifier::MODIFICATION, SemanticTokenModifier::DEFAULT_LIBRARY, SemanticTokenModifier::DEPRECATED, SemanticTokenModifier::STATIC, SemanticTokenModifier::new("scalar"), SemanticTokenModifier::new("array"), SemanticTokenModifier::new("hash"), ]
}
pub fn inlay_hints(analysis: &FileAnalysis, range: Range) -> Vec<InlayHint> {
let start = position_to_point(range.start);
let end = position_to_point(range.end);
let mut hints = Vec::new();
for sym in &analysis.symbols {
let decl_point = sym.selection_span.end;
if decl_point.row < start.row || decl_point.row > end.row {
continue;
}
match sym.kind {
FaSymKind::Variable => {
if sym.name == "$self" {
continue;
}
if let Some(ty) = analysis.inferred_type_via_bag(&sym.name, sym.span.start) {
if matches!(ty, InferredType::Numeric | InferredType::String) {
continue;
}
hints.push(InlayHint {
position: point_to_position(decl_point),
label: InlayHintLabel::String(format!(": {}", format_inferred_type(&ty))),
kind: Some(InlayHintKind::TYPE),
text_edits: None,
tooltip: None,
padding_left: Some(true),
padding_right: None,
data: None,
});
}
}
FaSymKind::Sub | FaSymKind::Method => {
if sym.namespace.is_framework() {
continue;
}
if matches!(sym.detail, SymbolDetail::Sub { .. }) {
if let Some(rt) = analysis.symbol_return_type_via_bag(sym.id, None) {
if matches!(rt, InferredType::Numeric | InferredType::String) {
continue;
}
hints.push(InlayHint {
position: point_to_position(decl_point),
label: InlayHintLabel::String(format!(
"→ {}",
format_inferred_type(&rt)
)),
kind: Some(InlayHintKind::TYPE),
text_edits: None,
tooltip: None,
padding_left: Some(true),
padding_right: None,
data: None,
});
}
}
}
_ => {}
}
}
hints
}
pub fn semantic_tokens(analysis: &FileAnalysis) -> Vec<SemanticToken> {
let tokens = analysis.semantic_tokens();
let mut result = Vec::new();
let mut prev_line: u32 = 0;
let mut prev_start: u32 = 0;
for t in &tokens {
let line = t.span.start.row as u32;
let start = t.span.start.column as u32;
let length = if t.span.start.row == t.span.end.row {
(t.span.end.column as u32).saturating_sub(start).max(1)
} else {
1
};
let delta_line = line - prev_line;
let delta_start = if delta_line == 0 {
start.saturating_sub(prev_start)
} else {
start
};
result.push(SemanticToken {
delta_line,
delta_start,
length,
token_type: t.token_type,
token_modifiers_bitset: t.modifiers,
});
prev_line = line;
prev_start = start;
}
result
}
fn imported_function_completions(
analysis: &FileAnalysis,
module_index: &ModuleIndex,
) -> Vec<CompletionCandidate> {
use crate::file_analysis::Span;
let mut candidates = Vec::new();
let mut seen = std::collections::HashSet::new();
for import in &analysis.imports {
let cached: Option<Arc<CachedModule>> = module_index.get_cached(&import.module_name);
for is in &import.imported_symbols {
let local = &is.local_name;
if !seen.insert(local.clone()) {
continue;
}
if !analysis.symbols_named(local).is_empty() {
continue;
}
let detail = completion_detail_for_import(is.remote(), cached.as_deref(), &import.module_name);
candidates.push(CompletionCandidate {
label: local.clone(),
kind: FaSymKind::Sub,
detail: Some(detail),
insert_text: None,
sort_priority: PRIORITY_EXPLICIT_IMPORT,
additional_edits: vec![],
display_override: None,
});
}
if let Some(ref cached) = cached {
let fa = &cached.analysis;
let all_exported: Vec<&String> = if import.imported_symbols.is_empty() {
fa.export.iter().collect()
} else {
let mut all = Vec::new();
all.extend(fa.export.iter());
all.extend(fa.export_ok.iter());
all
};
for name in all_exported {
if !seen.insert(name.clone()) {
continue;
}
if !analysis.symbols_named(name).is_empty() {
continue;
}
let rt_prefix = cached.sub_info(name)
.and_then(|s| s.return_type())
.map(|rt| format!("→ {} ", format_inferred_type(&rt)))
.unwrap_or_default();
let (detail, priority, additional_edits) =
if let Some(close_pos) = import.qw_close_paren {
let insert_point = Span {
start: close_pos,
end: close_pos,
};
(
format!("{}{} (auto-import)", rt_prefix, import.module_name),
PRIORITY_AUTO_ADD_QW,
vec![(insert_point, format!(" {}", name))],
)
} else {
(
format!("{}imported from {}", rt_prefix, import.module_name),
PRIORITY_BARE_IMPORT,
vec![],
)
};
candidates.push(CompletionCandidate {
label: name.clone(),
kind: FaSymKind::Sub,
detail: Some(detail),
insert_text: None,
sort_priority: priority,
additional_edits,
display_override: None,
});
}
}
}
candidates
}
fn unimported_function_completions(
analysis: &FileAnalysis,
module_index: &ModuleIndex,
point: Point,
stable_packages: Option<&[(String, usize)]>,
) -> Vec<CompletionCandidate> {
use crate::file_analysis::Span;
let mut candidates = Vec::new();
let imported_modules: std::collections::HashSet<&str> = analysis
.imports
.iter()
.map(|i| i.module_name.as_str())
.collect();
let mut insert_pos = find_use_insertion_position(analysis, point, stable_packages);
if insert_pos.line as usize > point.row {
let last_import_above = analysis.imports.iter().rev()
.find(|imp| imp.span.start.row < point.row);
if let Some(imp) = last_import_above {
insert_pos = Position { line: imp.span.end.row as u32 + 1, character: 0 };
} else {
let last_pkg_above = analysis.symbols.iter().rev()
.find(|s| matches!(s.kind, FaSymKind::Package | FaSymKind::Class) && s.selection_span.start.row < point.row);
if let Some(pkg) = last_pkg_above {
insert_pos = Position { line: pkg.selection_span.start.row as u32 + 1, character: 0 };
}
}
}
let insert_span = Span {
start: tree_sitter::Point {
row: insert_pos.line as usize,
column: insert_pos.character as usize,
},
end: tree_sitter::Point {
row: insert_pos.line as usize,
column: insert_pos.character as usize,
},
};
module_index.for_each_cached(|module_name, cached| {
if imported_modules.contains(module_name) {
return;
}
let fa = &cached.analysis;
let all_exported = fa.export.iter().chain(fa.export_ok.iter());
for name in all_exported {
if !analysis.symbols_named(name).is_empty() {
continue;
}
candidates.push(CompletionCandidate {
label: name.clone(),
kind: FaSymKind::Sub,
detail: Some(format!("{} (auto-import)", module_name)),
insert_text: None,
sort_priority: PRIORITY_UNIMPORTED,
additional_edits: vec![(
insert_span,
format!("use {} qw({});\n", module_name, name),
)],
display_override: None,
});
}
});
candidates.sort_by(|a, b| a.label.cmp(&b.label).then(a.detail.cmp(&b.detail)));
candidates
}
fn resolve_imported_function<'a>(
analysis: &'a FileAnalysis,
func_name: &str,
module_index: &ModuleIndex,
) -> Option<(&'a crate::file_analysis::Import, std::path::PathBuf, String)> {
for import in &analysis.imports {
if let Some(cached) = module_index.get_cached(&import.module_name) {
let explicit = import.imported_symbols.iter().find(|s| s.local_name == *func_name);
if let Some(is) = explicit {
return Some((import, cached.path.clone(), is.remote().to_string()));
}
let in_export_lists = cached.analysis.export.iter().any(|s| s == func_name)
|| cached.analysis.export_ok.iter().any(|s| s == func_name);
if in_export_lists {
return Some((import, cached.path.clone(), func_name.to_string()));
}
} else if let Some(is) = import.imported_symbols.iter().find(|s| s.local_name == *func_name) {
if let Some(path) = module_index.module_path_cached(&import.module_name) {
return Some((import, path, is.remote().to_string()));
}
}
}
None
}
fn completion_detail_for_import(
name: &str,
cached: Option<&CachedModule>,
module_name: &str,
) -> String {
if let Some(cached) = cached {
if let Some(sub_info) = cached.sub_info(name) {
if let Some(rt) = sub_info.return_type() {
return format!("→ {} ({})", format_inferred_type(&rt), module_name);
}
}
}
format!("imported from {}", module_name)
}
fn format_imported_signature(name: &str, sub_info: &SubInfo<'_>) -> String {
let params_str = sub_info
.params()
.iter()
.map(|p| p.name.as_str())
.collect::<Vec<_>>()
.join(", ");
let mut sig = format!("sub {}({})", name, params_str);
if let Some(rt) = sub_info.return_type() {
sig.push_str(&format!(" → {}", format_inferred_type(&rt)));
}
sig
}
static PERL_BUILTINS: &[&str] = &[
"abs", "accept", "alarm", "atan2",
"bind", "binmode", "bless",
"caller", "chdir", "chmod", "chomp", "chop", "chown", "chr", "chroot", "close",
"closedir", "connect", "cos", "crypt",
"dbmclose", "dbmopen", "defined", "delete", "die", "do", "dump",
"each", "endgrent", "endhostent", "endnetent", "endprotoent", "endpwent",
"endservent", "eof", "eval", "exec", "exists", "exit",
"fcntl", "fileno", "flock", "fork", "format", "formline",
"getc", "getgrent", "getgrgid", "getgrnam", "gethostbyaddr", "gethostbyname",
"gethostent", "getlogin", "getnetbyaddr", "getnetbyname", "getnetent",
"getpeername", "getpgrp", "getppid", "getpriority", "getprotobyname",
"getprotobynumber", "getprotoent", "getpwent", "getpwnam", "getpwuid",
"getservbyname", "getservbyport", "getservent", "getsockname", "getsockopt",
"glob", "gmtime", "goto", "grep",
"hex",
"import", "index", "int", "ioctl",
"join",
"keys", "kill",
"last", "lc", "lcfirst", "length", "link", "listen", "local", "localtime", "log",
"lstat",
"map", "mkdir", "msgctl", "msgget", "msgrcv", "msgsnd",
"my",
"new", "next", "no",
"oct", "open", "opendir", "ord", "our",
"pack", "pipe", "pop", "pos", "print", "printf", "prototype", "push",
"quotemeta",
"rand", "read", "readdir", "readline", "readlink", "readpipe", "recv", "redo",
"ref", "rename", "require", "reset", "return", "reverse", "rewinddir", "rindex",
"rmdir",
"say", "scalar", "seek", "seekdir", "select", "semctl", "semget", "semop", "send",
"setgrent", "sethostent", "setnetent", "setpgrp", "setpriority", "setprotoent",
"setpwent", "setservent", "setsockopt", "shift", "shmctl", "shmget", "shmread",
"shmwrite", "shutdown", "sin", "sleep", "socket", "socketpair", "sort", "splice",
"split", "sprintf", "sqrt", "srand", "stat", "state", "study", "sub", "substr",
"symlink", "syscall", "sysopen", "sysread", "sysseek", "system", "syswrite",
"tell", "telldir", "tie", "tied", "time", "times", "truncate",
"uc", "ucfirst", "umask", "undef", "unlink", "unpack", "unshift", "untie", "use",
"utime",
"values", "vec",
"wait", "waitpid", "wantarray", "warn", "write",
];
fn is_perl_builtin(name: &str) -> bool {
PERL_BUILTINS.binary_search(&name).is_ok()
}
pub fn collect_diagnostics(analysis: &FileAnalysis, module_index: &ModuleIndex) -> Vec<Diagnostic> {
let mut diagnostics = Vec::new();
for r in &analysis.refs {
if !matches!(r.kind, RefKind::FunctionCall { .. }) {
continue;
}
let name = &r.target_name;
if name.contains("::") {
continue;
}
if name.starts_with('&') {
continue;
}
if is_perl_builtin(name) {
continue;
}
if !analysis.symbols_named(name).is_empty() {
continue;
}
if analysis.framework_imports.contains(name.as_str()) {
continue;
}
let explicitly_imported = analysis.imports.iter().any(|imp| {
if imp.imported_symbols.iter().any(|s| s.local_name == *name) {
return true;
}
if imp.imported_symbols.is_empty() {
if let Some(cached) = module_index.get_cached(&imp.module_name) {
if cached.analysis.export.iter().any(|s| s == name) {
return true;
}
}
}
false
});
if explicitly_imported {
continue;
}
let range = span_to_range(r.span);
if let Some((import, _path, _remote)) = resolve_imported_function(analysis, name, module_index) {
diagnostics.push(Diagnostic {
range,
severity: Some(DiagnosticSeverity::HINT),
code: Some(NumberOrString::String("unresolved-function".into())),
source: Some("perl-lsp".into()),
message: format!(
"'{}' is exported by {} but not imported",
name, import.module_name,
),
data: Some(serde_json::json!({
"module": import.module_name,
"function": name,
})),
..Default::default()
});
} else {
let exporters = module_index.find_exporters(name);
if !exporters.is_empty() {
let msg = if exporters.len() == 1 {
format!(
"'{}' is exported by {} (not yet imported)",
name, exporters[0],
)
} else {
format!(
"'{}' is exported by {} and {} other module(s)",
name,
exporters[0],
exporters.len() - 1,
)
};
diagnostics.push(Diagnostic {
range,
severity: Some(DiagnosticSeverity::HINT),
code: Some(NumberOrString::String("unresolved-function".into())),
source: Some("perl-lsp".into()),
message: msg,
data: Some(serde_json::json!({
"modules": exporters,
"function": name,
})),
..Default::default()
});
} else {
diagnostics.push(Diagnostic {
range,
severity: Some(DiagnosticSeverity::INFORMATION),
code: Some(NumberOrString::String("unresolved-function".into())),
source: Some("perl-lsp".into()),
message: format!("'{}' is not defined in this file", name),
..Default::default()
});
}
}
}
let universal_methods = [
"new", "AUTOLOAD", "DESTROY", "can", "isa", "DOES", "VERSION",
"add_columns", "add_column", "set_primary_key", "table", "resultset_class",
"has_many", "has_one", "belongs_to", "might_have", "many_to_many",
"load_components",
"meta",
];
for r in &analysis.refs {
let (invocant, _invocant_span) = match &r.kind {
RefKind::MethodCall { invocant, invocant_span, .. } => (invocant, invocant_span),
_ => continue,
};
let method_name = &r.target_name;
if universal_methods.contains(&method_name.as_str()) {
continue;
}
let class_name = if !invocant.starts_with('$')
&& !invocant.starts_with('@')
&& !invocant.starts_with('%')
{
Some(invocant.clone())
} else {
analysis.inferred_type_via_bag(invocant, r.span.start)
.and_then(|ty| ty.class_name().map(|s| s.to_string()))
};
let class_name = match class_name {
Some(cn) => cn,
None => continue,
};
let is_local_class = analysis.symbols.iter().any(|s| {
matches!(s.kind, FaSymKind::Class | FaSymKind::Package) && s.name == class_name
});
if !is_local_class {
continue;
}
let has_methods = analysis.symbols.iter().any(|s| {
matches!(s.kind, FaSymKind::Sub | FaSymKind::Method)
&& analysis.symbol_in_class(s.id, &class_name)
});
if !has_methods {
continue;
}
if analysis.resolve_method_in_ancestors(&class_name, method_name, Some(module_index)).is_some() {
continue;
}
diagnostics.push(Diagnostic {
range: span_to_range(r.span),
severity: Some(DiagnosticSeverity::HINT),
code: Some(NumberOrString::String("unresolved-method".into())),
source: Some("perl-lsp".into()),
message: format!(
"'{}' is not defined in {}",
method_name, class_name,
),
..Default::default()
});
}
diagnostics
}
fn find_use_insertion_position(
analysis: &FileAnalysis,
point: Point,
stable_packages: Option<&[(String, usize)]>,
) -> Position {
let mut pkg_lines: Vec<usize> = analysis.symbols.iter()
.filter(|s| matches!(s.kind, FaSymKind::Package | FaSymKind::Class))
.map(|s| s.selection_span.start.row)
.collect();
if let Some(stable) = stable_packages {
if stable.len() > pkg_lines.len() {
for (_, line) in stable {
if !pkg_lines.contains(line) {
pkg_lines.push(*line);
}
}
}
}
pkg_lines.sort();
let pkg_start = pkg_lines.iter().rev()
.find(|&&line| line <= point.row)
.copied()
.unwrap_or(0);
let pkg_end = pkg_lines.iter()
.find(|&&line| line > point.row)
.copied()
.unwrap_or(usize::MAX);
let last_import = analysis.imports.iter().rev().find(|imp| {
imp.span.start.row >= pkg_start && imp.span.start.row < pkg_end
});
if let Some(imp) = last_import {
Position {
line: imp.span.end.row as u32 + 1,
character: 0,
}
} else {
Position {
line: pkg_start as u32 + 1,
character: 0,
}
}
}
pub fn code_actions(
diagnostics: &[Diagnostic],
analysis: &FileAnalysis,
uri: &Url,
) -> Vec<CodeActionOrCommand> {
let mut actions = Vec::new();
for diag in diagnostics {
let code_matches = matches!(
&diag.code,
Some(NumberOrString::String(s)) if s == "unresolved-function"
);
if !code_matches {
continue;
}
let data = match &diag.data {
Some(d) => d,
None => continue,
};
let func_name = match data.get("function").and_then(|v| v.as_str()) {
Some(f) => f,
None => continue,
};
if let Some(module_name) = data.get("module").and_then(|v| v.as_str()) {
if let Some(action) =
make_add_to_qw_action(analysis, uri, diag, module_name, func_name)
{
actions.push(action);
}
continue;
}
if let Some(modules) = data.get("modules").and_then(|v| v.as_array()) {
let diag_point = position_to_point(diag.range.start);
let mut insert_pos = find_use_insertion_position(analysis, diag_point, None);
if insert_pos.line > diag.range.start.line {
let last_import_above = analysis.imports.iter().rev()
.find(|imp| imp.span.start.row < diag_point.row);
if let Some(imp) = last_import_above {
insert_pos = Position { line: imp.span.end.row as u32 + 1, character: 0 };
} else {
let last_pkg_above = analysis.symbols.iter().rev()
.find(|s| matches!(s.kind, FaSymKind::Package | FaSymKind::Class) && s.selection_span.start.row < diag_point.row);
if let Some(pkg) = last_pkg_above {
insert_pos = Position { line: pkg.selection_span.start.row as u32 + 1, character: 0 };
}
}
}
for (i, module_val) in modules.iter().enumerate() {
if let Some(module_name) = module_val.as_str() {
let new_text = format!("use {} qw({});\n", module_name, func_name);
let edit = TextEdit {
range: Range {
start: insert_pos,
end: insert_pos,
},
new_text,
};
let mut changes = HashMap::new();
changes.insert(uri.clone(), vec![edit]);
actions.push(CodeActionOrCommand::CodeAction(CodeAction {
title: format!("Add 'use {} qw({})'", module_name, func_name),
kind: Some(CodeActionKind::QUICKFIX),
diagnostics: Some(vec![diag.clone()]),
edit: Some(WorkspaceEdit {
changes: Some(changes),
..Default::default()
}),
is_preferred: Some(i == 0 && modules.len() == 1),
..Default::default()
}));
}
}
}
}
actions
}
fn make_add_to_qw_action(
analysis: &FileAnalysis,
uri: &Url,
diag: &Diagnostic,
module_name: &str,
func_name: &str,
) -> Option<CodeActionOrCommand> {
let import = analysis
.imports
.iter()
.find(|imp| imp.module_name == module_name)?;
let close_pos = import.qw_close_paren?;
let insert_pos = point_to_position(close_pos);
let edit = TextEdit {
range: Range {
start: insert_pos,
end: insert_pos,
},
new_text: format!(" {}", func_name),
};
let mut changes = HashMap::new();
changes.insert(uri.clone(), vec![edit]);
Some(CodeActionOrCommand::CodeAction(CodeAction {
title: format!("Import '{}' from {}", func_name, module_name),
kind: Some(CodeActionKind::QUICKFIX),
diagnostics: Some(vec![diag.clone()]),
edit: Some(WorkspaceEdit {
changes: Some(changes),
..Default::default()
}),
is_preferred: Some(true),
..Default::default()
}))
}
#[cfg(test)]
#[path = "symbols_tests.rs"]
mod tests;