use tower_lsp_server::ls_types::{Position, Range};
pub struct LineIndex {
line_starts: Vec<usize>,
}
impl LineIndex {
pub fn new(text: &str) -> Self {
let mut line_starts = vec![0usize];
for (i, b) in text.bytes().enumerate() {
if b == b'\n' {
line_starts.push(i + 1);
}
}
Self { line_starts }
}
pub fn position_of(&self, offset: usize) -> Position {
let line = self
.line_starts
.partition_point(|&start| start <= offset)
.saturating_sub(1);
let col = offset - self.line_starts[line];
Position::new(line as u32, col as u32)
}
pub fn line_range(&self, line: usize) -> (usize, usize) {
let start = self.line_starts.get(line).copied().unwrap_or(0);
let end = self
.line_starts
.get(line + 1)
.map(|&s| s.saturating_sub(1))
.unwrap_or(start);
(start, end)
}
}
pub fn resolve_path(text: &str, index: &LineIndex, path: &str) -> Range {
if path == "/" || path.is_empty() {
return first_content_line_range(text, index);
}
let segments: Vec<&str> = path.strip_prefix('/').unwrap_or(path).split('/').collect();
if segments.is_empty() {
return first_content_line_range(text, index);
}
let lines: Vec<&str> = text.lines().collect();
let mut current_indent: i32 = -1; let mut search_start_line = 0usize;
let mut last_matched_line: Option<usize> = None;
for segment in &segments {
let array_index: Option<usize> = segment.parse().ok();
let mut found = false;
let mut line_num = search_start_line;
while line_num < lines.len() {
let line = lines[line_num];
let trimmed = line.trim();
if trimmed.is_empty() || trimmed.starts_with('#') {
line_num += 1;
continue;
}
let indent = (line.len() - line.trim_start().len()) as i32;
if indent <= current_indent && found {
break;
}
if indent <= current_indent {
line_num += 1;
continue;
}
if let Some(idx) = array_index {
if trimmed.starts_with("- ") && indent > current_indent {
let mut count = 0usize;
for (offset, sl) in lines[search_start_line..].iter().enumerate() {
let scan_line = search_start_line + offset;
let st = sl.trim();
if st.is_empty() || st.starts_with('#') {
continue;
}
let si = (sl.len() - sl.trim_start().len()) as i32;
if si < indent {
continue;
}
if si == indent && st.starts_with("- ") {
if count == idx {
last_matched_line = Some(scan_line);
search_start_line = scan_line + 1;
current_indent = indent;
found = true;
break;
}
count += 1;
}
if si < indent && count > 0 {
break;
}
}
break;
}
} else {
let key_pattern = format!("{segment}:");
if trimmed.starts_with(&key_pattern) || trimmed == *segment {
last_matched_line = Some(line_num);
search_start_line = line_num + 1;
current_indent = indent;
found = true;
break;
}
}
line_num += 1;
}
if !found && last_matched_line.is_none() {
break;
}
}
match last_matched_line {
Some(line_num) => {
let (start, end) = index.line_range(line_num);
Range::new(index.position_of(start), index.position_of(end))
}
None => first_content_line_range(text, index),
}
}
fn first_content_line_range(text: &str, index: &LineIndex) -> Range {
for (i, line) in text.lines().enumerate() {
let trimmed = line.trim();
if !trimmed.is_empty() && !trimmed.starts_with('#') && trimmed != "---" {
let (start, end) = index.line_range(i);
return Range::new(index.position_of(start), index.position_of(end));
}
}
Range::new(Position::new(0, 0), Position::new(0, 0))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn resolve_root_path() {
let text = "title: Test\nstatus: test\n";
let index = LineIndex::new(text);
let range = resolve_path(text, &index, "/");
assert_eq!(range.start.line, 0);
}
#[test]
fn resolve_top_level_key() {
let text = "title: Test\nstatus: experimental\nlevel: high\n";
let index = LineIndex::new(text);
let range = resolve_path(text, &index, "/status");
assert_eq!(range.start.line, 1);
let range = resolve_path(text, &index, "/level");
assert_eq!(range.start.line, 2);
}
#[test]
fn resolve_nested_key() {
let text = "\
title: Test
logsource:
category: test
product: windows
detection:
selection:
CommandLine|contains: whoami
condition: selection
";
let index = LineIndex::new(text);
let range = resolve_path(text, &index, "/logsource/category");
assert_eq!(range.start.line, 2);
let range = resolve_path(text, &index, "/detection/condition");
assert_eq!(range.start.line, 7);
}
#[test]
fn resolve_array_index() {
let text = "\
title: Test
tags:
- attack.execution
- attack.t1059
- cve.2024.1234
";
let index = LineIndex::new(text);
let range = resolve_path(text, &index, "/tags/1");
assert_eq!(range.start.line, 3);
}
}