use std::{
fmt,
path::{Path, PathBuf},
};
use text_size::{TextRange, TextSize};
use super::FileId;
#[derive(Debug, Clone, PartialEq, Eq, derive_more::From)]
pub enum FileOverride {
None,
Number(u32),
Path(String),
}
impl FileOverride {
pub fn is_none(&self) -> bool {
matches!(self, Self::None)
}
}
impl Default for FileOverride {
fn default() -> Self {
Self::None
}
}
impl fmt::Display for FileOverride {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
FileOverride::None => Ok(()),
FileOverride::Number(number) => write!(f, "{}", number),
FileOverride::Path(path) => write!(f, "{}", path),
}
}
}
pub trait Resolver {
fn resolve(&self, offset: TextSize) -> (u32, u32);
}
impl<'s> Resolver for &'s str {
fn resolve(&self, offset: TextSize) -> (u32, u32) {
let offset: usize = offset.into();
let offset = if offset >= self.len() {
self.len().max(1) - 1
} else {
offset
};
let line_start = line_span::find_line_start(self, offset);
let line_index = self
.bytes()
.take(line_start)
.filter(|c| *c == b'\n')
.count();
let pos_index = offset - line_start;
(line_index as _, pos_index as _)
}
}
pub trait HasFileNumber {
fn current_file(&self) -> FileId;
}
pub trait FileIdResolver {
fn resolve(&self, file_id: FileId) -> Option<&Path>;
}
#[derive(Default)]
pub struct LocatedBuilder {
pos: TextRange,
current_file: Option<FileId>,
path: Option<PathBuf>,
file_override: FileOverride,
line_number: u32,
column: u32,
}
impl LocatedBuilder {
pub fn new() -> Self {
Self::default()
}
pub fn pos(self, pos: impl Into<TextRange>) -> Self {
Self {
pos: pos.into(),
..self
}
}
pub fn current_file(self, file: impl Into<FileId>) -> Self {
Self {
current_file: Some(file.into()),
..self
}
}
pub fn path(self, path: impl Into<PathBuf>) -> Self {
Self {
path: Some(path.into()),
..self
}
}
pub fn file_override(self, file_override: impl Into<FileOverride>) -> Self {
Self {
file_override: file_override.into(),
..self
}
}
pub fn line_number(self, line_number: u32) -> Self {
Self {
line_number,
..self
}
}
pub fn column(self, column: u32) -> Self {
Self { column, ..self }
}
pub fn resolve(self, resolver: &impl Resolver) -> Self {
let (line, col) = resolver.resolve(self.pos.start());
Self {
line_number: line,
column: col,
..self
}
}
pub fn resolve_file(self, resolver: &(impl Resolver + HasFileNumber)) -> Self {
self.resolve(resolver).current_file(resolver.current_file())
}
pub fn resolve_path(self, resolver: &impl FileIdResolver) -> Self {
Self {
path: self
.current_file
.and_then(|current_file| resolver.resolve(current_file).map(Path::to_owned)),
..self
}
}
pub fn finish<E>(self, inner: E) -> Located<E> {
Located {
inner,
pos: self.pos,
current_file: self.current_file,
path: self.path,
file_override: self.file_override,
line_number: self.line_number,
column: self.column,
}
}
}
#[derive(Debug)]
pub struct Located<E> {
inner: E,
pos: TextRange,
current_file: Option<FileId>,
path: Option<PathBuf>,
file_override: FileOverride,
line_number: u32,
column: u32,
}
impl<E> Located<E> {
pub fn builder() -> LocatedBuilder {
LocatedBuilder::default()
}
pub fn map<F>(self, f: impl FnOnce(E) -> F) -> Located<F> {
Located {
inner: f(self.inner),
pos: self.pos,
current_file: self.current_file,
path: self.path,
file_override: self.file_override,
line_number: self.line_number,
column: self.column,
}
}
pub fn inner(&self) -> &E {
&self.inner
}
pub fn into_inner(self) -> E {
self.inner
}
pub fn current_file(&self) -> Option<FileId> {
self.current_file
}
pub fn set_current_file(&mut self, current_file: FileId) {
self.current_file = Some(current_file);
}
pub fn pos(&self) -> TextRange {
self.pos
}
pub fn line(&self) -> u32 {
self.line_number
}
pub fn col(&self) -> u32 {
self.column
}
}
impl<E: Clone> Clone for Located<E> {
fn clone(&self) -> Self {
Self {
inner: self.inner.clone(),
pos: self.pos,
current_file: self.current_file,
path: self.path.clone(),
file_override: self.file_override.clone(),
line_number: self.line_number,
column: self.column,
}
}
}
impl<E: PartialEq> PartialEq for Located<E> {
fn eq(&self, other: &Self) -> bool {
self.inner == other.inner
&& self.pos == other.pos
&& self.current_file == other.current_file
&& self.path == other.path
&& self.file_override == other.file_override
&& self.line_number == other.line_number
&& self.column == other.column
}
}
impl<E: Eq> Eq for Located<E> {}
impl<E: std::error::Error> std::error::Error for Located<E> {}
impl<E: std::fmt::Display> std::fmt::Display for Located<E> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
if self.file_override.is_none() {
if let Some(path) = self.path.as_ref() {
write!(f, "{}:", path.display())?;
} else if let Some(current_file) = self.current_file {
write!(f, "{}:", current_file)?;
}
} else {
write!(f, "{}:", self.file_override)?;
}
write!(
f,
"{}:{}: {}",
self.line_number + 1,
self.column + 1,
self.inner
)
}
}
#[cfg(test)]
mod tests {
use super::Resolver;
use std::convert::TryInto;
#[test]
fn resolved_position() {
let s = r#"
Hello,
World"#;
let offset = s.find('r').unwrap().try_into().unwrap();
let resolved = s.resolve(offset);
assert_eq!(resolved.0, 2);
assert_eq!(resolved.1, 2);
}
#[test]
fn resolved_position_last_char() {
let s = r#"
Hello,
World"#;
let offset = s.find('d').unwrap().try_into().unwrap();
let resolved = s.resolve(offset);
assert_eq!(resolved.0, 2);
assert_eq!(resolved.1, 4);
}
#[test]
fn resolved_position_out_of_bounds() {
let offset = 1.into();
assert_eq!("".resolve(offset).0, 0);
}
}