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 = [COMMENT, STRING, BYTE_STRING, C_STRING];
39    let list_kinds = [
40        RECORD_PAT_FIELD_LIST,
41        MATCH_ARM_LIST,
42        RECORD_FIELD_LIST,
43        TUPLE_FIELD_LIST,
44        RECORD_EXPR_FIELD_LIST,
45        VARIANT_LIST,
46        USE_TREE_LIST,
47        GENERIC_PARAM_LIST,
48        GENERIC_ARG_LIST,
49        TYPE_BOUND_LIST,
50        PARAM_LIST,
51        ARG_LIST,
52        ARRAY_EXPR,
53        TUPLE_EXPR,
54        TUPLE_TYPE,
55        TUPLE_PAT,
56        WHERE_CLAUSE,
57    ];
58
59    if range.is_empty() {
60        let offset = range.start();
61        let mut leaves = root.token_at_offset(offset);
62        if leaves.clone().all(|it| it.kind() == WHITESPACE) {
63            return Some(extend_ws(root, leaves.next()?, offset));
64        }
65        let leaf_range = match leaves {
66            TokenAtOffset::None => return None,
67            TokenAtOffset::Single(l) => {
68                if string_kinds.contains(&l.kind()) {
69                    extend_single_word_in_comment_or_string(&l, offset)
70                        .unwrap_or_else(|| l.text_range())
71                } else {
72                    l.text_range()
73                }
74            }
75            TokenAtOffset::Between(l, r) => pick_best(l, r).text_range(),
76        };
77        return Some(leaf_range);
78    };
79    let node = match root.covering_element(range) {
80        NodeOrToken::Token(token) => {
81            if token.text_range() != range {
82                return Some(token.text_range());
83            }
84            if let Some(comment) = ast::Comment::cast(token.clone()) {
85                if let Some(range) = extend_comments(comment) {
86                    return Some(range);
87                }
88            }
89            token.parent()?
90        }
91        NodeOrToken::Node(node) => node,
92    };
93
94    // if we are in single token_tree, we maybe live in macro or attr
95    if node.kind() == TOKEN_TREE {
96        if let Some(macro_call) = node.ancestors().find_map(ast::MacroCall::cast) {
97            if let Some(range) = extend_tokens_from_range(sema, macro_call, range) {
98                return Some(range);
99            }
100        }
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        if let Some(range) = extend_list_item(&node) {
111            return Some(range);
112        }
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') && !ws_suffix.contains('\n') {
225        if let Some(node) = ws.next_sibling_or_token() {
226            let start = match ws_prefix.rfind('\n') {
227                Some(idx) => ws.text_range().start() + TextSize::from((idx + 1) as u32),
228                None => node.text_range().start(),
229            };
230            let end = if root.text().char_at(node.text_range().end()) == Some('\n') {
231                node.text_range().end() + TextSize::of('\n')
232            } else {
233                node.text_range().end()
234            };
235            return TextRange::new(start, end);
236        }
237    }
238    ws.text_range()
239}
240
241fn pick_best(l: SyntaxToken, r: SyntaxToken) -> SyntaxToken {
242    return if priority(&r) > priority(&l) { r } else { l };
243    fn priority(n: &SyntaxToken) -> usize {
244        match n.kind() {
245            WHITESPACE => 0,
246            IDENT | T![self] | T![super] | T![crate] | T![Self] | LIFETIME_IDENT => 2,
247            _ => 1,
248        }
249    }
250}
251
252/// Extend list item selection to include nearby delimiter and whitespace.
253fn extend_list_item(node: &SyntaxNode) -> Option<TextRange> {
254    fn is_single_line_ws(node: &SyntaxToken) -> bool {
255        node.kind() == WHITESPACE && !node.text().contains('\n')
256    }
257
258    fn nearby_delimiter(
259        delimiter_kind: SyntaxKind,
260        node: &SyntaxNode,
261        dir: Direction,
262    ) -> Option<SyntaxToken> {
263        node.siblings_with_tokens(dir)
264            .skip(1)
265            .find(|node| match node {
266                NodeOrToken::Node(_) => true,
267                NodeOrToken::Token(it) => !is_single_line_ws(it),
268            })
269            .and_then(|it| it.into_token())
270            .filter(|node| node.kind() == delimiter_kind)
271    }
272
273    let delimiter = match node.kind() {
274        TYPE_BOUND => T![+],
275        _ => T![,],
276    };
277
278    if let Some(delimiter_node) = nearby_delimiter(delimiter, node, Direction::Next) {
279        // Include any following whitespace when delimiter is after list item.
280        let final_node = delimiter_node
281            .next_sibling_or_token()
282            .and_then(|it| it.into_token())
283            .filter(is_single_line_ws)
284            .unwrap_or(delimiter_node);
285
286        return Some(TextRange::new(node.text_range().start(), final_node.text_range().end()));
287    }
288    if let Some(delimiter_node) = nearby_delimiter(delimiter, node, Direction::Prev) {
289        return Some(TextRange::new(delimiter_node.text_range().start(), node.text_range().end()));
290    }
291
292    None
293}
294
295fn extend_comments(comment: ast::Comment) -> Option<TextRange> {
296    let prev = adj_comments(&comment, Direction::Prev);
297    let next = adj_comments(&comment, Direction::Next);
298    if prev != next {
299        Some(TextRange::new(prev.syntax().text_range().start(), next.syntax().text_range().end()))
300    } else {
301        None
302    }
303}
304
305fn adj_comments(comment: &ast::Comment, dir: Direction) -> ast::Comment {
306    let mut res = comment.clone();
307    for element in comment.syntax().siblings_with_tokens(dir) {
308        let token = match element.as_token() {
309            None => break,
310            Some(token) => token,
311        };
312        if let Some(c) = ast::Comment::cast(token.clone()) {
313            res = c
314        } else if token.kind() != WHITESPACE || token.text().contains("\n\n") {
315            break;
316        }
317    }
318    res
319}
320
321#[cfg(test)]
322mod tests {
323    use crate::fixture;
324
325    use super::*;
326
327    fn do_check(before: &str, afters: &[&str]) {
328        let (analysis, position) = fixture::position(before);
329        let before = analysis.file_text(position.file_id).unwrap();
330        let range = TextRange::empty(position.offset);
331        let mut frange = FileRange { file_id: position.file_id, range };
332
333        for &after in afters {
334            frange.range = analysis.extend_selection(frange).unwrap();
335            let actual = &before[frange.range];
336            assert_eq!(after, actual);
337        }
338    }
339
340    #[test]
341    fn test_extend_selection_arith() {
342        do_check(r#"fn foo() { $01 + 1 }"#, &["1", "1 + 1", "{ 1 + 1 }"]);
343    }
344
345    #[test]
346    fn test_extend_selection_list() {
347        do_check(r#"fn foo($0x: i32) {}"#, &["x", "x: i32"]);
348        do_check(r#"fn foo($0x: i32, y: i32) {}"#, &["x", "x: i32", "x: i32, "]);
349        do_check(r#"fn foo($0x: i32,y: i32) {}"#, &["x", "x: i32", "x: i32,", "(x: i32,y: i32)"]);
350        do_check(r#"fn foo(x: i32, $0y: i32) {}"#, &["y", "y: 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
354        do_check(r#"const FOO: [usize; 2] = [ 22$0 , 33];"#, &["22", "22 , "]);
355        do_check(r#"const FOO: [usize; 2] = [ 22 , 33$0];"#, &["33", ", 33"]);
356        do_check(r#"const FOO: [usize; 2] = [ 22 , 33$0 ,];"#, &["33", "33 ,", "[ 22 , 33 ,]"]);
357
358        do_check(r#"fn main() { (1, 2$0) }"#, &["2", ", 2", "(1, 2)"]);
359
360        do_check(
361            r#"
362const FOO: [usize; 2] = [
363    22,
364    $033,
365]"#,
366            &["33", "33,"],
367        );
368
369        do_check(
370            r#"
371const FOO: [usize; 2] = [
372    22
373    , 33$0,
374]"#,
375            &["33", "33,"],
376        );
377    }
378
379    #[test]
380    fn test_extend_selection_start_of_the_line() {
381        do_check(
382            r#"
383impl S {
384$0    fn foo() {
385
386    }
387}"#,
388            &["    fn foo() {\n\n    }\n"],
389        );
390    }
391
392    #[test]
393    fn test_extend_selection_doc_comments() {
394        do_check(
395            r#"
396struct A;
397
398/// bla
399/// bla
400struct B {
401    $0
402}
403            "#,
404            &["\n    \n", "{\n    \n}", "/// bla\n/// bla\nstruct B {\n    \n}"],
405        )
406    }
407
408    #[test]
409    fn test_extend_selection_comments() {
410        do_check(
411            r#"
412fn bar(){}
413
414// fn foo() {
415// 1 + $01
416// }
417
418// fn foo(){}
419    "#,
420            &["1", "// 1 + 1", "// fn foo() {\n// 1 + 1\n// }"],
421        );
422
423        do_check(
424            r#"
425// #[derive(Debug, Clone, Copy, PartialEq, Eq)]
426// pub enum Direction {
427//  $0   Next,
428//     Prev
429// }
430"#,
431            &[
432                "//     Next,",
433                "// #[derive(Debug, Clone, Copy, PartialEq, Eq)]\n// pub enum Direction {\n//     Next,\n//     Prev\n// }",
434            ],
435        );
436
437        do_check(
438            r#"
439/*
440foo
441_bar1$0*/
442"#,
443            &["_bar1", "/*\nfoo\n_bar1*/"],
444        );
445
446        do_check(r#"//!$0foo_2 bar"#, &["foo_2", "//!foo_2 bar"]);
447
448        do_check(r#"/$0/foo bar"#, &["//foo bar"]);
449    }
450
451    #[test]
452    fn test_extend_selection_prefer_idents() {
453        do_check(
454            r#"
455fn main() { foo$0+bar;}
456"#,
457            &["foo", "foo+bar"],
458        );
459        do_check(
460            r#"
461fn main() { foo+$0bar;}
462"#,
463            &["bar", "foo+bar"],
464        );
465    }
466
467    #[test]
468    fn test_extend_selection_prefer_lifetimes() {
469        do_check(r#"fn foo<$0'a>() {}"#, &["'a", "<'a>"]);
470        do_check(r#"fn foo<'a$0>() {}"#, &["'a", "<'a>"]);
471    }
472
473    #[test]
474    fn test_extend_selection_select_first_word() {
475        do_check(r#"// foo bar b$0az quxx"#, &["baz", "// foo bar baz quxx"]);
476        do_check(
477            r#"
478impl S {
479fn foo() {
480// hel$0lo world
481}
482}
483"#,
484            &["hello", "// hello world"],
485        );
486    }
487
488    #[test]
489    fn test_extend_selection_string() {
490        do_check(
491            r#"
492fn bar(){}
493
494" fn f$0oo() {"
495"#,
496            &["foo", "\" fn foo() {\""],
497        );
498    }
499
500    #[test]
501    fn test_extend_trait_bounds_list_in_where_clause() {
502        do_check(
503            r#"
504fn foo<R>()
505    where
506        R: req::Request + 'static,
507        R::Params: DeserializeOwned$0 + panic::UnwindSafe + 'static,
508        R::Result: Serialize + 'static,
509"#,
510            &[
511                "DeserializeOwned",
512                "DeserializeOwned + ",
513                "DeserializeOwned + panic::UnwindSafe + 'static",
514                "R::Params: DeserializeOwned + panic::UnwindSafe + 'static",
515                "R::Params: DeserializeOwned + panic::UnwindSafe + 'static,",
516            ],
517        );
518        do_check(r#"fn foo<T>() where T: $0Copy"#, &["Copy"]);
519        do_check(r#"fn foo<T>() where T: $0Copy + Display"#, &["Copy", "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: Copy + $0Display"#, &["Display", "+ Display"]);
523        do_check(r#"fn foo<T>() where T: Copy + $0Display + Sync"#, &["Display", "Display + "]);
524        do_check(r#"fn foo<T>() where T: Copy +$0Display"#, &["Display", "+Display"]);
525    }
526
527    #[test]
528    fn test_extend_trait_bounds_list_inline() {
529        do_check(r#"fn foo<T: $0Copy>() {}"#, &["Copy"]);
530        do_check(r#"fn foo<T: $0Copy + Display>() {}"#, &["Copy", "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: Copy + $0Display>() {}"#, &["Display", "+ Display"]);
534        do_check(r#"fn foo<T: Copy + $0Display + Sync>() {}"#, &["Display", "Display + "]);
535        do_check(r#"fn foo<T: Copy +$0Display>() {}"#, &["Display", "+Display"]);
536        do_check(
537            r#"fn foo<T: Copy$0 + Display, U: Copy>() {}"#,
538            &[
539                "Copy",
540                "Copy + ",
541                "Copy + Display",
542                "T: Copy + Display",
543                "T: Copy + Display, ",
544                "<T: Copy + Display, U: Copy>",
545            ],
546        );
547    }
548
549    #[test]
550    fn test_extend_selection_on_tuple_in_type() {
551        do_check(
552            r#"fn main() { let _: (krate, $0_crate_def_map, module_id) = (); }"#,
553            &["_crate_def_map", "_crate_def_map, ", "(krate, _crate_def_map, module_id)"],
554        );
555        // white space variations
556        do_check(
557            r#"fn main() { let _: (krate,$0_crate_def_map,module_id) = (); }"#,
558            &["_crate_def_map", "_crate_def_map,", "(krate,_crate_def_map,module_id)"],
559        );
560        do_check(
561            r#"
562fn main() { let _: (
563    krate,
564    _crate$0_def_map,
565    module_id
566) = (); }"#,
567            &[
568                "_crate_def_map",
569                "_crate_def_map,",
570                "(\n    krate,\n    _crate_def_map,\n    module_id\n)",
571            ],
572        );
573    }
574
575    #[test]
576    fn test_extend_selection_on_tuple_in_rvalue() {
577        do_check(
578            r#"fn main() { let var = (krate, _crate_def_map$0, module_id); }"#,
579            &["_crate_def_map", "_crate_def_map, ", "(krate, _crate_def_map, module_id)"],
580        );
581        // white space variations
582        do_check(
583            r#"fn main() { let var = (krate,_crate$0_def_map,module_id); }"#,
584            &["_crate_def_map", "_crate_def_map,", "(krate,_crate_def_map,module_id)"],
585        );
586        do_check(
587            r#"
588fn main() { let var = (
589    krate,
590    _crate_def_map$0,
591    module_id
592); }"#,
593            &[
594                "_crate_def_map",
595                "_crate_def_map,",
596                "(\n    krate,\n    _crate_def_map,\n    module_id\n)",
597            ],
598        );
599    }
600
601    #[test]
602    fn test_extend_selection_on_tuple_pat() {
603        do_check(
604            r#"fn main() { let (krate, _crate_def_map$0, module_id) = var; }"#,
605            &["_crate_def_map", "_crate_def_map, ", "(krate, _crate_def_map, module_id)"],
606        );
607        // white space variations
608        do_check(
609            r#"fn main() { let (krate,_crate$0_def_map,module_id) = var; }"#,
610            &["_crate_def_map", "_crate_def_map,", "(krate,_crate_def_map,module_id)"],
611        );
612        do_check(
613            r#"
614fn main() { let (
615    krate,
616    _crate_def_map$0,
617    module_id
618) = var; }"#,
619            &[
620                "_crate_def_map",
621                "_crate_def_map,",
622                "(\n    krate,\n    _crate_def_map,\n    module_id\n)",
623            ],
624        );
625    }
626
627    #[test]
628    fn extend_selection_inside_macros() {
629        do_check(
630            r#"macro_rules! foo { ($item:item) => {$item} }
631                foo!{fn hello(na$0me:usize){}}"#,
632            &[
633                "name",
634                "name:usize",
635                "(name:usize)",
636                "fn hello(name:usize){}",
637                "{fn hello(name:usize){}}",
638                "foo!{fn hello(name:usize){}}",
639            ],
640        );
641    }
642
643    #[test]
644    fn extend_selection_inside_recur_macros() {
645        do_check(
646            r#" macro_rules! foo2 { ($item:item) => {$item} }
647                macro_rules! foo { ($item:item) => {foo2!($item);} }
648                foo!{fn hello(na$0me:usize){}}"#,
649            &[
650                "name",
651                "name:usize",
652                "(name:usize)",
653                "fn hello(name:usize){}",
654                "{fn hello(name:usize){}}",
655                "foo!{fn hello(name:usize){}}",
656            ],
657        );
658    }
659
660    #[test]
661    fn extend_selection_inside_str_with_wide_char() {
662        // should not panic
663        do_check(
664            r#"fn main() { let x = "═$0═══════"; }"#,
665            &[
666                r#""════════""#,
667                r#"let x = "════════";"#,
668                r#"{ let x = "════════"; }"#,
669                r#"fn main() { let x = "════════"; }"#,
670            ],
671        );
672    }
673}