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