perl_position_tracking/
wire.rs1use crate::{offset_to_utf16_line_col, utf16_line_col_to_offset};
12use serde::{Deserialize, Serialize};
13
14#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
18pub struct WirePosition {
19 pub line: u32,
21 pub character: u32,
23}
24impl WirePosition {
25 pub fn new(line: u32, character: u32) -> Self {
27 Self { line, character }
28 }
29
30 pub fn from_byte_offset(source: &str, byte_offset: usize) -> Self {
32 let (line, character) = offset_to_utf16_line_col(source, byte_offset);
33 Self { line, character }
34 }
35
36 pub fn to_byte_offset(&self, source: &str) -> usize {
38 utf16_line_col_to_offset(source, self.line, self.character)
39 }
40}
41
42#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
44pub struct WireRange {
45 pub start: WirePosition,
47 pub end: WirePosition,
49}
50impl WireRange {
51 pub fn new(start: WirePosition, end: WirePosition) -> Self {
53 Self { start, end }
54 }
55
56 pub fn from_byte_offsets(source: &str, start_byte: usize, end_byte: usize) -> Self {
58 Self {
59 start: WirePosition::from_byte_offset(source, start_byte),
60 end: WirePosition::from_byte_offset(source, end_byte),
61 }
62 }
63
64 pub fn empty(pos: WirePosition) -> Self {
66 Self { start: pos, end: pos }
67 }
68
69 pub fn whole_document(source: &str) -> Self {
71 Self {
72 start: WirePosition::new(0, 0),
73 end: WirePosition::from_byte_offset(source, source.len()),
74 }
75 }
76}
77
78#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
80pub struct WireLocation {
81 pub uri: String,
83 pub range: WireRange,
85}
86impl WireLocation {
87 pub fn new(uri: String, range: WireRange) -> Self {
89 Self { uri, range }
90 }
91}
92#[cfg(feature = "lsp-compat")]
93impl From<WirePosition> for lsp_types::Position {
94 fn from(p: WirePosition) -> Self {
95 Self { line: p.line, character: p.character }
96 }
97}
98#[cfg(feature = "lsp-compat")]
99impl From<lsp_types::Position> for WirePosition {
100 fn from(p: lsp_types::Position) -> Self {
101 Self { line: p.line, character: p.character }
102 }
103}
104#[cfg(feature = "lsp-compat")]
105impl From<WireRange> for lsp_types::Range {
106 fn from(r: WireRange) -> Self {
107 Self { start: r.start.into(), end: r.end.into() }
108 }
109}
110#[cfg(feature = "lsp-compat")]
111impl From<lsp_types::Range> for WireRange {
112 fn from(r: lsp_types::Range) -> Self {
113 Self { start: r.start.into(), end: r.end.into() }
114 }
115}
116#[cfg(feature = "lsp-compat")]
117fn fallback_lsp_uri() -> lsp_types::Uri {
118 for candidate in ["file:///unknown", "file:///", "about:blank", "urn:perl-lsp:unknown"] {
119 if let Ok(uri) = candidate.parse::<lsp_types::Uri>() {
120 return uri;
121 }
122 }
123
124 let mut suffix = 0usize;
126 loop {
127 let candidate = format!("http://localhost/{suffix}");
128 if let Ok(uri) = candidate.parse::<lsp_types::Uri>() {
129 return uri;
130 }
131 suffix = suffix.saturating_add(1);
132 }
133}
134
135#[cfg(feature = "lsp-compat")]
136impl From<WireLocation> for lsp_types::Location {
137 fn from(l: WireLocation) -> Self {
138 let uri = match l.uri.parse::<lsp_types::Uri>() {
139 Ok(u) => u,
140 Err(_) => fallback_lsp_uri(),
141 };
142 Self { uri, range: l.range.into() }
143 }
144}
145
146#[cfg(all(test, feature = "lsp-compat"))]
147mod tests {
148 use super::*;
149
150 #[test]
151 fn wire_location_to_lsp_location_preserves_valid_uri() {
152 let wire_location = WireLocation::new(
153 "file:///tmp/example.pl".to_string(),
154 WireRange::new(WirePosition::new(1, 2), WirePosition::new(3, 4)),
155 );
156
157 let location: lsp_types::Location = wire_location.into();
158
159 assert_eq!(location.uri.as_str(), "file:///tmp/example.pl");
160 assert_eq!(location.range.start.line, 1);
161 assert_eq!(location.range.start.character, 2);
162 assert_eq!(location.range.end.line, 3);
163 assert_eq!(location.range.end.character, 4);
164 }
165
166 #[test]
167 fn wire_location_to_lsp_location_uses_fallback_for_invalid_uri() {
168 let wire_location = WireLocation::new(
169 "not a uri".to_string(),
170 WireRange::new(WirePosition::new(0, 0), WirePosition::new(0, 1)),
171 );
172
173 let location: lsp_types::Location = wire_location.into();
174
175 assert_ne!(location.uri.as_str(), "not a uri");
176 assert!(!location.uri.as_str().is_empty());
177 assert_eq!(location.range.start.line, 0);
178 assert_eq!(location.range.end.character, 1);
179 }
180}