From 1967884d6836219ee78a754ca5c66ac781351559 Mon Sep 17 00:00:00 2001 From: Aleksey Kladov Date: Tue, 8 Jan 2019 22:17:36 +0300 Subject: rename ra_editor -> ra_ide_api_light --- crates/ra_ide_api_light/src/assists.rs | 209 ++++++ crates/ra_ide_api_light/src/assists/add_derive.rs | 84 +++ crates/ra_ide_api_light/src/assists/add_impl.rs | 66 ++ .../src/assists/change_visibility.rs | 116 +++ crates/ra_ide_api_light/src/assists/flip_comma.rs | 31 + .../src/assists/introduce_variable.rs | 144 ++++ .../src/assists/replace_if_let_with_match.rs | 92 +++ .../ra_ide_api_light/src/assists/split_import.rs | 56 ++ crates/ra_ide_api_light/src/diagnostics.rs | 266 +++++++ crates/ra_ide_api_light/src/extend_selection.rs | 281 +++++++ crates/ra_ide_api_light/src/folding_ranges.rs | 297 ++++++++ crates/ra_ide_api_light/src/lib.rs | 168 +++++ crates/ra_ide_api_light/src/line_index.rs | 399 ++++++++++ crates/ra_ide_api_light/src/line_index_utils.rs | 363 +++++++++ crates/ra_ide_api_light/src/structure.rs | 129 ++++ crates/ra_ide_api_light/src/test_utils.rs | 41 + crates/ra_ide_api_light/src/typing.rs | 826 +++++++++++++++++++++ 17 files changed, 3568 insertions(+) create mode 100644 crates/ra_ide_api_light/src/assists.rs create mode 100644 crates/ra_ide_api_light/src/assists/add_derive.rs create mode 100644 crates/ra_ide_api_light/src/assists/add_impl.rs create mode 100644 crates/ra_ide_api_light/src/assists/change_visibility.rs create mode 100644 crates/ra_ide_api_light/src/assists/flip_comma.rs create mode 100644 crates/ra_ide_api_light/src/assists/introduce_variable.rs create mode 100644 crates/ra_ide_api_light/src/assists/replace_if_let_with_match.rs create mode 100644 crates/ra_ide_api_light/src/assists/split_import.rs create mode 100644 crates/ra_ide_api_light/src/diagnostics.rs create mode 100644 crates/ra_ide_api_light/src/extend_selection.rs create mode 100644 crates/ra_ide_api_light/src/folding_ranges.rs create mode 100644 crates/ra_ide_api_light/src/lib.rs create mode 100644 crates/ra_ide_api_light/src/line_index.rs create mode 100644 crates/ra_ide_api_light/src/line_index_utils.rs create mode 100644 crates/ra_ide_api_light/src/structure.rs create mode 100644 crates/ra_ide_api_light/src/test_utils.rs create mode 100644 crates/ra_ide_api_light/src/typing.rs (limited to 'crates/ra_ide_api_light/src') diff --git a/crates/ra_ide_api_light/src/assists.rs b/crates/ra_ide_api_light/src/assists.rs new file mode 100644 index 000000000..83eabfc85 --- /dev/null +++ b/crates/ra_ide_api_light/src/assists.rs @@ -0,0 +1,209 @@ +//! This modules contains various "assits": suggestions for source code edits +//! which are likely to occur at a given cursor positon. For example, if the +//! cursor is on the `,`, a possible assist is swapping the elments around the +//! comma. + +mod flip_comma; +mod add_derive; +mod add_impl; +mod introduce_variable; +mod change_visibility; +mod split_import; +mod replace_if_let_with_match; + +use ra_text_edit::{TextEdit, TextEditBuilder}; +use ra_syntax::{ + Direction, SyntaxNode, TextUnit, TextRange, SourceFile, AstNode, + algo::{find_leaf_at_offset, find_node_at_offset, find_covering_node, LeafAtOffset}, + ast::{self, AstToken}, +}; +use itertools::Itertools; + +pub use self::{ + flip_comma::flip_comma, + add_derive::add_derive, + add_impl::add_impl, + introduce_variable::introduce_variable, + change_visibility::change_visibility, + split_import::split_import, + replace_if_let_with_match::replace_if_let_with_match, +}; + +/// Return all the assists applicable at the given position. +pub fn assists(file: &SourceFile, range: TextRange) -> Vec { + let ctx = AssistCtx::new(file, range); + [ + flip_comma, + add_derive, + add_impl, + introduce_variable, + change_visibility, + split_import, + replace_if_let_with_match, + ] + .iter() + .filter_map(|&assist| ctx.clone().apply(assist)) + .collect() +} + +#[derive(Debug)] +pub struct LocalEdit { + pub label: String, + pub edit: TextEdit, + pub cursor_position: Option, +} + +fn non_trivia_sibling(node: &SyntaxNode, direction: Direction) -> Option<&SyntaxNode> { + node.siblings(direction) + .skip(1) + .find(|node| !node.kind().is_trivia()) +} + +/// `AssistCtx` allows to apply an assist or check if it could be applied. +/// +/// Assists use a somewhat overengeneered approach, given the current needs. The +/// assists workflow consists of two phases. In the first phase, a user asks for +/// the list of available assists. In the second phase, the user picks a +/// particular assist and it gets applied. +/// +/// There are two peculiarities here: +/// +/// * first, we ideally avoid computing more things then neccessary to answer +/// "is assist applicable" in the first phase. +/// * second, when we are appling assist, we don't have a gurantee that there +/// weren't any changes between the point when user asked for assists and when +/// they applied a particular assist. So, when applying assist, we need to do +/// all the checks from scratch. +/// +/// To avoid repeating the same code twice for both "check" and "apply" +/// functions, we use an approach remeniscent of that of Django's function based +/// views dealing with forms. Each assist receives a runtime parameter, +/// `should_compute_edit`. It first check if an edit is applicable (potentially +/// computing info required to compute the actual edit). If it is applicable, +/// and `should_compute_edit` is `true`, it then computes the actual edit. +/// +/// So, to implement the original assists workflow, we can first apply each edit +/// with `should_compute_edit = false`, and then applying the selected edit +/// again, with `should_compute_edit = true` this time. +/// +/// Note, however, that we don't actually use such two-phase logic at the +/// moment, because the LSP API is pretty awkward in this place, and it's much +/// easier to just compute the edit eagarly :-) +#[derive(Debug, Clone)] +pub struct AssistCtx<'a> { + source_file: &'a SourceFile, + range: TextRange, + should_compute_edit: bool, +} + +#[derive(Debug)] +pub enum Assist { + Applicable, + Edit(LocalEdit), +} + +#[derive(Default)] +struct AssistBuilder { + edit: TextEditBuilder, + cursor_position: Option, +} + +impl<'a> AssistCtx<'a> { + pub fn new(source_file: &'a SourceFile, range: TextRange) -> AssistCtx { + AssistCtx { + source_file, + range, + should_compute_edit: false, + } + } + + pub fn apply(mut self, assist: fn(AssistCtx) -> Option) -> Option { + self.should_compute_edit = true; + match assist(self) { + None => None, + Some(Assist::Edit(e)) => Some(e), + Some(Assist::Applicable) => unreachable!(), + } + } + + pub fn check(mut self, assist: fn(AssistCtx) -> Option) -> bool { + self.should_compute_edit = false; + match assist(self) { + None => false, + Some(Assist::Edit(_)) => unreachable!(), + Some(Assist::Applicable) => true, + } + } + + fn build(self, label: impl Into, f: impl FnOnce(&mut AssistBuilder)) -> Option { + if !self.should_compute_edit { + return Some(Assist::Applicable); + } + let mut edit = AssistBuilder::default(); + f(&mut edit); + Some(Assist::Edit(LocalEdit { + label: label.into(), + edit: edit.edit.finish(), + cursor_position: edit.cursor_position, + })) + } + + pub(crate) fn leaf_at_offset(&self) -> LeafAtOffset<&'a SyntaxNode> { + find_leaf_at_offset(self.source_file.syntax(), self.range.start()) + } + pub(crate) fn node_at_offset(&self) -> Option<&'a N> { + find_node_at_offset(self.source_file.syntax(), self.range.start()) + } + pub(crate) fn covering_node(&self) -> &'a SyntaxNode { + find_covering_node(self.source_file.syntax(), self.range) + } +} + +impl AssistBuilder { + fn replace(&mut self, range: TextRange, replace_with: impl Into) { + self.edit.replace(range, replace_with.into()) + } + fn replace_node_and_indent(&mut self, node: &SyntaxNode, replace_with: impl Into) { + let mut replace_with = replace_with.into(); + if let Some(indent) = calc_indent(node) { + replace_with = reindent(&replace_with, indent) + } + self.replace(node.range(), replace_with) + } + #[allow(unused)] + fn delete(&mut self, range: TextRange) { + self.edit.delete(range) + } + fn insert(&mut self, offset: TextUnit, text: impl Into) { + self.edit.insert(offset, text.into()) + } + fn set_cursor(&mut self, offset: TextUnit) { + self.cursor_position = Some(offset) + } +} + +fn calc_indent(node: &SyntaxNode) -> Option<&str> { + let prev = node.prev_sibling()?; + let ws_text = ast::Whitespace::cast(prev)?.text(); + ws_text.rfind('\n').map(|pos| &ws_text[pos + 1..]) +} + +fn reindent(text: &str, indent: &str) -> String { + let indent = format!("\n{}", indent); + text.lines().intersperse(&indent).collect() +} + +#[cfg(test)] +fn check_assist(assist: fn(AssistCtx) -> Option, before: &str, after: &str) { + crate::test_utils::check_action(before, after, |file, off| { + let range = TextRange::offset_len(off, 0.into()); + AssistCtx::new(file, range).apply(assist) + }) +} + +#[cfg(test)] +fn check_assist_range(assist: fn(AssistCtx) -> Option, before: &str, after: &str) { + crate::test_utils::check_action_range(before, after, |file, range| { + AssistCtx::new(file, range).apply(assist) + }) +} diff --git a/crates/ra_ide_api_light/src/assists/add_derive.rs b/crates/ra_ide_api_light/src/assists/add_derive.rs new file mode 100644 index 000000000..6e964d011 --- /dev/null +++ b/crates/ra_ide_api_light/src/assists/add_derive.rs @@ -0,0 +1,84 @@ +use ra_syntax::{ + ast::{self, AstNode, AttrsOwner}, + SyntaxKind::{WHITESPACE, COMMENT}, + TextUnit, +}; + +use crate::assists::{AssistCtx, Assist}; + +pub fn add_derive(ctx: AssistCtx) -> Option { + let nominal = ctx.node_at_offset::()?; + let node_start = derive_insertion_offset(nominal)?; + ctx.build("add `#[derive]`", |edit| { + let derive_attr = nominal + .attrs() + .filter_map(|x| x.as_call()) + .filter(|(name, _arg)| name == "derive") + .map(|(_name, arg)| arg) + .next(); + let offset = match derive_attr { + None => { + edit.insert(node_start, "#[derive()]\n"); + node_start + TextUnit::of_str("#[derive(") + } + Some(tt) => tt.syntax().range().end() - TextUnit::of_char(')'), + }; + edit.set_cursor(offset) + }) +} + +// Insert `derive` after doc comments. +fn derive_insertion_offset(nominal: &ast::NominalDef) -> Option { + let non_ws_child = nominal + .syntax() + .children() + .find(|it| it.kind() != COMMENT && it.kind() != WHITESPACE)?; + Some(non_ws_child.range().start()) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::assists::check_assist; + + #[test] + fn add_derive_new() { + check_assist( + add_derive, + "struct Foo { a: i32, <|>}", + "#[derive(<|>)]\nstruct Foo { a: i32, }", + ); + check_assist( + add_derive, + "struct Foo { <|> a: i32, }", + "#[derive(<|>)]\nstruct Foo { a: i32, }", + ); + } + + #[test] + fn add_derive_existing() { + check_assist( + add_derive, + "#[derive(Clone)]\nstruct Foo { a: i32<|>, }", + "#[derive(Clone<|>)]\nstruct Foo { a: i32, }", + ); + } + + #[test] + fn add_derive_new_with_doc_comment() { + check_assist( + add_derive, + " +/// `Foo` is a pretty important struct. +/// It does stuff. +struct Foo { a: i32<|>, } + ", + " +/// `Foo` is a pretty important struct. +/// It does stuff. +#[derive(<|>)] +struct Foo { a: i32, } + ", + ); + } +} diff --git a/crates/ra_ide_api_light/src/assists/add_impl.rs b/crates/ra_ide_api_light/src/assists/add_impl.rs new file mode 100644 index 000000000..2eda7cae2 --- /dev/null +++ b/crates/ra_ide_api_light/src/assists/add_impl.rs @@ -0,0 +1,66 @@ +use join_to_string::join; +use ra_syntax::{ + ast::{self, AstNode, AstToken, NameOwner, TypeParamsOwner}, + TextUnit, +}; + +use crate::assists::{AssistCtx, Assist}; + +pub fn add_impl(ctx: AssistCtx) -> Option { + let nominal = ctx.node_at_offset::()?; + let name = nominal.name()?; + ctx.build("add impl", |edit| { + let type_params = nominal.type_param_list(); + let start_offset = nominal.syntax().range().end(); + let mut buf = String::new(); + buf.push_str("\n\nimpl"); + if let Some(type_params) = type_params { + type_params.syntax().text().push_to(&mut buf); + } + buf.push_str(" "); + buf.push_str(name.text().as_str()); + if let Some(type_params) = type_params { + let lifetime_params = type_params + .lifetime_params() + .filter_map(|it| it.lifetime()) + .map(|it| it.text()); + let type_params = type_params + .type_params() + .filter_map(|it| it.name()) + .map(|it| it.text()); + join(lifetime_params.chain(type_params)) + .surround_with("<", ">") + .to_buf(&mut buf); + } + buf.push_str(" {\n"); + edit.set_cursor(start_offset + TextUnit::of_str(&buf)); + buf.push_str("\n}"); + edit.insert(start_offset, buf); + }) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::assists::check_assist; + + #[test] + fn test_add_impl() { + check_assist( + add_impl, + "struct Foo {<|>}\n", + "struct Foo {}\n\nimpl Foo {\n<|>\n}\n", + ); + check_assist( + add_impl, + "struct Foo {<|>}", + "struct Foo {}\n\nimpl Foo {\n<|>\n}", + ); + check_assist( + add_impl, + "struct Foo<'a, T: Foo<'a>> {<|>}", + "struct Foo<'a, T: Foo<'a>> {}\n\nimpl<'a, T: Foo<'a>> Foo<'a, T> {\n<|>\n}", + ); + } + +} diff --git a/crates/ra_ide_api_light/src/assists/change_visibility.rs b/crates/ra_ide_api_light/src/assists/change_visibility.rs new file mode 100644 index 000000000..89729e2c2 --- /dev/null +++ b/crates/ra_ide_api_light/src/assists/change_visibility.rs @@ -0,0 +1,116 @@ +use ra_syntax::{ + AstNode, + ast::{self, VisibilityOwner, NameOwner}, + SyntaxKind::{VISIBILITY, FN_KW, MOD_KW, STRUCT_KW, ENUM_KW, TRAIT_KW, FN_DEF, MODULE, STRUCT_DEF, ENUM_DEF, TRAIT_DEF, IDENT}, +}; + +use crate::assists::{AssistCtx, Assist}; + +pub fn change_visibility(ctx: AssistCtx) -> Option { + if let Some(vis) = ctx.node_at_offset::() { + return change_vis(ctx, vis); + } + add_vis(ctx) +} + +fn add_vis(ctx: AssistCtx) -> Option { + let item_keyword = ctx.leaf_at_offset().find(|leaf| match leaf.kind() { + FN_KW | MOD_KW | STRUCT_KW | ENUM_KW | TRAIT_KW => true, + _ => false, + }); + + let offset = if let Some(keyword) = item_keyword { + let parent = keyword.parent()?; + let def_kws = vec![FN_DEF, MODULE, STRUCT_DEF, ENUM_DEF, TRAIT_DEF]; + // Parent is not a definition, can't add visibility + if !def_kws.iter().any(|&def_kw| def_kw == parent.kind()) { + return None; + } + // Already have visibility, do nothing + if parent.children().any(|child| child.kind() == VISIBILITY) { + return None; + } + parent.range().start() + } else { + let ident = ctx.leaf_at_offset().find(|leaf| leaf.kind() == IDENT)?; + let field = ident.ancestors().find_map(ast::NamedFieldDef::cast)?; + if field.name()?.syntax().range() != ident.range() && field.visibility().is_some() { + return None; + } + field.syntax().range().start() + }; + + ctx.build("make pub(crate)", |edit| { + edit.insert(offset, "pub(crate) "); + edit.set_cursor(offset); + }) +} + +fn change_vis(ctx: AssistCtx, vis: &ast::Visibility) -> Option { + if vis.syntax().text() != "pub" { + return None; + } + ctx.build("chage to pub(crate)", |edit| { + edit.replace(vis.syntax().range(), "pub(crate)"); + edit.set_cursor(vis.syntax().range().start()); + }) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::assists::check_assist; + + #[test] + fn change_visibility_adds_pub_crate_to_items() { + check_assist( + change_visibility, + "<|>fn foo() {}", + "<|>pub(crate) fn foo() {}", + ); + check_assist( + change_visibility, + "f<|>n foo() {}", + "<|>pub(crate) fn foo() {}", + ); + check_assist( + change_visibility, + "<|>struct Foo {}", + "<|>pub(crate) struct Foo {}", + ); + check_assist( + change_visibility, + "<|>mod foo {}", + "<|>pub(crate) mod foo {}", + ); + check_assist( + change_visibility, + "<|>trait Foo {}", + "<|>pub(crate) trait Foo {}", + ); + check_assist(change_visibility, "m<|>od {}", "<|>pub(crate) mod {}"); + check_assist( + change_visibility, + "unsafe f<|>n foo() {}", + "<|>pub(crate) unsafe fn foo() {}", + ); + } + + #[test] + fn change_visibility_works_with_struct_fields() { + check_assist( + change_visibility, + "struct S { <|>field: u32 }", + "struct S { <|>pub(crate) field: u32 }", + ) + } + + #[test] + fn change_visibility_pub_to_pub_crate() { + check_assist( + change_visibility, + "<|>pub fn foo() {}", + "<|>pub(crate) fn foo() {}", + ) + } +} diff --git a/crates/ra_ide_api_light/src/assists/flip_comma.rs b/crates/ra_ide_api_light/src/assists/flip_comma.rs new file mode 100644 index 000000000..a343413cc --- /dev/null +++ b/crates/ra_ide_api_light/src/assists/flip_comma.rs @@ -0,0 +1,31 @@ +use ra_syntax::{ + Direction, + SyntaxKind::COMMA, +}; + +use crate::assists::{non_trivia_sibling, AssistCtx, Assist}; + +pub fn flip_comma(ctx: AssistCtx) -> Option { + let comma = ctx.leaf_at_offset().find(|leaf| leaf.kind() == COMMA)?; + let prev = non_trivia_sibling(comma, Direction::Prev)?; + let next = non_trivia_sibling(comma, Direction::Next)?; + ctx.build("flip comma", |edit| { + edit.replace(prev.range(), next.text()); + edit.replace(next.range(), prev.text()); + }) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::assists::check_assist; + + #[test] + fn flip_comma_works_for_function_parameters() { + check_assist( + flip_comma, + "fn foo(x: i32,<|> y: Result<(), ()>) {}", + "fn foo(y: Result<(), ()>,<|> x: i32) {}", + ) + } +} diff --git a/crates/ra_ide_api_light/src/assists/introduce_variable.rs b/crates/ra_ide_api_light/src/assists/introduce_variable.rs new file mode 100644 index 000000000..523ec7034 --- /dev/null +++ b/crates/ra_ide_api_light/src/assists/introduce_variable.rs @@ -0,0 +1,144 @@ +use ra_syntax::{ + ast::{self, AstNode}, + SyntaxKind::WHITESPACE, + SyntaxNode, TextUnit, +}; + +use crate::assists::{AssistCtx, Assist}; + +pub fn introduce_variable<'a>(ctx: AssistCtx) -> Option { + let node = ctx.covering_node(); + let expr = node.ancestors().filter_map(ast::Expr::cast).next()?; + + let anchor_stmt = anchor_stmt(expr)?; + let indent = anchor_stmt.prev_sibling()?; + if indent.kind() != WHITESPACE { + return None; + } + ctx.build("introduce variable", move |edit| { + let mut buf = String::new(); + + buf.push_str("let var_name = "); + expr.syntax().text().push_to(&mut buf); + let is_full_stmt = if let Some(expr_stmt) = ast::ExprStmt::cast(anchor_stmt) { + Some(expr.syntax()) == expr_stmt.expr().map(|e| e.syntax()) + } else { + false + }; + if is_full_stmt { + edit.replace(expr.syntax().range(), buf); + } else { + buf.push_str(";"); + indent.text().push_to(&mut buf); + edit.replace(expr.syntax().range(), "var_name".to_string()); + edit.insert(anchor_stmt.range().start(), buf); + } + edit.set_cursor(anchor_stmt.range().start() + TextUnit::of_str("let ")); + }) +} + +/// Statement or last in the block expression, which will follow +/// the freshly introduced var. +fn anchor_stmt(expr: &ast::Expr) -> Option<&SyntaxNode> { + expr.syntax().ancestors().find(|&node| { + if ast::Stmt::cast(node).is_some() { + return true; + } + if let Some(expr) = node + .parent() + .and_then(ast::Block::cast) + .and_then(|it| it.expr()) + { + if expr.syntax() == node { + return true; + } + } + false + }) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::assists::check_assist_range; + + #[test] + fn test_introduce_var_simple() { + check_assist_range( + introduce_variable, + " +fn foo() { + foo(<|>1 + 1<|>); +}", + " +fn foo() { + let <|>var_name = 1 + 1; + foo(var_name); +}", + ); + } + + #[test] + fn test_introduce_var_expr_stmt() { + check_assist_range( + introduce_variable, + " +fn foo() { + <|>1 + 1<|>; +}", + " +fn foo() { + let <|>var_name = 1 + 1; +}", + ); + } + + #[test] + fn test_introduce_var_part_of_expr_stmt() { + check_assist_range( + introduce_variable, + " +fn foo() { + <|>1<|> + 1; +}", + " +fn foo() { + let <|>var_name = 1; + var_name + 1; +}", + ); + } + + #[test] + fn test_introduce_var_last_expr() { + check_assist_range( + introduce_variable, + " +fn foo() { + bar(<|>1 + 1<|>) +}", + " +fn foo() { + let <|>var_name = 1 + 1; + bar(var_name) +}", + ); + } + + #[test] + fn test_introduce_var_last_full_expr() { + check_assist_range( + introduce_variable, + " +fn foo() { + <|>bar(1 + 1)<|> +}", + " +fn foo() { + let <|>var_name = bar(1 + 1); + var_name +}", + ); + } + +} diff --git a/crates/ra_ide_api_light/src/assists/replace_if_let_with_match.rs b/crates/ra_ide_api_light/src/assists/replace_if_let_with_match.rs new file mode 100644 index 000000000..30c371480 --- /dev/null +++ b/crates/ra_ide_api_light/src/assists/replace_if_let_with_match.rs @@ -0,0 +1,92 @@ +use ra_syntax::{ + AstNode, SyntaxKind::{L_CURLY, R_CURLY, WHITESPACE}, + ast, +}; + +use crate::assists::{AssistCtx, Assist}; + +pub fn replace_if_let_with_match(ctx: AssistCtx) -> Option { + let if_expr: &ast::IfExpr = ctx.node_at_offset()?; + let cond = if_expr.condition()?; + let pat = cond.pat()?; + let expr = cond.expr()?; + let then_block = if_expr.then_branch()?; + let else_block = if_expr.else_branch()?; + + ctx.build("replace with match", |edit| { + let match_expr = build_match_expr(expr, pat, then_block, else_block); + edit.replace_node_and_indent(if_expr.syntax(), match_expr); + edit.set_cursor(if_expr.syntax().range().start()) + }) +} + +fn build_match_expr( + expr: &ast::Expr, + pat1: &ast::Pat, + arm1: &ast::Block, + arm2: &ast::Block, +) -> String { + let mut buf = String::new(); + buf.push_str(&format!("match {} {{\n", expr.syntax().text())); + buf.push_str(&format!( + " {} => {}\n", + pat1.syntax().text(), + format_arm(arm1) + )); + buf.push_str(&format!(" _ => {}\n", format_arm(arm2))); + buf.push_str("}"); + buf +} + +fn format_arm(block: &ast::Block) -> String { + match extract_expression(block) { + None => block.syntax().text().to_string(), + Some(e) => format!("{},", e.syntax().text()), + } +} + +fn extract_expression(block: &ast::Block) -> Option<&ast::Expr> { + let expr = block.expr()?; + let non_trivial_children = block.syntax().children().filter(|it| { + !(it == &expr.syntax() + || it.kind() == L_CURLY + || it.kind() == R_CURLY + || it.kind() == WHITESPACE) + }); + if non_trivial_children.count() > 0 { + return None; + } + Some(expr) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::assists::check_assist; + + #[test] + fn test_replace_if_let_with_match_unwraps_simple_expressions() { + check_assist( + replace_if_let_with_match, + " +impl VariantData { + pub fn is_struct(&self) -> bool { + if <|>let VariantData::Struct(..) = *self { + true + } else { + false + } + } +} ", + " +impl VariantData { + pub fn is_struct(&self) -> bool { + <|>match *self { + VariantData::Struct(..) => true, + _ => false, + } + } +} ", + ) + } +} diff --git a/crates/ra_ide_api_light/src/assists/split_import.rs b/crates/ra_ide_api_light/src/assists/split_import.rs new file mode 100644 index 000000000..e4015f07d --- /dev/null +++ b/crates/ra_ide_api_light/src/assists/split_import.rs @@ -0,0 +1,56 @@ +use ra_syntax::{ + TextUnit, AstNode, SyntaxKind::COLONCOLON, + ast, + algo::generate, +}; + +use crate::assists::{AssistCtx, Assist}; + +pub fn split_import(ctx: AssistCtx) -> Option { + let colon_colon = ctx + .leaf_at_offset() + .find(|leaf| leaf.kind() == COLONCOLON)?; + let path = colon_colon.parent().and_then(ast::Path::cast)?; + let top_path = generate(Some(path), |it| it.parent_path()).last()?; + + let use_tree = top_path.syntax().ancestors().find_map(ast::UseTree::cast); + if use_tree.is_none() { + return None; + } + + let l_curly = colon_colon.range().end(); + let r_curly = match top_path.syntax().parent().and_then(ast::UseTree::cast) { + Some(tree) => tree.syntax().range().end(), + None => top_path.syntax().range().end(), + }; + + ctx.build("split import", |edit| { + edit.insert(l_curly, "{"); + edit.insert(r_curly, "}"); + edit.set_cursor(l_curly + TextUnit::of_str("{")); + }) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::assists::check_assist; + + #[test] + fn test_split_import() { + check_assist( + split_import, + "use crate::<|>db::RootDatabase;", + "use crate::{<|>db::RootDatabase};", + ) + } + + #[test] + fn split_import_works_with_trees() { + check_assist( + split_import, + "use algo:<|>:visitor::{Visitor, visit}", + "use algo::{<|>visitor::{Visitor, visit}}", + ) + } +} diff --git a/crates/ra_ide_api_light/src/diagnostics.rs b/crates/ra_ide_api_light/src/diagnostics.rs new file mode 100644 index 000000000..2b695dfdf --- /dev/null +++ b/crates/ra_ide_api_light/src/diagnostics.rs @@ -0,0 +1,266 @@ +use itertools::Itertools; + +use ra_syntax::{ + Location, SourceFile, SyntaxKind, TextRange, SyntaxNode, + ast::{self, AstNode}, + +}; +use ra_text_edit::{TextEdit, TextEditBuilder}; + +use crate::{Diagnostic, LocalEdit, Severity}; + +pub fn diagnostics(file: &SourceFile) -> Vec { + fn location_to_range(location: Location) -> TextRange { + match location { + Location::Offset(offset) => TextRange::offset_len(offset, 1.into()), + Location::Range(range) => range, + } + } + + let mut errors: Vec = file + .errors() + .into_iter() + .map(|err| Diagnostic { + range: location_to_range(err.location()), + msg: format!("Syntax Error: {}", err), + severity: Severity::Error, + fix: None, + }) + .collect(); + + for node in file.syntax().descendants() { + check_unnecessary_braces_in_use_statement(&mut errors, node); + check_struct_shorthand_initialization(&mut errors, node); + } + + errors +} + +fn check_unnecessary_braces_in_use_statement( + acc: &mut Vec, + node: &SyntaxNode, +) -> Option<()> { + let use_tree_list = ast::UseTreeList::cast(node)?; + if let Some((single_use_tree,)) = use_tree_list.use_trees().collect_tuple() { + let range = use_tree_list.syntax().range(); + let edit = + text_edit_for_remove_unnecessary_braces_with_self_in_use_statement(single_use_tree) + .unwrap_or_else(|| { + let to_replace = single_use_tree.syntax().text().to_string(); + let mut edit_builder = TextEditBuilder::default(); + edit_builder.delete(range); + edit_builder.insert(range.start(), to_replace); + edit_builder.finish() + }); + + acc.push(Diagnostic { + range, + msg: format!("Unnecessary braces in use statement"), + severity: Severity::WeakWarning, + fix: Some(LocalEdit { + label: "Remove unnecessary braces".to_string(), + edit, + cursor_position: None, + }), + }); + } + + Some(()) +} + +fn text_edit_for_remove_unnecessary_braces_with_self_in_use_statement( + single_use_tree: &ast::UseTree, +) -> Option { + let use_tree_list_node = single_use_tree.syntax().parent()?; + if single_use_tree + .path()? + .segment()? + .syntax() + .first_child()? + .kind() + == SyntaxKind::SELF_KW + { + let start = use_tree_list_node.prev_sibling()?.range().start(); + let end = use_tree_list_node.range().end(); + let range = TextRange::from_to(start, end); + let mut edit_builder = TextEditBuilder::default(); + edit_builder.delete(range); + return Some(edit_builder.finish()); + } + None +} + +fn check_struct_shorthand_initialization( + acc: &mut Vec, + node: &SyntaxNode, +) -> Option<()> { + let struct_lit = ast::StructLit::cast(node)?; + let named_field_list = struct_lit.named_field_list()?; + for named_field in named_field_list.fields() { + if let (Some(name_ref), Some(expr)) = (named_field.name_ref(), named_field.expr()) { + let field_name = name_ref.syntax().text().to_string(); + let field_expr = expr.syntax().text().to_string(); + if field_name == field_expr { + let mut edit_builder = TextEditBuilder::default(); + edit_builder.delete(named_field.syntax().range()); + edit_builder.insert(named_field.syntax().range().start(), field_name); + let edit = edit_builder.finish(); + + acc.push(Diagnostic { + range: named_field.syntax().range(), + msg: format!("Shorthand struct initialization"), + severity: Severity::WeakWarning, + fix: Some(LocalEdit { + label: "use struct shorthand initialization".to_string(), + edit, + cursor_position: None, + }), + }); + } + } + } + Some(()) +} + +#[cfg(test)] +mod tests { + use crate::test_utils::assert_eq_text; + + use super::*; + + type DiagnosticChecker = fn(&mut Vec, &SyntaxNode) -> Option<()>; + + fn check_not_applicable(code: &str, func: DiagnosticChecker) { + let file = SourceFile::parse(code); + let mut diagnostics = Vec::new(); + for node in file.syntax().descendants() { + func(&mut diagnostics, node); + } + assert!(diagnostics.is_empty()); + } + + fn check_apply(before: &str, after: &str, func: DiagnosticChecker) { + let file = SourceFile::parse(before); + let mut diagnostics = Vec::new(); + for node in file.syntax().descendants() { + func(&mut diagnostics, node); + } + let diagnostic = diagnostics + .pop() + .unwrap_or_else(|| panic!("no diagnostics for:\n{}\n", before)); + let fix = diagnostic.fix.unwrap(); + let actual = fix.edit.apply(&before); + assert_eq_text!(after, &actual); + } + + #[test] + fn test_check_unnecessary_braces_in_use_statement() { + check_not_applicable( + " + use a; + use a::{c, d::e}; + ", + check_unnecessary_braces_in_use_statement, + ); + check_apply( + "use {b};", + "use b;", + check_unnecessary_braces_in_use_statement, + ); + check_apply( + "use a::{c};", + "use a::c;", + check_unnecessary_braces_in_use_statement, + ); + check_apply( + "use a::{self};", + "use a;", + check_unnecessary_braces_in_use_statement, + ); + check_apply( + "use a::{c, d::{e}};", + "use a::{c, d::e};", + check_unnecessary_braces_in_use_statement, + ); + } + + #[test] + fn test_check_struct_shorthand_initialization() { + check_not_applicable( + r#" + struct A { + a: &'static str + } + + fn main() { + A { + a: "hello" + } + } + "#, + check_struct_shorthand_initialization, + ); + + check_apply( + r#" +struct A { + a: &'static str +} + +fn main() { + let a = "haha"; + A { + a: a + } +} + "#, + r#" +struct A { + a: &'static str +} + +fn main() { + let a = "haha"; + A { + a + } +} + "#, + check_struct_shorthand_initialization, + ); + + check_apply( + r#" +struct A { + a: &'static str, + b: &'static str +} + +fn main() { + let a = "haha"; + let b = "bb"; + A { + a: a, + b + } +} + "#, + r#" +struct A { + a: &'static str, + b: &'static str +} + +fn main() { + let a = "haha"; + let b = "bb"; + A { + a, + b + } +} + "#, + check_struct_shorthand_initialization, + ); + } +} diff --git a/crates/ra_ide_api_light/src/extend_selection.rs b/crates/ra_ide_api_light/src/extend_selection.rs new file mode 100644 index 000000000..08cae5a51 --- /dev/null +++ b/crates/ra_ide_api_light/src/extend_selection.rs @@ -0,0 +1,281 @@ +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 { + let string_kinds = [COMMENT, STRING, RAW_STRING, BYTE_STRING, RAW_BYTE_STRING]; + 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); + if string_kinds.contains(&node.kind()) && range == node.range() { + if let Some(range) = extend_comments(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 { + 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, + } + } +} + +fn extend_comments(node: &SyntaxNode) -> Option { + 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_start_of_the_lind() { + 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() {\""], + ); + } +} diff --git a/crates/ra_ide_api_light/src/folding_ranges.rs b/crates/ra_ide_api_light/src/folding_ranges.rs new file mode 100644 index 000000000..6f3106889 --- /dev/null +++ b/crates/ra_ide_api_light/src/folding_ranges.rs @@ -0,0 +1,297 @@ +use rustc_hash::FxHashSet; + +use ra_syntax::{ + ast, AstNode, Direction, SourceFile, SyntaxNode, TextRange, + SyntaxKind::{self, *}, +}; + +#[derive(Debug, PartialEq, Eq)] +pub enum FoldKind { + Comment, + Imports, + Block, +} + +#[derive(Debug)] +pub struct Fold { + pub range: TextRange, + pub kind: FoldKind, +} + +pub fn folding_ranges(file: &SourceFile) -> Vec { + let mut res = vec![]; + let mut visited_comments = FxHashSet::default(); + let mut visited_imports = FxHashSet::default(); + + for node in file.syntax().descendants() { + // Fold items that span multiple lines + if let Some(kind) = fold_kind(node.kind()) { + if has_newline(node) { + res.push(Fold { + range: node.range(), + kind, + }); + } + } + + // Fold groups of comments + if node.kind() == COMMENT && !visited_comments.contains(&node) { + if let Some(range) = contiguous_range_for_comment(node, &mut visited_comments) { + res.push(Fold { + range, + kind: FoldKind::Comment, + }) + } + } + + // Fold groups of imports + if node.kind() == USE_ITEM && !visited_imports.contains(&node) { + if let Some(range) = contiguous_range_for_group(node, &mut visited_imports) { + res.push(Fold { + range, + kind: FoldKind::Imports, + }) + } + } + } + + res +} + +fn fold_kind(kind: SyntaxKind) -> Option { + match kind { + COMMENT => Some(FoldKind::Comment), + USE_ITEM => Some(FoldKind::Imports), + NAMED_FIELD_DEF_LIST | FIELD_PAT_LIST | ITEM_LIST | EXTERN_ITEM_LIST | USE_TREE_LIST + | BLOCK | ENUM_VARIANT_LIST => Some(FoldKind::Block), + _ => None, + } +} + +fn has_newline(node: &SyntaxNode) -> bool { + for descendant in node.descendants() { + if let Some(ws) = ast::Whitespace::cast(descendant) { + if ws.has_newlines() { + return true; + } + } else if let Some(comment) = ast::Comment::cast(descendant) { + if comment.has_newlines() { + return true; + } + } + } + + false +} + +fn contiguous_range_for_group<'a>( + first: &'a SyntaxNode, + visited: &mut FxHashSet<&'a SyntaxNode>, +) -> Option { + visited.insert(first); + + let mut last = first; + for node in first.siblings(Direction::Next) { + if let Some(ws) = ast::Whitespace::cast(node) { + // There is a blank line, which means that the group ends here + if ws.count_newlines_lazy().take(2).count() == 2 { + break; + } + + // Ignore whitespace without blank lines + continue; + } + + // Stop if we find a node that doesn't belong to the group + if node.kind() != first.kind() { + break; + } + + visited.insert(node); + last = node; + } + + if first != last { + Some(TextRange::from_to( + first.range().start(), + last.range().end(), + )) + } else { + // The group consists of only one element, therefore it cannot be folded + None + } +} + +fn contiguous_range_for_comment<'a>( + first: &'a SyntaxNode, + visited: &mut FxHashSet<&'a SyntaxNode>, +) -> Option { + visited.insert(first); + + // Only fold comments of the same flavor + let group_flavor = ast::Comment::cast(first)?.flavor(); + + let mut last = first; + for node in first.siblings(Direction::Next) { + if let Some(ws) = ast::Whitespace::cast(node) { + // There is a blank line, which means the group ends here + if ws.count_newlines_lazy().take(2).count() == 2 { + break; + } + + // Ignore whitespace without blank lines + continue; + } + + match ast::Comment::cast(node) { + Some(next_comment) if next_comment.flavor() == group_flavor => { + visited.insert(node); + last = node; + } + // The comment group ends because either: + // * An element of a different kind was reached + // * A comment of a different flavor was reached + _ => break, + } + } + + if first != last { + Some(TextRange::from_to( + first.range().start(), + last.range().end(), + )) + } else { + // The group consists of only one element, therefore it cannot be folded + None + } +} + +#[cfg(test)] +mod tests { + use super::*; + use test_utils::extract_ranges; + + fn do_check(text: &str, fold_kinds: &[FoldKind]) { + let (ranges, text) = extract_ranges(text, "fold"); + let file = SourceFile::parse(&text); + let folds = folding_ranges(&file); + + assert_eq!( + folds.len(), + ranges.len(), + "The amount of folds is different than the expected amount" + ); + assert_eq!( + folds.len(), + fold_kinds.len(), + "The amount of fold kinds is different than the expected amount" + ); + for ((fold, range), fold_kind) in folds + .into_iter() + .zip(ranges.into_iter()) + .zip(fold_kinds.into_iter()) + { + assert_eq!(fold.range.start(), range.start()); + assert_eq!(fold.range.end(), range.end()); + assert_eq!(&fold.kind, fold_kind); + } + } + + #[test] + fn test_fold_comments() { + let text = r#" +// Hello +// this is a multiline +// comment +// + +// But this is not + +fn main() { + // We should + // also + // fold + // this one. + //! But this one is different + //! because it has another flavor + /* As does this + multiline comment */ +}"#; + + let fold_kinds = &[ + FoldKind::Comment, + FoldKind::Block, + FoldKind::Comment, + FoldKind::Comment, + FoldKind::Comment, + ]; + do_check(text, fold_kinds); + } + + #[test] + fn test_fold_imports() { + let text = r#" +use std::{ + str, + vec, + io as iop +}; + +fn main() { +}"#; + + let folds = &[FoldKind::Imports, FoldKind::Block, FoldKind::Block]; + do_check(text, folds); + } + + #[test] + fn test_fold_import_groups() { + let text = r#" +use std::str; +use std::vec; +use std::io as iop; + +use std::mem; +use std::f64; + +use std::collections::HashMap; +// Some random comment +use std::collections::VecDeque; + +fn main() { +}"#; + + let folds = &[FoldKind::Imports, FoldKind::Imports, FoldKind::Block]; + do_check(text, folds); + } + + #[test] + fn test_fold_import_and_groups() { + let text = r#" +use std::str; +use std::vec; +use std::io as iop; + +use std::mem; +use std::f64; + +use std::collections::{ + HashMap, + VecDeque, +}; +// Some random comment + +fn main() { +}"#; + + let folds = &[ + FoldKind::Imports, + FoldKind::Imports, + FoldKind::Imports, + FoldKind::Block, + FoldKind::Block, + ]; + do_check(text, folds); + } + +} diff --git a/crates/ra_ide_api_light/src/lib.rs b/crates/ra_ide_api_light/src/lib.rs new file mode 100644 index 000000000..5a6af19b7 --- /dev/null +++ b/crates/ra_ide_api_light/src/lib.rs @@ -0,0 +1,168 @@ +pub mod assists; +mod extend_selection; +mod folding_ranges; +mod line_index; +mod line_index_utils; +mod structure; +#[cfg(test)] +mod test_utils; +mod typing; +mod diagnostics; + +pub use self::{ + assists::LocalEdit, + extend_selection::extend_selection, + folding_ranges::{folding_ranges, Fold, FoldKind}, + line_index::{LineCol, LineIndex}, + line_index_utils::translate_offset_with_edit, + structure::{file_structure, StructureNode}, + typing::{join_lines, on_enter, on_dot_typed, on_eq_typed}, + diagnostics::diagnostics +}; +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 HighlightedRange { + pub range: TextRange, + pub tag: &'static str, +} + +#[derive(Debug, Copy, Clone)] +pub enum Severity { + Error, + WeakWarning, +} + +#[derive(Debug)] +pub struct Diagnostic { + pub range: TextRange, + pub msg: String, + pub severity: Severity, + pub fix: Option, +} + +pub fn matching_brace(file: &SourceFile, offset: TextUnit) -> Option { + const BRACES: &[SyntaxKind] = &[ + L_CURLY, R_CURLY, L_BRACK, R_BRACK, L_PAREN, R_PAREN, L_ANGLE, R_ANGLE, + ]; + let (brace_node, brace_idx) = find_leaf_at_offset(file.syntax(), offset) + .filter_map(|node| { + let idx = BRACES.iter().position(|&brace| brace == node.kind())?; + Some((node, idx)) + }) + .next()?; + let parent = brace_node.parent()?; + let matching_kind = BRACES[brace_idx ^ 1]; + let matching_node = parent + .children() + .find(|node| node.kind() == matching_kind)?; + Some(matching_node.range().start()) +} + +pub fn highlight(root: &SyntaxNode) -> Vec { + // Visited nodes to handle highlighting priorities + let mut highlighted = FxHashSet::default(); + let mut res = Vec::new(); + for node in root.descendants() { + if highlighted.contains(&node) { + continue; + } + let tag = match node.kind() { + COMMENT => "comment", + STRING | RAW_STRING | RAW_BYTE_STRING | BYTE_STRING => "string", + ATTR => "attribute", + NAME_REF => "text", + NAME => "function", + INT_NUMBER | FLOAT_NUMBER | CHAR | BYTE => "literal", + LIFETIME => "parameter", + k if k.is_keyword() => "keyword", + _ => { + if let Some(macro_call) = ast::MacroCall::cast(node) { + if let Some(path) = macro_call.path() { + if let Some(segment) = path.segment() { + if let Some(name_ref) = segment.name_ref() { + highlighted.insert(name_ref.syntax()); + let range_start = name_ref.syntax().range().start(); + let mut range_end = name_ref.syntax().range().end(); + for sibling in path.syntax().siblings(Direction::Next) { + match sibling.kind() { + EXCL | IDENT => range_end = sibling.range().end(), + _ => (), + } + } + res.push(HighlightedRange { + range: TextRange::from_to(range_start, range_end), + tag: "macro", + }) + } + } + } + } + continue; + } + }; + res.push(HighlightedRange { + range: node.range(), + tag, + }) + } + res +} + +pub fn syntax_tree(file: &SourceFile) -> String { + ::ra_syntax::utils::dump_tree(file.syntax()) +} + +#[cfg(test)] +mod tests { + use ra_syntax::AstNode; + + use crate::test_utils::{add_cursor, assert_eq_dbg, assert_eq_text, extract_offset}; + + use super::*; + + #[test] + fn test_highlighting() { + let file = SourceFile::parse( + r#" +// comment +fn main() {} + println!("Hello, {}!", 92); +"#, + ); + let hls = highlight(file.syntax()); + assert_eq_dbg( + r#"[HighlightedRange { range: [1; 11), tag: "comment" }, + HighlightedRange { range: [12; 14), tag: "keyword" }, + HighlightedRange { range: [15; 19), tag: "function" }, + HighlightedRange { range: [29; 37), tag: "macro" }, + HighlightedRange { range: [38; 50), tag: "string" }, + HighlightedRange { range: [52; 54), tag: "literal" }]"#, + &hls, + ); + } + + #[test] + fn test_matching_brace() { + fn do_check(before: &str, after: &str) { + let (pos, before) = extract_offset(before); + let file = SourceFile::parse(&before); + let new_pos = match matching_brace(&file, pos) { + None => pos, + Some(pos) => pos, + }; + let actual = add_cursor(&before, new_pos); + assert_eq_text!(after, &actual); + } + + do_check("struct Foo { a: i32, }<|>", "struct Foo <|>{ a: i32, }"); + } + +} diff --git a/crates/ra_ide_api_light/src/line_index.rs b/crates/ra_ide_api_light/src/line_index.rs new file mode 100644 index 000000000..898fee7e0 --- /dev/null +++ b/crates/ra_ide_api_light/src/line_index.rs @@ -0,0 +1,399 @@ +use crate::TextUnit; +use rustc_hash::FxHashMap; +use superslice::Ext; + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct LineIndex { + pub(crate) newlines: Vec, + pub(crate) utf16_lines: FxHashMap>, +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +pub struct LineCol { + pub line: u32, + pub col_utf16: u32, +} + +#[derive(Clone, Debug, Hash, PartialEq, Eq)] +pub(crate) struct Utf16Char { + pub(crate) start: TextUnit, + pub(crate) end: TextUnit, +} + +impl Utf16Char { + fn len(&self) -> TextUnit { + self.end - self.start + } +} + +impl LineIndex { + pub fn new(text: &str) -> LineIndex { + let mut utf16_lines = FxHashMap::default(); + let mut utf16_chars = Vec::new(); + + let mut newlines = vec![0.into()]; + let mut curr_row = 0.into(); + let mut curr_col = 0.into(); + let mut line = 0; + for c in text.chars() { + curr_row += TextUnit::of_char(c); + if c == '\n' { + newlines.push(curr_row); + + // Save any utf-16 characters seen in the previous line + if utf16_chars.len() > 0 { + utf16_lines.insert(line, utf16_chars); + utf16_chars = Vec::new(); + } + + // Prepare for processing the next line + curr_col = 0.into(); + line += 1; + continue; + } + + let char_len = TextUnit::of_char(c); + if char_len.to_usize() > 1 { + utf16_chars.push(Utf16Char { + start: curr_col, + end: curr_col + char_len, + }); + } + + curr_col += char_len; + } + + // Save any utf-16 characters seen in the last line + if utf16_chars.len() > 0 { + utf16_lines.insert(line, utf16_chars); + } + + LineIndex { + newlines, + utf16_lines, + } + } + + pub fn line_col(&self, offset: TextUnit) -> LineCol { + let line = self.newlines.upper_bound(&offset) - 1; + let line_start_offset = self.newlines[line]; + let col = offset - line_start_offset; + + LineCol { + line: line as u32, + col_utf16: self.utf8_to_utf16_col(line as u32, col) as u32, + } + } + + pub fn offset(&self, line_col: LineCol) -> TextUnit { + //TODO: return Result + let col = self.utf16_to_utf8_col(line_col.line, line_col.col_utf16); + self.newlines[line_col.line as usize] + col + } + + fn utf8_to_utf16_col(&self, line: u32, mut col: TextUnit) -> usize { + if let Some(utf16_chars) = self.utf16_lines.get(&line) { + let mut correction = TextUnit::from_usize(0); + for c in utf16_chars { + if col >= c.end { + correction += c.len() - TextUnit::from_usize(1); + } else { + // From here on, all utf16 characters come *after* the character we are mapping, + // so we don't need to take them into account + break; + } + } + + col -= correction; + } + + col.to_usize() + } + + fn utf16_to_utf8_col(&self, line: u32, col: u32) -> TextUnit { + let mut col: TextUnit = col.into(); + if let Some(utf16_chars) = self.utf16_lines.get(&line) { + for c in utf16_chars { + if col >= c.start { + col += c.len() - TextUnit::from_usize(1); + } else { + // From here on, all utf16 characters come *after* the character we are mapping, + // so we don't need to take them into account + break; + } + } + } + + col + } +} + +#[cfg(test)] +/// Simple reference implementation to use in proptests +pub fn to_line_col(text: &str, offset: TextUnit) -> LineCol { + let mut res = LineCol { + line: 0, + col_utf16: 0, + }; + for (i, c) in text.char_indices() { + if i + c.len_utf8() > offset.to_usize() { + // if it's an invalid offset, inside a multibyte char + // return as if it was at the start of the char + break; + } + if c == '\n' { + res.line += 1; + res.col_utf16 = 0; + } else { + res.col_utf16 += 1; + } + } + res +} + +#[cfg(test)] +mod test_line_index { + use super::*; + use proptest::{prelude::*, proptest, proptest_helper}; + use ra_text_edit::test_utils::{arb_text, arb_offset}; + + #[test] + fn test_line_index() { + let text = "hello\nworld"; + let index = LineIndex::new(text); + assert_eq!( + index.line_col(0.into()), + LineCol { + line: 0, + col_utf16: 0 + } + ); + assert_eq!( + index.line_col(1.into()), + LineCol { + line: 0, + col_utf16: 1 + } + ); + assert_eq!( + index.line_col(5.into()), + LineCol { + line: 0, + col_utf16: 5 + } + ); + assert_eq!( + index.line_col(6.into()), + LineCol { + line: 1, + col_utf16: 0 + } + ); + assert_eq!( + index.line_col(7.into()), + LineCol { + line: 1, + col_utf16: 1 + } + ); + assert_eq!( + index.line_col(8.into()), + LineCol { + line: 1, + col_utf16: 2 + } + ); + assert_eq!( + index.line_col(10.into()), + LineCol { + line: 1, + col_utf16: 4 + } + ); + assert_eq!( + index.line_col(11.into()), + LineCol { + line: 1, + col_utf16: 5 + } + ); + assert_eq!( + index.line_col(12.into()), + LineCol { + line: 1, + col_utf16: 6 + } + ); + + let text = "\nhello\nworld"; + let index = LineIndex::new(text); + assert_eq!( + index.line_col(0.into()), + LineCol { + line: 0, + col_utf16: 0 + } + ); + assert_eq!( + index.line_col(1.into()), + LineCol { + line: 1, + col_utf16: 0 + } + ); + assert_eq!( + index.line_col(2.into()), + LineCol { + line: 1, + col_utf16: 1 + } + ); + assert_eq!( + index.line_col(6.into()), + LineCol { + line: 1, + col_utf16: 5 + } + ); + assert_eq!( + index.line_col(7.into()), + LineCol { + line: 2, + col_utf16: 0 + } + ); + } + + fn arb_text_with_offset() -> BoxedStrategy<(TextUnit, String)> { + arb_text() + .prop_flat_map(|text| (arb_offset(&text), Just(text))) + .boxed() + } + + fn to_line_col(text: &str, offset: TextUnit) -> LineCol { + let mut res = LineCol { + line: 0, + col_utf16: 0, + }; + for (i, c) in text.char_indices() { + if i + c.len_utf8() > offset.to_usize() { + // if it's an invalid offset, inside a multibyte char + // return as if it was at the start of the char + break; + } + if c == '\n' { + res.line += 1; + res.col_utf16 = 0; + } else { + res.col_utf16 += 1; + } + } + res + } + + proptest! { + #[test] + fn test_line_index_proptest((offset, text) in arb_text_with_offset()) { + let expected = to_line_col(&text, offset); + let line_index = LineIndex::new(&text); + let actual = line_index.line_col(offset); + + assert_eq!(actual, expected); + } + } +} + +#[cfg(test)] +mod test_utf8_utf16_conv { + use super::*; + + #[test] + fn test_char_len() { + assert_eq!('メ'.len_utf8(), 3); + assert_eq!('メ'.len_utf16(), 1); + } + + #[test] + fn test_empty_index() { + let col_index = LineIndex::new( + " +const C: char = 'x'; +", + ); + assert_eq!(col_index.utf16_lines.len(), 0); + } + + #[test] + fn test_single_char() { + let col_index = LineIndex::new( + " +const C: char = 'メ'; +", + ); + + assert_eq!(col_index.utf16_lines.len(), 1); + assert_eq!(col_index.utf16_lines[&1].len(), 1); + assert_eq!( + col_index.utf16_lines[&1][0], + Utf16Char { + start: 17.into(), + end: 20.into() + } + ); + + // UTF-8 to UTF-16, no changes + assert_eq!(col_index.utf8_to_utf16_col(1, 15.into()), 15); + + // UTF-8 to UTF-16 + assert_eq!(col_index.utf8_to_utf16_col(1, 22.into()), 20); + + // UTF-16 to UTF-8, no changes + assert_eq!(col_index.utf16_to_utf8_col(1, 15), TextUnit::from(15)); + + // UTF-16 to UTF-8 + assert_eq!(col_index.utf16_to_utf8_col(1, 19), TextUnit::from(21)); + } + + #[test] + fn test_string() { + let col_index = LineIndex::new( + " +const C: char = \"メ メ\"; +", + ); + + assert_eq!(col_index.utf16_lines.len(), 1); + assert_eq!(col_index.utf16_lines[&1].len(), 2); + assert_eq!( + col_index.utf16_lines[&1][0], + Utf16Char { + start: 17.into(), + end: 20.into() + } + ); + assert_eq!( + col_index.utf16_lines[&1][1], + Utf16Char { + start: 21.into(), + end: 24.into() + } + ); + + // UTF-8 to UTF-16 + assert_eq!(col_index.utf8_to_utf16_col(1, 15.into()), 15); + + assert_eq!(col_index.utf8_to_utf16_col(1, 21.into()), 19); + assert_eq!(col_index.utf8_to_utf16_col(1, 25.into()), 21); + + assert!(col_index.utf8_to_utf16_col(2, 15.into()) == 15); + + // UTF-16 to UTF-8 + assert_eq!(col_index.utf16_to_utf8_col(1, 15), TextUnit::from_usize(15)); + + assert_eq!(col_index.utf16_to_utf8_col(1, 18), TextUnit::from_usize(20)); + assert_eq!(col_index.utf16_to_utf8_col(1, 19), TextUnit::from_usize(23)); + + assert_eq!(col_index.utf16_to_utf8_col(2, 15), TextUnit::from_usize(15)); + } + +} diff --git a/crates/ra_ide_api_light/src/line_index_utils.rs b/crates/ra_ide_api_light/src/line_index_utils.rs new file mode 100644 index 000000000..ec3269bbb --- /dev/null +++ b/crates/ra_ide_api_light/src/line_index_utils.rs @@ -0,0 +1,363 @@ +use ra_text_edit::{AtomTextEdit, TextEdit}; +use ra_syntax::{TextUnit, TextRange}; +use crate::{LineIndex, LineCol, line_index::Utf16Char}; + +#[derive(Debug, Clone)] +enum Step { + Newline(TextUnit), + Utf16Char(TextRange), +} + +#[derive(Debug)] +struct LineIndexStepIter<'a> { + line_index: &'a LineIndex, + next_newline_idx: usize, + utf16_chars: Option<(TextUnit, std::slice::Iter<'a, Utf16Char>)>, +} + +impl<'a> LineIndexStepIter<'a> { + fn from(line_index: &LineIndex) -> LineIndexStepIter { + let mut x = LineIndexStepIter { + line_index, + next_newline_idx: 0, + utf16_chars: None, + }; + // skip first newline since it's not real + x.next(); + x + } +} + +impl<'a> Iterator for LineIndexStepIter<'a> { + type Item = Step; + fn next(&mut self) -> Option { + self.utf16_chars + .as_mut() + .and_then(|(newline, x)| { + let x = x.next()?; + Some(Step::Utf16Char(TextRange::from_to( + *newline + x.start, + *newline + x.end, + ))) + }) + .or_else(|| { + let next_newline = *self.line_index.newlines.get(self.next_newline_idx)?; + self.utf16_chars = self + .line_index + .utf16_lines + .get(&(self.next_newline_idx as u32)) + .map(|x| (next_newline, x.iter())); + self.next_newline_idx += 1; + Some(Step::Newline(next_newline)) + }) + } +} + +#[derive(Debug)] +struct OffsetStepIter<'a> { + text: &'a str, + offset: TextUnit, +} + +impl<'a> Iterator for OffsetStepIter<'a> { + type Item = Step; + fn next(&mut self) -> Option { + let (next, next_offset) = self + .text + .char_indices() + .filter_map(|(i, c)| { + if c == '\n' { + let next_offset = self.offset + TextUnit::from_usize(i + 1); + let next = Step::Newline(next_offset); + Some((next, next_offset)) + } else { + let char_len = TextUnit::of_char(c); + if char_len.to_usize() > 1 { + let start = self.offset + TextUnit::from_usize(i); + let end = start + char_len; + let next = Step::Utf16Char(TextRange::from_to(start, end)); + let next_offset = end; + Some((next, next_offset)) + } else { + None + } + } + }) + .next()?; + let next_idx = (next_offset - self.offset).to_usize(); + self.text = &self.text[next_idx..]; + self.offset = next_offset; + Some(next) + } +} + +#[derive(Debug)] +enum NextSteps<'a> { + Use, + ReplaceMany(OffsetStepIter<'a>), + AddMany(OffsetStepIter<'a>), +} + +#[derive(Debug)] +struct TranslatedEdit<'a> { + delete: TextRange, + insert: &'a str, + diff: i64, +} + +struct Edits<'a> { + edits: &'a [AtomTextEdit], + current: Option>, + acc_diff: i64, +} + +impl<'a> Edits<'a> { + fn from_text_edit(text_edit: &'a TextEdit) -> Edits<'a> { + let mut x = Edits { + edits: text_edit.as_atoms(), + current: None, + acc_diff: 0, + }; + x.advance_edit(); + x + } + fn advance_edit(&mut self) { + self.acc_diff += self.current.as_ref().map_or(0, |x| x.diff); + match self.edits.split_first() { + Some((next, rest)) => { + let delete = self.translate_range(next.delete); + let diff = next.insert.len() as i64 - next.delete.len().to_usize() as i64; + self.current = Some(TranslatedEdit { + delete, + insert: &next.insert, + diff, + }); + self.edits = rest; + } + None => { + self.current = None; + } + } + } + + fn next_inserted_steps(&mut self) -> Option> { + let cur = self.current.as_ref()?; + let res = Some(OffsetStepIter { + offset: cur.delete.start(), + text: &cur.insert, + }); + self.advance_edit(); + res + } + + fn next_steps(&mut self, step: &Step) -> NextSteps { + let step_pos = match step { + &Step::Newline(n) => n, + &Step::Utf16Char(r) => r.end(), + }; + let res = match &mut self.current { + Some(edit) => { + if step_pos <= edit.delete.start() { + NextSteps::Use + } else if step_pos <= edit.delete.end() { + let iter = OffsetStepIter { + offset: edit.delete.start(), + text: &edit.insert, + }; + // empty slice to avoid returning steps again + edit.insert = &edit.insert[edit.insert.len()..]; + NextSteps::ReplaceMany(iter) + } else { + let iter = OffsetStepIter { + offset: edit.delete.start(), + text: &edit.insert, + }; + // empty slice to avoid returning steps again + edit.insert = &edit.insert[edit.insert.len()..]; + self.advance_edit(); + NextSteps::AddMany(iter) + } + } + None => NextSteps::Use, + }; + res + } + + fn translate_range(&self, range: TextRange) -> TextRange { + if self.acc_diff == 0 { + range + } else { + let start = self.translate(range.start()); + let end = self.translate(range.end()); + TextRange::from_to(start, end) + } + } + + fn translate(&self, x: TextUnit) -> TextUnit { + if self.acc_diff == 0 { + x + } else { + TextUnit::from((x.to_usize() as i64 + self.acc_diff) as u32) + } + } + + fn translate_step(&self, x: &Step) -> Step { + if self.acc_diff == 0 { + x.clone() + } else { + match x { + &Step::Newline(n) => Step::Newline(self.translate(n)), + &Step::Utf16Char(r) => Step::Utf16Char(self.translate_range(r)), + } + } + } +} + +#[derive(Debug)] +struct RunningLineCol { + line: u32, + last_newline: TextUnit, + col_adjust: TextUnit, +} + +impl RunningLineCol { + fn new() -> RunningLineCol { + RunningLineCol { + line: 0, + last_newline: TextUnit::from(0), + col_adjust: TextUnit::from(0), + } + } + + fn to_line_col(&self, offset: TextUnit) -> LineCol { + LineCol { + line: self.line, + col_utf16: ((offset - self.last_newline) - self.col_adjust).into(), + } + } + + fn add_line(&mut self, newline: TextUnit) { + self.line += 1; + self.last_newline = newline; + self.col_adjust = TextUnit::from(0); + } + + fn adjust_col(&mut self, range: &TextRange) { + self.col_adjust += range.len() - TextUnit::from(1); + } +} + +pub fn translate_offset_with_edit( + line_index: &LineIndex, + offset: TextUnit, + text_edit: &TextEdit, +) -> LineCol { + let mut state = Edits::from_text_edit(&text_edit); + + let mut res = RunningLineCol::new(); + + macro_rules! test_step { + ($x:ident) => { + match &$x { + Step::Newline(n) => { + if offset < *n { + return res.to_line_col(offset); + } else { + res.add_line(*n); + } + } + Step::Utf16Char(x) => { + if offset < x.end() { + // if the offset is inside a multibyte char it's invalid + // clamp it to the start of the char + let clamp = offset.min(x.start()); + return res.to_line_col(clamp); + } else { + res.adjust_col(x); + } + } + } + }; + } + + for orig_step in LineIndexStepIter::from(line_index) { + loop { + let translated_step = state.translate_step(&orig_step); + match state.next_steps(&translated_step) { + NextSteps::Use => { + test_step!(translated_step); + break; + } + NextSteps::ReplaceMany(ns) => { + for n in ns { + test_step!(n); + } + break; + } + NextSteps::AddMany(ns) => { + for n in ns { + test_step!(n); + } + } + } + } + } + + loop { + match state.next_inserted_steps() { + None => break, + Some(ns) => { + for n in ns { + test_step!(n); + } + } + } + } + + res.to_line_col(offset) +} + +#[cfg(test)] +mod test { + use super::*; + use proptest::{prelude::*, proptest, proptest_helper}; + use crate::line_index; + use ra_text_edit::test_utils::{arb_offset, arb_text_with_edit}; + use ra_text_edit::TextEdit; + + #[derive(Debug)] + struct ArbTextWithEditAndOffset { + text: String, + edit: TextEdit, + edited_text: String, + offset: TextUnit, + } + + fn arb_text_with_edit_and_offset() -> BoxedStrategy { + arb_text_with_edit() + .prop_flat_map(|x| { + let edited_text = x.edit.apply(&x.text); + let arb_offset = arb_offset(&edited_text); + (Just(x), Just(edited_text), arb_offset).prop_map(|(x, edited_text, offset)| { + ArbTextWithEditAndOffset { + text: x.text, + edit: x.edit, + edited_text, + offset, + } + }) + }) + .boxed() + } + + proptest! { + #[test] + fn test_translate_offset_with_edit(x in arb_text_with_edit_and_offset()) { + let expected = line_index::to_line_col(&x.edited_text, x.offset); + let line_index = LineIndex::new(&x.text); + let actual = translate_offset_with_edit(&line_index, x.offset, &x.edit); + + assert_eq!(actual, expected); + } + } +} diff --git a/crates/ra_ide_api_light/src/structure.rs b/crates/ra_ide_api_light/src/structure.rs new file mode 100644 index 000000000..8bd57555f --- /dev/null +++ b/crates/ra_ide_api_light/src/structure.rs @@ -0,0 +1,129 @@ +use crate::TextRange; + +use ra_syntax::{ + algo::visit::{visitor, Visitor}, + ast::{self, NameOwner}, + AstNode, SourceFile, SyntaxKind, SyntaxNode, WalkEvent, +}; + +#[derive(Debug, Clone)] +pub struct StructureNode { + pub parent: Option, + pub label: String, + pub navigation_range: TextRange, + pub node_range: TextRange, + pub kind: SyntaxKind, +} + +pub fn file_structure(file: &SourceFile) -> Vec { + let mut res = Vec::new(); + let mut stack = Vec::new(); + + for event in file.syntax().preorder() { + match event { + WalkEvent::Enter(node) => { + if let Some(mut symbol) = structure_node(node) { + symbol.parent = stack.last().map(|&n| n); + stack.push(res.len()); + res.push(symbol); + } + } + WalkEvent::Leave(node) => { + if structure_node(node).is_some() { + stack.pop().unwrap(); + } + } + } + } + res +} + +fn structure_node(node: &SyntaxNode) -> Option { + fn decl(node: &N) -> Option { + let name = node.name()?; + Some(StructureNode { + parent: None, + label: name.text().to_string(), + navigation_range: name.syntax().range(), + node_range: node.syntax().range(), + kind: node.syntax().kind(), + }) + } + + visitor() + .visit(decl::) + .visit(decl::) + .visit(decl::) + .visit(decl::) + .visit(decl::) + .visit(decl::) + .visit(decl::) + .visit(decl::) + .visit(decl::) + .visit(|im: &ast::ImplBlock| { + let target_type = im.target_type()?; + let target_trait = im.target_trait(); + let label = match target_trait { + None => format!("impl {}", target_type.syntax().text()), + Some(t) => format!( + "impl {} for {}", + t.syntax().text(), + target_type.syntax().text(), + ), + }; + + let node = StructureNode { + parent: None, + label, + navigation_range: target_type.syntax().range(), + node_range: im.syntax().range(), + kind: im.syntax().kind(), + }; + Some(node) + }) + .accept(node)? +} + +#[cfg(test)] +mod tests { + use super::*; + use test_utils::assert_eq_dbg; + + #[test] + fn test_file_structure() { + let file = SourceFile::parse( + r#" +struct Foo { + x: i32 +} + +mod m { + fn bar() {} +} + +enum E { X, Y(i32) } +type T = (); +static S: i32 = 92; +const C: i32 = 92; + +impl E {} + +impl fmt::Debug for E {} +"#, + ); + let structure = file_structure(&file); + assert_eq_dbg( + r#"[StructureNode { parent: None, label: "Foo", navigation_range: [8; 11), node_range: [1; 26), kind: STRUCT_DEF }, + StructureNode { parent: Some(0), label: "x", navigation_range: [18; 19), node_range: [18; 24), kind: NAMED_FIELD_DEF }, + StructureNode { parent: None, label: "m", navigation_range: [32; 33), node_range: [28; 53), kind: MODULE }, + StructureNode { parent: Some(2), label: "bar", navigation_range: [43; 46), node_range: [40; 51), kind: FN_DEF }, + StructureNode { parent: None, label: "E", navigation_range: [60; 61), node_range: [55; 75), kind: ENUM_DEF }, + StructureNode { parent: None, label: "T", navigation_range: [81; 82), node_range: [76; 88), kind: TYPE_DEF }, + StructureNode { parent: None, label: "S", navigation_range: [96; 97), node_range: [89; 108), kind: STATIC_DEF }, + StructureNode { parent: None, label: "C", navigation_range: [115; 116), node_range: [109; 127), kind: CONST_DEF }, + StructureNode { parent: None, label: "impl E", navigation_range: [134; 135), node_range: [129; 138), kind: IMPL_BLOCK }, + StructureNode { parent: None, label: "impl fmt::Debug for E", navigation_range: [160; 161), node_range: [140; 164), kind: IMPL_BLOCK }]"#, + &structure, + ) + } +} diff --git a/crates/ra_ide_api_light/src/test_utils.rs b/crates/ra_ide_api_light/src/test_utils.rs new file mode 100644 index 000000000..dc2470aa3 --- /dev/null +++ b/crates/ra_ide_api_light/src/test_utils.rs @@ -0,0 +1,41 @@ +use ra_syntax::{SourceFile, TextRange, TextUnit}; + +use crate::LocalEdit; +pub use test_utils::*; + +pub fn check_action Option>( + before: &str, + after: &str, + f: F, +) { + let (before_cursor_pos, before) = extract_offset(before); + let file = SourceFile::parse(&before); + let result = f(&file, before_cursor_pos).expect("code action is not applicable"); + let actual = result.edit.apply(&before); + let actual_cursor_pos = match result.cursor_position { + None => result + .edit + .apply_to_offset(before_cursor_pos) + .expect("cursor position is affected by the edit"), + Some(off) => off, + }; + let actual = add_cursor(&actual, actual_cursor_pos); + assert_eq_text!(after, &actual); +} + +pub fn check_action_range Option>( + before: &str, + after: &str, + f: F, +) { + let (range, before) = extract_range(before); + let file = SourceFile::parse(&before); + let result = f(&file, range).expect("code action is not applicable"); + let actual = result.edit.apply(&before); + let actual_cursor_pos = match result.cursor_position { + None => result.edit.apply_to_offset(range.start()).unwrap(), + Some(off) => off, + }; + let actual = add_cursor(&actual, actual_cursor_pos); + assert_eq_text!(after, &actual); +} diff --git a/crates/ra_ide_api_light/src/typing.rs b/crates/ra_ide_api_light/src/typing.rs new file mode 100644 index 000000000..d8177f245 --- /dev/null +++ b/crates/ra_ide_api_light/src/typing.rs @@ -0,0 +1,826 @@ +use std::mem; + +use itertools::Itertools; +use ra_syntax::{ + algo::{find_node_at_offset, find_covering_node, find_leaf_at_offset, LeafAtOffset}, + ast, + AstNode, Direction, SourceFile, SyntaxKind, + SyntaxKind::*, + SyntaxNode, TextRange, TextUnit, +}; + +use crate::{LocalEdit, TextEditBuilder}; + +pub fn join_lines(file: &SourceFile, range: TextRange) -> LocalEdit { + let range = if range.is_empty() { + let syntax = file.syntax(); + let text = syntax.text().slice(range.start()..); + let pos = match text.find('\n') { + None => { + return LocalEdit { + label: "join lines".to_string(), + edit: TextEditBuilder::default().finish(), + cursor_position: None, + }; + } + Some(pos) => pos, + }; + TextRange::offset_len(range.start() + pos, TextUnit::of_char('\n')) + } else { + range + }; + + let node = find_covering_node(file.syntax(), range); + let mut edit = TextEditBuilder::default(); + for node in node.descendants() { + let text = match node.leaf_text() { + Some(text) => text, + None => continue, + }; + let range = match range.intersection(&node.range()) { + Some(range) => range, + None => continue, + } - node.range().start(); + for (pos, _) in text[range].bytes().enumerate().filter(|&(_, b)| b == b'\n') { + let pos: TextUnit = (pos as u32).into(); + let off = node.range().start() + range.start() + pos; + if !edit.invalidates_offset(off) { + remove_newline(&mut edit, node, text.as_str(), off); + } + } + } + + LocalEdit { + label: "join lines".to_string(), + edit: edit.finish(), + cursor_position: None, + } +} + +pub fn on_enter(file: &SourceFile, offset: TextUnit) -> Option { + let comment = find_leaf_at_offset(file.syntax(), offset) + .left_biased() + .and_then(ast::Comment::cast)?; + + if let ast::CommentFlavor::Multiline = comment.flavor() { + return None; + } + + let prefix = comment.prefix(); + if offset < comment.syntax().range().start() + TextUnit::of_str(prefix) + TextUnit::from(1) { + return None; + } + + let indent = node_indent(file, comment.syntax())?; + let inserted = format!("\n{}{} ", indent, prefix); + let cursor_position = offset + TextUnit::of_str(&inserted); + let mut edit = TextEditBuilder::default(); + edit.insert(offset, inserted); + Some(LocalEdit { + label: "on enter".to_string(), + edit: edit.finish(), + cursor_position: Some(cursor_position), + }) +} + +fn node_indent<'a>(file: &'a SourceFile, node: &SyntaxNode) -> Option<&'a str> { + let ws = match find_leaf_at_offset(file.syntax(), node.range().start()) { + LeafAtOffset::Between(l, r) => { + assert!(r == node); + l + } + LeafAtOffset::Single(n) => { + assert!(n == node); + return Some(""); + } + LeafAtOffset::None => unreachable!(), + }; + if ws.kind() != WHITESPACE { + return None; + } + let text = ws.leaf_text().unwrap(); + let pos = text.as_str().rfind('\n').map(|it| it + 1).unwrap_or(0); + Some(&text[pos..]) +} + +pub fn on_eq_typed(file: &SourceFile, offset: TextUnit) -> Option { + let let_stmt: &ast::LetStmt = find_node_at_offset(file.syntax(), offset)?; + if let_stmt.has_semi() { + return None; + } + if let Some(expr) = let_stmt.initializer() { + let expr_range = expr.syntax().range(); + if expr_range.contains(offset) && offset != expr_range.start() { + return None; + } + if file + .syntax() + .text() + .slice(offset..expr_range.start()) + .contains('\n') + { + return None; + } + } else { + return None; + } + let offset = let_stmt.syntax().range().end(); + let mut edit = TextEditBuilder::default(); + edit.insert(offset, ";".to_string()); + Some(LocalEdit { + label: "add semicolon".to_string(), + edit: edit.finish(), + cursor_position: None, + }) +} + +pub fn on_dot_typed(file: &SourceFile, offset: TextUnit) -> Option { + let before_dot_offset = offset - TextUnit::of_char('.'); + + let whitespace = find_leaf_at_offset(file.syntax(), before_dot_offset).left_biased()?; + + // find whitespace just left of the dot + ast::Whitespace::cast(whitespace)?; + + // make sure there is a method call + let method_call = whitespace + .siblings(Direction::Prev) + // first is whitespace + .skip(1) + .next()?; + + ast::MethodCallExpr::cast(method_call)?; + + // find how much the _method call is indented + let method_chain_indent = method_call + .parent()? + .siblings(Direction::Prev) + .skip(1) + .next()? + .leaf_text() + .map(|x| last_line_indent_in_whitespace(x))?; + + let current_indent = TextUnit::of_str(last_line_indent_in_whitespace(whitespace.leaf_text()?)); + // TODO: indent is always 4 spaces now. A better heuristic could look on the previous line(s) + + let target_indent = TextUnit::of_str(method_chain_indent) + TextUnit::from_usize(4); + + let diff = target_indent - current_indent; + + let indent = "".repeat(diff.to_usize()); + + let cursor_position = offset + diff; + let mut edit = TextEditBuilder::default(); + edit.insert(before_dot_offset, indent); + Some(LocalEdit { + label: "indent dot".to_string(), + edit: edit.finish(), + cursor_position: Some(cursor_position), + }) +} + +/// Finds the last line in the whitespace +fn last_line_indent_in_whitespace(ws: &str) -> &str { + ws.split('\n').last().unwrap_or("") +} + +fn remove_newline( + edit: &mut TextEditBuilder, + node: &SyntaxNode, + node_text: &str, + offset: TextUnit, +) { + if node.kind() != WHITESPACE || node_text.bytes().filter(|&b| b == b'\n').count() != 1 { + // The node is either the first or the last in the file + let suff = &node_text[TextRange::from_to( + offset - node.range().start() + TextUnit::of_char('\n'), + TextUnit::of_str(node_text), + )]; + let spaces = suff.bytes().take_while(|&b| b == b' ').count(); + + edit.replace( + TextRange::offset_len(offset, ((spaces + 1) as u32).into()), + " ".to_string(), + ); + return; + } + + // Special case that turns something like: + // + // ``` + // my_function({<|> + // + // }) + // ``` + // + // into `my_function()` + if join_single_expr_block(edit, node).is_some() { + return; + } + // ditto for + // + // ``` + // use foo::{<|> + // bar + // }; + // ``` + if join_single_use_tree(edit, node).is_some() { + return; + } + + // The node is between two other nodes + let prev = node.prev_sibling().unwrap(); + let next = node.next_sibling().unwrap(); + if is_trailing_comma(prev.kind(), next.kind()) { + // Removes: trailing comma, newline (incl. surrounding whitespace) + edit.delete(TextRange::from_to(prev.range().start(), node.range().end())); + } else if prev.kind() == COMMA && next.kind() == R_CURLY { + // Removes: comma, newline (incl. surrounding whitespace) + let space = if let Some(left) = prev.prev_sibling() { + compute_ws(left, next) + } else { + " " + }; + edit.replace( + TextRange::from_to(prev.range().start(), node.range().end()), + space.to_string(), + ); + } else if let (Some(_), Some(next)) = (ast::Comment::cast(prev), ast::Comment::cast(next)) { + // Removes: newline (incl. surrounding whitespace), start of the next comment + edit.delete(TextRange::from_to( + node.range().start(), + next.syntax().range().start() + TextUnit::of_str(next.prefix()), + )); + } else { + // Remove newline but add a computed amount of whitespace characters + edit.replace(node.range(), compute_ws(prev, next).to_string()); + } +} + +fn is_trailing_comma(left: SyntaxKind, right: SyntaxKind) -> bool { + match (left, right) { + (COMMA, R_PAREN) | (COMMA, R_BRACK) => true, + _ => false, + } +} + +fn join_single_expr_block(edit: &mut TextEditBuilder, node: &SyntaxNode) -> Option<()> { + let block = ast::Block::cast(node.parent()?)?; + let block_expr = ast::BlockExpr::cast(block.syntax().parent()?)?; + let expr = single_expr(block)?; + edit.replace( + block_expr.syntax().range(), + expr.syntax().text().to_string(), + ); + Some(()) +} + +fn single_expr(block: &ast::Block) -> Option<&ast::Expr> { + let mut res = None; + for child in block.syntax().children() { + if let Some(expr) = ast::Expr::cast(child) { + if expr.syntax().text().contains('\n') { + return None; + } + if mem::replace(&mut res, Some(expr)).is_some() { + return None; + } + } else { + match child.kind() { + WHITESPACE | L_CURLY | R_CURLY => (), + _ => return None, + } + } + } + res +} + +fn join_single_use_tree(edit: &mut TextEditBuilder, node: &SyntaxNode) -> Option<()> { + let use_tree_list = ast::UseTreeList::cast(node.parent()?)?; + let (tree,) = use_tree_list.use_trees().collect_tuple()?; + edit.replace( + use_tree_list.syntax().range(), + tree.syntax().text().to_string(), + ); + Some(()) +} + +fn compute_ws(left: &SyntaxNode, right: &SyntaxNode) -> &'static str { + match left.kind() { + L_PAREN | L_BRACK => return "", + L_CURLY => { + if let USE_TREE = right.kind() { + return ""; + } + } + _ => (), + } + match right.kind() { + R_PAREN | R_BRACK => return "", + R_CURLY => { + if let USE_TREE = left.kind() { + return ""; + } + } + DOT => return "", + _ => (), + } + " " +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::test_utils::{ + add_cursor, assert_eq_text, check_action, extract_offset, extract_range, +}; + + fn check_join_lines(before: &str, after: &str) { + check_action(before, after, |file, offset| { + let range = TextRange::offset_len(offset, 0.into()); + let res = join_lines(file, range); + Some(res) + }) + } + + #[test] + fn test_join_lines_comma() { + check_join_lines( + r" +fn foo() { + <|>foo(1, + ) +} +", + r" +fn foo() { + <|>foo(1) +} +", + ); + } + + #[test] + fn test_join_lines_lambda_block() { + check_join_lines( + r" +pub fn reparse(&self, edit: &AtomTextEdit) -> File { + <|>self.incremental_reparse(edit).unwrap_or_else(|| { + self.full_reparse(edit) + }) +} +", + r" +pub fn reparse(&self, edit: &AtomTextEdit) -> File { + <|>self.incremental_reparse(edit).unwrap_or_else(|| self.full_reparse(edit)) +} +", + ); + } + + #[test] + fn test_join_lines_block() { + check_join_lines( + r" +fn foo() { + foo(<|>{ + 92 + }) +}", + r" +fn foo() { + foo(<|>92) +}", + ); + } + + #[test] + fn test_join_lines_use_items_left() { + // No space after the '{' + check_join_lines( + r" +<|>use ra_syntax::{ + TextUnit, TextRange, +};", + r" +<|>use ra_syntax::{TextUnit, TextRange, +};", + ); + } + + #[test] + fn test_join_lines_use_items_right() { + // No space after the '}' + check_join_lines( + r" +use ra_syntax::{ +<|> TextUnit, TextRange +};", + r" +use ra_syntax::{ +<|> TextUnit, TextRange};", + ); + } + + #[test] + fn test_join_lines_use_items_right_comma() { + // No space after the '}' + check_join_lines( + r" +use ra_syntax::{ +<|> TextUnit, TextRange, +};", + r" +use ra_syntax::{ +<|> TextUnit, TextRange};", + ); + } + + #[test] + fn test_join_lines_use_tree() { + check_join_lines( + r" +use ra_syntax::{ + algo::<|>{ + find_leaf_at_offset, + }, + ast, +};", + r" +use ra_syntax::{ + algo::<|>find_leaf_at_offset, + ast, +};", + ); + } + + #[test] + fn test_join_lines_normal_comments() { + check_join_lines( + r" +fn foo() { + // Hello<|> + // world! +} +", + r" +fn foo() { + // Hello<|> world! +} +", + ); + } + + #[test] + fn test_join_lines_doc_comments() { + check_join_lines( + r" +fn foo() { + /// Hello<|> + /// world! +} +", + r" +fn foo() { + /// Hello<|> world! +} +", + ); + } + + #[test] + fn test_join_lines_mod_comments() { + check_join_lines( + r" +fn foo() { + //! Hello<|> + //! world! +} +", + r" +fn foo() { + //! Hello<|> world! +} +", + ); + } + + #[test] + fn test_join_lines_multiline_comments_1() { + check_join_lines( + r" +fn foo() { + // Hello<|> + /* world! */ +} +", + r" +fn foo() { + // Hello<|> world! */ +} +", + ); + } + + #[test] + fn test_join_lines_multiline_comments_2() { + check_join_lines( + r" +fn foo() { + // The<|> + /* quick + brown + fox! */ +} +", + r" +fn foo() { + // The<|> quick + brown + fox! */ +} +", + ); + } + + fn check_join_lines_sel(before: &str, after: &str) { + let (sel, before) = extract_range(before); + let file = SourceFile::parse(&before); + let result = join_lines(&file, sel); + let actual = result.edit.apply(&before); + assert_eq_text!(after, &actual); + } + + #[test] + fn test_join_lines_selection_fn_args() { + check_join_lines_sel( + r" +fn foo() { + <|>foo(1, + 2, + 3, + <|>) +} + ", + r" +fn foo() { + foo(1, 2, 3) +} + ", + ); + } + + #[test] + fn test_join_lines_selection_struct() { + check_join_lines_sel( + r" +struct Foo <|>{ + f: u32, +}<|> + ", + r" +struct Foo { f: u32 } + ", + ); + } + + #[test] + fn test_join_lines_selection_dot_chain() { + check_join_lines_sel( + r" +fn foo() { + join(<|>type_params.type_params() + .filter_map(|it| it.name()) + .map(|it| it.text())<|>) +}", + r" +fn foo() { + join(type_params.type_params().filter_map(|it| it.name()).map(|it| it.text())) +}", + ); + } + + #[test] + fn test_join_lines_selection_lambda_block_body() { + check_join_lines_sel( + r" +pub fn handle_find_matching_brace() { + params.offsets + .map(|offset| <|>{ + world.analysis().matching_brace(&file, offset).unwrap_or(offset) + }<|>) + .collect(); +}", + r" +pub fn handle_find_matching_brace() { + params.offsets + .map(|offset| world.analysis().matching_brace(&file, offset).unwrap_or(offset)) + .collect(); +}", + ); + } + + #[test] + fn test_on_eq_typed() { + fn do_check(before: &str, after: &str) { + let (offset, before) = extract_offset(before); + let file = SourceFile::parse(&before); + let result = on_eq_typed(&file, offset).unwrap(); + let actual = result.edit.apply(&before); + assert_eq_text!(after, &actual); + } + + // do_check(r" + // fn foo() { + // let foo =<|> + // } + // ", r" + // fn foo() { + // let foo =; + // } + // "); + do_check( + r" +fn foo() { + let foo =<|> 1 + 1 +} +", + r" +fn foo() { + let foo = 1 + 1; +} +", + ); + // do_check(r" + // fn foo() { + // let foo =<|> + // let bar = 1; + // } + // ", r" + // fn foo() { + // let foo =; + // let bar = 1; + // } + // "); + } + + #[test] + fn test_on_dot_typed() { + fn do_check(before: &str, after: &str) { + let (offset, before) = extract_offset(before); + let file = SourceFile::parse(&before); + if let Some(result) = on_eq_typed(&file, offset) { + let actual = result.edit.apply(&before); + assert_eq_text!(after, &actual); + }; + } + // indent if continuing chain call + do_check( + r" + pub fn child(&self, db: &impl HirDatabase, name: &Name) -> Cancelable> { + self.child_impl(db, name) + .<|> + } +", + r" + pub fn child(&self, db: &impl HirDatabase, name: &Name) -> Cancelable> { + self.child_impl(db, name) + . + } +", + ); + + // do not indent if already indented + do_check( + r" + pub fn child(&self, db: &impl HirDatabase, name: &Name) -> Cancelable> { + self.child_impl(db, name) + .<|> + } +", + r" + pub fn child(&self, db: &impl HirDatabase, name: &Name) -> Cancelable> { + self.child_impl(db, name) + . + } +", + ); + + // indent if the previous line is already indented + do_check( + r" + pub fn child(&self, db: &impl HirDatabase, name: &Name) -> Cancelable> { + self.child_impl(db, name) + .first() + .<|> + } +", + r" + pub fn child(&self, db: &impl HirDatabase, name: &Name) -> Cancelable> { + self.child_impl(db, name) + .first() + . + } +", + ); + + // don't indent if indent matches previous line + do_check( + r" + pub fn child(&self, db: &impl HirDatabase, name: &Name) -> Cancelable> { + self.child_impl(db, name) + .first() + .<|> + } +", + r" + pub fn child(&self, db: &impl HirDatabase, name: &Name) -> Cancelable> { + self.child_impl(db, name) + .first() + . + } +", + ); + + // don't indent if there is no method call on previous line + do_check( + r" + pub fn child(&self, db: &impl HirDatabase, name: &Name) -> Cancelable> { + .<|> + } +", + r" + pub fn child(&self, db: &impl HirDatabase, name: &Name) -> Cancelable> { + . + } +", + ); + + // indent to match previous expr + do_check( + r" + pub fn child(&self, db: &impl HirDatabase, name: &Name) -> Cancelable> { + self.child_impl(db, name) +.<|> + } +", + r" + pub fn child(&self, db: &impl HirDatabase, name: &Name) -> Cancelable> { + self.child_impl(db, name) + . + } +", + ); + } + + #[test] + fn test_on_enter() { + fn apply_on_enter(before: &str) -> Option { + let (offset, before) = extract_offset(before); + let file = SourceFile::parse(&before); + let result = on_enter(&file, offset)?; + let actual = result.edit.apply(&before); + let actual = add_cursor(&actual, result.cursor_position.unwrap()); + Some(actual) + } + + fn do_check(before: &str, after: &str) { + let actual = apply_on_enter(before).unwrap(); + assert_eq_text!(after, &actual); + } + + fn do_check_noop(text: &str) { + assert!(apply_on_enter(text).is_none()) + } + + do_check( + r" +/// Some docs<|> +fn foo() { +} +", + r" +/// Some docs +/// <|> +fn foo() { +} +", + ); + do_check( + r" +impl S { + /// Some<|> docs. + fn foo() {} +} +", + r" +impl S { + /// Some + /// <|> docs. + fn foo() {} +} +", + ); + do_check_noop(r"<|>//! docz"); + } +} -- cgit v1.2.3 From fa3c9ce3921b6a3f67222bf4f9b4efdf4f11c2a5 Mon Sep 17 00:00:00 2001 From: Aleksey Kladov Date: Tue, 8 Jan 2019 22:30:32 +0300 Subject: fix usages after rename --- crates/ra_ide_api_light/src/lib.rs | 5 +++++ 1 file changed, 5 insertions(+) (limited to 'crates/ra_ide_api_light/src') diff --git a/crates/ra_ide_api_light/src/lib.rs b/crates/ra_ide_api_light/src/lib.rs index 5a6af19b7..40638eda8 100644 --- a/crates/ra_ide_api_light/src/lib.rs +++ b/crates/ra_ide_api_light/src/lib.rs @@ -1,3 +1,8 @@ +//! This crate provides thouse IDE features which use only a single file. +//! +//! This usually means functions which take sytnax tree as an input and produce +//! an edit or some auxilarly info. + pub mod assists; mod extend_selection; mod folding_ranges; -- cgit v1.2.3