Skip to main content

odoo_lsp/
utils.rs

1use core::fmt::{Debug, Display};
2use core::ops::{Add, Sub};
3use std::borrow::Cow;
4use std::ffi::OsStr;
5use std::path::Path;
6use std::sync::atomic::{AtomicBool, Ordering};
7
8use dashmap::try_result::TryResult;
9use ropey::RopeSlice;
10use tower_lsp_server::ls_types::*;
11use tracing::warn;
12use xmlparser::{StrSpan, TextPos, Token};
13
14mod visitor;
15pub use visitor::PreTravel;
16
17mod catch_panic;
18pub use catch_panic::CatchPanic;
19
20use crate::index::PathSymbol;
21use crate::prelude::*;
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#[macro_export]
41macro_rules! dig {
42	() => { None };
43	($start:expr, $($rest:tt)+) => {
44		$crate::dig!(@inner Some($start), $($rest)+)
45	};
46	(@inner $node:expr, $kind:ident($idx:literal).$($rest:tt)+) => {
47		$crate::dig!(
48			@inner
49			if let Some(node) = $node && let Some(child) = $crate::utils::python_nth_named_child_matching::<$idx>(node, stringify!($kind)) { Some(child) } else { None },
50			$($rest)+
51		)
52	};
53	(@inner $node:expr, $kind:ident.$($rest:tt)+) => {
54		$crate::dig!(@inner $node, $kind(0).$($rest)+)
55	};
56	(@inner $node:expr, $kind:ident($idx:literal)) => {
57		if let Some(node) = $node && let Some(child) = $crate::utils::python_nth_named_child_matching::<$idx>(node, stringify!($kind)) { Some(child) } else { None }
58	};
59	(@inner $node:expr, $kind:ident) => {
60		$crate::dig!(@inner $node, $kind(0))
61	};
62}
63
64/// Early return, with optional message passed to [`format_loc`](crate::format_loc!).
65#[macro_export]
66macro_rules! ok {
67    ($res:expr $(,)?) => {
68    	anyhow::Context::context($res, concat!($crate::loc!(), " ", stringify!($res)))?
69    };
70    ($res:expr, $($tt:tt)+) => {
71		anyhow::Context::with_context($res, || $crate::format_loc!($($tt)+))?
72    }
73}
74
75#[macro_export]
76macro_rules! await_did_open_document {
77	($self:expr, $path:expr) => {
78		let mut blocker = None;
79		{
80			if let Some(document) = $self
81				.document_map
82				.try_get($path)
83				.expect($crate::format_loc!("deadlock"))
84				&& document.setup.should_wait()
85			{
86				blocker = Some(std::sync::Arc::clone(&document.setup));
87			}
88		}
89		if let Some(blocker) = blocker {
90			blocker.wait($crate::loc!()).await;
91		}
92	};
93}
94
95/// FIXME: This hack is necessary to drop !Send locals before await points.
96#[repr(transparent)]
97#[derive(SmartDefault)]
98#[cfg(false)]
99struct EarlyReturn<'a, T>(
100	// By default, trait objects are bound to a 'static lifetime.
101	// This allows closures to capture references instead.
102	// However, any referenced value must still be Send.
103	#[default(None)] Option<Box<dyn FnOnce() -> BoxFuture<'a, T> + 'a + Send>>,
104);
105
106#[cfg(false)]
107impl<'a, T> EarlyReturn<'a, T> {
108	/// Lifts a certain async computation out of the current scope to be executed later.
109	pub fn lift<F, Fut>(&mut self, closure: F)
110	where
111		F: FnOnce() -> Fut + 'a + Send,
112		Fut: Future<Output = T> + 'a + Send,
113	{
114		self.0 = Some(Box::new(move || Box::pin(async move { closure().await })));
115	}
116	#[inline]
117	pub fn is_none(&self) -> bool {
118		self.0.is_none()
119	}
120	pub fn call(self) -> Option<BoxFuture<'a, T>> {
121		Some(self.0?())
122	}
123}
124
125/// A more economical version of [Location].
126#[derive(Clone, Debug)]
127pub struct MinLoc {
128	pub path: PathSymbol,
129	pub range: Range,
130}
131
132impl Display for MinLoc {
133	fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
134		f.write_fmt(format_args!(
135			"{}:{}:{}",
136			self.path,
137			self.range.start.line + 1,
138			self.range.start.character + 1
139		))
140	}
141}
142
143impl From<MinLoc> for Location {
144	fn from(value: MinLoc) -> Self {
145		Location {
146			uri: format!("file://{}", value.path).parse().unwrap(),
147			range: value.range,
148		}
149	}
150}
151
152pub struct SpanAdapter<T>(T);
153pub struct RopeAdapter<'a, T>(T, RopeSlice<'a>);
154
155/// Infallible version of [rope_conv] that doesn't require a [RopeSlice].
156/// Available conversions:
157/// - [xmlparser::TextPos] -> [Position]
158/// - [tree_sitter::Range] -> [Range]
159#[inline]
160pub fn span_conv<T, U>(src: T) -> U
161where
162	U: From<SpanAdapter<T>>,
163{
164	SpanAdapter(src).into()
165}
166
167/// Specializations for conversion between several types of offsets and ranges.  
168/// Available conversions:
169/// - [ByteOffset] <-> [Position]
170/// - [Range] -> [CharRange]
171/// - [Range] <-> [ByteRange]
172#[inline]
173pub fn rope_conv<T, U>(src: T, rope: RopeSlice<'_>) -> U
174where
175	for<'a> U: From<RopeAdapter<'a, T>>,
176{
177	RopeAdapter(src, rope).into()
178}
179
180impl<'a> From<RopeAdapter<'a, ByteOffset>> for Position {
181	fn from(value: RopeAdapter<'a, ByteOffset>) -> Self {
182		let RopeAdapter(offset, rope) = value;
183		let line = rope.byte_to_line_idx(offset.0, LINE_TYPE);
184		let line_start_byte = rope.line_to_byte_idx(line, LINE_TYPE);
185		let line_start_char = rope.byte_to_char_idx(line_start_byte);
186		let char_offset = rope.byte_to_char_idx(offset.0);
187		let column = char_offset - line_start_char;
188		Position::new(line as u32, column as u32)
189	}
190}
191
192impl<'a> From<RopeAdapter<'a, Position>> for ByteOffset {
193	fn from(value: RopeAdapter<'a, Position>) -> Self {
194		let RopeAdapter(position, rope) = value;
195		let CharOffset(char_offset) = position_to_char(position, rope);
196		let byte_offset = rope.char_to_byte_idx(char_offset);
197		ByteOffset(byte_offset)
198	}
199}
200
201impl<'a> From<RopeAdapter<'a, Range>> for CharRange {
202	fn from(value: RopeAdapter<'a, Range>) -> Self {
203		let RopeAdapter(range, rope) = value;
204		let start = position_to_char(range.start, rope);
205		let end = position_to_char(range.end, rope);
206		start..end
207	}
208}
209impl<'a> From<RopeAdapter<'a, Range>> for ByteRange {
210	fn from(value: RopeAdapter<'a, Range>) -> Self {
211		let RopeAdapter(range, rope) = value;
212		let start = rope_conv(range.start, rope);
213		let end = rope_conv(range.end, rope);
214		start..end
215	}
216}
217
218impl<'a> From<RopeAdapter<'a, ByteRange>> for Range {
219	fn from(value: RopeAdapter<'a, ByteRange>) -> Self {
220		let RopeAdapter(range, rope) = value;
221		let start = rope_conv(range.start, rope);
222		let end = rope_conv(range.end, rope);
223		Range { start, end }
224	}
225}
226
227fn position_to_char(position: Position, rope: RopeSlice<'_>) -> CharOffset {
228	let line_offset_in_byte = rope.line_to_byte_idx(position.line as usize, LINE_TYPE);
229	let line_offset_in_char = rope.byte_to_char_idx(line_offset_in_byte);
230	CharOffset(line_offset_in_char + position.character as usize)
231}
232
233impl From<SpanAdapter<TextPos>> for Position {
234	#[inline]
235	fn from(value: SpanAdapter<TextPos>) -> Self {
236		let SpanAdapter(position) = value;
237		Position {
238			line: position.row - 1_u32,
239			character: position.col - 1_u32,
240		}
241	}
242}
243
244impl From<SpanAdapter<tree_sitter::Range>> for Range {
245	#[inline]
246	fn from(value: SpanAdapter<tree_sitter::Range>) -> Self {
247		let SpanAdapter(range) = value;
248		Range {
249			start: Position {
250				line: range.start_point.row as u32,
251				character: range.start_point.column as u32,
252			},
253			end: Position {
254				line: range.end_point.row as u32,
255				character: range.end_point.column as u32,
256			},
257		}
258	}
259}
260
261pub fn token_span<'r, 't>(token: &'r Token<'t>) -> &'r StrSpan<'t> {
262	match token {
263		Token::Declaration { span, .. }
264		| Token::ProcessingInstruction { span, .. }
265		| Token::Comment { span, .. }
266		| Token::DtdStart { span, .. }
267		| Token::EmptyDtd { span, .. }
268		| Token::EntityDeclaration { span, .. }
269		| Token::DtdEnd { span, .. }
270		| Token::ElementStart { span, .. }
271		| Token::Attribute { span, .. }
272		| Token::ElementEnd { span, .. }
273		| Token::Text { text: span, .. }
274		| Token::Cdata { span, .. } => span,
275	}
276}
277
278/// Similar to [`str::split_once`]
279///
280/// Returns `src` if the string cannot be split by `sep`.
281pub fn cow_split_once<'src>(
282	mut src: Cow<'src, str>,
283	sep: &str,
284) -> Result<(Cow<'src, str>, Cow<'src, str>), Cow<'src, str>> {
285	match src {
286		Cow::Borrowed(inner) => inner
287			.split_once(sep)
288			.map(|(lhs, rhs)| (Cow::Borrowed(lhs), Cow::Borrowed(rhs)))
289			.ok_or(src),
290		Cow::Owned(ref mut inner) => {
291			let Some(offset) = inner.find(sep) else {
292				return Err(src);
293			};
294			let mut rhs = inner.split_off(offset);
295			rhs.replace_range(0..sep.len(), "");
296			Ok((Cow::Owned(core::mem::take(inner)), Cow::Owned(rhs)))
297		}
298	}
299}
300
301#[inline(always)]
302#[cold]
303pub const fn cold_path() {}
304
305/// Copied from <https://github.com/rust-lang/hashbrown/commit/64bd7db1d1b148594edfde112cdb6d6260e2cfc3>
306#[inline(always)]
307pub const fn likely(cond: bool) -> bool {
308	if cond {
309		true
310	} else {
311		cold_path();
312		false
313	}
314}
315
316#[inline(always)]
317pub const fn unlikely(cond: bool) -> bool {
318	if cond {
319		cold_path();
320		true
321	} else {
322		false
323	}
324}
325
326/// Only useful for Python, since the default grammar does not mark comments as extra nodes.
327#[allow(clippy::disallowed_methods)]
328#[inline]
329pub fn python_next_named_sibling(mut node: Node) -> Option<Node> {
330	loop {
331		node = node.next_named_sibling()?;
332		if likely(node.kind() != "comment") {
333			return Some(node);
334		}
335	}
336}
337
338#[allow(clippy::disallowed_methods)]
339#[inline]
340pub fn python_nth_named_child_matching<'node, const NTH: usize>(
341	mut node: Node<'node>,
342	kind: &'static str,
343) -> Option<Node<'node>> {
344	let mut idx = 0;
345	node = node.named_child(0)?;
346	loop {
347		if idx == NTH && likely(node.kind() == kind) {
348			return Some(node);
349		}
350		if likely(node.kind() != "comment") {
351			idx += 1;
352		}
353		node = node.next_named_sibling()?;
354	}
355}
356
357#[derive(Debug, Default, Copy, Clone, PartialEq, Eq, PartialOrd, Ord)]
358#[repr(transparent)]
359pub struct ByteOffset(pub usize);
360pub type ByteRange = core::ops::Range<ByteOffset>;
361
362impl From<usize> for ByteOffset {
363	#[inline]
364	fn from(value: usize) -> Self {
365		ByteOffset(value as _)
366	}
367}
368
369#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord)]
370#[repr(transparent)]
371pub struct CharOffset(pub usize);
372pub type CharRange = core::ops::Range<CharOffset>;
373
374pub trait RangeExt {
375	type Unit;
376	fn map_unit<F, V>(self, op: F) -> std::ops::Range<V>
377	where
378		F: FnMut(Self::Unit) -> V;
379
380	fn shrink(self, value: Self::Unit) -> std::ops::Range<Self::Unit>
381	where
382		Self: Sized,
383		Self::Unit: Add<Self::Unit, Output = Self::Unit> + Sub<Self::Unit, Output = Self::Unit> + Copy;
384
385	fn contains_end(&self, value: Self::Unit) -> bool
386	where
387		Self::Unit: PartialOrd;
388}
389
390pub trait Erase {
391	fn erase(&self) -> core::ops::Range<usize>;
392	fn intersects(&self, other: core::ops::Range<usize>) -> bool {
393		let this = self.erase();
394		this.end >= other.start || this.start < other.end
395	}
396}
397
398impl Erase for ByteRange {
399	#[inline]
400	fn erase(&self) -> core::ops::Range<usize> {
401		self.clone().map_unit(|unit| unit.0)
402	}
403}
404
405impl Erase for CharRange {
406	#[inline]
407	fn erase(&self) -> core::ops::Range<usize> {
408		self.clone().map_unit(|unit| unit.0)
409	}
410}
411
412impl<T> RangeExt for core::ops::Range<T> {
413	type Unit = T;
414
415	#[inline]
416	fn map_unit<F, V>(self, mut op: F) -> core::ops::Range<V>
417	where
418		F: FnMut(Self::Unit) -> V,
419	{
420		op(self.start)..op(self.end)
421	}
422
423	fn shrink(self, value: Self::Unit) -> core::ops::Range<Self::Unit>
424	where
425		Self: Sized,
426		Self::Unit: Add<Self::Unit, Output = Self::Unit> + Sub<Self::Unit, Output = Self::Unit> + Copy,
427	{
428		self.start + value..self.end - value
429	}
430
431	#[inline]
432	fn contains_end(&self, value: Self::Unit) -> bool
433	where
434		Self::Unit: PartialOrd,
435	{
436		self.contains(&value) || self.end == value
437	}
438}
439
440#[macro_export]
441macro_rules! loc {
442	() => {
443		concat!("[", file!(), ":", line!(), ":", column!(), "]")
444	};
445}
446
447#[macro_export]
448macro_rules! errloc {
449	($msg:literal $(, $($tt:tt)* )?) => {
450		::anyhow::anyhow!(concat!($crate::loc!(), " ", $msg) $(, $($tt)* )?)
451	}
452}
453
454/// [format] preceded with file location information.
455/// If no arguments are passed, a string literal is returned.
456#[macro_export]
457macro_rules! format_loc {
458	($tpl:literal) => {
459		concat!($crate::loc!(), " ", $tpl)
460	};
461	($tpl:literal $($tt:tt)*) => {
462		format!($crate::format_loc!($tpl) $($tt)*)
463	};
464}
465
466#[derive(Default)]
467pub struct MaxVec<T>(Vec<T>);
468
469impl<T> MaxVec<T> {
470	pub fn new(limit: usize) -> Self {
471		MaxVec(Vec::with_capacity(limit))
472	}
473	#[inline]
474	fn remaining_space(&self) -> usize {
475		self.0.capacity().saturating_sub(self.0.len())
476	}
477	#[inline]
478	pub fn has_space(&self) -> bool {
479		self.remaining_space() > 0
480	}
481	pub fn extend(&mut self, items: impl Iterator<Item = T>) {
482		self.0.extend(items.take(self.remaining_space()));
483	}
484	pub fn push_checked(&mut self, item: T) {
485		if self.has_space() {
486			self.0.push(item);
487		}
488	}
489	#[inline]
490	pub fn into_inner(self) -> Vec<T> {
491		self.0
492	}
493}
494
495impl<T> std::convert::AsMut<[T]> for MaxVec<T> {
496	#[inline]
497	fn as_mut(&mut self) -> &mut [T] {
498		&mut self.0
499	}
500}
501
502impl<T> std::ops::Deref for MaxVec<T> {
503	type Target = Vec<T>;
504	#[inline]
505	fn deref(&self) -> &Self::Target {
506		&self.0
507	}
508}
509
510pub trait TryResultExt {
511	type Result: Sized;
512	/// Panics if this is [`TryResult::Locked`].
513	fn expect(self, msg: &str) -> Option<Self::Result>;
514}
515
516impl<T: Sized> TryResultExt for TryResult<T> {
517	type Result = T;
518	fn expect(self, msg: &str) -> Option<Self::Result> {
519		match self {
520			TryResult::Present(item) => Some(item),
521			TryResult::Absent => None,
522			TryResult::Locked => panic!("{msg}"),
523		}
524	}
525}
526
527#[cfg(test)]
528pub fn init_for_test() {
529	use std::sync::Once;
530	use tracing_subscriber::{EnvFilter, layer::SubscriberExt, util::SubscriberInitExt};
531
532	static INIT: Once = Once::new();
533	INIT.call_once(|| {
534		tracing_subscriber::registry()
535			.with(tracing_subscriber::fmt::layer())
536			.with(EnvFilter::from("info,odoo_lsp=trace"))
537			.init();
538	});
539}
540
541#[derive(Default)]
542pub struct Semaphore {
543	should_wait: AtomicBool,
544	notifier: tokio::sync::Notify,
545}
546
547impl Semaphore {
548	pub fn init_semaphore() -> Self {
549		Self {
550			should_wait: AtomicBool::new(true),
551			..Default::default()
552		}
553	}
554}
555
556pub struct Blocker<'a>(&'a Semaphore);
557
558impl Semaphore {
559	#[must_use]
560	#[track_caller]
561	pub fn block(&self, context: &'static str) -> Blocker<'_> {
562		if self
563			.should_wait
564			.compare_exchange(false, true, Ordering::Acquire, Ordering::Acquire)
565			.is_err()
566		{
567			panic!(
568				"[{context}] thread={:?} attempted to lock {:p} which is already locked, it should call wait() first",
569				std::thread::current().id(),
570				self
571			);
572		}
573		info!(
574			"[{context}] thread={:?} acquired lock on {:p}",
575			std::thread::current().id(),
576			self
577		);
578		Blocker(self)
579	}
580
581	/// ### Safety
582	/// Should only be used by the initialization flow ONCE.
583	#[must_use]
584	pub unsafe fn block_unchecked(&self, context: &'static str) -> Blocker<'_> {
585		info!(
586			"[{context}] thread={:?} force-acquired lock on {:p}",
587			std::thread::current().id(),
588			self
589		);
590		Blocker(self)
591	}
592
593	pub const WAIT_LIMIT: std::time::Duration = std::time::Duration::from_secs(2);
594
595	/// Waits for a maximum of [`WAIT_LIMIT`][Self::WAIT_LIMIT] for a notification.
596	pub async fn wait(&self, context: &'static str) {
597		while self.should_wait.load(Ordering::Relaxed) {
598			tokio::select! {
599				_ = self.notifier.notified() => return,
600				_ = tokio::time::sleep(Self::WAIT_LIMIT) => {
601					warn!("[{context}] WAIT_LIMIT elapsed (thread={:?}, lock={self:p})", std::thread::current().id());
602				}
603			}
604		}
605	}
606
607	#[inline]
608	pub fn should_wait(&self) -> bool {
609		self.should_wait.load(Ordering::Relaxed)
610	}
611}
612
613impl Drop for Blocker<'_> {
614	#[track_caller]
615	fn drop(&mut self) {
616		info!(
617			"thread={:?} releasing lock on {:p}",
618			std::thread::current().id(),
619			self.0
620		);
621		self.0.should_wait.store(false, Ordering::Release);
622		self.0.notifier.notify_waiters();
623	}
624}
625
626/// Custom display trait to bypass orphan rules.
627/// Implemented on [`Option<_>`] to default to printing nothing.
628pub trait DisplayExt {
629	fn display(self) -> impl Display;
630}
631
632impl<T: Display> DisplayExt for Option<T> {
633	fn display(self) -> impl Display {
634		struct Adapter<T>(Option<T>);
635		impl<T: Display> Display for Adapter<T> {
636			fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
637				match &self.0 {
638					Some(value) => value.fmt(f),
639					None => Ok(()),
640				}
641			}
642		}
643		Adapter(self)
644	}
645}
646
647impl<T: Display> DisplayExt for &T {
648	fn display(self) -> impl Display {
649		self as &dyn Display
650	}
651}
652
653impl DisplayExt for std::fmt::Arguments<'_> {
654	fn display(self) -> impl Display {
655		#[repr(transparent)]
656		struct Adapter<'a>(std::fmt::Arguments<'a>);
657		impl Display for Adapter<'_> {
658			#[inline]
659			fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
660				f.write_fmt(self.0)
661			}
662		}
663		Adapter(self)
664	}
665}
666
667pub fn path_contains(path: impl AsRef<Path>, needle: impl AsRef<OsStr>) -> bool {
668	path.as_ref().components().any(|c| c.as_os_str() == needle.as_ref())
669}
670
671static WSL: std::sync::LazyLock<bool> = std::sync::LazyLock::new(|| {
672	#[cfg(not(unix))]
673	return false;
674
675	#[cfg(unix)]
676	return rustix::system::uname()
677		.release()
678		.to_str()
679		.is_ok_and(|release| release.contains("WSL"));
680});
681
682#[inline]
683fn wsl_to_windows_path(path: impl AsRef<OsStr>) -> Option<String> {
684	fn impl_(path: &OsStr) -> Result<String, String> {
685		let mut out = std::process::Command::new("wslpath")
686			.arg("-w")
687			.arg(path)
688			.output()
689			.map_err(|err| format_loc!("wslpath failed: {}", err))?;
690		let code = out.status.code().unwrap_or(-1);
691		if code != 0 {
692			return Err(format_loc!("wslpath failed with code={}", code));
693		}
694
695		Ok(String::from_utf8(core::mem::take(&mut out.stdout))
696			.map_err(|err| format_loc!("wslpath returned non-utf8 path: {}", err))?
697			.trim()
698			.to_string())
699	}
700	impl_(path.as_ref()).map_err(|err| tracing::error!("{err}")).ok()
701}
702
703/// Returns a path suitable for display on code editors e.g. VSCode.
704///
705/// Transforms the path on WSL only.
706pub fn to_display_path(path: impl AsRef<Path>) -> String {
707	if *WSL {
708		return wsl_to_windows_path(path.as_ref()).unwrap_or_else(|| path.as_ref().to_string_lossy().into_owned());
709	}
710
711	path.as_ref().to_string_lossy().into_owned()
712}
713
714pub struct Defer<T>(pub Option<T>)
715where
716	T: FnOnce();
717
718impl<T> Drop for Defer<T>
719where
720	T: FnOnce(),
721{
722	fn drop(&mut self) {
723		let func = self.0.take().unwrap();
724		func()
725	}
726}
727
728/// On Windows, rewrites the wide path prefix `\\?\C:` to `C:`  
729/// Source: https://stackoverflow.com/a/70970317
730#[inline]
731#[cfg(windows)]
732pub fn strict_canonicalize<P: AsRef<Path>>(path: P) -> anyhow::Result<std::path::PathBuf> {
733	use anyhow::Context;
734	use std::path::PathBuf;
735
736	fn impl_(path: PathBuf) -> anyhow::Result<PathBuf> {
737		let head = path.components().next().context("empty path")?;
738		let disk_;
739		let head = if let std::path::Component::Prefix(prefix) = head {
740			if let std::path::Prefix::VerbatimDisk(disk) = prefix.kind() {
741				disk_ = format!("{}:", disk as char);
742				Path::new(&disk_)
743					.components()
744					.next()
745					.context("failed to parse disk component")?
746			} else {
747				head
748			}
749		} else {
750			head
751		};
752		Ok(std::iter::once(head).chain(path.components().skip(1)).collect())
753	}
754	let canon = std::fs::canonicalize(path)?;
755	impl_(canon)
756}
757
758/// Replacement for `collect` since tree-sitter's StreamingIterator cannot be collected
759pub fn acc_vec<T>(mut acc: Vec<T>, item: &mut T) -> Vec<T>
760where
761	T: Default,
762{
763	acc.push(core::mem::take(item));
764	acc
765}
766
767#[cfg(test)]
768mod tests {
769	use super::{WSL, to_display_path};
770	use pretty_assertions::assert_eq;
771
772	#[test]
773	fn test_to_display_path() {
774		if *WSL {
775			assert_eq!(to_display_path("/mnt/c"), r"C:\");
776			let unix_path = to_display_path("/usr");
777			assert!(unix_path.starts_with(r"\\wsl"));
778			assert!(unix_path.ends_with(r"\usr"));
779		}
780	}
781
782	#[test]
783	#[cfg(windows)]
784	fn test_idempotent_canonicalization() {
785		use super::strict_canonicalize;
786		use std::path::Path;
787
788		let lhs = strict_canonicalize(Path::new(".")).unwrap();
789		let rhs = strict_canonicalize(&lhs).unwrap();
790		assert_eq!(lhs, rhs);
791	}
792
793	#[test]
794	fn test_python_first_nth_child_matching() {
795		use tree_sitter::Parser;
796		use tree_sitter_python::LANGUAGE;
797
798		let contents = r#"[
799			# A comment
800			1,
801			# Another comment
802			2,
803			3,	
804			}
805		}"#;
806
807		let mut parser = Parser::new();
808		parser.set_language(&LANGUAGE.into()).unwrap();
809		let tree = parser.parse(contents, None).unwrap();
810		let root = tree.root_node();
811		let list_node = root.named_child(0).unwrap();
812		let first_element = list_node.named_child(1).unwrap();
813		let second_element = super::python_nth_named_child_matching::<0>(list_node, "integer").unwrap();
814		pretty_assertions::assert_eq!(first_element, second_element);
815	}
816}