Skip to main content

logicaffeine_lsp/
selection_ranges.rs

1//! Selection ranges: expand-selection follows English structure —
2//! word → sentence → block → document.
3
4use tower_lsp::lsp_types::{Position, Range, SelectionRange};
5
6use logicaffeine_language::token::Span;
7
8use crate::document::DocumentState;
9
10pub fn selection_ranges(doc: &DocumentState, positions: &[Position]) -> Vec<SelectionRange> {
11    positions
12        .iter()
13        .map(|position| selection_chain(doc, *position))
14        .collect()
15}
16
17fn selection_chain(doc: &DocumentState, position: Position) -> SelectionRange {
18    let offset = doc.line_index.offset(position);
19
20    let mut spans: Vec<Span> = Vec::new();
21
22    // Word: the token under the cursor.
23    if let Some(token) = doc
24        .tokens
25        .iter()
26        .find(|t| t.span.start <= offset && offset < t.span.end)
27    {
28        spans.push(token.span);
29    }
30
31    // Sentence: the smallest statement containing the cursor.
32    if let Some((_, span)) = doc
33        .symbol_index
34        .statement_spans
35        .iter()
36        .filter(|(_, s)| s.start <= offset && offset < s.end)
37        .min_by_key(|(_, s)| s.end - s.start)
38    {
39        spans.push(*span);
40    }
41
42    // Block: the smallest `##` section containing the cursor.
43    if let Some((_, _, span)) = doc
44        .symbol_index
45        .block_spans
46        .iter()
47        .filter(|(_, _, s)| s.start <= offset && offset < s.end)
48        .min_by_key(|(_, _, s)| s.end - s.start)
49    {
50        spans.push(*span);
51    }
52
53    // Document.
54    spans.push(Span::new(0, doc.source.len()));
55
56    // Keep the chain strictly widening; drop non-nesting or equal steps.
57    let mut widening: Vec<Span> = Vec::new();
58    for span in spans {
59        match widening.last() {
60            Some(prev) if span.start <= prev.start && prev.end <= span.end && span != *prev => {
61                widening.push(span)
62            }
63            None => widening.push(span),
64            _ => {}
65        }
66    }
67
68    // Build innermost-out: the LSP shape is child-with-parent.
69    let mut chain: Option<SelectionRange> = None;
70    for span in widening.into_iter().rev() {
71        chain = Some(SelectionRange {
72            range: Range {
73                start: doc.line_index.position(span.start),
74                end: doc.line_index.position(span.end),
75            },
76            parent: chain.map(Box::new),
77        });
78    }
79    chain.unwrap_or(SelectionRange {
80        range: Range {
81            start: position,
82            end: position,
83        },
84        parent: None,
85    })
86}