Make selections in assists with trailing/leading whitespace more forgiving

This commit is contained in:
Lukas Wirth 2021-10-12 14:41:59 +02:00
parent 1cca1fa5bf
commit 03fcf1b246
14 changed files with 57 additions and 23 deletions

View file

@ -14,7 +14,7 @@ use ide_db::{
};
use syntax::{
algo::{self, find_node_at_offset, find_node_at_range},
AstNode, AstToken, SourceFile, SyntaxElement, SyntaxKind, SyntaxNode, SyntaxNodePtr,
AstNode, AstToken, Direction, SourceFile, SyntaxElement, SyntaxKind, SyntaxNode, SyntaxNodePtr,
SyntaxToken, TextRange, TextSize, TokenAtOffset,
};
use text_edit::{TextEdit, TextEditBuilder};
@ -57,6 +57,7 @@ pub(crate) struct AssistContext<'a> {
pub(crate) config: &'a AssistConfig,
pub(crate) sema: Semantics<'a, RootDatabase>,
pub(crate) frange: FileRange,
trimmed_range: TextRange,
source_file: SourceFile,
}
@ -67,7 +68,20 @@ impl<'a> AssistContext<'a> {
frange: FileRange,
) -> AssistContext<'a> {
let source_file = sema.parse(frange.file_id);
AssistContext { config, sema, frange, source_file }
let start = frange.range.start();
let end = frange.range.end();
let left = source_file.syntax().token_at_offset(start);
let right = source_file.syntax().token_at_offset(end);
let left =
left.right_biased().and_then(|t| algo::skip_whitespace_token(t, Direction::Next));
let right =
right.left_biased().and_then(|t| algo::skip_whitespace_token(t, Direction::Prev));
let left = left.map(|t| t.text_range().start()).unwrap_or(start).clamp(start, end);
let right = right.map(|t| t.text_range().end()).unwrap_or(end).clamp(start, end);
let trimmed_range = TextRange::new(left, right);
AssistContext { config, sema, frange, source_file, trimmed_range }
}
pub(crate) fn db(&self) -> &RootDatabase {
@ -79,6 +93,12 @@ impl<'a> AssistContext<'a> {
self.frange.range.start()
}
/// Returns the selected range trimmed for whitespace tokens, that is the range will be snapped
/// to the nearest enclosed token.
pub(crate) fn selection_trimmed(&self) -> TextRange {
self.trimmed_range
}
pub(crate) fn token_at_offset(&self) -> TokenAtOffset<SyntaxToken> {
self.source_file.syntax().token_at_offset(self.offset())
}
@ -92,13 +112,15 @@ impl<'a> AssistContext<'a> {
find_node_at_offset(self.source_file.syntax(), self.offset())
}
pub(crate) fn find_node_at_range<N: AstNode>(&self) -> Option<N> {
find_node_at_range(self.source_file.syntax(), self.frange.range)
find_node_at_range(self.source_file.syntax(), self.trimmed_range)
}
pub(crate) fn find_node_at_offset_with_descend<N: AstNode>(&self) -> Option<N> {
self.sema.find_node_at_offset_with_descend(self.source_file.syntax(), self.offset())
}
/// Returns the element covered by the selection range, this excludes trailing whitespace in the selection.
pub(crate) fn covering_element(&self) -> SyntaxElement {
self.source_file.syntax().covering_element(self.frange.range)
self.source_file.syntax().covering_element(self.selection_trimmed())
// self.source_file.syntax().covering_element(self.frange.range)
}
}

View file

