use lsp_types::{Position, Range, SelectionRange};
fn byte_offset(text: &str, pos: Position) -> usize {
let mut off = 0usize;
for (line, l) in text.split_inclusive('\n').enumerate() {
if line as u32 == pos.line {
let mut col = 0u32;
for (i, ch) in l.char_indices() {
if col == pos.character {
return off + i;
}
col += ch.len_utf16() as u32;
}
return off + l.len();
}
off += l.len();
}
off
}
fn make_range(text: &str, start: usize, end: usize) -> Range {
let start = start.min(text.len());
let end = end.min(text.len());
let mut line = 0u32;
let mut col = 0u32;
let mut i = 0usize;
let mut s = Position::new(0, 0);
let mut e = Position::new(0, 0);
let mut found_start = false;
let mut found_end = false;
for ch in text.chars() {
if i == start {
s = Position::new(line, col);
found_start = true;
}
if i == end {
e = Position::new(line, col);
found_end = true;
break;
}
i += ch.len_utf8();
if ch == '\n' {
line += 1;
col = 0;
} else {
col += ch.len_utf16() as u32;
}
}
if !found_start {
s = Position::new(line, col);
}
if !found_end {
e = Position::new(line, col);
}
Range::new(s, e)
}
fn word_span(bytes: &[u8], off: usize) -> (usize, usize) {
let safe_off = off.min(bytes.len().saturating_sub(1));
let start = (0..=safe_off)
.rev()
.find(|&i| {
i == 0
|| (!bytes[i - 1].is_ascii_alphanumeric()
&& bytes[i - 1] != b'_'
&& bytes[i - 1] != b'$'
&& bytes[i - 1] != b'@'
&& bytes[i - 1] != b'%')
})
.unwrap_or(off);
let end = (off..bytes.len())
.find(|&i| !bytes[i].is_ascii_alphanumeric() && bytes[i] != b'_')
.unwrap_or(bytes.len());
(start, end)
}
fn string_span(text: &str, off: usize) -> Option<(usize, usize, usize, usize)> {
let bytes = text.as_bytes();
for &q in b"\"'" {
let mut open = None;
for i in (0..off).rev() {
if bytes[i] == q {
let mut backslashes = 0usize;
let mut j = i;
while j > 0 && bytes[j - 1] == b'\\' {
backslashes += 1;
j -= 1;
}
if backslashes.is_multiple_of(2) {
open = Some(i);
break;
}
}
if bytes[i] == b'\n' {
break;
}
}
if let Some(open_pos) = open {
let mut i = off;
while i < bytes.len() {
if bytes[i] == q {
let mut backslashes = 0usize;
let mut j = i;
while j > 0 && bytes[j - 1] == b'\\' {
backslashes += 1;
j -= 1;
}
if backslashes.is_multiple_of(2) {
let content_start = open_pos + 1;
let content_end = i;
let full_start = open_pos;
let full_end = i + 1;
return Some((content_start, content_end, full_start, full_end));
}
}
if bytes[i] == b'\n' {
break;
}
i += 1;
}
}
}
None
}
fn hash_access_span(text: &str, off: usize) -> Option<(usize, usize, usize, usize, usize, usize)> {
let bytes = text.as_bytes();
let mut open = None;
let mut depth = 0i32;
for i in (0..off).rev() {
if bytes[i] == b'}' {
depth += 1;
} else if bytes[i] == b'{' {
if depth == 0 {
open = Some(i);
break;
}
depth -= 1;
}
}
let open_pos = open?;
if open_pos == 0 {
return None;
}
let before = &text[..open_pos];
let trimmed_before = before.trim_end();
let looks_like_hash = trimmed_before.ends_with(|c: char| c.is_ascii_alphanumeric() || c == '_')
|| trimmed_before.ends_with("->");
if !looks_like_hash {
return None;
}
let mut close = None;
let mut depth = 0i32;
for (i, &b) in bytes.iter().enumerate().skip(off) {
if b == b'{' {
depth += 1;
} else if b == b'}' {
if depth == 0 {
close = Some(i);
break;
}
depth -= 1;
}
}
let close_pos = close?;
let key_start = open_pos + 1;
let key_end = close_pos;
let subscript_start = open_pos;
let subscript_end = close_pos + 1;
let mut expr_start = open_pos;
while expr_start > 0 && bytes[expr_start - 1] == b' ' {
expr_start -= 1;
}
if expr_start >= 2 && &bytes[expr_start - 2..expr_start] == b"->" {
expr_start -= 2;
while expr_start > 0
&& (bytes[expr_start - 1].is_ascii_alphanumeric() || bytes[expr_start - 1] == b'_')
{
expr_start -= 1;
}
}
while expr_start > 0
&& (bytes[expr_start - 1].is_ascii_alphanumeric() || bytes[expr_start - 1] == b'_')
{
expr_start -= 1;
}
if expr_start > 0
&& (bytes[expr_start - 1] == b'$'
|| bytes[expr_start - 1] == b'@'
|| bytes[expr_start - 1] == b'%')
{
expr_start -= 1;
}
Some((key_start, key_end, subscript_start, subscript_end, expr_start, subscript_end))
}
fn sub_definition_span(
text: &str,
off: usize,
) -> Option<(usize, usize, Option<(usize, usize)>, usize, usize)> {
let bytes = text.as_bytes();
let sub_keyword = text[..off.min(text.len())].rfind("sub ")?;
let name_start = sub_keyword + 4;
let name_start = text[name_start..]
.find(|c: char| !c.is_whitespace())
.map(|i| name_start + i)
.unwrap_or(name_start);
let mut name_end = name_start;
while name_end < bytes.len()
&& (bytes[name_end].is_ascii_alphanumeric() || bytes[name_end] == b'_')
{
name_end += 1;
}
if off < sub_keyword {
return None;
}
let after_name = &text[name_end..];
let sig_span = if let Some(paren_off) = after_name.find('(') {
let sig_start = name_end + paren_off;
let mut depth = 0i32;
let mut sig_end = sig_start;
for (i, b) in bytes[sig_start..].iter().enumerate() {
if *b == b'(' {
depth += 1;
} else if *b == b')' {
depth -= 1;
if depth == 0 {
sig_end = sig_start + i + 1;
break;
}
}
}
if sig_end > sig_start { Some((sig_start, sig_end)) } else { None }
} else {
None
};
let func_end = {
let mut depth = 0i32;
let mut found_brace = false;
text[sub_keyword..]
.char_indices()
.find(|(_, c)| {
if *c == '{' {
found_brace = true;
depth += 1;
} else if *c == '}' && found_brace {
depth -= 1;
if depth == 0 {
return true;
}
}
false
})
.map(|(i, _)| sub_keyword + i + 1)
.unwrap_or(text.len())
};
Some((name_start, name_end, sig_span, sub_keyword, func_end))
}
fn build_chain(text: &str, spans: &[(usize, usize)]) -> SelectionRange {
let mut ranges: Vec<Range> = Vec::new();
for &(s, e) in spans {
let r = make_range(text, s, e);
if ranges.last().is_none_or(|prev| *prev != r) {
ranges.push(r);
}
}
let mut chain = SelectionRange { range: Range::default(), parent: None };
for r in ranges.into_iter().rev() {
chain = SelectionRange { range: r, parent: Some(Box::new(chain)) };
}
strip_default_tail(chain)
}
fn strip_default_tail(mut sel: SelectionRange) -> SelectionRange {
if sel.parent.is_none() && sel.range == Range::default() {
return sel;
}
if let Some(ref mut p) = sel.parent {
if p.parent.is_none() && p.range == Range::default() {
sel.parent = None;
} else {
**p = strip_default_tail(*p.clone());
}
}
sel
}
pub fn selection_ranges(text: &str, positions: &[Position]) -> Vec<SelectionRange> {
positions
.iter()
.map(|&pos| {
let off = byte_offset(text, pos);
let bytes = text.as_bytes();
let mut spans: Vec<(usize, usize)> = Vec::new();
let (w_start, w_end) = word_span(bytes, off);
spans.push((w_start, w_end));
if let Some((cs, ce, fs, fe)) = string_span(text, off) {
if cs <= w_start && ce >= w_end && (cs != w_start || ce != w_end) {
spans.push((cs, ce));
}
spans.push((fs, fe));
}
if let Some((ks, ke, ss, se, es, ee)) = hash_access_span(text, off) {
if ks <= w_start && ke >= w_end && (ks != w_start || ke != w_end) {
spans.push((ks, ke));
}
spans.push((ss, se));
spans.push((es, ee));
}
let line_start = text[..off].rfind('\n').map(|i| i + 1).unwrap_or(0);
let line_end = text[off..].find('\n').map(|i| off + i).unwrap_or(text.len());
let line_text = &text[line_start..line_end];
let trim_left = line_text.find(|c: char| !c.is_whitespace()).unwrap_or(0);
let trim_right = line_text
.rfind(|c: char| !c.is_whitespace())
.map(|i| i + 1)
.unwrap_or(line_text.len());
spans.push((line_start + trim_left, line_start + trim_right));
spans.push((line_start, line_end));
let stmt_start = text[..off]
.rfind(';')
.map(|i| {
text[i + 1..]
.chars()
.position(|c| !c.is_whitespace())
.map(|j| i + 1 + j)
.unwrap_or(i + 1)
})
.unwrap_or(0);
let stmt_end = text[off..]
.find(';')
.map(|i| off + i + 1)
.unwrap_or_else(|| text[off..].find('\n').map(|i| off + i).unwrap_or(text.len()));
spans.push((stmt_start, stmt_end));
let block_start = text[..off].rfind('{').unwrap_or(0);
let block_end = text[off..].find('}').map(|i| off + i + 1).unwrap_or(text.len());
if block_end > block_start {
spans.push((block_start, block_end));
}
if let Some((name_s, name_e, sig_span, full_s, full_e)) = sub_definition_span(text, off)
{
if off >= name_s && off <= name_e {
spans.push((name_s, name_e));
}
if let Some((sig_s, sig_e)) = sig_span {
spans.push((name_s, sig_e));
if off >= sig_s && off <= sig_e {
spans.push((sig_s, sig_e));
}
}
spans.push((full_s, full_e));
} else {
spans.push((0, text.len()));
}
spans.push((0, text.len()));
spans.sort_by(|a, b| {
let size_a = a.1.saturating_sub(a.0);
let size_b = b.1.saturating_sub(b.0);
size_a.cmp(&size_b)
});
spans.dedup();
spans.retain(|&(s, e)| s <= off && e >= off);
let mut filtered: Vec<(usize, usize)> = Vec::new();
for span in &spans {
if let Some(prev) = filtered.last() {
if span.0 <= prev.0 && span.1 >= prev.1 && (span.0 < prev.0 || span.1 > prev.1)
{
filtered.push(*span);
}
} else {
filtered.push(*span);
}
}
if filtered.is_empty() {
filtered.push((0, text.len()));
}
build_chain(text, &filtered)
})
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
fn chain_to_vec(sel: &SelectionRange) -> Vec<(u32, u32, u32, u32)> {
let mut out = Vec::new();
let mut cur = sel;
loop {
let r = &cur.range;
out.push((r.start.line, r.start.character, r.end.line, r.end.character));
if let Some(ref p) = cur.parent {
cur = p;
} else {
break;
}
}
out
}
#[test]
fn string_content_expands_to_full_string() {
let text = r#"my $x = "hello";"#;
let pos = Position::new(0, 10);
let results = selection_ranges(text, &[pos]);
assert_eq!(results.len(), 1);
let chain = chain_to_vec(&results[0]);
assert!(chain.len() >= 3, "expected at least 3 levels for string, got {}", chain.len());
for window in chain.windows(2) {
let inner = window[0];
let outer = window[1];
assert!(
outer.0 <= inner.0 && outer.2 >= inner.2,
"parent ({},{})..({},{}) must encompass child ({},{})..({},{})",
outer.0,
outer.1,
outer.2,
outer.3,
inner.0,
inner.1,
inner.2,
inner.3,
);
}
}
#[test]
fn hash_access_key_expands() {
let text = r#"my $v = $hash{key};"#;
let pos = Position::new(0, 14);
let results = selection_ranges(text, &[pos]);
assert_eq!(results.len(), 1);
let chain = chain_to_vec(&results[0]);
assert!(
chain.len() >= 3,
"expected at least 3 levels for hash access, got {}",
chain.len()
);
for window in chain.windows(2) {
let inner = window[0];
let outer = window[1];
assert!(outer.0 <= inner.0 && outer.2 >= inner.2, "parent must encompass child");
}
}
#[test]
fn function_name_expands_to_full_sub() {
let text = "sub greet ($name) {\n print $name;\n}\n";
let pos = Position::new(0, 5); let results = selection_ranges(text, &[pos]);
assert_eq!(results.len(), 1);
let chain = chain_to_vec(&results[0]);
assert!(
chain.len() >= 2,
"expected at least 2 levels for function name, got {}",
chain.len()
);
let last = &chain[chain.len() - 1];
assert_eq!(last.0, 0, "outermost should start at line 0");
}
#[test]
fn empty_text_returns_zero_range() {
let text = "";
let pos = Position::new(0, 0);
let results = selection_ranges(text, &[pos]);
assert_eq!(results.len(), 1);
}
#[test]
fn multiple_positions() {
let text = "my $x = 1;\nmy $y = 2;\n";
let positions = vec![Position::new(0, 3), Position::new(1, 3)];
let results = selection_ranges(text, &positions);
assert_eq!(results.len(), 2);
}
}