1use perl_ast::{Node, NodeKind, SourceLocation};
7
8#[derive(Debug, Clone, Copy, PartialEq, Eq)]
10pub enum DocumentHighlightKind {
11 Text = 1,
13 Read = 2,
15 Write = 3,
17}
18
19#[derive(Debug, Clone, PartialEq, Eq)]
21pub struct DocumentHighlight {
22 pub location: SourceLocation,
24 pub kind: DocumentHighlightKind,
26}
27
28pub struct DocumentHighlightProvider;
30
31impl Default for DocumentHighlightProvider {
32 fn default() -> Self {
33 Self::new()
34 }
35}
36
37impl DocumentHighlightProvider {
38 pub fn new() -> Self {
40 Self
41 }
42
43 pub fn find_highlights(
45 &self,
46 ast: &Node,
47 source: &str,
48 byte_offset: usize,
49 ) -> Vec<DocumentHighlight> {
50 let target_node = self.find_node_at_offset(ast, byte_offset);
52
53 let symbol_info = if let Some(ref node) = target_node {
55 self.extract_symbol_info_with_context(node, source, ast, byte_offset)
58 } else {
59 self.extract_symbol_at_offset(ast, source, byte_offset)
61 };
62
63 let symbol_info = match symbol_info {
64 Some(info) => info,
65 None => return Vec::new(),
66 };
67
68 let mut highlights = Vec::new();
70 self.collect_highlights(ast, source, &symbol_info, &mut highlights);
71
72 self.deduplicate_highlights(highlights)
74 }
75
76 fn deduplicate_highlights(&self, highlights: Vec<DocumentHighlight>) -> Vec<DocumentHighlight> {
78 use std::collections::HashMap;
79
80 let mut by_location: HashMap<(usize, usize), DocumentHighlight> = HashMap::new();
82
83 for h in highlights {
84 let key = (h.location.start, h.location.end);
85 by_location
86 .entry(key)
87 .and_modify(|existing| {
88 if (h.kind as u8) > (existing.kind as u8) {
90 *existing = h.clone();
91 }
92 })
93 .or_insert(h);
94 }
95
96 let mut result: Vec<_> = by_location.into_values().collect();
98 result.sort_by_key(|h| h.location.start);
99 result
100 }
101
102 fn find_node_at_offset(&self, node: &Node, offset: usize) -> Option<Node> {
104 if offset < node.location.start || offset >= node.location.end {
106 return None;
107 }
108
109 if let Some(children) = self.get_children(node) {
111 for child in children {
112 if let Some(found) = self.find_node_at_offset(child, offset) {
113 return Some(found);
114 }
115 }
116 }
117
118 if self.is_symbol_node(node) {
120 return Some(node.clone());
121 }
122
123 None
124 }
125
126 fn extract_symbol_at_offset(
129 &self,
130 node: &Node,
131 source: &str,
132 offset: usize,
133 ) -> Option<SymbolInfo> {
134 if offset < node.location.start || offset >= node.location.end {
135 return None;
136 }
137
138 if let NodeKind::Try { catch_blocks, .. } = &node.kind {
140 for (param, _) in catch_blocks {
141 if let Some(var_str) = param {
142 let node_source = source.get(node.location.start..node.location.end)?;
144 let relative_offset = offset - node.location.start;
145 for (pos, _) in node_source.match_indices(var_str.as_str()) {
147 if pos <= relative_offset && relative_offset < pos + var_str.len() {
148 let first_char = var_str.chars().next()?;
149 if matches!(first_char, '$' | '@' | '%') {
150 return Some(SymbolInfo {
151 name: var_str.get(1..)?.to_string(),
152 sigil: Some(first_char.to_string()),
153 is_method: false,
154 is_function: false,
155 });
156 }
157 }
158 }
159 }
160 }
161 }
162
163 if let NodeKind::Subroutine { name: Some(sub_name), name_span: Some(span), .. } = &node.kind
165 {
166 if offset >= span.start && offset <= span.end {
167 return Some(SymbolInfo {
168 name: sub_name.clone(),
169 sigil: None,
170 is_method: false,
171 is_function: true,
172 });
173 }
174 }
175
176 if let Some(children) = self.get_children(node) {
178 for child in children {
179 if let Some(info) = self.extract_symbol_at_offset(child, source, offset) {
180 return Some(info);
181 }
182 }
183 }
184
185 None
186 }
187
188 fn get_children<'a>(&self, node: &'a Node) -> Option<Vec<&'a Node>> {
190 match &node.kind {
191 NodeKind::Program { statements } => Some(statements.iter().collect()),
192 NodeKind::VariableDeclaration { variable, initializer, .. } => {
193 let mut children = vec![variable.as_ref()];
194 if let Some(init) = initializer {
195 children.push(init.as_ref());
196 }
197 Some(children)
198 }
199 NodeKind::VariableListDeclaration { variables, initializer, .. } => {
200 let mut children: Vec<&Node> = variables.iter().collect();
201 if let Some(init) = initializer {
202 children.push(init.as_ref());
203 }
204 Some(children)
205 }
206 NodeKind::Assignment { lhs, rhs, .. } => Some(vec![lhs.as_ref(), rhs.as_ref()]),
207 NodeKind::Binary { left, right, .. } => Some(vec![left.as_ref(), right.as_ref()]),
208 NodeKind::Unary { operand, .. } => Some(vec![operand.as_ref()]),
209 NodeKind::MethodCall { object, args, .. } => {
210 let mut children = vec![object.as_ref()];
211 children.extend(args.iter().map(|a| a as &Node));
212 Some(children)
213 }
214 NodeKind::FunctionCall { args, .. } => Some(args.iter().collect()),
215 NodeKind::Block { statements } => Some(statements.iter().collect()),
216 NodeKind::If { condition, then_branch, elsif_branches, else_branch } => {
217 let mut children = vec![condition.as_ref(), then_branch.as_ref()];
218 for (cond, branch) in elsif_branches {
219 children.push(cond.as_ref());
220 children.push(branch.as_ref());
221 }
222 if let Some(else_b) = else_branch {
223 children.push(else_b.as_ref());
224 }
225 Some(children)
226 }
227 NodeKind::For { init, condition, update, body, .. } => {
228 let mut children = Vec::new();
229 if let Some(i) = init {
230 children.push(i.as_ref());
231 }
232 if let Some(c) = condition {
233 children.push(c.as_ref());
234 }
235 if let Some(u) = update {
236 children.push(u.as_ref());
237 }
238 children.push(body.as_ref());
239 Some(children)
240 }
241 NodeKind::Foreach { variable, list, body, continue_block } => {
242 if let Some(cb) = continue_block {
243 Some(vec![variable.as_ref(), list.as_ref(), body.as_ref(), cb.as_ref()])
244 } else {
245 Some(vec![variable.as_ref(), list.as_ref(), body.as_ref()])
246 }
247 }
248 NodeKind::While { condition, body, .. } => {
249 Some(vec![condition.as_ref(), body.as_ref()])
250 }
251 NodeKind::Subroutine { body, signature, .. } => {
252 let mut children = Vec::new();
253 if let Some(sig) = signature {
254 if let NodeKind::Signature { parameters } = &sig.kind {
256 children.extend(parameters.iter());
257 } else {
258 children.push(sig.as_ref());
259 }
260 }
261 children.push(body.as_ref());
262 Some(children)
263 }
264 NodeKind::Return { value } => value.as_ref().map(|v| vec![v.as_ref()]),
265 NodeKind::ArrayLiteral { elements } => Some(elements.iter().collect()),
266 NodeKind::HashLiteral { pairs } => {
267 let mut children = Vec::new();
268 for (k, v) in pairs {
269 children.push(k);
270 children.push(v);
271 }
272 Some(children)
273 }
274 NodeKind::Ternary { condition, then_expr, else_expr } => {
275 Some(vec![condition.as_ref(), then_expr.as_ref(), else_expr.as_ref()])
276 }
277 NodeKind::VariableWithAttributes { variable, .. } => Some(vec![variable.as_ref()]),
278 NodeKind::ExpressionStatement { expression } => Some(vec![expression.as_ref()]),
279 NodeKind::StatementModifier { statement, condition, .. } => {
281 Some(vec![statement.as_ref(), condition.as_ref()])
282 }
283 NodeKind::Match { expr, .. }
285 | NodeKind::Substitution { expr, .. }
286 | NodeKind::Transliteration { expr, .. } => Some(vec![expr.as_ref()]),
287 NodeKind::Given { expr, body } => Some(vec![expr.as_ref(), body.as_ref()]),
289 NodeKind::When { condition, body } => Some(vec![condition.as_ref(), body.as_ref()]),
290 NodeKind::Default { body } => Some(vec![body.as_ref()]),
291 NodeKind::LabeledStatement { statement, .. } => Some(vec![statement.as_ref()]),
292 NodeKind::Eval { block } | NodeKind::Do { block } => Some(vec![block.as_ref()]),
294 NodeKind::Try { body, catch_blocks, finally_block } => {
296 let mut children = vec![body.as_ref()];
297 for (_, catch_body) in catch_blocks {
298 children.push(catch_body.as_ref());
299 }
300 if let Some(finally) = finally_block {
301 children.push(finally.as_ref());
302 }
303 Some(children)
304 }
305 NodeKind::Method { body, signature, .. } => {
307 let mut children = Vec::new();
308 if let Some(sig) = signature {
309 if let NodeKind::Signature { parameters } = &sig.kind {
311 children.extend(parameters.iter());
312 } else {
313 children.push(sig.as_ref());
314 }
315 }
316 children.push(body.as_ref());
317 Some(children)
318 }
319 NodeKind::IndirectCall { object, args, .. } => {
321 let mut children = vec![object.as_ref()];
322 children.extend(args.iter());
323 Some(children)
324 }
325 NodeKind::Class { body, .. } => Some(vec![body.as_ref()]),
327 NodeKind::Signature { parameters } => Some(parameters.iter().collect()),
329 NodeKind::MandatoryParameter { variable } => Some(vec![variable.as_ref()]),
330 NodeKind::OptionalParameter { variable, default_value } => {
331 Some(vec![variable.as_ref(), default_value.as_ref()])
332 }
333 NodeKind::SlurpyParameter { variable } => Some(vec![variable.as_ref()]),
334 NodeKind::NamedParameter { variable } => Some(vec![variable.as_ref()]),
335 _ => None,
336 }
337 }
338
339 fn is_symbol_node(&self, node: &Node) -> bool {
341 matches!(
342 node.kind,
343 NodeKind::Variable { .. }
344 | NodeKind::FunctionCall { .. }
345 | NodeKind::MethodCall { .. }
346 | NodeKind::Identifier { .. }
347 )
348 }
349
350 fn extract_symbol_info(&self, node: &Node, source: &str) -> Option<SymbolInfo> {
352 match &node.kind {
353 NodeKind::Variable { sigil, name } => Some(SymbolInfo {
354 name: name.clone(),
355 sigil: Some(sigil.clone()),
356 is_method: false,
357 is_function: false,
358 }),
359 NodeKind::Identifier { name } => Some(SymbolInfo {
360 name: name.clone(),
361 sigil: None,
362 is_method: false,
363 is_function: false,
364 }),
365 NodeKind::FunctionCall { name, .. } => Some(SymbolInfo {
366 name: name.clone(),
367 sigil: None,
368 is_method: false,
369 is_function: true,
370 }),
371 NodeKind::MethodCall { method, .. } => Some(SymbolInfo {
372 name: method.clone(),
373 sigil: None,
374 is_method: true,
375 is_function: false,
376 }),
377 _ => {
378 let text = source.get(node.location.start..node.location.end)?;
380 let first = text.chars().next();
382 match first {
383 Some(sigil @ ('$' | '@' | '%')) => Some(SymbolInfo {
384 name: text.get(1..).unwrap_or("").to_string(),
385 sigil: Some(sigil.to_string()),
386 is_method: false,
387 is_function: false,
388 }),
389 _ => None,
390 }
391 }
392 }
393 }
394
395 fn extract_symbol_info_with_context(
403 &self,
404 node: &Node,
405 source: &str,
406 ast: &Node,
407 byte_offset: usize,
408 ) -> Option<SymbolInfo> {
409 let base_info = self.extract_symbol_info(node, source)?;
410
411 if base_info.sigil.as_deref() != Some("$") {
413 return Some(base_info);
414 }
415
416 if let Some(bare_name) = base_info.name.strip_prefix('#') {
418 if !bare_name.is_empty() {
419 return Some(SymbolInfo {
420 name: bare_name.to_string(),
421 sigil: Some("@".to_string()),
422 is_method: false,
423 is_function: false,
424 });
425 }
426 }
427
428 if let Some(parent_op) = self.find_subscript_parent(ast, byte_offset) {
430 match parent_op.as_str() {
431 "[]" => {
432 return Some(SymbolInfo {
433 name: base_info.name,
434 sigil: Some("@".to_string()),
435 is_method: false,
436 is_function: false,
437 });
438 }
439 "{}" => {
440 return Some(SymbolInfo {
441 name: base_info.name,
442 sigil: Some("%".to_string()),
443 is_method: false,
444 is_function: false,
445 });
446 }
447 _ => {}
448 }
449 }
450
451 Some(base_info)
452 }
453
454 fn find_subscript_parent(&self, node: &Node, offset: usize) -> Option<String> {
458 if offset < node.location.start || offset >= node.location.end {
459 return None;
460 }
461
462 if let NodeKind::Binary { op, left, .. } = &node.kind {
464 if (op == "[]" || op == "{}")
465 && offset >= left.location.start
466 && offset < left.location.end
467 {
468 if let NodeKind::Variable { sigil, .. } = &left.kind {
470 if sigil == "$" {
471 return Some(op.clone());
472 }
473 }
474 }
475 }
476
477 if let Some(children) = self.get_children(node) {
479 for child in children {
480 if let Some(op) = self.find_subscript_parent(child, offset) {
481 return Some(op);
482 }
483 }
484 }
485
486 None
487 }
488
489 fn collect_highlights(
491 &self,
492 node: &Node,
493 source: &str,
494 target: &SymbolInfo,
495 highlights: &mut Vec<DocumentHighlight>,
496 ) {
497 self.collect_highlights_with_parent(node, source, target, highlights, None);
498 }
499
500 fn collect_highlights_with_parent(
502 &self,
503 node: &Node,
504 source: &str,
505 target: &SymbolInfo,
506 highlights: &mut Vec<DocumentHighlight>,
507 parent: Option<&Node>,
508 ) {
509 if self.node_matches_symbol(node, source, target) {
511 let kind = self.determine_highlight_kind_with_parent(node, parent);
512 highlights.push(DocumentHighlight { location: node.location, kind });
514 }
515
516 if let NodeKind::Variable { sigil, name } = &node.kind {
523 if !self.node_matches_symbol(node, source, target) {
524 if let Some(target_sigil) = &target.sigil {
525 let cross_match =
526 self.is_cross_sigil_match(sigil, name, target_sigil, &target.name, parent);
527 if cross_match {
528 let kind = self.determine_highlight_kind_with_parent(node, parent);
529 highlights.push(DocumentHighlight { location: node.location, kind });
530 }
531 }
532 }
533 }
534
535 if let NodeKind::Subroutine { name: Some(sub_name), name_span: Some(span), .. } = &node.kind
537 {
538 if target.is_function && sub_name == &target.name {
539 highlights.push(DocumentHighlight {
540 location: *span,
541 kind: DocumentHighlightKind::Write,
542 });
543 }
544 }
545
546 if let Some(children) = self.get_children(node) {
548 for child in children {
549 self.collect_highlights_with_parent(child, source, target, highlights, Some(node));
550 }
551 }
552
553 if let NodeKind::Try { catch_blocks, body, .. } = &node.kind {
555 if let Some(target_sigil) = &target.sigil {
556 let expected = format!("{}{}", target_sigil, target.name);
557 let mut search_from = body.location.end;
558 for (param, catch_body) in catch_blocks {
559 if let Some(var_str) = param {
560 if var_str == &expected {
561 let search_end = catch_body.location.start;
563 if search_from < search_end && search_end <= source.len() {
564 if let Some(search_area) = source.get(search_from..search_end) {
565 if let Some(pos) = search_area.find(var_str.as_str()) {
566 let var_start = search_from + pos;
567 highlights.push(DocumentHighlight {
568 location: SourceLocation {
569 start: var_start,
570 end: var_start + var_str.len(),
571 },
572 kind: DocumentHighlightKind::Write,
573 });
574 }
575 }
576 }
577 }
578 }
579 search_from = catch_body.location.end;
580 }
581 }
582 }
583
584 if let NodeKind::String { interpolated: true, .. } = &node.kind {
586 if let Some(target_sigil) = &target.sigil {
587 let expected = format!("{}{}", target_sigil, target.name);
588 if let Some(node_text) = source.get(node.location.start..node.location.end) {
589 for (pos, _) in node_text.match_indices(expected.as_str()) {
590 let end_pos = pos + expected.len();
592 if end_pos < node_text.len() {
593 let next = node_text.as_bytes()[end_pos];
594 if next.is_ascii_alphanumeric() || next == b'_' {
595 continue;
596 }
597 }
598 let abs_start = node.location.start + pos;
599 if abs_start == node.location.start
601 && node.location.end == abs_start + expected.len()
602 {
603 continue;
604 }
605 highlights.push(DocumentHighlight {
606 location: SourceLocation {
607 start: abs_start,
608 end: abs_start + expected.len(),
609 },
610 kind: DocumentHighlightKind::Read,
611 });
612 }
613 }
614 }
615 }
616 }
617
618 fn is_cross_sigil_match(
627 &self,
628 sigil: &str,
629 name: &str,
630 target_sigil: &str,
631 target_name: &str,
632 parent: Option<&Node>,
633 ) -> bool {
634 if target_sigil == "@" && sigil == "$" {
637 if let Some(bare) = name.strip_prefix('#') {
638 if bare == target_name {
639 return true;
640 }
641 }
642 }
643 if name != target_name {
649 return false;
650 }
651
652 if let Some(parent_node) = parent {
653 if let NodeKind::Binary { op, .. } = &parent_node.kind {
654 if target_sigil == "%" && sigil == "$" && op == "{}" {
656 return true;
657 }
658 if target_sigil == "%" && sigil == "@" && op == "{}" {
660 return true;
661 }
662 if target_sigil == "@" && sigil == "$" && op == "[]" {
664 return true;
665 }
666 }
669 }
670
671 false
672 }
673
674 fn node_matches_symbol(&self, node: &Node, source: &str, target: &SymbolInfo) -> bool {
676 match &node.kind {
677 NodeKind::Variable { sigil, name } => {
678 if let Some(target_sigil) = &target.sigil {
679 sigil == target_sigil && name == &target.name
680 } else {
681 false
682 }
683 }
684 NodeKind::Identifier { name } => {
685 !target.is_method && target.sigil.is_none() && name == &target.name
686 }
687 NodeKind::FunctionCall { name, .. } => target.is_function && name == &target.name,
688 NodeKind::MethodCall { method, .. } => target.is_method && method == &target.name,
689 _ => {
690 if let Some(target_sigil) = &target.sigil {
692 let expected = format!("{}{}", target_sigil, target.name);
693 source
694 .get(node.location.start..node.location.end)
695 .is_some_and(|text| text == expected)
696 } else {
697 false
698 }
699 }
700 }
701 }
702
703 fn determine_highlight_kind_with_parent(
705 &self,
706 node: &Node,
707 parent: Option<&Node>,
708 ) -> DocumentHighlightKind {
709 match &node.kind {
712 NodeKind::Variable { .. } => {
713 if let Some(parent_node) = parent {
715 match &parent_node.kind {
716 NodeKind::VariableDeclaration { variable, .. } => {
718 if std::ptr::eq(variable.as_ref(), node) {
719 DocumentHighlightKind::Write
720 } else {
721 DocumentHighlightKind::Read
722 }
723 }
724 NodeKind::VariableListDeclaration { variables, .. } => {
726 if variables.iter().any(|v| std::ptr::eq(v, node)) {
727 DocumentHighlightKind::Write
728 } else {
729 DocumentHighlightKind::Read
730 }
731 }
732 NodeKind::Assignment { lhs, .. } => {
734 if std::ptr::eq(lhs.as_ref(), node) {
735 DocumentHighlightKind::Write
736 } else {
737 DocumentHighlightKind::Read
738 }
739 }
740 NodeKind::Unary { op, operand, .. } => {
742 if (op == "++" || op == "--") && std::ptr::eq(operand.as_ref(), node) {
743 DocumentHighlightKind::Write
744 } else {
745 DocumentHighlightKind::Read
746 }
747 }
748 NodeKind::Foreach { variable, .. } => {
750 if std::ptr::eq(variable.as_ref(), node) {
751 DocumentHighlightKind::Write
752 } else {
753 DocumentHighlightKind::Read
754 }
755 }
756 NodeKind::MandatoryParameter { variable }
758 | NodeKind::SlurpyParameter { variable }
759 | NodeKind::NamedParameter { variable } => {
760 if std::ptr::eq(variable.as_ref(), node) {
761 DocumentHighlightKind::Write
762 } else {
763 DocumentHighlightKind::Read
764 }
765 }
766 NodeKind::OptionalParameter { variable, .. } => {
767 if std::ptr::eq(variable.as_ref(), node) {
768 DocumentHighlightKind::Write
769 } else {
770 DocumentHighlightKind::Read
771 }
772 }
773 _ => DocumentHighlightKind::Read,
775 }
776 } else {
777 DocumentHighlightKind::Read
779 }
780 }
781 _ => DocumentHighlightKind::Read,
782 }
783 }
784}
785
786struct SymbolInfo {
788 name: String,
789 sigil: Option<String>,
790 is_method: bool,
791 is_function: bool,
792}