#[derive(Debug, Clone)]
pub struct SourceFile {
pub text: String,
pub path: String,
line_starts: Option<Vec<usize>>,
}
impl SourceFile {
pub fn new(text: String, path: String) -> Self {
Self {
text,
path,
line_starts: None,
}
}
pub fn get_line_starts(&mut self) -> &[usize] {
self.line_starts
.get_or_insert_with(|| scan_line_starts(&self.text))
}
pub fn get_line_and_character_of_position(&mut self, position: usize) -> LineAndCharacter {
let starts = self.get_line_starts();
let mut line = binary_search(starts, position);
if line < 0 {
line = !line - 1;
}
let character = position - starts[line as usize];
LineAndCharacter {
line: line as u32,
character,
}
}
}
#[derive(Debug, Clone, Copy)]
pub struct LineAndCharacter {
pub line: u32,
pub character: usize,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SourceFileKind {
Js,
TypeSpec,
}
impl SourceFileKind {
pub fn from_extension(ext: &str) -> Option<Self> {
match ext {
".js" | ".mjs" => Some(SourceFileKind::Js),
".tsp" => Some(SourceFileKind::TypeSpec),
_ => None,
}
}
}
pub fn get_source_file_kind_from_path(path: &str) -> Option<SourceFileKind> {
let ext = get_extension_from_path(path)?;
SourceFileKind::from_extension(&ext)
}
fn get_extension_from_path(path: &str) -> Option<String> {
path.rsplit_once('.').map(|(_, ext)| format!(".{}", ext))
}
fn scan_line_starts(text: &str) -> Vec<usize> {
let mut starts = Vec::new();
let mut start = 0;
let mut pos = 0;
let bytes = text.as_bytes();
while pos < bytes.len() {
let ch = bytes[pos];
pos += 1;
match ch {
0x0d => {
if pos < bytes.len() && bytes[pos] == 0x0a {
pos += 1;
}
starts.push(start);
start = pos;
}
0x0a => {
starts.push(start);
start = pos;
}
_ => {}
}
}
starts.push(start);
starts
}
fn binary_search(array: &[usize], value: usize) -> isize {
let mut low = 0isize;
let mut high = array.len() as isize - 1;
while low <= high {
let middle = low + ((high - low) >> 1);
let v = array[middle as usize];
if v < value {
low = middle + 1;
} else if v > value {
high = middle - 1;
} else {
return middle;
}
}
!low
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_source_file_new() {
let sf = SourceFile::new("model Foo {}".to_string(), "test.tsp".to_string());
assert_eq!(sf.text, "model Foo {}");
assert_eq!(sf.path, "test.tsp");
}
#[test]
fn test_scan_line_starts_empty() {
let starts = scan_line_starts("");
assert_eq!(starts, vec![0]);
}
#[test]
fn test_scan_line_starts_single_line() {
let starts = scan_line_starts("hello");
assert_eq!(starts, vec![0]);
}
#[test]
fn test_scan_line_starts_multiple_lines() {
let starts = scan_line_starts("line1\nline2\nline3");
assert_eq!(starts, vec![0, 6, 12]);
}
#[test]
fn test_scan_line_starts_crlf() {
let starts = scan_line_starts("line1\r\nline2\r\nline3");
assert_eq!(starts, vec![0, 7, 14]);
}
#[test]
fn test_scan_line_starts_mixed() {
let starts = scan_line_starts("line1\nline2\r\nline3\nline4");
assert_eq!(starts, vec![0, 6, 13, 19]);
}
#[test]
fn test_binary_search_found() {
let arr = [1, 3, 5, 7, 9];
assert_eq!(binary_search(&arr, 5), 2);
assert_eq!(binary_search(&arr, 1), 0);
assert_eq!(binary_search(&arr, 9), 4);
}
#[test]
fn test_binary_search_not_found() {
let arr = [1, 3, 5, 7, 9];
let result = binary_search(&arr, 4);
assert!(result < 0);
}
#[test]
fn test_get_line_starts() {
let mut sf = SourceFile::new("line1\nline2\nline3".to_string(), "test.tsp".to_string());
let starts = sf.get_line_starts();
assert_eq!(starts, vec![0, 6, 12]);
}
#[test]
fn test_get_line_and_character() {
let mut sf = SourceFile::new("line1\nline2\nline3".to_string(), "test.tsp".to_string());
let pos = sf.get_line_and_character_of_position(0);
assert_eq!(pos.line, 0);
assert_eq!(pos.character, 0);
let pos = sf.get_line_and_character_of_position(6);
assert_eq!(pos.line, 1);
assert_eq!(pos.character, 0);
let pos = sf.get_line_and_character_of_position(12);
assert_eq!(pos.line, 2);
assert_eq!(pos.character, 0);
let pos = sf.get_line_and_character_of_position(8);
assert_eq!(pos.line, 1);
assert_eq!(pos.character, 2);
}
#[test]
fn test_source_file_kind_from_extension() {
assert_eq!(
SourceFileKind::from_extension(".js"),
Some(SourceFileKind::Js)
);
assert_eq!(
SourceFileKind::from_extension(".mjs"),
Some(SourceFileKind::Js)
);
assert_eq!(
SourceFileKind::from_extension(".tsp"),
Some(SourceFileKind::TypeSpec)
);
assert_eq!(SourceFileKind::from_extension(".txt"), None);
}
#[test]
fn test_get_source_file_kind_from_path() {
assert_eq!(
get_source_file_kind_from_path("file.js"),
Some(SourceFileKind::Js)
);
assert_eq!(
get_source_file_kind_from_path("file.mjs"),
Some(SourceFileKind::Js)
);
assert_eq!(
get_source_file_kind_from_path("file.tsp"),
Some(SourceFileKind::TypeSpec)
);
assert_eq!(get_source_file_kind_from_path("file.ts"), None);
}
}