]> git.lizzy.rs Git - rust.git/commitdiff
Merge #5951 #5975
authorbors[bot] <26634292+bors[bot]@users.noreply.github.com>
Fri, 11 Sep 2020 13:47:33 +0000 (13:47 +0000)
committerGitHub <noreply@github.com>
Fri, 11 Sep 2020 13:47:33 +0000 (13:47 +0000)
5951: Rename record_field_pat to record_pat_field r=jonas-schievink a=pksunkara

The token was renamed but not this.

5975: Report better errors in project.json/sysroot r=jonas-schievink a=jonas-schievink

This does a bunch of light refactoring so that the `Sysroot` is loaded later, which makes sure that any errors are reported to the user. I then added a check that reports an error if libcore is missing in the loaded sysroot. Since a sysroot without libcore is very useless, this indicates a configuration error.

Co-authored-by: Pavan Kumar Sunkara <pavan.sss1991@gmail.com>
Co-authored-by: Jonas Schievink <jonas.schievink@ferrous-systems.com>
20 files changed:
crates/assists/src/handlers/remove_dbg.rs
crates/base_db/src/input.rs
crates/ide/src/completion.rs
crates/ide/src/completion/complete_attribute.rs
crates/ide/src/completion/complete_mod.rs [new file with mode: 0644]
crates/ide/src/completion/complete_qualified_path.rs
crates/ide/src/completion/complete_unqualified_path.rs
crates/ide/src/completion/completion_context.rs
crates/ide/src/completion/patterns.rs
crates/ide/src/syntax_highlighting.rs
crates/ide/src/syntax_highlighting/test_data/highlighting.html
crates/ide/src/syntax_highlighting/tests.rs
crates/project_model/src/lib.rs
crates/project_model/src/project_json.rs
crates/project_model/src/sysroot.rs
crates/rust-analyzer/src/diagnostics/test_data/macro_compiler_error.txt
crates/rust-analyzer/src/diagnostics/to_proto.rs
crates/rust-analyzer/src/reload.rs
crates/vfs/src/file_set.rs
crates/vfs/src/vfs_path.rs

index 4e252edf02d797c1dc895ec0bc93dbdadbf030d2..0b581dc22a27163427d15f50290c2a585c264e2f 100644 (file)
@@ -1,6 +1,6 @@
 use syntax::{
     ast::{self, AstNode},
-    TextRange, TextSize, T,
+    SyntaxElement, TextRange, TextSize, T,
 };
 
 use crate::{AssistContext, AssistId, AssistKind, Assists};
 // ```
 pub(crate) fn remove_dbg(acc: &mut Assists, ctx: &AssistContext) -> Option<()> {
     let macro_call = ctx.find_node_at_offset::<ast::MacroCall>()?;
+    let new_contents = adjusted_macro_contents(&macro_call)?;
 
-    if !is_valid_macrocall(&macro_call, "dbg")? {
-        return None;
-    }
-
-    let is_leaf = macro_call.syntax().next_sibling().is_none();
-
+    let macro_text_range = macro_call.syntax().text_range();
     let macro_end = if macro_call.semicolon_token().is_some() {
-        macro_call.syntax().text_range().end() - TextSize::of(';')
+        macro_text_range.end() - TextSize::of(';')
     } else {
-        macro_call.syntax().text_range().end()
+        macro_text_range.end()
     };
 
-    // macro_range determines what will be deleted and replaced with macro_content
-    let macro_range = TextRange::new(macro_call.syntax().text_range().start(), macro_end);
-    let paste_instead_of_dbg = {
-        let text = macro_call.token_tree()?.syntax().text();
-
-        // leafiness determines if we should include the parenthesis or not
-        let slice_index: TextRange = if is_leaf {
-            // leaf means - we can extract the contents of the dbg! in text
-            TextRange::new(TextSize::of('('), text.len() - TextSize::of(')'))
-        } else {
-            // not leaf - means we should keep the parens
-            TextRange::up_to(text.len())
-        };
-        text.slice(slice_index).to_string()
-    };
+    acc.add(
+        AssistId("remove_dbg", AssistKind::Refactor),
+        "Remove dbg!()",
+        macro_text_range,
+        |builder| {
+            builder.replace(TextRange::new(macro_text_range.start(), macro_end), new_contents);
+        },
+    )
+}
 
-    let target = macro_call.syntax().text_range();
-    acc.add(AssistId("remove_dbg", AssistKind::Refactor), "Remove dbg!()", target, |builder| {
-        builder.replace(macro_range, paste_instead_of_dbg);
-    })
+fn adjusted_macro_contents(macro_call: &ast::MacroCall) -> Option<String> {
+    let contents = get_valid_macrocall_contents(&macro_call, "dbg")?;
+    let is_leaf = macro_call.syntax().next_sibling().is_none();
+    let macro_text_with_brackets = macro_call.token_tree()?.syntax().text();
+    let slice_index = if is_leaf || !needs_parentheses_around_macro_contents(contents) {
+        TextRange::new(TextSize::of('('), macro_text_with_brackets.len() - TextSize::of(')'))
+    } else {
+        // leave parenthesis around macro contents to preserve the semantics
+        TextRange::up_to(macro_text_with_brackets.len())
+    };
+    Some(macro_text_with_brackets.slice(slice_index).to_string())
 }
 
 /// Verifies that the given macro_call actually matches the given name
