use crate::lsp::position::{LineMap, Position, Range};
use crate::span::Span;
use std::sync::Arc;
#[derive(Debug, Clone)]
pub struct SourceFile {
file_name: String,
text: Arc<str>,
line_map: Option<LineMap>,
len: u32,
}
impl SourceFile {
pub fn new(file_name: impl Into<String>, text: impl Into<String>) -> Self {
let text: String = text.into();
let len = text.len() as u32;
let text: Arc<str> = Arc::from(text.into_boxed_str());
Self {
file_name: file_name.into(),
text,
line_map: None,
len,
}
}
pub fn with_line_map(file_name: impl Into<String>, text: impl Into<String>) -> Self {
let text: String = text.into();
let len = text.len() as u32;
let line_map = Some(LineMap::build(&text));
let text: Arc<str> = Arc::from(text.into_boxed_str());
Self {
file_name: file_name.into(),
text,
line_map,
len,
}
}
#[inline]
pub fn file_name(&self) -> &str {
&self.file_name
}
#[inline]
pub fn text(&self) -> &str {
&self.text
}
#[inline]
pub fn as_str(&self) -> &str {
&self.text
}
#[inline]
pub const fn len(&self) -> u32 {
self.len
}
#[inline]
pub const fn is_empty(&self) -> bool {
self.len == 0
}
pub fn char_at(&self, offset: u32) -> Option<char> {
let offset = offset as usize;
if offset >= self.text.len() {
return None;
}
if !self.text.is_char_boundary(offset) {
return None;
}
self.text[offset..].chars().next()
}
#[inline]
pub fn byte_at(&self, offset: u32) -> Option<u8> {
self.text.as_bytes().get(offset as usize).copied()
}
#[inline]
pub fn slice(&self, span: Span) -> &str {
span.slice_safe(&self.text)
}
pub fn slice_range(&self, start: u32, end: u32) -> &str {
Span::new(start, end).slice_safe(&self.text)
}
pub fn slice_from(&self, start: u32) -> &str {
let start = (start as usize).min(self.text.len());
&self.text[start..]
}
pub fn slice_to(&self, end: u32) -> &str {
let end = (end as usize).min(self.text.len());
&self.text[..end]
}
fn ensure_line_map(&mut self) {
if self.line_map.is_none() {
self.line_map = Some(LineMap::build(&self.text));
}
}
pub fn line_map(&mut self) -> &LineMap {
self.ensure_line_map();
self.line_map.as_ref().unwrap()
}
pub fn offset_to_position(&mut self, offset: u32) -> Position {
self.ensure_line_map();
self.line_map
.as_ref()
.unwrap()
.offset_to_position(offset, &self.text)
}
pub fn position_to_offset(&mut self, position: Position) -> Option<u32> {
self.ensure_line_map();
self.line_map
.as_ref()
.unwrap()
.position_to_offset(position, &self.text)
}
pub fn span_to_range(&mut self, span: Span) -> Range {
let start = self.offset_to_position(span.start);
let end = self.offset_to_position(span.end);
Range::new(start, end)
}
pub fn range_to_span(&mut self, range: Range) -> Option<Span> {
let start = self.position_to_offset(range.start)?;
let end = self.position_to_offset(range.end)?;
Some(Span::new(start, end))
}
pub fn line_count(&mut self) -> usize {
self.ensure_line_map();
self.line_map.as_ref().unwrap().line_count()
}
pub fn line_text(&mut self, line: u32) -> Option<&str> {
self.ensure_line_map();
let line_map = self.line_map.as_ref().unwrap();
let start = line_map.line_start(line as usize)? as usize;
let end = if (line as usize) + 1 < line_map.line_count() {
line_map.line_start((line as usize) + 1)? as usize
} else {
self.text.len()
};
let text = &self.text[start..end];
let text = text.strip_suffix("\r\n").unwrap_or(text);
let text = text.strip_suffix('\n').unwrap_or(text);
let text = text.strip_suffix('\r').unwrap_or(text);
Some(text)
}
pub fn into_text(self) -> Arc<str> {
self.text
}
pub fn into_parts(self) -> (String, Arc<str>) {
(self.file_name, self.text)
}
}
impl AsRef<str> for SourceFile {
fn as_ref(&self) -> &str {
&self.text
}
}
impl std::ops::Deref for SourceFile {
type Target = str;
fn deref(&self) -> &Self::Target {
&self.text
}
}
#[derive(Clone, Copy, Debug)]
pub struct SourceFileRef<'a> {
pub file_name: &'a str,
pub text: &'a str,
}
impl<'a> SourceFileRef<'a> {
pub const fn new(file_name: &'a str, text: &'a str) -> Self {
SourceFileRef { file_name, text }
}
pub fn from_source_file(source_file: &'a SourceFile) -> Self {
SourceFileRef {
file_name: &source_file.file_name,
text: source_file.text(),
}
}
pub const fn len(&self) -> u32 {
self.text.len() as u32
}
pub const fn is_empty(&self) -> bool {
self.text.is_empty()
}
pub fn slice(&self, span: Span) -> &'a str {
span.slice_safe(self.text)
}
}
impl<'a> From<&'a SourceFile> for SourceFileRef<'a> {
fn from(source_file: &'a SourceFile) -> Self {
SourceFileRef::from_source_file(source_file)
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
pub struct SourceId(pub u32);
impl SourceId {
pub const UNKNOWN: Self = Self(u32::MAX);
pub const fn new(id: u32) -> Self {
Self(id)
}
pub const fn is_unknown(&self) -> bool {
self.0 == u32::MAX
}
}
impl From<u32> for SourceId {
fn from(id: u32) -> Self {
Self(id)
}
}
impl From<SourceId> for u32 {
fn from(id: SourceId) -> Self {
id.0
}
}
#[derive(Clone, Debug)]
pub struct SourceLocation {
pub file_name: String,
pub span: Span,
pub start_line: u32,
pub start_column: u32,
pub end_line: u32,
pub end_column: u32,
}
impl SourceLocation {
pub const fn new(
file_name: String,
span: Span,
start_line: u32,
start_column: u32,
end_line: u32,
end_column: u32,
) -> Self {
Self {
file_name,
span,
start_line,
start_column,
end_line,
end_column,
}
}
pub fn from_span(source_file: &mut SourceFile, span: Span) -> Self {
let start_pos = source_file.offset_to_position(span.start);
let end_pos = source_file.offset_to_position(span.end);
Self {
file_name: source_file.file_name().to_string(),
span,
start_line: start_pos.line,
start_column: start_pos.character,
end_line: end_pos.line,
end_column: end_pos.character,
}
}
pub fn to_string_short(&self) -> String {
format!(
"{}:{}:{}",
self.file_name,
self.start_line + 1,
self.start_column + 1
)
}
pub fn to_string_visual_studio(&self) -> String {
format!(
"{}({},{})",
self.file_name,
self.start_line + 1,
self.start_column + 1
)
}
}
impl std::fmt::Display for SourceLocation {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.to_string_short())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_source_file_basic() {
let source = SourceFile::new("test.ts", "const x = 42;");
assert_eq!(source.file_name(), "test.ts");
assert_eq!(source.text(), "const x = 42;");
assert_eq!(source.len(), 13);
assert!(!source.is_empty());
}
#[test]
fn test_source_file_empty() {
let source = SourceFile::new("empty.ts", "");
assert!(source.is_empty());
assert_eq!(source.len(), 0);
}
#[test]
fn test_source_file_char_at() {
let source = SourceFile::new("test.ts", "hello");
assert_eq!(source.char_at(0), Some('h'));
assert_eq!(source.char_at(4), Some('o'));
assert_eq!(source.char_at(5), None);
}
#[test]
fn test_source_file_byte_at() {
let source = SourceFile::new("test.ts", "hello");
assert_eq!(source.byte_at(0), Some(b'h'));
assert_eq!(source.byte_at(4), Some(b'o'));
assert_eq!(source.byte_at(5), None);
}
#[test]
fn test_source_file_slice() {
let source = SourceFile::new("test.ts", "hello world");
let span = Span::new(0, 5);
assert_eq!(source.slice(span), "hello");
let span2 = Span::new(6, 11);
assert_eq!(source.slice(span2), "world");
}
#[test]
fn test_source_file_slice_safe() {
let source = SourceFile::new("test.ts", "hello");
let span = Span::new(0, 100); assert_eq!(source.slice(span), "hello");
}
#[test]
fn test_source_file_lines() {
let mut source = SourceFile::new("test.ts", "line1\nline2\nline3");
assert_eq!(source.line_count(), 3);
assert_eq!(source.line_text(0), Some("line1"));
assert_eq!(source.line_text(1), Some("line2"));
assert_eq!(source.line_text(2), Some("line3"));
assert_eq!(source.line_text(3), None);
}
#[test]
fn test_source_file_position_conversion() {
let mut source = SourceFile::new("test.ts", "const x = 1;\nlet y = 2;");
let pos = source.offset_to_position(0);
assert_eq!(pos.line, 0);
assert_eq!(pos.character, 0);
let pos = source.offset_to_position(13); assert_eq!(pos.line, 1);
assert_eq!(pos.character, 0);
let offset = source.position_to_offset(Position::new(1, 4)).unwrap();
assert_eq!(offset, 17); }
#[test]
fn test_source_file_span_to_range() {
let mut source = SourceFile::new("test.ts", "const x = 1;");
let span = Span::new(6, 7); let range = source.span_to_range(span);
assert_eq!(range.start.line, 0);
assert_eq!(range.start.character, 6);
assert_eq!(range.end.line, 0);
assert_eq!(range.end.character, 7);
}
#[test]
fn test_source_file_with_line_map() {
let source = SourceFile::with_line_map("test.ts", "a\nb\nc");
assert!(source.line_map.is_some());
}
#[test]
fn test_source_file_ref() {
let source = SourceFile::new("test.ts", "hello world");
let source_ref = SourceFileRef::from_source_file(&source);
assert_eq!(source_ref.file_name, "test.ts");
assert_eq!(source_ref.text, "hello world");
assert_eq!(source_ref.len(), 11);
}
#[test]
fn test_source_id() {
let id = SourceId::new(42);
assert_eq!(id.0, 42);
assert!(!id.is_unknown());
assert!(SourceId::UNKNOWN.is_unknown());
}
#[test]
fn test_source_location() {
let mut source = SourceFile::new("test.ts", "const x = 42;");
let span = Span::new(6, 7); let location = SourceLocation::from_span(&mut source, span);
assert_eq!(location.file_name, "test.ts");
assert_eq!(location.start_line, 0);
assert_eq!(location.start_column, 6);
assert_eq!(location.to_string_short(), "test.ts:1:7");
assert_eq!(location.to_string_visual_studio(), "test.ts(1,7)");
}
#[test]
fn test_source_file_into_parts() {
let source = SourceFile::new("test.ts", "content");
let (name, text) = source.into_parts();
assert_eq!(name, "test.ts");
assert_eq!(text.as_ref(), "content");
}
#[test]
fn test_source_file_deref() {
let source = SourceFile::new("test.ts", "hello");
assert!(source.starts_with("hel"));
assert_eq!(&*source, "hello");
}
}