use lsp_types::Range;
use std::collections::HashMap;
use std::fs;
use std::path::{Path, PathBuf};
#[derive(Debug, Default)]
pub struct SourceCache {
lines: HashMap<PathBuf, Vec<String>>,
}
impl SourceCache {
pub fn line_content(&mut self, path: &Path, line_index: usize) -> String {
self.lines(path)
.get(line_index)
.cloned()
.unwrap_or_else(|| "<line unavailable>".to_string())
}
pub fn range_content(&mut self, path: &Path, range: &Range) -> String {
let lines = self.lines(path);
let Some(start_line) = usize::try_from(range.start.line).ok() else {
return "<line unavailable>".to_string();
};
let Some(end_line) = usize::try_from(range.end.line).ok() else {
return "<line unavailable>".to_string();
};
if start_line > end_line || end_line >= lines.len() {
return "<line unavailable>".to_string();
}
let mut chunks = Vec::new();
for line_index in start_line..=end_line {
let Some(line) = lines.get(line_index) else {
return "<line unavailable>".to_string();
};
let start_char = if line_index == start_line {
usize::try_from(range.start.character).ok()
} else {
Some(0)
};
let end_char = if line_index == end_line {
usize::try_from(range.end.character).ok()
} else {
Some(line.chars().count())
};
let Some(start_byte) =
start_char.and_then(|value| byte_index_for_character(line, value))
else {
return "<line unavailable>".to_string();
};
let Some(end_byte) = end_char.and_then(|value| byte_index_for_character(line, value))
else {
return "<line unavailable>".to_string();
};
if start_byte > end_byte {
return "<line unavailable>".to_string();
}
chunks.push(line[start_byte..end_byte].to_string());
}
chunks.join("\n")
}
pub fn range_content_with_leading_comments(&mut self, path: &Path, range: &Range) -> String {
let Some(start_line) = usize::try_from(range.start.line).ok() else {
return "<line unavailable>".to_string();
};
let adjusted_start_line = comment_start_line(self.lines(path), start_line);
let mut range = *range;
if adjusted_start_line != start_line {
let Some(line) = u32::try_from(adjusted_start_line).ok() else {
return "<line unavailable>".to_string();
};
range.start.line = line;
range.start.character = 0;
}
self.range_content(path, &range)
}
fn lines(&mut self, path: &Path) -> &Vec<String> {
self.lines.entry(path.to_path_buf()).or_insert_with(|| {
fs::read_to_string(path)
.map(|contents| contents.lines().map(ToString::to_string).collect())
.unwrap_or_default()
})
}
}
fn byte_index_for_character(line: &str, character: usize) -> Option<usize> {
if character == line.chars().count() {
return Some(line.len());
}
line.char_indices().nth(character).map(|(index, _)| index)
}
fn comment_start_line(lines: &[String], start_line: usize) -> usize {
let mut line_index = start_line;
while line_index > 0 {
let previous_line = lines[line_index - 1].trim();
if previous_line.is_empty() {
break;
}
if is_line_comment(previous_line) {
line_index -= 1;
continue;
}
if previous_line.ends_with("*/") {
line_index = block_comment_start_line(lines, line_index - 1);
continue;
}
break;
}
line_index
}
fn is_line_comment(line: &str) -> bool {
line.starts_with("//")
|| (line.starts_with('#') && !line.starts_with("#!["))
|| line.starts_with("--")
}
fn block_comment_start_line(lines: &[String], end_line: usize) -> usize {
let mut line_index = end_line;
loop {
if lines[line_index].contains("/*") {
return line_index;
}
if line_index == 0 {
return 0;
}
line_index -= 1;
}
}