Skip to main content

deps_core/
position.rs

1//! Protocol-agnostic position and range types (issue #1071).
2//!
3//! [`Position`] and [`Range`] are the domain-level replacement for
4//! `tower_lsp_server::ls_types::{Position, Range}` in [`crate::ecosystem::Dependency`]'s
5//! range accessors: field-for-field identical, but this module carries no dependency on
6//! `tower-lsp-server`, so a consumer that only needs `deps-core`'s parsed-dependency data
7//! (e.g. `deps-cli`, `deps-engine`) never has to name an LSP-protocol type to read a
8//! dependency's position in its manifest.
9//!
10//! `deps-core`'s own [`crate::lsp_helpers`] still constructs real
11//! `tower_lsp_server::ls_types::{Hover, Diagnostic, CodeAction}` objects directly — that
12//! coupling is unaffected by this module (see `lib.rs`'s "LSP type stability" doc section)
13//! — and converts a [`Position`]/[`Range`] into its `ls_types` equivalent via the [`From`]
14//! impls below wherever one needs to be embedded in such a response. These impls are legal
15//! under Rust's orphan rules because [`Position`]/[`Range`] are local to this crate; the
16//! same is not true for `url::Url` ⇄ `tower_lsp_server::ls_types::Uri` (neither type is
17//! local to any crate that would want to convert between them), which is why that
18//! conversion is a pair of free functions in `deps-lsp` instead (see
19//! `crates/deps-lsp/src/lsp_types_interop.rs`).
20
21#[cfg(feature = "lsp-responses")]
22use tower_lsp_server::ls_types;
23
24/// A zero-indexed line/UTF-16-code-unit-offset position, field-for-field identical to
25/// `tower_lsp_server::ls_types::Position` but carrying no dependency on `tower-lsp-server`.
26///
27/// # Examples
28///
29/// ```
30/// use deps_core::position::Position;
31///
32/// let pos = Position::new(3, 7);
33/// assert_eq!(pos.line, 3);
34/// assert_eq!(pos.character, 7);
35/// assert_eq!(pos, Position::new(3, 7));
36/// ```
37#[non_exhaustive]
38#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
39pub struct Position {
40    /// Zero-indexed line number.
41    pub line: u32,
42    /// Zero-indexed UTF-16 code unit offset on [`Self::line`].
43    pub character: u32,
44}
45
46impl Position {
47    /// Builds a [`Position`] from its `line`/`character` fields.
48    ///
49    /// Needed because [`Self`] is `#[non_exhaustive]`: a struct literal only works inside
50    /// this crate, so every other crate (e.g. the 14 ecosystem crates constructing a
51    /// [`crate::ecosystem::Dependency`] impl) must call this constructor instead.
52    ///
53    /// # Examples
54    ///
55    /// ```
56    /// use deps_core::position::Position;
57    ///
58    /// let pos = Position::new(0, 4);
59    /// assert_eq!(pos.line, 0);
60    /// ```
61    #[must_use]
62    pub const fn new(line: u32, character: u32) -> Self {
63        Self { line, character }
64    }
65}
66
67#[cfg(feature = "lsp-responses")]
68impl From<ls_types::Position> for Position {
69    fn from(value: ls_types::Position) -> Self {
70        Self {
71            line: value.line,
72            character: value.character,
73        }
74    }
75}
76
77#[cfg(feature = "lsp-responses")]
78impl From<Position> for ls_types::Position {
79    fn from(value: Position) -> Self {
80        Self::new(value.line, value.character)
81    }
82}
83
84/// A `[start, end)` span over [`Position`]s, field-for-field identical to
85/// `tower_lsp_server::ls_types::Range` but carrying no dependency on `tower-lsp-server`.
86///
87/// # Examples
88///
89/// ```
90/// use deps_core::position::{Position, Range};
91///
92/// let range = Range::new(Position::new(0, 0), Position::new(0, 4));
93/// assert_eq!(range.start, Position::new(0, 0));
94/// assert_eq!(range.end, Position::new(0, 4));
95/// ```
96#[non_exhaustive]
97#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
98pub struct Range {
99    /// Inclusive start position.
100    pub start: Position,
101    /// Exclusive end position.
102    pub end: Position,
103}
104
105impl Range {
106    /// Builds a [`Range`] from its `start`/`end` fields.
107    ///
108    /// Needed because [`Self`] is `#[non_exhaustive]`: a struct literal only works inside
109    /// this crate, so every other crate (e.g. the 14 ecosystem crates constructing a
110    /// [`crate::ecosystem::Dependency`] impl) must call this constructor instead.
111    ///
112    /// # Examples
113    ///
114    /// ```
115    /// use deps_core::position::{Position, Range};
116    ///
117    /// let range = Range::new(Position::new(1, 0), Position::new(1, 8));
118    /// assert_eq!(range.start.line, 1);
119    /// ```
120    #[must_use]
121    pub const fn new(start: Position, end: Position) -> Self {
122        Self { start, end }
123    }
124}
125
126#[cfg(feature = "lsp-responses")]
127impl From<ls_types::Range> for Range {
128    fn from(value: ls_types::Range) -> Self {
129        Self {
130            start: value.start.into(),
131            end: value.end.into(),
132        }
133    }
134}
135
136#[cfg(feature = "lsp-responses")]
137impl From<Range> for ls_types::Range {
138    fn from(value: Range) -> Self {
139        Self::new(value.start.into(), value.end.into())
140    }
141}
142
143#[cfg(test)]
144mod tests {
145    use super::*;
146
147    #[cfg(feature = "lsp-responses")]
148    #[test]
149    fn test_position_roundtrips_through_ls_types() {
150        let original = Position::new(12, 34);
151        let ls_pos: ls_types::Position = original.into();
152        assert_eq!(ls_pos.line, 12);
153        assert_eq!(ls_pos.character, 34);
154        assert_eq!(Position::from(ls_pos), original);
155    }
156
157    #[cfg(feature = "lsp-responses")]
158    #[test]
159    fn test_range_roundtrips_through_ls_types() {
160        let original = Range::new(Position::new(0, 0), Position::new(2, 5));
161        let ls_range: ls_types::Range = original.into();
162        assert_eq!(ls_range.start.line, 0);
163        assert_eq!(ls_range.end.line, 2);
164        assert_eq!(Range::from(ls_range), original);
165    }
166
167    #[test]
168    fn test_range_usable_as_hashmap_key() {
169        use std::collections::HashMap;
170
171        let mut map: HashMap<Range, &str> = HashMap::new();
172        map.insert(
173            Range::new(Position::new(0, 0), Position::new(0, 4)),
174            "serde",
175        );
176        assert_eq!(
177            map.get(&Range::new(Position::new(0, 0), Position::new(0, 4))),
178            Some(&"serde")
179        );
180    }
181}