1use core::ops::{Add, Sub};
2use std::borrow::Cow;
3use std::ffi::OsStr;
4use std::fmt::Display;
5use std::future::Future;
6use std::path::{Path, PathBuf};
7use std::str::FromStr;
8use std::sync::atomic::{AtomicBool, Ordering};
9use std::sync::LazyLock;
10
11use dashmap::try_result::TryResult;
12use futures::future::BoxFuture;
13use ropey::{Rope, RopeSlice};
14use smart_default::SmartDefault;
15use tower_lsp_server::lsp_types::*;
16use xmlparser::{StrSpan, TextPos, Token};
17
18mod visitor;
19pub use visitor::PreTravel;
20
21use crate::index::PathSymbol;
22
23#[cfg(not(windows))]
24pub use std::fs::canonicalize as strict_canonicalize;
25
26#[macro_export]
28macro_rules! some {
29 ($opt:expr) => {
30 match $opt {
31 Some(it) => it,
32 None => {
33 tracing::trace!(concat!(stringify!($opt), " = None"));
34 return Ok(None);
35 }
36 }
37 };
38}
39
40#[macro_export]
42macro_rules! ok {
43 ($res:expr $(,)?) => { ($res)? };
44 ($res:expr, $($tt:tt)+) => {
45 anyhow::Context::with_context($res, || format_loc!($($tt)+))?
46 }
47}
48
49#[repr(transparent)]
50#[derive(SmartDefault)]
51pub struct EarlyReturn<'a, T>(
52 #[default(None)] Option<Box<dyn FnOnce() -> BoxFuture<'a, T> + 'a + Send>>,
56);
57
58impl<'a, T> EarlyReturn<'a, T> {
59 pub fn lift<F, Fut>(&mut self, closure: F)
61 where
62 F: FnOnce() -> Fut + 'a + Send,
63 Fut: Future<Output = T> + 'a + Send,
64 {
65 self.0 = Some(Box::new(move || Box::pin(async move { closure().await })));
66 }
67 #[inline]
68 pub fn is_none(&self) -> bool {
69 self.0.is_none()
70 }
71 pub fn call(self) -> Option<BoxFuture<'a, T>> {
72 Some(self.0?())
73 }
74}
75
76#[derive(Clone, Debug)]
78pub struct MinLoc {
79 pub path: PathSymbol,
80 pub range: Range,
81}
82
83impl Display for MinLoc {
84 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
85 f.write_fmt(format_args!(
86 "{}:{}:{}",
87 self.path,
88 self.range.start.line + 1,
89 self.range.start.character + 1
90 ))
91 }
92}
93
94impl From<MinLoc> for Location {
95 fn from(value: MinLoc) -> Self {
96 Location {
97 uri: format!("file://{}", value.path).parse().unwrap(),
98 range: value.range,
99 }
100 }
101}
102
103pub fn offset_to_position(offset: ByteOffset, rope: Rope) -> Option<Position> {
104 let line = rope.try_byte_to_line(offset.0).ok()?;
105 let line_start_char = rope.try_line_to_char(line).ok()?;
106 let char_offset = rope.try_byte_to_char(offset.0).ok()?;
107 let column = char_offset - line_start_char;
108 Some(Position::new(line as u32, column as u32))
109}
110
111pub fn position_to_offset(position: Position, rope: &Rope) -> Option<ByteOffset> {
112 let CharOffset(char_offset) = position_to_char(position, rope)?;
113 let byte_offset = rope.try_char_to_byte(char_offset).ok()?;
114 Some(ByteOffset(byte_offset))
115}
116
117pub fn position_to_offset_slice(position: Position, slice: &RopeSlice) -> Option<ByteOffset> {
118 let CharOffset(char_offset) = position_to_char_slice(position, slice)?;
119 let byte_offset = slice.try_char_to_byte(char_offset).ok()?;
120 Some(ByteOffset(byte_offset))
121}
122
123fn position_to_char(position: Position, rope: &Rope) -> Option<CharOffset> {
124 let line_offset_in_char = rope.try_line_to_char(position.line as usize).ok()?;
125 Some(CharOffset(line_offset_in_char + position.character as usize))
126}
127
128fn position_to_char_slice(position: Position, rope: &RopeSlice) -> Option<CharOffset> {
129 let line_offset_in_char = rope.try_line_to_char(position.line as usize).ok()?;
130 Some(CharOffset(line_offset_in_char + position.character as usize))
131}
132
133pub fn lsp_range_to_char_range(range: Range, rope: &Rope) -> Option<CharRange> {
134 let start = position_to_char(range.start, rope)?;
135 let end = position_to_char(range.end, rope)?;
136 Some(start..end)
137}
138
139pub fn lsp_range_to_offset_range(range: Range, rope: &Rope) -> Option<ByteRange> {
140 let start = position_to_offset(range.start, rope)?;
141 let end = position_to_offset(range.end, rope)?;
142 Some(start..end)
143}
144
145pub fn offset_range_to_lsp_range(range: ByteRange, rope: Rope) -> Option<Range> {
146 let start = offset_to_position(range.start, rope.clone())?;
147 let end = offset_to_position(range.end, rope)?;
148 Some(Range { start, end })
149}
150
151pub fn xml_position_to_lsp_position(position: TextPos) -> Position {
152 Position {
153 line: position.row - 1_u32,
154 character: position.col - 1_u32,
155 }
156}
157
158pub fn ts_range_to_lsp_range(range: tree_sitter::Range) -> Range {
159 Range {
160 start: Position {
161 line: range.start_point.row as u32,
162 character: range.start_point.column as u32,
163 },
164 end: Position {
165 line: range.end_point.row as u32,
166 character: range.end_point.column as u32,
167 },
168 }
169}
170
171pub fn token_span<'r, 't>(token: &'r Token<'t>) -> &'r StrSpan<'t> {
172 match token {
173 Token::Declaration { span, .. }
174 | Token::ProcessingInstruction { span, .. }
175 | Token::Comment { span, .. }
176 | Token::DtdStart { span, .. }
177 | Token::EmptyDtd { span, .. }
178 | Token::EntityDeclaration { span, .. }
179 | Token::DtdEnd { span, .. }
180 | Token::ElementStart { span, .. }
181 | Token::Attribute { span, .. }
182 | Token::ElementEnd { span, .. }
183 | Token::Text { text: span, .. }
184 | Token::Cdata { span, .. } => span,
185 }
186}
187
188pub fn cow_split_once<'src>(
189 mut src: Cow<'src, str>,
190 sep: &str,
191) -> Result<(Cow<'src, str>, Cow<'src, str>), Cow<'src, str>> {
192 match src {
193 Cow::Borrowed(inner) => inner
194 .split_once(sep)
195 .map(|(lhs, rhs)| (Cow::Borrowed(lhs), Cow::Borrowed(rhs)))
196 .ok_or(src),
197 Cow::Owned(ref mut inner) => {
198 let Some(offset) = inner.find(sep) else {
199 return Err(src);
200 };
201 let mut rhs = inner.split_off(offset);
202 rhs.replace_range(0..sep.len(), "");
203 Ok((Cow::Owned(core::mem::take(inner)), Cow::Owned(rhs)))
204 }
205 }
206}
207
208#[derive(Debug, Default, Copy, Clone, PartialEq, Eq, PartialOrd, Ord)]
209#[repr(transparent)]
210pub struct ByteOffset(pub usize);
211pub type ByteRange = core::ops::Range<ByteOffset>;
212
213impl From<usize> for ByteOffset {
214 #[inline]
215 fn from(value: usize) -> Self {
216 ByteOffset(value as _)
217 }
218}
219
220#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord)]
221#[repr(transparent)]
222pub struct CharOffset(pub usize);
223pub type CharRange = core::ops::Range<CharOffset>;
224
225pub trait RangeExt {
226 type Unit;
227 fn map_unit<F, V>(self, op: F) -> std::ops::Range<V>
228 where
229 F: FnMut(Self::Unit) -> V;
230
231 fn shrink(self, value: Self::Unit) -> std::ops::Range<Self::Unit>
232 where
233 Self: Sized,
234 Self::Unit: Add<Self::Unit, Output = Self::Unit> + Sub<Self::Unit, Output = Self::Unit> + Copy;
235
236 fn contains_end(&self, value: Self::Unit) -> bool
237 where
238 Self::Unit: PartialOrd;
239}
240
241pub trait Erase {
242 fn erase(&self) -> core::ops::Range<usize>;
243 fn intersects(&self, other: core::ops::Range<usize>) -> bool {
244 let this = self.erase();
245 this.end >= other.start || this.start < other.end
246 }
247}
248
249impl Erase for ByteRange {
250 #[inline]
251 fn erase(&self) -> core::ops::Range<usize> {
252 self.clone().map_unit(|unit| unit.0)
253 }
254}
255
256impl Erase for CharRange {
257 #[inline]
258 fn erase(&self) -> core::ops::Range<usize> {
259 self.clone().map_unit(|unit| unit.0)
260 }
261}
262
263impl<T> RangeExt for core::ops::Range<T> {
264 type Unit = T;
265
266 #[inline]
267 fn map_unit<F, V>(self, mut op: F) -> core::ops::Range<V>
268 where
269 F: FnMut(Self::Unit) -> V,
270 {
271 op(self.start)..op(self.end)
272 }
273
274 fn shrink(self, value: Self::Unit) -> core::ops::Range<Self::Unit>
275 where
276 Self: Sized,
277 Self::Unit: Add<Self::Unit, Output = Self::Unit> + Sub<Self::Unit, Output = Self::Unit> + Copy,
278 {
279 self.start + value..self.end - value
280 }
281
282 #[inline]
283 fn contains_end(&self, value: Self::Unit) -> bool
284 where
285 Self::Unit: PartialOrd,
286 {
287 self.contains(&value) || self.end == value
288 }
289}
290
291#[macro_export]
292macro_rules! loc {
293 () => {
294 concat!("[", file!(), ":", line!(), ":", column!(), "]")
295 };
296}
297
298#[macro_export]
299macro_rules! errloc {
300 ($msg:literal $(, $($tt:tt)* )?) => {
301 ::anyhow::anyhow!(concat!($crate::loc!(), " ", $msg) $(, $($tt)* )?)
302 }
303}
304
305#[macro_export]
308macro_rules! format_loc {
309 ($tpl:literal) => {
310 concat!($crate::loc!(), " ", $tpl)
311 };
312 ($tpl:literal $($tt:tt)*) => {
313 format!($crate::format_loc!($tpl) $($tt)*)
314 };
315}
316
317#[derive(Default)]
318pub struct MaxVec<T>(Vec<T>);
319
320impl<T> MaxVec<T> {
321 pub fn new(limit: usize) -> Self {
322 MaxVec(Vec::with_capacity(limit))
323 }
324 #[inline]
325 fn remaining_space(&self) -> usize {
326 self.0.capacity().saturating_sub(self.0.len())
327 }
328 #[inline]
329 pub fn has_space(&self) -> bool {
330 self.remaining_space() > 0
331 }
332 pub fn extend(&mut self, items: impl Iterator<Item = T>) {
333 self.0.extend(items.take(self.remaining_space()));
334 }
335 pub fn push_checked(&mut self, item: T) {
336 if self.has_space() {
337 self.0.push(item);
338 }
339 }
340 #[inline]
341 pub fn into_inner(self) -> Vec<T> {
342 self.0
343 }
344}
345
346impl<T> std::convert::AsMut<[T]> for MaxVec<T> {
347 #[inline]
348 fn as_mut(&mut self) -> &mut [T] {
349 &mut self.0
350 }
351}
352
353impl<T> std::ops::Deref for MaxVec<T> {
354 type Target = Vec<T>;
355 #[inline]
356 fn deref(&self) -> &Self::Target {
357 &self.0
358 }
359}
360
361pub trait TryResultExt {
362 type Result: Sized;
363 fn expect(self, msg: &str) -> Option<Self::Result>;
365}
366
367impl<T: Sized> TryResultExt for TryResult<T> {
368 type Result = T;
369 fn expect(self, msg: &str) -> Option<Self::Result> {
370 match self {
371 TryResult::Present(item) => Some(item),
372 TryResult::Absent => None,
373 TryResult::Locked => panic!("{msg}"),
374 }
375 }
376}
377
378#[cfg(test)]
379pub fn init_for_test() {
380 use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt, EnvFilter};
381
382 tracing_subscriber::registry()
383 .with(tracing_subscriber::fmt::layer())
384 .with(EnvFilter::from("info,odoo_lsp=trace"))
385 .init();
386}
387
388pub struct CondVar {
389 should_wait: AtomicBool,
390 notifier: tokio::sync::Notify,
391}
392
393impl Default for CondVar {
394 fn default() -> Self {
395 Self {
396 should_wait: AtomicBool::new(true),
397 notifier: Default::default(),
398 }
399 }
400}
401
402pub struct Blocker<'a>(&'a CondVar);
403
404impl CondVar {
405 pub fn block(&self) -> Blocker {
406 self.should_wait.store(true, Ordering::SeqCst);
407 Blocker(self)
408 }
409
410 const WAIT_LIMIT: std::time::Duration = std::time::Duration::from_secs(15);
411
412 pub async fn wait(&self) {
414 if self.should_wait.load(Ordering::SeqCst) {
415 tokio::select! {
416 _ = self.notifier.notified() => {}
417 _ = tokio::time::sleep(Self::WAIT_LIMIT) => {
418 tracing::warn!("WAIT_LIMIT elapsed (thread={:?})", std::thread::current().id());
419 }
420 }
421 }
422 }
423
424 #[inline]
425 pub fn should_wait(&self) -> bool {
426 self.should_wait.load(Ordering::SeqCst)
427 }
428}
429
430impl Drop for Blocker<'_> {
431 fn drop(&mut self) {
432 self.0.should_wait.store(false, Ordering::SeqCst);
433 self.0.notifier.notify_waiters();
434 }
435}
436
437pub trait DisplayExt {
440 fn display(self) -> impl Display;
441}
442
443impl<T: Display> DisplayExt for Option<T> {
469 fn display(self) -> impl Display {
470 struct Adapter<T>(Option<T>);
471 impl<T: Display> Display for Adapter<T> {
472 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
473 match &self.0 {
474 Some(value) => value.fmt(f),
475 None => Ok(()),
476 }
477 }
478 }
479 Adapter(self)
480 }
481}
482
483impl<T: Display> DisplayExt for &T {
484 fn display(self) -> impl Display {
485 self as &dyn Display
486 }
487}
488
489impl DisplayExt for std::fmt::Arguments<'_> {
490 fn display(self) -> impl Display {
491 #[repr(transparent)]
492 struct Adapter<'a>(std::fmt::Arguments<'a>);
493 impl Display for Adapter<'_> {
494 #[inline]
495 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
496 f.write_fmt(self.0)
497 }
498 }
499 Adapter(self)
500 }
501}
502
503pub fn path_contains(path: impl AsRef<Path>, needle: impl AsRef<OsStr>) -> bool {
504 path.as_ref().components().any(|c| c.as_os_str() == needle.as_ref())
505}
506
507pub trait UriExt {
508 fn to_file_path(&self) -> Option<Cow<Path>>;
509}
510
511impl UriExt for tower_lsp_server::lsp_types::Uri {
512 fn to_file_path(&self) -> Option<Cow<Path>> {
513 let path = match self.path().as_estr().decode().into_string_lossy() {
514 Cow::Borrowed(ref_) => Cow::Borrowed(Path::new(ref_)),
515 Cow::Owned(owned) => Cow::Owned(PathBuf::from(owned)),
516 };
517
518 #[cfg(windows)]
519 {
520 let authority = self.authority().expect("url has no authority component");
521 let host = authority.host().as_str();
522 if host.is_empty() {
523 let host = path.to_string_lossy();
526 let host = &host[1..];
527 return Some(Cow::Owned(PathBuf::from(host)));
528 }
529
530 let host = format!("{host}:");
531 Some(Cow::Owned(
532 Path::new(&host).components().chain(path.components()).collect(),
533 ))
534 }
535
536 #[cfg(not(windows))]
537 Some(path)
538 }
539}
540
541pub fn uri_from_file_path(path: &Path) -> Option<Uri> {
542 let fragment = if !path.is_absolute() {
543 Cow::from(strict_canonicalize(path).ok()?)
544 } else {
545 Cow::from(path)
546 };
547
548 #[cfg(windows)]
549 {
550 let raw = format!("file:///{}", fragment.to_string_lossy().replace("\\", "/"));
553 Uri::from_str(&raw).ok()
554 }
555
556 #[cfg(not(windows))]
557 Uri::from_str(&format!("file://{}", fragment.to_string_lossy())).ok()
558}
559
560static WSL: LazyLock<bool> = LazyLock::new(|| {
561 #[cfg(not(unix))]
562 return false;
563
564 #[cfg(unix)]
565 return rustix::system::uname()
566 .release()
567 .to_str()
568 .is_ok_and(|release| release.contains("WSL"));
569});
570
571#[inline]
572fn wsl_to_windows_path(path: impl AsRef<OsStr>) -> Option<String> {
573 fn impl_(path: &OsStr) -> Result<String, String> {
574 let mut out = std::process::Command::new("wslpath")
575 .arg("-w")
576 .arg(path)
577 .output()
578 .map_err(|err| format_loc!("wslpath failed: {}", err))?;
579 let code = out.status.code().unwrap_or(-1);
580 if code != 0 {
581 return Err(format_loc!("wslpath failed with code={}", code));
582 }
583
584 Ok(String::from_utf8(core::mem::take(&mut out.stdout))
585 .map_err(|err| format_loc!("wslpath returned non-utf8 path: {}", err))?
586 .trim()
587 .to_string())
588 }
589 impl_(path.as_ref()).map_err(|err| tracing::error!("{err}")).ok()
590}
591
592pub fn to_display_path(path: impl AsRef<Path>) -> String {
596 if *WSL {
597 return wsl_to_windows_path(path.as_ref()).unwrap_or_else(|| path.as_ref().to_string_lossy().into_owned());
598 }
599
600 path.as_ref().to_string_lossy().into_owned()
601}
602
603#[inline]
606#[cfg(windows)]
607pub fn strict_canonicalize<P: AsRef<Path>>(path: P) -> anyhow::Result<PathBuf> {
608 use anyhow::Context;
609
610 fn impl_(path: PathBuf) -> anyhow::Result<PathBuf> {
611 let head = path.components().next().context("empty path")?;
612 let disk_;
613 let head = if let std::path::Component::Prefix(prefix) = head {
614 if let std::path::Prefix::VerbatimDisk(disk) = prefix.kind() {
615 disk_ = format!("{}:", disk as char);
616 Path::new(&disk_)
617 .components()
618 .next()
619 .context("failed to parse disk component")?
620 } else {
621 head
622 }
623 } else {
624 head
625 };
626 Ok(std::iter::once(head).chain(path.components().skip(1)).collect())
627 }
628 let canon = std::fs::canonicalize(path)?;
629 impl_(canon)
630}
631
632#[cfg(test)]
633mod tests {
634 use std::{path::Path, str::FromStr};
635
636 use crate::utils::{strict_canonicalize, DisplayExt};
637
638 use super::{to_display_path, uri_from_file_path, UriExt, WSL};
639 use pretty_assertions::assert_eq;
640 use tower_lsp_server::lsp_types::Uri;
641
642 #[test]
643 fn test_to_display_path() {
644 if *WSL {
645 assert_eq!(to_display_path("/mnt/c"), r"C:\");
646 let unix_path = to_display_path("/usr");
647 assert!(unix_path.starts_with(r"\\wsl"));
648 assert!(unix_path.ends_with(r"\usr"));
649 }
650 }
651
652 #[test]
653 #[cfg(windows)]
654 fn test_idempotent_canonicalization() {
655 let lhs = strict_canonicalize(Path::new(".")).unwrap();
656 let rhs = strict_canonicalize(&lhs).unwrap();
657 assert_eq!(lhs, rhs);
658 }
659
660 #[test]
661 fn test_path_roundtrip_conversion() {
662 let src = strict_canonicalize(Path::new(".")).unwrap();
663 let conv = uri_from_file_path(&src).unwrap();
664 let roundtrip = conv.to_file_path().unwrap();
665 assert_eq!(src, roundtrip, "conv={conv:?} conv_display={}", conv.display());
666 }
667
668 #[test]
669 #[cfg(windows)]
670 fn test_windows_uri_roundtrip_conversion() {
671 let uri = Uri::from_str("file:///C:/Windows").unwrap();
672 let path = uri.to_file_path().unwrap();
673 assert_eq!(&path, Path::new("C:/Windows"), "uri={uri:?}");
674
675 let conv = uri_from_file_path(&path).unwrap();
676
677 assert_eq!(uri, conv, "path={path:?} left={} right={}", uri.as_str(), conv.as_str());
678 }
679}