use super::Span;
use crate::sass::{FormalArgs, Name};
use std::fmt::{self, Write};
use std::str::from_utf8;
use std::sync::Arc;
#[derive(Clone, Eq, PartialEq, PartialOrd, Ord)]
pub struct SourcePos {
p: Arc<SourcePosImpl>,
}
#[derive(Clone, Debug, Eq, PartialEq, PartialOrd, Ord)]
struct SourcePosImpl {
line_no: u32,
line_pos: usize,
length: usize,
line: String,
file: SourceName,
}
impl SourcePos {
pub fn from_to(start: Span, end: Span) -> Self {
let mut result = SourcePosImpl::from(start);
result.length =
std::cmp::max(1, end.location_offset() - start.location_offset());
SourcePos {
p: Arc::new(result),
}
}
pub(crate) fn in_call(&self, name: &str) -> Self {
SourcePos {
p: Arc::new(SourcePosImpl {
file: SourceName::called(name, self.clone()),
..(*self.p).clone()
}),
}
}
pub(crate) fn mock_function(
name: &Name,
args: &FormalArgs,
module: &str,
) -> Self {
SourcePos::mock_impl(name, "@function", args, module)
}
pub(crate) fn mock_mixin(
name: &Name,
args: &FormalArgs,
module: &str,
) -> Self {
SourcePos::mock_impl(name, "@mixin", args, module)
}
fn mock_impl(
name: &Name,
kind: &str,
args: &FormalArgs,
module: &str,
) -> Self {
let line = format!("{} {}{} {{", kind, name, args);
let line_pos = kind.chars().count() + 2;
let length = line.chars().count() - 1 - line_pos;
SourcePos {
p: Arc::new(SourcePosImpl {
line,
line_no: 1,
line_pos,
length,
file: SourceName::root(module),
}),
}
}
pub fn show(&self, out: &mut impl Write) -> fmt::Result {
self.show_impl(out, "", '^', "")?;
self.show_files(out)
}
pub fn show_detail(
&self,
out: &mut impl Write,
marker: char,
what: &str,
) -> fmt::Result {
let filename = if self.p.file.name.is_empty() {
String::new()
} else {
format!("--> {}", self.p.file.name)
};
self.show_impl(out, &filename, marker, what)
}
fn show_impl(
&self,
out: &mut impl Write,
arrow: &str,
marker: char,
what: &str,
) -> fmt::Result {
let lnw = self.p.line_no.to_string().len();
write!(out, "{0:lnw$} ,{arrow}", "", arrow = arrow, lnw = lnw)?;
self.show_inner(out, lnw, marker, what)?;
write!(out, "{0:lnw$} '", "", lnw = lnw)
}
pub(crate) fn show_inner(
&self,
out: &mut impl Write,
lnw: usize,
marker: char,
what: &str,
) -> fmt::Result {
writeln!(
out,
"\n{ln:<lnw$} | {line}\
\n{0:lnw$} |{0:>lpos$}{mark}{what}",
"",
line = self.p.line,
ln = self.p.line_no,
lnw = lnw,
lpos = self.p.line_pos,
mark = marker.to_string().repeat(self.p.length),
what = what,
)
}
pub fn show_files(&self, out: &mut impl Write) -> fmt::Result {
let mut nextpos = Some(self);
let mut lines = Vec::new();
while let Some(pos) = nextpos {
lines.push((
format!(
"{file} {row}:{col}",
file = pos.p.file.name(),
row = pos.p.line_no,
col = pos.p.line_pos,
),
pos.p.file.imported.to_string(),
));
nextpos = pos.p.file.imported.next();
}
if let Some(whatw) = lines.iter().map(|(what, _why)| what.len()).max()
{
for (what, why) in lines {
write!(
out,
"\n{0:lnw$} {what:whatw$} {why}",
"",
lnw = self.p.line_no.to_string().len(),
what = what,
whatw = whatw,
why = why,
)?;
}
}
Ok(())
}
pub(crate) fn opt_back(mut self, s: &str) -> Self {
let p: &mut SourcePosImpl = Arc::make_mut(&mut self.p);
if p.line[..p.line_pos - 1].ends_with(s) {
let len = s.chars().count();
p.line_pos -= len;
p.length += len;
}
self
}
pub(crate) fn opt_in_calc(mut self) -> Self {
let p: &mut SourcePosImpl = Arc::make_mut(&mut self.p);
let s = "calc(";
let part = &p.line[p.line_pos - 1..];
if part.starts_with(s) && part.chars().nth(p.length - 1) == Some(')')
{
let len = s.chars().count();
p.line_pos += len;
p.length -= len;
p.length -= 1;
}
self
}
pub fn is_builtin(&self) -> bool {
self.p.file.is_builtin()
}
pub(crate) fn same_file_as(&self, other: &Self) -> bool {
self.p.file.name == other.p.file.name
}
pub(crate) fn file_url(&self) -> &str {
&self.p.file.name
}
}
impl From<Span<'_>> for SourcePos {
fn from(span: Span) -> Self {
SourcePos {
p: Arc::new(SourcePosImpl::from(span)),
}
}
}
impl From<Span<'_>> for SourcePosImpl {
fn from(span: Span) -> Self {
SourcePosImpl {
line: from_utf8(span.get_line_beginning())
.unwrap_or("<<failed to display line>>")
.trim_end()
.to_string(),
line_no: span.location_line(),
line_pos: span.get_utf8_column(),
length: 1,
file: span.extra.clone(),
}
}
}
impl fmt::Debug for SourcePos {
fn fmt(&self, out: &mut fmt::Formatter) -> fmt::Result {
out.write_char('{')?;
let chars = self.p.line.chars();
out.write_char('"')?;
for (i, c) in chars.enumerate() {
if i + 1 == self.p.line_pos {
out.write_char('[')?;
}
write!(out, "{}", c.escape_debug())?;
if i + 1 + 1 == self.p.line_pos + self.p.length {
out.write_char(']')?;
}
}
out.write_char('"')?;
out.write_char(',')?;
let mut nextpos = Some(self);
while let Some(pos) = nextpos {
write!(
out,
" {}:{} {}",
pos.p.file.name, pos.p.line_no, pos.p.file.imported
)?;
nextpos = pos.p.file.imported.next();
}
out.write_char('}')
}
}
#[derive(Clone, Debug, Eq, PartialEq, PartialOrd, Ord)]
pub struct SourceName {
name: String,
imported: SourceKind,
}
impl SourceName {
pub fn root<T: ToString>(name: T) -> Self {
SourceName {
name: name.to_string(),
imported: SourceKind::Root,
}
}
pub fn imported<T: ToString>(name: T, from: SourcePos) -> Self {
SourceName {
name: name.to_string(),
imported: SourceKind::Imported(from),
}
}
pub fn used<T: ToString>(name: T, from: SourcePos) -> Self {
SourceName {
name: name.to_string(),
imported: SourceKind::Used(from),
}
}
pub fn load_css<T: ToString>(name: T, from: SourcePos) -> Self {
SourceName {
name: name.to_string(),
imported: SourceKind::Called("load-css".to_string(), from),
}
}
pub fn called<T: ToString>(name: T, from: SourcePos) -> Self {
SourceName {
name: from.p.file.name.clone(),
imported: SourceKind::Called(name.to_string(), from),
}
}
pub fn name(&self) -> &str {
&self.name
}
pub fn is_builtin(&self) -> bool {
self.name.starts_with("sass:") || self.name.is_empty()
}
}
#[derive(Clone, Debug, Eq, PartialEq, PartialOrd, Ord)]
enum SourceKind {
Root,
Imported(SourcePos),
Used(SourcePos),
Called(String, SourcePos),
}
impl SourceKind {
fn next(&self) -> Option<&SourcePos> {
match self {
SourceKind::Root => None,
SourceKind::Imported(pos) => Some(pos),
SourceKind::Used(pos) => Some(pos),
SourceKind::Called(_, pos) => Some(pos),
}
}
}
impl fmt::Display for SourceKind {
fn fmt(&self, out: &mut fmt::Formatter) -> fmt::Result {
match self {
SourceKind::Root => out.write_str("root stylesheet"),
SourceKind::Imported(_) => out.write_str("@import"),
SourceKind::Used(_) => out.write_str("@use"),
SourceKind::Called(name, _) => write!(out, "{}()", name),
}
}
}