mirror of
https://github.com/rust-lang/rust-analyzer
synced 2025-01-11 12:48:47 +00:00
move extend selection from ra_ide_api_light to ra_ide_api
This commit is contained in:
parent
3eb56f7a6a
commit
b931a472c4
6 changed files with 403 additions and 401 deletions
1
Cargo.lock
generated
1
Cargo.lock
generated
|
@ -945,6 +945,7 @@ dependencies = [
|
|||
"ra_batch 0.1.0",
|
||||
"ra_db 0.1.0",
|
||||
"ra_hir 0.1.0",
|
||||
"ra_ide_api 0.1.0",
|
||||
"ra_ide_api_light 0.1.0",
|
||||
"ra_syntax 0.1.0",
|
||||
"tools 0.1.0",
|
||||
|
|
|
@ -13,6 +13,7 @@ flexi_logger = "0.11.0"
|
|||
indicatif = "0.11.0"
|
||||
|
||||
ra_syntax = { path = "../ra_syntax" }
|
||||
ra_ide_api = { path = "../ra_ide_api" }
|
||||
ra_ide_api_light = { path = "../ra_ide_api_light" }
|
||||
tools = { path = "../tools" }
|
||||
ra_batch = { path = "../ra_batch" }
|
||||
|
|
|
@ -4,7 +4,8 @@ use std::{fs, io::Read, path::Path, time::Instant};
|
|||
|
||||
use clap::{App, Arg, SubCommand};
|
||||
use join_to_string::join;
|
||||
use ra_ide_api_light::{extend_selection, file_structure};
|
||||
use ra_ide_api::{Analysis, FileRange};
|
||||
use ra_ide_api_light::file_structure;
|
||||
use ra_syntax::{SourceFile, TextRange, TreeArc, AstNode};
|
||||
use tools::collect_tests;
|
||||
use flexi_logger::Logger;
|
||||
|
@ -59,8 +60,8 @@ fn main() -> Result<()> {
|
|||
("extend-selection", Some(matches)) => {
|
||||
let start: u32 = matches.value_of("start").unwrap().parse()?;
|
||||
let end: u32 = matches.value_of("end").unwrap().parse()?;
|
||||
let file = file()?;
|
||||
let sels = selections(&file, start, end);
|
||||
let text = read_stdin()?;
|
||||
let sels = selections(text, start, end);
|
||||
println!("{}", sels)
|
||||
}
|
||||
("analysis-stats", Some(matches)) => {
|
||||
|
@ -98,12 +99,17 @@ fn render_test(file: &Path, line: usize) -> Result<(String, String)> {
|
|||
Ok((test.text, tree))
|
||||
}
|
||||
|
||||
fn selections(file: &SourceFile, start: u32, end: u32) -> String {
|
||||
fn selections(text: String, start: u32, end: u32) -> String {
|
||||
let (analysis, file_id) = Analysis::from_single_file(text);
|
||||
let mut ranges = Vec::new();
|
||||
let mut cur = Some(TextRange::from_to((start - 1).into(), (end - 1).into()));
|
||||
while let Some(r) = cur {
|
||||
ranges.push(r);
|
||||
cur = extend_selection(file.syntax(), r);
|
||||
let mut range = TextRange::from_to((start - 1).into(), (end - 1).into());
|
||||
loop {
|
||||
ranges.push(range);
|
||||
let next = analysis.extend_selection(FileRange { file_id, range }).unwrap();
|
||||
if range == next {
|
||||
break;
|
||||
}
|
||||
range = next;
|
||||
}
|
||||
let ranges = ranges
|
||||
.iter()
|
||||
|
|
|
@ -1,13 +1,378 @@
|
|||
use ra_db::SourceDatabase;
|
||||
use ra_syntax::AstNode;
|
||||
|
||||
use crate::{
|
||||
TextRange, FileRange,
|
||||
db::RootDatabase,
|
||||
use ra_syntax::{
|
||||
Direction, SyntaxNode, TextRange, TextUnit, AstNode,
|
||||
algo::{find_covering_node, find_leaf_at_offset, LeafAtOffset},
|
||||
SyntaxKind::*,
|
||||
};
|
||||
|
||||
use crate::{FileRange, db::RootDatabase};
|
||||
|
||||
// FIXME: restore macro support
|
||||
pub(crate) fn extend_selection(db: &RootDatabase, frange: FileRange) -> TextRange {
|
||||
let source_file = db.parse(frange.file_id);
|
||||
ra_ide_api_light::extend_selection(source_file.syntax(), frange.range).unwrap_or(frange.range)
|
||||
try_extend_selection(source_file.syntax(), frange.range).unwrap_or(frange.range)
|
||||
}
|
||||
|
||||
fn try_extend_selection(root: &SyntaxNode, range: TextRange) -> Option<TextRange> {
|
||||
let string_kinds = [COMMENT, STRING, RAW_STRING, BYTE_STRING, RAW_BYTE_STRING];
|
||||
let list_kinds = [
|
||||
FIELD_PAT_LIST,
|
||||
MATCH_ARM_LIST,
|
||||
NAMED_FIELD_DEF_LIST,
|
||||
POS_FIELD_DEF_LIST,
|
||||
NAMED_FIELD_LIST,
|
||||
ENUM_VARIANT_LIST,
|
||||
USE_TREE_LIST,
|
||||
TYPE_PARAM_LIST,
|
||||
TYPE_ARG_LIST,
|
||||
PARAM_LIST,
|
||||
ARG_LIST,
|
||||
ARRAY_EXPR,
|
||||
];
|
||||
|
||||
if range.is_empty() {
|
||||
let offset = range.start();
|
||||
let mut leaves = find_leaf_at_offset(root, offset);
|
||||
if leaves.clone().all(|it| it.kind() == WHITESPACE) {
|
||||
return Some(extend_ws(root, leaves.next()?, offset));
|
||||
}
|
||||
let leaf_range = match leaves {
|
||||
LeafAtOffset::None => return None,
|
||||
LeafAtOffset::Single(l) => {
|
||||
if string_kinds.contains(&l.kind()) {
|
||||
extend_single_word_in_comment_or_string(l, offset).unwrap_or_else(|| l.range())
|
||||
} else {
|
||||
l.range()
|
||||
}
|
||||
}
|
||||
LeafAtOffset::Between(l, r) => pick_best(l, r).range(),
|
||||
};
|
||||
return Some(leaf_range);
|
||||
};
|
||||
let node = find_covering_node(root, range);
|
||||
|
||||
// Using shallowest node with same range allows us to traverse siblings.
|
||||
let node = node.ancestors().take_while(|n| n.range() == node.range()).last().unwrap();
|
||||
|
||||
if range == node.range() {
|
||||
if string_kinds.contains(&node.kind()) {
|
||||
if let Some(range) = extend_comments(node) {
|
||||
return Some(range);
|
||||
}
|
||||
}
|
||||
|
||||
if node.parent().map(|n| list_kinds.contains(&n.kind())) == Some(true) {
|
||||
if let Some(range) = extend_list_item(node) {
|
||||
return Some(range);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
match node.ancestors().skip_while(|n| n.range() == range).next() {
|
||||
None => None,
|
||||
Some(parent) => Some(parent.range()),
|
||||
}
|
||||
}
|
||||
|
||||
fn extend_single_word_in_comment_or_string(
|
||||
leaf: &SyntaxNode,
|
||||
offset: TextUnit,
|
||||
) -> Option<TextRange> {
|
||||
let text: &str = leaf.leaf_text()?;
|
||||
let cursor_position: u32 = (offset - leaf.range().start()).into();
|
||||
|
||||
let (before, after) = text.split_at(cursor_position as usize);
|
||||
|
||||
fn non_word_char(c: char) -> bool {
|
||||
!(c.is_alphanumeric() || c == '_')
|
||||
}
|
||||
|
||||
let start_idx = before.rfind(non_word_char)? as u32;
|
||||
let end_idx = after.find(non_word_char).unwrap_or(after.len()) as u32;
|
||||
|
||||
let from: TextUnit = (start_idx + 1).into();
|
||||
let to: TextUnit = (cursor_position + end_idx).into();
|
||||
|
||||
let range = TextRange::from_to(from, to);
|
||||
if range.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(range + leaf.range().start())
|
||||
}
|
||||
}
|
||||
|
||||
fn extend_ws(root: &SyntaxNode, ws: &SyntaxNode, offset: TextUnit) -> TextRange {
|
||||
let ws_text = ws.leaf_text().unwrap();
|
||||
let suffix = TextRange::from_to(offset, ws.range().end()) - ws.range().start();
|
||||
let prefix = TextRange::from_to(ws.range().start(), offset) - ws.range().start();
|
||||
let ws_suffix = &ws_text.as_str()[suffix];
|
||||
let ws_prefix = &ws_text.as_str()[prefix];
|
||||
if ws_text.contains('\n') && !ws_suffix.contains('\n') {
|
||||
if let Some(node) = ws.next_sibling() {
|
||||
let start = match ws_prefix.rfind('\n') {
|
||||
Some(idx) => ws.range().start() + TextUnit::from((idx + 1) as u32),
|
||||
None => node.range().start(),
|
||||
};
|
||||
let end = if root.text().char_at(node.range().end()) == Some('\n') {
|
||||
node.range().end() + TextUnit::of_char('\n')
|
||||
} else {
|
||||
node.range().end()
|
||||
};
|
||||
return TextRange::from_to(start, end);
|
||||
}
|
||||
}
|
||||
ws.range()
|
||||
}
|
||||
|
||||
fn pick_best<'a>(l: &'a SyntaxNode, r: &'a SyntaxNode) -> &'a SyntaxNode {
|
||||
return if priority(r) > priority(l) { r } else { l };
|
||||
fn priority(n: &SyntaxNode) -> usize {
|
||||
match n.kind() {
|
||||
WHITESPACE => 0,
|
||||
IDENT | SELF_KW | SUPER_KW | CRATE_KW | LIFETIME => 2,
|
||||
_ => 1,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Extend list item selection to include nearby comma and whitespace.
|
||||
fn extend_list_item(node: &SyntaxNode) -> Option<TextRange> {
|
||||
fn is_single_line_ws(node: &SyntaxNode) -> bool {
|
||||
node.kind() == WHITESPACE && !node.leaf_text().unwrap().contains('\n')
|
||||
}
|
||||
|
||||
fn nearby_comma(node: &SyntaxNode, dir: Direction) -> Option<&SyntaxNode> {
|
||||
node.siblings(dir)
|
||||
.skip(1)
|
||||
.skip_while(|node| is_single_line_ws(node))
|
||||
.next()
|
||||
.filter(|node| node.kind() == COMMA)
|
||||
}
|
||||
|
||||
if let Some(comma_node) = nearby_comma(node, Direction::Prev) {
|
||||
return Some(TextRange::from_to(comma_node.range().start(), node.range().end()));
|
||||
}
|
||||
|
||||
if let Some(comma_node) = nearby_comma(node, Direction::Next) {
|
||||
// Include any following whitespace when comma if after list item.
|
||||
let final_node = comma_node
|
||||
.siblings(Direction::Next)
|
||||
.skip(1)
|
||||
.next()
|
||||
.filter(|node| is_single_line_ws(node))
|
||||
.unwrap_or(comma_node);
|
||||
|
||||
return Some(TextRange::from_to(node.range().start(), final_node.range().end()));
|
||||
}
|
||||
|
||||
return None;
|
||||
}
|
||||
|
||||
fn extend_comments(node: &SyntaxNode) -> Option<TextRange> {
|
||||
let prev = adj_comments(node, Direction::Prev);
|
||||
let next = adj_comments(node, Direction::Next);
|
||||
if prev != next {
|
||||
Some(TextRange::from_to(prev.range().start(), next.range().end()))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn adj_comments(node: &SyntaxNode, dir: Direction) -> &SyntaxNode {
|
||||
let mut res = node;
|
||||
for node in node.siblings(dir) {
|
||||
match node.kind() {
|
||||
COMMENT => res = node,
|
||||
WHITESPACE if !node.leaf_text().unwrap().as_str().contains("\n\n") => (),
|
||||
_ => break,
|
||||
}
|
||||
}
|
||||
res
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use ra_syntax::{SourceFile, AstNode};
|
||||
use test_utils::extract_offset;
|
||||
|
||||
use super::*;
|
||||
|
||||
fn do_check(before: &str, afters: &[&str]) {
|
||||
let (cursor, before) = extract_offset(before);
|
||||
let file = SourceFile::parse(&before);
|
||||
let mut range = TextRange::offset_len(cursor, 0.into());
|
||||
for &after in afters {
|
||||
range = try_extend_selection(file.syntax(), range).unwrap();
|
||||
let actual = &before[range];
|
||||
assert_eq!(after, actual);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_extend_selection_arith() {
|
||||
do_check(r#"fn foo() { <|>1 + 1 }"#, &["1", "1 + 1", "{ 1 + 1 }"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_extend_selection_list() {
|
||||
do_check(r#"fn foo(<|>x: i32) {}"#, &["x", "x: i32"]);
|
||||
do_check(r#"fn foo(<|>x: i32, y: i32) {}"#, &["x", "x: i32", "x: i32, "]);
|
||||
do_check(r#"fn foo(<|>x: i32,y: i32) {}"#, &["x", "x: i32", "x: i32,"]);
|
||||
do_check(r#"fn foo(x: i32, <|>y: i32) {}"#, &["y", "y: i32", ", y: i32"]);
|
||||
do_check(r#"fn foo(x: i32, <|>y: i32, ) {}"#, &["y", "y: i32", ", y: i32"]);
|
||||
do_check(r#"fn foo(x: i32,<|>y: i32) {}"#, &["y", "y: i32", ",y: i32"]);
|
||||
|
||||
do_check(r#"const FOO: [usize; 2] = [ 22<|> , 33];"#, &["22", "22 , "]);
|
||||
do_check(r#"const FOO: [usize; 2] = [ 22 , 33<|>];"#, &["33", ", 33"]);
|
||||
do_check(r#"const FOO: [usize; 2] = [ 22 , 33<|> ,];"#, &["33", ", 33"]);
|
||||
|
||||
do_check(
|
||||
r#"
|
||||
const FOO: [usize; 2] = [
|
||||
22,
|
||||
<|>33,
|
||||
]"#,
|
||||
&["33", "33,"],
|
||||
);
|
||||
|
||||
do_check(
|
||||
r#"
|
||||
const FOO: [usize; 2] = [
|
||||
22
|
||||
, 33<|>,
|
||||
]"#,
|
||||
&["33", ", 33"],
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_extend_selection_start_of_the_line() {
|
||||
do_check(
|
||||
r#"
|
||||
impl S {
|
||||
<|> fn foo() {
|
||||
|
||||
}
|
||||
}"#,
|
||||
&[" fn foo() {\n\n }\n"],
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_extend_selection_doc_comments() {
|
||||
do_check(
|
||||
r#"
|
||||
struct A;
|
||||
|
||||
/// bla
|
||||
/// bla
|
||||
struct B {
|
||||
<|>
|
||||
}
|
||||
"#,
|
||||
&["\n \n", "{\n \n}", "/// bla\n/// bla\nstruct B {\n \n}"],
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_extend_selection_comments() {
|
||||
do_check(
|
||||
r#"
|
||||
fn bar(){}
|
||||
|
||||
// fn foo() {
|
||||
// 1 + <|>1
|
||||
// }
|
||||
|
||||
// fn foo(){}
|
||||
"#,
|
||||
&["1", "// 1 + 1", "// fn foo() {\n// 1 + 1\n// }"],
|
||||
);
|
||||
|
||||
do_check(
|
||||
r#"
|
||||
// #[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
// pub enum Direction {
|
||||
// <|> Next,
|
||||
// Prev
|
||||
// }
|
||||
"#,
|
||||
&[
|
||||
"// Next,",
|
||||
"// #[derive(Debug, Clone, Copy, PartialEq, Eq)]\n// pub enum Direction {\n// Next,\n// Prev\n// }",
|
||||
],
|
||||
);
|
||||
|
||||
do_check(
|
||||
r#"
|
||||
/*
|
||||
foo
|
||||
_bar1<|>*/
|
||||
"#,
|
||||
&["_bar1", "/*\nfoo\n_bar1*/"],
|
||||
);
|
||||
|
||||
do_check(
|
||||
r#"
|
||||
//!<|>foo_2 bar
|
||||
"#,
|
||||
&["foo_2", "//!foo_2 bar"],
|
||||
);
|
||||
|
||||
do_check(
|
||||
r#"
|
||||
/<|>/foo bar
|
||||
"#,
|
||||
&["//foo bar"],
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_extend_selection_prefer_idents() {
|
||||
do_check(
|
||||
r#"
|
||||
fn main() { foo<|>+bar;}
|
||||
"#,
|
||||
&["foo", "foo+bar"],
|
||||
);
|
||||
do_check(
|
||||
r#"
|
||||
fn main() { foo+<|>bar;}
|
||||
"#,
|
||||
&["bar", "foo+bar"],
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_extend_selection_prefer_lifetimes() {
|
||||
do_check(r#"fn foo<<|>'a>() {}"#, &["'a", "<'a>"]);
|
||||
do_check(r#"fn foo<'a<|>>() {}"#, &["'a", "<'a>"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_extend_selection_select_first_word() {
|
||||
do_check(r#"// foo bar b<|>az quxx"#, &["baz", "// foo bar baz quxx"]);
|
||||
do_check(
|
||||
r#"
|
||||
impl S {
|
||||
fn foo() {
|
||||
// hel<|>lo world
|
||||
}
|
||||
}
|
||||
"#,
|
||||
&["hello", "// hello world"],
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_extend_selection_string() {
|
||||
do_check(
|
||||
r#"
|
||||
fn bar(){}
|
||||
|
||||
" fn f<|>oo() {"
|
||||
"#,
|
||||
&["foo", "\" fn foo() {\""],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,369 +0,0 @@
|
|||
use ra_syntax::{
|
||||
Direction, SyntaxNode, TextRange, TextUnit,
|
||||
algo::{find_covering_node, find_leaf_at_offset, LeafAtOffset},
|
||||
SyntaxKind::*,
|
||||
};
|
||||
|
||||
pub fn extend_selection(root: &SyntaxNode, range: TextRange) -> Option<TextRange> {
|
||||
let string_kinds = [COMMENT, STRING, RAW_STRING, BYTE_STRING, RAW_BYTE_STRING];
|
||||
let list_kinds = [
|
||||
FIELD_PAT_LIST,
|
||||
MATCH_ARM_LIST,
|
||||
NAMED_FIELD_DEF_LIST,
|
||||
POS_FIELD_DEF_LIST,
|
||||
NAMED_FIELD_LIST,
|
||||
ENUM_VARIANT_LIST,
|
||||
USE_TREE_LIST,
|
||||
TYPE_PARAM_LIST,
|
||||
TYPE_ARG_LIST,
|
||||
PARAM_LIST,
|
||||
ARG_LIST,
|
||||
ARRAY_EXPR,
|
||||
];
|
||||
|
||||
if range.is_empty() {
|
||||
let offset = range.start();
|
||||
let mut leaves = find_leaf_at_offset(root, offset);
|
||||
if leaves.clone().all(|it| it.kind() == WHITESPACE) {
|
||||
return Some(extend_ws(root, leaves.next()?, offset));
|
||||
}
|
||||
let leaf_range = match leaves {
|
||||
LeafAtOffset::None => return None,
|
||||
LeafAtOffset::Single(l) => {
|
||||
if string_kinds.contains(&l.kind()) {
|
||||
extend_single_word_in_comment_or_string(l, offset).unwrap_or_else(|| l.range())
|
||||
} else {
|
||||
l.range()
|
||||
}
|
||||
}
|
||||
LeafAtOffset::Between(l, r) => pick_best(l, r).range(),
|
||||
};
|
||||
return Some(leaf_range);
|
||||
};
|
||||
let node = find_covering_node(root, range);
|
||||
|
||||
// Using shallowest node with same range allows us to traverse siblings.
|
||||
let node = node.ancestors().take_while(|n| n.range() == node.range()).last().unwrap();
|
||||
|
||||
if range == node.range() {
|
||||
if string_kinds.contains(&node.kind()) {
|
||||
if let Some(range) = extend_comments(node) {
|
||||
return Some(range);
|
||||
}
|
||||
}
|
||||
|
||||
if node.parent().map(|n| list_kinds.contains(&n.kind())) == Some(true) {
|
||||
if let Some(range) = extend_list_item(node) {
|
||||
return Some(range);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
match node.ancestors().skip_while(|n| n.range() == range).next() {
|
||||
None => None,
|
||||
Some(parent) => Some(parent.range()),
|
||||
}
|
||||
}
|
||||
|
||||
fn extend_single_word_in_comment_or_string(
|
||||
leaf: &SyntaxNode,
|
||||
offset: TextUnit,
|
||||
) -> Option<TextRange> {
|
||||
let text: &str = leaf.leaf_text()?;
|
||||
let cursor_position: u32 = (offset - leaf.range().start()).into();
|
||||
|
||||
let (before, after) = text.split_at(cursor_position as usize);
|
||||
|
||||
fn non_word_char(c: char) -> bool {
|
||||
!(c.is_alphanumeric() || c == '_')
|
||||
}
|
||||
|
||||
let start_idx = before.rfind(non_word_char)? as u32;
|
||||
let end_idx = after.find(non_word_char).unwrap_or(after.len()) as u32;
|
||||
|
||||
let from: TextUnit = (start_idx + 1).into();
|
||||
let to: TextUnit = (cursor_position + end_idx).into();
|
||||
|
||||
let range = TextRange::from_to(from, to);
|
||||
if range.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(range + leaf.range().start())
|
||||
}
|
||||
}
|
||||
|
||||
fn extend_ws(root: &SyntaxNode, ws: &SyntaxNode, offset: TextUnit) -> TextRange {
|
||||
let ws_text = ws.leaf_text().unwrap();
|
||||
let suffix = TextRange::from_to(offset, ws.range().end()) - ws.range().start();
|
||||
let prefix = TextRange::from_to(ws.range().start(), offset) - ws.range().start();
|
||||
let ws_suffix = &ws_text.as_str()[suffix];
|
||||
let ws_prefix = &ws_text.as_str()[prefix];
|
||||
if ws_text.contains('\n') && !ws_suffix.contains('\n') {
|
||||
if let Some(node) = ws.next_sibling() {
|
||||
let start = match ws_prefix.rfind('\n') {
|
||||
Some(idx) => ws.range().start() + TextUnit::from((idx + 1) as u32),
|
||||
None => node.range().start(),
|
||||
};
|
||||
let end = if root.text().char_at(node.range().end()) == Some('\n') {
|
||||
node.range().end() + TextUnit::of_char('\n')
|
||||
} else {
|
||||
node.range().end()
|
||||
};
|
||||
return TextRange::from_to(start, end);
|
||||
}
|
||||
}
|
||||
ws.range()
|
||||
}
|
||||
|
||||
fn pick_best<'a>(l: &'a SyntaxNode, r: &'a SyntaxNode) -> &'a SyntaxNode {
|
||||
return if priority(r) > priority(l) { r } else { l };
|
||||
fn priority(n: &SyntaxNode) -> usize {
|
||||
match n.kind() {
|
||||
WHITESPACE => 0,
|
||||
IDENT | SELF_KW | SUPER_KW | CRATE_KW | LIFETIME => 2,
|
||||
_ => 1,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Extend list item selection to include nearby comma and whitespace.
|
||||
fn extend_list_item(node: &SyntaxNode) -> Option<TextRange> {
|
||||
fn is_single_line_ws(node: &SyntaxNode) -> bool {
|
||||
node.kind() == WHITESPACE && !node.leaf_text().unwrap().contains('\n')
|
||||
}
|
||||
|
||||
fn nearby_comma(node: &SyntaxNode, dir: Direction) -> Option<&SyntaxNode> {
|
||||
node.siblings(dir)
|
||||
.skip(1)
|
||||
.skip_while(|node| is_single_line_ws(node))
|
||||
.next()
|
||||
.filter(|node| node.kind() == COMMA)
|
||||
}
|
||||
|
||||
if let Some(comma_node) = nearby_comma(node, Direction::Prev) {
|
||||
return Some(TextRange::from_to(comma_node.range().start(), node.range().end()));
|
||||
}
|
||||
|
||||
if let Some(comma_node) = nearby_comma(node, Direction::Next) {
|
||||
// Include any following whitespace when comma if after list item.
|
||||
let final_node = comma_node
|
||||
.siblings(Direction::Next)
|
||||
.skip(1)
|
||||
.next()
|
||||
.filter(|node| is_single_line_ws(node))
|
||||
.unwrap_or(comma_node);
|
||||
|
||||
return Some(TextRange::from_to(node.range().start(), final_node.range().end()));
|
||||
}
|
||||
|
||||
return None;
|
||||
}
|
||||
|
||||
fn extend_comments(node: &SyntaxNode) -> Option<TextRange> {
|
||||
let prev = adj_comments(node, Direction::Prev);
|
||||
let next = adj_comments(node, Direction::Next);
|
||||
if prev != next {
|
||||
Some(TextRange::from_to(prev.range().start(), next.range().end()))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn adj_comments(node: &SyntaxNode, dir: Direction) -> &SyntaxNode {
|
||||
let mut res = node;
|
||||
for node in node.siblings(dir) {
|
||||
match node.kind() {
|
||||
COMMENT => res = node,
|
||||
WHITESPACE if !node.leaf_text().unwrap().as_str().contains("\n\n") => (),
|
||||
_ => break,
|
||||
}
|
||||
}
|
||||
res
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use ra_syntax::{SourceFile, AstNode};
|
||||
use test_utils::extract_offset;
|
||||
|
||||
use super::*;
|
||||
|
||||
fn do_check(before: &str, afters: &[&str]) {
|
||||
let (cursor, before) = extract_offset(before);
|
||||
let file = SourceFile::parse(&before);
|
||||
let mut range = TextRange::offset_len(cursor, 0.into());
|
||||
for &after in afters {
|
||||
range = extend_selection(file.syntax(), range).unwrap();
|
||||
let actual = &before[range];
|
||||
assert_eq!(after, actual);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_extend_selection_arith() {
|
||||
do_check(r#"fn foo() { <|>1 + 1 }"#, &["1", "1 + 1", "{ 1 + 1 }"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_extend_selection_list() {
|
||||
do_check(r#"fn foo(<|>x: i32) {}"#, &["x", "x: i32"]);
|
||||
do_check(r#"fn foo(<|>x: i32, y: i32) {}"#, &["x", "x: i32", "x: i32, "]);
|
||||
do_check(r#"fn foo(<|>x: i32,y: i32) {}"#, &["x", "x: i32", "x: i32,"]);
|
||||
do_check(r#"fn foo(x: i32, <|>y: i32) {}"#, &["y", "y: i32", ", y: i32"]);
|
||||
do_check(r#"fn foo(x: i32, <|>y: i32, ) {}"#, &["y", "y: i32", ", y: i32"]);
|
||||
do_check(r#"fn foo(x: i32,<|>y: i32) {}"#, &["y", "y: i32", ",y: i32"]);
|
||||
|
||||
do_check(r#"const FOO: [usize; 2] = [ 22<|> , 33];"#, &["22", "22 , "]);
|
||||
do_check(r#"const FOO: [usize; 2] = [ 22 , 33<|>];"#, &["33", ", 33"]);
|
||||
do_check(r#"const FOO: [usize; 2] = [ 22 , 33<|> ,];"#, &["33", ", 33"]);
|
||||
|
||||
do_check(
|
||||
r#"
|
||||
const FOO: [usize; 2] = [
|
||||
22,
|
||||
<|>33,
|
||||
]"#,
|
||||
&["33", "33,"],
|
||||
);
|
||||
|
||||
do_check(
|
||||
r#"
|
||||
const FOO: [usize; 2] = [
|
||||
22
|
||||
, 33<|>,
|
||||
]"#,
|
||||
&["33", ", 33"],
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_extend_selection_start_of_the_line() {
|
||||
do_check(
|
||||
r#"
|
||||
impl S {
|
||||
<|> fn foo() {
|
||||
|
||||
}
|
||||
}"#,
|
||||
&[" fn foo() {\n\n }\n"],
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_extend_selection_doc_comments() {
|
||||
do_check(
|
||||
r#"
|
||||
struct A;
|
||||
|
||||
/// bla
|
||||
/// bla
|
||||
struct B {
|
||||
<|>
|
||||
}
|
||||
"#,
|
||||
&["\n \n", "{\n \n}", "/// bla\n/// bla\nstruct B {\n \n}"],
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_extend_selection_comments() {
|
||||
do_check(
|
||||
r#"
|
||||
fn bar(){}
|
||||
|
||||
// fn foo() {
|
||||
// 1 + <|>1
|
||||
// }
|
||||
|
||||
// fn foo(){}
|
||||
"#,
|
||||
&["1", "// 1 + 1", "// fn foo() {\n// 1 + 1\n// }"],
|
||||
);
|
||||
|
||||
do_check(
|
||||
r#"
|
||||
// #[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
// pub enum Direction {
|
||||
// <|> Next,
|
||||
// Prev
|
||||
// }
|
||||
"#,
|
||||
&[
|
||||
"// Next,",
|
||||
"// #[derive(Debug, Clone, Copy, PartialEq, Eq)]\n// pub enum Direction {\n// Next,\n// Prev\n// }",
|
||||
],
|
||||
);
|
||||
|
||||
do_check(
|
||||
r#"
|
||||
/*
|
||||
foo
|
||||
_bar1<|>*/
|
||||
"#,
|
||||
&["_bar1", "/*\nfoo\n_bar1*/"],
|
||||
);
|
||||
|
||||
do_check(
|
||||
r#"
|
||||
//!<|>foo_2 bar
|
||||
"#,
|
||||
&["foo_2", "//!foo_2 bar"],
|
||||
);
|
||||
|
||||
do_check(
|
||||
r#"
|
||||
/<|>/foo bar
|
||||
"#,
|
||||
&["//foo bar"],
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_extend_selection_prefer_idents() {
|
||||
do_check(
|
||||
r#"
|
||||
fn main() { foo<|>+bar;}
|
||||
"#,
|
||||
&["foo", "foo+bar"],
|
||||
);
|
||||
do_check(
|
||||
r#"
|
||||
fn main() { foo+<|>bar;}
|
||||
"#,
|
||||
&["bar", "foo+bar"],
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_extend_selection_prefer_lifetimes() {
|
||||
do_check(r#"fn foo<<|>'a>() {}"#, &["'a", "<'a>"]);
|
||||
do_check(r#"fn foo<'a<|>>() {}"#, &["'a", "<'a>"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_extend_selection_select_first_word() {
|
||||
do_check(r#"// foo bar b<|>az quxx"#, &["baz", "// foo bar baz quxx"]);
|
||||
do_check(
|
||||
r#"
|
||||
impl S {
|
||||
fn foo() {
|
||||
// hel<|>lo world
|
||||
}
|
||||
}
|
||||
"#,
|
||||
&["hello", "// hello world"],
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_extend_selection_string() {
|
||||
do_check(
|
||||
r#"
|
||||
fn bar(){}
|
||||
|
||||
" fn f<|>oo() {"
|
||||
"#,
|
||||
&["foo", "\" fn foo() {\""],
|
||||
);
|
||||
}
|
||||
}
|
|
@ -3,7 +3,6 @@
|
|||
//! This usually means functions which take syntax tree as an input and produce
|
||||
//! an edit or some auxiliary info.
|
||||
|
||||
mod extend_selection;
|
||||
mod folding_ranges;
|
||||
mod line_index;
|
||||
mod line_index_utils;
|
||||
|
@ -14,15 +13,16 @@ mod join_lines;
|
|||
mod typing;
|
||||
mod diagnostics;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct LocalEdit {
|
||||
pub label: String,
|
||||
pub edit: ra_text_edit::TextEdit,
|
||||
pub cursor_position: Option<TextUnit>,
|
||||
}
|
||||
use rustc_hash::FxHashSet;
|
||||
use ra_text_edit::TextEditBuilder;
|
||||
use ra_syntax::{
|
||||
SourceFile, SyntaxNode, TextRange, TextUnit, Direction,
|
||||
algo::find_leaf_at_offset,
|
||||
SyntaxKind::{self, *},
|
||||
ast::{self, AstNode},
|
||||
};
|
||||
|
||||
pub use self::{
|
||||
extend_selection::extend_selection,
|
||||
pub use crate::{
|
||||
folding_ranges::{folding_ranges, Fold, FoldKind},
|
||||
line_index::{LineCol, LineIndex},
|
||||
line_index_utils::translate_offset_with_edit,
|
||||
|
@ -30,16 +30,14 @@ pub use self::{
|
|||
diagnostics::diagnostics,
|
||||
join_lines::join_lines,
|
||||
typing::{on_enter, on_dot_typed, on_eq_typed},
|
||||
};
|
||||
|
||||
};
|
||||
use ra_text_edit::TextEditBuilder;
|
||||
use ra_syntax::{
|
||||
SourceFile, SyntaxNode, TextRange, TextUnit, Direction,
|
||||
SyntaxKind::{self, *},
|
||||
ast::{self, AstNode},
|
||||
algo::find_leaf_at_offset,
|
||||
};
|
||||
use rustc_hash::FxHashSet;
|
||||
#[derive(Debug)]
|
||||
pub struct LocalEdit {
|
||||
pub label: String,
|
||||
pub edit: ra_text_edit::TextEdit,
|
||||
pub cursor_position: Option<TextUnit>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct HighlightedRange {
|
||||
|
|
Loading…
Reference in a new issue