mcpls_core/bridge/translator/
selection_range.rs1use lsp_types::{
4 PartialResultParams, SelectionRangeParams, TextDocumentIdentifier, WorkDoneProgressParams,
5};
6
7use super::Translator;
8use super::dto::{MAX_SELECTION_CHAIN, Position, SelectionRangesResult};
9use super::navigation::ItemBudget;
10use super::routing::{FileLocalCapability, IndexingGate};
11use crate::bridge::ClientPath;
12use crate::error::Result;
13
14fn chain(first: lsp_types::SelectionRange) -> (Vec<lsp_types::Range>, bool) {
20 let mut next = Some(first);
21 let ancestors = std::iter::from_fn(|| {
22 let node = next.take()?;
23 next = node.parent.map(|parent| *parent);
24 Some(node.range)
25 });
26 let mut budget = ItemBudget::with_cap(MAX_SELECTION_CHAIN);
27 let ranges = budget.admit_iter(ancestors);
28 (ranges, budget.truncated())
29}
30
31impl Translator {
32 pub async fn handle_selection_range(
47 &self,
48 file_path: ClientPath,
49 position: Position,
50 ) -> Result<SelectionRangesResult> {
51 let doc = self
52 .prepare_positioned_document(
53 &file_path,
54 IndexingGate::FileLocal(FileLocalCapability::SelectionRange),
55 &[position],
56 )
57 .await?;
58 let (server_id, client, uri) = (doc.server_id(), doc.client(), doc.uri());
59 let ctx = self.encoding_ctx(server_id);
60 let response_uri = uri.clone();
61 let lsp_position = ctx.to_lsp(uri, position).await;
62
63 let params = SelectionRangeParams {
64 text_document: TextDocumentIdentifier { uri: uri.clone() },
65 positions: vec![lsp_position],
66 work_done_progress_params: WorkDoneProgressParams::default(),
67 partial_result_params: PartialResultParams::default(),
68 };
69
70 let response = client
71 .request_typed::<lsp_types::SelectionRangeRequest>(params, client.request_timeout())
72 .await?;
73
74 let (lsp_ranges, truncated) = response
75 .and_then(|chains| chains.into_iter().next())
76 .map_or_default(chain);
77 let mut ranges = Vec::with_capacity(lsp_ranges.len());
78 for range in lsp_ranges {
79 ranges.push(ctx.normalize_range(&response_uri, range).await);
80 }
81
82 Ok(SelectionRangesResult {
83 ranges,
84 truncated,
85 positions_degraded: ctx.positions_degraded(),
86 })
87 }
88}
89
90#[cfg(test)]
91mod tests {
92 use std::sync::Arc;
93 use std::time::Duration;
94 use std::{assert_matches, fs};
95
96 use tempfile::TempDir;
97 use tokio::io::BufReader;
98 use tokio::time::timeout;
99
100 use super::*;
101 use crate::bridge::translator::testing::*;
102 use crate::config::ServerId;
103 use crate::error::Error;
104 use crate::test_lsp::client_path;
105
106 fn caps() -> lsp_types::ServerCapabilities {
107 lsp_types::ServerCapabilities {
108 selection_range_provider: Some(lsp_types::SelectionRangeProvider::Bool(true)),
109 ..Default::default()
110 }
111 }
112
113 fn range_json(start: u32, end: u32) -> serde_json::Value {
114 serde_json::json!({
115 "start": {"line": 0, "character": start},
116 "end": {"line": 0, "character": end}
117 })
118 }
119
120 fn nested(widths: &[(u32, u32)]) -> serde_json::Value {
121 widths
122 .iter()
123 .rev()
124 .fold(serde_json::Value::Null, |parent, (s, e)| {
125 let mut node = serde_json::json!({ "range": range_json(*s, *e) });
126 if !parent.is_null() {
127 node["parent"] = parent;
128 }
129 node
130 })
131 }
132
133 async fn selection_with_response(
134 source: &str,
135 response: serde_json::Value,
136 encoding: Option<lsp_types::PositionEncodingKind>,
137 ) -> (serde_json::Value, SelectionRangesResult) {
138 let dir = TempDir::new().unwrap();
139 let server_id = ServerId::from_static("rust");
140 let (translator, mut server) = encoding.map_or_else(
141 || translator_with_capabilities(&dir, &server_id, caps()),
142 |encoding| {
143 translator_with_capabilities_and_encoding(&dir, &server_id, caps(), encoding)
144 },
145 );
146 let path = dir.path().join("a.rs");
147 fs::write(&path, source).unwrap();
148
149 let translator = Arc::new(translator);
150 let handle = {
151 let translator = Arc::clone(&translator);
152 let path = path.to_string_lossy().into_owned();
153 tokio::spawn(async move {
154 translator
155 .handle_selection_range(client_path(path), pos(1, 5))
156 .await
157 })
158 };
159 let mut wire = BufReader::new(&mut server.write_stdout);
160 assert_eq!(
161 read_framed_message(&mut wire).await["method"],
162 "textDocument/didOpen"
163 );
164 let request = read_framed_message(&mut wire).await;
165 assert_eq!(request["method"], "textDocument/selectionRange");
166 write_response(&mut server.read_half_stdin, &request["id"], response).await;
167
168 let result = timeout(Duration::from_secs(30), handle)
169 .await
170 .expect("handler call should not hang")
171 .unwrap()
172 .unwrap();
173 (request, result)
174 }
175
176 #[tokio::test]
177 async fn returns_the_chain_innermost_first_and_sends_one_position() {
178 let (request, result) = selection_with_response(
179 "f(a + b);",
180 serde_json::json!([nested(&[(2, 3), (2, 7), (0, 8), (0, 9)])]),
181 None,
182 )
183 .await;
184
185 assert_eq!(
186 request["params"]["positions"],
187 serde_json::json!([{"line": 0, "character": 4}])
188 );
189 let spans: Vec<_> = result
190 .ranges
191 .iter()
192 .map(|r| (r.start.character, r.end.character))
193 .collect();
194 assert_eq!(spans, [(3, 4), (3, 8), (1, 9), (1, 10)]);
195 assert!(!result.truncated);
196 assert_eq!(result.positions_degraded, None);
197 let wire = serde_json::to_value(&result).unwrap();
198 assert!(wire.get("truncated").is_none());
199 }
200
201 #[tokio::test]
202 async fn keeps_equal_consecutive_ranges_as_reported() {
203 let (_, result) = selection_with_response(
204 "f(a);",
205 serde_json::json!([nested(&[(2, 3), (2, 3), (0, 4)])]),
206 None,
207 )
208 .await;
209 assert_eq!(result.ranges.len(), 3);
210 }
211
212 #[tokio::test]
213 async fn null_and_empty_answers_are_an_empty_chain() {
214 for response in [serde_json::Value::Null, serde_json::json!([])] {
215 let (_, result) = selection_with_response("f(a);", response, None).await;
216 assert!(result.ranges.is_empty() && !result.truncated);
217 }
218 }
219
220 #[tokio::test]
221 async fn a_chain_longer_than_the_cap_keeps_the_innermost_ranges() {
222 let widths: Vec<(u32, u32)> = (0..100).map(|i| (50 - i / 2, 60 + i)).collect();
223 let (_, result) =
224 selection_with_response("x", serde_json::json!([nested(&widths)]), None).await;
225
226 assert_eq!(result.ranges.len(), MAX_SELECTION_CHAIN);
227 assert!(result.truncated);
228 assert_eq!(result.ranges[0].end.character, 61);
229 assert_eq!(
230 result.ranges[MAX_SELECTION_CHAIN - 1].end.character,
231 60 + u32::try_from(MAX_SELECTION_CHAIN).unwrap()
232 );
233 assert_eq!(serde_json::to_value(&result).unwrap()["truncated"], true);
234 }
235
236 #[tokio::test]
237 async fn a_chain_of_exactly_the_cap_is_not_truncated_and_one_more_is() {
238 let widths = |len: u32| -> Vec<(u32, u32)> { (0..len).map(|i| (50 - i, 60 + i)).collect() };
239 let cap = u32::try_from(MAX_SELECTION_CHAIN).unwrap();
240
241 let (_, exact) =
242 selection_with_response("x", serde_json::json!([nested(&widths(cap))]), None).await;
243 assert_eq!(exact.ranges.len(), MAX_SELECTION_CHAIN);
244 assert!(!exact.truncated);
245
246 let (_, over) =
247 selection_with_response("x", serde_json::json!([nested(&widths(cap + 1))]), None).await;
248 assert_eq!(over.ranges.len(), MAX_SELECTION_CHAIN);
249 assert!(over.truncated);
250 }
251
252 #[tokio::test]
253 async fn only_the_first_chain_of_the_response_is_used() {
254 let (_, result) = selection_with_response(
255 "f(a);",
256 serde_json::json!([nested(&[(2, 3)]), nested(&[(0, 4), (0, 5)])]),
257 None,
258 )
259 .await;
260 assert_eq!(result.ranges.len(), 1);
261 }
262
263 #[tokio::test]
264 async fn utf8_server_ranges_and_position_are_converted() {
265 let (request, result) = selection_with_response(
267 "é = a;",
268 serde_json::json!([nested(&[(5, 6), (0, 7)])]),
269 Some(lsp_types::PositionEncodingKind::UTF8),
270 )
271 .await;
272 assert_eq!(request["params"]["positions"][0]["character"], 5);
273 assert_eq!(result.ranges[0].start.character, 5);
274 assert_eq!(result.ranges[0].end.character, 6);
275 assert_eq!(result.positions_degraded, None);
276 }
277
278 #[tokio::test]
279 async fn rejects_missing_capability() {
280 let dir = TempDir::new().unwrap();
281 let (translator, _server) = translator_with_capabilities(
282 &dir,
283 &ServerId::from_static("rust"),
284 lsp_types::ServerCapabilities::default(),
285 );
286 let path = dir.path().join("a.rs");
287 fs::write(&path, "f(a);").unwrap();
288 let result = translator
289 .handle_selection_range(client_path(path.to_string_lossy().into_owned()), pos(1, 1))
290 .await;
291 assert_matches!(result, Err(Error::CapabilityNotSupported { .. }));
292 }
293
294 #[test]
295 fn chain_is_bounded_whatever_the_nesting() {
296 let mut node = lsp_types::SelectionRange {
297 range: lsp_types::Range::default(),
298 parent: None,
299 };
300 for _ in 0..120 {
301 node = lsp_types::SelectionRange {
302 range: lsp_types::Range::default(),
303 parent: Some(Box::new(node)),
304 };
305 }
306 let (ranges, truncated) = chain(node);
307 assert_eq!(ranges.len(), MAX_SELECTION_CHAIN);
308 assert!(truncated);
309 }
310}