//! Position tracking utilities for converting byte offsets to line/column //! positions //! //! This module provides efficient conversion between byte offsets and (line, //! column) positions for LSP support. use std::collections::HashMap; /// Position tracker that can quickly convert byte offsets to line/column /// positions #[derive(Clone)] pub struct PositionTracker { /// Map from line number to byte offset where that line starts line_starts: Vec, /// Cache of offset -> (line, col) lookups cache: HashMap, } impl PositionTracker { /// Create a new position tracker from source text pub fn new(source: &str) -> Self { let mut line_starts = vec![0]; // Find all newline positions for (offset, ch) in source.char_indices() { if ch == '\n' { line_starts.push(offset + 1); } } Self { line_starts, cache: HashMap::new(), } } /// Convert a byte offset to (line, column) position /// Returns (line, col) where both are 0-indexed pub fn offset_to_position(&mut self, offset: usize) -> (usize, usize) { // Check cache first if let Some(&pos) = self.cache.get(&offset) { return pos; } // Binary search to find the line let line = match self.line_starts.binary_search(&offset) { | Ok(line) => line, // Exact match - start of a line | Err(line) => line - 1, // Insert position tells us the line }; let line_start = self.line_starts[line]; let col = offset - line_start; self.cache.insert(offset, (line, col)); (line, col) } /// Get the total number of lines pub fn line_count(&self) -> usize { self.line_starts.len() } /// Get the byte offset for the start of a line pub fn line_offset(&self, line: usize) -> Option { self.line_starts.get(line).copied() } } /// Create a Span with proper line/column information from byte offsets pub fn create_span_with_position( tracker: &mut PositionTracker, start: usize, end: usize, ) -> crate::syntax::ast::Span { let (start_line, start_col) = tracker.offset_to_position(start); let (end_line, end_col) = tracker.offset_to_position(end); crate::syntax::ast::Span::with_position(start, end, start_line, start_col, end_line, end_col) } #[cfg(test)] mod tests { use super::*; #[test] fn test_position_tracking() { let source = "line 1\nline 2\nline 3"; let mut tracker = PositionTracker::new(source); // Start of first line assert_eq!(tracker.offset_to_position(0), (0, 0)); // Middle of first line assert_eq!(tracker.offset_to_position(3), (0, 3)); // Start of second line (after first \n) assert_eq!(tracker.offset_to_position(7), (1, 0)); // Start of third line assert_eq!(tracker.offset_to_position(14), (2, 0)); } #[test] fn test_multiline_unicode() { let source = "Hello 世界\nLine 2"; let mut tracker = PositionTracker::new(source); // Start of file assert_eq!(tracker.offset_to_position(0), (0, 0)); // After "Hello " assert_eq!(tracker.offset_to_position(6), (0, 6)); // Start of line 2 let newline_offset = "Hello 世界\n".len(); assert_eq!(tracker.offset_to_position(newline_offset), (1, 0)); } }