Skip to main content

mcpls_core/bridge/translator/
selection_range.rs

1//! Selection range handler.
2
3use 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
14/// The ranges of `first` and its ancestors, innermost first, and whether the
15/// chain was longer than [`MAX_SELECTION_CHAIN`].
16///
17/// Walks the `parent` links iteratively, so the cost is bounded by the cap
18/// whatever the nesting.
19fn 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    /// Handle a selection range request: the ranges enclosing `position`,
33    /// innermost first.
34    ///
35    /// One position is sent per request. The chain is returned as the server
36    /// reported it, cut to [`MAX_SELECTION_CHAIN`] ranges (the outermost are
37    /// dropped and `truncated` is set); an empty or `null` answer is an empty
38    /// chain. Both answers are syntactic, so the request does not wait for
39    /// indexing.
40    ///
41    /// # Errors
42    ///
43    /// Returns an error if the LSP request fails, the file cannot be opened,
44    /// or the routed server does not advertise `selectionRangeProvider`
45    /// support.
46    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        // "é" is 2 UTF-8 bytes: byte offset 5 is character 4 (1-based 5).
266        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}