From 1967884d6836219ee78a754ca5c66ac781351559 Mon Sep 17 00:00:00 2001 From: Aleksey Kladov Date: Tue, 8 Jan 2019 22:17:36 +0300 Subject: [PATCH 1/5] rename ra_editor -> ra_ide_api_light --- crates/{ra_editor => ra_ide_api_light}/Cargo.toml | 0 crates/{ra_editor => ra_ide_api_light}/src/assists.rs | 0 crates/{ra_editor => ra_ide_api_light}/src/assists/add_derive.rs | 0 crates/{ra_editor => ra_ide_api_light}/src/assists/add_impl.rs | 0 .../src/assists/change_visibility.rs | 0 crates/{ra_editor => ra_ide_api_light}/src/assists/flip_comma.rs | 0 .../src/assists/introduce_variable.rs | 0 .../src/assists/replace_if_let_with_match.rs | 0 .../{ra_editor => ra_ide_api_light}/src/assists/split_import.rs | 0 crates/{ra_editor => ra_ide_api_light}/src/diagnostics.rs | 0 crates/{ra_editor => ra_ide_api_light}/src/extend_selection.rs | 0 crates/{ra_editor => ra_ide_api_light}/src/folding_ranges.rs | 0 crates/{ra_editor => ra_ide_api_light}/src/lib.rs | 0 crates/{ra_editor => ra_ide_api_light}/src/line_index.rs | 0 crates/{ra_editor => ra_ide_api_light}/src/line_index_utils.rs | 0 crates/{ra_editor => ra_ide_api_light}/src/structure.rs | 0 crates/{ra_editor => ra_ide_api_light}/src/test_utils.rs | 0 crates/{ra_editor => ra_ide_api_light}/src/typing.rs | 0 18 files changed, 0 insertions(+), 0 deletions(-) rename crates/{ra_editor => ra_ide_api_light}/Cargo.toml (100%) rename crates/{ra_editor => ra_ide_api_light}/src/assists.rs (100%) rename crates/{ra_editor => ra_ide_api_light}/src/assists/add_derive.rs (100%) rename crates/{ra_editor => ra_ide_api_light}/src/assists/add_impl.rs (100%) rename crates/{ra_editor => ra_ide_api_light}/src/assists/change_visibility.rs (100%) rename crates/{ra_editor => ra_ide_api_light}/src/assists/flip_comma.rs (100%) rename crates/{ra_editor => ra_ide_api_light}/src/assists/introduce_variable.rs (100%) rename crates/{ra_editor => ra_ide_api_light}/src/assists/replace_if_let_with_match.rs (100%) rename crates/{ra_editor => ra_ide_api_light}/src/assists/split_import.rs (100%) rename crates/{ra_editor => ra_ide_api_light}/src/diagnostics.rs (100%) rename crates/{ra_editor => ra_ide_api_light}/src/extend_selection.rs (100%) rename crates/{ra_editor => ra_ide_api_light}/src/folding_ranges.rs (100%) rename crates/{ra_editor => ra_ide_api_light}/src/lib.rs (100%) rename crates/{ra_editor => ra_ide_api_light}/src/line_index.rs (100%) rename crates/{ra_editor => ra_ide_api_light}/src/line_index_utils.rs (100%) rename crates/{ra_editor => ra_ide_api_light}/src/structure.rs (100%) rename crates/{ra_editor => ra_ide_api_light}/src/test_utils.rs (100%) rename crates/{ra_editor => ra_ide_api_light}/src/typing.rs (100%) diff --git a/crates/ra_editor/Cargo.toml b/crates/ra_ide_api_light/Cargo.toml similarity index 100% rename from crates/ra_editor/Cargo.toml rename to crates/ra_ide_api_light/Cargo.toml diff --git a/crates/ra_editor/src/assists.rs b/crates/ra_ide_api_light/src/assists.rs similarity index 100% rename from crates/ra_editor/src/assists.rs rename to crates/ra_ide_api_light/src/assists.rs diff --git a/crates/ra_editor/src/assists/add_derive.rs b/crates/ra_ide_api_light/src/assists/add_derive.rs similarity index 100% rename from crates/ra_editor/src/assists/add_derive.rs rename to crates/ra_ide_api_light/src/assists/add_derive.rs diff --git a/crates/ra_editor/src/assists/add_impl.rs b/crates/ra_ide_api_light/src/assists/add_impl.rs similarity index 100% rename from crates/ra_editor/src/assists/add_impl.rs rename to crates/ra_ide_api_light/src/assists/add_impl.rs diff --git a/crates/ra_editor/src/assists/change_visibility.rs b/crates/ra_ide_api_light/src/assists/change_visibility.rs similarity index 100% rename from crates/ra_editor/src/assists/change_visibility.rs rename to crates/ra_ide_api_light/src/assists/change_visibility.rs diff --git a/crates/ra_editor/src/assists/flip_comma.rs b/crates/ra_ide_api_light/src/assists/flip_comma.rs similarity index 100% rename from crates/ra_editor/src/assists/flip_comma.rs rename to crates/ra_ide_api_light/src/assists/flip_comma.rs diff --git a/crates/ra_editor/src/assists/introduce_variable.rs b/crates/ra_ide_api_light/src/assists/introduce_variable.rs similarity index 100% rename from crates/ra_editor/src/assists/introduce_variable.rs rename to crates/ra_ide_api_light/src/assists/introduce_variable.rs diff --git a/crates/ra_editor/src/assists/replace_if_let_with_match.rs b/crates/ra_ide_api_light/src/assists/replace_if_let_with_match.rs similarity index 100% rename from crates/ra_editor/src/assists/replace_if_let_with_match.rs rename to crates/ra_ide_api_light/src/assists/replace_if_let_with_match.rs diff --git a/crates/ra_editor/src/assists/split_import.rs b/crates/ra_ide_api_light/src/assists/split_import.rs similarity index 100% rename from crates/ra_editor/src/assists/split_import.rs rename to crates/ra_ide_api_light/src/assists/split_import.rs diff --git a/crates/ra_editor/src/diagnostics.rs b/crates/ra_ide_api_light/src/diagnostics.rs similarity index 100% rename from crates/ra_editor/src/diagnostics.rs rename to crates/ra_ide_api_light/src/diagnostics.rs diff --git a/crates/ra_editor/src/extend_selection.rs b/crates/ra_ide_api_light/src/extend_selection.rs similarity index 100% rename from crates/ra_editor/src/extend_selection.rs rename to crates/ra_ide_api_light/src/extend_selection.rs diff --git a/crates/ra_editor/src/folding_ranges.rs b/crates/ra_ide_api_light/src/folding_ranges.rs similarity index 100% rename from crates/ra_editor/src/folding_ranges.rs rename to crates/ra_ide_api_light/src/folding_ranges.rs diff --git a/crates/ra_editor/src/lib.rs b/crates/ra_ide_api_light/src/lib.rs similarity index 100% rename from crates/ra_editor/src/lib.rs rename to crates/ra_ide_api_light/src/lib.rs diff --git a/crates/ra_editor/src/line_index.rs b/crates/ra_ide_api_light/src/line_index.rs similarity index 100% rename from crates/ra_editor/src/line_index.rs rename to crates/ra_ide_api_light/src/line_index.rs diff --git a/crates/ra_editor/src/line_index_utils.rs b/crates/ra_ide_api_light/src/line_index_utils.rs similarity index 100% rename from crates/ra_editor/src/line_index_utils.rs rename to crates/ra_ide_api_light/src/line_index_utils.rs diff --git a/crates/ra_editor/src/structure.rs b/crates/ra_ide_api_light/src/structure.rs similarity index 100% rename from crates/ra_editor/src/structure.rs rename to crates/ra_ide_api_light/src/structure.rs diff --git a/crates/ra_editor/src/test_utils.rs b/crates/ra_ide_api_light/src/test_utils.rs similarity index 100% rename from crates/ra_editor/src/test_utils.rs rename to crates/ra_ide_api_light/src/test_utils.rs diff --git a/crates/ra_editor/src/typing.rs b/crates/ra_ide_api_light/src/typing.rs similarity index 100% rename from crates/ra_editor/src/typing.rs rename to crates/ra_ide_api_light/src/typing.rs From fa3c9ce3921b6a3f67222bf4f9b4efdf4f11c2a5 Mon Sep 17 00:00:00 2001 From: Aleksey Kladov Date: Tue, 8 Jan 2019 22:30:32 +0300 Subject: [PATCH 2/5] fix usages after rename --- Cargo.lock | 58 +++++++++++-------- crates/ra_analysis/Cargo.toml | 2 +- crates/ra_analysis/src/extend_selection.rs | 4 +- crates/ra_analysis/src/imp.rs | 4 +- crates/ra_analysis/src/lib.rs | 21 ++++--- crates/ra_analysis/src/syntax_highlighting.rs | 7 +-- crates/ra_cli/Cargo.toml | 2 +- crates/ra_cli/src/main.rs | 2 +- crates/ra_ide_api_light/Cargo.toml | 2 +- crates/ra_ide_api_light/src/lib.rs | 5 ++ 10 files changed, 62 insertions(+), 45 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index f1d8e5462c29..f99a0342432e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -171,7 +171,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "crossbeam-utils 0.6.3 (registry+https://github.com/rust-lang/crates.io-index)", "parking_lot 0.7.1 (registry+https://github.com/rust-lang/crates.io-index)", - "rand 0.6.3 (registry+https://github.com/rust-lang/crates.io-index)", + "rand 0.6.4 (registry+https://github.com/rust-lang/crates.io-index)", "smallvec 0.6.7 (registry+https://github.com/rust-lang/crates.io-index)", ] @@ -548,7 +548,7 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "libc 0.2.46 (registry+https://github.com/rust-lang/crates.io-index)", - "rand 0.6.3 (registry+https://github.com/rust-lang/crates.io-index)", + "rand 0.6.4 (registry+https://github.com/rust-lang/crates.io-index)", "rustc_version 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)", "smallvec 0.6.7 (registry+https://github.com/rust-lang/crates.io-index)", "winapi 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)", @@ -645,8 +645,8 @@ dependencies = [ "log 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)", "parking_lot 0.7.1 (registry+https://github.com/rust-lang/crates.io-index)", "ra_db 0.1.0", - "ra_editor 0.1.0", "ra_hir 0.1.0", + "ra_ide_api_light 0.1.0", "ra_syntax 0.1.0", "ra_text_edit 0.1.0", "rayon 1.0.3 (registry+https://github.com/rust-lang/crates.io-index)", @@ -668,7 +668,7 @@ dependencies = [ "clap 2.32.0 (registry+https://github.com/rust-lang/crates.io-index)", "failure 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)", "join_to_string 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", - "ra_editor 0.1.0", + "ra_ide_api_light 0.1.0", "ra_syntax 0.1.0", "tools 0.1.0", ] @@ -686,20 +686,6 @@ dependencies = [ "test_utils 0.1.0", ] -[[package]] -name = "ra_editor" -version = "0.1.0" -dependencies = [ - "itertools 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)", - "join_to_string 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", - "proptest 0.8.7 (registry+https://github.com/rust-lang/crates.io-index)", - "ra_syntax 0.1.0", - "ra_text_edit 0.1.0", - "rustc-hash 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)", - "superslice 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)", - "test_utils 0.1.0", -] - [[package]] name = "ra_hir" version = "0.1.0" @@ -718,6 +704,20 @@ dependencies = [ "test_utils 0.1.0", ] +[[package]] +name = "ra_ide_api_light" +version = "0.1.0" +dependencies = [ + "itertools 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)", + "join_to_string 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", + "proptest 0.8.7 (registry+https://github.com/rust-lang/crates.io-index)", + "ra_syntax 0.1.0", + "ra_text_edit 0.1.0", + "rustc-hash 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)", + "superslice 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)", + "test_utils 0.1.0", +] + [[package]] name = "ra_lsp_server" version = "0.1.0" @@ -806,7 +806,7 @@ dependencies = [ [[package]] name = "rand" -version = "0.6.3" +version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "autocfg 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", @@ -815,7 +815,7 @@ dependencies = [ "rand_core 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)", "rand_hc 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)", "rand_isaac 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", - "rand_os 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)", + "rand_os 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", "rand_pcg 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", "rand_xorshift 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", "winapi 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)", @@ -861,13 +861,14 @@ dependencies = [ [[package]] name = "rand_os" -version = "0.1.0" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "cloudabi 0.0.3 (registry+https://github.com/rust-lang/crates.io-index)", "fuchsia-zircon 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)", "libc 0.2.46 (registry+https://github.com/rust-lang/crates.io-index)", "rand_core 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)", + "rdrand 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", "winapi 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)", ] @@ -909,6 +910,14 @@ dependencies = [ "num_cpus 1.9.0 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "rdrand" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "rand_core 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "redox_syscall" version = "0.1.50" @@ -1164,7 +1173,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "cfg-if 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)", "libc 0.2.46 (registry+https://github.com/rust-lang/crates.io-index)", - "rand 0.6.3 (registry+https://github.com/rust-lang/crates.io-index)", + "rand 0.6.4 (registry+https://github.com/rust-lang/crates.io-index)", "redox_syscall 0.1.50 (registry+https://github.com/rust-lang/crates.io-index)", "remove_dir_all 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)", "winapi 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)", @@ -1551,17 +1560,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index" "checksum quick-error 1.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "9274b940887ce9addde99c4eee6b5c44cc494b182b97e73dc8ffdcb3397fd3f0" "checksum quote 0.6.10 (registry+https://github.com/rust-lang/crates.io-index)" = "53fa22a1994bd0f9372d7a816207d8a2677ad0325b073f5c5332760f0fb62b5c" "checksum rand 0.5.5 (registry+https://github.com/rust-lang/crates.io-index)" = "e464cd887e869cddcae8792a4ee31d23c7edd516700695608f5b98c67ee0131c" -"checksum rand 0.6.3 (registry+https://github.com/rust-lang/crates.io-index)" = "b65e163105a6284f841bd23100a015895f54340e88a5ffc9ca7b8b33827cfce0" +"checksum rand 0.6.4 (registry+https://github.com/rust-lang/crates.io-index)" = "3906503e80ac6cbcacb2c2973fa8e473f24d7e2747c8c92bb230c2441cad96b5" "checksum rand_chacha 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "556d3a1ca6600bfcbab7c7c91ccb085ac7fbbcd70e008a98742e7847f4f7bcef" "checksum rand_core 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "1961a422c4d189dfb50ffa9320bf1f2a9bd54ecb92792fb9477f99a1045f3372" "checksum rand_core 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "0905b6b7079ec73b314d4c748701f6931eb79fd97c668caa3f1899b22b32c6db" "checksum rand_hc 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "7b40677c7be09ae76218dc623efbf7b18e34bced3f38883af07bb75630a21bc4" "checksum rand_isaac 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "ded997c9d5f13925be2a6fd7e66bf1872597f759fd9dd93513dd7e92e5a5ee08" -"checksum rand_os 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "de5ac4de1c2973e1391dc305cb0fbf8788cb58068e98255439b7485a77022273" +"checksum rand_os 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "f46fbd5550acf75b0c2730f5dd1873751daf9beb8f11b44027778fae50d7feca" "checksum rand_pcg 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "086bd09a33c7044e56bb44d5bdde5a60e7f119a9e95b0775f545de759a32fe05" "checksum rand_xorshift 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "cbf7e9e623549b0e21f6e97cf8ecf247c1a8fd2e8a992ae265314300b2455d5c" "checksum rayon 1.0.3 (registry+https://github.com/rust-lang/crates.io-index)" = "373814f27745b2686b350dd261bfd24576a6fb0e2c5919b3a2b6005f820b0473" "checksum rayon-core 1.4.1 (registry+https://github.com/rust-lang/crates.io-index)" = "b055d1e92aba6877574d8fe604a63c8b5df60f60e5982bf7ccbb1338ea527356" +"checksum rdrand 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "678054eb77286b51581ba43620cc911abf02758c91f93f479767aed0f90458b2" "checksum redox_syscall 0.1.50 (registry+https://github.com/rust-lang/crates.io-index)" = "52ee9a534dc1301776eff45b4fa92d2c39b1d8c3d3357e6eb593e0d795506fc2" "checksum redox_termios 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "7e891cfe48e9100a70a3b6eb652fef28920c117d366339687bd5576160db0f76" "checksum regex 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "37e7cbbd370869ce2e8dff25c7018702d10b21a20ef7135316f8daecd6c25b7f" diff --git a/crates/ra_analysis/Cargo.toml b/crates/ra_analysis/Cargo.toml index 11c78ced82c4..3c08142795c3 100644 --- a/crates/ra_analysis/Cargo.toml +++ b/crates/ra_analysis/Cargo.toml @@ -16,7 +16,7 @@ parking_lot = "0.7.0" unicase = "2.2.0" ra_syntax = { path = "../ra_syntax" } -ra_editor = { path = "../ra_editor" } +ra_ide_api_light = { path = "../ra_ide_api_light" } ra_text_edit = { path = "../ra_text_edit" } ra_db = { path = "../ra_db" } hir = { path = "../ra_hir", package = "ra_hir" } diff --git a/crates/ra_analysis/src/extend_selection.rs b/crates/ra_analysis/src/extend_selection.rs index 3b130f96648d..c3c809c9fe33 100644 --- a/crates/ra_analysis/src/extend_selection.rs +++ b/crates/ra_analysis/src/extend_selection.rs @@ -14,7 +14,7 @@ pub(crate) fn extend_selection(db: &RootDatabase, frange: FileRange) -> TextRang if let Some(range) = extend_selection_in_macro(db, &source_file, frange) { return range; } - ra_editor::extend_selection(source_file.syntax(), frange.range).unwrap_or(frange.range) + ra_ide_api_light::extend_selection(source_file.syntax(), frange.range).unwrap_or(frange.range) } fn extend_selection_in_macro( @@ -25,7 +25,7 @@ fn extend_selection_in_macro( let macro_call = find_macro_call(source_file.syntax(), frange.range)?; let (off, exp) = hir::MacroDef::ast_expand(macro_call)?; let dst_range = exp.map_range_forward(frange.range - off)?; - let dst_range = ra_editor::extend_selection(&exp.syntax(), dst_range)?; + let dst_range = ra_ide_api_light::extend_selection(&exp.syntax(), dst_range)?; let src_range = exp.map_range_back(dst_range)? + off; Some(src_range) } diff --git a/crates/ra_analysis/src/imp.rs b/crates/ra_analysis/src/imp.rs index 2b9963b3c23d..7c60ab7d6f4e 100644 --- a/crates/ra_analysis/src/imp.rs +++ b/crates/ra_analysis/src/imp.rs @@ -6,7 +6,7 @@ use hir::{ self, Problem, source_binder, }; use ra_db::{FilesDatabase, SourceRoot, SourceRootId, SyntaxDatabase}; -use ra_editor::{self, assists, LocalEdit, Severity}; +use ra_ide_api_light::{self, assists, LocalEdit, Severity}; use ra_syntax::{ TextRange, AstNode, SourceFile, ast::{self, NameOwner}, @@ -194,7 +194,7 @@ impl db::RootDatabase { pub(crate) fn diagnostics(&self, file_id: FileId) -> Cancelable> { let syntax = self.source_file(file_id); - let mut res = ra_editor::diagnostics(&syntax) + let mut res = ra_ide_api_light::diagnostics(&syntax) .into_iter() .map(|d| Diagnostic { range: d.range, diff --git a/crates/ra_analysis/src/lib.rs b/crates/ra_analysis/src/lib.rs index 48df08416b0d..183e3670691e 100644 --- a/crates/ra_analysis/src/lib.rs +++ b/crates/ra_analysis/src/lib.rs @@ -44,7 +44,7 @@ pub use crate::{ completion::{CompletionItem, CompletionItemKind, InsertText}, runnables::{Runnable, RunnableKind}, }; -pub use ra_editor::{ +pub use ra_ide_api_light::{ Fold, FoldKind, HighlightedRange, Severity, StructureNode, LineIndex, LineCol, translate_offset_with_edit, }; @@ -336,25 +336,28 @@ impl Analysis { /// Returns position of the mathcing brace (all types of braces are /// supported). pub fn matching_brace(&self, file: &SourceFile, offset: TextUnit) -> Option { - ra_editor::matching_brace(file, offset) + ra_ide_api_light::matching_brace(file, offset) } /// Returns a syntax tree represented as `String`, for debug purposes. // FIXME: use a better name here. pub fn syntax_tree(&self, file_id: FileId) -> String { let file = self.db.source_file(file_id); - ra_editor::syntax_tree(&file) + ra_ide_api_light::syntax_tree(&file) } /// Returns an edit to remove all newlines in the range, cleaning up minor /// stuff like trailing commas. pub fn join_lines(&self, frange: FileRange) -> SourceChange { let file = self.db.source_file(frange.file_id); - SourceChange::from_local_edit(frange.file_id, ra_editor::join_lines(&file, frange.range)) + SourceChange::from_local_edit( + frange.file_id, + ra_ide_api_light::join_lines(&file, frange.range), + ) } /// Returns an edit which should be applied when opening a new line, fixing /// up minor stuff like continuing the comment. pub fn on_enter(&self, position: FilePosition) -> Option { let file = self.db.source_file(position.file_id); - let edit = ra_editor::on_enter(&file, position.offset)?; + let edit = ra_ide_api_light::on_enter(&file, position.offset)?; Some(SourceChange::from_local_edit(position.file_id, edit)) } /// Returns an edit which should be applied after `=` was typed. Primarily, @@ -362,25 +365,25 @@ impl Analysis { // FIXME: use a snippet completion instead of this hack here. pub fn on_eq_typed(&self, position: FilePosition) -> Option { let file = self.db.source_file(position.file_id); - let edit = ra_editor::on_eq_typed(&file, position.offset)?; + let edit = ra_ide_api_light::on_eq_typed(&file, position.offset)?; Some(SourceChange::from_local_edit(position.file_id, edit)) } /// Returns an edit which should be applied when a dot ('.') is typed on a blank line, indenting the line appropriately. pub fn on_dot_typed(&self, position: FilePosition) -> Option { let file = self.db.source_file(position.file_id); - let edit = ra_editor::on_dot_typed(&file, position.offset)?; + let edit = ra_ide_api_light::on_dot_typed(&file, position.offset)?; Some(SourceChange::from_local_edit(position.file_id, edit)) } /// Returns a tree representation of symbols in the file. Useful to draw a /// file outline. pub fn file_structure(&self, file_id: FileId) -> Vec { let file = self.db.source_file(file_id); - ra_editor::file_structure(&file) + ra_ide_api_light::file_structure(&file) } /// Returns the set of folding ranges. pub fn folding_ranges(&self, file_id: FileId) -> Vec { let file = self.db.source_file(file_id); - ra_editor::folding_ranges(&file) + ra_ide_api_light::folding_ranges(&file) } /// Fuzzy searches for a symbol. pub fn symbol_search(&self, query: Query) -> Cancelable> { diff --git a/crates/ra_analysis/src/syntax_highlighting.rs b/crates/ra_analysis/src/syntax_highlighting.rs index d2dc6cfbb9b4..cb19e9515a14 100644 --- a/crates/ra_analysis/src/syntax_highlighting.rs +++ b/crates/ra_analysis/src/syntax_highlighting.rs @@ -1,22 +1,21 @@ use ra_syntax::{ast, AstNode,}; -use ra_editor::HighlightedRange; use ra_db::SyntaxDatabase; use crate::{ + FileId, Cancelable, HighlightedRange, db::RootDatabase, - FileId, Cancelable, }; pub(crate) fn highlight(db: &RootDatabase, file_id: FileId) -> Cancelable> { let source_file = db.source_file(file_id); - let mut res = ra_editor::highlight(source_file.syntax()); + let mut res = ra_ide_api_light::highlight(source_file.syntax()); for macro_call in source_file .syntax() .descendants() .filter_map(ast::MacroCall::cast) { if let Some((off, exp)) = hir::MacroDef::ast_expand(macro_call) { - let mapped_ranges = ra_editor::highlight(&exp.syntax()) + let mapped_ranges = ra_ide_api_light::highlight(&exp.syntax()) .into_iter() .filter_map(|r| { let mapped_range = exp.map_range_back(r.range)?; diff --git a/crates/ra_cli/Cargo.toml b/crates/ra_cli/Cargo.toml index 83f1d91e0b82..eb1722d5e77e 100644 --- a/crates/ra_cli/Cargo.toml +++ b/crates/ra_cli/Cargo.toml @@ -10,5 +10,5 @@ clap = "2.32.0" failure = "0.1.4" join_to_string = "0.1.1" ra_syntax = { path = "../ra_syntax" } -ra_editor = { path = "../ra_editor" } +ra_ide_api_light = { path = "../ra_ide_api_light" } tools = { path = "../tools" } diff --git a/crates/ra_cli/src/main.rs b/crates/ra_cli/src/main.rs index 0d12f3a88a82..43fb2fc4ce03 100644 --- a/crates/ra_cli/src/main.rs +++ b/crates/ra_cli/src/main.rs @@ -2,7 +2,7 @@ use std::{fs, io::Read, path::Path, time::Instant}; use clap::{App, Arg, SubCommand}; use join_to_string::join; -use ra_editor::{extend_selection, file_structure, syntax_tree}; +use ra_ide_api_light::{extend_selection, file_structure, syntax_tree}; use ra_syntax::{SourceFile, TextRange, TreePtr, AstNode}; use tools::collect_tests; diff --git a/crates/ra_ide_api_light/Cargo.toml b/crates/ra_ide_api_light/Cargo.toml index a97d2308f2b7..8c192fca6c02 100644 --- a/crates/ra_ide_api_light/Cargo.toml +++ b/crates/ra_ide_api_light/Cargo.toml @@ -1,6 +1,6 @@ [package] edition = "2018" -name = "ra_editor" +name = "ra_ide_api_light" version = "0.1.0" authors = ["Aleksey Kladov "] publish = false diff --git a/crates/ra_ide_api_light/src/lib.rs b/crates/ra_ide_api_light/src/lib.rs index 5a6af19b7cf9..40638eda8e3d 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; From 6bca91af532d79abbced5b151cb4188ff8625c04 Mon Sep 17 00:00:00 2001 From: Aleksey Kladov Date: Tue, 8 Jan 2019 22:30:56 +0300 Subject: [PATCH 3/5] rename ra_analysis -> ra_ide_api --- crates/ra_analysis/Cargo.toml | 23 - crates/ra_analysis/src/call_info.rs | 451 ---------------- crates/ra_analysis/src/completion.rs | 77 --- .../src/completion/complete_dot.rs | 121 ----- .../src/completion/complete_fn_param.rs | 102 ---- .../src/completion/complete_keyword.rs | 339 ------------ .../src/completion/complete_path.rs | 128 ----- .../src/completion/complete_scope.rs | 192 ------- .../src/completion/complete_snippet.rs | 73 --- .../src/completion/completion_context.rs | 205 ------- .../src/completion/completion_item.rs | 244 --------- crates/ra_analysis/src/db.rs | 128 ----- crates/ra_analysis/src/extend_selection.rs | 56 -- crates/ra_analysis/src/goto_defenition.rs | 139 ----- crates/ra_analysis/src/hover.rs | 257 --------- crates/ra_analysis/src/imp.rs | 309 ----------- crates/ra_analysis/src/lib.rs | 509 ------------------ crates/ra_analysis/src/mock_analysis.rs | 135 ----- crates/ra_analysis/src/runnables.rs | 89 --- crates/ra_analysis/src/symbol_index.rs | 222 -------- crates/ra_analysis/src/syntax_highlighting.rs | 92 ---- crates/ra_analysis/tests/test/main.rs | 249 --------- crates/ra_analysis/tests/test/runnables.rs | 109 ---- 23 files changed, 4249 deletions(-) delete mode 100644 crates/ra_analysis/Cargo.toml delete mode 100644 crates/ra_analysis/src/call_info.rs delete mode 100644 crates/ra_analysis/src/completion.rs delete mode 100644 crates/ra_analysis/src/completion/complete_dot.rs delete mode 100644 crates/ra_analysis/src/completion/complete_fn_param.rs delete mode 100644 crates/ra_analysis/src/completion/complete_keyword.rs delete mode 100644 crates/ra_analysis/src/completion/complete_path.rs delete mode 100644 crates/ra_analysis/src/completion/complete_scope.rs delete mode 100644 crates/ra_analysis/src/completion/complete_snippet.rs delete mode 100644 crates/ra_analysis/src/completion/completion_context.rs delete mode 100644 crates/ra_analysis/src/completion/completion_item.rs delete mode 100644 crates/ra_analysis/src/db.rs delete mode 100644 crates/ra_analysis/src/extend_selection.rs delete mode 100644 crates/ra_analysis/src/goto_defenition.rs delete mode 100644 crates/ra_analysis/src/hover.rs delete mode 100644 crates/ra_analysis/src/imp.rs delete mode 100644 crates/ra_analysis/src/lib.rs delete mode 100644 crates/ra_analysis/src/mock_analysis.rs delete mode 100644 crates/ra_analysis/src/runnables.rs delete mode 100644 crates/ra_analysis/src/symbol_index.rs delete mode 100644 crates/ra_analysis/src/syntax_highlighting.rs delete mode 100644 crates/ra_analysis/tests/test/main.rs delete mode 100644 crates/ra_analysis/tests/test/runnables.rs diff --git a/crates/ra_analysis/Cargo.toml b/crates/ra_analysis/Cargo.toml deleted file mode 100644 index 3c08142795c3..000000000000 --- a/crates/ra_analysis/Cargo.toml +++ /dev/null @@ -1,23 +0,0 @@ -[package] -edition = "2018" -name = "ra_analysis" -version = "0.1.0" -authors = ["Aleksey Kladov "] - -[dependencies] -itertools = "0.8.0" -log = "0.4.5" -relative-path = "0.4.0" -rayon = "1.0.2" -fst = "0.3.1" -salsa = "0.9.1" -rustc-hash = "1.0" -parking_lot = "0.7.0" -unicase = "2.2.0" - -ra_syntax = { path = "../ra_syntax" } -ra_ide_api_light = { path = "../ra_ide_api_light" } -ra_text_edit = { path = "../ra_text_edit" } -ra_db = { path = "../ra_db" } -hir = { path = "../ra_hir", package = "ra_hir" } -test_utils = { path = "../test_utils" } diff --git a/crates/ra_analysis/src/call_info.rs b/crates/ra_analysis/src/call_info.rs deleted file mode 100644 index 27b760780ca8..000000000000 --- a/crates/ra_analysis/src/call_info.rs +++ /dev/null @@ -1,451 +0,0 @@ -use std::cmp::{max, min}; - -use ra_db::{SyntaxDatabase, Cancelable}; -use ra_syntax::{ - AstNode, SyntaxNode, TextUnit, TextRange, - SyntaxKind::FN_DEF, - ast::{self, ArgListOwner, DocCommentsOwner}, - algo::find_node_at_offset, -}; - -use crate::{FilePosition, CallInfo, db::RootDatabase}; - -/// Computes parameter information for the given call expression. -pub(crate) fn call_info(db: &RootDatabase, position: FilePosition) -> Cancelable> { - let file = db.source_file(position.file_id); - let syntax = file.syntax(); - - // Find the calling expression and it's NameRef - let calling_node = ctry!(FnCallNode::with_node(syntax, position.offset)); - let name_ref = ctry!(calling_node.name_ref()); - - // Resolve the function's NameRef (NOTE: this isn't entirely accurate). - let file_symbols = db.index_resolve(name_ref)?; - let symbol = ctry!(file_symbols.into_iter().find(|it| it.ptr.kind() == FN_DEF)); - let fn_file = db.source_file(symbol.file_id); - let fn_def = symbol.ptr.resolve(&fn_file); - let fn_def = ast::FnDef::cast(&fn_def).unwrap(); - let mut call_info = ctry!(CallInfo::new(fn_def)); - // If we have a calling expression let's find which argument we are on - let num_params = call_info.parameters.len(); - let has_self = fn_def.param_list().and_then(|l| l.self_param()).is_some(); - - if num_params == 1 { - if !has_self { - call_info.active_parameter = Some(0); - } - } else if num_params > 1 { - // Count how many parameters into the call we are. - // TODO: This is best effort for now and should be fixed at some point. - // It may be better to see where we are in the arg_list and then check - // where offset is in that list (or beyond). - // Revisit this after we get documentation comments in. - if let Some(ref arg_list) = calling_node.arg_list() { - let start = arg_list.syntax().range().start(); - - let range_search = TextRange::from_to(start, position.offset); - let mut commas: usize = arg_list - .syntax() - .text() - .slice(range_search) - .to_string() - .matches(',') - .count(); - - // If we have a method call eat the first param since it's just self. - if has_self { - commas += 1; - } - - call_info.active_parameter = Some(commas); - } - } - - Ok(Some(call_info)) -} - -enum FnCallNode<'a> { - CallExpr(&'a ast::CallExpr), - MethodCallExpr(&'a ast::MethodCallExpr), -} - -impl<'a> FnCallNode<'a> { - pub fn with_node(syntax: &'a SyntaxNode, offset: TextUnit) -> Option> { - if let Some(expr) = find_node_at_offset::(syntax, offset) { - return Some(FnCallNode::CallExpr(expr)); - } - if let Some(expr) = find_node_at_offset::(syntax, offset) { - return Some(FnCallNode::MethodCallExpr(expr)); - } - None - } - - pub fn name_ref(&self) -> Option<&'a ast::NameRef> { - match *self { - FnCallNode::CallExpr(call_expr) => Some(match call_expr.expr()?.kind() { - ast::ExprKind::PathExpr(path_expr) => path_expr.path()?.segment()?.name_ref()?, - _ => return None, - }), - - FnCallNode::MethodCallExpr(call_expr) => call_expr - .syntax() - .children() - .filter_map(ast::NameRef::cast) - .nth(0), - } - } - - pub fn arg_list(&self) -> Option<&'a ast::ArgList> { - match *self { - FnCallNode::CallExpr(expr) => expr.arg_list(), - FnCallNode::MethodCallExpr(expr) => expr.arg_list(), - } - } -} - -impl CallInfo { - fn new(node: &ast::FnDef) -> Option { - let mut doc = None; - - // Strip the body out for the label. - let mut label: String = if let Some(body) = node.body() { - let body_range = body.syntax().range(); - let label: String = node - .syntax() - .children() - .filter(|child| !child.range().is_subrange(&body_range)) - .map(|node| node.text().to_string()) - .collect(); - label - } else { - node.syntax().text().to_string() - }; - - if let Some((comment_range, docs)) = extract_doc_comments(node) { - let comment_range = comment_range - .checked_sub(node.syntax().range().start()) - .unwrap(); - let start = comment_range.start().to_usize(); - let end = comment_range.end().to_usize(); - - // Remove the comment from the label - label.replace_range(start..end, ""); - - // Massage markdown - let mut processed_lines = Vec::new(); - let mut in_code_block = false; - for line in docs.lines() { - if line.starts_with("```") { - in_code_block = !in_code_block; - } - - let line = if in_code_block && line.starts_with("```") && !line.contains("rust") { - "```rust".into() - } else { - line.to_string() - }; - - processed_lines.push(line); - } - - if !processed_lines.is_empty() { - doc = Some(processed_lines.join("\n")); - } - } - - Some(CallInfo { - parameters: param_list(node), - label: label.trim().to_owned(), - doc, - active_parameter: None, - }) - } -} - -fn extract_doc_comments(node: &ast::FnDef) -> Option<(TextRange, String)> { - if node.doc_comments().count() == 0 { - return None; - } - - let comment_text = node.doc_comment_text(); - - let (begin, end) = node - .doc_comments() - .map(|comment| comment.syntax().range()) - .map(|range| (range.start().to_usize(), range.end().to_usize())) - .fold((std::usize::MAX, std::usize::MIN), |acc, range| { - (min(acc.0, range.0), max(acc.1, range.1)) - }); - - let range = TextRange::from_to(TextUnit::from_usize(begin), TextUnit::from_usize(end)); - - Some((range, comment_text)) -} - -fn param_list(node: &ast::FnDef) -> Vec { - let mut res = vec![]; - if let Some(param_list) = node.param_list() { - if let Some(self_param) = param_list.self_param() { - res.push(self_param.syntax().text().to_string()) - } - - // Maybe use param.pat here? See if we can just extract the name? - //res.extend(param_list.params().map(|p| p.syntax().text().to_string())); - res.extend( - param_list - .params() - .filter_map(|p| p.pat()) - .map(|pat| pat.syntax().text().to_string()), - ); - } - res -} - -#[cfg(test)] -mod tests { - use super::*; - - use crate::mock_analysis::single_file_with_position; - - fn call_info(text: &str) -> CallInfo { - let (analysis, position) = single_file_with_position(text); - analysis.call_info(position).unwrap().unwrap() - } - - #[test] - fn test_fn_signature_two_args_first() { - let info = call_info( - r#"fn foo(x: u32, y: u32) -> u32 {x + y} -fn bar() { foo(<|>3, ); }"#, - ); - - assert_eq!(info.parameters, vec!("x".to_string(), "y".to_string())); - assert_eq!(info.active_parameter, Some(0)); - } - - #[test] - fn test_fn_signature_two_args_second() { - let info = call_info( - r#"fn foo(x: u32, y: u32) -> u32 {x + y} -fn bar() { foo(3, <|>); }"#, - ); - - assert_eq!(info.parameters, vec!("x".to_string(), "y".to_string())); - assert_eq!(info.active_parameter, Some(1)); - } - - #[test] - fn test_fn_signature_for_impl() { - let info = call_info( - r#"struct F; impl F { pub fn new() { F{}} } -fn bar() {let _ : F = F::new(<|>);}"#, - ); - - assert_eq!(info.parameters, Vec::::new()); - assert_eq!(info.active_parameter, None); - } - - #[test] - fn test_fn_signature_for_method_self() { - let info = call_info( - r#"struct F; -impl F { - pub fn new() -> F{ - F{} - } - - pub fn do_it(&self) {} -} - -fn bar() { - let f : F = F::new(); - f.do_it(<|>); -}"#, - ); - - assert_eq!(info.parameters, vec!["&self".to_string()]); - assert_eq!(info.active_parameter, None); - } - - #[test] - fn test_fn_signature_for_method_with_arg() { - let info = call_info( - r#"struct F; -impl F { - pub fn new() -> F{ - F{} - } - - pub fn do_it(&self, x: i32) {} -} - -fn bar() { - let f : F = F::new(); - f.do_it(<|>); -}"#, - ); - - assert_eq!(info.parameters, vec!["&self".to_string(), "x".to_string()]); - assert_eq!(info.active_parameter, Some(1)); - } - - #[test] - fn test_fn_signature_with_docs_simple() { - let info = call_info( - r#" -/// test -// non-doc-comment -fn foo(j: u32) -> u32 { - j -} - -fn bar() { - let _ = foo(<|>); -} -"#, - ); - - assert_eq!(info.parameters, vec!["j".to_string()]); - assert_eq!(info.active_parameter, Some(0)); - assert_eq!(info.label, "fn foo(j: u32) -> u32".to_string()); - assert_eq!(info.doc, Some("test".into())); - } - - #[test] - fn test_fn_signature_with_docs() { - let info = call_info( - r#" -/// Adds one to the number given. -/// -/// # Examples -/// -/// ``` -/// let five = 5; -/// -/// assert_eq!(6, my_crate::add_one(5)); -/// ``` -pub fn add_one(x: i32) -> i32 { - x + 1 -} - -pub fn do() { - add_one(<|> -}"#, - ); - - assert_eq!(info.parameters, vec!["x".to_string()]); - assert_eq!(info.active_parameter, Some(0)); - assert_eq!(info.label, "pub fn add_one(x: i32) -> i32".to_string()); - assert_eq!( - info.doc, - Some( - r#"Adds one to the number given. - -# Examples - -```rust -let five = 5; - -assert_eq!(6, my_crate::add_one(5)); -```"# - .into() - ) - ); - } - - #[test] - fn test_fn_signature_with_docs_impl() { - let info = call_info( - r#" -struct addr; -impl addr { - /// Adds one to the number given. - /// - /// # Examples - /// - /// ``` - /// let five = 5; - /// - /// assert_eq!(6, my_crate::add_one(5)); - /// ``` - pub fn add_one(x: i32) -> i32 { - x + 1 - } -} - -pub fn do_it() { - addr {}; - addr::add_one(<|>); -}"#, - ); - - assert_eq!(info.parameters, vec!["x".to_string()]); - assert_eq!(info.active_parameter, Some(0)); - assert_eq!(info.label, "pub fn add_one(x: i32) -> i32".to_string()); - assert_eq!( - info.doc, - Some( - r#"Adds one to the number given. - -# Examples - -```rust -let five = 5; - -assert_eq!(6, my_crate::add_one(5)); -```"# - .into() - ) - ); - } - - #[test] - fn test_fn_signature_with_docs_from_actix() { - let info = call_info( - r#" -pub trait WriteHandler -where - Self: Actor, - Self::Context: ActorContext, -{ - /// Method is called when writer emits error. - /// - /// If this method returns `ErrorAction::Continue` writer processing - /// continues otherwise stream processing stops. - fn error(&mut self, err: E, ctx: &mut Self::Context) -> Running { - Running::Stop - } - - /// Method is called when writer finishes. - /// - /// By default this method stops actor's `Context`. - fn finished(&mut self, ctx: &mut Self::Context) { - ctx.stop() - } -} - -pub fn foo() { - WriteHandler r; - r.finished(<|>); -} - -"#, - ); - - assert_eq!( - info.parameters, - vec!["&mut self".to_string(), "ctx".to_string()] - ); - assert_eq!(info.active_parameter, Some(1)); - assert_eq!( - info.doc, - Some( - r#"Method is called when writer finishes. - -By default this method stops actor's `Context`."# - .into() - ) - ); - } - -} diff --git a/crates/ra_analysis/src/completion.rs b/crates/ra_analysis/src/completion.rs deleted file mode 100644 index ce777a771e2e..000000000000 --- a/crates/ra_analysis/src/completion.rs +++ /dev/null @@ -1,77 +0,0 @@ -mod completion_item; -mod completion_context; - -mod complete_dot; -mod complete_fn_param; -mod complete_keyword; -mod complete_snippet; -mod complete_path; -mod complete_scope; - -use ra_db::SyntaxDatabase; - -use crate::{ - db, - Cancelable, FilePosition, - completion::{ - completion_item::{Completions, CompletionKind}, - completion_context::CompletionContext, - }, -}; - -pub use crate::completion::completion_item::{CompletionItem, InsertText, CompletionItemKind}; - -/// Main entry point for completion. We run completion as a two-phase process. -/// -/// First, we look at the position and collect a so-called `CompletionContext. -/// This is a somewhat messy process, because, during completion, syntax tree is -/// incomplete and can look really weird. -/// -/// Once the context is collected, we run a series of completion routines which -/// look at the context and produce completion items. One subtelty about this -/// phase is that completion engine should not filter by the substring which is -/// already present, it should give all possible variants for the identifier at -/// the caret. In other words, for -/// -/// ```no-run -/// fn f() { -/// let foo = 92; -/// let _ = bar<|> -/// } -/// ``` -/// -/// `foo` *should* be present among the completion variants. Filtering by -/// identifier prefix/fuzzy match should be done higher in the stack, together -/// with ordering of completions (currently this is done by the client). -pub(crate) fn completions( - db: &db::RootDatabase, - position: FilePosition, -) -> Cancelable> { - let original_file = db.source_file(position.file_id); - let ctx = ctry!(CompletionContext::new(db, &original_file, position)?); - - let mut acc = Completions::default(); - - complete_fn_param::complete_fn_param(&mut acc, &ctx); - complete_keyword::complete_expr_keyword(&mut acc, &ctx); - complete_keyword::complete_use_tree_keyword(&mut acc, &ctx); - complete_snippet::complete_expr_snippet(&mut acc, &ctx); - complete_snippet::complete_item_snippet(&mut acc, &ctx); - complete_path::complete_path(&mut acc, &ctx)?; - complete_scope::complete_scope(&mut acc, &ctx)?; - complete_dot::complete_dot(&mut acc, &ctx)?; - - Ok(Some(acc)) -} - -#[cfg(test)] -fn check_completion(code: &str, expected_completions: &str, kind: CompletionKind) { - use crate::mock_analysis::{single_file_with_position, analysis_and_position}; - let (analysis, position) = if code.contains("//-") { - analysis_and_position(code) - } else { - single_file_with_position(code) - }; - let completions = completions(&analysis.db, position).unwrap().unwrap(); - completions.assert_match(expected_completions, kind); -} diff --git a/crates/ra_analysis/src/completion/complete_dot.rs b/crates/ra_analysis/src/completion/complete_dot.rs deleted file mode 100644 index 5d4e60dc525c..000000000000 --- a/crates/ra_analysis/src/completion/complete_dot.rs +++ /dev/null @@ -1,121 +0,0 @@ -use hir::{Ty, Def}; - -use crate::Cancelable; -use crate::completion::{CompletionContext, Completions, CompletionKind, CompletionItem, CompletionItemKind}; - -/// Complete dot accesses, i.e. fields or methods (currently only fields). -pub(super) fn complete_dot(acc: &mut Completions, ctx: &CompletionContext) -> Cancelable<()> { - let (function, receiver) = match (&ctx.function, ctx.dot_receiver) { - (Some(function), Some(receiver)) => (function, receiver), - _ => return Ok(()), - }; - let infer_result = function.infer(ctx.db)?; - let syntax_mapping = function.body_syntax_mapping(ctx.db)?; - let expr = match syntax_mapping.node_expr(receiver) { - Some(expr) => expr, - None => return Ok(()), - }; - let receiver_ty = infer_result[expr].clone(); - if !ctx.is_method_call { - complete_fields(acc, ctx, receiver_ty)?; - } - Ok(()) -} - -fn complete_fields(acc: &mut Completions, ctx: &CompletionContext, receiver: Ty) -> Cancelable<()> { - for receiver in receiver.autoderef(ctx.db) { - match receiver { - Ty::Adt { def_id, .. } => { - match def_id.resolve(ctx.db)? { - Def::Struct(s) => { - let variant_data = s.variant_data(ctx.db)?; - for field in variant_data.fields() { - CompletionItem::new( - CompletionKind::Reference, - field.name().to_string(), - ) - .kind(CompletionItemKind::Field) - .add_to(acc); - } - } - // TODO unions - _ => {} - } - } - Ty::Tuple(fields) => { - for (i, _ty) in fields.iter().enumerate() { - CompletionItem::new(CompletionKind::Reference, i.to_string()) - .kind(CompletionItemKind::Field) - .add_to(acc); - } - } - _ => {} - }; - } - Ok(()) -} - -#[cfg(test)] -mod tests { - use crate::completion::*; - - fn check_ref_completion(code: &str, expected_completions: &str) { - check_completion(code, expected_completions, CompletionKind::Reference); - } - - #[test] - fn test_struct_field_completion() { - check_ref_completion( - r" - struct A { the_field: u32 } - fn foo(a: A) { - a.<|> - } - ", - r#"the_field"#, - ); - } - - #[test] - fn test_struct_field_completion_self() { - check_ref_completion( - r" - struct A { the_field: u32 } - impl A { - fn foo(self) { - self.<|> - } - } - ", - r#"the_field"#, - ); - } - - #[test] - fn test_struct_field_completion_autoderef() { - check_ref_completion( - r" - struct A { the_field: u32 } - impl A { - fn foo(&self) { - self.<|> - } - } - ", - r#"the_field"#, - ); - } - - #[test] - fn test_no_struct_field_completion_for_method_call() { - check_ref_completion( - r" - struct A { the_field: u32 } - fn foo(a: A) { - a.<|>() - } - ", - r#""#, - ); - } -} diff --git a/crates/ra_analysis/src/completion/complete_fn_param.rs b/crates/ra_analysis/src/completion/complete_fn_param.rs deleted file mode 100644 index c1739e47eab5..000000000000 --- a/crates/ra_analysis/src/completion/complete_fn_param.rs +++ /dev/null @@ -1,102 +0,0 @@ -use ra_syntax::{ - algo::visit::{visitor_ctx, VisitorCtx}, - ast, - AstNode, -}; -use rustc_hash::FxHashMap; - -use crate::completion::{CompletionContext, Completions, CompletionKind, CompletionItem}; - -/// Complete repeated parametes, both name and type. For example, if all -/// functions in a file have a `spam: &mut Spam` parameter, a completion with -/// `spam: &mut Spam` insert text/label and `spam` lookup string will be -/// suggested. -pub(super) fn complete_fn_param(acc: &mut Completions, ctx: &CompletionContext) { - if !ctx.is_param { - return; - } - - let mut params = FxHashMap::default(); - for node in ctx.leaf.ancestors() { - let _ = visitor_ctx(&mut params) - .visit::(process) - .visit::(process) - .accept(node); - } - params - .into_iter() - .filter_map(|(label, (count, param))| { - let lookup = param.pat()?.syntax().text().to_string(); - if count < 2 { - None - } else { - Some((label, lookup)) - } - }) - .for_each(|(label, lookup)| { - CompletionItem::new(CompletionKind::Magic, label) - .lookup_by(lookup) - .add_to(acc) - }); - - fn process<'a, N: ast::FnDefOwner>( - node: &'a N, - params: &mut FxHashMap, - ) { - node.functions() - .filter_map(|it| it.param_list()) - .flat_map(|it| it.params()) - .for_each(|param| { - let text = param.syntax().text().to_string(); - params.entry(text).or_insert((0, param)).0 += 1; - }) - } -} - -#[cfg(test)] -mod tests { - use crate::completion::*; - - fn check_magic_completion(code: &str, expected_completions: &str) { - check_completion(code, expected_completions, CompletionKind::Magic); - } - - #[test] - fn test_param_completion_last_param() { - check_magic_completion( - r" - fn foo(file_id: FileId) {} - fn bar(file_id: FileId) {} - fn baz(file<|>) {} - ", - r#"file_id "file_id: FileId""#, - ); - } - - #[test] - fn test_param_completion_nth_param() { - check_magic_completion( - r" - fn foo(file_id: FileId) {} - fn bar(file_id: FileId) {} - fn baz(file<|>, x: i32) {} - ", - r#"file_id "file_id: FileId""#, - ); - } - - #[test] - fn test_param_completion_trait_param() { - check_magic_completion( - r" - pub(crate) trait SourceRoot { - pub fn contains(&self, file_id: FileId) -> bool; - pub fn module_map(&self) -> &ModuleMap; - pub fn lines(&self, file_id: FileId) -> &LineIndex; - pub fn syntax(&self, file<|>) - } - ", - r#"file_id "file_id: FileId""#, - ); - } -} diff --git a/crates/ra_analysis/src/completion/complete_keyword.rs b/crates/ra_analysis/src/completion/complete_keyword.rs deleted file mode 100644 index d350f06ceb60..000000000000 --- a/crates/ra_analysis/src/completion/complete_keyword.rs +++ /dev/null @@ -1,339 +0,0 @@ -use ra_syntax::{ - algo::visit::{visitor, Visitor}, - AstNode, - ast::{self, LoopBodyOwner}, - SyntaxKind::*, SyntaxNode, -}; - -use crate::completion::{CompletionContext, CompletionItem, Completions, CompletionKind, CompletionItemKind}; - -pub(super) fn complete_use_tree_keyword(acc: &mut Completions, ctx: &CompletionContext) { - // complete keyword "crate" in use stmt - match (ctx.use_item_syntax.as_ref(), ctx.path_prefix.as_ref()) { - (Some(_), None) => { - CompletionItem::new(CompletionKind::Keyword, "crate") - .kind(CompletionItemKind::Keyword) - .lookup_by("crate") - .snippet("crate::") - .add_to(acc); - CompletionItem::new(CompletionKind::Keyword, "self") - .kind(CompletionItemKind::Keyword) - .lookup_by("self") - .add_to(acc); - CompletionItem::new(CompletionKind::Keyword, "super") - .kind(CompletionItemKind::Keyword) - .lookup_by("super") - .add_to(acc); - } - (Some(_), Some(_)) => { - CompletionItem::new(CompletionKind::Keyword, "self") - .kind(CompletionItemKind::Keyword) - .lookup_by("self") - .add_to(acc); - CompletionItem::new(CompletionKind::Keyword, "super") - .kind(CompletionItemKind::Keyword) - .lookup_by("super") - .add_to(acc); - } - _ => {} - } -} - -fn keyword(kw: &str, snippet: &str) -> CompletionItem { - CompletionItem::new(CompletionKind::Keyword, kw) - .kind(CompletionItemKind::Keyword) - .snippet(snippet) - .build() -} - -pub(super) fn complete_expr_keyword(acc: &mut Completions, ctx: &CompletionContext) { - if !ctx.is_trivial_path { - return; - } - - let fn_def = match ctx.function_syntax { - Some(it) => it, - None => return, - }; - acc.add(keyword("if", "if $0 {}")); - acc.add(keyword("match", "match $0 {}")); - acc.add(keyword("while", "while $0 {}")); - acc.add(keyword("loop", "loop {$0}")); - - if ctx.after_if { - acc.add(keyword("else", "else {$0}")); - acc.add(keyword("else if", "else if $0 {}")); - } - if is_in_loop_body(ctx.leaf) { - if ctx.can_be_stmt { - acc.add(keyword("continue", "continue;")); - acc.add(keyword("break", "break;")); - } else { - acc.add(keyword("continue", "continue")); - acc.add(keyword("break", "break")); - } - } - acc.add_all(complete_return(fn_def, ctx.can_be_stmt)); -} - -fn is_in_loop_body(leaf: &SyntaxNode) -> bool { - for node in leaf.ancestors() { - if node.kind() == FN_DEF || node.kind() == LAMBDA_EXPR { - break; - } - let loop_body = visitor() - .visit::(LoopBodyOwner::loop_body) - .visit::(LoopBodyOwner::loop_body) - .visit::(LoopBodyOwner::loop_body) - .accept(node); - if let Some(Some(body)) = loop_body { - if leaf.range().is_subrange(&body.syntax().range()) { - return true; - } - } - } - false -} - -fn complete_return(fn_def: &ast::FnDef, can_be_stmt: bool) -> Option { - let snip = match (can_be_stmt, fn_def.ret_type().is_some()) { - (true, true) => "return $0;", - (true, false) => "return;", - (false, true) => "return $0", - (false, false) => "return", - }; - Some(keyword("return", snip)) -} - -#[cfg(test)] -mod tests { - use crate::completion::{CompletionKind, check_completion}; - fn check_keyword_completion(code: &str, expected_completions: &str) { - check_completion(code, expected_completions, CompletionKind::Keyword); - } - - #[test] - fn completes_keywords_in_use_stmt() { - check_keyword_completion( - r" - use <|> - ", - r#" - crate "crate" "crate::" - self "self" - super "super" - "#, - ); - - check_keyword_completion( - r" - use a::<|> - ", - r#" - self "self" - super "super" - "#, - ); - - check_keyword_completion( - r" - use a::{b, <|>} - ", - r#" - self "self" - super "super" - "#, - ); - } - - #[test] - fn completes_various_keywords_in_function() { - check_keyword_completion( - r" - fn quux() { - <|> - } - ", - r#" - if "if $0 {}" - match "match $0 {}" - while "while $0 {}" - loop "loop {$0}" - return "return;" - "#, - ); - } - - #[test] - fn completes_else_after_if() { - check_keyword_completion( - r" - fn quux() { - if true { - () - } <|> - } - ", - r#" - if "if $0 {}" - match "match $0 {}" - while "while $0 {}" - loop "loop {$0}" - else "else {$0}" - else if "else if $0 {}" - return "return;" - "#, - ); - } - - #[test] - fn test_completion_return_value() { - check_keyword_completion( - r" - fn quux() -> i32 { - <|> - 92 - } - ", - r#" - if "if $0 {}" - match "match $0 {}" - while "while $0 {}" - loop "loop {$0}" - return "return $0;" - "#, - ); - check_keyword_completion( - r" - fn quux() { - <|> - 92 - } - ", - r#" - if "if $0 {}" - match "match $0 {}" - while "while $0 {}" - loop "loop {$0}" - return "return;" - "#, - ); - } - - #[test] - fn dont_add_semi_after_return_if_not_a_statement() { - check_keyword_completion( - r" - fn quux() -> i32 { - match () { - () => <|> - } - } - ", - r#" - if "if $0 {}" - match "match $0 {}" - while "while $0 {}" - loop "loop {$0}" - return "return $0" - "#, - ); - } - - #[test] - fn last_return_in_block_has_semi() { - check_keyword_completion( - r" - fn quux() -> i32 { - if condition { - <|> - } - } - ", - r#" - if "if $0 {}" - match "match $0 {}" - while "while $0 {}" - loop "loop {$0}" - return "return $0;" - "#, - ); - check_keyword_completion( - r" - fn quux() -> i32 { - if condition { - <|> - } - let x = 92; - x - } - ", - r#" - if "if $0 {}" - match "match $0 {}" - while "while $0 {}" - loop "loop {$0}" - return "return $0;" - "#, - ); - } - - #[test] - fn completes_break_and_continue_in_loops() { - check_keyword_completion( - r" - fn quux() -> i32 { - loop { <|> } - } - ", - r#" - if "if $0 {}" - match "match $0 {}" - while "while $0 {}" - loop "loop {$0}" - continue "continue;" - break "break;" - return "return $0;" - "#, - ); - // No completion: lambda isolates control flow - check_keyword_completion( - r" - fn quux() -> i32 { - loop { || { <|> } } - } - ", - r#" - if "if $0 {}" - match "match $0 {}" - while "while $0 {}" - loop "loop {$0}" - return "return $0;" - "#, - ); - } - - #[test] - fn no_semi_after_break_continue_in_expr() { - check_keyword_completion( - r" - fn f() { - loop { - match () { - () => br<|> - } - } - } - ", - r#" - if "if $0 {}" - match "match $0 {}" - while "while $0 {}" - loop "loop {$0}" - continue "continue" - break "break" - return "return" - "#, - ) - } -} diff --git a/crates/ra_analysis/src/completion/complete_path.rs b/crates/ra_analysis/src/completion/complete_path.rs deleted file mode 100644 index 4723a65a6b06..000000000000 --- a/crates/ra_analysis/src/completion/complete_path.rs +++ /dev/null @@ -1,128 +0,0 @@ -use crate::{ - Cancelable, - completion::{CompletionItem, CompletionItemKind, Completions, CompletionKind, CompletionContext}, -}; - -pub(super) fn complete_path(acc: &mut Completions, ctx: &CompletionContext) -> Cancelable<()> { - let (path, module) = match (&ctx.path_prefix, &ctx.module) { - (Some(path), Some(module)) => (path.clone(), module), - _ => return Ok(()), - }; - let def_id = match module.resolve_path(ctx.db, &path)?.take_types() { - Some(it) => it, - None => return Ok(()), - }; - match def_id.resolve(ctx.db)? { - hir::Def::Module(module) => { - let module_scope = module.scope(ctx.db)?; - module_scope.entries().for_each(|(name, res)| { - CompletionItem::new(CompletionKind::Reference, name.to_string()) - .from_resolution(ctx, res) - .add_to(acc) - }); - } - hir::Def::Enum(e) => e - .variants(ctx.db)? - .into_iter() - .for_each(|(name, _variant)| { - CompletionItem::new(CompletionKind::Reference, name.to_string()) - .kind(CompletionItemKind::EnumVariant) - .add_to(acc) - }), - _ => return Ok(()), - }; - Ok(()) -} - -#[cfg(test)] -mod tests { - use crate::completion::{CompletionKind, check_completion}; - - fn check_reference_completion(code: &str, expected_completions: &str) { - check_completion(code, expected_completions, CompletionKind::Reference); - } - - #[test] - fn completes_use_item_starting_with_self() { - check_reference_completion( - r" - use self::m::<|>; - - mod m { - struct Bar; - } - ", - "Bar", - ); - } - - #[test] - fn completes_use_item_starting_with_crate() { - check_reference_completion( - " - //- /lib.rs - mod foo; - struct Spam; - //- /foo.rs - use crate::Sp<|> - ", - "Spam;foo", - ); - } - - #[test] - fn completes_nested_use_tree() { - check_reference_completion( - " - //- /lib.rs - mod foo; - struct Spam; - //- /foo.rs - use crate::{Sp<|>}; - ", - "Spam;foo", - ); - } - - #[test] - fn completes_deeply_nested_use_tree() { - check_reference_completion( - " - //- /lib.rs - mod foo; - pub mod bar { - pub mod baz { - pub struct Spam; - } - } - //- /foo.rs - use crate::{bar::{baz::Sp<|>}}; - ", - "Spam", - ); - } - - #[test] - fn completes_enum_variant() { - check_reference_completion( - " - //- /lib.rs - enum E { Foo, Bar(i32) } - fn foo() { let _ = E::<|> } - ", - "Foo;Bar", - ); - } - - #[test] - fn dont_render_function_parens_in_use_item() { - check_reference_completion( - " - //- /lib.rs - mod m { pub fn foo() {} } - use crate::m::f<|>; - ", - "foo", - ) - } -} diff --git a/crates/ra_analysis/src/completion/complete_scope.rs b/crates/ra_analysis/src/completion/complete_scope.rs deleted file mode 100644 index ee9052d3d5d5..000000000000 --- a/crates/ra_analysis/src/completion/complete_scope.rs +++ /dev/null @@ -1,192 +0,0 @@ -use rustc_hash::FxHashSet; -use ra_syntax::TextUnit; - -use crate::{ - Cancelable, - completion::{CompletionItem, CompletionItemKind, Completions, CompletionKind, CompletionContext}, -}; - -pub(super) fn complete_scope(acc: &mut Completions, ctx: &CompletionContext) -> Cancelable<()> { - if !ctx.is_trivial_path { - return Ok(()); - } - let module = match &ctx.module { - Some(it) => it, - None => return Ok(()), - }; - if let Some(function) = &ctx.function { - let scopes = function.scopes(ctx.db)?; - complete_fn(acc, &scopes, ctx.offset); - } - - let module_scope = module.scope(ctx.db)?; - let (file_id, _) = module.defenition_source(ctx.db)?; - module_scope - .entries() - .filter(|(_name, res)| { - // Don't expose this item - // FIXME: this penetrates through all kinds of abstractions, - // we need to figura out the way to do it less ugly. - match res.import { - None => true, - Some(import) => { - let range = import.range(ctx.db, file_id); - !range.is_subrange(&ctx.leaf.range()) - } - } - }) - .for_each(|(name, res)| { - CompletionItem::new(CompletionKind::Reference, name.to_string()) - .from_resolution(ctx, res) - .add_to(acc) - }); - Ok(()) -} - -fn complete_fn(acc: &mut Completions, scopes: &hir::ScopesWithSyntaxMapping, offset: TextUnit) { - let mut shadowed = FxHashSet::default(); - scopes - .scope_chain_for_offset(offset) - .flat_map(|scope| scopes.scopes.entries(scope).iter()) - .filter(|entry| shadowed.insert(entry.name())) - .for_each(|entry| { - CompletionItem::new(CompletionKind::Reference, entry.name().to_string()) - .kind(CompletionItemKind::Binding) - .add_to(acc) - }); -} - -#[cfg(test)] -mod tests { - use crate::completion::{CompletionKind, check_completion}; - - fn check_reference_completion(code: &str, expected_completions: &str) { - check_completion(code, expected_completions, CompletionKind::Reference); - } - - #[test] - fn completes_bindings_from_let() { - check_reference_completion( - r" - fn quux(x: i32) { - let y = 92; - 1 + <|>; - let z = (); - } - ", - r#"y;x;quux "quux($0)""#, - ); - } - - #[test] - fn completes_bindings_from_if_let() { - check_reference_completion( - r" - fn quux() { - if let Some(x) = foo() { - let y = 92; - }; - if let Some(a) = bar() { - let b = 62; - 1 + <|> - } - } - ", - r#"b;a;quux "quux()$0""#, - ); - } - - #[test] - fn completes_bindings_from_for() { - check_reference_completion( - r" - fn quux() { - for x in &[1, 2, 3] { - <|> - } - } - ", - r#"x;quux "quux()$0""#, - ); - } - - #[test] - fn completes_module_items() { - check_reference_completion( - r" - struct Foo; - enum Baz {} - fn quux() { - <|> - } - ", - r#"quux "quux()$0";Foo;Baz"#, - ); - } - - #[test] - fn completes_module_items_in_nested_modules() { - check_reference_completion( - r" - struct Foo; - mod m { - struct Bar; - fn quux() { <|> } - } - ", - r#"quux "quux()$0";Bar"#, - ); - } - - #[test] - fn completes_return_type() { - check_reference_completion( - r" - struct Foo; - fn x() -> <|> - ", - r#"Foo;x "x()$0""#, - ) - } - - #[test] - fn dont_show_both_completions_for_shadowing() { - check_reference_completion( - r" - fn foo() -> { - let bar = 92; - { - let bar = 62; - <|> - } - } - ", - r#"bar;foo "foo()$0""#, - ) - } - - #[test] - fn completes_self_in_methods() { - check_reference_completion(r"impl S { fn foo(&self) { <|> } }", "self") - } - - #[test] - fn inserts_parens_for_function_calls() { - check_reference_completion( - r" - fn no_args() {} - fn main() { no_<|> } - ", - r#"no_args "no_args()$0" - main "main()$0""#, - ); - check_reference_completion( - r" - fn with_args(x: i32, y: String) {} - fn main() { with_<|> } - ", - r#"main "main()$0" - with_args "with_args($0)""#, - ); - } -} diff --git a/crates/ra_analysis/src/completion/complete_snippet.rs b/crates/ra_analysis/src/completion/complete_snippet.rs deleted file mode 100644 index a495751dda02..000000000000 --- a/crates/ra_analysis/src/completion/complete_snippet.rs +++ /dev/null @@ -1,73 +0,0 @@ -use crate::completion::{CompletionItem, Completions, CompletionKind, CompletionItemKind, CompletionContext, completion_item::Builder}; - -fn snippet(label: &str, snippet: &str) -> Builder { - CompletionItem::new(CompletionKind::Snippet, label) - .snippet(snippet) - .kind(CompletionItemKind::Snippet) -} - -pub(super) fn complete_expr_snippet(acc: &mut Completions, ctx: &CompletionContext) { - if !(ctx.is_trivial_path && ctx.function_syntax.is_some()) { - return; - } - snippet("pd", "eprintln!(\"$0 = {:?}\", $0);").add_to(acc); - snippet("ppd", "eprintln!(\"$0 = {:#?}\", $0);").add_to(acc); -} - -pub(super) fn complete_item_snippet(acc: &mut Completions, ctx: &CompletionContext) { - if !ctx.is_new_item { - return; - } - snippet( - "Test function", - "\ -#[test] -fn ${1:feature}() { - $0 -}", - ) - .lookup_by("tfn") - .add_to(acc); - - snippet("pub(crate)", "pub(crate) $0").add_to(acc); -} - -#[cfg(test)] -mod tests { - use crate::completion::{CompletionKind, check_completion}; - fn check_snippet_completion(code: &str, expected_completions: &str) { - check_completion(code, expected_completions, CompletionKind::Snippet); - } - - #[test] - fn completes_snippets_in_expressions() { - check_snippet_completion( - r"fn foo(x: i32) { <|> }", - r##" - pd "eprintln!(\"$0 = {:?}\", $0);" - ppd "eprintln!(\"$0 = {:#?}\", $0);" - "##, - ); - } - - #[test] - fn completes_snippets_in_items() { - // check_snippet_completion(r" - // <|> - // ", - // r##"[CompletionItem { label: "Test function", lookup: None, snippet: Some("#[test]\nfn test_${1:feature}() {\n$0\n}"##, - // ); - check_snippet_completion( - r" - #[cfg(test)] - mod tests { - <|> - } - ", - r##" - tfn "Test function" "#[test]\nfn ${1:feature}() {\n $0\n}" - pub(crate) "pub(crate) $0" - "##, - ); - } -} diff --git a/crates/ra_analysis/src/completion/completion_context.rs b/crates/ra_analysis/src/completion/completion_context.rs deleted file mode 100644 index 01786bb69e91..000000000000 --- a/crates/ra_analysis/src/completion/completion_context.rs +++ /dev/null @@ -1,205 +0,0 @@ -use ra_text_edit::AtomTextEdit; -use ra_syntax::{ - AstNode, SyntaxNode, SourceFile, TextUnit, TextRange, - ast, - algo::{find_leaf_at_offset, find_covering_node, find_node_at_offset}, - SyntaxKind::*, -}; -use hir::source_binder; - -use crate::{db, FilePosition, Cancelable}; - -/// `CompletionContext` is created early during completion to figure out, where -/// exactly is the cursor, syntax-wise. -#[derive(Debug)] -pub(super) struct CompletionContext<'a> { - pub(super) db: &'a db::RootDatabase, - pub(super) offset: TextUnit, - pub(super) leaf: &'a SyntaxNode, - pub(super) module: Option, - pub(super) function: Option, - pub(super) function_syntax: Option<&'a ast::FnDef>, - pub(super) use_item_syntax: Option<&'a ast::UseItem>, - pub(super) is_param: bool, - /// A single-indent path, like `foo`. - pub(super) is_trivial_path: bool, - /// If not a trivial, path, the prefix (qualifier). - pub(super) path_prefix: Option, - pub(super) after_if: bool, - /// `true` if we are a statement or a last expr in the block. - pub(super) can_be_stmt: bool, - /// Something is typed at the "top" level, in module or impl/trait. - pub(super) is_new_item: bool, - /// The receiver if this is a field or method access, i.e. writing something.<|> - pub(super) dot_receiver: Option<&'a ast::Expr>, - /// If this is a method call in particular, i.e. the () are already there. - pub(super) is_method_call: bool, -} - -impl<'a> CompletionContext<'a> { - pub(super) fn new( - db: &'a db::RootDatabase, - original_file: &'a SourceFile, - position: FilePosition, - ) -> Cancelable>> { - let module = source_binder::module_from_position(db, position)?; - let leaf = - ctry!(find_leaf_at_offset(original_file.syntax(), position.offset).left_biased()); - let mut ctx = CompletionContext { - db, - leaf, - offset: position.offset, - module, - function: None, - function_syntax: None, - use_item_syntax: None, - is_param: false, - is_trivial_path: false, - path_prefix: None, - after_if: false, - can_be_stmt: false, - is_new_item: false, - dot_receiver: None, - is_method_call: false, - }; - ctx.fill(original_file, position.offset); - Ok(Some(ctx)) - } - - fn fill(&mut self, original_file: &'a SourceFile, offset: TextUnit) { - // Insert a fake ident to get a valid parse tree. We will use this file - // to determine context, though the original_file will be used for - // actual completion. - let file = { - let edit = AtomTextEdit::insert(offset, "intellijRulezz".to_string()); - original_file.reparse(&edit) - }; - - // First, let's try to complete a reference to some declaration. - if let Some(name_ref) = find_node_at_offset::(file.syntax(), offset) { - // Special case, `trait T { fn foo(i_am_a_name_ref) {} }`. - // See RFC#1685. - if is_node::(name_ref.syntax()) { - self.is_param = true; - return; - } - self.classify_name_ref(original_file, name_ref); - } - - // Otherwise, see if this is a declaration. We can use heuristics to - // suggest declaration names, see `CompletionKind::Magic`. - if let Some(name) = find_node_at_offset::(file.syntax(), offset) { - if is_node::(name.syntax()) { - self.is_param = true; - return; - } - } - } - fn classify_name_ref(&mut self, original_file: &'a SourceFile, name_ref: &ast::NameRef) { - let name_range = name_ref.syntax().range(); - let top_node = name_ref - .syntax() - .ancestors() - .take_while(|it| it.range() == name_range) - .last() - .unwrap(); - - match top_node.parent().map(|it| it.kind()) { - Some(SOURCE_FILE) | Some(ITEM_LIST) => { - self.is_new_item = true; - return; - } - _ => (), - } - - self.use_item_syntax = self.leaf.ancestors().find_map(ast::UseItem::cast); - - self.function_syntax = self - .leaf - .ancestors() - .take_while(|it| it.kind() != SOURCE_FILE && it.kind() != MODULE) - .find_map(ast::FnDef::cast); - match (&self.module, self.function_syntax) { - (Some(module), Some(fn_def)) => { - let function = source_binder::function_from_module(self.db, module, fn_def); - self.function = Some(function); - } - _ => (), - } - - let parent = match name_ref.syntax().parent() { - Some(it) => it, - None => return, - }; - if let Some(segment) = ast::PathSegment::cast(parent) { - let path = segment.parent_path(); - if let Some(mut path) = hir::Path::from_ast(path) { - if !path.is_ident() { - path.segments.pop().unwrap(); - self.path_prefix = Some(path); - return; - } - } - if path.qualifier().is_none() { - self.is_trivial_path = true; - - // Find either enclosing expr statement (thing with `;`) or a - // block. If block, check that we are the last expr. - self.can_be_stmt = name_ref - .syntax() - .ancestors() - .find_map(|node| { - if let Some(stmt) = ast::ExprStmt::cast(node) { - return Some(stmt.syntax().range() == name_ref.syntax().range()); - } - if let Some(block) = ast::Block::cast(node) { - return Some( - block.expr().map(|e| e.syntax().range()) - == Some(name_ref.syntax().range()), - ); - } - None - }) - .unwrap_or(false); - - if let Some(off) = name_ref.syntax().range().start().checked_sub(2.into()) { - if let Some(if_expr) = - find_node_at_offset::(original_file.syntax(), off) - { - if if_expr.syntax().range().end() < name_ref.syntax().range().start() { - self.after_if = true; - } - } - } - } - } - if let Some(field_expr) = ast::FieldExpr::cast(parent) { - // The receiver comes before the point of insertion of the fake - // ident, so it should have the same range in the non-modified file - self.dot_receiver = field_expr - .expr() - .map(|e| e.syntax().range()) - .and_then(|r| find_node_with_range(original_file.syntax(), r)); - } - if let Some(method_call_expr) = ast::MethodCallExpr::cast(parent) { - // As above - self.dot_receiver = method_call_expr - .expr() - .map(|e| e.syntax().range()) - .and_then(|r| find_node_with_range(original_file.syntax(), r)); - self.is_method_call = true; - } - } -} - -fn find_node_with_range(syntax: &SyntaxNode, range: TextRange) -> Option<&N> { - let node = find_covering_node(syntax, range); - node.ancestors().find_map(N::cast) -} - -fn is_node(node: &SyntaxNode) -> bool { - match node.ancestors().filter_map(N::cast).next() { - None => false, - Some(n) => n.syntax().range() == node.range(), - } -} diff --git a/crates/ra_analysis/src/completion/completion_item.rs b/crates/ra_analysis/src/completion/completion_item.rs deleted file mode 100644 index a25b87beefee..000000000000 --- a/crates/ra_analysis/src/completion/completion_item.rs +++ /dev/null @@ -1,244 +0,0 @@ -use hir::PerNs; - -use crate::completion::CompletionContext; - -/// `CompletionItem` describes a single completion variant in the editor pop-up. -/// It is basically a POD with various properties. To construct a -/// `CompletionItem`, use `new` method and the `Builder` struct. -#[derive(Debug)] -pub struct CompletionItem { - /// Used only internally in tests, to check only specific kind of - /// completion. - completion_kind: CompletionKind, - label: String, - lookup: Option, - snippet: Option, - kind: Option, -} - -pub enum InsertText { - PlainText { text: String }, - Snippet { text: String }, -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum CompletionItemKind { - Snippet, - Keyword, - Module, - Function, - Struct, - Enum, - EnumVariant, - Binding, - Field, -} - -#[derive(Debug, PartialEq, Eq)] -pub(crate) enum CompletionKind { - /// Parser-based keyword completion. - Keyword, - /// Your usual "complete all valid identifiers". - Reference, - /// "Secret sauce" completions. - Magic, - Snippet, -} - -impl CompletionItem { - pub(crate) fn new(completion_kind: CompletionKind, label: impl Into) -> Builder { - let label = label.into(); - Builder { - completion_kind, - label, - lookup: None, - snippet: None, - kind: None, - } - } - /// What user sees in pop-up in the UI. - pub fn label(&self) -> &str { - &self.label - } - /// What string is used for filtering. - pub fn lookup(&self) -> &str { - self.lookup - .as_ref() - .map(|it| it.as_str()) - .unwrap_or(self.label()) - } - /// What is inserted. - pub fn insert_text(&self) -> InsertText { - match &self.snippet { - None => InsertText::PlainText { - text: self.label.clone(), - }, - Some(it) => InsertText::Snippet { text: it.clone() }, - } - } - - pub fn kind(&self) -> Option { - self.kind - } -} - -/// A helper to make `CompletionItem`s. -#[must_use] -pub(crate) struct Builder { - completion_kind: CompletionKind, - label: String, - lookup: Option, - snippet: Option, - kind: Option, -} - -impl Builder { - pub(crate) fn add_to(self, acc: &mut Completions) { - acc.add(self.build()) - } - - pub(crate) fn build(self) -> CompletionItem { - CompletionItem { - label: self.label, - lookup: self.lookup, - snippet: self.snippet, - kind: self.kind, - completion_kind: self.completion_kind, - } - } - pub(crate) fn lookup_by(mut self, lookup: impl Into) -> Builder { - self.lookup = Some(lookup.into()); - self - } - pub(crate) fn snippet(mut self, snippet: impl Into) -> Builder { - self.snippet = Some(snippet.into()); - self - } - pub(crate) fn kind(mut self, kind: CompletionItemKind) -> Builder { - self.kind = Some(kind); - self - } - pub(super) fn from_resolution( - mut self, - ctx: &CompletionContext, - resolution: &hir::Resolution, - ) -> Builder { - let resolved = resolution.def_id.and_then(|d| d.resolve(ctx.db).ok()); - let kind = match resolved { - PerNs { - types: Some(hir::Def::Module(..)), - .. - } => CompletionItemKind::Module, - PerNs { - types: Some(hir::Def::Struct(..)), - .. - } => CompletionItemKind::Struct, - PerNs { - types: Some(hir::Def::Enum(..)), - .. - } => CompletionItemKind::Enum, - PerNs { - values: Some(hir::Def::Function(function)), - .. - } => return self.from_function(ctx, function), - _ => return self, - }; - self.kind = Some(kind); - self - } - - fn from_function(mut self, ctx: &CompletionContext, function: hir::Function) -> Builder { - // If not an import, add parenthesis automatically. - if ctx.use_item_syntax.is_none() { - if function.signature(ctx.db).args().is_empty() { - self.snippet = Some(format!("{}()$0", self.label)); - } else { - self.snippet = Some(format!("{}($0)", self.label)); - } - } - self.kind = Some(CompletionItemKind::Function); - self - } -} - -impl Into for Builder { - fn into(self) -> CompletionItem { - self.build() - } -} - -/// Represents an in-progress set of completions being built. -#[derive(Debug, Default)] -pub(crate) struct Completions { - buf: Vec, -} - -impl Completions { - pub(crate) fn add(&mut self, item: impl Into) { - self.buf.push(item.into()) - } - pub(crate) fn add_all(&mut self, items: I) - where - I: IntoIterator, - I::Item: Into, - { - items.into_iter().for_each(|item| self.add(item.into())) - } - - #[cfg(test)] - pub(crate) fn assert_match(&self, expected: &str, kind: CompletionKind) { - let expected = normalize(expected); - let actual = self.debug_render(kind); - test_utils::assert_eq_text!(expected.as_str(), actual.as_str(),); - - /// Normalize the textual representation of `Completions`: - /// replace `;` with newlines, normalize whitespace - fn normalize(expected: &str) -> String { - use ra_syntax::{tokenize, TextUnit, TextRange, SyntaxKind::SEMI}; - let mut res = String::new(); - for line in expected.trim().lines() { - let line = line.trim(); - let mut start_offset: TextUnit = 0.into(); - // Yep, we use rust tokenize in completion tests :-) - for token in tokenize(line) { - let range = TextRange::offset_len(start_offset, token.len); - start_offset += token.len; - if token.kind == SEMI { - res.push('\n'); - } else { - res.push_str(&line[range]); - } - } - - res.push('\n'); - } - res - } - } - - #[cfg(test)] - fn debug_render(&self, kind: CompletionKind) -> String { - let mut res = String::new(); - for c in self.buf.iter() { - if c.completion_kind == kind { - if let Some(lookup) = &c.lookup { - res.push_str(lookup); - res.push_str(&format!(" {:?}", c.label)); - } else { - res.push_str(&c.label); - } - if let Some(snippet) = &c.snippet { - res.push_str(&format!(" {:?}", snippet)); - } - res.push('\n'); - } - } - res - } -} - -impl Into> for Completions { - fn into(self) -> Vec { - self.buf - } -} diff --git a/crates/ra_analysis/src/db.rs b/crates/ra_analysis/src/db.rs deleted file mode 100644 index 9d46609ecfd5..000000000000 --- a/crates/ra_analysis/src/db.rs +++ /dev/null @@ -1,128 +0,0 @@ -use std::{fmt, sync::Arc}; - -use salsa::{self, Database}; -use ra_db::{LocationIntener, BaseDatabase, FileId}; - -use crate::{symbol_index, LineIndex}; - -#[derive(Debug)] -pub(crate) struct RootDatabase { - runtime: salsa::Runtime, - id_maps: Arc, -} - -#[derive(Default)] -struct IdMaps { - defs: LocationIntener, - macros: LocationIntener, -} - -impl fmt::Debug for IdMaps { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - f.debug_struct("IdMaps") - .field("n_defs", &self.defs.len()) - .finish() - } -} - -impl salsa::Database for RootDatabase { - fn salsa_runtime(&self) -> &salsa::Runtime { - &self.runtime - } -} - -impl Default for RootDatabase { - fn default() -> RootDatabase { - let mut db = RootDatabase { - runtime: salsa::Runtime::default(), - id_maps: Default::default(), - }; - db.query_mut(ra_db::CrateGraphQuery) - .set((), Default::default()); - db.query_mut(ra_db::LocalRootsQuery) - .set((), Default::default()); - db.query_mut(ra_db::LibraryRootsQuery) - .set((), Default::default()); - db - } -} - -impl salsa::ParallelDatabase for RootDatabase { - fn snapshot(&self) -> salsa::Snapshot { - salsa::Snapshot::new(RootDatabase { - runtime: self.runtime.snapshot(self), - id_maps: self.id_maps.clone(), - }) - } -} - -impl BaseDatabase for RootDatabase {} - -impl AsRef> for RootDatabase { - fn as_ref(&self) -> &LocationIntener { - &self.id_maps.defs - } -} - -impl AsRef> for RootDatabase { - fn as_ref(&self) -> &LocationIntener { - &self.id_maps.macros - } -} - -salsa::query_group! { - pub(crate) trait LineIndexDatabase: ra_db::FilesDatabase + BaseDatabase { - fn line_index(file_id: FileId) -> Arc { - type LineIndexQuery; - } - } -} - -fn line_index(db: &impl ra_db::FilesDatabase, file_id: FileId) -> Arc { - let text = db.file_text(file_id); - Arc::new(LineIndex::new(&*text)) -} - -salsa::database_storage! { - pub(crate) struct RootDatabaseStorage for RootDatabase { - impl ra_db::FilesDatabase { - fn file_text() for ra_db::FileTextQuery; - fn file_relative_path() for ra_db::FileRelativePathQuery; - fn file_source_root() for ra_db::FileSourceRootQuery; - fn source_root() for ra_db::SourceRootQuery; - fn local_roots() for ra_db::LocalRootsQuery; - fn library_roots() for ra_db::LibraryRootsQuery; - fn crate_graph() for ra_db::CrateGraphQuery; - } - impl ra_db::SyntaxDatabase { - fn source_file() for ra_db::SourceFileQuery; - } - impl LineIndexDatabase { - fn line_index() for LineIndexQuery; - } - impl symbol_index::SymbolsDatabase { - fn file_symbols() for symbol_index::FileSymbolsQuery; - fn library_symbols() for symbol_index::LibrarySymbolsQuery; - } - impl hir::db::HirDatabase { - fn hir_source_file() for hir::db::HirSourceFileQuery; - fn expand_macro_invocation() for hir::db::ExpandMacroCallQuery; - fn module_tree() for hir::db::ModuleTreeQuery; - fn fn_scopes() for hir::db::FnScopesQuery; - fn file_items() for hir::db::SourceFileItemsQuery; - fn file_item() for hir::db::FileItemQuery; - fn input_module_items() for hir::db::InputModuleItemsQuery; - fn item_map() for hir::db::ItemMapQuery; - fn submodules() for hir::db::SubmodulesQuery; - fn infer() for hir::db::InferQuery; - fn type_for_def() for hir::db::TypeForDefQuery; - fn type_for_field() for hir::db::TypeForFieldQuery; - fn struct_data() for hir::db::StructDataQuery; - fn enum_data() for hir::db::EnumDataQuery; - fn impls_in_module() for hir::db::ImplsInModuleQuery; - fn body_hir() for hir::db::BodyHirQuery; - fn body_syntax_mapping() for hir::db::BodySyntaxMappingQuery; - fn fn_signature() for hir::db::FnSignatureQuery; - } - } -} diff --git a/crates/ra_analysis/src/extend_selection.rs b/crates/ra_analysis/src/extend_selection.rs deleted file mode 100644 index c3c809c9fe33..000000000000 --- a/crates/ra_analysis/src/extend_selection.rs +++ /dev/null @@ -1,56 +0,0 @@ -use ra_db::SyntaxDatabase; -use ra_syntax::{ - SyntaxNode, AstNode, SourceFile, - ast, algo::find_covering_node, -}; - -use crate::{ - TextRange, FileRange, - db::RootDatabase, -}; - -pub(crate) fn extend_selection(db: &RootDatabase, frange: FileRange) -> TextRange { - let source_file = db.source_file(frange.file_id); - if let Some(range) = extend_selection_in_macro(db, &source_file, frange) { - return range; - } - ra_ide_api_light::extend_selection(source_file.syntax(), frange.range).unwrap_or(frange.range) -} - -fn extend_selection_in_macro( - _db: &RootDatabase, - source_file: &SourceFile, - frange: FileRange, -) -> Option { - let macro_call = find_macro_call(source_file.syntax(), frange.range)?; - let (off, exp) = hir::MacroDef::ast_expand(macro_call)?; - let dst_range = exp.map_range_forward(frange.range - off)?; - let dst_range = ra_ide_api_light::extend_selection(&exp.syntax(), dst_range)?; - let src_range = exp.map_range_back(dst_range)? + off; - Some(src_range) -} - -fn find_macro_call(node: &SyntaxNode, range: TextRange) -> Option<&ast::MacroCall> { - find_covering_node(node, range) - .ancestors() - .find_map(ast::MacroCall::cast) -} - -#[cfg(test)] -mod tests { - use crate::mock_analysis::single_file_with_range; - use test_utils::assert_eq_dbg; - - #[test] - fn extend_selection_inside_macros() { - let (analysis, frange) = single_file_with_range( - " - fn main() { - ctry!(foo(|x| <|>x<|>)); - } - ", - ); - let r = analysis.extend_selection(frange); - assert_eq_dbg("[51; 56)", &r); - } -} diff --git a/crates/ra_analysis/src/goto_defenition.rs b/crates/ra_analysis/src/goto_defenition.rs deleted file mode 100644 index fcd8d315e24a..000000000000 --- a/crates/ra_analysis/src/goto_defenition.rs +++ /dev/null @@ -1,139 +0,0 @@ -use ra_db::{FileId, Cancelable, SyntaxDatabase}; -use ra_syntax::{ - TextRange, AstNode, ast, SyntaxKind::{NAME, MODULE}, - algo::find_node_at_offset, -}; - -use crate::{FilePosition, NavigationTarget, db::RootDatabase}; - -pub(crate) fn goto_defenition( - db: &RootDatabase, - position: FilePosition, -) -> Cancelable>> { - let file = db.source_file(position.file_id); - let syntax = file.syntax(); - if let Some(name_ref) = find_node_at_offset::(syntax, position.offset) { - return Ok(Some(reference_defenition(db, position.file_id, name_ref)?)); - } - if let Some(name) = find_node_at_offset::(syntax, position.offset) { - return name_defenition(db, position.file_id, name); - } - Ok(None) -} - -pub(crate) fn reference_defenition( - db: &RootDatabase, - file_id: FileId, - name_ref: &ast::NameRef, -) -> Cancelable> { - if let Some(fn_descr) = - hir::source_binder::function_from_child_node(db, file_id, name_ref.syntax())? - { - let scope = fn_descr.scopes(db)?; - // First try to resolve the symbol locally - if let Some(entry) = scope.resolve_local_name(name_ref) { - let nav = NavigationTarget { - file_id, - name: entry.name().to_string().into(), - range: entry.ptr().range(), - kind: NAME, - ptr: None, - }; - return Ok(vec![nav]); - }; - } - // If that fails try the index based approach. - let navs = db - .index_resolve(name_ref)? - .into_iter() - .map(NavigationTarget::from_symbol) - .collect(); - Ok(navs) -} - -fn name_defenition( - db: &RootDatabase, - file_id: FileId, - name: &ast::Name, -) -> Cancelable>> { - if let Some(module) = name.syntax().parent().and_then(ast::Module::cast) { - if module.has_semi() { - if let Some(child_module) = - hir::source_binder::module_from_declaration(db, file_id, module)? - { - let (file_id, _) = child_module.defenition_source(db)?; - let name = match child_module.name(db)? { - Some(name) => name.to_string().into(), - None => "".into(), - }; - let nav = NavigationTarget { - file_id, - name, - range: TextRange::offset_len(0.into(), 0.into()), - kind: MODULE, - ptr: None, - }; - return Ok(Some(vec![nav])); - } - } - } - Ok(None) -} - -#[cfg(test)] -mod tests { - use test_utils::assert_eq_dbg; - use crate::mock_analysis::analysis_and_position; - - #[test] - fn goto_defenition_works_in_items() { - let (analysis, pos) = analysis_and_position( - " - //- /lib.rs - struct Foo; - enum E { X(Foo<|>) } - ", - ); - - let symbols = analysis.goto_defenition(pos).unwrap().unwrap(); - assert_eq_dbg( - r#"[NavigationTarget { file_id: FileId(1), name: "Foo", - kind: STRUCT_DEF, range: [0; 11), - ptr: Some(LocalSyntaxPtr { range: [0; 11), kind: STRUCT_DEF }) }]"#, - &symbols, - ); - } - - #[test] - fn goto_defenition_works_for_module_declaration() { - let (analysis, pos) = analysis_and_position( - " - //- /lib.rs - mod <|>foo; - //- /foo.rs - // empty - ", - ); - - let symbols = analysis.goto_defenition(pos).unwrap().unwrap(); - assert_eq_dbg( - r#"[NavigationTarget { file_id: FileId(2), name: "foo", kind: MODULE, range: [0; 0), ptr: None }]"#, - &symbols, - ); - - let (analysis, pos) = analysis_and_position( - " - //- /lib.rs - mod <|>foo; - //- /foo/mod.rs - // empty - ", - ); - - let symbols = analysis.goto_defenition(pos).unwrap().unwrap(); - assert_eq_dbg( - r#"[NavigationTarget { file_id: FileId(2), name: "foo", kind: MODULE, range: [0; 0), ptr: None }]"#, - &symbols, - ); - } -} diff --git a/crates/ra_analysis/src/hover.rs b/crates/ra_analysis/src/hover.rs deleted file mode 100644 index 475524ee1827..000000000000 --- a/crates/ra_analysis/src/hover.rs +++ /dev/null @@ -1,257 +0,0 @@ -use ra_db::{Cancelable, SyntaxDatabase}; -use ra_syntax::{ - AstNode, SyntaxNode, TreePtr, - ast::{self, NameOwner}, - algo::{find_covering_node, find_node_at_offset, find_leaf_at_offset, visit::{visitor, Visitor}}, -}; - -use crate::{db::RootDatabase, RangeInfo, FilePosition, FileRange, NavigationTarget}; - -pub(crate) fn hover( - db: &RootDatabase, - position: FilePosition, -) -> Cancelable>> { - let file = db.source_file(position.file_id); - let mut res = Vec::new(); - - let mut range = None; - if let Some(name_ref) = find_node_at_offset::(file.syntax(), position.offset) { - let navs = crate::goto_defenition::reference_defenition(db, position.file_id, name_ref)?; - for nav in navs { - res.extend(doc_text_for(db, nav)?) - } - if !res.is_empty() { - range = Some(name_ref.syntax().range()) - } - } - if range.is_none() { - let node = find_leaf_at_offset(file.syntax(), position.offset).find_map(|leaf| { - leaf.ancestors() - .find(|n| ast::Expr::cast(*n).is_some() || ast::Pat::cast(*n).is_some()) - }); - let node = ctry!(node); - let frange = FileRange { - file_id: position.file_id, - range: node.range(), - }; - res.extend(type_of(db, frange)?); - range = Some(node.range()); - }; - - let range = ctry!(range); - if res.is_empty() { - return Ok(None); - } - let res = RangeInfo::new(range, res.join("\n\n---\n")); - Ok(Some(res)) -} - -pub(crate) fn type_of(db: &RootDatabase, frange: FileRange) -> Cancelable> { - let file = db.source_file(frange.file_id); - let syntax = file.syntax(); - let leaf_node = find_covering_node(syntax, frange.range); - // if we picked identifier, expand to pattern/expression - let node = leaf_node - .ancestors() - .take_while(|it| it.range() == leaf_node.range()) - .find(|&it| ast::Expr::cast(it).is_some() || ast::Pat::cast(it).is_some()) - .unwrap_or(leaf_node); - let parent_fn = ctry!(node.ancestors().find_map(ast::FnDef::cast)); - let function = ctry!(hir::source_binder::function_from_source( - db, - frange.file_id, - parent_fn - )?); - let infer = function.infer(db)?; - let syntax_mapping = function.body_syntax_mapping(db)?; - if let Some(expr) = ast::Expr::cast(node).and_then(|e| syntax_mapping.node_expr(e)) { - Ok(Some(infer[expr].to_string())) - } else if let Some(pat) = ast::Pat::cast(node).and_then(|p| syntax_mapping.node_pat(p)) { - Ok(Some(infer[pat].to_string())) - } else { - Ok(None) - } -} - -// FIXME: this should not really use navigation target. Rather, approximatelly -// resovled symbol should return a `DefId`. -fn doc_text_for(db: &RootDatabase, nav: NavigationTarget) -> Cancelable> { - let result = match (nav.description(db), nav.docs(db)) { - (Some(desc), Some(docs)) => Some("```rust\n".to_string() + &*desc + "\n```\n\n" + &*docs), - (Some(desc), None) => Some("```rust\n".to_string() + &*desc + "\n```"), - (None, Some(docs)) => Some(docs), - _ => None, - }; - - Ok(result) -} - -impl NavigationTarget { - fn node(&self, db: &RootDatabase) -> Option> { - let source_file = db.source_file(self.file_id); - let source_file = source_file.syntax(); - let node = source_file - .descendants() - .find(|node| node.kind() == self.kind && node.range() == self.range)? - .to_owned(); - Some(node) - } - - fn docs(&self, db: &RootDatabase) -> Option { - let node = self.node(db)?; - fn doc_comments(node: &N) -> Option { - let comments = node.doc_comment_text(); - if comments.is_empty() { - None - } else { - Some(comments) - } - } - - visitor() - .visit(doc_comments::) - .visit(doc_comments::) - .visit(doc_comments::) - .visit(doc_comments::) - .visit(doc_comments::) - .visit(doc_comments::) - .visit(doc_comments::) - .visit(doc_comments::) - .accept(&node)? - } - - /// Get a description of this node. - /// - /// e.g. `struct Name`, `enum Name`, `fn Name` - fn description(&self, db: &RootDatabase) -> Option { - // TODO: After type inference is done, add type information to improve the output - let node = self.node(db)?; - // TODO: Refactor to be have less repetition - visitor() - .visit(|node: &ast::FnDef| { - let mut string = "fn ".to_string(); - node.name()?.syntax().text().push_to(&mut string); - Some(string) - }) - .visit(|node: &ast::StructDef| { - let mut string = "struct ".to_string(); - node.name()?.syntax().text().push_to(&mut string); - Some(string) - }) - .visit(|node: &ast::EnumDef| { - let mut string = "enum ".to_string(); - node.name()?.syntax().text().push_to(&mut string); - Some(string) - }) - .visit(|node: &ast::TraitDef| { - let mut string = "trait ".to_string(); - node.name()?.syntax().text().push_to(&mut string); - Some(string) - }) - .visit(|node: &ast::Module| { - let mut string = "mod ".to_string(); - node.name()?.syntax().text().push_to(&mut string); - Some(string) - }) - .visit(|node: &ast::TypeDef| { - let mut string = "type ".to_string(); - node.name()?.syntax().text().push_to(&mut string); - Some(string) - }) - .visit(|node: &ast::ConstDef| { - let mut string = "const ".to_string(); - node.name()?.syntax().text().push_to(&mut string); - Some(string) - }) - .visit(|node: &ast::StaticDef| { - let mut string = "static ".to_string(); - node.name()?.syntax().text().push_to(&mut string); - Some(string) - }) - .accept(&node)? - } -} - -#[cfg(test)] -mod tests { - use ra_syntax::TextRange; - use crate::mock_analysis::{single_file_with_position, single_file_with_range}; - - #[test] - fn hover_shows_type_of_an_expression() { - let (analysis, position) = single_file_with_position( - " - pub fn foo() -> u32 { 1 } - - fn main() { - let foo_test = foo()<|>; - } - ", - ); - let hover = analysis.hover(position).unwrap().unwrap(); - assert_eq!(hover.range, TextRange::from_to(95.into(), 100.into())); - assert_eq!(hover.info, "u32"); - } - - #[test] - fn hover_for_local_variable() { - let (analysis, position) = single_file_with_position("fn func(foo: i32) { fo<|>o; }"); - let hover = analysis.hover(position).unwrap().unwrap(); - assert_eq!(hover.info, "i32"); - } - - #[test] - fn hover_for_local_variable_pat() { - let (analysis, position) = single_file_with_position("fn func(fo<|>o: i32) {}"); - let hover = analysis.hover(position).unwrap().unwrap(); - assert_eq!(hover.info, "i32"); - } - - #[test] - fn test_type_of_for_function() { - let (analysis, range) = single_file_with_range( - " - pub fn foo() -> u32 { 1 }; - - fn main() { - let foo_test = <|>foo()<|>; - } - ", - ); - - let type_name = analysis.type_of(range).unwrap().unwrap(); - assert_eq!("u32", &type_name); - } - - // FIXME: improve type_of to make this work - #[test] - fn test_type_of_for_expr_1() { - let (analysis, range) = single_file_with_range( - " - fn main() { - let foo = <|>1 + foo_test<|>; - } - ", - ); - - let type_name = analysis.type_of(range).unwrap().unwrap(); - assert_eq!("[unknown]", &type_name); - } - - // FIXME: improve type_of to make this work - #[test] - fn test_type_of_for_expr_2() { - let (analysis, range) = single_file_with_range( - " - fn main() { - let foo: usize = 1; - let bar = <|>1 + foo_test<|>; - } - ", - ); - - let type_name = analysis.type_of(range).unwrap().unwrap(); - assert_eq!("[unknown]", &type_name); - } - -} diff --git a/crates/ra_analysis/src/imp.rs b/crates/ra_analysis/src/imp.rs deleted file mode 100644 index 7c60ab7d6f4e..000000000000 --- a/crates/ra_analysis/src/imp.rs +++ /dev/null @@ -1,309 +0,0 @@ -use std::sync::Arc; - -use salsa::Database; - -use hir::{ - self, Problem, source_binder, -}; -use ra_db::{FilesDatabase, SourceRoot, SourceRootId, SyntaxDatabase}; -use ra_ide_api_light::{self, assists, LocalEdit, Severity}; -use ra_syntax::{ - TextRange, AstNode, SourceFile, - ast::{self, NameOwner}, - algo::find_node_at_offset, - SyntaxKind::*, -}; - -use crate::{ - AnalysisChange, - Cancelable, NavigationTarget, - CrateId, db, Diagnostic, FileId, FilePosition, FileRange, FileSystemEdit, - Query, RootChange, SourceChange, SourceFileEdit, - symbol_index::{LibrarySymbolsQuery, FileSymbol}, -}; - -impl db::RootDatabase { - pub(crate) fn apply_change(&mut self, change: AnalysisChange) { - log::info!("apply_change {:?}", change); - // self.gc_syntax_trees(); - if !change.new_roots.is_empty() { - let mut local_roots = Vec::clone(&self.local_roots()); - for (root_id, is_local) in change.new_roots { - self.query_mut(ra_db::SourceRootQuery) - .set(root_id, Default::default()); - if is_local { - local_roots.push(root_id); - } - } - self.query_mut(ra_db::LocalRootsQuery) - .set((), Arc::new(local_roots)); - } - - for (root_id, root_change) in change.roots_changed { - self.apply_root_change(root_id, root_change); - } - for (file_id, text) in change.files_changed { - self.query_mut(ra_db::FileTextQuery).set(file_id, text) - } - if !change.libraries_added.is_empty() { - let mut libraries = Vec::clone(&self.library_roots()); - for library in change.libraries_added { - libraries.push(library.root_id); - self.query_mut(ra_db::SourceRootQuery) - .set(library.root_id, Default::default()); - self.query_mut(LibrarySymbolsQuery) - .set_constant(library.root_id, Arc::new(library.symbol_index)); - self.apply_root_change(library.root_id, library.root_change); - } - self.query_mut(ra_db::LibraryRootsQuery) - .set((), Arc::new(libraries)); - } - if let Some(crate_graph) = change.crate_graph { - self.query_mut(ra_db::CrateGraphQuery) - .set((), Arc::new(crate_graph)) - } - } - - fn apply_root_change(&mut self, root_id: SourceRootId, root_change: RootChange) { - let mut source_root = SourceRoot::clone(&self.source_root(root_id)); - for add_file in root_change.added { - self.query_mut(ra_db::FileTextQuery) - .set(add_file.file_id, add_file.text); - self.query_mut(ra_db::FileRelativePathQuery) - .set(add_file.file_id, add_file.path.clone()); - self.query_mut(ra_db::FileSourceRootQuery) - .set(add_file.file_id, root_id); - source_root.files.insert(add_file.path, add_file.file_id); - } - for remove_file in root_change.removed { - self.query_mut(ra_db::FileTextQuery) - .set(remove_file.file_id, Default::default()); - source_root.files.remove(&remove_file.path); - } - self.query_mut(ra_db::SourceRootQuery) - .set(root_id, Arc::new(source_root)); - } - - #[allow(unused)] - /// Ideally, we should call this function from time to time to collect heavy - /// syntax trees. However, if we actually do that, everything is recomputed - /// for some reason. Needs investigation. - fn gc_syntax_trees(&mut self) { - self.query(ra_db::SourceFileQuery) - .sweep(salsa::SweepStrategy::default().discard_values()); - self.query(hir::db::SourceFileItemsQuery) - .sweep(salsa::SweepStrategy::default().discard_values()); - self.query(hir::db::FileItemQuery) - .sweep(salsa::SweepStrategy::default().discard_values()); - } -} - -impl db::RootDatabase { - /// This returns `Vec` because a module may be included from several places. We - /// don't handle this case yet though, so the Vec has length at most one. - pub(crate) fn parent_module( - &self, - position: FilePosition, - ) -> Cancelable> { - let module = match source_binder::module_from_position(self, position)? { - None => return Ok(Vec::new()), - Some(it) => it, - }; - let (file_id, ast_module) = match module.declaration_source(self)? { - None => return Ok(Vec::new()), - Some(it) => it, - }; - let name = ast_module.name().unwrap(); - Ok(vec![NavigationTarget { - file_id, - name: name.text().clone(), - range: name.syntax().range(), - kind: MODULE, - ptr: None, - }]) - } - /// Returns `Vec` for the same reason as `parent_module` - pub(crate) fn crate_for(&self, file_id: FileId) -> Cancelable> { - let module = match source_binder::module_from_file_id(self, file_id)? { - Some(it) => it, - None => return Ok(Vec::new()), - }; - let krate = match module.krate(self)? { - Some(it) => it, - None => return Ok(Vec::new()), - }; - Ok(vec![krate.crate_id()]) - } - pub(crate) fn find_all_refs( - &self, - position: FilePosition, - ) -> Cancelable> { - let file = self.source_file(position.file_id); - // Find the binding associated with the offset - let (binding, descr) = match find_binding(self, &file, position)? { - None => return Ok(Vec::new()), - Some(it) => it, - }; - - let mut ret = binding - .name() - .into_iter() - .map(|name| (position.file_id, name.syntax().range())) - .collect::>(); - ret.extend( - descr - .scopes(self)? - .find_all_refs(binding) - .into_iter() - .map(|ref_desc| (position.file_id, ref_desc.range)), - ); - - return Ok(ret); - - fn find_binding<'a>( - db: &db::RootDatabase, - source_file: &'a SourceFile, - position: FilePosition, - ) -> Cancelable> { - let syntax = source_file.syntax(); - if let Some(binding) = find_node_at_offset::(syntax, position.offset) { - let descr = ctry!(source_binder::function_from_child_node( - db, - position.file_id, - binding.syntax(), - )?); - return Ok(Some((binding, descr))); - }; - let name_ref = ctry!(find_node_at_offset::(syntax, position.offset)); - let descr = ctry!(source_binder::function_from_child_node( - db, - position.file_id, - name_ref.syntax(), - )?); - let scope = descr.scopes(db)?; - let resolved = ctry!(scope.resolve_local_name(name_ref)); - let resolved = resolved.ptr().resolve(source_file); - let binding = ctry!(find_node_at_offset::( - syntax, - resolved.range().end() - )); - Ok(Some((binding, descr))) - } - } - - pub(crate) fn diagnostics(&self, file_id: FileId) -> Cancelable> { - let syntax = self.source_file(file_id); - - let mut res = ra_ide_api_light::diagnostics(&syntax) - .into_iter() - .map(|d| Diagnostic { - range: d.range, - message: d.msg, - severity: d.severity, - fix: d.fix.map(|fix| SourceChange::from_local_edit(file_id, fix)), - }) - .collect::>(); - if let Some(m) = source_binder::module_from_file_id(self, file_id)? { - for (name_node, problem) in m.problems(self)? { - let source_root = self.file_source_root(file_id); - let diag = match problem { - Problem::UnresolvedModule { candidate } => { - let create_file = FileSystemEdit::CreateFile { - source_root, - path: candidate.clone(), - }; - let fix = SourceChange { - label: "create module".to_string(), - source_file_edits: Vec::new(), - file_system_edits: vec![create_file], - cursor_position: None, - }; - Diagnostic { - range: name_node.range(), - message: "unresolved module".to_string(), - severity: Severity::Error, - fix: Some(fix), - } - } - Problem::NotDirOwner { move_to, candidate } => { - let move_file = FileSystemEdit::MoveFile { - src: file_id, - dst_source_root: source_root, - dst_path: move_to.clone(), - }; - let create_file = FileSystemEdit::CreateFile { - source_root, - path: move_to.join(candidate), - }; - let fix = SourceChange { - label: "move file and create module".to_string(), - source_file_edits: Vec::new(), - file_system_edits: vec![move_file, create_file], - cursor_position: None, - }; - Diagnostic { - range: name_node.range(), - message: "can't declare module at this location".to_string(), - severity: Severity::Error, - fix: Some(fix), - } - } - }; - res.push(diag) - } - }; - Ok(res) - } - - pub(crate) fn assists(&self, frange: FileRange) -> Vec { - let file = self.source_file(frange.file_id); - assists::assists(&file, frange.range) - .into_iter() - .map(|local_edit| SourceChange::from_local_edit(frange.file_id, local_edit)) - .collect() - } - - pub(crate) fn rename( - &self, - position: FilePosition, - new_name: &str, - ) -> Cancelable> { - let res = self - .find_all_refs(position)? - .iter() - .map(|(file_id, text_range)| SourceFileEdit { - file_id: *file_id, - edit: { - let mut builder = ra_text_edit::TextEditBuilder::default(); - builder.replace(*text_range, new_name.into()); - builder.finish() - }, - }) - .collect::>(); - Ok(res) - } - pub(crate) fn index_resolve(&self, name_ref: &ast::NameRef) -> Cancelable> { - let name = name_ref.text(); - let mut query = Query::new(name.to_string()); - query.exact(); - query.limit(4); - crate::symbol_index::world_symbols(self, query) - } -} - -impl SourceChange { - pub(crate) fn from_local_edit(file_id: FileId, edit: LocalEdit) -> SourceChange { - let file_edit = SourceFileEdit { - file_id, - edit: edit.edit, - }; - SourceChange { - label: edit.label, - source_file_edits: vec![file_edit], - file_system_edits: vec![], - cursor_position: edit - .cursor_position - .map(|offset| FilePosition { offset, file_id }), - } - } -} diff --git a/crates/ra_analysis/src/lib.rs b/crates/ra_analysis/src/lib.rs deleted file mode 100644 index 183e3670691e..000000000000 --- a/crates/ra_analysis/src/lib.rs +++ /dev/null @@ -1,509 +0,0 @@ -//! ra_analyzer crate provides "ide-centric" APIs for the rust-analyzer. What -//! powers this API are the `RootDatabase` struct, which defines a `salsa` -//! database, and the `ra_hir` crate, where majority of the analysis happens. -//! However, IDE specific bits of the analysis (most notably completion) happen -//! in this crate. -macro_rules! ctry { - ($expr:expr) => { - match $expr { - None => return Ok(None), - Some(it) => it, - } - }; -} - -mod completion; -mod db; -mod goto_defenition; -mod imp; -pub mod mock_analysis; -mod runnables; -mod symbol_index; - -mod extend_selection; -mod hover; -mod call_info; -mod syntax_highlighting; - -use std::{fmt, sync::Arc}; - -use ra_syntax::{SmolStr, SourceFile, TreePtr, SyntaxKind, TextRange, TextUnit}; -use ra_text_edit::TextEdit; -use ra_db::{SyntaxDatabase, FilesDatabase, LocalSyntaxPtr}; -use rayon::prelude::*; -use relative_path::RelativePathBuf; -use rustc_hash::FxHashMap; -use salsa::ParallelDatabase; - -use crate::{ - symbol_index::{FileSymbol, SymbolIndex}, - db::LineIndexDatabase, -}; - -pub use crate::{ - completion::{CompletionItem, CompletionItemKind, InsertText}, - runnables::{Runnable, RunnableKind}, -}; -pub use ra_ide_api_light::{ - Fold, FoldKind, HighlightedRange, Severity, StructureNode, - LineIndex, LineCol, translate_offset_with_edit, -}; -pub use ra_db::{ - Cancelable, Canceled, CrateGraph, CrateId, FileId, FilePosition, FileRange, SourceRootId -}; - -#[derive(Default)] -pub struct AnalysisChange { - new_roots: Vec<(SourceRootId, bool)>, - roots_changed: FxHashMap, - files_changed: Vec<(FileId, Arc)>, - libraries_added: Vec, - crate_graph: Option, -} - -#[derive(Default)] -struct RootChange { - added: Vec, - removed: Vec, -} - -#[derive(Debug)] -struct AddFile { - file_id: FileId, - path: RelativePathBuf, - text: Arc, -} - -#[derive(Debug)] -struct RemoveFile { - file_id: FileId, - path: RelativePathBuf, -} - -impl fmt::Debug for AnalysisChange { - fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result { - let mut d = fmt.debug_struct("AnalysisChange"); - if !self.new_roots.is_empty() { - d.field("new_roots", &self.new_roots); - } - if !self.roots_changed.is_empty() { - d.field("roots_changed", &self.roots_changed); - } - if !self.files_changed.is_empty() { - d.field("files_changed", &self.files_changed.len()); - } - if !self.libraries_added.is_empty() { - d.field("libraries_added", &self.libraries_added.len()); - } - if !self.crate_graph.is_some() { - d.field("crate_graph", &self.crate_graph); - } - d.finish() - } -} - -impl fmt::Debug for RootChange { - fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result { - fmt.debug_struct("AnalysisChange") - .field("added", &self.added.len()) - .field("removed", &self.removed.len()) - .finish() - } -} - -impl AnalysisChange { - pub fn new() -> AnalysisChange { - AnalysisChange::default() - } - pub fn add_root(&mut self, root_id: SourceRootId, is_local: bool) { - self.new_roots.push((root_id, is_local)); - } - pub fn add_file( - &mut self, - root_id: SourceRootId, - file_id: FileId, - path: RelativePathBuf, - text: Arc, - ) { - let file = AddFile { - file_id, - path, - text, - }; - self.roots_changed - .entry(root_id) - .or_default() - .added - .push(file); - } - pub fn change_file(&mut self, file_id: FileId, new_text: Arc) { - self.files_changed.push((file_id, new_text)) - } - pub fn remove_file(&mut self, root_id: SourceRootId, file_id: FileId, path: RelativePathBuf) { - let file = RemoveFile { file_id, path }; - self.roots_changed - .entry(root_id) - .or_default() - .removed - .push(file); - } - pub fn add_library(&mut self, data: LibraryData) { - self.libraries_added.push(data) - } - pub fn set_crate_graph(&mut self, graph: CrateGraph) { - self.crate_graph = Some(graph); - } -} - -#[derive(Debug)] -pub struct SourceChange { - pub label: String, - pub source_file_edits: Vec, - pub file_system_edits: Vec, - pub cursor_position: Option, -} - -#[derive(Debug)] -pub struct SourceFileEdit { - pub file_id: FileId, - pub edit: TextEdit, -} - -#[derive(Debug)] -pub enum FileSystemEdit { - CreateFile { - source_root: SourceRootId, - path: RelativePathBuf, - }, - MoveFile { - src: FileId, - dst_source_root: SourceRootId, - dst_path: RelativePathBuf, - }, -} - -#[derive(Debug)] -pub struct Diagnostic { - pub message: String, - pub range: TextRange, - pub fix: Option, - pub severity: Severity, -} - -#[derive(Debug)] -pub struct Query { - query: String, - lowercased: String, - only_types: bool, - libs: bool, - exact: bool, - limit: usize, -} - -impl Query { - pub fn new(query: String) -> Query { - let lowercased = query.to_lowercase(); - Query { - query, - lowercased, - only_types: false, - libs: false, - exact: false, - limit: usize::max_value(), - } - } - pub fn only_types(&mut self) { - self.only_types = true; - } - pub fn libs(&mut self) { - self.libs = true; - } - pub fn exact(&mut self) { - self.exact = true; - } - pub fn limit(&mut self, limit: usize) { - self.limit = limit - } -} - -/// `NavigationTarget` represents and element in the editor's UI whihc you can -/// click on to navigate to a particular piece of code. -/// -/// Typically, a `NavigationTarget` corresponds to some element in the source -/// code, like a function or a struct, but this is not strictly required. -#[derive(Debug, Clone)] -pub struct NavigationTarget { - file_id: FileId, - name: SmolStr, - kind: SyntaxKind, - range: TextRange, - // Should be DefId ideally - ptr: Option, -} - -impl NavigationTarget { - fn from_symbol(symbol: FileSymbol) -> NavigationTarget { - NavigationTarget { - file_id: symbol.file_id, - name: symbol.name.clone(), - kind: symbol.ptr.kind(), - range: symbol.ptr.range(), - ptr: Some(symbol.ptr.clone()), - } - } - pub fn name(&self) -> &SmolStr { - &self.name - } - pub fn kind(&self) -> SyntaxKind { - self.kind - } - pub fn file_id(&self) -> FileId { - self.file_id - } - pub fn range(&self) -> TextRange { - self.range - } -} - -#[derive(Debug)] -pub struct RangeInfo { - pub range: TextRange, - pub info: T, -} - -impl RangeInfo { - fn new(range: TextRange, info: T) -> RangeInfo { - RangeInfo { range, info } - } -} - -#[derive(Debug)] -pub struct CallInfo { - pub label: String, - pub doc: Option, - pub parameters: Vec, - pub active_parameter: Option, -} - -/// `AnalysisHost` stores the current state of the world. -#[derive(Debug, Default)] -pub struct AnalysisHost { - db: db::RootDatabase, -} - -impl AnalysisHost { - /// Returns a snapshot of the current state, which you can query for - /// semantic information. - pub fn analysis(&self) -> Analysis { - Analysis { - db: self.db.snapshot(), - } - } - /// Applies changes to the current state of the world. If there are - /// outstanding snapshots, they will be canceled. - pub fn apply_change(&mut self, change: AnalysisChange) { - self.db.apply_change(change) - } -} - -/// Analysis is a snapshot of a world state at a moment in time. It is the main -/// entry point for asking semantic information about the world. When the world -/// state is advanced using `AnalysisHost::apply_change` method, all existing -/// `Analysis` are canceled (most method return `Err(Canceled)`). -#[derive(Debug)] -pub struct Analysis { - db: salsa::Snapshot, -} - -impl Analysis { - /// Gets the text of the source file. - pub fn file_text(&self, file_id: FileId) -> Arc { - self.db.file_text(file_id) - } - /// Gets the syntax tree of the file. - pub fn file_syntax(&self, file_id: FileId) -> TreePtr { - self.db.source_file(file_id).clone() - } - /// Gets the file's `LineIndex`: data structure to convert between absolute - /// offsets and line/column representation. - pub fn file_line_index(&self, file_id: FileId) -> Arc { - self.db.line_index(file_id) - } - /// Selects the next syntactic nodes encopasing the range. - pub fn extend_selection(&self, frange: FileRange) -> TextRange { - extend_selection::extend_selection(&self.db, frange) - } - /// Returns position of the mathcing brace (all types of braces are - /// supported). - pub fn matching_brace(&self, file: &SourceFile, offset: TextUnit) -> Option { - ra_ide_api_light::matching_brace(file, offset) - } - /// Returns a syntax tree represented as `String`, for debug purposes. - // FIXME: use a better name here. - pub fn syntax_tree(&self, file_id: FileId) -> String { - let file = self.db.source_file(file_id); - ra_ide_api_light::syntax_tree(&file) - } - /// Returns an edit to remove all newlines in the range, cleaning up minor - /// stuff like trailing commas. - pub fn join_lines(&self, frange: FileRange) -> SourceChange { - let file = self.db.source_file(frange.file_id); - SourceChange::from_local_edit( - frange.file_id, - ra_ide_api_light::join_lines(&file, frange.range), - ) - } - /// Returns an edit which should be applied when opening a new line, fixing - /// up minor stuff like continuing the comment. - pub fn on_enter(&self, position: FilePosition) -> Option { - let file = self.db.source_file(position.file_id); - let edit = ra_ide_api_light::on_enter(&file, position.offset)?; - Some(SourceChange::from_local_edit(position.file_id, edit)) - } - /// Returns an edit which should be applied after `=` was typed. Primarily, - /// this works when adding `let =`. - // FIXME: use a snippet completion instead of this hack here. - pub fn on_eq_typed(&self, position: FilePosition) -> Option { - let file = self.db.source_file(position.file_id); - let edit = ra_ide_api_light::on_eq_typed(&file, position.offset)?; - Some(SourceChange::from_local_edit(position.file_id, edit)) - } - /// Returns an edit which should be applied when a dot ('.') is typed on a blank line, indenting the line appropriately. - pub fn on_dot_typed(&self, position: FilePosition) -> Option { - let file = self.db.source_file(position.file_id); - let edit = ra_ide_api_light::on_dot_typed(&file, position.offset)?; - Some(SourceChange::from_local_edit(position.file_id, edit)) - } - /// Returns a tree representation of symbols in the file. Useful to draw a - /// file outline. - pub fn file_structure(&self, file_id: FileId) -> Vec { - let file = self.db.source_file(file_id); - ra_ide_api_light::file_structure(&file) - } - /// Returns the set of folding ranges. - pub fn folding_ranges(&self, file_id: FileId) -> Vec { - let file = self.db.source_file(file_id); - ra_ide_api_light::folding_ranges(&file) - } - /// Fuzzy searches for a symbol. - pub fn symbol_search(&self, query: Query) -> Cancelable> { - let res = symbol_index::world_symbols(&*self.db, query)? - .into_iter() - .map(NavigationTarget::from_symbol) - .collect(); - Ok(res) - } - pub fn goto_defenition( - &self, - position: FilePosition, - ) -> Cancelable>> { - goto_defenition::goto_defenition(&*self.db, position) - } - /// Finds all usages of the reference at point. - pub fn find_all_refs(&self, position: FilePosition) -> Cancelable> { - self.db.find_all_refs(position) - } - /// Returns a short text descrbing element at position. - pub fn hover(&self, position: FilePosition) -> Cancelable>> { - hover::hover(&*self.db, position) - } - /// Computes parameter information for the given call expression. - pub fn call_info(&self, position: FilePosition) -> Cancelable> { - call_info::call_info(&*self.db, position) - } - /// Returns a `mod name;` declaration which created the current module. - pub fn parent_module(&self, position: FilePosition) -> Cancelable> { - self.db.parent_module(position) - } - /// Returns crates this file belongs too. - pub fn crate_for(&self, file_id: FileId) -> Cancelable> { - self.db.crate_for(file_id) - } - /// Returns the root file of the given crate. - pub fn crate_root(&self, crate_id: CrateId) -> Cancelable { - Ok(self.db.crate_graph().crate_root(crate_id)) - } - /// Returns the set of possible targets to run for the current file. - pub fn runnables(&self, file_id: FileId) -> Cancelable> { - runnables::runnables(&*self.db, file_id) - } - /// Computes syntax highlighting for the given file. - pub fn highlight(&self, file_id: FileId) -> Cancelable> { - syntax_highlighting::highlight(&*self.db, file_id) - } - /// Computes completions at the given position. - pub fn completions(&self, position: FilePosition) -> Cancelable>> { - let completions = completion::completions(&self.db, position)?; - Ok(completions.map(|it| it.into())) - } - /// Computes assists (aks code actons aka intentions) for the given - /// position. - pub fn assists(&self, frange: FileRange) -> Cancelable> { - Ok(self.db.assists(frange)) - } - /// Computes the set of diagnostics for the given file. - pub fn diagnostics(&self, file_id: FileId) -> Cancelable> { - self.db.diagnostics(file_id) - } - /// Computes the type of the expression at the given position. - pub fn type_of(&self, frange: FileRange) -> Cancelable> { - hover::type_of(&*self.db, frange) - } - /// Returns the edit required to rename reference at the position to the new - /// name. - pub fn rename( - &self, - position: FilePosition, - new_name: &str, - ) -> Cancelable> { - self.db.rename(position, new_name) - } -} - -pub struct LibraryData { - root_id: SourceRootId, - root_change: RootChange, - symbol_index: SymbolIndex, -} - -impl fmt::Debug for LibraryData { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - f.debug_struct("LibraryData") - .field("root_id", &self.root_id) - .field("root_change", &self.root_change) - .field("n_symbols", &self.symbol_index.len()) - .finish() - } -} - -impl LibraryData { - pub fn prepare( - root_id: SourceRootId, - files: Vec<(FileId, RelativePathBuf, Arc)>, - ) -> LibraryData { - let symbol_index = SymbolIndex::for_files(files.par_iter().map(|(file_id, _, text)| { - let file = SourceFile::parse(text); - (*file_id, file) - })); - let mut root_change = RootChange::default(); - root_change.added = files - .into_iter() - .map(|(file_id, path, text)| AddFile { - file_id, - path, - text, - }) - .collect(); - LibraryData { - root_id, - root_change, - symbol_index, - } - } -} - -#[test] -fn analysis_is_send() { - fn is_send() {} - is_send::(); -} diff --git a/crates/ra_analysis/src/mock_analysis.rs b/crates/ra_analysis/src/mock_analysis.rs deleted file mode 100644 index 846c76cfe7ae..000000000000 --- a/crates/ra_analysis/src/mock_analysis.rs +++ /dev/null @@ -1,135 +0,0 @@ -use std::sync::Arc; - -use relative_path::RelativePathBuf; -use test_utils::{extract_offset, extract_range, parse_fixture, CURSOR_MARKER}; -use ra_db::mock::FileMap; - -use crate::{Analysis, AnalysisChange, AnalysisHost, CrateGraph, FileId, FilePosition, FileRange, SourceRootId}; - -/// Mock analysis is used in test to bootstrap an AnalysisHost/Analysis -/// from a set of in-memory files. -#[derive(Debug, Default)] -pub struct MockAnalysis { - files: Vec<(String, String)>, -} - -impl MockAnalysis { - pub fn new() -> MockAnalysis { - MockAnalysis::default() - } - /// Creates `MockAnalysis` using a fixture data in the following format: - /// - /// ```notrust - /// //- /main.rs - /// mod foo; - /// fn main() {} - /// - /// //- /foo.rs - /// struct Baz; - /// ``` - pub fn with_files(fixture: &str) -> MockAnalysis { - let mut res = MockAnalysis::new(); - for entry in parse_fixture(fixture) { - res.add_file(&entry.meta, &entry.text); - } - res - } - - /// Same as `with_files`, but requires that a single file contains a `<|>` marker, - /// whose position is also returned. - pub fn with_files_and_position(fixture: &str) -> (MockAnalysis, FilePosition) { - let mut position = None; - let mut res = MockAnalysis::new(); - for entry in parse_fixture(fixture) { - if entry.text.contains(CURSOR_MARKER) { - assert!( - position.is_none(), - "only one marker (<|>) per fixture is allowed" - ); - position = Some(res.add_file_with_position(&entry.meta, &entry.text)); - } else { - res.add_file(&entry.meta, &entry.text); - } - } - let position = position.expect("expected a marker (<|>)"); - (res, position) - } - - pub fn add_file(&mut self, path: &str, text: &str) -> FileId { - let file_id = FileId((self.files.len() + 1) as u32); - self.files.push((path.to_string(), text.to_string())); - file_id - } - pub fn add_file_with_position(&mut self, path: &str, text: &str) -> FilePosition { - let (offset, text) = extract_offset(text); - let file_id = FileId((self.files.len() + 1) as u32); - self.files.push((path.to_string(), text.to_string())); - FilePosition { file_id, offset } - } - pub fn add_file_with_range(&mut self, path: &str, text: &str) -> FileRange { - let (range, text) = extract_range(text); - let file_id = FileId((self.files.len() + 1) as u32); - self.files.push((path.to_string(), text.to_string())); - FileRange { file_id, range } - } - pub fn id_of(&self, path: &str) -> FileId { - let (idx, _) = self - .files - .iter() - .enumerate() - .find(|(_, (p, _text))| path == p) - .expect("no file in this mock"); - FileId(idx as u32 + 1) - } - pub fn analysis_host(self) -> AnalysisHost { - let mut host = AnalysisHost::default(); - let mut file_map = FileMap::default(); - let source_root = SourceRootId(0); - let mut change = AnalysisChange::new(); - change.add_root(source_root, true); - let mut crate_graph = CrateGraph::default(); - for (path, contents) in self.files.into_iter() { - assert!(path.starts_with('/')); - let path = RelativePathBuf::from_path(&path[1..]).unwrap(); - let file_id = file_map.add(path.clone()); - if path == "/lib.rs" || path == "/main.rs" { - crate_graph.add_crate_root(file_id); - } - change.add_file(source_root, file_id, path, Arc::new(contents)); - } - change.set_crate_graph(crate_graph); - // change.set_file_resolver(Arc::new(file_map)); - host.apply_change(change); - host - } - pub fn analysis(self) -> Analysis { - self.analysis_host().analysis() - } -} - -/// Creates analysis from a multi-file fixture, returns positions marked with <|>. -pub fn analysis_and_position(fixture: &str) -> (Analysis, FilePosition) { - let (mock, position) = MockAnalysis::with_files_and_position(fixture); - (mock.analysis(), position) -} - -/// Creates analysis for a single file. -pub fn single_file(code: &str) -> (Analysis, FileId) { - let mut mock = MockAnalysis::new(); - let file_id = mock.add_file("/main.rs", code); - (mock.analysis(), file_id) -} - -/// Creates analysis for a single file, returns position marked with <|>. -pub fn single_file_with_position(code: &str) -> (Analysis, FilePosition) { - let mut mock = MockAnalysis::new(); - let pos = mock.add_file_with_position("/main.rs", code); - (mock.analysis(), pos) -} - -/// Creates analysis for a single file, returns range marked with a pair of <|>. -pub fn single_file_with_range(code: &str) -> (Analysis, FileRange) { - let mut mock = MockAnalysis::new(); - let pos = mock.add_file_with_range("/main.rs", code); - (mock.analysis(), pos) -} diff --git a/crates/ra_analysis/src/runnables.rs b/crates/ra_analysis/src/runnables.rs deleted file mode 100644 index 98b1d2d55211..000000000000 --- a/crates/ra_analysis/src/runnables.rs +++ /dev/null @@ -1,89 +0,0 @@ -use itertools::Itertools; -use ra_syntax::{ - TextRange, SyntaxNode, - ast::{self, AstNode, NameOwner, ModuleItemOwner}, -}; -use ra_db::{Cancelable, SyntaxDatabase}; - -use crate::{db::RootDatabase, FileId}; - -#[derive(Debug)] -pub struct Runnable { - pub range: TextRange, - pub kind: RunnableKind, -} - -#[derive(Debug)] -pub enum RunnableKind { - Test { name: String }, - TestMod { path: String }, - Bin, -} - -pub(crate) fn runnables(db: &RootDatabase, file_id: FileId) -> Cancelable> { - let source_file = db.source_file(file_id); - let res = source_file - .syntax() - .descendants() - .filter_map(|i| runnable(db, file_id, i)) - .collect(); - Ok(res) -} - -fn runnable(db: &RootDatabase, file_id: FileId, item: &SyntaxNode) -> Option { - if let Some(fn_def) = ast::FnDef::cast(item) { - runnable_fn(fn_def) - } else if let Some(m) = ast::Module::cast(item) { - runnable_mod(db, file_id, m) - } else { - None - } -} - -fn runnable_fn(fn_def: &ast::FnDef) -> Option { - let name = fn_def.name()?.text(); - let kind = if name == "main" { - RunnableKind::Bin - } else if fn_def.has_atom_attr("test") { - RunnableKind::Test { - name: name.to_string(), - } - } else { - return None; - }; - Some(Runnable { - range: fn_def.syntax().range(), - kind, - }) -} - -fn runnable_mod(db: &RootDatabase, file_id: FileId, module: &ast::Module) -> Option { - let has_test_function = module - .item_list()? - .items() - .filter_map(|it| match it.kind() { - ast::ModuleItemKind::FnDef(it) => Some(it), - _ => None, - }) - .any(|f| f.has_atom_attr("test")); - if !has_test_function { - return None; - } - let range = module.syntax().range(); - let module = - hir::source_binder::module_from_child_node(db, file_id, module.syntax()).ok()??; - - // FIXME: thread cancellation instead of `.ok`ing - let path = module - .path_to_root(db) - .ok()? - .into_iter() - .rev() - .filter_map(|it| it.name(db).ok()) - .filter_map(|it| it) - .join("::"); - Some(Runnable { - range, - kind: RunnableKind::TestMod { path }, - }) -} diff --git a/crates/ra_analysis/src/symbol_index.rs b/crates/ra_analysis/src/symbol_index.rs deleted file mode 100644 index 8dd15b40e573..000000000000 --- a/crates/ra_analysis/src/symbol_index.rs +++ /dev/null @@ -1,222 +0,0 @@ -//! This module handles fuzzy-searching of functions, structs and other symbols -//! by name across the whole workspace and dependencies. -//! -//! It works by building an incrementally-updated text-search index of all -//! symbols. The backbone of the index is the **awesome** `fst` crate by -//! @BurntSushi. -//! -//! In a nutshell, you give a set of strings to the `fst`, and it builds a -//! finite state machine describing this set of strtings. The strings which -//! could fuzzy-match a pattern can also be described by a finite state machine. -//! What is freakingly cool is that you can now traverse both state machines in -//! lock-step to enumerate the strings which are both in the input set and -//! fuzz-match the query. Or, more formally, given two langauges described by -//! fsts, one can build an product fst which describes the intersection of the -//! languages. -//! -//! `fst` does not support cheap updating of the index, but it supports unioning -//! of state machines. So, to account for changing source code, we build an fst -//! for each library (which is assumed to never change) and an fst for each rust -//! file in the current workspace, and run a query aginst the union of all -//! thouse fsts. -use std::{ - cmp::Ordering, - hash::{Hash, Hasher}, - sync::Arc, -}; - -use fst::{self, Streamer}; -use ra_syntax::{ - SyntaxNode, SourceFile, SmolStr, TreePtr, AstNode, - algo::{visit::{visitor, Visitor}, find_covering_node}, - SyntaxKind::{self, *}, - ast::{self, NameOwner}, -}; -use ra_db::{SourceRootId, FilesDatabase, LocalSyntaxPtr}; -use salsa::ParallelDatabase; -use rayon::prelude::*; - -use crate::{ - Cancelable, FileId, Query, - db::RootDatabase, -}; - -salsa::query_group! { - pub(crate) trait SymbolsDatabase: hir::db::HirDatabase { - fn file_symbols(file_id: FileId) -> Cancelable> { - type FileSymbolsQuery; - } - fn library_symbols(id: SourceRootId) -> Arc { - type LibrarySymbolsQuery; - storage input; - } - } -} - -fn file_symbols(db: &impl SymbolsDatabase, file_id: FileId) -> Cancelable> { - db.check_canceled()?; - let source_file = db.source_file(file_id); - let mut symbols = source_file - .syntax() - .descendants() - .filter_map(to_symbol) - .map(move |(name, ptr)| FileSymbol { name, ptr, file_id }) - .collect::>(); - - for (name, text_range) in hir::source_binder::macro_symbols(db, file_id)? { - let node = find_covering_node(source_file.syntax(), text_range); - let ptr = LocalSyntaxPtr::new(node); - symbols.push(FileSymbol { file_id, name, ptr }) - } - - Ok(Arc::new(SymbolIndex::new(symbols))) -} - -pub(crate) fn world_symbols(db: &RootDatabase, query: Query) -> Cancelable> { - /// Need to wrap Snapshot to provide `Clone` impl for `map_with` - struct Snap(salsa::Snapshot); - impl Clone for Snap { - fn clone(&self) -> Snap { - Snap(self.0.snapshot()) - } - } - - let buf: Vec> = if query.libs { - let snap = Snap(db.snapshot()); - db.library_roots() - .par_iter() - .map_with(snap, |db, &lib_id| db.0.library_symbols(lib_id)) - .collect() - } else { - let mut files = Vec::new(); - for &root in db.local_roots().iter() { - let sr = db.source_root(root); - files.extend(sr.files.values().map(|&it| it)) - } - - let snap = Snap(db.snapshot()); - files - .par_iter() - .map_with(snap, |db, &file_id| db.0.file_symbols(file_id)) - .filter_map(|it| it.ok()) - .collect() - }; - Ok(query.search(&buf)) -} - -#[derive(Default, Debug)] -pub(crate) struct SymbolIndex { - symbols: Vec, - map: fst::Map, -} - -impl PartialEq for SymbolIndex { - fn eq(&self, other: &SymbolIndex) -> bool { - self.symbols == other.symbols - } -} - -impl Eq for SymbolIndex {} - -impl Hash for SymbolIndex { - fn hash(&self, hasher: &mut H) { - self.symbols.hash(hasher) - } -} - -impl SymbolIndex { - fn new(mut symbols: Vec) -> SymbolIndex { - fn cmp(s1: &FileSymbol, s2: &FileSymbol) -> Ordering { - unicase::Ascii::new(s1.name.as_str()).cmp(&unicase::Ascii::new(s2.name.as_str())) - } - symbols.par_sort_by(cmp); - symbols.dedup_by(|s1, s2| cmp(s1, s2) == Ordering::Equal); - let names = symbols.iter().map(|it| it.name.as_str().to_lowercase()); - let map = fst::Map::from_iter(names.into_iter().zip(0u64..)).unwrap(); - SymbolIndex { symbols, map } - } - - pub(crate) fn len(&self) -> usize { - self.symbols.len() - } - - pub(crate) fn for_files( - files: impl ParallelIterator)>, - ) -> SymbolIndex { - let symbols = files - .flat_map(|(file_id, file)| { - file.syntax() - .descendants() - .filter_map(to_symbol) - .map(move |(name, ptr)| FileSymbol { name, ptr, file_id }) - .collect::>() - }) - .collect::>(); - SymbolIndex::new(symbols) - } -} - -impl Query { - pub(crate) fn search(self, indices: &[Arc]) -> Vec { - let mut op = fst::map::OpBuilder::new(); - for file_symbols in indices.iter() { - let automaton = fst::automaton::Subsequence::new(&self.lowercased); - op = op.add(file_symbols.map.search(automaton)) - } - let mut stream = op.union(); - let mut res = Vec::new(); - while let Some((_, indexed_values)) = stream.next() { - if res.len() >= self.limit { - break; - } - for indexed_value in indexed_values { - let file_symbols = &indices[indexed_value.index]; - let idx = indexed_value.value as usize; - - let symbol = &file_symbols.symbols[idx]; - if self.only_types && !is_type(symbol.ptr.kind()) { - continue; - } - if self.exact && symbol.name != self.query { - continue; - } - res.push(symbol.clone()); - } - } - res - } -} - -fn is_type(kind: SyntaxKind) -> bool { - match kind { - STRUCT_DEF | ENUM_DEF | TRAIT_DEF | TYPE_DEF => true, - _ => false, - } -} - -/// The actual data that is stored in the index. It should be as compact as -/// possible. -#[derive(Debug, Clone, PartialEq, Eq, Hash)] -pub(crate) struct FileSymbol { - pub(crate) file_id: FileId, - pub(crate) name: SmolStr, - pub(crate) ptr: LocalSyntaxPtr, -} - -fn to_symbol(node: &SyntaxNode) -> Option<(SmolStr, LocalSyntaxPtr)> { - fn decl(node: &N) -> Option<(SmolStr, LocalSyntaxPtr)> { - let name = node.name()?.text().clone(); - let ptr = LocalSyntaxPtr::new(node.syntax()); - Some((name, ptr)) - } - visitor() - .visit(decl::) - .visit(decl::) - .visit(decl::) - .visit(decl::) - .visit(decl::) - .visit(decl::) - .visit(decl::) - .visit(decl::) - .accept(node)? -} diff --git a/crates/ra_analysis/src/syntax_highlighting.rs b/crates/ra_analysis/src/syntax_highlighting.rs deleted file mode 100644 index cb19e9515a14..000000000000 --- a/crates/ra_analysis/src/syntax_highlighting.rs +++ /dev/null @@ -1,92 +0,0 @@ -use ra_syntax::{ast, AstNode,}; -use ra_db::SyntaxDatabase; - -use crate::{ - FileId, Cancelable, HighlightedRange, - db::RootDatabase, -}; - -pub(crate) fn highlight(db: &RootDatabase, file_id: FileId) -> Cancelable> { - let source_file = db.source_file(file_id); - let mut res = ra_ide_api_light::highlight(source_file.syntax()); - for macro_call in source_file - .syntax() - .descendants() - .filter_map(ast::MacroCall::cast) - { - if let Some((off, exp)) = hir::MacroDef::ast_expand(macro_call) { - let mapped_ranges = ra_ide_api_light::highlight(&exp.syntax()) - .into_iter() - .filter_map(|r| { - let mapped_range = exp.map_range_back(r.range)?; - let res = HighlightedRange { - range: mapped_range + off, - tag: r.tag, - }; - Some(res) - }); - res.extend(mapped_ranges); - } - } - Ok(res) -} - -#[cfg(test)] -mod tests { - use crate::mock_analysis::single_file; - use test_utils::assert_eq_dbg; - - #[test] - fn highlights_code_inside_macros() { - let (analysis, file_id) = single_file( - " - fn main() { - ctry!({ let x = 92; x}); - vec![{ let x = 92; x}]; - } - ", - ); - let highlights = analysis.highlight(file_id).unwrap(); - assert_eq_dbg( - r#"[HighlightedRange { range: [13; 15), tag: "keyword" }, - HighlightedRange { range: [16; 20), tag: "function" }, - HighlightedRange { range: [41; 46), tag: "macro" }, - HighlightedRange { range: [49; 52), tag: "keyword" }, - HighlightedRange { range: [57; 59), tag: "literal" }, - HighlightedRange { range: [82; 86), tag: "macro" }, - HighlightedRange { range: [89; 92), tag: "keyword" }, - HighlightedRange { range: [97; 99), tag: "literal" }, - HighlightedRange { range: [49; 52), tag: "keyword" }, - HighlightedRange { range: [53; 54), tag: "function" }, - HighlightedRange { range: [57; 59), tag: "literal" }, - HighlightedRange { range: [61; 62), tag: "text" }, - HighlightedRange { range: [89; 92), tag: "keyword" }, - HighlightedRange { range: [93; 94), tag: "function" }, - HighlightedRange { range: [97; 99), tag: "literal" }, - HighlightedRange { range: [101; 102), tag: "text" }]"#, - &highlights, - ) - } - - // FIXME: this test is not really necessary: artifact of the inital hacky - // macros implementation. - #[test] - fn highlight_query_group_macro() { - let (analysis, file_id) = single_file( - " - salsa::query_group! { - pub trait HirDatabase: SyntaxDatabase {} - } - ", - ); - let highlights = analysis.highlight(file_id).unwrap(); - assert_eq_dbg( - r#"[HighlightedRange { range: [20; 32), tag: "macro" }, - HighlightedRange { range: [13; 18), tag: "text" }, - HighlightedRange { range: [51; 54), tag: "keyword" }, - HighlightedRange { range: [55; 60), tag: "keyword" }, - HighlightedRange { range: [61; 72), tag: "function" }]"#, - &highlights, - ) - } -} diff --git a/crates/ra_analysis/tests/test/main.rs b/crates/ra_analysis/tests/test/main.rs deleted file mode 100644 index 2c0735cb50cf..000000000000 --- a/crates/ra_analysis/tests/test/main.rs +++ /dev/null @@ -1,249 +0,0 @@ -mod runnables; - -use ra_syntax::TextRange; -use test_utils::{assert_eq_dbg, assert_eq_text}; - -use ra_analysis::{ - mock_analysis::{analysis_and_position, single_file, single_file_with_position, MockAnalysis}, - AnalysisChange, CrateGraph, FileId, Query -}; - -#[test] -fn test_unresolved_module_diagnostic() { - let (analysis, file_id) = single_file("mod foo;"); - let diagnostics = analysis.diagnostics(file_id).unwrap(); - assert_eq_dbg( - r#"[Diagnostic { - message: "unresolved module", - range: [4; 7), - fix: Some(SourceChange { - label: "create module", - source_file_edits: [], - file_system_edits: [CreateFile { source_root: SourceRootId(0), path: "foo.rs" }], - cursor_position: None }), - severity: Error }]"#, - &diagnostics, - ); -} - -// FIXME: move this test to hir -#[test] -fn test_unresolved_module_diagnostic_no_diag_for_inline_mode() { - let (analysis, file_id) = single_file("mod foo {}"); - let diagnostics = analysis.diagnostics(file_id).unwrap(); - assert_eq_dbg(r#"[]"#, &diagnostics); -} - -#[test] -fn test_resolve_parent_module() { - let (analysis, pos) = analysis_and_position( - " - //- /lib.rs - mod foo; - //- /foo.rs - <|>// empty - ", - ); - let symbols = analysis.parent_module(pos).unwrap(); - assert_eq_dbg( - r#"[NavigationTarget { file_id: FileId(1), name: "foo", kind: MODULE, range: [4; 7), ptr: None }]"#, - &symbols, - ); -} - -#[test] -fn test_resolve_parent_module_for_inline() { - let (analysis, pos) = analysis_and_position( - " - //- /lib.rs - mod foo { - mod bar { - mod baz { <|> } - } - } - ", - ); - let symbols = analysis.parent_module(pos).unwrap(); - assert_eq_dbg( - r#"[NavigationTarget { file_id: FileId(1), name: "baz", kind: MODULE, range: [36; 39), ptr: None }]"#, - &symbols, - ); -} - -#[test] -fn test_resolve_crate_root() { - let mock = MockAnalysis::with_files( - " - //- /bar.rs - mod foo; - //- /bar/foo.rs - // emtpy <|> - ", - ); - let root_file = mock.id_of("/bar.rs"); - let mod_file = mock.id_of("/bar/foo.rs"); - let mut host = mock.analysis_host(); - assert!(host.analysis().crate_for(mod_file).unwrap().is_empty()); - - let mut crate_graph = CrateGraph::default(); - let crate_id = crate_graph.add_crate_root(root_file); - let mut change = AnalysisChange::new(); - change.set_crate_graph(crate_graph); - host.apply_change(change); - - assert_eq!(host.analysis().crate_for(mod_file).unwrap(), vec![crate_id]); -} - -fn get_all_refs(text: &str) -> Vec<(FileId, TextRange)> { - let (analysis, position) = single_file_with_position(text); - analysis.find_all_refs(position).unwrap() -} - -#[test] -fn test_find_all_refs_for_local() { - let code = r#" - fn main() { - let mut i = 1; - let j = 1; - i = i<|> + j; - - { - i = 0; - } - - i = 5; - }"#; - - let refs = get_all_refs(code); - assert_eq!(refs.len(), 5); -} - -#[test] -fn test_find_all_refs_for_param_inside() { - let code = r#" - fn foo(i : u32) -> u32 { - i<|> - }"#; - - let refs = get_all_refs(code); - assert_eq!(refs.len(), 2); -} - -#[test] -fn test_find_all_refs_for_fn_param() { - let code = r#" - fn foo(i<|> : u32) -> u32 { - i - }"#; - - let refs = get_all_refs(code); - assert_eq!(refs.len(), 2); -} -#[test] -fn test_rename_for_local() { - test_rename( - r#" - fn main() { - let mut i = 1; - let j = 1; - i = i<|> + j; - - { - i = 0; - } - - i = 5; - }"#, - "k", - r#" - fn main() { - let mut k = 1; - let j = 1; - k = k + j; - - { - k = 0; - } - - k = 5; - }"#, - ); -} - -#[test] -fn test_rename_for_param_inside() { - test_rename( - r#" - fn foo(i : u32) -> u32 { - i<|> - }"#, - "j", - r#" - fn foo(j : u32) -> u32 { - j - }"#, - ); -} - -#[test] -fn test_rename_refs_for_fn_param() { - test_rename( - r#" - fn foo(i<|> : u32) -> u32 { - i - }"#, - "new_name", - r#" - fn foo(new_name : u32) -> u32 { - new_name - }"#, - ); -} - -#[test] -fn test_rename_for_mut_param() { - test_rename( - r#" - fn foo(mut i<|> : u32) -> u32 { - i - }"#, - "new_name", - r#" - fn foo(mut new_name : u32) -> u32 { - new_name - }"#, - ); -} - -fn test_rename(text: &str, new_name: &str, expected: &str) { - let (analysis, position) = single_file_with_position(text); - let edits = analysis.rename(position, new_name).unwrap(); - let mut text_edit_bulder = ra_text_edit::TextEditBuilder::default(); - let mut file_id: Option = None; - for edit in edits { - file_id = Some(edit.file_id); - for atom in edit.edit.as_atoms() { - text_edit_bulder.replace(atom.delete, atom.insert.clone()); - } - } - let result = text_edit_bulder - .finish() - .apply(&*analysis.file_text(file_id.unwrap())); - assert_eq_text!(expected, &*result); -} - -#[test] -fn world_symbols_include_stuff_from_macros() { - let (analysis, _) = single_file( - " -salsa::query_group! { -pub trait HirDatabase: SyntaxDatabase {} -} - ", - ); - - let mut symbols = analysis.symbol_search(Query::new("Hir".into())).unwrap(); - let s = symbols.pop().unwrap(); - assert_eq!(s.name(), "HirDatabase"); - assert_eq!(s.range(), TextRange::from_to(33.into(), 44.into())); -} diff --git a/crates/ra_analysis/tests/test/runnables.rs b/crates/ra_analysis/tests/test/runnables.rs deleted file mode 100644 index e6e0afbc317c..000000000000 --- a/crates/ra_analysis/tests/test/runnables.rs +++ /dev/null @@ -1,109 +0,0 @@ -use test_utils::assert_eq_dbg; - -use ra_analysis::mock_analysis::analysis_and_position; - -#[test] -fn test_runnables() { - let (analysis, pos) = analysis_and_position( - r#" - //- /lib.rs - <|> //empty - fn main() {} - - #[test] - fn test_foo() {} - - #[test] - #[ignore] - fn test_foo() {} - "#, - ); - let runnables = analysis.runnables(pos.file_id).unwrap(); - assert_eq_dbg( - r#"[Runnable { range: [1; 21), kind: Bin }, - Runnable { range: [22; 46), kind: Test { name: "test_foo" } }, - Runnable { range: [47; 81), kind: Test { name: "test_foo" } }]"#, - &runnables, - ) -} - -#[test] -fn test_runnables_module() { - let (analysis, pos) = analysis_and_position( - r#" - //- /lib.rs - <|> //empty - mod test_mod { - #[test] - fn test_foo1() {} - } - "#, - ); - let runnables = analysis.runnables(pos.file_id).unwrap(); - assert_eq_dbg( - r#"[Runnable { range: [1; 59), kind: TestMod { path: "test_mod" } }, - Runnable { range: [28; 57), kind: Test { name: "test_foo1" } }]"#, - &runnables, - ) -} - -#[test] -fn test_runnables_one_depth_layer_module() { - let (analysis, pos) = analysis_and_position( - r#" - //- /lib.rs - <|> //empty - mod foo { - mod test_mod { - #[test] - fn test_foo1() {} - } - } - "#, - ); - let runnables = analysis.runnables(pos.file_id).unwrap(); - assert_eq_dbg( - r#"[Runnable { range: [23; 85), kind: TestMod { path: "foo::test_mod" } }, - Runnable { range: [46; 79), kind: Test { name: "test_foo1" } }]"#, - &runnables, - ) -} - -#[test] -fn test_runnables_multiple_depth_module() { - let (analysis, pos) = analysis_and_position( - r#" - //- /lib.rs - <|> //empty - mod foo { - mod bar { - mod test_mod { - #[test] - fn test_foo1() {} - } - } - } - "#, - ); - let runnables = analysis.runnables(pos.file_id).unwrap(); - assert_eq_dbg( - r#"[Runnable { range: [41; 115), kind: TestMod { path: "foo::bar::test_mod" } }, - Runnable { range: [68; 105), kind: Test { name: "test_foo1" } }]"#, - &runnables, - ) -} - -#[test] -fn test_runnables_no_test_function_in_module() { - let (analysis, pos) = analysis_and_position( - r#" - //- /lib.rs - <|> //empty - mod test_mod { - fn foo1() {} - } - "#, - ); - let runnables = analysis.runnables(pos.file_id).unwrap(); - assert_eq_dbg(r#"[]"#, &runnables) -} From 5b573deb20b15451788dd2861e9fc6e69ed0472e Mon Sep 17 00:00:00 2001 From: Aleksey Kladov Date: Tue, 8 Jan 2019 22:33:36 +0300 Subject: [PATCH 4/5] fix usages after rename --- Cargo.lock | 44 +- crates/ra_ide_api/Cargo.toml | 23 + crates/ra_ide_api/src/call_info.rs | 451 ++++++++++++++++ crates/ra_ide_api/src/completion.rs | 77 +++ .../ra_ide_api/src/completion/complete_dot.rs | 121 +++++ .../src/completion/complete_fn_param.rs | 102 ++++ .../src/completion/complete_keyword.rs | 339 ++++++++++++ .../src/completion/complete_path.rs | 128 +++++ .../src/completion/complete_scope.rs | 192 +++++++ .../src/completion/complete_snippet.rs | 73 +++ .../src/completion/completion_context.rs | 205 +++++++ .../src/completion/completion_item.rs | 244 +++++++++ crates/ra_ide_api/src/db.rs | 128 +++++ crates/ra_ide_api/src/extend_selection.rs | 56 ++ crates/ra_ide_api/src/goto_defenition.rs | 139 +++++ crates/ra_ide_api/src/hover.rs | 257 +++++++++ crates/ra_ide_api/src/imp.rs | 309 +++++++++++ crates/ra_ide_api/src/lib.rs | 509 ++++++++++++++++++ crates/ra_ide_api/src/mock_analysis.rs | 135 +++++ crates/ra_ide_api/src/runnables.rs | 89 +++ crates/ra_ide_api/src/symbol_index.rs | 222 ++++++++ crates/ra_ide_api/src/syntax_highlighting.rs | 92 ++++ crates/ra_ide_api/tests/test/main.rs | 249 +++++++++ crates/ra_ide_api/tests/test/runnables.rs | 109 ++++ crates/ra_lsp_server/Cargo.toml | 2 +- crates/ra_lsp_server/src/conv.rs | 2 +- crates/ra_lsp_server/src/main_loop.rs | 2 +- .../ra_lsp_server/src/main_loop/handlers.rs | 4 +- .../src/main_loop/subscriptions.rs | 2 +- crates/ra_lsp_server/src/server_world.rs | 6 +- 30 files changed, 4280 insertions(+), 31 deletions(-) create mode 100644 crates/ra_ide_api/Cargo.toml create mode 100644 crates/ra_ide_api/src/call_info.rs create mode 100644 crates/ra_ide_api/src/completion.rs create mode 100644 crates/ra_ide_api/src/completion/complete_dot.rs create mode 100644 crates/ra_ide_api/src/completion/complete_fn_param.rs create mode 100644 crates/ra_ide_api/src/completion/complete_keyword.rs create mode 100644 crates/ra_ide_api/src/completion/complete_path.rs create mode 100644 crates/ra_ide_api/src/completion/complete_scope.rs create mode 100644 crates/ra_ide_api/src/completion/complete_snippet.rs create mode 100644 crates/ra_ide_api/src/completion/completion_context.rs create mode 100644 crates/ra_ide_api/src/completion/completion_item.rs create mode 100644 crates/ra_ide_api/src/db.rs create mode 100644 crates/ra_ide_api/src/extend_selection.rs create mode 100644 crates/ra_ide_api/src/goto_defenition.rs create mode 100644 crates/ra_ide_api/src/hover.rs create mode 100644 crates/ra_ide_api/src/imp.rs create mode 100644 crates/ra_ide_api/src/lib.rs create mode 100644 crates/ra_ide_api/src/mock_analysis.rs create mode 100644 crates/ra_ide_api/src/runnables.rs create mode 100644 crates/ra_ide_api/src/symbol_index.rs create mode 100644 crates/ra_ide_api/src/syntax_highlighting.rs create mode 100644 crates/ra_ide_api/tests/test/main.rs create mode 100644 crates/ra_ide_api/tests/test/runnables.rs diff --git a/Cargo.lock b/Cargo.lock index f99a0342432e..354cc138b96b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -636,27 +636,6 @@ dependencies = [ "proc-macro2 0.4.24 (registry+https://github.com/rust-lang/crates.io-index)", ] -[[package]] -name = "ra_analysis" -version = "0.1.0" -dependencies = [ - "fst 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)", - "itertools 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)", - "log 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)", - "parking_lot 0.7.1 (registry+https://github.com/rust-lang/crates.io-index)", - "ra_db 0.1.0", - "ra_hir 0.1.0", - "ra_ide_api_light 0.1.0", - "ra_syntax 0.1.0", - "ra_text_edit 0.1.0", - "rayon 1.0.3 (registry+https://github.com/rust-lang/crates.io-index)", - "relative-path 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", - "rustc-hash 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)", - "salsa 0.9.1 (registry+https://github.com/rust-lang/crates.io-index)", - "test_utils 0.1.0", - "unicase 2.2.0 (registry+https://github.com/rust-lang/crates.io-index)", -] - [[package]] name = "ra_arena" version = "0.1.0" @@ -704,6 +683,27 @@ dependencies = [ "test_utils 0.1.0", ] +[[package]] +name = "ra_ide_api" +version = "0.1.0" +dependencies = [ + "fst 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)", + "itertools 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)", + "log 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)", + "parking_lot 0.7.1 (registry+https://github.com/rust-lang/crates.io-index)", + "ra_db 0.1.0", + "ra_hir 0.1.0", + "ra_ide_api_light 0.1.0", + "ra_syntax 0.1.0", + "ra_text_edit 0.1.0", + "rayon 1.0.3 (registry+https://github.com/rust-lang/crates.io-index)", + "relative-path 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", + "rustc-hash 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)", + "salsa 0.9.1 (registry+https://github.com/rust-lang/crates.io-index)", + "test_utils 0.1.0", + "unicase 2.2.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "ra_ide_api_light" version = "0.1.0" @@ -733,7 +733,7 @@ dependencies = [ "languageserver-types 0.53.1 (registry+https://github.com/rust-lang/crates.io-index)", "log 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)", "parking_lot 0.7.1 (registry+https://github.com/rust-lang/crates.io-index)", - "ra_analysis 0.1.0", + "ra_ide_api 0.1.0", "ra_syntax 0.1.0", "ra_text_edit 0.1.0", "ra_vfs 0.1.0", diff --git a/crates/ra_ide_api/Cargo.toml b/crates/ra_ide_api/Cargo.toml new file mode 100644 index 000000000000..d42a664b65bf --- /dev/null +++ b/crates/ra_ide_api/Cargo.toml @@ -0,0 +1,23 @@ +[package] +edition = "2018" +name = "ra_ide_api" +version = "0.1.0" +authors = ["Aleksey Kladov "] + +[dependencies] +itertools = "0.8.0" +log = "0.4.5" +relative-path = "0.4.0" +rayon = "1.0.2" +fst = "0.3.1" +salsa = "0.9.1" +rustc-hash = "1.0" +parking_lot = "0.7.0" +unicase = "2.2.0" + +ra_syntax = { path = "../ra_syntax" } +ra_ide_api_light = { path = "../ra_ide_api_light" } +ra_text_edit = { path = "../ra_text_edit" } +ra_db = { path = "../ra_db" } +hir = { path = "../ra_hir", package = "ra_hir" } +test_utils = { path = "../test_utils" } diff --git a/crates/ra_ide_api/src/call_info.rs b/crates/ra_ide_api/src/call_info.rs new file mode 100644 index 000000000000..27b760780ca8 --- /dev/null +++ b/crates/ra_ide_api/src/call_info.rs @@ -0,0 +1,451 @@ +use std::cmp::{max, min}; + +use ra_db::{SyntaxDatabase, Cancelable}; +use ra_syntax::{ + AstNode, SyntaxNode, TextUnit, TextRange, + SyntaxKind::FN_DEF, + ast::{self, ArgListOwner, DocCommentsOwner}, + algo::find_node_at_offset, +}; + +use crate::{FilePosition, CallInfo, db::RootDatabase}; + +/// Computes parameter information for the given call expression. +pub(crate) fn call_info(db: &RootDatabase, position: FilePosition) -> Cancelable> { + let file = db.source_file(position.file_id); + let syntax = file.syntax(); + + // Find the calling expression and it's NameRef + let calling_node = ctry!(FnCallNode::with_node(syntax, position.offset)); + let name_ref = ctry!(calling_node.name_ref()); + + // Resolve the function's NameRef (NOTE: this isn't entirely accurate). + let file_symbols = db.index_resolve(name_ref)?; + let symbol = ctry!(file_symbols.into_iter().find(|it| it.ptr.kind() == FN_DEF)); + let fn_file = db.source_file(symbol.file_id); + let fn_def = symbol.ptr.resolve(&fn_file); + let fn_def = ast::FnDef::cast(&fn_def).unwrap(); + let mut call_info = ctry!(CallInfo::new(fn_def)); + // If we have a calling expression let's find which argument we are on + let num_params = call_info.parameters.len(); + let has_self = fn_def.param_list().and_then(|l| l.self_param()).is_some(); + + if num_params == 1 { + if !has_self { + call_info.active_parameter = Some(0); + } + } else if num_params > 1 { + // Count how many parameters into the call we are. + // TODO: This is best effort for now and should be fixed at some point. + // It may be better to see where we are in the arg_list and then check + // where offset is in that list (or beyond). + // Revisit this after we get documentation comments in. + if let Some(ref arg_list) = calling_node.arg_list() { + let start = arg_list.syntax().range().start(); + + let range_search = TextRange::from_to(start, position.offset); + let mut commas: usize = arg_list + .syntax() + .text() + .slice(range_search) + .to_string() + .matches(',') + .count(); + + // If we have a method call eat the first param since it's just self. + if has_self { + commas += 1; + } + + call_info.active_parameter = Some(commas); + } + } + + Ok(Some(call_info)) +} + +enum FnCallNode<'a> { + CallExpr(&'a ast::CallExpr), + MethodCallExpr(&'a ast::MethodCallExpr), +} + +impl<'a> FnCallNode<'a> { + pub fn with_node(syntax: &'a SyntaxNode, offset: TextUnit) -> Option> { + if let Some(expr) = find_node_at_offset::(syntax, offset) { + return Some(FnCallNode::CallExpr(expr)); + } + if let Some(expr) = find_node_at_offset::(syntax, offset) { + return Some(FnCallNode::MethodCallExpr(expr)); + } + None + } + + pub fn name_ref(&self) -> Option<&'a ast::NameRef> { + match *self { + FnCallNode::CallExpr(call_expr) => Some(match call_expr.expr()?.kind() { + ast::ExprKind::PathExpr(path_expr) => path_expr.path()?.segment()?.name_ref()?, + _ => return None, + }), + + FnCallNode::MethodCallExpr(call_expr) => call_expr + .syntax() + .children() + .filter_map(ast::NameRef::cast) + .nth(0), + } + } + + pub fn arg_list(&self) -> Option<&'a ast::ArgList> { + match *self { + FnCallNode::CallExpr(expr) => expr.arg_list(), + FnCallNode::MethodCallExpr(expr) => expr.arg_list(), + } + } +} + +impl CallInfo { + fn new(node: &ast::FnDef) -> Option { + let mut doc = None; + + // Strip the body out for the label. + let mut label: String = if let Some(body) = node.body() { + let body_range = body.syntax().range(); + let label: String = node + .syntax() + .children() + .filter(|child| !child.range().is_subrange(&body_range)) + .map(|node| node.text().to_string()) + .collect(); + label + } else { + node.syntax().text().to_string() + }; + + if let Some((comment_range, docs)) = extract_doc_comments(node) { + let comment_range = comment_range + .checked_sub(node.syntax().range().start()) + .unwrap(); + let start = comment_range.start().to_usize(); + let end = comment_range.end().to_usize(); + + // Remove the comment from the label + label.replace_range(start..end, ""); + + // Massage markdown + let mut processed_lines = Vec::new(); + let mut in_code_block = false; + for line in docs.lines() { + if line.starts_with("```") { + in_code_block = !in_code_block; + } + + let line = if in_code_block && line.starts_with("```") && !line.contains("rust") { + "```rust".into() + } else { + line.to_string() + }; + + processed_lines.push(line); + } + + if !processed_lines.is_empty() { + doc = Some(processed_lines.join("\n")); + } + } + + Some(CallInfo { + parameters: param_list(node), + label: label.trim().to_owned(), + doc, + active_parameter: None, + }) + } +} + +fn extract_doc_comments(node: &ast::FnDef) -> Option<(TextRange, String)> { + if node.doc_comments().count() == 0 { + return None; + } + + let comment_text = node.doc_comment_text(); + + let (begin, end) = node + .doc_comments() + .map(|comment| comment.syntax().range()) + .map(|range| (range.start().to_usize(), range.end().to_usize())) + .fold((std::usize::MAX, std::usize::MIN), |acc, range| { + (min(acc.0, range.0), max(acc.1, range.1)) + }); + + let range = TextRange::from_to(TextUnit::from_usize(begin), TextUnit::from_usize(end)); + + Some((range, comment_text)) +} + +fn param_list(node: &ast::FnDef) -> Vec { + let mut res = vec![]; + if let Some(param_list) = node.param_list() { + if let Some(self_param) = param_list.self_param() { + res.push(self_param.syntax().text().to_string()) + } + + // Maybe use param.pat here? See if we can just extract the name? + //res.extend(param_list.params().map(|p| p.syntax().text().to_string())); + res.extend( + param_list + .params() + .filter_map(|p| p.pat()) + .map(|pat| pat.syntax().text().to_string()), + ); + } + res +} + +#[cfg(test)] +mod tests { + use super::*; + + use crate::mock_analysis::single_file_with_position; + + fn call_info(text: &str) -> CallInfo { + let (analysis, position) = single_file_with_position(text); + analysis.call_info(position).unwrap().unwrap() + } + + #[test] + fn test_fn_signature_two_args_first() { + let info = call_info( + r#"fn foo(x: u32, y: u32) -> u32 {x + y} +fn bar() { foo(<|>3, ); }"#, + ); + + assert_eq!(info.parameters, vec!("x".to_string(), "y".to_string())); + assert_eq!(info.active_parameter, Some(0)); + } + + #[test] + fn test_fn_signature_two_args_second() { + let info = call_info( + r#"fn foo(x: u32, y: u32) -> u32 {x + y} +fn bar() { foo(3, <|>); }"#, + ); + + assert_eq!(info.parameters, vec!("x".to_string(), "y".to_string())); + assert_eq!(info.active_parameter, Some(1)); + } + + #[test] + fn test_fn_signature_for_impl() { + let info = call_info( + r#"struct F; impl F { pub fn new() { F{}} } +fn bar() {let _ : F = F::new(<|>);}"#, + ); + + assert_eq!(info.parameters, Vec::::new()); + assert_eq!(info.active_parameter, None); + } + + #[test] + fn test_fn_signature_for_method_self() { + let info = call_info( + r#"struct F; +impl F { + pub fn new() -> F{ + F{} + } + + pub fn do_it(&self) {} +} + +fn bar() { + let f : F = F::new(); + f.do_it(<|>); +}"#, + ); + + assert_eq!(info.parameters, vec!["&self".to_string()]); + assert_eq!(info.active_parameter, None); + } + + #[test] + fn test_fn_signature_for_method_with_arg() { + let info = call_info( + r#"struct F; +impl F { + pub fn new() -> F{ + F{} + } + + pub fn do_it(&self, x: i32) {} +} + +fn bar() { + let f : F = F::new(); + f.do_it(<|>); +}"#, + ); + + assert_eq!(info.parameters, vec!["&self".to_string(), "x".to_string()]); + assert_eq!(info.active_parameter, Some(1)); + } + + #[test] + fn test_fn_signature_with_docs_simple() { + let info = call_info( + r#" +/// test +// non-doc-comment +fn foo(j: u32) -> u32 { + j +} + +fn bar() { + let _ = foo(<|>); +} +"#, + ); + + assert_eq!(info.parameters, vec!["j".to_string()]); + assert_eq!(info.active_parameter, Some(0)); + assert_eq!(info.label, "fn foo(j: u32) -> u32".to_string()); + assert_eq!(info.doc, Some("test".into())); + } + + #[test] + fn test_fn_signature_with_docs() { + let info = call_info( + r#" +/// Adds one to the number given. +/// +/// # Examples +/// +/// ``` +/// let five = 5; +/// +/// assert_eq!(6, my_crate::add_one(5)); +/// ``` +pub fn add_one(x: i32) -> i32 { + x + 1 +} + +pub fn do() { + add_one(<|> +}"#, + ); + + assert_eq!(info.parameters, vec!["x".to_string()]); + assert_eq!(info.active_parameter, Some(0)); + assert_eq!(info.label, "pub fn add_one(x: i32) -> i32".to_string()); + assert_eq!( + info.doc, + Some( + r#"Adds one to the number given. + +# Examples + +```rust +let five = 5; + +assert_eq!(6, my_crate::add_one(5)); +```"# + .into() + ) + ); + } + + #[test] + fn test_fn_signature_with_docs_impl() { + let info = call_info( + r#" +struct addr; +impl addr { + /// Adds one to the number given. + /// + /// # Examples + /// + /// ``` + /// let five = 5; + /// + /// assert_eq!(6, my_crate::add_one(5)); + /// ``` + pub fn add_one(x: i32) -> i32 { + x + 1 + } +} + +pub fn do_it() { + addr {}; + addr::add_one(<|>); +}"#, + ); + + assert_eq!(info.parameters, vec!["x".to_string()]); + assert_eq!(info.active_parameter, Some(0)); + assert_eq!(info.label, "pub fn add_one(x: i32) -> i32".to_string()); + assert_eq!( + info.doc, + Some( + r#"Adds one to the number given. + +# Examples + +```rust +let five = 5; + +assert_eq!(6, my_crate::add_one(5)); +```"# + .into() + ) + ); + } + + #[test] + fn test_fn_signature_with_docs_from_actix() { + let info = call_info( + r#" +pub trait WriteHandler +where + Self: Actor, + Self::Context: ActorContext, +{ + /// Method is called when writer emits error. + /// + /// If this method returns `ErrorAction::Continue` writer processing + /// continues otherwise stream processing stops. + fn error(&mut self, err: E, ctx: &mut Self::Context) -> Running { + Running::Stop + } + + /// Method is called when writer finishes. + /// + /// By default this method stops actor's `Context`. + fn finished(&mut self, ctx: &mut Self::Context) { + ctx.stop() + } +} + +pub fn foo() { + WriteHandler r; + r.finished(<|>); +} + +"#, + ); + + assert_eq!( + info.parameters, + vec!["&mut self".to_string(), "ctx".to_string()] + ); + assert_eq!(info.active_parameter, Some(1)); + assert_eq!( + info.doc, + Some( + r#"Method is called when writer finishes. + +By default this method stops actor's `Context`."# + .into() + ) + ); + } + +} diff --git a/crates/ra_ide_api/src/completion.rs b/crates/ra_ide_api/src/completion.rs new file mode 100644 index 000000000000..ce777a771e2e --- /dev/null +++ b/crates/ra_ide_api/src/completion.rs @@ -0,0 +1,77 @@ +mod completion_item; +mod completion_context; + +mod complete_dot; +mod complete_fn_param; +mod complete_keyword; +mod complete_snippet; +mod complete_path; +mod complete_scope; + +use ra_db::SyntaxDatabase; + +use crate::{ + db, + Cancelable, FilePosition, + completion::{ + completion_item::{Completions, CompletionKind}, + completion_context::CompletionContext, + }, +}; + +pub use crate::completion::completion_item::{CompletionItem, InsertText, CompletionItemKind}; + +/// Main entry point for completion. We run completion as a two-phase process. +/// +/// First, we look at the position and collect a so-called `CompletionContext. +/// This is a somewhat messy process, because, during completion, syntax tree is +/// incomplete and can look really weird. +/// +/// Once the context is collected, we run a series of completion routines which +/// look at the context and produce completion items. One subtelty about this +/// phase is that completion engine should not filter by the substring which is +/// already present, it should give all possible variants for the identifier at +/// the caret. In other words, for +/// +/// ```no-run +/// fn f() { +/// let foo = 92; +/// let _ = bar<|> +/// } +/// ``` +/// +/// `foo` *should* be present among the completion variants. Filtering by +/// identifier prefix/fuzzy match should be done higher in the stack, together +/// with ordering of completions (currently this is done by the client). +pub(crate) fn completions( + db: &db::RootDatabase, + position: FilePosition, +) -> Cancelable> { + let original_file = db.source_file(position.file_id); + let ctx = ctry!(CompletionContext::new(db, &original_file, position)?); + + let mut acc = Completions::default(); + + complete_fn_param::complete_fn_param(&mut acc, &ctx); + complete_keyword::complete_expr_keyword(&mut acc, &ctx); + complete_keyword::complete_use_tree_keyword(&mut acc, &ctx); + complete_snippet::complete_expr_snippet(&mut acc, &ctx); + complete_snippet::complete_item_snippet(&mut acc, &ctx); + complete_path::complete_path(&mut acc, &ctx)?; + complete_scope::complete_scope(&mut acc, &ctx)?; + complete_dot::complete_dot(&mut acc, &ctx)?; + + Ok(Some(acc)) +} + +#[cfg(test)] +fn check_completion(code: &str, expected_completions: &str, kind: CompletionKind) { + use crate::mock_analysis::{single_file_with_position, analysis_and_position}; + let (analysis, position) = if code.contains("//-") { + analysis_and_position(code) + } else { + single_file_with_position(code) + }; + let completions = completions(&analysis.db, position).unwrap().unwrap(); + completions.assert_match(expected_completions, kind); +} diff --git a/crates/ra_ide_api/src/completion/complete_dot.rs b/crates/ra_ide_api/src/completion/complete_dot.rs new file mode 100644 index 000000000000..5d4e60dc525c --- /dev/null +++ b/crates/ra_ide_api/src/completion/complete_dot.rs @@ -0,0 +1,121 @@ +use hir::{Ty, Def}; + +use crate::Cancelable; +use crate::completion::{CompletionContext, Completions, CompletionKind, CompletionItem, CompletionItemKind}; + +/// Complete dot accesses, i.e. fields or methods (currently only fields). +pub(super) fn complete_dot(acc: &mut Completions, ctx: &CompletionContext) -> Cancelable<()> { + let (function, receiver) = match (&ctx.function, ctx.dot_receiver) { + (Some(function), Some(receiver)) => (function, receiver), + _ => return Ok(()), + }; + let infer_result = function.infer(ctx.db)?; + let syntax_mapping = function.body_syntax_mapping(ctx.db)?; + let expr = match syntax_mapping.node_expr(receiver) { + Some(expr) => expr, + None => return Ok(()), + }; + let receiver_ty = infer_result[expr].clone(); + if !ctx.is_method_call { + complete_fields(acc, ctx, receiver_ty)?; + } + Ok(()) +} + +fn complete_fields(acc: &mut Completions, ctx: &CompletionContext, receiver: Ty) -> Cancelable<()> { + for receiver in receiver.autoderef(ctx.db) { + match receiver { + Ty::Adt { def_id, .. } => { + match def_id.resolve(ctx.db)? { + Def::Struct(s) => { + let variant_data = s.variant_data(ctx.db)?; + for field in variant_data.fields() { + CompletionItem::new( + CompletionKind::Reference, + field.name().to_string(), + ) + .kind(CompletionItemKind::Field) + .add_to(acc); + } + } + // TODO unions + _ => {} + } + } + Ty::Tuple(fields) => { + for (i, _ty) in fields.iter().enumerate() { + CompletionItem::new(CompletionKind::Reference, i.to_string()) + .kind(CompletionItemKind::Field) + .add_to(acc); + } + } + _ => {} + }; + } + Ok(()) +} + +#[cfg(test)] +mod tests { + use crate::completion::*; + + fn check_ref_completion(code: &str, expected_completions: &str) { + check_completion(code, expected_completions, CompletionKind::Reference); + } + + #[test] + fn test_struct_field_completion() { + check_ref_completion( + r" + struct A { the_field: u32 } + fn foo(a: A) { + a.<|> + } + ", + r#"the_field"#, + ); + } + + #[test] + fn test_struct_field_completion_self() { + check_ref_completion( + r" + struct A { the_field: u32 } + impl A { + fn foo(self) { + self.<|> + } + } + ", + r#"the_field"#, + ); + } + + #[test] + fn test_struct_field_completion_autoderef() { + check_ref_completion( + r" + struct A { the_field: u32 } + impl A { + fn foo(&self) { + self.<|> + } + } + ", + r#"the_field"#, + ); + } + + #[test] + fn test_no_struct_field_completion_for_method_call() { + check_ref_completion( + r" + struct A { the_field: u32 } + fn foo(a: A) { + a.<|>() + } + ", + r#""#, + ); + } +} diff --git a/crates/ra_ide_api/src/completion/complete_fn_param.rs b/crates/ra_ide_api/src/completion/complete_fn_param.rs new file mode 100644 index 000000000000..c1739e47eab5 --- /dev/null +++ b/crates/ra_ide_api/src/completion/complete_fn_param.rs @@ -0,0 +1,102 @@ +use ra_syntax::{ + algo::visit::{visitor_ctx, VisitorCtx}, + ast, + AstNode, +}; +use rustc_hash::FxHashMap; + +use crate::completion::{CompletionContext, Completions, CompletionKind, CompletionItem}; + +/// Complete repeated parametes, both name and type. For example, if all +/// functions in a file have a `spam: &mut Spam` parameter, a completion with +/// `spam: &mut Spam` insert text/label and `spam` lookup string will be +/// suggested. +pub(super) fn complete_fn_param(acc: &mut Completions, ctx: &CompletionContext) { + if !ctx.is_param { + return; + } + + let mut params = FxHashMap::default(); + for node in ctx.leaf.ancestors() { + let _ = visitor_ctx(&mut params) + .visit::(process) + .visit::(process) + .accept(node); + } + params + .into_iter() + .filter_map(|(label, (count, param))| { + let lookup = param.pat()?.syntax().text().to_string(); + if count < 2 { + None + } else { + Some((label, lookup)) + } + }) + .for_each(|(label, lookup)| { + CompletionItem::new(CompletionKind::Magic, label) + .lookup_by(lookup) + .add_to(acc) + }); + + fn process<'a, N: ast::FnDefOwner>( + node: &'a N, + params: &mut FxHashMap, + ) { + node.functions() + .filter_map(|it| it.param_list()) + .flat_map(|it| it.params()) + .for_each(|param| { + let text = param.syntax().text().to_string(); + params.entry(text).or_insert((0, param)).0 += 1; + }) + } +} + +#[cfg(test)] +mod tests { + use crate::completion::*; + + fn check_magic_completion(code: &str, expected_completions: &str) { + check_completion(code, expected_completions, CompletionKind::Magic); + } + + #[test] + fn test_param_completion_last_param() { + check_magic_completion( + r" + fn foo(file_id: FileId) {} + fn bar(file_id: FileId) {} + fn baz(file<|>) {} + ", + r#"file_id "file_id: FileId""#, + ); + } + + #[test] + fn test_param_completion_nth_param() { + check_magic_completion( + r" + fn foo(file_id: FileId) {} + fn bar(file_id: FileId) {} + fn baz(file<|>, x: i32) {} + ", + r#"file_id "file_id: FileId""#, + ); + } + + #[test] + fn test_param_completion_trait_param() { + check_magic_completion( + r" + pub(crate) trait SourceRoot { + pub fn contains(&self, file_id: FileId) -> bool; + pub fn module_map(&self) -> &ModuleMap; + pub fn lines(&self, file_id: FileId) -> &LineIndex; + pub fn syntax(&self, file<|>) + } + ", + r#"file_id "file_id: FileId""#, + ); + } +} diff --git a/crates/ra_ide_api/src/completion/complete_keyword.rs b/crates/ra_ide_api/src/completion/complete_keyword.rs new file mode 100644 index 000000000000..d350f06ceb60 --- /dev/null +++ b/crates/ra_ide_api/src/completion/complete_keyword.rs @@ -0,0 +1,339 @@ +use ra_syntax::{ + algo::visit::{visitor, Visitor}, + AstNode, + ast::{self, LoopBodyOwner}, + SyntaxKind::*, SyntaxNode, +}; + +use crate::completion::{CompletionContext, CompletionItem, Completions, CompletionKind, CompletionItemKind}; + +pub(super) fn complete_use_tree_keyword(acc: &mut Completions, ctx: &CompletionContext) { + // complete keyword "crate" in use stmt + match (ctx.use_item_syntax.as_ref(), ctx.path_prefix.as_ref()) { + (Some(_), None) => { + CompletionItem::new(CompletionKind::Keyword, "crate") + .kind(CompletionItemKind::Keyword) + .lookup_by("crate") + .snippet("crate::") + .add_to(acc); + CompletionItem::new(CompletionKind::Keyword, "self") + .kind(CompletionItemKind::Keyword) + .lookup_by("self") + .add_to(acc); + CompletionItem::new(CompletionKind::Keyword, "super") + .kind(CompletionItemKind::Keyword) + .lookup_by("super") + .add_to(acc); + } + (Some(_), Some(_)) => { + CompletionItem::new(CompletionKind::Keyword, "self") + .kind(CompletionItemKind::Keyword) + .lookup_by("self") + .add_to(acc); + CompletionItem::new(CompletionKind::Keyword, "super") + .kind(CompletionItemKind::Keyword) + .lookup_by("super") + .add_to(acc); + } + _ => {} + } +} + +fn keyword(kw: &str, snippet: &str) -> CompletionItem { + CompletionItem::new(CompletionKind::Keyword, kw) + .kind(CompletionItemKind::Keyword) + .snippet(snippet) + .build() +} + +pub(super) fn complete_expr_keyword(acc: &mut Completions, ctx: &CompletionContext) { + if !ctx.is_trivial_path { + return; + } + + let fn_def = match ctx.function_syntax { + Some(it) => it, + None => return, + }; + acc.add(keyword("if", "if $0 {}")); + acc.add(keyword("match", "match $0 {}")); + acc.add(keyword("while", "while $0 {}")); + acc.add(keyword("loop", "loop {$0}")); + + if ctx.after_if { + acc.add(keyword("else", "else {$0}")); + acc.add(keyword("else if", "else if $0 {}")); + } + if is_in_loop_body(ctx.leaf) { + if ctx.can_be_stmt { + acc.add(keyword("continue", "continue;")); + acc.add(keyword("break", "break;")); + } else { + acc.add(keyword("continue", "continue")); + acc.add(keyword("break", "break")); + } + } + acc.add_all(complete_return(fn_def, ctx.can_be_stmt)); +} + +fn is_in_loop_body(leaf: &SyntaxNode) -> bool { + for node in leaf.ancestors() { + if node.kind() == FN_DEF || node.kind() == LAMBDA_EXPR { + break; + } + let loop_body = visitor() + .visit::(LoopBodyOwner::loop_body) + .visit::(LoopBodyOwner::loop_body) + .visit::(LoopBodyOwner::loop_body) + .accept(node); + if let Some(Some(body)) = loop_body { + if leaf.range().is_subrange(&body.syntax().range()) { + return true; + } + } + } + false +} + +fn complete_return(fn_def: &ast::FnDef, can_be_stmt: bool) -> Option { + let snip = match (can_be_stmt, fn_def.ret_type().is_some()) { + (true, true) => "return $0;", + (true, false) => "return;", + (false, true) => "return $0", + (false, false) => "return", + }; + Some(keyword("return", snip)) +} + +#[cfg(test)] +mod tests { + use crate::completion::{CompletionKind, check_completion}; + fn check_keyword_completion(code: &str, expected_completions: &str) { + check_completion(code, expected_completions, CompletionKind::Keyword); + } + + #[test] + fn completes_keywords_in_use_stmt() { + check_keyword_completion( + r" + use <|> + ", + r#" + crate "crate" "crate::" + self "self" + super "super" + "#, + ); + + check_keyword_completion( + r" + use a::<|> + ", + r#" + self "self" + super "super" + "#, + ); + + check_keyword_completion( + r" + use a::{b, <|>} + ", + r#" + self "self" + super "super" + "#, + ); + } + + #[test] + fn completes_various_keywords_in_function() { + check_keyword_completion( + r" + fn quux() { + <|> + } + ", + r#" + if "if $0 {}" + match "match $0 {}" + while "while $0 {}" + loop "loop {$0}" + return "return;" + "#, + ); + } + + #[test] + fn completes_else_after_if() { + check_keyword_completion( + r" + fn quux() { + if true { + () + } <|> + } + ", + r#" + if "if $0 {}" + match "match $0 {}" + while "while $0 {}" + loop "loop {$0}" + else "else {$0}" + else if "else if $0 {}" + return "return;" + "#, + ); + } + + #[test] + fn test_completion_return_value() { + check_keyword_completion( + r" + fn quux() -> i32 { + <|> + 92 + } + ", + r#" + if "if $0 {}" + match "match $0 {}" + while "while $0 {}" + loop "loop {$0}" + return "return $0;" + "#, + ); + check_keyword_completion( + r" + fn quux() { + <|> + 92 + } + ", + r#" + if "if $0 {}" + match "match $0 {}" + while "while $0 {}" + loop "loop {$0}" + return "return;" + "#, + ); + } + + #[test] + fn dont_add_semi_after_return_if_not_a_statement() { + check_keyword_completion( + r" + fn quux() -> i32 { + match () { + () => <|> + } + } + ", + r#" + if "if $0 {}" + match "match $0 {}" + while "while $0 {}" + loop "loop {$0}" + return "return $0" + "#, + ); + } + + #[test] + fn last_return_in_block_has_semi() { + check_keyword_completion( + r" + fn quux() -> i32 { + if condition { + <|> + } + } + ", + r#" + if "if $0 {}" + match "match $0 {}" + while "while $0 {}" + loop "loop {$0}" + return "return $0;" + "#, + ); + check_keyword_completion( + r" + fn quux() -> i32 { + if condition { + <|> + } + let x = 92; + x + } + ", + r#" + if "if $0 {}" + match "match $0 {}" + while "while $0 {}" + loop "loop {$0}" + return "return $0;" + "#, + ); + } + + #[test] + fn completes_break_and_continue_in_loops() { + check_keyword_completion( + r" + fn quux() -> i32 { + loop { <|> } + } + ", + r#" + if "if $0 {}" + match "match $0 {}" + while "while $0 {}" + loop "loop {$0}" + continue "continue;" + break "break;" + return "return $0;" + "#, + ); + // No completion: lambda isolates control flow + check_keyword_completion( + r" + fn quux() -> i32 { + loop { || { <|> } } + } + ", + r#" + if "if $0 {}" + match "match $0 {}" + while "while $0 {}" + loop "loop {$0}" + return "return $0;" + "#, + ); + } + + #[test] + fn no_semi_after_break_continue_in_expr() { + check_keyword_completion( + r" + fn f() { + loop { + match () { + () => br<|> + } + } + } + ", + r#" + if "if $0 {}" + match "match $0 {}" + while "while $0 {}" + loop "loop {$0}" + continue "continue" + break "break" + return "return" + "#, + ) + } +} diff --git a/crates/ra_ide_api/src/completion/complete_path.rs b/crates/ra_ide_api/src/completion/complete_path.rs new file mode 100644 index 000000000000..4723a65a6b06 --- /dev/null +++ b/crates/ra_ide_api/src/completion/complete_path.rs @@ -0,0 +1,128 @@ +use crate::{ + Cancelable, + completion::{CompletionItem, CompletionItemKind, Completions, CompletionKind, CompletionContext}, +}; + +pub(super) fn complete_path(acc: &mut Completions, ctx: &CompletionContext) -> Cancelable<()> { + let (path, module) = match (&ctx.path_prefix, &ctx.module) { + (Some(path), Some(module)) => (path.clone(), module), + _ => return Ok(()), + }; + let def_id = match module.resolve_path(ctx.db, &path)?.take_types() { + Some(it) => it, + None => return Ok(()), + }; + match def_id.resolve(ctx.db)? { + hir::Def::Module(module) => { + let module_scope = module.scope(ctx.db)?; + module_scope.entries().for_each(|(name, res)| { + CompletionItem::new(CompletionKind::Reference, name.to_string()) + .from_resolution(ctx, res) + .add_to(acc) + }); + } + hir::Def::Enum(e) => e + .variants(ctx.db)? + .into_iter() + .for_each(|(name, _variant)| { + CompletionItem::new(CompletionKind::Reference, name.to_string()) + .kind(CompletionItemKind::EnumVariant) + .add_to(acc) + }), + _ => return Ok(()), + }; + Ok(()) +} + +#[cfg(test)] +mod tests { + use crate::completion::{CompletionKind, check_completion}; + + fn check_reference_completion(code: &str, expected_completions: &str) { + check_completion(code, expected_completions, CompletionKind::Reference); + } + + #[test] + fn completes_use_item_starting_with_self() { + check_reference_completion( + r" + use self::m::<|>; + + mod m { + struct Bar; + } + ", + "Bar", + ); + } + + #[test] + fn completes_use_item_starting_with_crate() { + check_reference_completion( + " + //- /lib.rs + mod foo; + struct Spam; + //- /foo.rs + use crate::Sp<|> + ", + "Spam;foo", + ); + } + + #[test] + fn completes_nested_use_tree() { + check_reference_completion( + " + //- /lib.rs + mod foo; + struct Spam; + //- /foo.rs + use crate::{Sp<|>}; + ", + "Spam;foo", + ); + } + + #[test] + fn completes_deeply_nested_use_tree() { + check_reference_completion( + " + //- /lib.rs + mod foo; + pub mod bar { + pub mod baz { + pub struct Spam; + } + } + //- /foo.rs + use crate::{bar::{baz::Sp<|>}}; + ", + "Spam", + ); + } + + #[test] + fn completes_enum_variant() { + check_reference_completion( + " + //- /lib.rs + enum E { Foo, Bar(i32) } + fn foo() { let _ = E::<|> } + ", + "Foo;Bar", + ); + } + + #[test] + fn dont_render_function_parens_in_use_item() { + check_reference_completion( + " + //- /lib.rs + mod m { pub fn foo() {} } + use crate::m::f<|>; + ", + "foo", + ) + } +} diff --git a/crates/ra_ide_api/src/completion/complete_scope.rs b/crates/ra_ide_api/src/completion/complete_scope.rs new file mode 100644 index 000000000000..ee9052d3d5d5 --- /dev/null +++ b/crates/ra_ide_api/src/completion/complete_scope.rs @@ -0,0 +1,192 @@ +use rustc_hash::FxHashSet; +use ra_syntax::TextUnit; + +use crate::{ + Cancelable, + completion::{CompletionItem, CompletionItemKind, Completions, CompletionKind, CompletionContext}, +}; + +pub(super) fn complete_scope(acc: &mut Completions, ctx: &CompletionContext) -> Cancelable<()> { + if !ctx.is_trivial_path { + return Ok(()); + } + let module = match &ctx.module { + Some(it) => it, + None => return Ok(()), + }; + if let Some(function) = &ctx.function { + let scopes = function.scopes(ctx.db)?; + complete_fn(acc, &scopes, ctx.offset); + } + + let module_scope = module.scope(ctx.db)?; + let (file_id, _) = module.defenition_source(ctx.db)?; + module_scope + .entries() + .filter(|(_name, res)| { + // Don't expose this item + // FIXME: this penetrates through all kinds of abstractions, + // we need to figura out the way to do it less ugly. + match res.import { + None => true, + Some(import) => { + let range = import.range(ctx.db, file_id); + !range.is_subrange(&ctx.leaf.range()) + } + } + }) + .for_each(|(name, res)| { + CompletionItem::new(CompletionKind::Reference, name.to_string()) + .from_resolution(ctx, res) + .add_to(acc) + }); + Ok(()) +} + +fn complete_fn(acc: &mut Completions, scopes: &hir::ScopesWithSyntaxMapping, offset: TextUnit) { + let mut shadowed = FxHashSet::default(); + scopes + .scope_chain_for_offset(offset) + .flat_map(|scope| scopes.scopes.entries(scope).iter()) + .filter(|entry| shadowed.insert(entry.name())) + .for_each(|entry| { + CompletionItem::new(CompletionKind::Reference, entry.name().to_string()) + .kind(CompletionItemKind::Binding) + .add_to(acc) + }); +} + +#[cfg(test)] +mod tests { + use crate::completion::{CompletionKind, check_completion}; + + fn check_reference_completion(code: &str, expected_completions: &str) { + check_completion(code, expected_completions, CompletionKind::Reference); + } + + #[test] + fn completes_bindings_from_let() { + check_reference_completion( + r" + fn quux(x: i32) { + let y = 92; + 1 + <|>; + let z = (); + } + ", + r#"y;x;quux "quux($0)""#, + ); + } + + #[test] + fn completes_bindings_from_if_let() { + check_reference_completion( + r" + fn quux() { + if let Some(x) = foo() { + let y = 92; + }; + if let Some(a) = bar() { + let b = 62; + 1 + <|> + } + } + ", + r#"b;a;quux "quux()$0""#, + ); + } + + #[test] + fn completes_bindings_from_for() { + check_reference_completion( + r" + fn quux() { + for x in &[1, 2, 3] { + <|> + } + } + ", + r#"x;quux "quux()$0""#, + ); + } + + #[test] + fn completes_module_items() { + check_reference_completion( + r" + struct Foo; + enum Baz {} + fn quux() { + <|> + } + ", + r#"quux "quux()$0";Foo;Baz"#, + ); + } + + #[test] + fn completes_module_items_in_nested_modules() { + check_reference_completion( + r" + struct Foo; + mod m { + struct Bar; + fn quux() { <|> } + } + ", + r#"quux "quux()$0";Bar"#, + ); + } + + #[test] + fn completes_return_type() { + check_reference_completion( + r" + struct Foo; + fn x() -> <|> + ", + r#"Foo;x "x()$0""#, + ) + } + + #[test] + fn dont_show_both_completions_for_shadowing() { + check_reference_completion( + r" + fn foo() -> { + let bar = 92; + { + let bar = 62; + <|> + } + } + ", + r#"bar;foo "foo()$0""#, + ) + } + + #[test] + fn completes_self_in_methods() { + check_reference_completion(r"impl S { fn foo(&self) { <|> } }", "self") + } + + #[test] + fn inserts_parens_for_function_calls() { + check_reference_completion( + r" + fn no_args() {} + fn main() { no_<|> } + ", + r#"no_args "no_args()$0" + main "main()$0""#, + ); + check_reference_completion( + r" + fn with_args(x: i32, y: String) {} + fn main() { with_<|> } + ", + r#"main "main()$0" + with_args "with_args($0)""#, + ); + } +} diff --git a/crates/ra_ide_api/src/completion/complete_snippet.rs b/crates/ra_ide_api/src/completion/complete_snippet.rs new file mode 100644 index 000000000000..a495751dda02 --- /dev/null +++ b/crates/ra_ide_api/src/completion/complete_snippet.rs @@ -0,0 +1,73 @@ +use crate::completion::{CompletionItem, Completions, CompletionKind, CompletionItemKind, CompletionContext, completion_item::Builder}; + +fn snippet(label: &str, snippet: &str) -> Builder { + CompletionItem::new(CompletionKind::Snippet, label) + .snippet(snippet) + .kind(CompletionItemKind::Snippet) +} + +pub(super) fn complete_expr_snippet(acc: &mut Completions, ctx: &CompletionContext) { + if !(ctx.is_trivial_path && ctx.function_syntax.is_some()) { + return; + } + snippet("pd", "eprintln!(\"$0 = {:?}\", $0);").add_to(acc); + snippet("ppd", "eprintln!(\"$0 = {:#?}\", $0);").add_to(acc); +} + +pub(super) fn complete_item_snippet(acc: &mut Completions, ctx: &CompletionContext) { + if !ctx.is_new_item { + return; + } + snippet( + "Test function", + "\ +#[test] +fn ${1:feature}() { + $0 +}", + ) + .lookup_by("tfn") + .add_to(acc); + + snippet("pub(crate)", "pub(crate) $0").add_to(acc); +} + +#[cfg(test)] +mod tests { + use crate::completion::{CompletionKind, check_completion}; + fn check_snippet_completion(code: &str, expected_completions: &str) { + check_completion(code, expected_completions, CompletionKind::Snippet); + } + + #[test] + fn completes_snippets_in_expressions() { + check_snippet_completion( + r"fn foo(x: i32) { <|> }", + r##" + pd "eprintln!(\"$0 = {:?}\", $0);" + ppd "eprintln!(\"$0 = {:#?}\", $0);" + "##, + ); + } + + #[test] + fn completes_snippets_in_items() { + // check_snippet_completion(r" + // <|> + // ", + // r##"[CompletionItem { label: "Test function", lookup: None, snippet: Some("#[test]\nfn test_${1:feature}() {\n$0\n}"##, + // ); + check_snippet_completion( + r" + #[cfg(test)] + mod tests { + <|> + } + ", + r##" + tfn "Test function" "#[test]\nfn ${1:feature}() {\n $0\n}" + pub(crate) "pub(crate) $0" + "##, + ); + } +} diff --git a/crates/ra_ide_api/src/completion/completion_context.rs b/crates/ra_ide_api/src/completion/completion_context.rs new file mode 100644 index 000000000000..01786bb69e91 --- /dev/null +++ b/crates/ra_ide_api/src/completion/completion_context.rs @@ -0,0 +1,205 @@ +use ra_text_edit::AtomTextEdit; +use ra_syntax::{ + AstNode, SyntaxNode, SourceFile, TextUnit, TextRange, + ast, + algo::{find_leaf_at_offset, find_covering_node, find_node_at_offset}, + SyntaxKind::*, +}; +use hir::source_binder; + +use crate::{db, FilePosition, Cancelable}; + +/// `CompletionContext` is created early during completion to figure out, where +/// exactly is the cursor, syntax-wise. +#[derive(Debug)] +pub(super) struct CompletionContext<'a> { + pub(super) db: &'a db::RootDatabase, + pub(super) offset: TextUnit, + pub(super) leaf: &'a SyntaxNode, + pub(super) module: Option, + pub(super) function: Option, + pub(super) function_syntax: Option<&'a ast::FnDef>, + pub(super) use_item_syntax: Option<&'a ast::UseItem>, + pub(super) is_param: bool, + /// A single-indent path, like `foo`. + pub(super) is_trivial_path: bool, + /// If not a trivial, path, the prefix (qualifier). + pub(super) path_prefix: Option, + pub(super) after_if: bool, + /// `true` if we are a statement or a last expr in the block. + pub(super) can_be_stmt: bool, + /// Something is typed at the "top" level, in module or impl/trait. + pub(super) is_new_item: bool, + /// The receiver if this is a field or method access, i.e. writing something.<|> + pub(super) dot_receiver: Option<&'a ast::Expr>, + /// If this is a method call in particular, i.e. the () are already there. + pub(super) is_method_call: bool, +} + +impl<'a> CompletionContext<'a> { + pub(super) fn new( + db: &'a db::RootDatabase, + original_file: &'a SourceFile, + position: FilePosition, + ) -> Cancelable>> { + let module = source_binder::module_from_position(db, position)?; + let leaf = + ctry!(find_leaf_at_offset(original_file.syntax(), position.offset).left_biased()); + let mut ctx = CompletionContext { + db, + leaf, + offset: position.offset, + module, + function: None, + function_syntax: None, + use_item_syntax: None, + is_param: false, + is_trivial_path: false, + path_prefix: None, + after_if: false, + can_be_stmt: false, + is_new_item: false, + dot_receiver: None, + is_method_call: false, + }; + ctx.fill(original_file, position.offset); + Ok(Some(ctx)) + } + + fn fill(&mut self, original_file: &'a SourceFile, offset: TextUnit) { + // Insert a fake ident to get a valid parse tree. We will use this file + // to determine context, though the original_file will be used for + // actual completion. + let file = { + let edit = AtomTextEdit::insert(offset, "intellijRulezz".to_string()); + original_file.reparse(&edit) + }; + + // First, let's try to complete a reference to some declaration. + if let Some(name_ref) = find_node_at_offset::(file.syntax(), offset) { + // Special case, `trait T { fn foo(i_am_a_name_ref) {} }`. + // See RFC#1685. + if is_node::(name_ref.syntax()) { + self.is_param = true; + return; + } + self.classify_name_ref(original_file, name_ref); + } + + // Otherwise, see if this is a declaration. We can use heuristics to + // suggest declaration names, see `CompletionKind::Magic`. + if let Some(name) = find_node_at_offset::(file.syntax(), offset) { + if is_node::(name.syntax()) { + self.is_param = true; + return; + } + } + } + fn classify_name_ref(&mut self, original_file: &'a SourceFile, name_ref: &ast::NameRef) { + let name_range = name_ref.syntax().range(); + let top_node = name_ref + .syntax() + .ancestors() + .take_while(|it| it.range() == name_range) + .last() + .unwrap(); + + match top_node.parent().map(|it| it.kind()) { + Some(SOURCE_FILE) | Some(ITEM_LIST) => { + self.is_new_item = true; + return; + } + _ => (), + } + + self.use_item_syntax = self.leaf.ancestors().find_map(ast::UseItem::cast); + + self.function_syntax = self + .leaf + .ancestors() + .take_while(|it| it.kind() != SOURCE_FILE && it.kind() != MODULE) + .find_map(ast::FnDef::cast); + match (&self.module, self.function_syntax) { + (Some(module), Some(fn_def)) => { + let function = source_binder::function_from_module(self.db, module, fn_def); + self.function = Some(function); + } + _ => (), + } + + let parent = match name_ref.syntax().parent() { + Some(it) => it, + None => return, + }; + if let Some(segment) = ast::PathSegment::cast(parent) { + let path = segment.parent_path(); + if let Some(mut path) = hir::Path::from_ast(path) { + if !path.is_ident() { + path.segments.pop().unwrap(); + self.path_prefix = Some(path); + return; + } + } + if path.qualifier().is_none() { + self.is_trivial_path = true; + + // Find either enclosing expr statement (thing with `;`) or a + // block. If block, check that we are the last expr. + self.can_be_stmt = name_ref + .syntax() + .ancestors() + .find_map(|node| { + if let Some(stmt) = ast::ExprStmt::cast(node) { + return Some(stmt.syntax().range() == name_ref.syntax().range()); + } + if let Some(block) = ast::Block::cast(node) { + return Some( + block.expr().map(|e| e.syntax().range()) + == Some(name_ref.syntax().range()), + ); + } + None + }) + .unwrap_or(false); + + if let Some(off) = name_ref.syntax().range().start().checked_sub(2.into()) { + if let Some(if_expr) = + find_node_at_offset::(original_file.syntax(), off) + { + if if_expr.syntax().range().end() < name_ref.syntax().range().start() { + self.after_if = true; + } + } + } + } + } + if let Some(field_expr) = ast::FieldExpr::cast(parent) { + // The receiver comes before the point of insertion of the fake + // ident, so it should have the same range in the non-modified file + self.dot_receiver = field_expr + .expr() + .map(|e| e.syntax().range()) + .and_then(|r| find_node_with_range(original_file.syntax(), r)); + } + if let Some(method_call_expr) = ast::MethodCallExpr::cast(parent) { + // As above + self.dot_receiver = method_call_expr + .expr() + .map(|e| e.syntax().range()) + .and_then(|r| find_node_with_range(original_file.syntax(), r)); + self.is_method_call = true; + } + } +} + +fn find_node_with_range(syntax: &SyntaxNode, range: TextRange) -> Option<&N> { + let node = find_covering_node(syntax, range); + node.ancestors().find_map(N::cast) +} + +fn is_node(node: &SyntaxNode) -> bool { + match node.ancestors().filter_map(N::cast).next() { + None => false, + Some(n) => n.syntax().range() == node.range(), + } +} diff --git a/crates/ra_ide_api/src/completion/completion_item.rs b/crates/ra_ide_api/src/completion/completion_item.rs new file mode 100644 index 000000000000..a25b87beefee --- /dev/null +++ b/crates/ra_ide_api/src/completion/completion_item.rs @@ -0,0 +1,244 @@ +use hir::PerNs; + +use crate::completion::CompletionContext; + +/// `CompletionItem` describes a single completion variant in the editor pop-up. +/// It is basically a POD with various properties. To construct a +/// `CompletionItem`, use `new` method and the `Builder` struct. +#[derive(Debug)] +pub struct CompletionItem { + /// Used only internally in tests, to check only specific kind of + /// completion. + completion_kind: CompletionKind, + label: String, + lookup: Option, + snippet: Option, + kind: Option, +} + +pub enum InsertText { + PlainText { text: String }, + Snippet { text: String }, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum CompletionItemKind { + Snippet, + Keyword, + Module, + Function, + Struct, + Enum, + EnumVariant, + Binding, + Field, +} + +#[derive(Debug, PartialEq, Eq)] +pub(crate) enum CompletionKind { + /// Parser-based keyword completion. + Keyword, + /// Your usual "complete all valid identifiers". + Reference, + /// "Secret sauce" completions. + Magic, + Snippet, +} + +impl CompletionItem { + pub(crate) fn new(completion_kind: CompletionKind, label: impl Into) -> Builder { + let label = label.into(); + Builder { + completion_kind, + label, + lookup: None, + snippet: None, + kind: None, + } + } + /// What user sees in pop-up in the UI. + pub fn label(&self) -> &str { + &self.label + } + /// What string is used for filtering. + pub fn lookup(&self) -> &str { + self.lookup + .as_ref() + .map(|it| it.as_str()) + .unwrap_or(self.label()) + } + /// What is inserted. + pub fn insert_text(&self) -> InsertText { + match &self.snippet { + None => InsertText::PlainText { + text: self.label.clone(), + }, + Some(it) => InsertText::Snippet { text: it.clone() }, + } + } + + pub fn kind(&self) -> Option { + self.kind + } +} + +/// A helper to make `CompletionItem`s. +#[must_use] +pub(crate) struct Builder { + completion_kind: CompletionKind, + label: String, + lookup: Option, + snippet: Option, + kind: Option, +} + +impl Builder { + pub(crate) fn add_to(self, acc: &mut Completions) { + acc.add(self.build()) + } + + pub(crate) fn build(self) -> CompletionItem { + CompletionItem { + label: self.label, + lookup: self.lookup, + snippet: self.snippet, + kind: self.kind, + completion_kind: self.completion_kind, + } + } + pub(crate) fn lookup_by(mut self, lookup: impl Into) -> Builder { + self.lookup = Some(lookup.into()); + self + } + pub(crate) fn snippet(mut self, snippet: impl Into) -> Builder { + self.snippet = Some(snippet.into()); + self + } + pub(crate) fn kind(mut self, kind: CompletionItemKind) -> Builder { + self.kind = Some(kind); + self + } + pub(super) fn from_resolution( + mut self, + ctx: &CompletionContext, + resolution: &hir::Resolution, + ) -> Builder { + let resolved = resolution.def_id.and_then(|d| d.resolve(ctx.db).ok()); + let kind = match resolved { + PerNs { + types: Some(hir::Def::Module(..)), + .. + } => CompletionItemKind::Module, + PerNs { + types: Some(hir::Def::Struct(..)), + .. + } => CompletionItemKind::Struct, + PerNs { + types: Some(hir::Def::Enum(..)), + .. + } => CompletionItemKind::Enum, + PerNs { + values: Some(hir::Def::Function(function)), + .. + } => return self.from_function(ctx, function), + _ => return self, + }; + self.kind = Some(kind); + self + } + + fn from_function(mut self, ctx: &CompletionContext, function: hir::Function) -> Builder { + // If not an import, add parenthesis automatically. + if ctx.use_item_syntax.is_none() { + if function.signature(ctx.db).args().is_empty() { + self.snippet = Some(format!("{}()$0", self.label)); + } else { + self.snippet = Some(format!("{}($0)", self.label)); + } + } + self.kind = Some(CompletionItemKind::Function); + self + } +} + +impl Into for Builder { + fn into(self) -> CompletionItem { + self.build() + } +} + +/// Represents an in-progress set of completions being built. +#[derive(Debug, Default)] +pub(crate) struct Completions { + buf: Vec, +} + +impl Completions { + pub(crate) fn add(&mut self, item: impl Into) { + self.buf.push(item.into()) + } + pub(crate) fn add_all(&mut self, items: I) + where + I: IntoIterator, + I::Item: Into, + { + items.into_iter().for_each(|item| self.add(item.into())) + } + + #[cfg(test)] + pub(crate) fn assert_match(&self, expected: &str, kind: CompletionKind) { + let expected = normalize(expected); + let actual = self.debug_render(kind); + test_utils::assert_eq_text!(expected.as_str(), actual.as_str(),); + + /// Normalize the textual representation of `Completions`: + /// replace `;` with newlines, normalize whitespace + fn normalize(expected: &str) -> String { + use ra_syntax::{tokenize, TextUnit, TextRange, SyntaxKind::SEMI}; + let mut res = String::new(); + for line in expected.trim().lines() { + let line = line.trim(); + let mut start_offset: TextUnit = 0.into(); + // Yep, we use rust tokenize in completion tests :-) + for token in tokenize(line) { + let range = TextRange::offset_len(start_offset, token.len); + start_offset += token.len; + if token.kind == SEMI { + res.push('\n'); + } else { + res.push_str(&line[range]); + } + } + + res.push('\n'); + } + res + } + } + + #[cfg(test)] + fn debug_render(&self, kind: CompletionKind) -> String { + let mut res = String::new(); + for c in self.buf.iter() { + if c.completion_kind == kind { + if let Some(lookup) = &c.lookup { + res.push_str(lookup); + res.push_str(&format!(" {:?}", c.label)); + } else { + res.push_str(&c.label); + } + if let Some(snippet) = &c.snippet { + res.push_str(&format!(" {:?}", snippet)); + } + res.push('\n'); + } + } + res + } +} + +impl Into> for Completions { + fn into(self) -> Vec { + self.buf + } +} diff --git a/crates/ra_ide_api/src/db.rs b/crates/ra_ide_api/src/db.rs new file mode 100644 index 000000000000..9d46609ecfd5 --- /dev/null +++ b/crates/ra_ide_api/src/db.rs @@ -0,0 +1,128 @@ +use std::{fmt, sync::Arc}; + +use salsa::{self, Database}; +use ra_db::{LocationIntener, BaseDatabase, FileId}; + +use crate::{symbol_index, LineIndex}; + +#[derive(Debug)] +pub(crate) struct RootDatabase { + runtime: salsa::Runtime, + id_maps: Arc, +} + +#[derive(Default)] +struct IdMaps { + defs: LocationIntener, + macros: LocationIntener, +} + +impl fmt::Debug for IdMaps { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + f.debug_struct("IdMaps") + .field("n_defs", &self.defs.len()) + .finish() + } +} + +impl salsa::Database for RootDatabase { + fn salsa_runtime(&self) -> &salsa::Runtime { + &self.runtime + } +} + +impl Default for RootDatabase { + fn default() -> RootDatabase { + let mut db = RootDatabase { + runtime: salsa::Runtime::default(), + id_maps: Default::default(), + }; + db.query_mut(ra_db::CrateGraphQuery) + .set((), Default::default()); + db.query_mut(ra_db::LocalRootsQuery) + .set((), Default::default()); + db.query_mut(ra_db::LibraryRootsQuery) + .set((), Default::default()); + db + } +} + +impl salsa::ParallelDatabase for RootDatabase { + fn snapshot(&self) -> salsa::Snapshot { + salsa::Snapshot::new(RootDatabase { + runtime: self.runtime.snapshot(self), + id_maps: self.id_maps.clone(), + }) + } +} + +impl BaseDatabase for RootDatabase {} + +impl AsRef> for RootDatabase { + fn as_ref(&self) -> &LocationIntener { + &self.id_maps.defs + } +} + +impl AsRef> for RootDatabase { + fn as_ref(&self) -> &LocationIntener { + &self.id_maps.macros + } +} + +salsa::query_group! { + pub(crate) trait LineIndexDatabase: ra_db::FilesDatabase + BaseDatabase { + fn line_index(file_id: FileId) -> Arc { + type LineIndexQuery; + } + } +} + +fn line_index(db: &impl ra_db::FilesDatabase, file_id: FileId) -> Arc { + let text = db.file_text(file_id); + Arc::new(LineIndex::new(&*text)) +} + +salsa::database_storage! { + pub(crate) struct RootDatabaseStorage for RootDatabase { + impl ra_db::FilesDatabase { + fn file_text() for ra_db::FileTextQuery; + fn file_relative_path() for ra_db::FileRelativePathQuery; + fn file_source_root() for ra_db::FileSourceRootQuery; + fn source_root() for ra_db::SourceRootQuery; + fn local_roots() for ra_db::LocalRootsQuery; + fn library_roots() for ra_db::LibraryRootsQuery; + fn crate_graph() for ra_db::CrateGraphQuery; + } + impl ra_db::SyntaxDatabase { + fn source_file() for ra_db::SourceFileQuery; + } + impl LineIndexDatabase { + fn line_index() for LineIndexQuery; + } + impl symbol_index::SymbolsDatabase { + fn file_symbols() for symbol_index::FileSymbolsQuery; + fn library_symbols() for symbol_index::LibrarySymbolsQuery; + } + impl hir::db::HirDatabase { + fn hir_source_file() for hir::db::HirSourceFileQuery; + fn expand_macro_invocation() for hir::db::ExpandMacroCallQuery; + fn module_tree() for hir::db::ModuleTreeQuery; + fn fn_scopes() for hir::db::FnScopesQuery; + fn file_items() for hir::db::SourceFileItemsQuery; + fn file_item() for hir::db::FileItemQuery; + fn input_module_items() for hir::db::InputModuleItemsQuery; + fn item_map() for hir::db::ItemMapQuery; + fn submodules() for hir::db::SubmodulesQuery; + fn infer() for hir::db::InferQuery; + fn type_for_def() for hir::db::TypeForDefQuery; + fn type_for_field() for hir::db::TypeForFieldQuery; + fn struct_data() for hir::db::StructDataQuery; + fn enum_data() for hir::db::EnumDataQuery; + fn impls_in_module() for hir::db::ImplsInModuleQuery; + fn body_hir() for hir::db::BodyHirQuery; + fn body_syntax_mapping() for hir::db::BodySyntaxMappingQuery; + fn fn_signature() for hir::db::FnSignatureQuery; + } + } +} diff --git a/crates/ra_ide_api/src/extend_selection.rs b/crates/ra_ide_api/src/extend_selection.rs new file mode 100644 index 000000000000..c3c809c9fe33 --- /dev/null +++ b/crates/ra_ide_api/src/extend_selection.rs @@ -0,0 +1,56 @@ +use ra_db::SyntaxDatabase; +use ra_syntax::{ + SyntaxNode, AstNode, SourceFile, + ast, algo::find_covering_node, +}; + +use crate::{ + TextRange, FileRange, + db::RootDatabase, +}; + +pub(crate) fn extend_selection(db: &RootDatabase, frange: FileRange) -> TextRange { + let source_file = db.source_file(frange.file_id); + if let Some(range) = extend_selection_in_macro(db, &source_file, frange) { + return range; + } + ra_ide_api_light::extend_selection(source_file.syntax(), frange.range).unwrap_or(frange.range) +} + +fn extend_selection_in_macro( + _db: &RootDatabase, + source_file: &SourceFile, + frange: FileRange, +) -> Option { + let macro_call = find_macro_call(source_file.syntax(), frange.range)?; + let (off, exp) = hir::MacroDef::ast_expand(macro_call)?; + let dst_range = exp.map_range_forward(frange.range - off)?; + let dst_range = ra_ide_api_light::extend_selection(&exp.syntax(), dst_range)?; + let src_range = exp.map_range_back(dst_range)? + off; + Some(src_range) +} + +fn find_macro_call(node: &SyntaxNode, range: TextRange) -> Option<&ast::MacroCall> { + find_covering_node(node, range) + .ancestors() + .find_map(ast::MacroCall::cast) +} + +#[cfg(test)] +mod tests { + use crate::mock_analysis::single_file_with_range; + use test_utils::assert_eq_dbg; + + #[test] + fn extend_selection_inside_macros() { + let (analysis, frange) = single_file_with_range( + " + fn main() { + ctry!(foo(|x| <|>x<|>)); + } + ", + ); + let r = analysis.extend_selection(frange); + assert_eq_dbg("[51; 56)", &r); + } +} diff --git a/crates/ra_ide_api/src/goto_defenition.rs b/crates/ra_ide_api/src/goto_defenition.rs new file mode 100644 index 000000000000..fcd8d315e24a --- /dev/null +++ b/crates/ra_ide_api/src/goto_defenition.rs @@ -0,0 +1,139 @@ +use ra_db::{FileId, Cancelable, SyntaxDatabase}; +use ra_syntax::{ + TextRange, AstNode, ast, SyntaxKind::{NAME, MODULE}, + algo::find_node_at_offset, +}; + +use crate::{FilePosition, NavigationTarget, db::RootDatabase}; + +pub(crate) fn goto_defenition( + db: &RootDatabase, + position: FilePosition, +) -> Cancelable>> { + let file = db.source_file(position.file_id); + let syntax = file.syntax(); + if let Some(name_ref) = find_node_at_offset::(syntax, position.offset) { + return Ok(Some(reference_defenition(db, position.file_id, name_ref)?)); + } + if let Some(name) = find_node_at_offset::(syntax, position.offset) { + return name_defenition(db, position.file_id, name); + } + Ok(None) +} + +pub(crate) fn reference_defenition( + db: &RootDatabase, + file_id: FileId, + name_ref: &ast::NameRef, +) -> Cancelable> { + if let Some(fn_descr) = + hir::source_binder::function_from_child_node(db, file_id, name_ref.syntax())? + { + let scope = fn_descr.scopes(db)?; + // First try to resolve the symbol locally + if let Some(entry) = scope.resolve_local_name(name_ref) { + let nav = NavigationTarget { + file_id, + name: entry.name().to_string().into(), + range: entry.ptr().range(), + kind: NAME, + ptr: None, + }; + return Ok(vec![nav]); + }; + } + // If that fails try the index based approach. + let navs = db + .index_resolve(name_ref)? + .into_iter() + .map(NavigationTarget::from_symbol) + .collect(); + Ok(navs) +} + +fn name_defenition( + db: &RootDatabase, + file_id: FileId, + name: &ast::Name, +) -> Cancelable>> { + if let Some(module) = name.syntax().parent().and_then(ast::Module::cast) { + if module.has_semi() { + if let Some(child_module) = + hir::source_binder::module_from_declaration(db, file_id, module)? + { + let (file_id, _) = child_module.defenition_source(db)?; + let name = match child_module.name(db)? { + Some(name) => name.to_string().into(), + None => "".into(), + }; + let nav = NavigationTarget { + file_id, + name, + range: TextRange::offset_len(0.into(), 0.into()), + kind: MODULE, + ptr: None, + }; + return Ok(Some(vec![nav])); + } + } + } + Ok(None) +} + +#[cfg(test)] +mod tests { + use test_utils::assert_eq_dbg; + use crate::mock_analysis::analysis_and_position; + + #[test] + fn goto_defenition_works_in_items() { + let (analysis, pos) = analysis_and_position( + " + //- /lib.rs + struct Foo; + enum E { X(Foo<|>) } + ", + ); + + let symbols = analysis.goto_defenition(pos).unwrap().unwrap(); + assert_eq_dbg( + r#"[NavigationTarget { file_id: FileId(1), name: "Foo", + kind: STRUCT_DEF, range: [0; 11), + ptr: Some(LocalSyntaxPtr { range: [0; 11), kind: STRUCT_DEF }) }]"#, + &symbols, + ); + } + + #[test] + fn goto_defenition_works_for_module_declaration() { + let (analysis, pos) = analysis_and_position( + " + //- /lib.rs + mod <|>foo; + //- /foo.rs + // empty + ", + ); + + let symbols = analysis.goto_defenition(pos).unwrap().unwrap(); + assert_eq_dbg( + r#"[NavigationTarget { file_id: FileId(2), name: "foo", kind: MODULE, range: [0; 0), ptr: None }]"#, + &symbols, + ); + + let (analysis, pos) = analysis_and_position( + " + //- /lib.rs + mod <|>foo; + //- /foo/mod.rs + // empty + ", + ); + + let symbols = analysis.goto_defenition(pos).unwrap().unwrap(); + assert_eq_dbg( + r#"[NavigationTarget { file_id: FileId(2), name: "foo", kind: MODULE, range: [0; 0), ptr: None }]"#, + &symbols, + ); + } +} diff --git a/crates/ra_ide_api/src/hover.rs b/crates/ra_ide_api/src/hover.rs new file mode 100644 index 000000000000..475524ee1827 --- /dev/null +++ b/crates/ra_ide_api/src/hover.rs @@ -0,0 +1,257 @@ +use ra_db::{Cancelable, SyntaxDatabase}; +use ra_syntax::{ + AstNode, SyntaxNode, TreePtr, + ast::{self, NameOwner}, + algo::{find_covering_node, find_node_at_offset, find_leaf_at_offset, visit::{visitor, Visitor}}, +}; + +use crate::{db::RootDatabase, RangeInfo, FilePosition, FileRange, NavigationTarget}; + +pub(crate) fn hover( + db: &RootDatabase, + position: FilePosition, +) -> Cancelable>> { + let file = db.source_file(position.file_id); + let mut res = Vec::new(); + + let mut range = None; + if let Some(name_ref) = find_node_at_offset::(file.syntax(), position.offset) { + let navs = crate::goto_defenition::reference_defenition(db, position.file_id, name_ref)?; + for nav in navs { + res.extend(doc_text_for(db, nav)?) + } + if !res.is_empty() { + range = Some(name_ref.syntax().range()) + } + } + if range.is_none() { + let node = find_leaf_at_offset(file.syntax(), position.offset).find_map(|leaf| { + leaf.ancestors() + .find(|n| ast::Expr::cast(*n).is_some() || ast::Pat::cast(*n).is_some()) + }); + let node = ctry!(node); + let frange = FileRange { + file_id: position.file_id, + range: node.range(), + }; + res.extend(type_of(db, frange)?); + range = Some(node.range()); + }; + + let range = ctry!(range); + if res.is_empty() { + return Ok(None); + } + let res = RangeInfo::new(range, res.join("\n\n---\n")); + Ok(Some(res)) +} + +pub(crate) fn type_of(db: &RootDatabase, frange: FileRange) -> Cancelable> { + let file = db.source_file(frange.file_id); + let syntax = file.syntax(); + let leaf_node = find_covering_node(syntax, frange.range); + // if we picked identifier, expand to pattern/expression + let node = leaf_node + .ancestors() + .take_while(|it| it.range() == leaf_node.range()) + .find(|&it| ast::Expr::cast(it).is_some() || ast::Pat::cast(it).is_some()) + .unwrap_or(leaf_node); + let parent_fn = ctry!(node.ancestors().find_map(ast::FnDef::cast)); + let function = ctry!(hir::source_binder::function_from_source( + db, + frange.file_id, + parent_fn + )?); + let infer = function.infer(db)?; + let syntax_mapping = function.body_syntax_mapping(db)?; + if let Some(expr) = ast::Expr::cast(node).and_then(|e| syntax_mapping.node_expr(e)) { + Ok(Some(infer[expr].to_string())) + } else if let Some(pat) = ast::Pat::cast(node).and_then(|p| syntax_mapping.node_pat(p)) { + Ok(Some(infer[pat].to_string())) + } else { + Ok(None) + } +} + +// FIXME: this should not really use navigation target. Rather, approximatelly +// resovled symbol should return a `DefId`. +fn doc_text_for(db: &RootDatabase, nav: NavigationTarget) -> Cancelable> { + let result = match (nav.description(db), nav.docs(db)) { + (Some(desc), Some(docs)) => Some("```rust\n".to_string() + &*desc + "\n```\n\n" + &*docs), + (Some(desc), None) => Some("```rust\n".to_string() + &*desc + "\n```"), + (None, Some(docs)) => Some(docs), + _ => None, + }; + + Ok(result) +} + +impl NavigationTarget { + fn node(&self, db: &RootDatabase) -> Option> { + let source_file = db.source_file(self.file_id); + let source_file = source_file.syntax(); + let node = source_file + .descendants() + .find(|node| node.kind() == self.kind && node.range() == self.range)? + .to_owned(); + Some(node) + } + + fn docs(&self, db: &RootDatabase) -> Option { + let node = self.node(db)?; + fn doc_comments(node: &N) -> Option { + let comments = node.doc_comment_text(); + if comments.is_empty() { + None + } else { + Some(comments) + } + } + + visitor() + .visit(doc_comments::) + .visit(doc_comments::) + .visit(doc_comments::) + .visit(doc_comments::) + .visit(doc_comments::) + .visit(doc_comments::) + .visit(doc_comments::) + .visit(doc_comments::) + .accept(&node)? + } + + /// Get a description of this node. + /// + /// e.g. `struct Name`, `enum Name`, `fn Name` + fn description(&self, db: &RootDatabase) -> Option { + // TODO: After type inference is done, add type information to improve the output + let node = self.node(db)?; + // TODO: Refactor to be have less repetition + visitor() + .visit(|node: &ast::FnDef| { + let mut string = "fn ".to_string(); + node.name()?.syntax().text().push_to(&mut string); + Some(string) + }) + .visit(|node: &ast::StructDef| { + let mut string = "struct ".to_string(); + node.name()?.syntax().text().push_to(&mut string); + Some(string) + }) + .visit(|node: &ast::EnumDef| { + let mut string = "enum ".to_string(); + node.name()?.syntax().text().push_to(&mut string); + Some(string) + }) + .visit(|node: &ast::TraitDef| { + let mut string = "trait ".to_string(); + node.name()?.syntax().text().push_to(&mut string); + Some(string) + }) + .visit(|node: &ast::Module| { + let mut string = "mod ".to_string(); + node.name()?.syntax().text().push_to(&mut string); + Some(string) + }) + .visit(|node: &ast::TypeDef| { + let mut string = "type ".to_string(); + node.name()?.syntax().text().push_to(&mut string); + Some(string) + }) + .visit(|node: &ast::ConstDef| { + let mut string = "const ".to_string(); + node.name()?.syntax().text().push_to(&mut string); + Some(string) + }) + .visit(|node: &ast::StaticDef| { + let mut string = "static ".to_string(); + node.name()?.syntax().text().push_to(&mut string); + Some(string) + }) + .accept(&node)? + } +} + +#[cfg(test)] +mod tests { + use ra_syntax::TextRange; + use crate::mock_analysis::{single_file_with_position, single_file_with_range}; + + #[test] + fn hover_shows_type_of_an_expression() { + let (analysis, position) = single_file_with_position( + " + pub fn foo() -> u32 { 1 } + + fn main() { + let foo_test = foo()<|>; + } + ", + ); + let hover = analysis.hover(position).unwrap().unwrap(); + assert_eq!(hover.range, TextRange::from_to(95.into(), 100.into())); + assert_eq!(hover.info, "u32"); + } + + #[test] + fn hover_for_local_variable() { + let (analysis, position) = single_file_with_position("fn func(foo: i32) { fo<|>o; }"); + let hover = analysis.hover(position).unwrap().unwrap(); + assert_eq!(hover.info, "i32"); + } + + #[test] + fn hover_for_local_variable_pat() { + let (analysis, position) = single_file_with_position("fn func(fo<|>o: i32) {}"); + let hover = analysis.hover(position).unwrap().unwrap(); + assert_eq!(hover.info, "i32"); + } + + #[test] + fn test_type_of_for_function() { + let (analysis, range) = single_file_with_range( + " + pub fn foo() -> u32 { 1 }; + + fn main() { + let foo_test = <|>foo()<|>; + } + ", + ); + + let type_name = analysis.type_of(range).unwrap().unwrap(); + assert_eq!("u32", &type_name); + } + + // FIXME: improve type_of to make this work + #[test] + fn test_type_of_for_expr_1() { + let (analysis, range) = single_file_with_range( + " + fn main() { + let foo = <|>1 + foo_test<|>; + } + ", + ); + + let type_name = analysis.type_of(range).unwrap().unwrap(); + assert_eq!("[unknown]", &type_name); + } + + // FIXME: improve type_of to make this work + #[test] + fn test_type_of_for_expr_2() { + let (analysis, range) = single_file_with_range( + " + fn main() { + let foo: usize = 1; + let bar = <|>1 + foo_test<|>; + } + ", + ); + + let type_name = analysis.type_of(range).unwrap().unwrap(); + assert_eq!("[unknown]", &type_name); + } + +} diff --git a/crates/ra_ide_api/src/imp.rs b/crates/ra_ide_api/src/imp.rs new file mode 100644 index 000000000000..7c60ab7d6f4e --- /dev/null +++ b/crates/ra_ide_api/src/imp.rs @@ -0,0 +1,309 @@ +use std::sync::Arc; + +use salsa::Database; + +use hir::{ + self, Problem, source_binder, +}; +use ra_db::{FilesDatabase, SourceRoot, SourceRootId, SyntaxDatabase}; +use ra_ide_api_light::{self, assists, LocalEdit, Severity}; +use ra_syntax::{ + TextRange, AstNode, SourceFile, + ast::{self, NameOwner}, + algo::find_node_at_offset, + SyntaxKind::*, +}; + +use crate::{ + AnalysisChange, + Cancelable, NavigationTarget, + CrateId, db, Diagnostic, FileId, FilePosition, FileRange, FileSystemEdit, + Query, RootChange, SourceChange, SourceFileEdit, + symbol_index::{LibrarySymbolsQuery, FileSymbol}, +}; + +impl db::RootDatabase { + pub(crate) fn apply_change(&mut self, change: AnalysisChange) { + log::info!("apply_change {:?}", change); + // self.gc_syntax_trees(); + if !change.new_roots.is_empty() { + let mut local_roots = Vec::clone(&self.local_roots()); + for (root_id, is_local) in change.new_roots { + self.query_mut(ra_db::SourceRootQuery) + .set(root_id, Default::default()); + if is_local { + local_roots.push(root_id); + } + } + self.query_mut(ra_db::LocalRootsQuery) + .set((), Arc::new(local_roots)); + } + + for (root_id, root_change) in change.roots_changed { + self.apply_root_change(root_id, root_change); + } + for (file_id, text) in change.files_changed { + self.query_mut(ra_db::FileTextQuery).set(file_id, text) + } + if !change.libraries_added.is_empty() { + let mut libraries = Vec::clone(&self.library_roots()); + for library in change.libraries_added { + libraries.push(library.root_id); + self.query_mut(ra_db::SourceRootQuery) + .set(library.root_id, Default::default()); + self.query_mut(LibrarySymbolsQuery) + .set_constant(library.root_id, Arc::new(library.symbol_index)); + self.apply_root_change(library.root_id, library.root_change); + } + self.query_mut(ra_db::LibraryRootsQuery) + .set((), Arc::new(libraries)); + } + if let Some(crate_graph) = change.crate_graph { + self.query_mut(ra_db::CrateGraphQuery) + .set((), Arc::new(crate_graph)) + } + } + + fn apply_root_change(&mut self, root_id: SourceRootId, root_change: RootChange) { + let mut source_root = SourceRoot::clone(&self.source_root(root_id)); + for add_file in root_change.added { + self.query_mut(ra_db::FileTextQuery) + .set(add_file.file_id, add_file.text); + self.query_mut(ra_db::FileRelativePathQuery) + .set(add_file.file_id, add_file.path.clone()); + self.query_mut(ra_db::FileSourceRootQuery) + .set(add_file.file_id, root_id); + source_root.files.insert(add_file.path, add_file.file_id); + } + for remove_file in root_change.removed { + self.query_mut(ra_db::FileTextQuery) + .set(remove_file.file_id, Default::default()); + source_root.files.remove(&remove_file.path); + } + self.query_mut(ra_db::SourceRootQuery) + .set(root_id, Arc::new(source_root)); + } + + #[allow(unused)] + /// Ideally, we should call this function from time to time to collect heavy + /// syntax trees. However, if we actually do that, everything is recomputed + /// for some reason. Needs investigation. + fn gc_syntax_trees(&mut self) { + self.query(ra_db::SourceFileQuery) + .sweep(salsa::SweepStrategy::default().discard_values()); + self.query(hir::db::SourceFileItemsQuery) + .sweep(salsa::SweepStrategy::default().discard_values()); + self.query(hir::db::FileItemQuery) + .sweep(salsa::SweepStrategy::default().discard_values()); + } +} + +impl db::RootDatabase { + /// This returns `Vec` because a module may be included from several places. We + /// don't handle this case yet though, so the Vec has length at most one. + pub(crate) fn parent_module( + &self, + position: FilePosition, + ) -> Cancelable> { + let module = match source_binder::module_from_position(self, position)? { + None => return Ok(Vec::new()), + Some(it) => it, + }; + let (file_id, ast_module) = match module.declaration_source(self)? { + None => return Ok(Vec::new()), + Some(it) => it, + }; + let name = ast_module.name().unwrap(); + Ok(vec![NavigationTarget { + file_id, + name: name.text().clone(), + range: name.syntax().range(), + kind: MODULE, + ptr: None, + }]) + } + /// Returns `Vec` for the same reason as `parent_module` + pub(crate) fn crate_for(&self, file_id: FileId) -> Cancelable> { + let module = match source_binder::module_from_file_id(self, file_id)? { + Some(it) => it, + None => return Ok(Vec::new()), + }; + let krate = match module.krate(self)? { + Some(it) => it, + None => return Ok(Vec::new()), + }; + Ok(vec![krate.crate_id()]) + } + pub(crate) fn find_all_refs( + &self, + position: FilePosition, + ) -> Cancelable> { + let file = self.source_file(position.file_id); + // Find the binding associated with the offset + let (binding, descr) = match find_binding(self, &file, position)? { + None => return Ok(Vec::new()), + Some(it) => it, + }; + + let mut ret = binding + .name() + .into_iter() + .map(|name| (position.file_id, name.syntax().range())) + .collect::>(); + ret.extend( + descr + .scopes(self)? + .find_all_refs(binding) + .into_iter() + .map(|ref_desc| (position.file_id, ref_desc.range)), + ); + + return Ok(ret); + + fn find_binding<'a>( + db: &db::RootDatabase, + source_file: &'a SourceFile, + position: FilePosition, + ) -> Cancelable> { + let syntax = source_file.syntax(); + if let Some(binding) = find_node_at_offset::(syntax, position.offset) { + let descr = ctry!(source_binder::function_from_child_node( + db, + position.file_id, + binding.syntax(), + )?); + return Ok(Some((binding, descr))); + }; + let name_ref = ctry!(find_node_at_offset::(syntax, position.offset)); + let descr = ctry!(source_binder::function_from_child_node( + db, + position.file_id, + name_ref.syntax(), + )?); + let scope = descr.scopes(db)?; + let resolved = ctry!(scope.resolve_local_name(name_ref)); + let resolved = resolved.ptr().resolve(source_file); + let binding = ctry!(find_node_at_offset::( + syntax, + resolved.range().end() + )); + Ok(Some((binding, descr))) + } + } + + pub(crate) fn diagnostics(&self, file_id: FileId) -> Cancelable> { + let syntax = self.source_file(file_id); + + let mut res = ra_ide_api_light::diagnostics(&syntax) + .into_iter() + .map(|d| Diagnostic { + range: d.range, + message: d.msg, + severity: d.severity, + fix: d.fix.map(|fix| SourceChange::from_local_edit(file_id, fix)), + }) + .collect::>(); + if let Some(m) = source_binder::module_from_file_id(self, file_id)? { + for (name_node, problem) in m.problems(self)? { + let source_root = self.file_source_root(file_id); + let diag = match problem { + Problem::UnresolvedModule { candidate } => { + let create_file = FileSystemEdit::CreateFile { + source_root, + path: candidate.clone(), + }; + let fix = SourceChange { + label: "create module".to_string(), + source_file_edits: Vec::new(), + file_system_edits: vec![create_file], + cursor_position: None, + }; + Diagnostic { + range: name_node.range(), + message: "unresolved module".to_string(), + severity: Severity::Error, + fix: Some(fix), + } + } + Problem::NotDirOwner { move_to, candidate } => { + let move_file = FileSystemEdit::MoveFile { + src: file_id, + dst_source_root: source_root, + dst_path: move_to.clone(), + }; + let create_file = FileSystemEdit::CreateFile { + source_root, + path: move_to.join(candidate), + }; + let fix = SourceChange { + label: "move file and create module".to_string(), + source_file_edits: Vec::new(), + file_system_edits: vec![move_file, create_file], + cursor_position: None, + }; + Diagnostic { + range: name_node.range(), + message: "can't declare module at this location".to_string(), + severity: Severity::Error, + fix: Some(fix), + } + } + }; + res.push(diag) + } + }; + Ok(res) + } + + pub(crate) fn assists(&self, frange: FileRange) -> Vec { + let file = self.source_file(frange.file_id); + assists::assists(&file, frange.range) + .into_iter() + .map(|local_edit| SourceChange::from_local_edit(frange.file_id, local_edit)) + .collect() + } + + pub(crate) fn rename( + &self, + position: FilePosition, + new_name: &str, + ) -> Cancelable> { + let res = self + .find_all_refs(position)? + .iter() + .map(|(file_id, text_range)| SourceFileEdit { + file_id: *file_id, + edit: { + let mut builder = ra_text_edit::TextEditBuilder::default(); + builder.replace(*text_range, new_name.into()); + builder.finish() + }, + }) + .collect::>(); + Ok(res) + } + pub(crate) fn index_resolve(&self, name_ref: &ast::NameRef) -> Cancelable> { + let name = name_ref.text(); + let mut query = Query::new(name.to_string()); + query.exact(); + query.limit(4); + crate::symbol_index::world_symbols(self, query) + } +} + +impl SourceChange { + pub(crate) fn from_local_edit(file_id: FileId, edit: LocalEdit) -> SourceChange { + let file_edit = SourceFileEdit { + file_id, + edit: edit.edit, + }; + SourceChange { + label: edit.label, + source_file_edits: vec![file_edit], + file_system_edits: vec![], + cursor_position: edit + .cursor_position + .map(|offset| FilePosition { offset, file_id }), + } + } +} diff --git a/crates/ra_ide_api/src/lib.rs b/crates/ra_ide_api/src/lib.rs new file mode 100644 index 000000000000..183e3670691e --- /dev/null +++ b/crates/ra_ide_api/src/lib.rs @@ -0,0 +1,509 @@ +//! ra_analyzer crate provides "ide-centric" APIs for the rust-analyzer. What +//! powers this API are the `RootDatabase` struct, which defines a `salsa` +//! database, and the `ra_hir` crate, where majority of the analysis happens. +//! However, IDE specific bits of the analysis (most notably completion) happen +//! in this crate. +macro_rules! ctry { + ($expr:expr) => { + match $expr { + None => return Ok(None), + Some(it) => it, + } + }; +} + +mod completion; +mod db; +mod goto_defenition; +mod imp; +pub mod mock_analysis; +mod runnables; +mod symbol_index; + +mod extend_selection; +mod hover; +mod call_info; +mod syntax_highlighting; + +use std::{fmt, sync::Arc}; + +use ra_syntax::{SmolStr, SourceFile, TreePtr, SyntaxKind, TextRange, TextUnit}; +use ra_text_edit::TextEdit; +use ra_db::{SyntaxDatabase, FilesDatabase, LocalSyntaxPtr}; +use rayon::prelude::*; +use relative_path::RelativePathBuf; +use rustc_hash::FxHashMap; +use salsa::ParallelDatabase; + +use crate::{ + symbol_index::{FileSymbol, SymbolIndex}, + db::LineIndexDatabase, +}; + +pub use crate::{ + completion::{CompletionItem, CompletionItemKind, InsertText}, + runnables::{Runnable, RunnableKind}, +}; +pub use ra_ide_api_light::{ + Fold, FoldKind, HighlightedRange, Severity, StructureNode, + LineIndex, LineCol, translate_offset_with_edit, +}; +pub use ra_db::{ + Cancelable, Canceled, CrateGraph, CrateId, FileId, FilePosition, FileRange, SourceRootId +}; + +#[derive(Default)] +pub struct AnalysisChange { + new_roots: Vec<(SourceRootId, bool)>, + roots_changed: FxHashMap, + files_changed: Vec<(FileId, Arc)>, + libraries_added: Vec, + crate_graph: Option, +} + +#[derive(Default)] +struct RootChange { + added: Vec, + removed: Vec, +} + +#[derive(Debug)] +struct AddFile { + file_id: FileId, + path: RelativePathBuf, + text: Arc, +} + +#[derive(Debug)] +struct RemoveFile { + file_id: FileId, + path: RelativePathBuf, +} + +impl fmt::Debug for AnalysisChange { + fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result { + let mut d = fmt.debug_struct("AnalysisChange"); + if !self.new_roots.is_empty() { + d.field("new_roots", &self.new_roots); + } + if !self.roots_changed.is_empty() { + d.field("roots_changed", &self.roots_changed); + } + if !self.files_changed.is_empty() { + d.field("files_changed", &self.files_changed.len()); + } + if !self.libraries_added.is_empty() { + d.field("libraries_added", &self.libraries_added.len()); + } + if !self.crate_graph.is_some() { + d.field("crate_graph", &self.crate_graph); + } + d.finish() + } +} + +impl fmt::Debug for RootChange { + fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result { + fmt.debug_struct("AnalysisChange") + .field("added", &self.added.len()) + .field("removed", &self.removed.len()) + .finish() + } +} + +impl AnalysisChange { + pub fn new() -> AnalysisChange { + AnalysisChange::default() + } + pub fn add_root(&mut self, root_id: SourceRootId, is_local: bool) { + self.new_roots.push((root_id, is_local)); + } + pub fn add_file( + &mut self, + root_id: SourceRootId, + file_id: FileId, + path: RelativePathBuf, + text: Arc, + ) { + let file = AddFile { + file_id, + path, + text, + }; + self.roots_changed + .entry(root_id) + .or_default() + .added + .push(file); + } + pub fn change_file(&mut self, file_id: FileId, new_text: Arc) { + self.files_changed.push((file_id, new_text)) + } + pub fn remove_file(&mut self, root_id: SourceRootId, file_id: FileId, path: RelativePathBuf) { + let file = RemoveFile { file_id, path }; + self.roots_changed + .entry(root_id) + .or_default() + .removed + .push(file); + } + pub fn add_library(&mut self, data: LibraryData) { + self.libraries_added.push(data) + } + pub fn set_crate_graph(&mut self, graph: CrateGraph) { + self.crate_graph = Some(graph); + } +} + +#[derive(Debug)] +pub struct SourceChange { + pub label: String, + pub source_file_edits: Vec, + pub file_system_edits: Vec, + pub cursor_position: Option, +} + +#[derive(Debug)] +pub struct SourceFileEdit { + pub file_id: FileId, + pub edit: TextEdit, +} + +#[derive(Debug)] +pub enum FileSystemEdit { + CreateFile { + source_root: SourceRootId, + path: RelativePathBuf, + }, + MoveFile { + src: FileId, + dst_source_root: SourceRootId, + dst_path: RelativePathBuf, + }, +} + +#[derive(Debug)] +pub struct Diagnostic { + pub message: String, + pub range: TextRange, + pub fix: Option, + pub severity: Severity, +} + +#[derive(Debug)] +pub struct Query { + query: String, + lowercased: String, + only_types: bool, + libs: bool, + exact: bool, + limit: usize, +} + +impl Query { + pub fn new(query: String) -> Query { + let lowercased = query.to_lowercase(); + Query { + query, + lowercased, + only_types: false, + libs: false, + exact: false, + limit: usize::max_value(), + } + } + pub fn only_types(&mut self) { + self.only_types = true; + } + pub fn libs(&mut self) { + self.libs = true; + } + pub fn exact(&mut self) { + self.exact = true; + } + pub fn limit(&mut self, limit: usize) { + self.limit = limit + } +} + +/// `NavigationTarget` represents and element in the editor's UI whihc you can +/// click on to navigate to a particular piece of code. +/// +/// Typically, a `NavigationTarget` corresponds to some element in the source +/// code, like a function or a struct, but this is not strictly required. +#[derive(Debug, Clone)] +pub struct NavigationTarget { + file_id: FileId, + name: SmolStr, + kind: SyntaxKind, + range: TextRange, + // Should be DefId ideally + ptr: Option, +} + +impl NavigationTarget { + fn from_symbol(symbol: FileSymbol) -> NavigationTarget { + NavigationTarget { + file_id: symbol.file_id, + name: symbol.name.clone(), + kind: symbol.ptr.kind(), + range: symbol.ptr.range(), + ptr: Some(symbol.ptr.clone()), + } + } + pub fn name(&self) -> &SmolStr { + &self.name + } + pub fn kind(&self) -> SyntaxKind { + self.kind + } + pub fn file_id(&self) -> FileId { + self.file_id + } + pub fn range(&self) -> TextRange { + self.range + } +} + +#[derive(Debug)] +pub struct RangeInfo { + pub range: TextRange, + pub info: T, +} + +impl RangeInfo { + fn new(range: TextRange, info: T) -> RangeInfo { + RangeInfo { range, info } + } +} + +#[derive(Debug)] +pub struct CallInfo { + pub label: String, + pub doc: Option, + pub parameters: Vec, + pub active_parameter: Option, +} + +/// `AnalysisHost` stores the current state of the world. +#[derive(Debug, Default)] +pub struct AnalysisHost { + db: db::RootDatabase, +} + +impl AnalysisHost { + /// Returns a snapshot of the current state, which you can query for + /// semantic information. + pub fn analysis(&self) -> Analysis { + Analysis { + db: self.db.snapshot(), + } + } + /// Applies changes to the current state of the world. If there are + /// outstanding snapshots, they will be canceled. + pub fn apply_change(&mut self, change: AnalysisChange) { + self.db.apply_change(change) + } +} + +/// Analysis is a snapshot of a world state at a moment in time. It is the main +/// entry point for asking semantic information about the world. When the world +/// state is advanced using `AnalysisHost::apply_change` method, all existing +/// `Analysis` are canceled (most method return `Err(Canceled)`). +#[derive(Debug)] +pub struct Analysis { + db: salsa::Snapshot, +} + +impl Analysis { + /// Gets the text of the source file. + pub fn file_text(&self, file_id: FileId) -> Arc { + self.db.file_text(file_id) + } + /// Gets the syntax tree of the file. + pub fn file_syntax(&self, file_id: FileId) -> TreePtr { + self.db.source_file(file_id).clone() + } + /// Gets the file's `LineIndex`: data structure to convert between absolute + /// offsets and line/column representation. + pub fn file_line_index(&self, file_id: FileId) -> Arc { + self.db.line_index(file_id) + } + /// Selects the next syntactic nodes encopasing the range. + pub fn extend_selection(&self, frange: FileRange) -> TextRange { + extend_selection::extend_selection(&self.db, frange) + } + /// Returns position of the mathcing brace (all types of braces are + /// supported). + pub fn matching_brace(&self, file: &SourceFile, offset: TextUnit) -> Option { + ra_ide_api_light::matching_brace(file, offset) + } + /// Returns a syntax tree represented as `String`, for debug purposes. + // FIXME: use a better name here. + pub fn syntax_tree(&self, file_id: FileId) -> String { + let file = self.db.source_file(file_id); + ra_ide_api_light::syntax_tree(&file) + } + /// Returns an edit to remove all newlines in the range, cleaning up minor + /// stuff like trailing commas. + pub fn join_lines(&self, frange: FileRange) -> SourceChange { + let file = self.db.source_file(frange.file_id); + SourceChange::from_local_edit( + frange.file_id, + ra_ide_api_light::join_lines(&file, frange.range), + ) + } + /// Returns an edit which should be applied when opening a new line, fixing + /// up minor stuff like continuing the comment. + pub fn on_enter(&self, position: FilePosition) -> Option { + let file = self.db.source_file(position.file_id); + let edit = ra_ide_api_light::on_enter(&file, position.offset)?; + Some(SourceChange::from_local_edit(position.file_id, edit)) + } + /// Returns an edit which should be applied after `=` was typed. Primarily, + /// this works when adding `let =`. + // FIXME: use a snippet completion instead of this hack here. + pub fn on_eq_typed(&self, position: FilePosition) -> Option { + let file = self.db.source_file(position.file_id); + let edit = ra_ide_api_light::on_eq_typed(&file, position.offset)?; + Some(SourceChange::from_local_edit(position.file_id, edit)) + } + /// Returns an edit which should be applied when a dot ('.') is typed on a blank line, indenting the line appropriately. + pub fn on_dot_typed(&self, position: FilePosition) -> Option { + let file = self.db.source_file(position.file_id); + let edit = ra_ide_api_light::on_dot_typed(&file, position.offset)?; + Some(SourceChange::from_local_edit(position.file_id, edit)) + } + /// Returns a tree representation of symbols in the file. Useful to draw a + /// file outline. + pub fn file_structure(&self, file_id: FileId) -> Vec { + let file = self.db.source_file(file_id); + ra_ide_api_light::file_structure(&file) + } + /// Returns the set of folding ranges. + pub fn folding_ranges(&self, file_id: FileId) -> Vec { + let file = self.db.source_file(file_id); + ra_ide_api_light::folding_ranges(&file) + } + /// Fuzzy searches for a symbol. + pub fn symbol_search(&self, query: Query) -> Cancelable> { + let res = symbol_index::world_symbols(&*self.db, query)? + .into_iter() + .map(NavigationTarget::from_symbol) + .collect(); + Ok(res) + } + pub fn goto_defenition( + &self, + position: FilePosition, + ) -> Cancelable>> { + goto_defenition::goto_defenition(&*self.db, position) + } + /// Finds all usages of the reference at point. + pub fn find_all_refs(&self, position: FilePosition) -> Cancelable> { + self.db.find_all_refs(position) + } + /// Returns a short text descrbing element at position. + pub fn hover(&self, position: FilePosition) -> Cancelable>> { + hover::hover(&*self.db, position) + } + /// Computes parameter information for the given call expression. + pub fn call_info(&self, position: FilePosition) -> Cancelable> { + call_info::call_info(&*self.db, position) + } + /// Returns a `mod name;` declaration which created the current module. + pub fn parent_module(&self, position: FilePosition) -> Cancelable> { + self.db.parent_module(position) + } + /// Returns crates this file belongs too. + pub fn crate_for(&self, file_id: FileId) -> Cancelable> { + self.db.crate_for(file_id) + } + /// Returns the root file of the given crate. + pub fn crate_root(&self, crate_id: CrateId) -> Cancelable { + Ok(self.db.crate_graph().crate_root(crate_id)) + } + /// Returns the set of possible targets to run for the current file. + pub fn runnables(&self, file_id: FileId) -> Cancelable> { + runnables::runnables(&*self.db, file_id) + } + /// Computes syntax highlighting for the given file. + pub fn highlight(&self, file_id: FileId) -> Cancelable> { + syntax_highlighting::highlight(&*self.db, file_id) + } + /// Computes completions at the given position. + pub fn completions(&self, position: FilePosition) -> Cancelable>> { + let completions = completion::completions(&self.db, position)?; + Ok(completions.map(|it| it.into())) + } + /// Computes assists (aks code actons aka intentions) for the given + /// position. + pub fn assists(&self, frange: FileRange) -> Cancelable> { + Ok(self.db.assists(frange)) + } + /// Computes the set of diagnostics for the given file. + pub fn diagnostics(&self, file_id: FileId) -> Cancelable> { + self.db.diagnostics(file_id) + } + /// Computes the type of the expression at the given position. + pub fn type_of(&self, frange: FileRange) -> Cancelable> { + hover::type_of(&*self.db, frange) + } + /// Returns the edit required to rename reference at the position to the new + /// name. + pub fn rename( + &self, + position: FilePosition, + new_name: &str, + ) -> Cancelable> { + self.db.rename(position, new_name) + } +} + +pub struct LibraryData { + root_id: SourceRootId, + root_change: RootChange, + symbol_index: SymbolIndex, +} + +impl fmt::Debug for LibraryData { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + f.debug_struct("LibraryData") + .field("root_id", &self.root_id) + .field("root_change", &self.root_change) + .field("n_symbols", &self.symbol_index.len()) + .finish() + } +} + +impl LibraryData { + pub fn prepare( + root_id: SourceRootId, + files: Vec<(FileId, RelativePathBuf, Arc)>, + ) -> LibraryData { + let symbol_index = SymbolIndex::for_files(files.par_iter().map(|(file_id, _, text)| { + let file = SourceFile::parse(text); + (*file_id, file) + })); + let mut root_change = RootChange::default(); + root_change.added = files + .into_iter() + .map(|(file_id, path, text)| AddFile { + file_id, + path, + text, + }) + .collect(); + LibraryData { + root_id, + root_change, + symbol_index, + } + } +} + +#[test] +fn analysis_is_send() { + fn is_send() {} + is_send::(); +} diff --git a/crates/ra_ide_api/src/mock_analysis.rs b/crates/ra_ide_api/src/mock_analysis.rs new file mode 100644 index 000000000000..846c76cfe7ae --- /dev/null +++ b/crates/ra_ide_api/src/mock_analysis.rs @@ -0,0 +1,135 @@ +use std::sync::Arc; + +use relative_path::RelativePathBuf; +use test_utils::{extract_offset, extract_range, parse_fixture, CURSOR_MARKER}; +use ra_db::mock::FileMap; + +use crate::{Analysis, AnalysisChange, AnalysisHost, CrateGraph, FileId, FilePosition, FileRange, SourceRootId}; + +/// Mock analysis is used in test to bootstrap an AnalysisHost/Analysis +/// from a set of in-memory files. +#[derive(Debug, Default)] +pub struct MockAnalysis { + files: Vec<(String, String)>, +} + +impl MockAnalysis { + pub fn new() -> MockAnalysis { + MockAnalysis::default() + } + /// Creates `MockAnalysis` using a fixture data in the following format: + /// + /// ```notrust + /// //- /main.rs + /// mod foo; + /// fn main() {} + /// + /// //- /foo.rs + /// struct Baz; + /// ``` + pub fn with_files(fixture: &str) -> MockAnalysis { + let mut res = MockAnalysis::new(); + for entry in parse_fixture(fixture) { + res.add_file(&entry.meta, &entry.text); + } + res + } + + /// Same as `with_files`, but requires that a single file contains a `<|>` marker, + /// whose position is also returned. + pub fn with_files_and_position(fixture: &str) -> (MockAnalysis, FilePosition) { + let mut position = None; + let mut res = MockAnalysis::new(); + for entry in parse_fixture(fixture) { + if entry.text.contains(CURSOR_MARKER) { + assert!( + position.is_none(), + "only one marker (<|>) per fixture is allowed" + ); + position = Some(res.add_file_with_position(&entry.meta, &entry.text)); + } else { + res.add_file(&entry.meta, &entry.text); + } + } + let position = position.expect("expected a marker (<|>)"); + (res, position) + } + + pub fn add_file(&mut self, path: &str, text: &str) -> FileId { + let file_id = FileId((self.files.len() + 1) as u32); + self.files.push((path.to_string(), text.to_string())); + file_id + } + pub fn add_file_with_position(&mut self, path: &str, text: &str) -> FilePosition { + let (offset, text) = extract_offset(text); + let file_id = FileId((self.files.len() + 1) as u32); + self.files.push((path.to_string(), text.to_string())); + FilePosition { file_id, offset } + } + pub fn add_file_with_range(&mut self, path: &str, text: &str) -> FileRange { + let (range, text) = extract_range(text); + let file_id = FileId((self.files.len() + 1) as u32); + self.files.push((path.to_string(), text.to_string())); + FileRange { file_id, range } + } + pub fn id_of(&self, path: &str) -> FileId { + let (idx, _) = self + .files + .iter() + .enumerate() + .find(|(_, (p, _text))| path == p) + .expect("no file in this mock"); + FileId(idx as u32 + 1) + } + pub fn analysis_host(self) -> AnalysisHost { + let mut host = AnalysisHost::default(); + let mut file_map = FileMap::default(); + let source_root = SourceRootId(0); + let mut change = AnalysisChange::new(); + change.add_root(source_root, true); + let mut crate_graph = CrateGraph::default(); + for (path, contents) in self.files.into_iter() { + assert!(path.starts_with('/')); + let path = RelativePathBuf::from_path(&path[1..]).unwrap(); + let file_id = file_map.add(path.clone()); + if path == "/lib.rs" || path == "/main.rs" { + crate_graph.add_crate_root(file_id); + } + change.add_file(source_root, file_id, path, Arc::new(contents)); + } + change.set_crate_graph(crate_graph); + // change.set_file_resolver(Arc::new(file_map)); + host.apply_change(change); + host + } + pub fn analysis(self) -> Analysis { + self.analysis_host().analysis() + } +} + +/// Creates analysis from a multi-file fixture, returns positions marked with <|>. +pub fn analysis_and_position(fixture: &str) -> (Analysis, FilePosition) { + let (mock, position) = MockAnalysis::with_files_and_position(fixture); + (mock.analysis(), position) +} + +/// Creates analysis for a single file. +pub fn single_file(code: &str) -> (Analysis, FileId) { + let mut mock = MockAnalysis::new(); + let file_id = mock.add_file("/main.rs", code); + (mock.analysis(), file_id) +} + +/// Creates analysis for a single file, returns position marked with <|>. +pub fn single_file_with_position(code: &str) -> (Analysis, FilePosition) { + let mut mock = MockAnalysis::new(); + let pos = mock.add_file_with_position("/main.rs", code); + (mock.analysis(), pos) +} + +/// Creates analysis for a single file, returns range marked with a pair of <|>. +pub fn single_file_with_range(code: &str) -> (Analysis, FileRange) { + let mut mock = MockAnalysis::new(); + let pos = mock.add_file_with_range("/main.rs", code); + (mock.analysis(), pos) +} diff --git a/crates/ra_ide_api/src/runnables.rs b/crates/ra_ide_api/src/runnables.rs new file mode 100644 index 000000000000..98b1d2d55211 --- /dev/null +++ b/crates/ra_ide_api/src/runnables.rs @@ -0,0 +1,89 @@ +use itertools::Itertools; +use ra_syntax::{ + TextRange, SyntaxNode, + ast::{self, AstNode, NameOwner, ModuleItemOwner}, +}; +use ra_db::{Cancelable, SyntaxDatabase}; + +use crate::{db::RootDatabase, FileId}; + +#[derive(Debug)] +pub struct Runnable { + pub range: TextRange, + pub kind: RunnableKind, +} + +#[derive(Debug)] +pub enum RunnableKind { + Test { name: String }, + TestMod { path: String }, + Bin, +} + +pub(crate) fn runnables(db: &RootDatabase, file_id: FileId) -> Cancelable> { + let source_file = db.source_file(file_id); + let res = source_file + .syntax() + .descendants() + .filter_map(|i| runnable(db, file_id, i)) + .collect(); + Ok(res) +} + +fn runnable(db: &RootDatabase, file_id: FileId, item: &SyntaxNode) -> Option { + if let Some(fn_def) = ast::FnDef::cast(item) { + runnable_fn(fn_def) + } else if let Some(m) = ast::Module::cast(item) { + runnable_mod(db, file_id, m) + } else { + None + } +} + +fn runnable_fn(fn_def: &ast::FnDef) -> Option { + let name = fn_def.name()?.text(); + let kind = if name == "main" { + RunnableKind::Bin + } else if fn_def.has_atom_attr("test") { + RunnableKind::Test { + name: name.to_string(), + } + } else { + return None; + }; + Some(Runnable { + range: fn_def.syntax().range(), + kind, + }) +} + +fn runnable_mod(db: &RootDatabase, file_id: FileId, module: &ast::Module) -> Option { + let has_test_function = module + .item_list()? + .items() + .filter_map(|it| match it.kind() { + ast::ModuleItemKind::FnDef(it) => Some(it), + _ => None, + }) + .any(|f| f.has_atom_attr("test")); + if !has_test_function { + return None; + } + let range = module.syntax().range(); + let module = + hir::source_binder::module_from_child_node(db, file_id, module.syntax()).ok()??; + + // FIXME: thread cancellation instead of `.ok`ing + let path = module + .path_to_root(db) + .ok()? + .into_iter() + .rev() + .filter_map(|it| it.name(db).ok()) + .filter_map(|it| it) + .join("::"); + Some(Runnable { + range, + kind: RunnableKind::TestMod { path }, + }) +} diff --git a/crates/ra_ide_api/src/symbol_index.rs b/crates/ra_ide_api/src/symbol_index.rs new file mode 100644 index 000000000000..8dd15b40e573 --- /dev/null +++ b/crates/ra_ide_api/src/symbol_index.rs @@ -0,0 +1,222 @@ +//! This module handles fuzzy-searching of functions, structs and other symbols +//! by name across the whole workspace and dependencies. +//! +//! It works by building an incrementally-updated text-search index of all +//! symbols. The backbone of the index is the **awesome** `fst` crate by +//! @BurntSushi. +//! +//! In a nutshell, you give a set of strings to the `fst`, and it builds a +//! finite state machine describing this set of strtings. The strings which +//! could fuzzy-match a pattern can also be described by a finite state machine. +//! What is freakingly cool is that you can now traverse both state machines in +//! lock-step to enumerate the strings which are both in the input set and +//! fuzz-match the query. Or, more formally, given two langauges described by +//! fsts, one can build an product fst which describes the intersection of the +//! languages. +//! +//! `fst` does not support cheap updating of the index, but it supports unioning +//! of state machines. So, to account for changing source code, we build an fst +//! for each library (which is assumed to never change) and an fst for each rust +//! file in the current workspace, and run a query aginst the union of all +//! thouse fsts. +use std::{ + cmp::Ordering, + hash::{Hash, Hasher}, + sync::Arc, +}; + +use fst::{self, Streamer}; +use ra_syntax::{ + SyntaxNode, SourceFile, SmolStr, TreePtr, AstNode, + algo::{visit::{visitor, Visitor}, find_covering_node}, + SyntaxKind::{self, *}, + ast::{self, NameOwner}, +}; +use ra_db::{SourceRootId, FilesDatabase, LocalSyntaxPtr}; +use salsa::ParallelDatabase; +use rayon::prelude::*; + +use crate::{ + Cancelable, FileId, Query, + db::RootDatabase, +}; + +salsa::query_group! { + pub(crate) trait SymbolsDatabase: hir::db::HirDatabase { + fn file_symbols(file_id: FileId) -> Cancelable> { + type FileSymbolsQuery; + } + fn library_symbols(id: SourceRootId) -> Arc { + type LibrarySymbolsQuery; + storage input; + } + } +} + +fn file_symbols(db: &impl SymbolsDatabase, file_id: FileId) -> Cancelable> { + db.check_canceled()?; + let source_file = db.source_file(file_id); + let mut symbols = source_file + .syntax() + .descendants() + .filter_map(to_symbol) + .map(move |(name, ptr)| FileSymbol { name, ptr, file_id }) + .collect::>(); + + for (name, text_range) in hir::source_binder::macro_symbols(db, file_id)? { + let node = find_covering_node(source_file.syntax(), text_range); + let ptr = LocalSyntaxPtr::new(node); + symbols.push(FileSymbol { file_id, name, ptr }) + } + + Ok(Arc::new(SymbolIndex::new(symbols))) +} + +pub(crate) fn world_symbols(db: &RootDatabase, query: Query) -> Cancelable> { + /// Need to wrap Snapshot to provide `Clone` impl for `map_with` + struct Snap(salsa::Snapshot); + impl Clone for Snap { + fn clone(&self) -> Snap { + Snap(self.0.snapshot()) + } + } + + let buf: Vec> = if query.libs { + let snap = Snap(db.snapshot()); + db.library_roots() + .par_iter() + .map_with(snap, |db, &lib_id| db.0.library_symbols(lib_id)) + .collect() + } else { + let mut files = Vec::new(); + for &root in db.local_roots().iter() { + let sr = db.source_root(root); + files.extend(sr.files.values().map(|&it| it)) + } + + let snap = Snap(db.snapshot()); + files + .par_iter() + .map_with(snap, |db, &file_id| db.0.file_symbols(file_id)) + .filter_map(|it| it.ok()) + .collect() + }; + Ok(query.search(&buf)) +} + +#[derive(Default, Debug)] +pub(crate) struct SymbolIndex { + symbols: Vec, + map: fst::Map, +} + +impl PartialEq for SymbolIndex { + fn eq(&self, other: &SymbolIndex) -> bool { + self.symbols == other.symbols + } +} + +impl Eq for SymbolIndex {} + +impl Hash for SymbolIndex { + fn hash(&self, hasher: &mut H) { + self.symbols.hash(hasher) + } +} + +impl SymbolIndex { + fn new(mut symbols: Vec) -> SymbolIndex { + fn cmp(s1: &FileSymbol, s2: &FileSymbol) -> Ordering { + unicase::Ascii::new(s1.name.as_str()).cmp(&unicase::Ascii::new(s2.name.as_str())) + } + symbols.par_sort_by(cmp); + symbols.dedup_by(|s1, s2| cmp(s1, s2) == Ordering::Equal); + let names = symbols.iter().map(|it| it.name.as_str().to_lowercase()); + let map = fst::Map::from_iter(names.into_iter().zip(0u64..)).unwrap(); + SymbolIndex { symbols, map } + } + + pub(crate) fn len(&self) -> usize { + self.symbols.len() + } + + pub(crate) fn for_files( + files: impl ParallelIterator)>, + ) -> SymbolIndex { + let symbols = files + .flat_map(|(file_id, file)| { + file.syntax() + .descendants() + .filter_map(to_symbol) + .map(move |(name, ptr)| FileSymbol { name, ptr, file_id }) + .collect::>() + }) + .collect::>(); + SymbolIndex::new(symbols) + } +} + +impl Query { + pub(crate) fn search(self, indices: &[Arc]) -> Vec { + let mut op = fst::map::OpBuilder::new(); + for file_symbols in indices.iter() { + let automaton = fst::automaton::Subsequence::new(&self.lowercased); + op = op.add(file_symbols.map.search(automaton)) + } + let mut stream = op.union(); + let mut res = Vec::new(); + while let Some((_, indexed_values)) = stream.next() { + if res.len() >= self.limit { + break; + } + for indexed_value in indexed_values { + let file_symbols = &indices[indexed_value.index]; + let idx = indexed_value.value as usize; + + let symbol = &file_symbols.symbols[idx]; + if self.only_types && !is_type(symbol.ptr.kind()) { + continue; + } + if self.exact && symbol.name != self.query { + continue; + } + res.push(symbol.clone()); + } + } + res + } +} + +fn is_type(kind: SyntaxKind) -> bool { + match kind { + STRUCT_DEF | ENUM_DEF | TRAIT_DEF | TYPE_DEF => true, + _ => false, + } +} + +/// The actual data that is stored in the index. It should be as compact as +/// possible. +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub(crate) struct FileSymbol { + pub(crate) file_id: FileId, + pub(crate) name: SmolStr, + pub(crate) ptr: LocalSyntaxPtr, +} + +fn to_symbol(node: &SyntaxNode) -> Option<(SmolStr, LocalSyntaxPtr)> { + fn decl(node: &N) -> Option<(SmolStr, LocalSyntaxPtr)> { + let name = node.name()?.text().clone(); + let ptr = LocalSyntaxPtr::new(node.syntax()); + Some((name, ptr)) + } + visitor() + .visit(decl::) + .visit(decl::) + .visit(decl::) + .visit(decl::) + .visit(decl::) + .visit(decl::) + .visit(decl::) + .visit(decl::) + .accept(node)? +} diff --git a/crates/ra_ide_api/src/syntax_highlighting.rs b/crates/ra_ide_api/src/syntax_highlighting.rs new file mode 100644 index 000000000000..cb19e9515a14 --- /dev/null +++ b/crates/ra_ide_api/src/syntax_highlighting.rs @@ -0,0 +1,92 @@ +use ra_syntax::{ast, AstNode,}; +use ra_db::SyntaxDatabase; + +use crate::{ + FileId, Cancelable, HighlightedRange, + db::RootDatabase, +}; + +pub(crate) fn highlight(db: &RootDatabase, file_id: FileId) -> Cancelable> { + let source_file = db.source_file(file_id); + let mut res = ra_ide_api_light::highlight(source_file.syntax()); + for macro_call in source_file + .syntax() + .descendants() + .filter_map(ast::MacroCall::cast) + { + if let Some((off, exp)) = hir::MacroDef::ast_expand(macro_call) { + let mapped_ranges = ra_ide_api_light::highlight(&exp.syntax()) + .into_iter() + .filter_map(|r| { + let mapped_range = exp.map_range_back(r.range)?; + let res = HighlightedRange { + range: mapped_range + off, + tag: r.tag, + }; + Some(res) + }); + res.extend(mapped_ranges); + } + } + Ok(res) +} + +#[cfg(test)] +mod tests { + use crate::mock_analysis::single_file; + use test_utils::assert_eq_dbg; + + #[test] + fn highlights_code_inside_macros() { + let (analysis, file_id) = single_file( + " + fn main() { + ctry!({ let x = 92; x}); + vec![{ let x = 92; x}]; + } + ", + ); + let highlights = analysis.highlight(file_id).unwrap(); + assert_eq_dbg( + r#"[HighlightedRange { range: [13; 15), tag: "keyword" }, + HighlightedRange { range: [16; 20), tag: "function" }, + HighlightedRange { range: [41; 46), tag: "macro" }, + HighlightedRange { range: [49; 52), tag: "keyword" }, + HighlightedRange { range: [57; 59), tag: "literal" }, + HighlightedRange { range: [82; 86), tag: "macro" }, + HighlightedRange { range: [89; 92), tag: "keyword" }, + HighlightedRange { range: [97; 99), tag: "literal" }, + HighlightedRange { range: [49; 52), tag: "keyword" }, + HighlightedRange { range: [53; 54), tag: "function" }, + HighlightedRange { range: [57; 59), tag: "literal" }, + HighlightedRange { range: [61; 62), tag: "text" }, + HighlightedRange { range: [89; 92), tag: "keyword" }, + HighlightedRange { range: [93; 94), tag: "function" }, + HighlightedRange { range: [97; 99), tag: "literal" }, + HighlightedRange { range: [101; 102), tag: "text" }]"#, + &highlights, + ) + } + + // FIXME: this test is not really necessary: artifact of the inital hacky + // macros implementation. + #[test] + fn highlight_query_group_macro() { + let (analysis, file_id) = single_file( + " + salsa::query_group! { + pub trait HirDatabase: SyntaxDatabase {} + } + ", + ); + let highlights = analysis.highlight(file_id).unwrap(); + assert_eq_dbg( + r#"[HighlightedRange { range: [20; 32), tag: "macro" }, + HighlightedRange { range: [13; 18), tag: "text" }, + HighlightedRange { range: [51; 54), tag: "keyword" }, + HighlightedRange { range: [55; 60), tag: "keyword" }, + HighlightedRange { range: [61; 72), tag: "function" }]"#, + &highlights, + ) + } +} diff --git a/crates/ra_ide_api/tests/test/main.rs b/crates/ra_ide_api/tests/test/main.rs new file mode 100644 index 000000000000..d1dc07e5b590 --- /dev/null +++ b/crates/ra_ide_api/tests/test/main.rs @@ -0,0 +1,249 @@ +mod runnables; + +use ra_syntax::TextRange; +use test_utils::{assert_eq_dbg, assert_eq_text}; + +use ra_ide_api::{ + mock_analysis::{analysis_and_position, single_file, single_file_with_position, MockAnalysis}, + AnalysisChange, CrateGraph, FileId, Query +}; + +#[test] +fn test_unresolved_module_diagnostic() { + let (analysis, file_id) = single_file("mod foo;"); + let diagnostics = analysis.diagnostics(file_id).unwrap(); + assert_eq_dbg( + r#"[Diagnostic { + message: "unresolved module", + range: [4; 7), + fix: Some(SourceChange { + label: "create module", + source_file_edits: [], + file_system_edits: [CreateFile { source_root: SourceRootId(0), path: "foo.rs" }], + cursor_position: None }), + severity: Error }]"#, + &diagnostics, + ); +} + +// FIXME: move this test to hir +#[test] +fn test_unresolved_module_diagnostic_no_diag_for_inline_mode() { + let (analysis, file_id) = single_file("mod foo {}"); + let diagnostics = analysis.diagnostics(file_id).unwrap(); + assert_eq_dbg(r#"[]"#, &diagnostics); +} + +#[test] +fn test_resolve_parent_module() { + let (analysis, pos) = analysis_and_position( + " + //- /lib.rs + mod foo; + //- /foo.rs + <|>// empty + ", + ); + let symbols = analysis.parent_module(pos).unwrap(); + assert_eq_dbg( + r#"[NavigationTarget { file_id: FileId(1), name: "foo", kind: MODULE, range: [4; 7), ptr: None }]"#, + &symbols, + ); +} + +#[test] +fn test_resolve_parent_module_for_inline() { + let (analysis, pos) = analysis_and_position( + " + //- /lib.rs + mod foo { + mod bar { + mod baz { <|> } + } + } + ", + ); + let symbols = analysis.parent_module(pos).unwrap(); + assert_eq_dbg( + r#"[NavigationTarget { file_id: FileId(1), name: "baz", kind: MODULE, range: [36; 39), ptr: None }]"#, + &symbols, + ); +} + +#[test] +fn test_resolve_crate_root() { + let mock = MockAnalysis::with_files( + " + //- /bar.rs + mod foo; + //- /bar/foo.rs + // emtpy <|> + ", + ); + let root_file = mock.id_of("/bar.rs"); + let mod_file = mock.id_of("/bar/foo.rs"); + let mut host = mock.analysis_host(); + assert!(host.analysis().crate_for(mod_file).unwrap().is_empty()); + + let mut crate_graph = CrateGraph::default(); + let crate_id = crate_graph.add_crate_root(root_file); + let mut change = AnalysisChange::new(); + change.set_crate_graph(crate_graph); + host.apply_change(change); + + assert_eq!(host.analysis().crate_for(mod_file).unwrap(), vec![crate_id]); +} + +fn get_all_refs(text: &str) -> Vec<(FileId, TextRange)> { + let (analysis, position) = single_file_with_position(text); + analysis.find_all_refs(position).unwrap() +} + +#[test] +fn test_find_all_refs_for_local() { + let code = r#" + fn main() { + let mut i = 1; + let j = 1; + i = i<|> + j; + + { + i = 0; + } + + i = 5; + }"#; + + let refs = get_all_refs(code); + assert_eq!(refs.len(), 5); +} + +#[test] +fn test_find_all_refs_for_param_inside() { + let code = r#" + fn foo(i : u32) -> u32 { + i<|> + }"#; + + let refs = get_all_refs(code); + assert_eq!(refs.len(), 2); +} + +#[test] +fn test_find_all_refs_for_fn_param() { + let code = r#" + fn foo(i<|> : u32) -> u32 { + i + }"#; + + let refs = get_all_refs(code); + assert_eq!(refs.len(), 2); +} +#[test] +fn test_rename_for_local() { + test_rename( + r#" + fn main() { + let mut i = 1; + let j = 1; + i = i<|> + j; + + { + i = 0; + } + + i = 5; + }"#, + "k", + r#" + fn main() { + let mut k = 1; + let j = 1; + k = k + j; + + { + k = 0; + } + + k = 5; + }"#, + ); +} + +#[test] +fn test_rename_for_param_inside() { + test_rename( + r#" + fn foo(i : u32) -> u32 { + i<|> + }"#, + "j", + r#" + fn foo(j : u32) -> u32 { + j + }"#, + ); +} + +#[test] +fn test_rename_refs_for_fn_param() { + test_rename( + r#" + fn foo(i<|> : u32) -> u32 { + i + }"#, + "new_name", + r#" + fn foo(new_name : u32) -> u32 { + new_name + }"#, + ); +} + +#[test] +fn test_rename_for_mut_param() { + test_rename( + r#" + fn foo(mut i<|> : u32) -> u32 { + i + }"#, + "new_name", + r#" + fn foo(mut new_name : u32) -> u32 { + new_name + }"#, + ); +} + +fn test_rename(text: &str, new_name: &str, expected: &str) { + let (analysis, position) = single_file_with_position(text); + let edits = analysis.rename(position, new_name).unwrap(); + let mut text_edit_bulder = ra_text_edit::TextEditBuilder::default(); + let mut file_id: Option = None; + for edit in edits { + file_id = Some(edit.file_id); + for atom in edit.edit.as_atoms() { + text_edit_bulder.replace(atom.delete, atom.insert.clone()); + } + } + let result = text_edit_bulder + .finish() + .apply(&*analysis.file_text(file_id.unwrap())); + assert_eq_text!(expected, &*result); +} + +#[test] +fn world_symbols_include_stuff_from_macros() { + let (analysis, _) = single_file( + " +salsa::query_group! { +pub trait HirDatabase: SyntaxDatabase {} +} + ", + ); + + let mut symbols = analysis.symbol_search(Query::new("Hir".into())).unwrap(); + let s = symbols.pop().unwrap(); + assert_eq!(s.name(), "HirDatabase"); + assert_eq!(s.range(), TextRange::from_to(33.into(), 44.into())); +} diff --git a/crates/ra_ide_api/tests/test/runnables.rs b/crates/ra_ide_api/tests/test/runnables.rs new file mode 100644 index 000000000000..da8d5e0d5dd0 --- /dev/null +++ b/crates/ra_ide_api/tests/test/runnables.rs @@ -0,0 +1,109 @@ +use test_utils::assert_eq_dbg; + +use ra_ide_api::mock_analysis::analysis_and_position; + +#[test] +fn test_runnables() { + let (analysis, pos) = analysis_and_position( + r#" + //- /lib.rs + <|> //empty + fn main() {} + + #[test] + fn test_foo() {} + + #[test] + #[ignore] + fn test_foo() {} + "#, + ); + let runnables = analysis.runnables(pos.file_id).unwrap(); + assert_eq_dbg( + r#"[Runnable { range: [1; 21), kind: Bin }, + Runnable { range: [22; 46), kind: Test { name: "test_foo" } }, + Runnable { range: [47; 81), kind: Test { name: "test_foo" } }]"#, + &runnables, + ) +} + +#[test] +fn test_runnables_module() { + let (analysis, pos) = analysis_and_position( + r#" + //- /lib.rs + <|> //empty + mod test_mod { + #[test] + fn test_foo1() {} + } + "#, + ); + let runnables = analysis.runnables(pos.file_id).unwrap(); + assert_eq_dbg( + r#"[Runnable { range: [1; 59), kind: TestMod { path: "test_mod" } }, + Runnable { range: [28; 57), kind: Test { name: "test_foo1" } }]"#, + &runnables, + ) +} + +#[test] +fn test_runnables_one_depth_layer_module() { + let (analysis, pos) = analysis_and_position( + r#" + //- /lib.rs + <|> //empty + mod foo { + mod test_mod { + #[test] + fn test_foo1() {} + } + } + "#, + ); + let runnables = analysis.runnables(pos.file_id).unwrap(); + assert_eq_dbg( + r#"[Runnable { range: [23; 85), kind: TestMod { path: "foo::test_mod" } }, + Runnable { range: [46; 79), kind: Test { name: "test_foo1" } }]"#, + &runnables, + ) +} + +#[test] +fn test_runnables_multiple_depth_module() { + let (analysis, pos) = analysis_and_position( + r#" + //- /lib.rs + <|> //empty + mod foo { + mod bar { + mod test_mod { + #[test] + fn test_foo1() {} + } + } + } + "#, + ); + let runnables = analysis.runnables(pos.file_id).unwrap(); + assert_eq_dbg( + r#"[Runnable { range: [41; 115), kind: TestMod { path: "foo::bar::test_mod" } }, + Runnable { range: [68; 105), kind: Test { name: "test_foo1" } }]"#, + &runnables, + ) +} + +#[test] +fn test_runnables_no_test_function_in_module() { + let (analysis, pos) = analysis_and_position( + r#" + //- /lib.rs + <|> //empty + mod test_mod { + fn foo1() {} + } + "#, + ); + let runnables = analysis.runnables(pos.file_id).unwrap(); + assert_eq_dbg(r#"[]"#, &runnables) +} diff --git a/crates/ra_lsp_server/Cargo.toml b/crates/ra_lsp_server/Cargo.toml index b9fd61105c60..296fae34f916 100644 --- a/crates/ra_lsp_server/Cargo.toml +++ b/crates/ra_lsp_server/Cargo.toml @@ -29,7 +29,7 @@ parking_lot = "0.7.0" thread_worker = { path = "../thread_worker" } ra_syntax = { path = "../ra_syntax" } ra_text_edit = { path = "../ra_text_edit" } -ra_analysis = { path = "../ra_analysis" } +ra_ide_api = { path = "../ra_ide_api" } gen_lsp_server = { path = "../gen_lsp_server" } ra_vfs = { path = "../ra_vfs" } diff --git a/crates/ra_lsp_server/src/conv.rs b/crates/ra_lsp_server/src/conv.rs index b3f8c83ccdf3..5c8b3c194bd3 100644 --- a/crates/ra_lsp_server/src/conv.rs +++ b/crates/ra_lsp_server/src/conv.rs @@ -4,7 +4,7 @@ use languageserver_types::{ TextDocumentItem, TextDocumentPositionParams, Url, VersionedTextDocumentIdentifier, WorkspaceEdit, }; -use ra_analysis::{ +use ra_ide_api::{ CompletionItem, CompletionItemKind, FileId, FilePosition, FileRange, FileSystemEdit, InsertText, NavigationTarget, SourceChange, SourceFileEdit, LineCol, LineIndex, translate_offset_with_edit diff --git a/crates/ra_lsp_server/src/main_loop.rs b/crates/ra_lsp_server/src/main_loop.rs index 2dc1be26aac1..96923fac7733 100644 --- a/crates/ra_lsp_server/src/main_loop.rs +++ b/crates/ra_lsp_server/src/main_loop.rs @@ -10,7 +10,7 @@ use gen_lsp_server::{ handle_shutdown, ErrorCode, RawMessage, RawNotification, RawRequest, RawResponse, }; use languageserver_types::NumberOrString; -use ra_analysis::{Canceled, FileId, LibraryData}; +use ra_ide_api::{Canceled, FileId, LibraryData}; use ra_vfs::VfsTask; use rayon; use rustc_hash::FxHashSet; diff --git a/crates/ra_lsp_server/src/main_loop/handlers.rs b/crates/ra_lsp_server/src/main_loop/handlers.rs index b7777bfc3931..a653c5ada0ba 100644 --- a/crates/ra_lsp_server/src/main_loop/handlers.rs +++ b/crates/ra_lsp_server/src/main_loop/handlers.rs @@ -8,7 +8,7 @@ use languageserver_types::{ ParameterInformation, ParameterLabel, Position, PrepareRenameResponse, Range, RenameParams, SignatureInformation, SymbolInformation, TextDocumentIdentifier, TextEdit, WorkspaceEdit, }; -use ra_analysis::{ +use ra_ide_api::{ FileId, FilePosition, FileRange, FoldKind, Query, RunnableKind, Severity, SourceChange, }; use ra_syntax::{TextUnit, AstNode}; @@ -736,7 +736,7 @@ fn highlight(world: &ServerWorld, file_id: FileId) -> Result> { } fn to_diagnostic_severity(severity: Severity) -> DiagnosticSeverity { - use ra_analysis::Severity::*; + use ra_ide_api::Severity::*; match severity { Error => DiagnosticSeverity::Error, diff --git a/crates/ra_lsp_server/src/main_loop/subscriptions.rs b/crates/ra_lsp_server/src/main_loop/subscriptions.rs index 03f41e8705f5..a83e0155730e 100644 --- a/crates/ra_lsp_server/src/main_loop/subscriptions.rs +++ b/crates/ra_lsp_server/src/main_loop/subscriptions.rs @@ -1,4 +1,4 @@ -use ra_analysis::FileId; +use ra_ide_api::FileId; use rustc_hash::FxHashSet; pub struct Subscriptions { diff --git a/crates/ra_lsp_server/src/server_world.rs b/crates/ra_lsp_server/src/server_world.rs index ebf2b15ccd80..76c76766d36a 100644 --- a/crates/ra_lsp_server/src/server_world.rs +++ b/crates/ra_lsp_server/src/server_world.rs @@ -1,10 +1,10 @@ use std::{ - path::{PathBuf}, + path::PathBuf, sync::Arc, }; use languageserver_types::Url; -use ra_analysis::{ +use ra_ide_api::{ Analysis, AnalysisChange, AnalysisHost, CrateGraph, FileId, LibraryData, SourceRootId }; @@ -12,7 +12,7 @@ use ra_vfs::{Vfs, VfsChange, VfsFile, VfsRoot}; use rustc_hash::FxHashMap; use relative_path::RelativePathBuf; use parking_lot::RwLock; -use failure::{format_err}; +use failure::format_err; use crate::{ project_model::{CargoWorkspace, TargetKind}, From 0c62b1bb7a49bf527780ce1f8cade5eb4fbfdb2d Mon Sep 17 00:00:00 2001 From: Aleksey Kladov Date: Tue, 8 Jan 2019 22:45:52 +0300 Subject: [PATCH 5/5] fix the docs --- ARCHITECTURE.md | 57 ++++++++++++++++++++---------------- crates/ra_db/src/lib.rs | 2 +- crates/ra_ide_api/src/lib.rs | 10 +++++-- 3 files changed, 40 insertions(+), 29 deletions(-) diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 277b29c12237..9c404f4c3ddb 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -15,7 +15,7 @@ More specifically, input data consists of a set of test files (`(PathBuf, String)` pairs) and an information about project structure, the so called `CrateGraph`. Crate graph specifies which files are crate roots, which cfg flags are specified for each crate (TODO: actually implement this) and what are -dependencies between the crate. The analyzer keeps all these input data in +dependencies between the crates. The analyzer keeps all these input data in memory and never does any IO. Because the input data is source code, which typically measures in tens of megabytes at most, keeping all input data in memory is OK. @@ -74,9 +74,9 @@ notes. - `algo`: generic tree algorithms, including `walk` for O(1) stack space tree traversal (this is cool) and `visit` for type-driven visiting the nodes (this is double plus cool, if you understand how - `Visitor` works, you understand rust-analyzer). + `Visitor` works, you understand the design of syntax trees). -Test for ra_syntax are mostly data-driven: `tests/data/parser` contains a bunch of `.rs` +Tests for ra_syntax are mostly data-driven: `tests/data/parser` contains a bunch of `.rs` (test vectors) and `.txt` files with corresponding syntax trees. During testing, we check `.rs` against `.txt`. If the `.txt` file is missing, it is created (this is how you update tests). Additionally, running `cargo gen-tests` will walk the grammar module and collect @@ -107,41 +107,46 @@ guessing a HIR for a particular source position. Underneath, HIR works on top of salsa, using a `HirDatabase` trait. -### `crates/ra_analysis` +### `crates/ra_ide_api` -A stateful library for analyzing many Rust files as they change. -`AnalysisHost` is a mutable entity (clojure's atom) which holds the -current state, incorporates changes and handles out `Analysis` --- an -immutable and consistent snapshot of world state at a point in time, which -actually powers analysis. +A stateful library for analyzing many Rust files as they change. `AnalysisHost` +is a mutable entity (clojure's atom) which holds the current state, incorporates +changes and handles out `Analysis` --- an immutable and consistent snapshot of +world state at a point in time, which actually powers analysis. -One interesting aspect of analysis is its support for cancellation. When a change -is applied to `AnalysisHost`, first all currently active snapshots are +One interesting aspect of analysis is its support for cancellation. When a +change is applied to `AnalysisHost`, first all currently active snapshots are cancelled. Only after all snapshots are dropped the change actually affects the database. -### `crates/ra_lsp_server` +APIs in this crate are IDE centric: they take text offsets as input and produce +offsets and strings as output. This works on top of rich code model powered by +`hir`. -An LSP implementation which uses `ra_analysis` for managing state and -`ra_editor` for actually doing useful stuff. +### `crates/ra_ide_api_light` -See [#79](https://github.com/rust-analyzer/rust-analyzer/pull/79/) as an -example of PR which adds a new feature to `ra_editor` and exposes it -to `ra_lsp_server`. +All IDE features which can be implemented if you only have access to a single +file. `ra_ide_api_light` could be used to enhance editing of Rust code without +the need to fiddle with build-systems, file synchronization and such. -### `crates/ra_editor` - -All IDE features which can be implemented if you only have access to a -single file. `ra_editor` could be used to enhance editing of Rust code -without the need to fiddle with build-systems, file -synchronization and such. - -In a sense, `ra_editor` is just a bunch of pure functions which take a +In a sense, `ra_ide_api_light` is just a bunch of pure functions which take a syntax tree as input. -The tests for `ra_editor` are `#[cfg(test)] mod tests` unit-tests spread +The tests for `ra_ide_api_light` are `#[cfg(test)] mod tests` unit-tests spread throughout its modules. + +### `crates/ra_lsp_server` + +An LSP implementation which wraps `ra_ide_api` into a langauge server protocol. + +### `crates/ra_vfs` + +Although `hir` and `ra_ide_api` don't do any io, we need to be able to read +files from disk at the end of the day. This is what `ra_vfs` does. It also +manages overlays: "dirty" files in the editor, whose "true" contents is +different from data on disk. + ### `crates/gen_lsp_server` A language server scaffold, exposing a synchronous crossbeam-channel based API. diff --git a/crates/ra_db/src/lib.rs b/crates/ra_db/src/lib.rs index f56f70983ac4..e680d9fc374b 100644 --- a/crates/ra_db/src/lib.rs +++ b/crates/ra_db/src/lib.rs @@ -1,4 +1,4 @@ -//! ra_db defines basic database traits. Concrete DB is defined by ra_analysis. +//! ra_db defines basic database traits. Concrete DB is defined by ra_ide_api. mod cancelation; mod syntax_ptr; mod input; diff --git a/crates/ra_ide_api/src/lib.rs b/crates/ra_ide_api/src/lib.rs index 183e3670691e..7e9ca20341ef 100644 --- a/crates/ra_ide_api/src/lib.rs +++ b/crates/ra_ide_api/src/lib.rs @@ -1,8 +1,14 @@ -//! ra_analyzer crate provides "ide-centric" APIs for the rust-analyzer. What -//! powers this API are the `RootDatabase` struct, which defines a `salsa` +//! ra_ide_api crate provides "ide-centric" APIs for the rust-analyzer. That is, +//! it generally operates with files and text ranges, and returns results as +//! Strings, suitable for displaying to the human. +//! +//! What powers this API are the `RootDatabase` struct, which defines a `salsa` //! database, and the `ra_hir` crate, where majority of the analysis happens. //! However, IDE specific bits of the analysis (most notably completion) happen //! in this crate. +//! +//! The sibling `ra_ide_api_light` handles thouse bits of IDE functionality +//! which are restricted to a single file and need only syntax. macro_rules! ctry { ($expr:expr) => { match $expr {