use crate::ids::DocumentId;
use std::fmt;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct SourceSpan {
pub start: usize,
pub end: usize,
}
impl SourceSpan {
pub fn new(start: usize, end: usize) -> Self {
Self { start, end }
}
pub fn len(&self) -> usize {
self.end.saturating_sub(self.start)
}
pub fn is_empty(&self) -> bool {
self.start >= self.end
}
}
#[derive(Debug, Clone)]
pub struct SourceRef {
pub doc_id: DocumentId,
pub span: SourceSpan,
pub schema_defaults_doc: Option<DocumentId>,
}
impl SourceRef {
pub fn new(doc_id: DocumentId, span: SourceSpan) -> Self {
Self {
doc_id,
span,
schema_defaults_doc: None,
}
}
pub fn defaults_doc(&self) -> DocumentId {
self.schema_defaults_doc.unwrap_or(self.doc_id)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SourceLocation {
pub base_uri: String,
pub line: usize, pub column: usize, }
impl fmt::Display for SourceLocation {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}:{}:{}", self.base_uri, self.line, self.column)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum SourceRetention {
#[default]
Retain,
DropText,
DropAll,
}
#[derive(Debug, Clone)]
pub struct SourceMap {
pub base_uri: String,
pub text: String,
pub line_starts: Vec<usize>,
}
impl SourceMap {
pub fn new(base_uri: String, text: String) -> Self {
let line_starts = build_line_starts(text.as_bytes());
Self {
base_uri,
text,
line_starts,
}
}
pub fn locate(&self, offset: usize) -> SourceLocation {
let (line, line_start) = self.find_line(offset);
let column = self.count_utf8_chars(line_start, offset) + 1;
SourceLocation {
base_uri: self.base_uri.clone(),
line,
column,
}
}
fn find_line(&self, offset: usize) -> (usize, usize) {
match self.line_starts.binary_search(&offset) {
Ok(idx) => (idx + 1, offset), Err(idx) => {
if idx == 0 {
(1, 0)
} else {
(idx, self.line_starts[idx - 1])
}
}
}
}
fn count_utf8_chars(&self, start: usize, end: usize) -> usize {
let end = end.min(self.text.len());
let start = start.min(end);
self.text[start..end].chars().count()
}
pub fn get_text(&self, span: &SourceSpan) -> Option<&str> {
if span.end <= self.text.len() && span.start <= span.end {
Some(&self.text[span.start..span.end])
} else {
None
}
}
pub fn into_compact(self) -> CompactSourceMap {
CompactSourceMap {
base_uri: self.base_uri,
line_starts: self.line_starts,
text_len: self.text.len(),
}
}
}
#[derive(Debug, Clone)]
pub struct CompactSourceMap {
pub base_uri: String,
pub line_starts: Vec<usize>,
pub text_len: usize, }
impl CompactSourceMap {
pub fn locate(&self, offset: usize) -> SourceLocation {
let (line, line_start) = self.find_line(offset);
let column = offset.saturating_sub(line_start) + 1;
SourceLocation {
base_uri: self.base_uri.clone(),
line,
column,
}
}
fn find_line(&self, offset: usize) -> (usize, usize) {
match self.line_starts.binary_search(&offset) {
Ok(idx) => (idx + 1, offset),
Err(idx) => {
if idx == 0 {
(1, 0)
} else {
(idx, self.line_starts[idx - 1])
}
}
}
}
}
#[derive(Debug, Default)]
pub enum SourceMapStorage {
Full(Vec<SourceMap>),
Compact(Vec<CompactSourceMap>),
#[default]
None,
}
impl SourceMapStorage {
pub fn new() -> Self {
SourceMapStorage::Full(Vec::new())
}
pub fn add(&mut self, map: SourceMap) -> DocumentId {
match self {
SourceMapStorage::Full(maps) => {
let id = maps.len() as DocumentId;
maps.push(map);
id
}
SourceMapStorage::Compact(maps) => {
let id = maps.len() as DocumentId;
maps.push(map.into_compact());
id
}
SourceMapStorage::None => 0, }
}
pub fn locate(&self, source_ref: &SourceRef) -> Option<SourceLocation> {
match self {
SourceMapStorage::Full(maps) => {
let map = maps.get(source_ref.doc_id as usize)?;
Some(map.locate(source_ref.span.start))
}
SourceMapStorage::Compact(maps) => {
let map = maps.get(source_ref.doc_id as usize)?;
Some(map.locate(source_ref.span.start))
}
SourceMapStorage::None => None,
}
}
pub fn get_text(&self, doc_id: DocumentId, span: &SourceSpan) -> Option<&str> {
match self {
SourceMapStorage::Full(maps) => {
let map = maps.get(doc_id as usize)?;
map.get_text(span)
}
_ => None,
}
}
pub fn compact(&mut self) {
if let SourceMapStorage::Full(maps) = self {
let compact_maps = maps.drain(..).map(|map| map.into_compact()).collect();
*self = SourceMapStorage::Compact(compact_maps);
}
}
pub fn drop_all(&mut self) {
*self = SourceMapStorage::None;
}
pub fn is_empty(&self) -> bool {
match self {
SourceMapStorage::Full(maps) => maps.is_empty(),
SourceMapStorage::Compact(maps) => maps.is_empty(),
SourceMapStorage::None => true,
}
}
pub fn len(&self) -> usize {
match self {
SourceMapStorage::Full(maps) => maps.len(),
SourceMapStorage::Compact(maps) => maps.len(),
SourceMapStorage::None => 0,
}
}
}
pub fn build_line_starts(bytes: &[u8]) -> Vec<usize> {
let mut line_starts = vec![0];
let mut i = 0;
while i < bytes.len() {
match bytes[i] {
b'\n' => {
line_starts.push(i + 1);
i += 1;
}
b'\r' => {
if i + 1 < bytes.len() && bytes[i + 1] == b'\n' {
line_starts.push(i + 2);
i += 2;
} else {
line_starts.push(i + 1);
i += 1;
}
}
_ => {
i += 1;
}
}
}
line_starts
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_build_line_starts_lf() {
let bytes = b"line1\nline2\nline3";
let starts = build_line_starts(bytes);
assert_eq!(starts, vec![0, 6, 12]);
}
#[test]
fn test_build_line_starts_crlf() {
let bytes = b"line1\r\nline2\r\nline3";
let starts = build_line_starts(bytes);
assert_eq!(starts, vec![0, 7, 14]);
}
#[test]
fn test_build_line_starts_cr() {
let bytes = b"line1\rline2\rline3";
let starts = build_line_starts(bytes);
assert_eq!(starts, vec![0, 6, 12]);
}
#[test]
fn test_build_line_starts_mixed() {
let bytes = b"line1\nline2\r\nline3\rline4";
let starts = build_line_starts(bytes);
assert_eq!(starts, vec![0, 6, 13, 19]);
}
#[test]
fn test_source_map_locate() {
let source = "line1\nline2\nline3".to_string();
let map = SourceMap::new("test.xsd".to_string(), source);
let loc = map.locate(0);
assert_eq!(loc.line, 1);
assert_eq!(loc.column, 1);
let loc = map.locate(6);
assert_eq!(loc.line, 2);
assert_eq!(loc.column, 1);
let loc = map.locate(8);
assert_eq!(loc.line, 2);
assert_eq!(loc.column, 3);
}
#[test]
fn test_source_map_utf8_columns() {
let source = "Hello 世界\nNext line".to_string();
let map = SourceMap::new("test.xsd".to_string(), source);
let loc = map.locate(6);
assert_eq!(loc.line, 1);
assert_eq!(loc.column, 7); }
#[test]
fn test_source_map_get_text() {
let source = "line1\nline2\nline3".to_string();
let map = SourceMap::new("test.xsd".to_string(), source);
let span = SourceSpan::new(0, 5);
assert_eq!(map.get_text(&span), Some("line1"));
let span = SourceSpan::new(6, 11);
assert_eq!(map.get_text(&span), Some("line2"));
}
#[test]
fn test_source_map_storage() {
let mut storage = SourceMapStorage::new();
let map1 = SourceMap::new("test1.xsd".to_string(), "line1\nline2".to_string());
let doc_id = storage.add(map1);
let source_ref = SourceRef::new(doc_id, SourceSpan::new(0, 5));
let loc = storage.locate(&source_ref).unwrap();
assert_eq!(loc.line, 1);
assert_eq!(loc.column, 1);
storage.compact();
let loc = storage.locate(&source_ref).unwrap();
assert_eq!(loc.line, 1);
assert!(loc.column > 0);
assert!(storage.get_text(doc_id, &SourceSpan::new(0, 5)).is_none());
}
}