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}