odoo_lsp/
utils.rs

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/// Unwraps the option in the context of a function that returns [`Result<Option<_>>`].
27#[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/// Early return, with optional message passed to [`format_loc`](crate::format_loc!).
41#[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	// By default, trait objects are bound to a 'static lifetime.
53	// This allows closures to capture references instead.
54	// However, any values must still be Send.
55	#[default(None)] Option<Box<dyn FnOnce() -> BoxFuture<'a, T> + 'a + Send>>,
56);
57
58impl<'a, T> EarlyReturn<'a, T> {
59	/// Lifts a certain async computation out of the current scope to be executed later.
60	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/// A more economical version of [Location].
77#[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/// [format] preceded with file location information.
306/// If no arguments are passed, a string literal is returned.
307#[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	/// Panics if this is [`TryResult::Locked`].
364	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	/// Waits for a maximum of [`WAIT_LIMIT`][Self::WAIT_LIMIT] for a notification.
413	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
437// /// Custom display trait to bypass orphan rules.
438// /// Implemented on [`Option<_>`] to default to printing nothing.
439pub trait DisplayExt {
440	fn display(self) -> impl Display;
441}
442
443// impl<T: Display> DisplayExt for Option<T> {
444// 	fn display(&self) -> impl Display {
445// 		#[repr(transparent)]
446// 		struct OptionDisplay<'a, T>(Option<&'a T>);
447// 		impl<T: Display> Display for OptionDisplay<'_, T> {
448// 			#[inline]
449// 			fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> std::fmt::Result {
450// 				match &self.0 {
451// 					Some(value) => value.fmt(f),
452// 					None => Ok(()),
453// 				}
454// 			}
455// 		}
456// 		OptionDisplay(self.as_ref())
457// 	}
458// }
459
460// impl<T: Display> DisplayExt for Option<&T> {
461// 	fn display(self) -> impl Display {
462// 		match self {
463// 			Some(value) => value as &dyn Display,
464// 			None => &"" as &dyn Display,
465// 		}
466// 	}
467// }
468impl<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				// very high chance this is a `file:///` uri
524				// in which case the path will include a leading slash we need to remove
525				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		// we want to parse a triple-slash path for Windows paths
551		// it's a shorthand for `file://localhost/C:/Windows` with the `localhost` omitted
552		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
592/// Returns a path suitable for display on code editors e.g. VSCode.
593///
594/// Transforms the path on WSL only.
595pub 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/// On Windows, rewrites the wide path prefix `\\?\C:` to `C:`  
604/// Source: https://stackoverflow.com/a/70970317
605#[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}