1use crate::input::{SourceFile, SourceName};
2use crate::parser::{DebugBytes, Span};
3use crate::sass::{FormalArgs, Name};
4use std::cmp::Ordering;
5use std::fmt::{self, Write};
6use std::ops::Range;
7
8#[derive(Clone)]
14pub struct SourcePos {
15 start: usize,
16 end: usize,
17 source: SourceFile,
18}
19
20impl SourcePos {
21 pub(crate) fn new_range(source: SourceFile, range: Range<usize>) -> Self {
22 Self {
23 start: range.start,
24 end: range.end,
25 source,
26 }
27 }
28 pub(crate) fn borrow(&self) -> Span {
29 Span::new_range(&self.source, self.range())
30 }
31 fn range(&self) -> Range<usize> {
32 self.start..self.end
33 }
34 pub fn fragment(&self) -> &[u8] {
36 &self.source.data()[self.range()]
37 }
38 pub fn is_at_end(&self) -> bool {
40 self.start == self.source.data().len()
41 }
42
43 pub fn line_no(&self) -> usize {
45 self.source.data()[0..self.start]
46 .iter()
47 .filter(|c| c == &&b'\n')
48 .count()
49 + 1
50 }
51 fn line_pos(&self) -> usize {
52 let data = self.source.data();
53 let start = (0..self.start)
54 .rev()
55 .find(|i| data.get(*i) == Some(&b'\n'))
56 .map_or(0, |n| n + 1);
57 self.start - start + 1
58 }
59
60 pub fn show(&self, out: &mut impl Write) -> fmt::Result {
67 self.show_impl(out, "", '^', "")?;
68 self.show_files(out)
69 }
70
71 pub fn show_two(
73 out: &mut fmt::Formatter,
74 one: &Self,
75 one_name: &str,
76 other: &Self,
77 other_name: &str,
78 ) -> fmt::Result {
79 if one.file_url() == other.file_url() {
80 if one < other {
81 Self::show_in_file(out, one, one_name, other, other_name)
82 } else {
83 Self::show_in_file(out, other, other_name, one, one_name)
84 }?;
85 } else {
86 one.show_detail(out, '^', one_name)?;
87 writeln!(out)?;
88 other.show_detail(out, '=', other_name)?;
89 }
90 one.show_files(out)
91 }
92
93 fn show_in_file(
94 out: &mut fmt::Formatter,
95 first: &Self,
96 first_name: &str,
97 second: &Self,
98 second_name: &str,
99 ) -> fmt::Result {
100 let ellipsis = first.line_no() + 1 < second.line_no();
101 let lnw = second.line_no().to_string().len();
102 let lnw = if ellipsis { std::cmp::max(3, lnw) } else { lnw };
103 writeln!(out, "{0:lnw$} ,", "", lnw = lnw)?;
104 first.show_inner(out, lnw, '=', first_name)?;
105 if ellipsis {
106 writeln!(out, "... |")?;
107 }
108 second.show_inner(out, lnw, '^', second_name)?;
109 write!(out, "{0:lnw$} '", "", lnw = lnw)?;
110 Ok(())
111 }
112
113 fn show_detail(
114 &self,
115 out: &mut impl Write,
116 marker: char,
117 what: &str,
118 ) -> fmt::Result {
119 let filename = self.file_url();
120 let filename = if filename.is_empty() {
121 String::new()
122 } else {
123 format!("--> {filename}")
124 };
125 self.show_impl(out, &filename, marker, what)
126 }
127
128 fn show_impl(
129 &self,
130 out: &mut impl Write,
131 arrow: &str,
132 marker: char,
133 what: &str,
134 ) -> fmt::Result {
135 let lnw = self.line_no().to_string().len();
136 writeln!(out, "{0:lnw$} ,{arrow}", "", arrow = arrow, lnw = lnw)?;
137 self.show_inner(out, lnw, marker, what)?;
138 write!(out, "{0:lnw$} '", "", lnw = lnw)
139 }
140 fn show_inner(
141 &self,
142 out: &mut impl Write,
143 lnw: usize,
144 marker: char,
145 what: &str,
146 ) -> fmt::Result {
147 let data = self.source.data();
148 let start = (0..self.start)
149 .rev()
150 .find(|i| data.get(*i) == Some(&b'\n'))
151 .map_or(0, |n| n + 1);
152 let end = (self.start..)
153 .find(|i| data.get(*i).map_or(true, |b| b == &b'\n'))
154 .unwrap();
155 let line = &data[start..end];
156 let line_no = self.line_no();
157 write!(
158 out,
159 "{ln:<lnw$} | {line}\
160 \n{0:lnw$} | {0:>lpos$}{mark}",
161 "",
162 line = String::from_utf8_lossy(line).trim_end_matches('\r'),
163 ln = line_no,
164 lnw = lnw,
165 lpos = self.start - start, mark = marker.to_string().repeat((self.end - self.start).max(1)), )?;
168 if what.is_empty() {
169 writeln!(out)
170 } else {
171 writeln!(out, " {what}")
172 }
173 }
174
175 fn show_files(&self, out: &mut impl Write) -> fmt::Result {
177 let mut nextpos = Some(self.clone());
178 let mut lines = Vec::new();
179 while let Some(pos) = nextpos {
180 lines.push((
181 format!(
182 "{file} {row}:{col}",
183 file = pos.file_url(),
184 row = pos.line_no(),
185 col = pos.line_pos(),
186 ),
187 pos.source.source().imported.to_string(),
188 ));
189 nextpos = pos.source.source().imported.next().cloned();
190 }
191 if let Some(whatw) = lines.iter().map(|(what, _why)| what.len()).max()
192 {
193 for (what, why) in lines {
194 write!(
195 out,
196 "\n{0:lnw$} {what:whatw$} {why}",
197 "",
198 lnw = self.line_no().to_string().len(),
199 what = what,
200 whatw = whatw,
201 why = why,
202 )?;
203 }
204 }
205 Ok(())
206 }
207
208 pub(crate) fn opt_back(mut self, s: &str) -> Self {
210 let len = s.len();
211 if self.source.data().get(self.start - len..self.start)
212 == Some(s.as_bytes())
213 {
214 self.start -= len;
215 }
216 self
217 }
218
219 pub(crate) fn opt_in_calc(mut self) -> Self {
224 let s = b"calc(";
225 let part = &self.fragment();
226 if part.starts_with(s) && part.ends_with(b")") {
227 self.start += s.len();
228 self.end -= 1;
229 }
230 self
231 }
232
233 pub(crate) fn opt_trail_ws(mut self) -> Self {
235 while self.source.data().get(self.end) == Some(&b' ') {
236 self.end += 1;
237 }
238 self
239 }
240
241 pub(crate) fn in_call(&self, name: &str) -> Self {
242 Self {
243 start: self.start,
244 end: self.end,
245 source: SourceFile::scss_bytes(
246 self.source.data(),
247 SourceName::called(name, self.clone()),
248 ),
249 }
250 }
251
252 pub fn is_builtin(&self) -> bool {
254 self.source.source().is_builtin()
255 }
256
257 pub(crate) fn mock_function(
258 name: &Name,
259 args: &FormalArgs,
260 module: &str,
261 ) -> Self {
262 Self::mock_impl("@function", name, args, module)
263 }
264 pub(crate) fn mock_mixin(
265 name: &Name,
266 args: &FormalArgs,
267 module: &str,
268 ) -> Self {
269 Self::mock_impl("@mixin", name, args, module)
270 }
271 fn mock_impl(
272 kind: &str,
273 name: &Name,
274 args: &FormalArgs,
275 module: &str,
276 ) -> Self {
277 let line = format!("{kind} {name}{args} {{");
278 Self {
279 start: kind.len() + 1,
280 end: line.len() - 2,
281 source: SourceFile::scss_bytes(line, SourceName::root(module)),
282 }
283 }
284
285 pub fn file_url(&self) -> &str {
287 self.source.source().name()
288 }
289}
290
291impl From<SourceFile> for SourcePos {
292 fn from(source: SourceFile) -> Self {
293 Self {
294 start: 0,
295 end: source.data().len(),
296 source,
297 }
298 }
299}
300
301impl PartialOrd for SourcePos {
302 fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
303 Some(self.cmp(other))
304 }
305}
306impl Ord for SourcePos {
307 fn cmp(&self, other: &Self) -> std::cmp::Ordering {
308 self.source
309 .cmp(&other.source)
310 .then(self.start.cmp(&other.start))
311 .then(self.end.cmp(&other.end))
312 }
313}
314impl PartialEq for SourcePos {
315 fn eq(&self, other: &Self) -> bool {
316 self.partial_cmp(other) == Some(Ordering::Equal)
317 }
318}
319impl Eq for SourcePos {}
320
321impl std::fmt::Debug for SourcePos {
322 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
323 f.debug_struct("OwnedSpan")
324 .field("range", &self.range())
325 .field("data", &DebugBytes(self.fragment()))
326 .finish()
327 }
328}