-/// and contains proper ending tokens
-fn is_valid_macrocall(macro_call: &ast::MacroCall, macro_name: &str) -> Option<bool> {
+/// and contains proper ending tokens, then returns the contents between the ending tokens
+fn get_valid_macrocall_contents(
+    macro_call: &ast::MacroCall,
+    macro_name: &str,
+) -> Option<Vec<SyntaxElement>> {
     let path = macro_call.path()?;
     let name_ref = path.segment()?.name_ref()?;
 
     // Make sure it is actually a dbg-macro call, dbg followed by !
     let excl = path.syntax().next_sibling_or_token()?;
-
     if name_ref.text() != macro_name || excl.kind() != T![!] {
         return None;
     }
 
-    let node = macro_call.token_tree()?.syntax().clone();
-    let first_child = node.first_child_or_token()?;
-    let last_child = node.last_child_or_token()?;
+    let mut children_with_tokens = macro_call.token_tree()?.syntax().children_with_tokens();
+    let first_child = children_with_tokens.next()?;
+    let mut contents_between_brackets = children_with_tokens.collect::<Vec<_>>();
+    let last_child = contents_between_brackets.pop()?;
+
+    if contents_between_brackets.is_empty() {
+        None
+    } else {
+        match (first_child.kind(), last_child.kind()) {
+            (T!['('], T![')']) | (T!['['], T![']']) | (T!['{'], T!['}']) => {
+                Some(contents_between_brackets)
+            }
+            _ => None,
+        }
+    }
+}
+
+fn needs_parentheses_around_macro_contents(macro_contents: Vec<SyntaxElement>) -> bool {
+    if macro_contents.len() < 2 {
+        return false;
+    }
+
+    let mut macro_contents_kind_not_in_brackets = Vec::with_capacity(macro_contents.len());
 
-    match (first_child.kind(), last_child.kind()) {
-        (T!['('], T![')']) | (T!['['], T![']']) | (T!['{'], T!['}']) => Some(true),
-        _ => Some(false),
+    let mut first_bracket_in_macro = None;
+    let mut unpaired_brackets_in_contents = Vec::new();
+    for element in macro_contents {
+        match element.kind() {
+            T!['('] | T!['['] | T!['{'] => {
+                if let None = first_bracket_in_macro {
+                    first_bracket_in_macro = Some(element.clone())
+                }
+                unpaired_brackets_in_contents.push(element);
+            }
+            T![')'] => {
+                if !matches!(unpaired_brackets_in_contents.pop(), Some(correct_bracket) if correct_bracket.kind() == T!['('])
+                {
+                    return true;
+                }
+            }
+            T![']'] => {
+                if !matches!(unpaired_brackets_in_contents.pop(), Some(correct_bracket) if correct_bracket.kind() == T!['['])
+                {
+                    return true;
+                }
+            }
+            T!['}'] => {
+                if !matches!(unpaired_brackets_in_contents.pop(), Some(correct_bracket) if correct_bracket.kind() == T!['{'])
+                {
+                    return true;
+                }
+            }
+            other_kind => {
+                if unpaired_brackets_in_contents.is_empty() {
+                    macro_contents_kind_not_in_brackets.push(other_kind);
+                }
+            }
+        }
     }
+
+    !unpaired_brackets_in_contents.is_empty()
+        || matches!(first_bracket_in_macro, Some(bracket) if bracket.kind() != T!['('])
+        || macro_contents_kind_not_in_brackets
+            .into_iter()
+            .any(|macro_contents_kind| macro_contents_kind.is_punct())
 }
 
 #[cfg(test)]
@@ -156,6 +212,29 @@ fn test_remove_dbg_keep_semicolon() {
         );
     }
 
+    #[test]
+    fn remove_dbg_from_non_leaf_simple_expression() {
+        check_assist(
+            remove_dbg,
+            "
+fn main() {
+    let mut a = 1;
+    while dbg!<|>(a) < 10000 {
+        a += 1;
+    }
+}
+",
+            "
+fn main() {
+    let mut a = 1;
+    while a < 10000 {
+        a += 1;
+    }
+}
+",
+        );
+    }
+
     #[test]
     fn test_remove_dbg_keep_expression() {
         check_assist(
@@ -163,6 +242,8 @@ fn test_remove_dbg_keep_expression() {
             r#"let res = <|>dbg!(a + b).foo();"#,
             r#"let res = (a + b).foo();"#,
         );
+
+        check_assist(remove_dbg, r#"let res = <|>dbg!(2 + 2) * 5"#, r#"let res = (2 + 2) * 5"#);
     }
 
     #[test]
index f3d65cdf02f510cd13b69efc466077f20d06fb01..9a61f1d5660877d40c6215a4fda547f7b59c0be7 100644 (file)
@@ -12,7 +12,7 @@
 use rustc_hash::{FxHashMap, FxHashSet};
 use syntax::SmolStr;
 use tt::TokenExpander;
-use vfs::file_set::FileSet;
+use vfs::{file_set::FileSet, VfsPath};
 
 pub use vfs::FileId;
 
@@ -43,6 +43,12 @@ pub fn new_local(file_set: FileSet) -> SourceRoot {
     pub fn new_library(file_set: FileSet) -> SourceRoot {
         SourceRoot { is_library: true, file_set }
     }
+    pub fn path_for_file(&self, file: &FileId) -> Option<&VfsPath> {
+        self.file_set.path_for_file(file)
+    }
+    pub fn file_for_path(&self, path: &VfsPath) -> Option<&FileId> {
+        self.file_set.file_for_path(path)
+    }
     pub fn iter(&self) -> impl Iterator<Item = FileId> + '_ {
         self.file_set.iter()
     }
index 33bed699172bc9692ef94d06c0ddef983b73c998..daea2aa9585b187d8b3c5b97b4d11b7db06ae583 100644 (file)
@@ -19,6 +19,7 @@
 mod complete_postfix;
 mod complete_macro_in_item_position;
 mod complete_trait_impl;
+mod complete_mod;
 
 use ide_db::RootDatabase;
 
@@ -124,6 +125,7 @@ pub(crate) fn completions(
     complete_postfix::complete_postfix(&mut acc, &ctx);
     complete_macro_in_item_position::complete_macro_in_item_position(&mut acc, &ctx);
     complete_trait_impl::complete_trait_impl(&mut acc, &ctx);
+    complete_mod::complete_mod(&mut acc, &ctx);
 
     Some(acc)
 }
index 0abfaebcbc669f4b887d9f33560c8a6f3d2567f9..f4a9864d10e7aab77d19c932c7c236b4a4be62f1 100644 (file)
 };
 
 pub(super) fn complete_attribute(acc: &mut Completions, ctx: &CompletionContext) -> Option<()> {
+    if ctx.mod_declaration_under_caret.is_some() {
+        return None;
+    }
+
     let attribute = ctx.attribute_under_caret.as_ref()?;
     match (attribute.path(), attribute.token_tree()) {
         (Some(path), Some(token_tree)) if path.to_string() == "derive" => {
diff --git a/crates/ide/src/completion/complete_mod.rs b/crates/ide/src/completion/complete_mod.rs
new file mode 100644 (file)
index 0000000..3cfc2e1
--- /dev/null
@@ -0,0 +1,324 @@
+//! Completes mod declarations.
+
+use base_db::{SourceDatabaseExt, VfsPath};
+use hir::{Module, ModuleSource};
+use ide_db::RootDatabase;
+use rustc_hash::FxHashSet;
+
+use crate::{CompletionItem, CompletionItemKind};
+
+use super::{
+    completion_context::CompletionContext, completion_item::CompletionKind,
+    completion_item::Completions,
+};
+
+/// Complete mod declaration, i.e. `mod <|> ;`
+pub(super) fn complete_mod(acc: &mut Completions, ctx: &CompletionContext) -> Option<()> {
+    let mod_under_caret = match &ctx.mod_declaration_under_caret {
+        Some(mod_under_caret) if mod_under_caret.item_list().is_some() => return None,
+        Some(mod_under_caret) => mod_under_caret,
+        None => return None,
+    };
+
+    let _p = profile::span("completion::complete_mod");
+
+    let current_module = ctx.scope.module()?;
+
+    let module_definition_file =
+        current_module.definition_source(ctx.db).file_id.original_file(ctx.db);
+    let source_root = ctx.db.source_root(ctx.db.file_source_root(module_definition_file));
+    let directory_to_look_for_submodules = directory_to_look_for_submodules(
+        current_module,
+        ctx.db,
+        source_root.path_for_file(&module_definition_file)?,
+    )?;
+
+    let existing_mod_declarations = current_module
+        .children(ctx.db)
+        .filter_map(|module| Some(module.name(ctx.db)?.to_string()))
+        .collect::<FxHashSet<_>>();
+
+    let module_declaration_file =
+        current_module.declaration_source(ctx.db).map(|module_declaration_source_file| {
+            module_declaration_source_file.file_id.original_file(ctx.db)
+        });
+
+    source_root
+        .iter()
+        .filter(|submodule_candidate_file| submodule_candidate_file != &module_definition_file)
+        .filter(|submodule_candidate_file| {
+            Some(submodule_candidate_file) != module_declaration_file.as_ref()
+        })
+        .filter_map(|submodule_file| {
+            let submodule_path = source_root.path_for_file(&submodule_file)?;
+            let directory_with_submodule = submodule_path.parent()?;
+            match submodule_path.name_and_extension()? {
+                ("lib", Some("rs")) | ("main", Some("rs")) => None,
+                ("mod", Some("rs")) => {
+                    if directory_with_submodule.parent()? == directory_to_look_for_submodules {
+                        match directory_with_submodule.name_and_extension()? {
+                            (directory_name, None) => Some(directory_name.to_owned()),
+                            _ => None,
+                        }
+                    } else {
+                        None
+                    }
+                }
+                (file_name, Some("rs"))
+                    if directory_with_submodule == directory_to_look_for_submodules =>
+                {
+                    Some(file_name.to_owned())
+                }
+                _ => None,
+            }
+        })
+        .filter(|name| !existing_mod_declarations.contains(name))
+        .for_each(|submodule_name| {
+            let mut label = submodule_name;
+            if mod_under_caret.semicolon_token().is_none() {
+                label.push(';')
+            }
+            acc.add(
+                CompletionItem::new(CompletionKind::Magic, ctx.source_range(), &label)
+                    .kind(CompletionItemKind::Module),
+            )
+        });
+
+    Some(())
+}
+
+fn directory_to_look_for_submodules(
+    module: Module,
+    db: &RootDatabase,
+    module_file_path: &VfsPath,
+) -> Option<VfsPath> {
+    let directory_with_module_path = module_file_path.parent()?;
+    let base_directory = match module_file_path.name_and_extension()? {
+        ("mod", Some("rs")) | ("lib", Some("rs")) | ("main", Some("rs")) => {
+            Some(directory_with_module_path)
+        }
+        (regular_rust_file_name, Some("rs")) => {
+            if matches!(
+                (
+                    directory_with_module_path
+                        .parent()
+                        .as_ref()
+                        .and_then(|path| path.name_and_extension()),
+                    directory_with_module_path.name_and_extension(),
+                ),
+                (Some(("src", None)), Some(("bin", None)))
+            ) {
+                // files in /src/bin/ can import each other directly
+                Some(directory_with_module_path)
+            } else {
+                directory_with_module_path.join(regular_rust_file_name)
+            }
+        }
+        _ => None,
+    }?;
+
+    let mut resulting_path = base_directory;
+    for module in module_chain_to_containing_module_file(module, db) {
+        if let Some(name) = module.name(db) {
+            resulting_path = resulting_path.join(&name.to_string())?;
+        }
+    }
+
+    Some(resulting_path)
+}
+
+fn module_chain_to_containing_module_file(
+    current_module: Module,
+    db: &RootDatabase,
+) -> Vec<Module> {
+    let mut path = Vec::new();
+
+    let mut current_module = Some(current_module);
+    while let Some(ModuleSource::Module(_)) =
+        current_module.map(|module| module.definition_source(db).value)
+    {
+        if let Some(module) = current_module {
+            path.insert(0, module);
+            current_module = module.parent(db);
+        } else {
+            current_module = None;
+        }
+    }
+
+    path
+}
+
+#[cfg(test)]
+mod tests {
+    use crate::completion::{test_utils::completion_list, CompletionKind};
+    use expect_test::{expect, Expect};
+
+    fn check(ra_fixture: &str, expect: Expect) {
+        let actual = completion_list(ra_fixture, CompletionKind::Magic);
+        expect.assert_eq(&actual);
+    }
+
+    #[test]
+    fn lib_module_completion() {
+        check(
+            r#"
+            //- /lib.rs
+            mod <|>
+            //- /foo.rs
+            fn foo() {}
+            //- /foo/ignored_foo.rs
+            fn ignored_foo() {}
+            //- /bar/mod.rs
+            fn bar() {}
+            //- /bar/ignored_bar.rs
+            fn ignored_bar() {}
+        "#,
+            expect![[r#"
+                md bar;
+                md foo;
+            "#]],
+        );
+    }
+
+    #[test]
+    fn no_module_completion_with_module_body() {
+        check(
+            r#"
+            //- /lib.rs
+            mod <|> {
+
+            }
+            //- /foo.rs
+            fn foo() {}
+        "#,
+            expect![[r#""#]],
+        );
+    }
+
+    #[test]
+    fn main_module_completion() {
+        check(
+            r#"
+            //- /main.rs
+            mod <|>
+            //- /foo.rs
+            fn foo() {}
+            //- /foo/ignored_foo.rs
+            fn ignored_foo() {}
+            //- /bar/mod.rs
+            fn bar() {}
+            //- /bar/ignored_bar.rs
+            fn ignored_bar() {}
+        "#,
+            expect![[r#"
+                md bar;
+                md foo;
+            "#]],
+        );
+    }
+
+    #[test]
+    fn main_test_module_completion() {
+        check(
+            r#"
+            //- /main.rs
+            mod tests {
+                mod <|>;
+            }
+            //- /tests/foo.rs
+            fn foo() {}
+        "#,
+            expect![[r#"
+                md foo
+            "#]],
+        );
+    }
+
+    #[test]
+    fn directly_nested_module_completion() {
+        check(
+            r#"
+            //- /lib.rs
+            mod foo;
+            //- /foo.rs
+            mod <|>;
+            //- /foo/bar.rs
+            fn bar() {}
+            //- /foo/bar/ignored_bar.rs
+            fn ignored_bar() {}
+            //- /foo/baz/mod.rs
+            fn baz() {}
+            //- /foo/moar/ignored_moar.rs
+            fn ignored_moar() {}
+        "#,
+            expect![[r#"
+                md bar
+                md baz
+            "#]],
+        );
+    }
+
+    #[test]
+    fn nested_in_source_module_completion() {
+        check(
+            r#"
+            //- /lib.rs
+            mod foo;
+            //- /foo.rs
+            mod bar {
+                mod <|>
+            }
+            //- /foo/bar/baz.rs
+            fn baz() {}
+        "#,
+            expect![[r#"
+                md baz;
+            "#]],
+        );
+    }
+
+    // FIXME binary modules are not supported in tests properly
+    // Binary modules are a bit special, they allow importing the modules from `/src/bin`
+    // and that's why are good to test two things:
+    // * no cycles are allowed in mod declarations
+    // * no modules from the parent directory are proposed
+    // Unfortunately, binary modules support is in cargo not rustc,
+    // hence the test does not work now
+    //
+    // #[test]
+    // fn regular_bin_module_completion() {
+    //     check(
+    //         r#"
+    //         //- /src/bin.rs
+    //         fn main() {}
+    //         //- /src/bin/foo.rs
+    //         mod <|>
+    //         //- /src/bin/bar.rs
+    //         fn bar() {}
+    //         //- /src/bin/bar/bar_ignored.rs
+    //         fn bar_ignored() {}
+    //     "#,
+    //         expect![[r#"
+    //             md bar;
+    //         "#]],
+    //     );
+    // }
+
+    #[test]
+    fn already_declared_bin_module_completion_omitted() {
+        check(
+            r#"
+            //- /src/bin.rs
+            fn main() {}
+            //- /src/bin/foo.rs
+            mod <|>
+            //- /src/bin/bar.rs
+            mod foo;
+            fn bar() {}
+            //- /src/bin/bar/bar_ignored.rs
+            fn bar_ignored() {}
+        "#,
+            expect![[r#""#]],
+        );
+    }
+}
index accb09f7e8c874cbc646a0ddde2a5ccc51f33c60..79de50792782d237d33afe90be5a24df1e8c2fe2 100644 (file)
@@ -13,7 +13,7 @@ pub(super) fn complete_qualified_path(acc: &mut Completions, ctx: &CompletionCon
         None => return,
     };
 
-    if ctx.attribute_under_caret.is_some() {
+    if ctx.attribute_under_caret.is_some() || ctx.mod_declaration_under_caret.is_some() {
         return;
     }
 
index 1f1b682a78c98a7ccba16447d0571441eeda82e7..8eda4b64d49aa94d141b2911ea8518ff4bddc490 100644 (file)
@@ -13,6 +13,7 @@ pub(super) fn complete_unqualified_path(acc: &mut Completions, ctx: &CompletionC
     if ctx.record_lit_syntax.is_some()
         || ctx.record_pat_syntax.is_some()
         || ctx.attribute_under_caret.is_some()
+        || ctx.mod_declaration_under_caret.is_some()
     {
         return;
     }
index 47355d5dcba3ae91d1d99e32bf89576e5f721624..161f59c1e48c5b6d7df563dad70b341f40410684 100644 (file)
@@ -77,6 +77,7 @@ pub(crate) struct CompletionContext<'a> {
     pub(super) is_path_type: bool,
     pub(super) has_type_args: bool,
     pub(super) attribute_under_caret: Option<ast::Attr>,
+    pub(super) mod_declaration_under_caret: Option<ast::Module>,
     pub(super) unsafe_is_prev: bool,
     pub(super) if_is_prev: bool,
     pub(super) block_expr_parent: bool,
@@ -152,6 +153,7 @@ pub(super) fn new(
             has_type_args: false,
             dot_receiver_is_ambiguous_float_literal: false,
             attribute_under_caret: None,
+            mod_declaration_under_caret: None,
             unsafe_is_prev: false,
             in_loop_body: false,
             ref_pat_parent: false,
@@ -238,7 +240,10 @@ fn fill_keyword_patterns(&mut self, file_with_fake_ident: &SyntaxNode, offset: T
         self.trait_as_prev_sibling = has_trait_as_prev_sibling(syntax_element.clone());
         self.is_match_arm = is_match_arm(syntax_element.clone());
         self.has_item_list_or_source_file_parent =
-            has_item_list_or_source_file_parent(syntax_element);
+            has_item_list_or_source_file_parent(syntax_element.clone());
+        self.mod_declaration_under_caret =
+            find_node_at_offset::<ast::Module>(&file_with_fake_ident, offset)
+                .filter(|module| module.item_list().is_none());
     }
 
     fn fill(
index c6ae589db0de4d8c608f4a0f3c0d2bfb5985b699..b17ddf1338a50b0cf89b470b9a01ccf402e98fc6 100644 (file)
@@ -115,6 +115,7 @@ pub(crate) fn if_is_prev(element: SyntaxElement) -> bool {
         .filter(|it| it.kind() == IF_KW)
         .is_some()
 }
+
 #[test]
 fn test_if_is_prev() {
     check_pattern_is_applicable(r"if l<|>", if_is_prev);
index 25d6f7abd829057d2f61e3679e09d61325b01d08..d9fc25d88bc8423eebfed1d8a8451be98e4807f9 100644 (file)
@@ -4,7 +4,7 @@
 #[cfg(test)]
 mod tests;
 
-use hir::{Name, Semantics, VariantDef};
+use hir::{Local, Name, Semantics, VariantDef};
 use ide_db::{
     defs::{classify_name, classify_name_ref, Definition, NameClass, NameRefClass},
     RootDatabase,
@@ -13,8 +13,8 @@
 use syntax::{
     ast::{self, HasFormatSpecifier},
     AstNode, AstToken, Direction, NodeOrToken, SyntaxElement,
-    SyntaxKind::*,
-    TextRange, WalkEvent, T,
+    SyntaxKind::{self, *},
+    SyntaxNode, SyntaxToken, TextRange, WalkEvent, T,
 };
 
 use crate::FileId;
@@ -454,6 +454,32 @@ fn macro_call_range(macro_call: &ast::MacroCall) -> Option<TextRange> {
     Some(TextRange::new(range_start, range_end))
 }
 
+/// Returns true if the parent nodes of `node` all match the `SyntaxKind`s in `kinds` exactly.
+fn parents_match(mut node: NodeOrToken<SyntaxNode, SyntaxToken>, mut kinds: &[SyntaxKind]) -> bool {
+    while let (Some(parent), [kind, rest @ ..]) = (&node.parent(), kinds) {
+        if parent.kind() != *kind {
+            return false;
+        }
+
+        // FIXME: Would be nice to get parent out of the match, but binding by-move and by-value
+        // in the same pattern is unstable: rust-lang/rust#68354.
+        node = node.parent().unwrap().into();
+        kinds = rest;
+    }
+
+    // Only true if we matched all expected kinds
+    kinds.len() == 0
+}
+
+fn is_consumed_lvalue(
+    node: NodeOrToken<SyntaxNode, SyntaxToken>,
+    local: &Local,
+    db: &RootDatabase,
+) -> bool {
+    // When lvalues are passed as arguments and they're not Copy, then mark them as Consuming.
+    parents_match(node, &[PATH_SEGMENT, PATH, PATH_EXPR, ARG_LIST]) && !local.ty(db).is_copy(db)
+}
+
 fn highlight_element(
     sema: &Semantics<RootDatabase>,
     bindings_shadow_count: &mut FxHashMap<Name, u32>,
@@ -522,6 +548,12 @@ fn highlight_element(
 
                             let mut h = highlight_def(db, def);
 
+                            if let Definition::Local(local) = &def {
+                                if is_consumed_lvalue(name_ref.syntax().clone().into(), local, db) {
+                                    h |= HighlightModifier::Consuming;
+                                }
+                            }
+
                             if let Some(parent) = name_ref.syntax().parent() {
                                 if matches!(parent.kind(), FIELD_EXPR | RECORD_PAT_FIELD) {
                                     if let Definition::Field(field) = def {
@@ -645,21 +677,30 @@ fn highlight_element(
                         .and_then(ast::SelfParam::cast)
                         .and_then(|p| p.mut_token())
                         .is_some();
-                    // closure to enforce lazyness
-                    let self_path = || {
-                        sema.resolve_path(&element.parent()?.parent().and_then(ast::Path::cast)?)
-                    };
+                    let self_path = &element
+                        .parent()
+                        .as_ref()
+                        .and_then(SyntaxNode::parent)
+                        .and_then(ast::Path::cast)
+                        .and_then(|p| sema.resolve_path(&p));
+                    let mut h = HighlightTag::SelfKeyword.into();
                     if self_param_is_mut
-                        || matches!(self_path(),
+                        || matches!(self_path,
                             Some(hir::PathResolution::Local(local))
                                 if local.is_self(db)
                                     && (local.is_mut(db) || local.ty(db).is_mutable_reference())
                         )
                     {
-                        HighlightTag::SelfKeyword | HighlightModifier::Mutable
-                    } else {
-                        HighlightTag::SelfKeyword.into()
+                        h |= HighlightModifier::Mutable
                     }
+
+                    if let Some(hir::PathResolution::Local(local)) = self_path {
+                        if is_consumed_lvalue(element, &local, db) {
+                            h |= HighlightModifier::Consuming;
+                        }
+                    }
+
+                    h
                 }
                 T![ref] => element
                     .parent()
index d0df2e0ec04b8ed21c5870147b16f59d4f2189c2..cde42024c03defa31f4a041c34f6d01656949a7f 100644 (file)
@@ -61,8 +61,8 @@ pre                 { color: #DCDCCC; background: #3F3F3F; font-size: 22px; padd
 <span class="punctuation">}</span>
 
 <span class="keyword">impl</span> <span class="struct">Foo</span> <span class="punctuation">{</span>
-    <span class="keyword">fn</span> <span class="function declaration">baz</span><span class="punctuation">(</span><span class="keyword">mut</span> <span class="self_keyword mutable">self</span><span class="punctuation">)</span> <span class="operator">-&gt;</span> <span class="builtin_type">i32</span> <span class="punctuation">{</span>
-        <span class="self_keyword">self</span><span class="punctuation">.</span><span class="field">x</span>
+    <span class="keyword">fn</span> <span class="function declaration">baz</span><span class="punctuation">(</span><span class="keyword">mut</span> <span class="self_keyword mutable">self</span><span class="punctuation">,</span> <span class="value_param declaration">f</span><span class="punctuation">:</span> <span class="struct">Foo</span><span class="punctuation">)</span> <span class="operator">-&gt;</span> <span class="builtin_type">i32</span> <span class="punctuation">{</span>
+        <span class="value_param">f</span><span class="punctuation">.</span><span class="function consuming">baz</span><span class="punctuation">(</span><span class="self_keyword consuming">self</span><span class="punctuation">)</span>
     <span class="punctuation">}</span>
 
     <span class="keyword">fn</span> <span class="function declaration">qux</span><span class="punctuation">(</span><span class="operator">&</span><span class="keyword">mut</span> <span class="self_keyword mutable">self</span><span class="punctuation">)</span> <span class="punctuation">{</span>
@@ -80,8 +80,8 @@ pre                 { color: #DCDCCC; background: #3F3F3F; font-size: 22px; padd
 <span class="punctuation">}</span>
 
 <span class="keyword">impl</span> <span class="struct">FooCopy</span> <span class="punctuation">{</span>
-    <span class="keyword">fn</span> <span class="function declaration">baz</span><span class="punctuation">(</span><span class="self_keyword">self</span><span class="punctuation">)</span> <span class="operator">-&gt;</span> <span class="builtin_type">u32</span> <span class="punctuation">{</span>
-        <span class="self_keyword">self</span><span class="punctuation">.</span><span class="field">x</span>
+    <span class="keyword">fn</span> <span class="function declaration">baz</span><span class="punctuation">(</span><span class="self_keyword">self</span><span class="punctuation">,</span> <span class="value_param declaration">f</span><span class="punctuation">:</span> <span class="struct">FooCopy</span><span class="punctuation">)</span> <span class="operator">-&gt;</span> <span class="builtin_type">u32</span> <span class="punctuation">{</span>
+        <span class="value_param">f</span><span class="punctuation">.</span><span class="function">baz</span><span class="punctuation">(</span><span class="self_keyword">self</span><span class="punctuation">)</span>
     <span class="punctuation">}</span>
 
     <span class="keyword">fn</span> <span class="function declaration">qux</span><span class="punctuation">(</span><span class="operator">&</span><span class="keyword">mut</span> <span class="self_keyword mutable">self</span><span class="punctuation">)</span> <span class="punctuation">{</span>
@@ -144,14 +144,15 @@ pre                 { color: #DCDCCC; background: #3F3F3F; font-size: 22px; padd
     <span class="variable">y</span><span class="punctuation">;</span>
 
     <span class="keyword">let</span> <span class="keyword">mut</span> <span class="variable declaration mutable">foo</span> <span class="operator">=</span> <span class="struct">Foo</span> <span class="punctuation">{</span> <span class="field">x</span><span class="punctuation">,</span> <span class="field">y</span><span class="punctuation">:</span> <span class="variable mutable">x</span> <span class="punctuation">}</span><span class="punctuation">;</span>
+    <span class="keyword">let</span> <span class="variable declaration">foo2</span> <span class="operator">=</span> <span class="variable mutable">foo</span><span class="punctuation">.</span><span class="unresolved_reference">clone</span><span class="punctuation">(</span><span class="punctuation">)</span><span class="punctuation">;</span>
     <span class="variable mutable">foo</span><span class="punctuation">.</span><span class="function">quop</span><span class="punctuation">(</span><span class="punctuation">)</span><span class="punctuation">;</span>
     <span class="variable mutable">foo</span><span class="punctuation">.</span><span class="function mutable">qux</span><span class="punctuation">(</span><span class="punctuation">)</span><span class="punctuation">;</span>
-    <span class="variable mutable">foo</span><span class="punctuation">.</span><span class="function consuming">baz</span><span class="punctuation">(</span><span class="punctuation">)</span><span class="punctuation">;</span>
+    <span class="variable mutable">foo</span><span class="punctuation">.</span><span class="function consuming">baz</span><span class="punctuation">(</span><span class="variable consuming">foo2</span><span class="punctuation">)</span><span class="punctuation">;</span>
 
     <span class="keyword">let</span> <span class="keyword">mut</span> <span class="variable declaration mutable">copy</span> <span class="operator">=</span> <span class="struct">FooCopy</span> <span class="punctuation">{</span> <span class="field">x</span> <span class="punctuation">}</span><span class="punctuation">;</span>
     <span class="variable mutable">copy</span><span class="punctuation">.</span><span class="function">quop</span><span class="punctuation">(</span><span class="punctuation">)</span><span class="punctuation">;</span>
     <span class="variable mutable">copy</span><span class="punctuation">.</span><span class="function mutable">qux</span><span class="punctuation">(</span><span class="punctuation">)</span><span class="punctuation">;</span>
-    <span class="variable mutable">copy</span><span class="punctuation">.</span><span class="function">baz</span><span class="punctuation">(</span><span class="punctuation">)</span><span class="punctuation">;</span>
+    <span class="variable mutable">copy</span><span class="punctuation">.</span><span class="function">baz</span><span class="punctuation">(</span><span class="variable mutable">copy</span><span class="punctuation">)</span><span class="punctuation">;</span>
 <span class="punctuation">}</span>
 
 <span class="keyword">enum</span> <span class="enum declaration">Option</span><span class="punctuation">&lt;</span><span class="type_param declaration">T</span><span class="punctuation">&gt;</span> <span class="punctuation">{</span>
index 6f72a29bdc495604cb066e415dcbad4866651d43..57d4e1252d28ca99a331024340ec099cf9e7fb36 100644 (file)
@@ -35,8 +35,8 @@ fn bar(&self) -> i32 {
 }
 
 impl Foo {
-    fn baz(mut self) -> i32 {
-        self.x
+    fn baz(mut self, f: Foo) -> i32 {
+        f.baz(self)
     }
 
     fn qux(&mut self) {
@@ -54,8 +54,8 @@ struct FooCopy {
 }
 
 impl FooCopy {
-    fn baz(self) -> u32 {
-        self.x
+    fn baz(self, f: FooCopy) -> u32 {
+        f.baz(self)
     }
 
     fn qux(&mut self) {
@@ -118,14 +118,15 @@ fn main() {
     y;
 
     let mut foo = Foo { x, y: x };
+    let foo2 = foo.clone();
     foo.quop();
     foo.qux();
-    foo.baz();
+    foo.baz(foo2);
 
     let mut copy = FooCopy { x };
     copy.quop();
     copy.qux();
-    copy.baz();
+    copy.baz(copy);
 }
 
 enum Option<T> {
index 2d91939ce0c62d5cc4c912299ba68425f06167e1..288c39e49478b2fd23b8420e91c6f8fd55487816 100644 (file)
@@ -33,7 +33,7 @@ pub enum ProjectWorkspace {
     /// Project workspace was discovered by running `cargo metadata` and `rustc --print sysroot`.
     Cargo { cargo: CargoWorkspace, sysroot: Sysroot },
     /// Project workspace was manually specified using a `rust-project.json` file.
-    Json { project: ProjectJson },
+    Json { project: ProjectJson, sysroot: Option<Sysroot> },
 }
 
 impl fmt::Debug for ProjectWorkspace {
@@ -44,10 +44,10 @@ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
                 .field("n_packages", &cargo.packages().len())
                 .field("n_sysroot_crates", &sysroot.crates().len())
                 .finish(),
-            ProjectWorkspace::Json { project } => {
+            ProjectWorkspace::Json { project, sysroot } => {
                 let mut debug_struct = f.debug_struct("Json");
                 debug_struct.field("n_crates", &project.n_crates());
-                if let Some(sysroot) = &project.sysroot {
+                if let Some(sysroot) = sysroot {
                     debug_struct.field("n_sysroot_crates", &sysroot.crates().len());
                 }
                 debug_struct.finish()
@@ -169,7 +169,11 @@ pub fn load(
                 })?;
                 let project_location = project_json.parent().unwrap().to_path_buf();
                 let project = ProjectJson::new(&project_location, data);
-                ProjectWorkspace::Json { project }
+                let sysroot = match &project.sysroot_src {
+                    Some(path) => Some(Sysroot::load(path)?),
+                    None => None,
+                };
+                ProjectWorkspace::Json { project, sysroot }
             }
             ProjectManifest::CargoToml(cargo_toml) => {
                 let cargo_version = utf8_stdout({
@@ -203,12 +207,21 @@ pub fn load(
         Ok(res)
     }
 
+    pub fn load_inline(project_json: ProjectJson) -> Result<ProjectWorkspace> {
+        let sysroot = match &project_json.sysroot_src {
+            Some(path) => Some(Sysroot::load(path)?),
+            None => None,
+        };
+
+        Ok(ProjectWorkspace::Json { project: project_json, sysroot })
+    }
+
     /// Returns the roots for the current `ProjectWorkspace`
     /// The return type contains the path and whether or not
     /// the root is a member of the current workspace
     pub fn to_roots(&self) -> Vec<PackageRoot> {
         match self {
-            ProjectWorkspace::Json { project } => project
+            ProjectWorkspace::Json { project, sysroot } => project
                 .crates()
                 .map(|(_, krate)| PackageRoot {
                     is_member: krate.is_workspace_member,
@@ -217,7 +230,7 @@ pub fn to_roots(&self) -> Vec<PackageRoot> {
                 })
                 .collect::<FxHashSet<_>>()
                 .into_iter()
-                .chain(project.sysroot.as_ref().into_iter().flat_map(|sysroot| {
+                .chain(sysroot.as_ref().into_iter().flat_map(|sysroot| {
                     sysroot.crates().map(move |krate| PackageRoot {
                         is_member: false,
                         include: vec![sysroot[krate].root_dir().to_path_buf()],
@@ -255,7 +268,7 @@ pub fn to_roots(&self) -> Vec<PackageRoot> {
 
     pub fn proc_macro_dylib_paths(&self) -> Vec<AbsPathBuf> {
         match self {
-            ProjectWorkspace::Json { project } => project
+            ProjectWorkspace::Json { project, sysroot: _ } => project
                 .crates()
                 .filter_map(|(_, krate)| krate.proc_macro_dylib_path.as_ref())
                 .cloned()
@@ -285,9 +298,8 @@ pub fn to_crate_graph(
     ) -> CrateGraph {
         let mut crate_graph = CrateGraph::default();
         match self {
-            ProjectWorkspace::Json { project } => {
-                let sysroot_dps = project
-                    .sysroot
+            ProjectWorkspace::Json { project, sysroot } => {
+                let sysroot_dps = sysroot
                     .as_ref()
                     .map(|sysroot| sysroot_to_crate_graph(&mut crate_graph, sysroot, target, load));
 
index 5a0fe749a597846bfae09ee2d5a322866ce7423c..979e9005839069e386ea46590b24f8f0fd81b7e0 100644 (file)
@@ -7,12 +7,12 @@
 use rustc_hash::FxHashMap;
 use serde::{de, Deserialize};
 
-use crate::{cfg_flag::CfgFlag, Sysroot};
+use crate::cfg_flag::CfgFlag;
 
 /// Roots and crates that compose this Rust project.
 #[derive(Clone, Debug, Eq, PartialEq)]
 pub struct ProjectJson {
-    pub(crate) sysroot: Option<Sysroot>,
+    pub(crate) sysroot_src: Option<AbsPathBuf>,
     crates: Vec<Crate>,
 }
 
@@ -35,7 +35,7 @@ pub struct Crate {
 impl ProjectJson {
     pub fn new(base: &AbsPath, data: ProjectJsonData) -> ProjectJson {
         ProjectJson {
-            sysroot: data.sysroot_src.map(|it| base.join(it)).map(|it| Sysroot::load(&it)),
+            sysroot_src: data.sysroot_src.map(|it| base.join(it)),
             crates: data
                 .crates
                 .into_iter()
index 74c0eda9a52230761da23593096c9a3a5dc383e2..871808d89e5d1e536ec3e2dc6953d5e721fdff64 100644 (file)
@@ -51,11 +51,11 @@ pub fn crates<'a>(&'a self) -> impl Iterator<Item = SysrootCrate> + ExactSizeIte
     pub fn discover(cargo_toml: &AbsPath) -> Result<Sysroot> {
         let current_dir = cargo_toml.parent().unwrap();
         let sysroot_src_dir = discover_sysroot_src_dir(current_dir)?;
-        let res = Sysroot::load(&sysroot_src_dir);
+        let res = Sysroot::load(&sysroot_src_dir)?;
         Ok(res)
     }
 
-    pub fn load(sysroot_src_dir: &AbsPath) -> Sysroot {
+    pub fn load(sysroot_src_dir: &AbsPath) -> Result<Sysroot> {
         let mut sysroot = Sysroot { crates: Arena::default() };
 
         for name in SYSROOT_CRATES.trim().lines() {
@@ -89,7 +89,14 @@ pub fn load(sysroot_src_dir: &AbsPath) -> Sysroot {
             }
         }
 
-        sysroot
+        if sysroot.by_name("core").is_none() {
+            anyhow::bail!(
+                "could not find libcore in sysroot path `{}`",
+                sysroot_src_dir.as_ref().display()
+            );
+        }
+
+        Ok(sysroot)
     }
 
     fn by_name(&self, name: &str) -> Option<SysrootCrate> {
index 89dae7d5a69d0aafba1a097613c8cd6de6079936..00e8da8a70c801bdf7a0fbcfb01dac5488391046 100644 (file)
         },
         fixes: [],
     },
+    MappedRustDiagnostic {
+        url: "file:///test/crates/hir_def/src/path.rs",
+        diagnostic: Diagnostic {
+            range: Range {
+                start: Position {
+                    line: 264,
+                    character: 8,
+                },
+                end: Position {
+                    line: 264,
+                    character: 76,
+                },
+            },
+            severity: Some(
+                Error,
+            ),
+            code: None,
+            source: Some(
+                "rustc",
+            ),
+            message: "Please register your known path in the path module",
+            related_information: Some(
+                [
+                    DiagnosticRelatedInformation {
+                        location: Location {
+                            uri: "file:///test/crates/hir_def/src/data.rs",
+                            range: Range {
+                                start: Position {
+                                    line: 79,
+                                    character: 15,
+                                },
+                                end: Position {
+                                    line: 79,
+                                    character: 41,
+                                },
+                            },
+                        },
+                        message: "Exact error occured here",
+                    },
+                ],
+            ),
+            tags: None,
+        },
+        fixes: [],
+    },
 ]
index f69a949f213dd618a9f7629802bd2fced19e8143..33606edda4b0c03a75290ebe470cc9c27b32b7cd 100644 (file)
@@ -225,12 +225,43 @@ pub(crate) fn map_rust_diagnostic_to_lsp(
 
             // If error occurs from macro expansion, add related info pointing to
             // where the error originated
-            if !is_from_macro(&primary_span.file_name) && primary_span.expansion.is_some() {
-                related_information.push(lsp_types::DiagnosticRelatedInformation {
-                    location: location_naive(workspace_root, &primary_span),
-                    message: "Error originated from macro here".to_string(),
-                });
-            }
+            // Also, we would generate an additional diagnostic, so that exact place of macro
+            // will be highlighted in the error origin place.
+            let additional_diagnostic =
+                if !is_from_macro(&primary_span.file_name) && primary_span.expansion.is_some() {
+                    let in_macro_location = location_naive(workspace_root, &primary_span);
+
+                    // Add related information for the main disagnostic.
+                    related_information.push(lsp_types::DiagnosticRelatedInformation {
+                        location: in_macro_location.clone(),
+                        message: "Error originated from macro here".to_string(),
+                    });
+
+                    // For the additional in-macro diagnostic we add the inverse message pointing to the error location in code.
+                    let information_for_additional_diagnostic =
+                        vec![lsp_types::DiagnosticRelatedInformation {
+                            location: location.clone(),
+                            message: "Exact error occured here".to_string(),
+                        }];
+
+                    let diagnostic = lsp_types::Diagnostic {
+                        range: in_macro_location.range,
+                        severity,
+                        code: code.clone().map(lsp_types::NumberOrString::String),
+                        source: Some(source.clone()),
+                        message: message.clone(),
+                        related_information: Some(information_for_additional_diagnostic),
+                        tags: if tags.is_empty() { None } else { Some(tags.clone()) },
+                    };
+
+                    Some(MappedRustDiagnostic {
+                        url: in_macro_location.uri,
+                        diagnostic,
+                        fixes: fixes.clone(),
+                    })
+                } else {
+                    None
+                };
 
             let diagnostic = lsp_types::Diagnostic {
                 range: location.range,
@@ -246,8 +277,14 @@ pub(crate) fn map_rust_diagnostic_to_lsp(
                 tags: if tags.is_empty() { None } else { Some(tags.clone()) },
             };
 
-            MappedRustDiagnostic { url: location.uri, diagnostic, fixes: fixes.clone() }
+            let main_diagnostic =
+                MappedRustDiagnostic { url: location.uri, diagnostic, fixes: fixes.clone() };
+            match additional_diagnostic {
+                None => vec![main_diagnostic],
+                Some(additional_diagnostic) => vec![main_diagnostic, additional_diagnostic],
+            }
         })
+        .flatten()
         .collect()
 }
 
index 20019b9445867e14b5d49740def38f85fa200ca1..bab6f8a71bbbc3ae6c24512f60782c9d0a4ce6be 100644 (file)
@@ -109,7 +109,7 @@ pub(crate) fn fetch_workspaces(&mut self) {
                             )
                         }
                         LinkedProject::InlineJsonProject(it) => {
-                            Ok(project_model::ProjectWorkspace::Json { project: it.clone() })
+                            project_model::ProjectWorkspace::load_inline(it.clone())
                         }
                     })
                     .collect::<Vec<_>>();
index e9196fcd2f9441772a003f0db074c8dcafee736e..4aa2d6526be782bc26262922eb766f8669a08fd4 100644 (file)
@@ -23,13 +23,22 @@ pub fn resolve_path(&self, anchor: FileId, path: &str) -> Option<FileId> {
         let mut base = self.paths[&anchor].clone();
         base.pop();
         let path = base.join(path)?;
-        let res = self.files.get(&path).copied();
-        res
+        self.files.get(&path).copied()
+    }
+
+    pub fn file_for_path(&self, path: &VfsPath) -> Option<&FileId> {
+        self.files.get(path)
     }
+
+    pub fn path_for_file(&self, file: &FileId) -> Option<&VfsPath> {
+        self.paths.get(file)
+    }
+
     pub fn insert(&mut self, file_id: FileId, path: VfsPath) {
         self.files.insert(path.clone(), file_id);
         self.paths.insert(file_id, path);
     }
+
     pub fn iter(&self) -> impl Iterator<Item = FileId> + '_ {
         self.paths.keys().copied()
     }
index 944a702df0fcd073b9df4356a13d0bbadd77afeb..022a0be1e39240fd9144d809d8afc81f7179c5b9 100644 (file)
@@ -48,6 +48,24 @@ pub fn starts_with(&self, other: &VfsPath) -> bool {
             (VfsPathRepr::VirtualPath(_), _) => false,
         }
     }
+    pub fn parent(&self) -> Option<VfsPath> {
+        let mut parent = self.clone();
+        if parent.pop() {
+            Some(parent)
+        } else {
+            None
+        }
+    }
+
+    pub fn name_and_extension(&self) -> Option<(&str, Option<&str>)> {
+        match &self.0 {
+            VfsPathRepr::PathBuf(p) => Some((
+                p.file_stem()?.to_str()?,
+                p.extension().and_then(|extension| extension.to_str()),
+            )),
+            VfsPathRepr::VirtualPath(p) => p.name_and_extension(),
+        }
+    }
 
     // Don't make this `pub`
     pub(crate) fn encode(&self, buf: &mut Vec<u8>) {
@@ -268,4 +286,60 @@ fn join(&self, mut path: &str) -> Option<VirtualPath> {
         res.0 = format!("{}/{}", res.0, path);
         Some(res)
     }
+
+    pub fn name_and_extension(&self) -> Option<(&str, Option<&str>)> {
+        let file_path = if self.0.ends_with('/') { &self.0[..&self.0.len() - 1] } else { &self.0 };
+        let file_name = match file_path.rfind('/') {
+            Some(position) => &file_path[position + 1..],
+            None => file_path,
+        };
+
+        if file_name.is_empty() {
+            None
+        } else {
+            let mut file_stem_and_extension = file_name.rsplitn(2, '.');
+            let extension = file_stem_and_extension.next();
+            let file_stem = file_stem_and_extension.next();
+
+            match (file_stem, extension) {
+                (None, None) => None,
+                (None, Some(_)) | (Some(""), Some(_)) => Some((file_name, None)),
+                (Some(file_stem), extension) => Some((file_stem, extension)),
+            }
+        }
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+
+    #[test]
+    fn virtual_path_extensions() {
+        assert_eq!(VirtualPath("/".to_string()).name_and_extension(), None);
+        assert_eq!(
+            VirtualPath("/directory".to_string()).name_and_extension(),
+            Some(("directory", None))
+        );
+        assert_eq!(
+            VirtualPath("/directory/".to_string()).name_and_extension(),
+            Some(("directory", None))
+        );
+        assert_eq!(
+            VirtualPath("/directory/file".to_string()).name_and_extension(),
+            Some(("file", None))
+        );
+        assert_eq!(
+            VirtualPath("/directory/.file".to_string()).name_and_extension(),
+            Some((".file", None))
+        );
+        assert_eq!(
+            VirtualPath("/directory/.file.rs".to_string()).name_and_extension(),
+            Some((".file", Some("rs")))
+        );
+        assert_eq!(
+            VirtualPath("/directory/file.rs".to_string()).name_and_extension(),
+            Some(("file", Some("rs")))
+        );
+    }
 }