use serde::{Deserialize, Serialize};
use crate::file_analysis::{
AccessKind, Bridge, HandlerDisplay, HandlerOwner, HashKeyOwner, InferredType, ParamInfo, Span,
SymKind, SymbolDetail,
};
pub mod cli;
pub mod rhai_host;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum CallKind {
Function,
Method,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ArgInfo {
pub text: String,
pub string_value: Option<String>,
pub span: Span,
#[serde(default)]
pub content_span: Option<Span>,
pub inferred_type: Option<InferredType>,
#[serde(default)]
pub sub_params: Vec<EmittedParam>,
#[serde(default)]
pub callable_return_edge: Option<crate::witnesses::WitnessAttachment>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CallContext {
pub call_kind: CallKind,
pub function_name: Option<String>,
pub method_name: Option<String>,
pub receiver_text: Option<String>,
pub receiver_type: Option<InferredType>,
pub args: Vec<ArgInfo>,
pub call_span: Span,
pub selection_span: Span,
pub current_package: Option<String>,
pub current_package_parents: Vec<String>,
pub current_package_uses: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct UseContext {
pub module_name: String,
pub imports: Vec<String>,
pub raw_args: Vec<String>,
pub current_package: Option<String>,
pub span: Span,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EmittedParam {
pub name: String,
pub default: Option<String>,
pub is_slurpy: bool,
#[serde(default)]
pub is_invocant: bool,
}
impl From<EmittedParam> for ParamInfo {
fn from(p: EmittedParam) -> Self {
ParamInfo {
name: p.name,
default: p.default,
is_slurpy: p.is_slurpy,
is_invocant: p.is_invocant,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum EmitAction {
Method {
name: String,
span: Span,
selection_span: Span,
params: Vec<EmittedParam>,
is_method: bool,
return_type: Option<InferredType>,
doc: Option<String>,
#[serde(default)]
on_class: Option<String>,
#[serde(default)]
display: Option<HandlerDisplay>,
#[serde(default)]
hide_in_outline: bool,
#[serde(default)]
opaque_return: bool,
#[serde(default)]
outline_label: Option<String>,
#[serde(default)]
return_via_edge: Option<crate::witnesses::WitnessAttachment>,
},
HashKeyDef {
name: String,
owner: HashKeyOwner,
span: Span,
selection_span: Span,
},
HashKeyAccess {
name: String,
owner: HashKeyOwner,
var_text: String,
span: Span,
access: AccessKind,
},
PackageParent { package: String, parent: String },
Handler {
name: String,
owner: HandlerOwner,
dispatchers: Vec<String>,
params: Vec<EmittedParam>,
span: Span,
selection_span: Span,
#[serde(default)]
display: HandlerDisplay,
#[serde(default)]
hide_in_outline: bool,
#[serde(default)]
outline_label: Option<String>,
},
DispatchCall {
name: String,
dispatcher: String,
owner: HandlerOwner,
span: Span,
var_text: String,
},
MethodCallRef {
method_name: String,
invocant: String,
span: Span,
#[serde(default)]
invocant_span: Option<Span>,
},
Symbol {
name: String,
kind: SymKind,
span: Span,
selection_span: Span,
detail: SymbolDetail,
#[serde(default)]
return_type: Option<InferredType>,
},
PluginNamespace {
id: String,
kind: String,
bridges: Vec<Bridge>,
#[serde(default)]
entity_names: Vec<String>,
decl_span: Span,
},
FrameworkImport { keyword: String },
Import {
module_name: String,
imported_symbols: Vec<crate::file_analysis::ImportedSymbol>,
span: Span,
},
VarType {
variable: String,
at: Span,
inferred_type: InferredType,
},
SyntheticUse {
module: String,
#[serde(default)]
args: Vec<String>,
#[serde(default)]
imports: Vec<String>,
span: Span,
},
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum Trigger {
UsesModule(String),
ClassIsa(String),
Always,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TypeOverride {
pub target: OverrideTarget,
pub return_type: InferredType,
pub reason: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum OverrideTarget {
Method { class: String, name: String },
Sub {
#[serde(default)]
package: Option<String>,
name: String,
},
}
pub trait FrameworkPlugin: Send + Sync {
fn id(&self) -> &str;
fn triggers(&self) -> &[Trigger];
fn overrides(&self) -> &[TypeOverride] {
&[]
}
#[allow(unused_variables)]
fn on_use(&self, ctx: &UseContext) -> Vec<EmitAction> {
Vec::new()
}
#[allow(unused_variables)]
fn on_function_call(&self, ctx: &CallContext) -> Vec<EmitAction> {
Vec::new()
}
#[allow(unused_variables)]
fn on_method_call(&self, ctx: &CallContext) -> Vec<EmitAction> {
Vec::new()
}
#[allow(unused_variables)]
fn on_signature_help(&self, ctx: &SigHelpQueryContext) -> Option<PluginSigHelpAnswer> {
None
}
#[allow(unused_variables)]
fn on_completion(&self, ctx: &CompletionQueryContext) -> Option<PluginCompletionAnswer> {
None
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SigHelpQueryContext {
pub call: Option<CallFrame>,
pub cursor_inside: Option<ContainerFrame>,
pub current_package: Option<String>,
#[serde(default)]
pub current_use_module: Option<String>,
}
pub type CompletionQueryContext = SigHelpQueryContext;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CallFrame {
pub is_method: bool,
pub name: String,
pub receiver_text: Option<String>,
pub receiver_type: Option<InferredType>,
pub args: Vec<ArgInfo>,
pub cursor_arg_index: usize,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ContainerFrame {
pub kind: ContainerKind,
pub active_slot: usize,
pub existing_keys: Vec<String>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum ContainerKind {
Array,
Hash,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PluginSignatureHelp {
pub label: String,
pub params: Vec<String>,
pub active_param: usize,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum PluginSigHelpAnswer {
Show(PluginSignatureHelp),
Silent,
ShowHandler {
owner_class: String,
dispatcher: String,
handler_name: String,
active_param: usize,
},
ShowCallSig,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PluginCompletionAnswer {
pub items: Vec<PluginCompletion>,
#[serde(default)]
pub exclusive: bool,
#[serde(default)]
pub dispatch_targets_for: Option<DispatchTargetRequest>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DispatchTargetRequest {
pub owner_class: String,
pub dispatcher_names: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PluginCompletion {
pub label: String,
pub kind: CompletionKindHint,
#[serde(default)]
pub detail: Option<String>,
#[serde(default)]
pub insert_text: Option<String>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum CompletionKindHint {
Function,
Method,
Field,
Property,
Value,
Event,
Operator,
Keyword,
Task,
Helper,
Route,
}
#[derive(Default)]
pub struct PluginRegistry {
plugins: Vec<Box<dyn FrameworkPlugin>>,
}
impl std::fmt::Debug for PluginRegistry {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("PluginRegistry")
.field("count", &self.plugins.len())
.field("ids", &self.plugins.iter().map(|p| p.id()).collect::<Vec<_>>())
.finish()
}
}
impl PluginRegistry {
pub fn new() -> Self {
Self::default()
}
pub fn register(&mut self, plugin: Box<dyn FrameworkPlugin>) {
self.plugins.push(plugin);
}
pub fn is_empty(&self) -> bool {
self.plugins.is_empty()
}
pub fn all(&self) -> impl Iterator<Item = &dyn FrameworkPlugin> {
self.plugins.iter().map(|p| p.as_ref())
}
pub fn overrides<'a>(&'a self) -> impl Iterator<Item = (&'a str, &'a TypeOverride)> + 'a {
self.plugins
.iter()
.flat_map(|p| p.overrides().iter().map(move |o| (p.id(), o)))
}
pub fn applicable<'a>(
&'a self,
query: &TriggerQuery<'_>,
) -> impl Iterator<Item = &'a dyn FrameworkPlugin> + 'a {
let uses = query.package_uses.to_vec();
let parents = query.package_parents.to_vec();
self.plugins.iter().filter_map(move |p| {
let fires = p.triggers().iter().any(|t| match t {
Trigger::Always => true,
Trigger::UsesModule(m) => uses.iter().any(|u| u == m),
Trigger::ClassIsa(prefix) => parents.iter().any(|parent| {
parent == prefix
|| parent.starts_with(&format!("{}::", prefix))
}),
});
if fires {
Some(p.as_ref())
} else {
None
}
})
}
}
pub struct TriggerQuery<'a> {
pub package_uses: &'a [String],
pub package_parents: &'a [String],
}
#[cfg(test)]
mod tests {
use super::*;
use tree_sitter::Point;
fn span(r1: usize, c1: usize, r2: usize, c2: usize) -> Span {
Span {
start: Point::new(r1, c1),
end: Point::new(r2, c2),
}
}
struct StubPlugin {
id: &'static str,
triggers: Vec<Trigger>,
sig_answer: Option<PluginSigHelpAnswer>,
completion_answer: Option<PluginCompletionAnswer>,
}
impl FrameworkPlugin for StubPlugin {
fn id(&self) -> &str { self.id }
fn triggers(&self) -> &[Trigger] { &self.triggers }
fn on_signature_help(&self, _ctx: &SigHelpQueryContext) -> Option<PluginSigHelpAnswer> {
self.sig_answer.clone()
}
fn on_completion(&self, _ctx: &CompletionQueryContext) -> Option<PluginCompletionAnswer> {
self.completion_answer.clone()
}
}
fn stub(id: &'static str) -> StubPlugin {
StubPlugin { id, triggers: vec![Trigger::Always], sig_answer: None, completion_answer: None }
}
fn empty_qctx() -> SigHelpQueryContext {
SigHelpQueryContext {
call: None,
cursor_inside: None,
current_package: None,
current_use_module: None,
}
}
#[test]
fn trigger_uses_module_matches_only_when_used() {
let mut reg = PluginRegistry::new();
reg.register(Box::new(StubPlugin {
id: "mojo-base",
triggers: vec![Trigger::UsesModule("Mojo::Base".into())],
sig_answer: None,
completion_answer: None,
}));
let uses_mojo: Vec<String> = vec!["Mojo::Base".into()];
let uses_none: Vec<String> = vec![];
let parents: Vec<String> = vec![];
let m = reg.applicable(&TriggerQuery {
package_uses: &uses_mojo,
package_parents: &parents,
}).count();
assert_eq!(m, 1);
let m = reg.applicable(&TriggerQuery {
package_uses: &uses_none,
package_parents: &parents,
}).count();
assert_eq!(m, 0);
}
#[test]
fn trigger_class_isa_prefix_matches_descendants() {
let mut reg = PluginRegistry::new();
reg.register(Box::new(StubPlugin {
id: "dbic",
triggers: vec![Trigger::ClassIsa("DBIx::Class".into())],
sig_answer: None,
completion_answer: None,
}));
let uses: Vec<String> = vec![];
let parents = vec!["DBIx::Class::Core".to_string()];
let m = reg.applicable(&TriggerQuery {
package_uses: &uses,
package_parents: &parents,
}).count();
assert_eq!(m, 1, "DBIx::Class::Core should match prefix DBIx::Class");
let other = vec!["Something::Else".to_string()];
let m = reg.applicable(&TriggerQuery {
package_uses: &uses,
package_parents: &other,
}).count();
assert_eq!(m, 0);
}
#[test]
fn emitted_param_converts_to_param_info() {
let ep = EmittedParam {
name: "$val".into(),
default: None,
is_slurpy: false,
is_invocant: false,
};
let pi: ParamInfo = ep.into();
assert_eq!(pi.name, "$val");
}
#[test]
fn emit_action_serializable() {
let action = EmitAction::Method {
name: "foo".into(),
span: span(0, 0, 0, 3),
selection_span: span(0, 0, 0, 3),
params: vec![],
is_method: true,
return_type: None,
doc: None,
on_class: None,
display: None,
hide_in_outline: false,
opaque_return: false,
outline_label: None,
return_via_edge: None,
};
let json = serde_json::to_string(&action).unwrap();
assert!(json.contains("\"Method\""));
assert!(json.contains("\"foo\""));
}
#[test]
fn iteration_order_is_registration_order() {
let mut reg = PluginRegistry::new();
reg.register(Box::new(stub("first")));
reg.register(Box::new(stub("second")));
let uses: Vec<String> = vec![];
let parents: Vec<String> = vec![];
let ids: Vec<&str> = reg.applicable(&TriggerQuery {
package_uses: &uses, package_parents: &parents,
}).map(|p| p.id()).collect();
assert_eq!(ids, vec!["first", "second"],
"applicable() preserves registration order — symbols.rs relies on \
this for deterministic first-match-wins behavior");
}
#[test]
fn sig_help_silent_variant_is_distinct_from_show() {
let silent = PluginSigHelpAnswer::Silent;
let json = serde_json::to_string(&silent).unwrap();
let back: PluginSigHelpAnswer = serde_json::from_str(&json).unwrap();
assert!(matches!(back, PluginSigHelpAnswer::Silent));
let show = PluginSigHelpAnswer::Show(PluginSignatureHelp {
label: "x".into(), params: vec![], active_param: 0,
});
let show_json = serde_json::to_string(&show).unwrap();
assert!(show_json.contains("Show"));
assert!(!show_json.contains("\"Silent\""));
}
#[test]
fn completion_exclusive_and_dispatch_targets_coexist() {
let ans = PluginCompletionAnswer {
items: vec![],
exclusive: true,
dispatch_targets_for: Some(DispatchTargetRequest {
owner_class: "Minion".into(),
dispatcher_names: vec!["enqueue".into()],
}),
};
assert!(ans.exclusive);
assert!(ans.dispatch_targets_for.is_some());
let json = serde_json::to_string(&ans).unwrap();
let back: PluginCompletionAnswer = serde_json::from_str(&json).unwrap();
assert!(back.exclusive);
assert_eq!(
back.dispatch_targets_for.unwrap().owner_class,
"Minion"
);
}
#[test]
fn first_answering_plugin_claims_sig_help() {
let mut reg = PluginRegistry::new();
reg.register(Box::new(StubPlugin {
id: "winner",
triggers: vec![Trigger::Always],
sig_answer: Some(PluginSigHelpAnswer::Silent),
completion_answer: None,
}));
reg.register(Box::new(StubPlugin {
id: "loser",
triggers: vec![Trigger::Always],
sig_answer: Some(PluginSigHelpAnswer::Show(PluginSignatureHelp {
label: "never-seen".into(), params: vec![], active_param: 0,
})),
completion_answer: None,
}));
let uses: Vec<String> = vec![];
let parents: Vec<String> = vec![];
let qctx = empty_qctx();
let mut answers = Vec::new();
for p in reg.applicable(&TriggerQuery {
package_uses: &uses, package_parents: &parents,
}) {
if let Some(a) = p.on_signature_help(&qctx) {
answers.push((p.id().to_string(), a));
break; }
}
assert_eq!(answers.len(), 1);
assert_eq!(answers[0].0, "winner");
assert!(matches!(answers[0].1, PluginSigHelpAnswer::Silent));
}
}