Skip to main content

ide/
extend_selection.rs

1use std::iter::successors;
2
3use hir::Semantics;
4use ide_db::RootDatabase;
5use syntax::{
6    Direction, NodeOrToken,
7    SyntaxKind::{self, *},
8    SyntaxNode, SyntaxToken, T, TextRange, TextSize, TokenAtOffset,
9    algo::{self, skip_trivia_token},
10    ast::{self, AstNode, AstToken},
11};
12
13use crate::FileRange;
14
15// Feature: Expand and Shrink Selection
16//
17// Extends or shrinks the current selection to the encompassing syntactic construct
18// (expression, statement, item, module, etc). It works with multiple cursors.
19//
20// | Editor  | Shortcut |
21// |---------|----------|
22// | VS Code | <kbd>Alt+Shift+→</kbd>, <kbd>Alt+Shift+←</kbd> |
23//
24// ![Expand and Shrink Selection](https://user-images.githubusercontent.com/48062697/113020651-b42fc800-917a-11eb-8a4f-cf1a07859fac.gif)
25pub(crate) fn extend_selection(db: &RootDatabase, frange: FileRange) -> TextRange {
26    let sema = Semantics::new(db);
27    let src = sema.parse_guess_edition(frange.file_id);
28    try_extend_selection(&sema, src.syntax(), frange).unwrap_or(frange.range)
29}
30
31fn try_extend_selection(
32    sema: &Semantics<'_, RootDatabase>,
33    root: &SyntaxNode,
34    frange: FileRange,
35) -> Option<TextRange> {
36    let range = frange.range;
37
38    let string_kinds =
39        [COMMENT, INNER_DOC_COMMENT, OUTER_DOC_COMMENT, STRING, BYTE_STRING, C_STRING];
40    let list_kinds = [
41        RECORD_PAT_FIELD_LIST,
42        MATCH_ARM_LIST,
43        RECORD_FIELD_LIST,
44        TUPLE_FIELD_LIST,
45        RECORD_EXPR_FIELD_LIST,
46        VARIANT_LIST,
47        USE_TREE_LIST,
48        GENERIC_PARAM_LIST,
49        GENERIC_ARG_LIST,
50        TYPE_BOUND_LIST,
51        PARAM_LIST,
52        ARG_LIST,
53        ARRAY_EXPR,
54        TUPLE_EXPR,
55        TUPLE_TYPE,
56        TUPLE_PAT,
57        WHERE_CLAUSE,
58    ];
59
60    if range.is_empty() {
61        let offset = range.start();
62        let mut leaves = root.token_at_offset(offset);
63        if leaves.clone().all(|it| it.kind() == WHITESPACE) {
64            return Some(extend_ws(root, leaves.next()?, offset));
65        }
66        let leaf_range = match leaves {
67            TokenAtOffset::None => return None,
68            TokenAtOffset::Single(l) => {
69                if string_kinds.contains(&l.kind()) {
70                    extend_single_word_in_comment_or_string(&l, offset)
71                        .unwrap_or_else(|| l.text_range())
72                } else {
73                    l.text_range()
74                }
75            }
76            TokenAtOffset::Between(l, r) => pick_best(l, r).text_range(),
77        };
78        return Some(leaf_range);
79    };
80    let node = match root.covering_element(range) {
81        NodeOrToken::Token(token) => {
82            if token.text_range() != range {
83                return Some(token.text_range());
84            }
85            if let Some(comment) = ast::AnyComment::cast(token.clone())
86                && let Some(range) = extend_comments(comment)
87            {
88                return Some(range);
89            }
90            token.parent()?
91        }
92        NodeOrToken::Node(node) => node,
93    };
94
95    // if we are in single token_tree, we maybe live in macro or attr
96    if node.kind() == TOKEN_TREE
97        && let Some(macro_call) = node.ancestors().find_map(ast::MacroCall::cast)
98        && let Some(range) = extend_tokens_from_range(sema, macro_call, range)
99    {
100        return Some(range);
101    }
102
103    if node.text_range() != range {
104        return Some(node.text_range());
105    }
106
107    let node = shallowest_node(&node);
108
109    if node.parent().is_some_and(|n| list_kinds.contains(&n.kind()))
110        && let Some(range) = extend_list_item(&node)
111    {
112        return Some(range);
113    }
114
115    node.parent().map(|it| it.text_range())
116}
117
118fn extend_tokens_from_range(
119    sema: &Semantics<'_, RootDatabase>,
120    macro_call: ast::MacroCall,
121    original_range: TextRange,
122) -> Option<TextRange> {
123    let src = macro_call.syntax().covering_element(original_range);
124    let (first_token, last_token) = match src {
125        NodeOrToken::Node(it) => (it.first_token()?, it.last_token()?),
126        NodeOrToken::Token(it) => (it.clone(), it),
127    };
128
129    let mut first_token = skip_trivia_token(first_token, Direction::Next)?;
130    let mut last_token = skip_trivia_token(last_token, Direction::Prev)?;
131
132    while !original_range.contains_range(first_token.text_range()) {
133        first_token = skip_trivia_token(first_token.next_token()?, Direction::Next)?;
134    }
135    while !original_range.contains_range(last_token.text_range()) {
136        last_token = skip_trivia_token(last_token.prev_token()?, Direction::Prev)?;
137    }
138
139    // compute original mapped token range
140    let extended = {
141        let fst_expanded = sema.descend_into_macros_single_exact(first_token.clone());
142        let lst_expanded = sema.descend_into_macros_single_exact(last_token.clone());
143        let mut lca =
144            algo::least_common_ancestor(&fst_expanded.parent()?, &lst_expanded.parent()?)?;
145        lca = shallowest_node(&lca);
146        if lca.first_token() == Some(fst_expanded) && lca.last_token() == Some(lst_expanded) {
147            lca = lca.parent()?;
148        }
149        lca
150    };
151
152    // Compute parent node range
153    let validate = || {
154        let extended = &extended;
155        move |token: &SyntaxToken| -> bool {
156            let expanded = sema.descend_into_macros_single_exact(token.clone());
157            let parent = match expanded.parent() {
158                Some(it) => it,
159                None => return false,
160            };
161            algo::least_common_ancestor(extended, &parent).as_ref() == Some(extended)
162        }
163    };
164
165    // Find the first and last text range under expanded parent
166    let first = successors(Some(first_token), |token| {
167        let token = token.prev_token()?;
168        skip_trivia_token(token, Direction::Prev)
169    })
170    .take_while(validate())
171    .last()?;
172
173    let last = successors(Some(last_token), |token| {
174        let token = token.next_token()?;
175        skip_trivia_token(token, Direction::Next)
176    })
177    .take_while(validate())
178    .last()?;
179
180    let range = first.text_range().cover(last.text_range());
181    if range.contains_range(original_range) && original_range != range { Some(range) } else { None }
182}
183
184/// Find the shallowest node with same range, which allows us to traverse siblings.
185fn shallowest_node(node: &SyntaxNode) -> SyntaxNode {
186    node.ancestors().take_while(|n| n.text_range() == node.text_range()).last().unwrap()
187}
188
189fn extend_single_word_in_comment_or_string(
190    leaf: &SyntaxToken,
191    offset: TextSize,
192) -> Option<TextRange> {
193    let text: &str = leaf.text();
194    let cursor_position: u32 = (offset - leaf.text_range().start()).into();
195
196    let (before, after) = text.split_at(cursor_position as usize);
197
198    fn non_word_char(c: char) -> bool {
199        !(c.is_alphanumeric() || c == '_')
200    }
201
202    let start_idx = before.rfind(non_word_char)? as u32;
203    let end_idx = after.find(non_word_char).unwrap_or(after.len()) as u32;
204
205    // FIXME: use `ceil_char_boundary` from `std::str` when it gets stable
206    // https://github.com/rust-lang/rust/issues/93743
207    fn ceil_char_boundary(text: &str, index: u32) -> u32 {
208        (index..).find(|&index| text.is_char_boundary(index as usize)).unwrap_or(text.len() as u32)
209    }
210
211    let from: TextSize = ceil_char_boundary(text, start_idx + 1).into();
212    let to: TextSize = (cursor_position + end_idx).into();
213
214    let range = TextRange::new(from, to);
215    if range.is_empty() { None } else { Some(range + leaf.text_range().start()) }
216}
217
218fn extend_ws(root: &SyntaxNode, ws: SyntaxToken, offset: TextSize) -> TextRange {
219    let ws_text = ws.text();
220    let suffix = TextRange::new(offset, ws.text_range().end()) - ws.text_range().start();
221    let prefix = TextRange::new(ws.text_range().start(), offset) - ws.text_range().start();
222    let ws_suffix = &ws_text[suffix];
223    let ws_prefix = &ws_text[prefix];
224    if ws_text.contains('\n')
225        && !ws_suffix.contains('\n')
226        && let Some(node) = ws.next_sibling_or_token()
227    {
228        let start = match ws_prefix.rfind('\n') {
229            Some(idx) => ws.text_range().start() + TextSize::from((idx + 1) as u32),
230            None => node.text_range().start(),
231        };
232        let end = if root.text().char_at(node.text_range().end()) == Some('\n') {
233            node.text_range().end() + TextSize::of('\n')
234        } else {
235            node.text_range().end()
236        };
237        return TextRange::new(start, end);
238    }
239    ws.text_range()
240}
241
242fn pick_best(l: SyntaxToken, r: SyntaxToken) -> SyntaxToken {
243    return if priority(&r) > priority(&l) { r } else { l };
244    fn priority(n: &SyntaxToken) -> usize {
245        match n.kind() {
246            WHITESPACE => 0,
247            IDENT | T![self] | T![super] | T![crate] | T![Self] | LIFETIME_IDENT => 2,
248            _ => 1,
249        }
250    }
251}
252
253/// Extend list item selection to include nearby delimiter and whitespace.
254fn extend_list_item(node: &SyntaxNode) -> Option<TextRange> {
255    fn is_single_line_ws(node: &SyntaxToken) -> bool {
256        node.kind() == WHITESPACE && !node.text().contains('\n')
257    }
258
259    fn nearby_delimiter(
260        delimiter_kind: SyntaxKind,
261        node: &SyntaxNode,
262        dir: Direction,
263    ) -> Option<SyntaxToken> {
264        node.siblings_with_tokens(dir)
265            .skip(1)
266            .find(|node| match node {
267                NodeOrToken::Node(_) => true,
268                NodeOrToken::Token(it) => !is_single_line_ws(it),
269            })
270            .and_then(|it| it.into_token())
271            .filter(|node| node.kind() == delimiter_kind)
272    }
273
274    let delimiter = match node.kind() {
275        TYPE_BOUND => T![+],
276        _ => T![,],
277    };
278
279    if let Some(delimiter_node) = nearby_delimiter(delimiter, node, Direction::Next) {
280        // Include any following whitespace when delimiter is after list item.
281        let final_node = delimiter_node
282            .next_sibling_or_token()
283            .and_then(|it| it.into_token())
284            .filter(is_single_line_ws)
285            .unwrap_or(delimiter_node);
286
287        return Some(TextRange::new(node.text_range().start(), final_node.text_range().end()));
288    }
289    if let Some(delimiter_node) = nearby_delimiter(delimiter, node, Direction::Prev) {
290        return Some(TextRange::new(delimiter_node.text_range().start(), node.text_range().end()));
291    }
292
293    None
294}
295
296fn extend_comments(comment: ast::AnyComment) -> Option<TextRange> {
297    let prev = adj_comments(&comment, Direction::Prev);
298    let next = adj_comments(&comment, Direction::Next);
299    if prev != next {
300        Some(TextRange::new(prev.syntax().text_range().start(), next.syntax().text_range().end()))
301    } else {
302        None
303    }
304}
305
306fn adj_comments(comment: &ast::AnyComment, dir: Direction) -> ast::AnyComment {
307    let mut res = comment.clone();
308    for element in comment.syntax().siblings_with_tokens(dir) {
309        let token = match element.as_token() {
310            None => break,
311            Some(token) => token,
312        };
313        if let Some(c) = ast::AnyComment::cast(token.clone()) {
314            res = c
315        } else if token.kind() != WHITESPACE || token.text().contains("\n\n") {
316            break;
317        }
318    }
319    res
320}
321
322#[cfg(test)]
323mod tests {
324    use crate::fixture;
325
326    use super::*;
327
328    fn do_check(before: &str, afters: &[&str]) {
329        let (analysis, position) = fixture::position(before);
330        let before = analysis.file_text(position.file_id).unwrap();
331        let range = TextRange::empty(position.offset);
332        let mut frange = FileRange { file_id: position.file_id, range };
333
334        for &after in afters {
335            frange.range = analysis.extend_selection(frange).unwrap();
336            let actual = &before[frange.range];
337            assert_eq!(after, actual);
338        }
339    }
340
341    #[test]
342    fn test_extend_selection_arith() {
343        do_check(r#"fn foo() { $01 + 1 }"#, &["1", "1 + 1", "{ 1 + 1 }"]);
344    }
345
346    #[test]
347    fn test_extend_selection_list() {
348        do_check(r#"fn foo($0x: i32) {}"#, &["x", "x: i32"]);
349        do_check(r#"fn foo($0x: i32, y: i32) {}"#, &["x", "x: i32", "x: i32, "]);
350        do_check(r#"fn foo($0x: i32,y: i32) {}"#, &["x", "x: i32", "x: i32,", "(x: i32,y: i32)"]);
351        do_check(r#"fn foo(x: i32, $0y: i32) {}"#, &["y", "y: i32", ", y: i32"]);
352        do_check(r#"fn foo(x: i32, $0y: i32, ) {}"#, &["y", "y: i32", "y: i32, "]);
353        do_check(r#"fn foo(x: i32,$0y: i32) {}"#, &["y", "y: i32", ",y: i32"]);
354
355        do_check(r#"const FOO: [usize; 2] = [ 22$0 , 33];"#, &["22", "22 , "]);
356        do_check(r#"const FOO: [usize; 2] = [ 22 , 33$0];"#, &["33", ", 33"]);
357        do_check(r#"const FOO: [usize; 2] = [ 22 , 33$0 ,];"#, &["33", "33 ,", "[ 22 , 33 ,]"]);
358
359        do_check(r#"fn main() { (1, 2$0) }"#, &["2", ", 2", "(1, 2)"]);
360
361        do_check(
362            r#"
363const FOO: [usize; 2] = [
364    22,
365    $033,
366]"#,
367            &["33", "33,"],
368        );
369
370        do_check(
371            r#"
372const FOO: [usize; 2] = [
373    22
374    , 33$0,
375]"#,
376            &["33", "33,"],
377        );
378    }
379
380    #[test]
381    fn test_extend_selection_start_of_the_line() {
382        do_check(
383            r#"
384impl S {
385$0    fn foo() {
386
387    }
388}"#,
389            &["    fn foo() {\n\n    }\n"],
390        );
391    }
392
393    #[test]
394    fn test_extend_selection_doc_comments() {
395        do_check(
396            r#"
397struct A;
398
399/// bla
400/// bla
401struct B {
402    $0
403}
404            "#,
405            &["\n    \n", "{\n    \n}", "/// bla\n/// bla\nstruct B {\n    \n}"],
406        )
407    }
408
409    #[test]
410    fn test_extend_selection_comments() {
411        do_check(
412            r#"
413fn bar(){}
414
415// fn foo() {
416// 1 + $01
417// }
418
419// fn foo(){}
420    "#,
421            &["1", "// 1 + 1", "// fn foo() {\n// 1 + 1\n// }"],
422        );
423
424        do_check(
425            r#"
426// #[derive(Debug, Clone, Copy, PartialEq, Eq)]
427// pub enum Direction {
428//  $0   Next,
429//     Prev
430// }
431"#,
432            &[
433                "//     Next,",
434                "// #[derive(Debug, Clone, Copy, PartialEq, Eq)]\n// pub enum Direction {\n//     Next,\n//     Prev\n// }",
435            ],
436        );
437
438        do_check(
439            r#"
440/*
441foo
442_bar1$0*/
443"#,
444            &["_bar1", "/*\nfoo\n_bar1*/"],
445        );
446
447        do_check(r#"//!$0foo_2 bar"#, &["foo_2", "//!foo_2 bar"]);
448
449        do_check(r#"/$0/foo bar"#, &["//foo bar"]);
450    }
451
452    #[test]
453    fn test_extend_selection_prefer_idents() {
454        do_check(
455            r#"
456fn main() { foo$0+bar;}
457"#,
458            &["foo", "foo+bar"],
459        );
460        do_check(
461            r#"
462fn main() { foo+$0bar;}
463"#,
464            &["bar", "foo+bar"],
465        );
466    }
467
468    #[test]
469    fn test_extend_selection_prefer_lifetimes() {
470        do_check(r#"fn foo<$0'a>() {}"#, &["'a", "<'a>"]);
471        do_check(r#"fn foo<'a$0>() {}"#, &["'a", "<'a>"]);
472    }
473
474    #[test]
475    fn test_extend_selection_select_first_word() {
476        do_check(r#"// foo bar b$0az quxx"#, &["baz", "// foo bar baz quxx"]);
477        do_check(
478            r#"
479impl S {
480fn foo() {
481// hel$0lo world
482}
483}
484"#,
485            &["hello", "// hello world"],
486        );
487    }
488
489    #[test]
490    fn test_extend_selection_string() {
491        do_check(
492            r#"
493fn bar(){}
494
495" fn f$0oo() {"
496"#,
497            &["foo", "\" fn foo() {\""],
498        );
499    }
500
501    #[test]
502    fn test_extend_trait_bounds_list_in_where_clause() {
503        do_check(
504            r#"
505fn foo<R>()
506    where
507        R: req::Request + 'static,
508        R::Params: DeserializeOwned$0 + panic::UnwindSafe + 'static,
509        R::Result: Serialize + 'static,
510"#,
511            &[
512                "DeserializeOwned",
513                "DeserializeOwned + ",
514                "DeserializeOwned + panic::UnwindSafe + 'static",
515                "R::Params: DeserializeOwned + panic::UnwindSafe + 'static",
516                "R::Params: DeserializeOwned + panic::UnwindSafe + 'static,",
517            ],
518        );
519        do_check(r#"fn foo<T>() where T: $0Copy"#, &["Copy"]);
520        do_check(r#"fn foo<T>() where T: $0Copy + Display"#, &["Copy", "Copy + "]);
521        do_check(r#"fn foo<T>() where T: $0Copy +Display"#, &["Copy", "Copy +"]);
522        do_check(r#"fn foo<T>() where T: $0Copy+Display"#, &["Copy", "Copy+"]);
523        do_check(r#"fn foo<T>() where T: Copy + $0Display"#, &["Display", "+ Display"]);
524        do_check(r#"fn foo<T>() where T: Copy + $0Display + Sync"#, &["Display", "Display + "]);
525        do_check(r#"fn foo<T>() where T: Copy +$0Display"#, &["Display", "+Display"]);
526    }
527
528    #[test]
529    fn test_extend_trait_bounds_list_inline() {
530        do_check(r#"fn foo<T: $0Copy>() {}"#, &["Copy"]);
531        do_check(r#"fn foo<T: $0Copy + Display>() {}"#, &["Copy", "Copy + "]);
532        do_check(r#"fn foo<T: $0Copy +Display>() {}"#, &["Copy", "Copy +"]);
533        do_check(r#"fn foo<T: $0Copy+Display>() {}"#, &["Copy", "Copy+"]);
534        do_check(r#"fn foo<T: Copy + $0Display>() {}"#, &["Display", "+ Display"]);
535        do_check(r#"fn foo<T: Copy + $0Display + Sync>() {}"#, &["Display", "Display + "]);
536        do_check(r#"fn foo<T: Copy +$0Display>() {}"#, &["Display", "+Display"]);
537        do_check(
538            r#"fn foo<T: Copy$0 + Display, U: Copy>() {}"#,
539            &[
540                "Copy",
541                "Copy + ",
542                "Copy + Display",
543                "T: Copy + Display",
544                "T: Copy + Display, ",
545                "<T: Copy + Display, U: Copy>",
546            ],
547        );
548    }
549
550    #[test]
551    fn test_extend_selection_on_tuple_in_type() {
552        do_check(
553            r#"fn main() { let _: (krate, $0_crate_def_map, module_id) = (); }"#,
554            &["_crate_def_map", "_crate_def_map, ", "(krate, _crate_def_map, module_id)"],
555        );
556        // white space variations
557        do_check(
558            r#"fn main() { let _: (krate,$0_crate_def_map,module_id) = (); }"#,
559            &["_crate_def_map", "_crate_def_map,", "(krate,_crate_def_map,module_id)"],
560        );
561        do_check(
562            r#"
563fn main() { let _: (
564    krate,
565    _crate$0_def_map,
566    module_id
567) = (); }"#,
568            &[
569                "_crate_def_map",
570                "_crate_def_map,",
571                "(\n    krate,\n    _crate_def_map,\n    module_id\n)",
572            ],
573        );
574    }
575
576    #[test]
577    fn test_extend_selection_on_tuple_in_rvalue() {
578        do_check(
579            r#"fn main() { let var = (krate, _crate_def_map$0, module_id); }"#,
580            &["_crate_def_map", "_crate_def_map, ", "(krate, _crate_def_map, module_id)"],
581        );
582        // white space variations
583        do_check(
584            r#"fn main() { let var = (krate,_crate$0_def_map,module_id); }"#,
585            &["_crate_def_map", "_crate_def_map,", "(krate,_crate_def_map,module_id)"],
586        );
587        do_check(
588            r#"
589fn main() { let var = (
590    krate,
591    _crate_def_map$0,
592    module_id
593); }"#,
594            &[
595                "_crate_def_map",
596                "_crate_def_map,",
597                "(\n    krate,\n    _crate_def_map,\n    module_id\n)",
598            ],
599        );
600    }
601
602    #[test]
603    fn test_extend_selection_on_tuple_pat() {
604        do_check(
605            r#"fn main() { let (krate, _crate_def_map$0, module_id) = var; }"#,
606            &["_crate_def_map", "_crate_def_map, ", "(krate, _crate_def_map, module_id)"],
607        );
608        // white space variations
609        do_check(
610            r#"fn main() { let (krate,_crate$0_def_map,module_id) = var; }"#,
611            &["_crate_def_map", "_crate_def_map,", "(krate,_crate_def_map,module_id)"],
612        );
613        do_check(
614            r#"
615fn main() { let (
616    krate,
617    _crate_def_map$0,
618    module_id
619) = var; }"#,
620            &[
621                "_crate_def_map",
622                "_crate_def_map,",
623                "(\n    krate,\n    _crate_def_map,\n    module_id\n)",
624            ],
625        );
626    }
627
628    #[test]
629    fn extend_selection_inside_macros() {
630        do_check(
631            r#"macro_rules! foo { ($item:item) => {$item} }
632                foo!{fn hello(na$0me:usize){}}"#,
633            &[
634                "name",
635                "name:usize",
636                "(name:usize)",
637                "fn hello(name:usize){}",
638                "{fn hello(name:usize){}}",
639                "foo!{fn hello(name:usize){}}",
640            ],
641        );
642    }
643
644    #[test]
645    fn extend_selection_inside_recur_macros() {
646        do_check(
647            r#" macro_rules! foo2 { ($item:item) => {$item} }
648                macro_rules! foo { ($item:item) => {foo2!($item);} }
649                foo!{fn hello(na$0me:usize){}}"#,
650            &[
651                "name",
652                "name:usize",
653                "(name:usize)",
654                "fn hello(name:usize){}",
655                "{fn hello(name:usize){}}",
656                "foo!{fn hello(name:usize){}}",
657            ],
658        );
659    }
660
661    #[test]
662    fn extend_selection_inside_str_with_wide_char() {
663        // should not panic
664        do_check(
665            r#"fn main() { let x = "═$0═══════"; }"#,
666            &[
667                r#""════════""#,
668                r#"let x = "════════";"#,
669                r#"{ let x = "════════"; }"#,
670                r#"fn main() { let x = "════════"; }"#,
671            ],
672        );
673    }
674}