@ -107,11 +107,11 @@ fn extract_tail(ctx: &AssistContext) -> Option<(FnType, ast::Expr, InsertOrRepla
let ret_range = TextRange::new(rparen_pos, ret_range_end);
(FnType::Function, tail_expr, ret_range, action)
};
let frange = ctx.frange.range;
if return_type_range.contains_range(frange) {
let range = ctx.selection_trimmed();
if return_type_range.contains_range(range) {
cov_mark::hit!(cursor_in_ret_position);
cov_mark::hit!(cursor_in_ret_position_closure);
} else if tail_expr.syntax().text_range().contains_range(frange) {
} else if tail_expr.syntax().text_range().contains_range(range) {
cov_mark::hit!(cursor_on_tail);
cov_mark::hit!(cursor_on_tail_closure);
} else {

View file

@ -33,7 +33,7 @@ pub(crate) fn apply_demorgan(acc: &mut Assists, ctx: &AssistContext) -> Option<(
_ => return None,
};
let cursor_in_range = op_range.contains_range(ctx.frange.range);
let cursor_in_range = op_range.contains_range(ctx.selection_trimmed());
if !cursor_in_range {
return None;
}

View file

@ -199,7 +199,7 @@ fn validate_method_call_expr(
expr: ast::MethodCallExpr,
) -> Option<(ast::Expr, ast::Expr)> {
let name_ref = expr.name_ref()?;
if name_ref.syntax().text_range().intersect(ctx.frange.range).is_none() {
if !name_ref.syntax().text_range().contains_range(ctx.selection_trimmed()) {
cov_mark::hit!(test_for_each_not_applicable_invalid_cursor_pos);
return None;
}

View file

@ -56,7 +56,7 @@ type FxIndexSet<T> = indexmap::IndexSet<T, BuildHasherDefault<FxHasher>>;
// }
// ```
pub(crate) fn extract_function(acc: &mut Assists, ctx: &AssistContext) -> Option<()> {
let range = ctx.frange.range;
let range = ctx.selection_trimmed();
if range.is_empty() {
return None;
}

View file

@ -31,6 +31,7 @@ pub(crate) fn extract_variable(acc: &mut Assists, ctx: &AssistContext) -> Option
if ctx.frange.range.is_empty() {
return None;
}
let node = match ctx.covering_element() {
NodeOrToken::Node(it) => it,
NodeOrToken::Token(it) if it.kind() == COMMENT => {
@ -238,7 +239,7 @@ fn foo() {
extract_variable,
r#"
fn foo() {
$01 + 1$0;
$0 1 + 1$0;
}"#,
r#"
fn foo() {
@ -247,12 +248,12 @@ fn foo() {
);
check_assist(
extract_variable,
"
r"
fn foo() {
$0{ let x = 0; x }$0
something_else();
}",
"
r"
fn foo() {
let $0var_name = { let x = 0; x };
something_else();
@ -264,11 +265,11 @@ fn foo() {
fn test_extract_var_part_of_expr_stmt() {
check_assist(
extract_variable,
"
r"
fn foo() {
$01$0 + 1;
}",
"
r"
fn foo() {
let $0var_name = 1;
var_name + 1;

View file

@ -23,7 +23,7 @@ pub(crate) fn flip_binexpr(acc: &mut Assists, ctx: &AssistContext) -> Option<()>
let rhs = expr.rhs()?.syntax().clone();
let op_range = expr.op_token()?.text_range();
// The assist should be applied only if the cursor is on the operator
let cursor_in_range = op_range.contains_range(ctx.frange.range);
let cursor_in_range = op_range.contains_range(ctx.selection_trimmed());
if !cursor_in_range {
return None;
}

View file

@ -199,7 +199,7 @@ pub(crate) fn inline_call(acc: &mut Assists, ctx: &AssistContext) -> Option<()>
let param_list = fn_source.value.param_list()?;
let FileRange { file_id, range } = fn_source.syntax().original_file_range(ctx.sema.db);
if file_id == ctx.frange.file_id && range.contains(ctx.frange.range.start()) {
if file_id == ctx.frange.file_id && range.contains(ctx.offset()) {
cov_mark::hit!(inline_call_recursive);
return None;
}

View file

@ -1,7 +1,7 @@
use either::Either;
use hir::{PathResolution, Semantics};
use ide_db::{
base_db::{FileId, FileRange},
base_db::FileId,
defs::Definition,
search::{FileReference, UsageSearchResult},
RootDatabase,
@ -33,7 +33,8 @@ use crate::{
// }
// ```
pub(crate) fn inline_local_variable(acc: &mut Assists, ctx: &AssistContext) -> Option<()> {
let FileRange { file_id, range } = ctx.frange;
let file_id = ctx.frange.file_id;
let range = ctx.selection_trimmed();
let InlineData { let_stmt, delete_let, references, target } =
if let Some(let_stmt) = ctx.find_node_at_offset::<ast::LetStmt>() {
inline_let(&ctx.sema, let_stmt, range, file_id)

View file

@ -29,7 +29,7 @@ pub(crate) fn invert_if(acc: &mut Assists, ctx: &AssistContext) -> Option<()> {
let if_keyword = ctx.find_token_syntax_at_offset(T![if])?;
let expr = ast::IfExpr::cast(if_keyword.parent()?)?;
let if_range = if_keyword.text_range();
let cursor_in_range = if_range.contains_range(ctx.frange.range);
let cursor_in_range = if_range.contains_range(ctx.selection_trimmed());
if !cursor_in_range {
return None;
}

View file

@ -27,7 +27,7 @@ pub(crate) fn move_from_mod_rs(acc: &mut Assists, ctx: &AssistContext) -> Option
let source_file = ctx.find_node_at_offset::<ast::SourceFile>()?;
let module = ctx.sema.to_module_def(ctx.frange.file_id)?;
// Enable this assist if the user select all "meaningful" content in the source file
let trimmed_selected_range = trimmed_text_range(&source_file, ctx.frange.range);
let trimmed_selected_range = trimmed_text_range(&source_file, ctx.selection_trimmed());
let trimmed_file_range = trimmed_text_range(&source_file, source_file.syntax().text_range());
if !module.is_mod_rs(ctx.db()) {
cov_mark::hit!(not_mod_rs);

View file

@ -27,7 +27,7 @@ pub(crate) fn move_to_mod_rs(acc: &mut Assists, ctx: &AssistContext) -> Option<(
let source_file = ctx.find_node_at_offset::<ast::SourceFile>()?;
let module = ctx.sema.to_module_def(ctx.frange.file_id)?;
// Enable this assist if the user select all "meaningful" content in the source file
let trimmed_selected_range = trimmed_text_range(&source_file, ctx.frange.range);
let trimmed_selected_range = trimmed_text_range(&source_file, ctx.selection_trimmed());
let trimmed_file_range = trimmed_text_range(&source_file, source_file.syntax().text_range());
if module.is_mod_rs(ctx.db()) {
cov_mark::hit!(already_mod_rs);

View file

@ -48,7 +48,7 @@ pub(crate) fn replace_if_let_with_match(acc: &mut Assists, ctx: &AssistContext)
if_expr.syntax().text_range().start(),
if_expr.then_branch()?.syntax().text_range().start(),
);
let cursor_in_range = available_range.contains_range(ctx.frange.range);
let cursor_in_range = available_range.contains_range(ctx.selection_trimmed());
if !cursor_in_range {
return None;
}

View file

@ -53,6 +53,16 @@ pub fn skip_trivia_token(mut token: SyntaxToken, direction: Direction) -> Option
}
Some(token)
}
/// Skip to next non `whitespace` token
pub fn skip_whitespace_token(mut token: SyntaxToken, direction: Direction) -> Option<SyntaxToken> {
while token.kind() == SyntaxKind::WHITESPACE {
token = match direction {
Direction::Next => token.next_token()?,
Direction::Prev => token.prev_token()?,
}
}
Some(token)
}
/// Finds the first sibling in the given direction which is not `trivia`
pub fn non_trivia_sibling(element: SyntaxElement, direction: Direction) -> Option<SyntaxElement> {