//! Fundamental domain types. use serde::{Deserialize, Deserializer, Serialize}; use std::{ num::NonZeroU64, path::{Path, PathBuf}, }; use thiserror::Error; /// A value that failed a domain-level invariant. #[derive(Debug, Clone, PartialEq, Eq, Error)] #[error("domain invariant violated: {detail}")] pub struct InvariantViolation { detail: &'static str, } impl InvariantViolation { /// Creates a new invariant violation. #[must_use] pub fn new(detail: &'static str) -> Self { Self { detail } } } /// Process generation for a rust-analyzer worker. #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)] #[serde(transparent)] pub struct Generation(NonZeroU64); impl Generation { /// Returns the first generation. #[must_use] pub fn genesis() -> Self { Self(NonZeroU64::MIN) } /// Returns the inner integer value. #[must_use] pub fn get(self) -> u64 { self.0.get() } /// Advances to the next generation. #[must_use] pub fn next(self) -> Self { let next = self.get().saturating_add(1); let non_zero = NonZeroU64::new(next).map_or(NonZeroU64::MAX, |value| value); Self(non_zero) } } /// A non-empty absolute workspace root. #[derive(Debug, Clone, PartialEq, Eq, Hash)] pub struct WorkspaceRoot(PathBuf); impl WorkspaceRoot { /// Constructs a validated workspace root. pub fn try_new(path: PathBuf) -> Result { if !path.is_absolute() { return Err(InvariantViolation::new("workspace root must be absolute")); } if path.as_os_str().is_empty() { return Err(InvariantViolation::new("workspace root must be non-empty")); } Ok(Self(path)) } /// Returns the root path. #[must_use] pub fn as_path(&self) -> &Path { self.0.as_path() } } /// A non-empty absolute source file path. #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize)] #[serde(transparent)] pub struct SourceFilePath(PathBuf); impl SourceFilePath { /// Constructs a validated source file path. pub fn try_new(path: PathBuf) -> Result { if !path.is_absolute() { return Err(InvariantViolation::new("source file path must be absolute")); } if path.as_os_str().is_empty() { return Err(InvariantViolation::new( "source file path must be non-empty", )); } Ok(Self(path)) } /// Returns the underlying path. #[must_use] pub fn as_path(&self) -> &Path { self.0.as_path() } } impl<'de> Deserialize<'de> for SourceFilePath { fn deserialize(deserializer: D) -> Result where D: Deserializer<'de>, { let path = PathBuf::deserialize(deserializer)?; Self::try_new(path).map_err(serde::de::Error::custom) } } /// One-indexed source line number. #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)] #[serde(transparent)] pub struct OneIndexedLine(NonZeroU64); impl OneIndexedLine { /// Constructs a one-indexed line. pub fn try_new(raw: u64) -> Result { let line = NonZeroU64::new(raw).ok_or(InvariantViolation::new("line must be >= 1"))?; Ok(Self(line)) } /// Returns the one-indexed value. #[must_use] pub fn get(self) -> u64 { self.0.get() } /// Returns the corresponding zero-indexed value for LSP. #[must_use] pub fn to_zero_indexed(self) -> u32 { let raw = self.get().saturating_sub(1); u32::try_from(raw).unwrap_or(u32::MAX) } } /// One-indexed source column number. #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)] #[serde(transparent)] pub struct OneIndexedColumn(NonZeroU64); impl OneIndexedColumn { /// Constructs a one-indexed column. pub fn try_new(raw: u64) -> Result { let column = NonZeroU64::new(raw).ok_or(InvariantViolation::new("column must be >= 1"))?; Ok(Self(column)) } /// Returns the one-indexed value. #[must_use] pub fn get(self) -> u64 { self.0.get() } /// Returns the corresponding zero-indexed value for LSP. #[must_use] pub fn to_zero_indexed(self) -> u32 { let raw = self.get().saturating_sub(1); u32::try_from(raw).unwrap_or(u32::MAX) } } /// A file-local source point. #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)] pub struct SourcePoint { line: OneIndexedLine, column: OneIndexedColumn, } impl SourcePoint { /// Constructs a file-local source point. #[must_use] pub fn new(line: OneIndexedLine, column: OneIndexedColumn) -> Self { Self { line, column } } /// Returns the line component. #[must_use] pub fn line(self) -> OneIndexedLine { self.line } /// Returns the column component. #[must_use] pub fn column(self) -> OneIndexedColumn { self.column } } /// Request position in a source file. #[derive(Debug, Clone, PartialEq, Eq, Serialize)] pub struct SourcePosition { file_path: SourceFilePath, #[serde(flatten)] point: SourcePoint, } impl SourcePosition { /// Constructs a request position. #[must_use] pub fn new(file_path: SourceFilePath, point: SourcePoint) -> Self { Self { file_path, point } } /// Returns the source file path. #[must_use] pub fn file_path(&self) -> &SourceFilePath { &self.file_path } /// Returns the file-local point. #[must_use] pub fn point(&self) -> SourcePoint { self.point } /// Returns the one-indexed line. #[must_use] pub fn line(&self) -> OneIndexedLine { self.point.line() } /// Returns the one-indexed column. #[must_use] pub fn column(&self) -> OneIndexedColumn { self.point.column() } } #[derive(Debug, Clone, Deserialize)] struct SourcePositionWire { file_path: SourceFilePath, #[serde(flatten)] point: SourcePoint, } impl<'de> Deserialize<'de> for SourcePosition { fn deserialize(deserializer: D) -> Result where D: Deserializer<'de>, { let SourcePositionWire { file_path, point } = SourcePositionWire::deserialize(deserializer)?; Ok(Self::new(file_path, point)) } } /// A concrete source location. #[derive(Debug, Clone, PartialEq, Eq, Serialize)] pub struct SourceLocation { file_path: SourceFilePath, #[serde(flatten)] point: SourcePoint, } impl SourceLocation { /// Constructs a source location. #[must_use] pub fn new(file_path: SourceFilePath, point: SourcePoint) -> Self { Self { file_path, point } } /// Returns the source file path. #[must_use] pub fn file_path(&self) -> &SourceFilePath { &self.file_path } /// Returns the file-local point. #[must_use] pub fn point(&self) -> SourcePoint { self.point } /// Returns the one-indexed line. #[must_use] pub fn line(&self) -> OneIndexedLine { self.point.line() } /// Returns the one-indexed column. #[must_use] pub fn column(&self) -> OneIndexedColumn { self.point.column() } } #[derive(Debug, Clone, Deserialize)] struct SourceLocationWire { file_path: SourceFilePath, #[serde(flatten)] point: SourcePoint, } impl<'de> Deserialize<'de> for SourceLocation { fn deserialize(deserializer: D) -> Result where D: Deserializer<'de>, { let SourceLocationWire { file_path, point } = SourceLocationWire::deserialize(deserializer)?; Ok(Self::new(file_path, point)) } } /// A source range in a specific file. #[derive(Debug, Clone, PartialEq, Eq, Serialize)] pub struct SourceRange { file_path: SourceFilePath, start: SourcePoint, end: SourcePoint, } impl SourceRange { /// Constructs a validated source range. pub fn try_new( file_path: SourceFilePath, start: SourcePoint, end: SourcePoint, ) -> Result { if end < start { return Err(InvariantViolation::new( "source range end must not precede start", )); } Ok(Self { file_path, start, end, }) } /// Returns the source file path. #[must_use] pub fn file_path(&self) -> &SourceFilePath { &self.file_path } /// Returns the start point. #[must_use] pub fn start(&self) -> SourcePoint { self.start } /// Returns the end point. #[must_use] pub fn end(&self) -> SourcePoint { self.end } } #[derive(Debug, Clone, Deserialize)] struct SourceRangeWire { file_path: SourceFilePath, start: SourcePoint, end: SourcePoint, } impl<'de> Deserialize<'de> for SourceRange { fn deserialize(deserializer: D) -> Result where D: Deserializer<'de>, { let SourceRangeWire { file_path, start, end, } = SourceRangeWire::deserialize(deserializer)?; Self::try_new(file_path, start, end).map_err(serde::de::Error::custom) } } /// A monotonically increasing request sequence. #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] pub struct RequestSequence(NonZeroU64); impl RequestSequence { /// Starts a fresh sequence. #[must_use] pub fn genesis() -> Self { Self(NonZeroU64::MIN) } /// Returns the current integer value. #[must_use] pub fn get(self) -> u64 { self.0.get() } /// Consumes and returns the next sequence. #[must_use] pub fn next(self) -> Self { let next = self.get().saturating_add(1); let non_zero = NonZeroU64::new(next).map_or(NonZeroU64::MAX, |value| value); Self(non_zero) } } #[cfg(test)] mod tests { use super::{ Generation, InvariantViolation, OneIndexedColumn, OneIndexedLine, RequestSequence, SourceFilePath, SourcePoint, SourceRange, }; use assert_matches::assert_matches; use std::{num::NonZeroU64, path::PathBuf}; #[test] fn generation_advances_monotonically() { let first = Generation::genesis(); let second = first.next(); let third = second.next(); assert!(first < second); assert!(second < third); } #[test] fn generation_saturates_at_maximum() { let max = Generation(NonZeroU64::MAX); assert_eq!(max.next(), max); } #[test] fn line_must_be_one_or_greater() { assert_matches!(OneIndexedLine::try_new(0), Err(InvariantViolation { .. })); } #[test] fn column_must_be_one_or_greater() { assert_matches!(OneIndexedColumn::try_new(0), Err(InvariantViolation { .. })); } #[test] fn source_range_rejects_reversed_points() { let file_path = SourceFilePath::try_new(PathBuf::from("/tmp/range.rs")); assert!(file_path.is_ok()); let file_path = match file_path { Ok(value) => value, Err(_) => return, }; let start = SourcePoint::new( OneIndexedLine::try_new(4).unwrap_or(OneIndexedLine(NonZeroU64::MIN)), OneIndexedColumn::try_new(3).unwrap_or(OneIndexedColumn(NonZeroU64::MIN)), ); let end = SourcePoint::new( OneIndexedLine::try_new(2).unwrap_or(OneIndexedLine(NonZeroU64::MIN)), OneIndexedColumn::try_new(1).unwrap_or(OneIndexedColumn(NonZeroU64::MIN)), ); assert_matches!( SourceRange::try_new(file_path, start, end), Err(InvariantViolation { .. }) ); } #[test] fn request_sequence_saturates_at_maximum() { let max = RequestSequence(NonZeroU64::MAX); assert_eq!(max.next().get(), u64::MAX); } }