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}