1use perl_builtins::builtin_signatures::create_builtin_signatures;
22use perl_parser_core::ast::{Node, NodeKind};
23use perl_position_tracking::{WirePosition as Position, WireRange as Range};
24use perl_semantic_analyzer::declaration::get_node_children;
25use serde_json::Value;
26use serde_json::json;
27
28#[derive(Debug, Clone, Copy, PartialEq, Eq)]
30pub enum InlayHintKind {
31 Type = 1,
33 Parameter = 2,
35}
36
37#[derive(Debug, Clone)]
39pub struct InlayHint {
40 pub position: Position,
42 pub label: String,
44 pub kind: InlayHintKind,
46 pub padding_left: bool,
48 pub padding_right: bool,
50 pub tooltip: Option<String>,
52 pub location: Option<HintLocation>,
54}
55
56#[derive(Debug, Clone)]
58pub struct HintLocation {
59 pub uri: String,
61 pub range: (usize, usize),
63}
64
65pub struct InlayHintsProvider;
67
68impl InlayHintsProvider {
69 pub fn new() -> Self {
71 Self
72 }
73
74 pub fn generate_hints(
76 &self,
77 ast: &Node,
78 to_pos16: &impl Fn(usize) -> (u32, u32),
79 range: Option<Range>,
80 ) -> Vec<InlayHint> {
81 let mut hints = Vec::new();
82 hints.extend(self.parameter_hints(ast, to_pos16, range));
83 hints.extend(self.trivial_type_hints(ast, to_pos16, range));
84 hints
85 }
86
87 pub fn parameter_hints(
89 &self,
90 ast: &Node,
91 to_pos16: &impl Fn(usize) -> (u32, u32),
92 range: Option<Range>,
93 ) -> Vec<InlayHint> {
94 parameter_hints(ast, to_pos16, range)
95 .into_iter()
96 .filter_map(|v| {
97 let pos = v["position"].clone();
98 let label = v["label"].as_str()?.to_string();
99 let kind = match v["kind"].as_u64().unwrap_or(1) {
100 2 => InlayHintKind::Parameter,
101 _ => InlayHintKind::Type,
102 };
103 let tooltip = v.get("tooltip").and_then(|t| t.as_str()).map(|s| s.to_string());
104 Some(InlayHint {
105 position: Position::new(
106 pos["line"].as_u64()? as u32,
107 pos["character"].as_u64()? as u32,
108 ),
109 label,
110 kind,
111 padding_left: v["paddingLeft"].as_bool().unwrap_or(false),
112 padding_right: v["paddingRight"].as_bool().unwrap_or(false),
113 tooltip,
114 location: None,
115 })
116 })
117 .collect()
118 }
119
120 pub fn trivial_type_hints(
122 &self,
123 ast: &Node,
124 to_pos16: &impl Fn(usize) -> (u32, u32),
125 range: Option<Range>,
126 ) -> Vec<InlayHint> {
127 trivial_type_hints(ast, to_pos16, range)
128 .into_iter()
129 .filter_map(|v| {
130 let pos = v["position"].clone();
131 let label = v["label"].as_str()?.to_string();
132 let kind = match v["kind"].as_u64().unwrap_or(1) {
133 2 => InlayHintKind::Parameter,
134 _ => InlayHintKind::Type,
135 };
136 let tooltip = v.get("tooltip").and_then(|t| t.as_str()).map(|s| s.to_string());
137 Some(InlayHint {
138 position: Position::new(
139 pos["line"].as_u64()? as u32,
140 pos["character"].as_u64()? as u32,
141 ),
142 label,
143 kind,
144 padding_left: v["paddingLeft"].as_bool().unwrap_or(false),
145 padding_right: v["paddingRight"].as_bool().unwrap_or(false),
146 tooltip,
147 location: None,
148 })
149 })
150 .collect()
151 }
152}
153
154impl Default for InlayHintsProvider {
155 fn default() -> Self {
156 Self::new()
157 }
158}
159
160fn pos_in_range(pos: Position, range: Range) -> bool {
161 if pos.line < range.start.line || pos.line > range.end.line {
162 return false;
163 }
164 if pos.line == range.start.line && pos.character < range.start.character {
165 return false;
166 }
167 if pos.line == range.end.line && pos.character >= range.end.character {
168 return false;
169 }
170 true
171}
172
173pub fn extract_param_names(signature: &str) -> Vec<String> {
186 let rest = match signature.find(' ') {
188 Some(idx) => &signature[idx + 1..],
189 None => return Vec::new(),
190 };
191
192 let mut params = Vec::new();
193 for group in rest.split(", ") {
195 for token in group.split(' ') {
197 if token.is_empty() {
198 continue;
199 }
200 let cleaned = token.trim_matches('/');
202 params.push(cleaned.to_lowercase());
203 }
204 }
205 params
206}
207
208pub fn parameter_hints(
225 ast: &Node,
226 to_pos16: &impl Fn(usize) -> (u32, u32),
227 range: Option<Range>,
228) -> Vec<Value> {
229 let sigs = create_builtin_signatures();
230 let mut out = Vec::new();
231 walk_ast(ast, &mut |node| {
232 if let NodeKind::FunctionCall { name, args } = &node.kind
233 && let Some(builtin) = sigs.get(name.as_str())
234 {
235 if let Some(first_sig) = builtin.signatures.first() {
238 let param_names = extract_param_names(first_sig);
239
240 if param_names.len() <= 1 {
244 return true;
245 }
246
247 for (i, arg) in args.iter().enumerate() {
248 if i >= param_names.len() {
249 break;
250 }
251 let (l, c) = to_pos16(arg.location.start);
252
253 if let Some(filter_range) = range {
255 let hint_pos = Position::new(l, c);
256 if !pos_in_range(hint_pos, filter_range) {
257 continue;
258 }
259 }
260
261 let mut hint = json!({
264 "position": { "line": l, "character": c },
265 "label": format!("{}:", param_names[i]),
266 "kind": 2, "paddingLeft": false,
268 "paddingRight": true,
269 "data": {
270 "functionName": name.as_str(),
271 "paramIndex": i,
272 }
273 });
274
275 if let Some(doc) = builtin_doc_summary(name.as_str(), ¶m_names[i], i) {
278 hint["data"]["docSummary"] = json!(doc);
279 }
280
281 out.push(hint);
282 }
283 }
284 }
285 true
286 });
287 out
288}
289
290pub fn trivial_type_hints(
305 ast: &Node,
306 to_pos16: &impl Fn(usize) -> (u32, u32),
307 range: Option<Range>,
308) -> Vec<Value> {
309 let mut out = Vec::new();
310 walk_ast(ast, &mut |node| {
311 let type_hint = match &node.kind {
312 NodeKind::Number { .. } => Some(("Num".to_string(), Some("Numeric literal"))),
313 NodeKind::String { .. } => Some(("Str".to_string(), Some("String literal"))),
314 NodeKind::HashLiteral { .. } => Some(("Hash".to_string(), Some("Hash reference"))),
315 NodeKind::ArrayLiteral { .. } => Some(("Array".to_string(), Some("Array reference"))),
316 NodeKind::Regex { .. } => Some(("Regex".to_string(), Some("Regular expression"))),
317 NodeKind::Subroutine { name: None, .. } => {
318 Some(("CodeRef".to_string(), Some("Anonymous subroutine (code reference)")))
319 }
320 _ => infer_semantic_type(node).map(|t| (t, None)),
322 };
323
324 if let Some((hint, tooltip)) = type_hint {
325 let (l, c) = to_pos16(node.location.end);
326
327 if let Some(filter_range) = range {
329 let hint_pos = Position::new(l, c);
330 if !pos_in_range(hint_pos, filter_range) {
331 return true;
332 }
333 }
334
335 let mut val = json!({
336 "position": {"line": l, "character": c},
337 "label": format!(": {}", hint),
338 "kind": 1, "paddingLeft": true,
340 "paddingRight": false
341 });
342
343 if let Some(tt) = tooltip {
345 val["data"] = json!({ "tooltip": tt });
346 }
347
348 out.push(val);
349 }
350 true
351 });
352 out
353}
354
355pub fn infer_semantic_type(node: &Node) -> Option<String> {
369 match &node.kind {
370 NodeKind::FunctionCall { name, .. } => function_return_type(name),
371 NodeKind::MethodCall { method, .. } => method_return_type(method),
372 NodeKind::Variable { name, sigil } => {
373 match (sigil.as_str(), name.as_str()) {
375 ("$", _) if name.ends_with("_fh") || name.ends_with("_handle") => {
376 Some("FileHandle".to_string())
377 }
378 ("$", _) if name.ends_with("_ref") => Some("Ref".to_string()),
379 ("@", _) if name.ends_with("_nums") => Some("@Nums".to_string()),
380 ("@", _) if name.ends_with("_strs") => Some("@Strs".to_string()),
381 ("@", _) if name.ends_with("_lines") => Some("@Lines".to_string()),
382 ("%", _) => Some("Hash".to_string()),
383 _ => None,
384 }
385 }
386 _ => None,
387 }
388}
389
390fn function_return_type(name: &str) -> Option<String> {
392 match name {
393 "open" => Some("Bool|FileHandle".to_string()),
394 "split" => Some("@Str".to_string()),
395 "join" => Some("Str".to_string()),
396 "keys" | "values" | "each" => Some("List".to_string()),
397 "map" | "grep" => Some("@List".to_string()),
398 "sort" => Some("@Sorted".to_string()),
399 "reverse" => Some("@List|Str".to_string()),
400 "scalar" => Some("Scalar".to_string()),
401 "ref" => Some("Str|Undef".to_string()),
402 "bless" => Some("Object".to_string()),
403 "stat" | "lstat" => Some("@Stat".to_string()),
404 "localtime" | "gmtime" => Some("@Time|Str".to_string()),
405 "caller" => Some("@Caller|Hash".to_string()),
406 "wantarray" => Some("Bool|Undef".to_string()),
407 "defined" => Some("Bool".to_string()),
408 "length" | "index" | "rindex" | "substr" => Some("Int".to_string()),
409 "abs" | "int" | "sqrt" | "exp" | "log" | "cos" | "sin" => Some("Num".to_string()),
410 "chr" => Some("Str".to_string()),
411 "ord" => Some("Int".to_string()),
412 "uc" | "lc" | "ucfirst" | "lcfirst" => Some("Str".to_string()),
413 "pack" => Some("Str".to_string()),
414 "unpack" => Some("@Mixed".to_string()),
415 _ => None,
416 }
417}
418
419fn method_return_type(method: &str) -> Option<String> {
421 match method {
422 "new" => Some("Object".to_string()),
423 "count" | "size" | "length" => Some("Int".to_string()),
424 "push" | "unshift" | "splice" => Some("Int".to_string()),
425 "pop" | "shift" => Some("Scalar".to_string()),
426 "keys" | "values" => Some("@List".to_string()),
427 "exists" | "defined" => Some("Bool".to_string()),
428 "delete" => Some("Scalar".to_string()),
429 "fetch" | "get" => Some("Scalar".to_string()),
430 "put" | "set" | "store" => Some("Undef".to_string()),
431 "find" | "search" => Some("@Results|Undef".to_string()),
432 "first" | "next" => Some("Scalar|Undef".to_string()),
433 "all" => Some("@All".to_string()),
434 "each" | "iterator" => Some("Iterator".to_string()),
435 "isa" => Some("Bool".to_string()),
436 "can" => Some("CodeRef|Undef".to_string()),
437 "clone" => Some("Object".to_string()),
438 "to_string" | "as_string" | "stringify" => Some("Str".to_string()),
439 "to_array" | "as_array" | "elements" => Some("@Array".to_string()),
440 "to_hash" | "as_hash" => Some("%Hash".to_string()),
441 _ => None,
442 }
443}
444
445fn builtin_doc_summary(function: &str, param: &str, _param_index: usize) -> Option<String> {
455 let sigs = create_builtin_signatures();
456 let builtin = sigs.get(function)?;
457 if let Some(first_sig) = builtin.signatures.first() {
460 let param_names = extract_param_names(first_sig);
461 if param_names.contains(¶m.to_string()) {
462 return Some(builtin.documentation.to_string());
466 }
467 }
468 None
469}
470
471fn walk_ast<F>(node: &Node, visitor: &mut F) -> bool
472where
473 F: FnMut(&Node) -> bool,
474{
475 if !visitor(node) {
476 return false;
477 }
478
479 for child in get_node_children(node) {
480 if !walk_ast(child, visitor) {
481 return false;
482 }
483 }
484
485 true
486}