Skip to main content

mcpls_core/bridge/translator/
selection_range.rs

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