Repository: rust-analyzer/rust-analyzer
Pull request #8955 (2021-05-23 21:38:23)

feature: Support standalone Rust files
![standalone](https://user-images.githubusercontent.com/2690773/119277037-0b579380-bc26-11eb-8d77-20d46ab4916a.gif)

Closes https://github.com/rust-analyzer/rust-analyzer/issues/6388

Caveats: 

* I've decided to support multiple detached files in the code (anticipating the scratch files), but I found no way to open multiple files in VSCode at once: running `code *.rs` makes the plugin to register in the `vscode.workspace.textDocuments` only the first file, while code actually displays all files later.
Apparently what happens is the same as when you have VSCode open at some workplace already and then run `code some_other_file.rs`: it gets opened in the same workspace of the same VSCode with no server to support it.
If there's a way to override it, I'd appreciate the pointer.

* No way to toggle inlay hints, since the setting is updated for the workspace (which does not exist for a single file opened)
> [2021-05-24 00:22:49.100] [exthost] [error] Error: Unable to write to Workspace Settings because no workspace is opened. Please open a workspace first and try again.

* No runners/lens to run or check the code are implemented for this mode. 
In theory, we can detect `rustc`, run it on a file and run the resulting binary, but not sure if worth doing it at this stage.

Otherwise imports, hints, completion and other features work.


Patch:
diff --git a/crates/hir_def/src/item_tree.rs b/crates/hir_def/src/item_tree.rs
index 4a5f44027e..528270d49d 100644
--- a/crates/hir_def/src/item_tree.rs
+++ b/crates/hir_def/src/item_tree.rs
@@ -1,34 +1,4 @@
 //! A simplified AST that only contains items.
-//!
-//! This is the primary IR used throughout `hir_def`. It is the input to the name resolution
-//! algorithm, as well as to the queries defined in `adt.rs`, `data.rs`, and most things in
-//! `attr.rs`.
-//!
-//! `ItemTree`s are built per `HirFileId`, from the syntax tree of the parsed file. This means that
-//! they are crate-independent: they don't know which `#[cfg]`s are active or which module they
-//! belong to, since those concepts don't exist at this level (a single `ItemTree` might be part of
-//! multiple crates, or might be included into the same crate twice via `#[path]`).
-//!
-//! One important purpose of this layer is to provide an "invalidation barrier" for incremental
-//! computations: when typing inside an item body, the `ItemTree` of the modified file is typically
-//! unaffected, so we don't have to recompute name resolution results or item data (see `data.rs`).
-//!
-//! The `ItemTree` for the currently open file can be displayed by using the VS Code command
-//! "Rust Analyzer: Debug ItemTree".
-//!
-//! Compared to rustc's architecture, `ItemTree` has properties from both rustc's AST and HIR: many
-//! syntax-level Rust features are already desugared to simpler forms in the `ItemTree`, but name
-//! resolution has not yet been performed. `ItemTree`s are per-file, while rustc's AST and HIR are
-//! per-crate, because we are interested in incrementally computing it.
-//!
-//! The representation of items in the `ItemTree` should generally mirror the surface syntax: it is
-//! usually a bad idea to desugar a syntax-level construct to something that is structurally
-//! different here. Name resolution needs to be able to process attributes and expand macros
-//! (including attribute macros), and having a 1-to-1 mapping between syntax and the `ItemTree`
-//! avoids introducing subtle bugs.
-//!
-//! In general, any item in the `ItemTree` stores its `AstId`, which allows mapping it back to its
-//! surface syntax.
 
 mod lower;
 mod pretty;
@@ -530,8 +500,8 @@ pub struct Import {
     pub alias: Option<ImportAlias>,
     pub visibility: RawVisibilityId,
     pub is_glob: bool,
-    /// AST ID of the `use` item this import was derived from. Note that many `Import`s can map to
-    /// the same `use` item.
+    /// AST ID of the `use` or `extern crate` item this import was derived from. Note that many
+    /// `Import`s can map to the same `use` item.
     pub ast_id: FileAstId<ast::Use>,
     /// Index of this `Import` when the containing `Use` is visited via `ModPath::expand_use_item`.
     ///
diff --git a/crates/ide_assists/src/handlers/generate_getter.rs b/crates/ide_assists/src/handlers/generate_getter.rs
index 09971226e1..df7d1bb954 100644
--- a/crates/ide_assists/src/handlers/generate_getter.rs
+++ b/crates/ide_assists/src/handlers/generate_getter.rs
@@ -23,46 +23,12 @@ use crate::{
 //
 // impl Person {
 //     /// Get a reference to the person's name.
-//     fn $0name(&self) -> &str {
-//         self.name.as_str()
+//     fn name(&self) -> &String {
+//         &self.name
 //     }
 // }
 // ```
 pub(crate) fn generate_getter(acc: &mut Assists, ctx: &AssistContext) -> Option<()> {
-    generate_getter_impl(acc, ctx, false)
-}
-
-// Assist: generate_getter_mut
-//
-// Generate a mut getter method.
-//
-// ```
-// struct Person {
-//     nam$0e: String,
-// }
-// ```
-// ->
-// ```
-// struct Person {
-//     name: String,
-// }
-//
-// impl Person {
-//     /// Get a mutable reference to the person's name.
-//     fn $0name_mut(&mut self) -> &mut String {
-//         &mut self.name
-//     }
-// }
-// ```
-pub(crate) fn generate_getter_mut(acc: &mut Assists, ctx: &AssistContext) -> Option<()> {
-    generate_getter_impl(acc, ctx, true)
-}
-
-pub(crate) fn generate_getter_impl(
-    acc: &mut Assists,
-    ctx: &AssistContext,
-    mutable: bool,
-) -> Option<()> {
     let strukt = ctx.find_node_at_offset::<ast::Struct>()?;
     let field = ctx.find_node_at_offset::<ast::RecordField>()?;
 
@@ -71,52 +37,39 @@ pub(crate) fn generate_getter_impl(
     let field_ty = field.ty()?;
 
     // Return early if we've found an existing fn
-    let mut fn_name = to_lower_snake_case(&field_name.to_string());
-    if mutable {
-        format_to!(fn_name, "_mut");
-    }
+    let fn_name = to_lower_snake_case(&field_name.to_string());
     let impl_def = find_struct_impl(&ctx, &ast::Adt::Struct(strukt.clone()), fn_name.as_str())?;
 
-    let (id, label) = if mutable {
-        ("generate_getter_mut", "Generate a mut getter method")
-    } else {
-        ("generate_getter", "Generate a getter method")
-    };
     let target = field.syntax().text_range();
     acc.add_group(
         &GroupLabel("Generate getter/setter".to_owned()),
-        AssistId(id, AssistKind::Generate),
-        label,
+        AssistId("generate_getter", AssistKind::Generate),
+        "Generate a getter method",
         target,
         |builder| {
             let mut buf = String::with_capacity(512);
 
+            let fn_name_spaced = fn_name.replace('_', " ");
+            let strukt_name_spaced =
+                to_lower_snake_case(&strukt_name.to_string()).replace('_', " ");
+
             if impl_def.is_some() {
                 buf.push('\n');
             }
 
             let vis = strukt.visibility().map_or(String::new(), |v| format!("{} ", v));
-            let (ty, body) = if mutable {
-                (format!("&mut {}", field_ty), format!("&mut self.{}", field_name))
-            } else {
-                useless_type_special_case(&field_name.to_string(), &field_ty)
-                    .unwrap_or_else(|| (format!("&{}", field_ty), format!("&self.{}", field_name)))
-            };
-
             format_to!(
                 buf,
-                "    /// Get a {}reference to the {}'s {}.
-    {}fn {}(&{}self) -> {} {{
-        {}
+                "    /// Get a reference to the {}'s {}.
+    {}fn {}(&self) -> &{} {{
+        &self.{}
     }}",
-                mutable.then(|| "mutable ").unwrap_or_default(),
-                to_lower_snake_case(&strukt_name.to_string()).replace('_', " "),
-                fn_name.trim_end_matches("_mut").replace('_', " "),
+                strukt_name_spaced,
+                fn_name_spaced,
                 vis,
                 fn_name,
-                mutable.then(|| "mut ").unwrap_or_default(),
-                ty,
-                body,
+                field_ty,
+                fn_name,
             );
 
             let start_offset = impl_def
@@ -126,120 +79,56 @@ pub(crate) fn generate_getter_impl(
                     strukt.syntax().text_range().end()
                 });
 
-            match ctx.config.snippet_cap {
-                Some(cap) => {
-                    builder.insert_snippet(cap, start_offset, buf.replacen("fn ", "fn $0", 1))
-                }
-                None => builder.insert(start_offset, buf),
-            }
+            builder.insert(start_offset, buf);
         },
     )
 }
 
-fn useless_type_special_case(field_name: &str, field_ty: &ast::Type) -> Option<(String, String)> {
-    if field_ty.to_string() == "String" {
-        cov_mark::hit!(useless_type_special_case);
-        return Some(("&str".to_string(), format!("self.{}.as_str()", field_name)));
-    }
-    if let Some(arg) = ty_ctor(field_ty, "Vec") {
-        return Some((format!("&[{}]", arg), format!("self.{}.as_slice()", field_name)));
-    }
-    if let Some(arg) = ty_ctor(field_ty, "Box") {
-        return Some((format!("&{}", arg), format!("self.{}.as_ref()", field_name)));
-    }
-    if let Some(arg) = ty_ctor(field_ty, "Option") {
-        return Some((format!("Option<&{}>", arg), format!("self.{}.as_ref()", field_name)));
-    }
-    None
-}
-
-// FIXME: This should rely on semantic info.
-fn ty_ctor(ty: &ast::Type, ctor: &str) -> Option<String> {
-    let res = ty.to_string().strip_prefix(ctor)?.strip_prefix('<')?.strip_suffix('>')?.to_string();
-    Some(res)
-}
-
 #[cfg(test)]
 mod tests {
     use crate::tests::{check_assist, check_assist_not_applicable};
 
     use super::*;
 
+    fn check_not_applicable(ra_fixture: &str) {
+        check_assist_not_applicable(generate_getter, ra_fixture)
+    }
+
     #[test]
     fn test_generate_getter_from_field() {
         check_assist(
             generate_getter,
             r#"
-struct Context {
-    dat$0a: Data,
-}
-"#,
+struct Context<T: Clone> {
+    dat$0a: T,
+}"#,
             r#"
-struct Context {
-    data: Data,
+struct Context<T: Clone> {
+    data: T,
 }
 
-impl Context {
+impl<T: Clone> Context<T> {
     /// Get a reference to the context's data.
-    fn $0data(&self) -> &Data {
+    fn data(&self) -> &T {
         &self.data
     }
-}
-"#,
-        );
-
-        check_assist(
-            generate_getter_mut,
-            r#"
-struct Context {
-    dat$0a: Data,
-}
-"#,
-            r#"
-struct Context {
-    data: Data,
-}
-
-impl Context {
-    /// Get a mutable reference to the context's data.
-    fn $0data_mut(&mut self) -> &mut Data {
-        &mut self.data
-    }
-}
-"#,
+}"#,
         );
     }
 
     #[test]
     fn test_generate_getter_already_implemented() {
-        check_assist_not_applicable(
-            generate_getter,
+        check_not_applicable(
             r#"
-struct Context {
-    dat$0a: Data,
+struct Context<T: Clone> {
+    dat$0a: T,
 }
 
-impl Context {
-    fn data(&self) -> &Data {
+impl<T: Clone> Context<T> {
+    fn data(&self) -> &T {
         &self.data
     }
-}
-"#,
-        );
-
-        check_assist_not_applicable(
-            generate_getter_mut,
-            r#"
-struct Context {
-    dat$0a: Data,
-}
-
-impl Context {
-    fn data_mut(&mut self) -> &mut Data {
-        &mut self.data
-    }
-}
-"#,
+}"#,
         );
     }
 
@@ -248,22 +137,20 @@ impl Context {
         check_assist(
             generate_getter,
             r#"
-pub(crate) struct Context {
-    dat$0a: Data,
-}
-"#,
+pub(crate) struct Context<T: Clone> {
+    dat$0a: T,
+}"#,
             r#"
-pub(crate) struct Context {
-    data: Data,
+pub(crate) struct Context<T: Clone> {
+    data: T,
 }
 
-impl Context {
+impl<T: Clone> Context<T> {
     /// Get a reference to the context's data.
-    pub(crate) fn $0data(&self) -> &Data {
+    pub(crate) fn data(&self) -> &T {
         &self.data
     }
-}
-"#,
+}"#,
         );
     }
 
@@ -272,105 +159,34 @@ impl Context {
         check_assist(
             generate_getter,
             r#"
-struct Context {
-    data: Data,
+struct Context<T: Clone> {
+    data: T,
     cou$0nt: usize,
 }
 
-impl Context {
+impl<T: Clone> Context<T> {
     /// Get a reference to the context's data.
-    fn data(&self) -> &Data {
+    fn data(&self) -> &T {
         &self.data
     }
-}
-"#,
+}"#,
             r#"
-struct Context {
-    data: Data,
+struct Context<T: Clone> {
+    data: T,
     count: usize,
 }
 
-impl Context {
+impl<T: Clone> Context<T> {
     /// Get a reference to the context's data.
-    fn data(&self) -> &Data {
+    fn data(&self) -> &T {
         &self.data
     }
 
     /// Get a reference to the context's count.
-    fn $0count(&self) -> &usize {
+    fn count(&self) -> &usize {
         &self.count
     }
-}
-"#,
-        );
-    }
-
-    #[test]
-    fn test_special_cases() {
-        cov_mark::check!(useless_type_special_case);
-        check_assist(
-            generate_getter,
-            r#"
-struct S { foo: $0String }
-"#,
-            r#"
-struct S { foo: String }
-
-impl S {
-    /// Get a reference to the s's foo.
-    fn $0foo(&self) -> &str {
-        self.foo.as_str()
-    }
-}
-"#,
-        );
-        check_assist(
-            generate_getter,
-            r#"
-struct S { foo: $0Box<Sweets> }
-"#,
-            r#"
-struct S { foo: Box<Sweets> }
-
-impl S {
-    /// Get a reference to the s's foo.
-    fn $0foo(&self) -> &Sweets {
-        self.foo.as_ref()
-    }
-}
-"#,
-        );
-        check_assist(
-            generate_getter,
-            r#"
-struct S { foo: $0Vec<()> }
-"#,
-            r#"
-struct S { foo: Vec<()> }
-
-impl S {
-    /// Get a reference to the s's foo.
-    fn $0foo(&self) -> &[()] {
-        self.foo.as_slice()
-    }
-}
-"#,
-        );
-        check_assist(
-            generate_getter,
-            r#"
-struct S { foo: $0Option<Failure> }
-"#,
-            r#"
-struct S { foo: Option<Failure> }
-
-impl S {
-    /// Get a reference to the s's foo.
-    fn $0foo(&self) -> Option<&Failure> {
-        self.foo.as_ref()
-    }
-}
-"#,
+}"#,
         );
     }
 }
diff --git a/crates/ide_assists/src/handlers/generate_getter_mut.rs b/crates/ide_assists/src/handlers/generate_getter_mut.rs
new file mode 100644
index 0000000000..821c2eed54
--- /dev/null
+++ b/crates/ide_assists/src/handlers/generate_getter_mut.rs
@@ -0,0 +1,195 @@
+use stdx::{format_to, to_lower_snake_case};
+use syntax::ast::{self, AstNode, NameOwner, VisibilityOwner};
+
+use crate::{
+    utils::{find_impl_block_end, find_struct_impl, generate_impl_text},
+    AssistContext, AssistId, AssistKind, Assists, GroupLabel,
+};
+
+// Assist: generate_getter_mut
+//
+// Generate a mut getter method.
+//
+// ```
+// struct Person {
+//     nam$0e: String,
+// }
+// ```
+// ->
+// ```
+// struct Person {
+//     name: String,
+// }
+//
+// impl Person {
+//     /// Get a mutable reference to the person's name.
+//     fn name_mut(&mut self) -> &mut String {
+//         &mut self.name
+//     }
+// }
+// ```
+pub(crate) fn generate_getter_mut(acc: &mut Assists, ctx: &AssistContext) -> Option<()> {
+    let strukt = ctx.find_node_at_offset::<ast::Struct>()?;
+    let field = ctx.find_node_at_offset::<ast::RecordField>()?;
+
+    let strukt_name = strukt.name()?;
+    let field_name = field.name()?;
+    let field_ty = field.ty()?;
+
+    // Return early if we've found an existing fn
+    let fn_name = to_lower_snake_case(&field_name.to_string());
+    let impl_def = find_struct_impl(
+        &ctx,
+        &ast::Adt::Struct(strukt.clone()),
+        format!("{}_mut", fn_name).as_str(),
+    )?;
+
+    let target = field.syntax().text_range();
+    acc.add_group(
+        &GroupLabel("Generate getter/setter".to_owned()),
+        AssistId("generate_getter_mut", AssistKind::Generate),
+        "Generate a mut getter method",
+        target,
+        |builder| {
+            let mut buf = String::with_capacity(512);
+            let fn_name_spaced = fn_name.replace('_', " ");
+            let strukt_name_spaced =
+                to_lower_snake_case(&strukt_name.to_string()).replace('_', " ");
+
+            if impl_def.is_some() {
+                buf.push('\n');
+            }
+
+            let vis = strukt.visibility().map_or(String::new(), |v| format!("{} ", v));
+            format_to!(
+                buf,
+                "    /// Get a mutable reference to the {}'s {}.
+    {}fn {}_mut(&mut self) -> &mut {} {{
+        &mut self.{}
+    }}",
+                strukt_name_spaced,
+                fn_name_spaced,
+                vis,
+                fn_name,
+                field_ty,
+                fn_name,
+            );
+
+            let start_offset = impl_def
+                .and_then(|impl_def| find_impl_block_end(impl_def, &mut buf))
+                .unwrap_or_else(|| {
+                    buf = generate_impl_text(&ast::Adt::Struct(strukt.clone()), &buf);
+                    strukt.syntax().text_range().end()
+                });
+
+            builder.insert(start_offset, buf);
+        },
+    )
+}
+
+#[cfg(test)]
+mod tests {
+    use crate::tests::{check_assist, check_assist_not_applicable};
+
+    use super::*;
+
+    fn check_not_applicable(ra_fixture: &str) {
+        check_assist_not_applicable(generate_getter_mut, ra_fixture)
+    }
+
+    #[test]
+    fn test_generate_getter_mut_from_field() {
+        check_assist(
+            generate_getter_mut,
+            r#"
+struct Context<T: Clone> {
+    dat$0a: T,
+}"#,
+            r#"
+struct Context<T: Clone> {
+    data: T,
+}
+
+impl<T: Clone> Context<T> {
+    /// Get a mutable reference to the context's data.
+    fn data_mut(&mut self) -> &mut T {
+        &mut self.data
+    }
+}"#,
+        );
+    }
+
+    #[test]
+    fn test_generate_getter_mut_already_implemented() {
+        check_not_applicable(
+            r#"
+struct Context<T: Clone> {
+    dat$0a: T,
+}
+
+impl<T: Clone> Context<T> {
+    fn data_mut(&mut self) -> &mut T {
+        &mut self.data
+    }
+}"#,
+        );
+    }
+
+    #[test]
+    fn test_generate_getter_mut_from_field_with_visibility_marker() {
+        check_assist(
+            generate_getter_mut,
+            r#"
+pub(crate) struct Context<T: Clone> {
+    dat$0a: T,
+}"#,
+            r#"
+pub(crate) struct Context<T: Clone> {
+    data: T,
+}
+
+impl<T: Clone> Context<T> {
+    /// Get a mutable reference to the context's data.
+    pub(crate) fn data_mut(&mut self) -> &mut T {
+        &mut self.data
+    }
+}"#,
+        );
+    }
+
+    #[test]
+    fn test_multiple_generate_getter_mut() {
+        check_assist(
+            generate_getter_mut,
+            r#"
+struct Context<T: Clone> {
+    data: T,
+    cou$0nt: usize,
+}
+
+impl<T: Clone> Context<T> {
+    /// Get a mutable reference to the context's data.
+    fn data_mut(&mut self) -> &mut T {
+        &mut self.data
+    }
+}"#,
+            r#"
+struct Context<T: Clone> {
+    data: T,
+    count: usize,
+}
+
+impl<T: Clone> Context<T> {
+    /// Get a mutable reference to the context's data.
+    fn data_mut(&mut self) -> &mut T {
+        &mut self.data
+    }
+
+    /// Get a mutable reference to the context's count.
+    fn count_mut(&mut self) -> &mut usize {
+        &mut self.count
+    }
+}"#,
+        );
+    }
+}
diff --git a/crates/ide_assists/src/lib.rs b/crates/ide_assists/src/lib.rs
index 16af72927b..4cd82f8c16 100644
--- a/crates/ide_assists/src/lib.rs
+++ b/crates/ide_assists/src/lib.rs
@@ -206,6 +206,7 @@ mod handlers {
     mod generate_enum_projection_method;
     mod generate_from_impl_for_enum;
     mod generate_function;
+    mod generate_getter_mut;
     mod generate_getter;
     mod generate_impl;
     mod generate_new;
@@ -275,8 +276,8 @@ mod handlers {
             generate_enum_projection_method::generate_enum_try_into_method,
             generate_from_impl_for_enum::generate_from_impl_for_enum,
             generate_function::generate_function,
+            generate_getter_mut::generate_getter_mut,
             generate_getter::generate_getter,
-            generate_getter::generate_getter_mut,
             generate_impl::generate_impl,
             generate_new::generate_new,
             generate_setter::generate_setter,
diff --git a/crates/ide_db/src/ty_filter.rs b/crates/ide_db/src/ty_filter.rs
index 766d8c6288..00678bf3e9 100644
--- a/crates/ide_db/src/ty_filter.rs
+++ b/crates/ide_db/src/ty_filter.rs
@@ -4,7 +4,7 @@
 
 use std::iter;
 
-use hir::Semantics;
+use hir::{Adt, Semantics, Type};
 use syntax::ast::{self, make};
 
 use crate::RootDatabase;
@@ -20,9 +20,9 @@ impl TryEnum {
     const ALL: [TryEnum; 2] = [TryEnum::Option, TryEnum::Result];
 
     /// Returns `Some(..)` if the provided type is an enum that implements `std::ops::Try`.
-    pub fn from_ty(sema: &Semantics<RootDatabase>, ty: &hir::Type) -> Option<TryEnum> {
+    pub fn from_ty(sema: &Semantics<RootDatabase>, ty: &Type) -> Option<TryEnum> {
         let enum_ = match ty.as_adt() {
-            Some(hir::Adt::Enum(it)) => it,
+            Some(Adt::Enum(it)) => it,
             _ => return None,
         };
         TryEnum::ALL.iter().find_map(|&var| {
diff --git a/crates/project_model/src/sysroot.rs b/crates/project_model/src/sysroot.rs
index 3b0ff506d4..4e39d6dd3c 100644
--- a/crates/project_model/src/sysroot.rs
+++ b/crates/project_model/src/sysroot.rs
@@ -50,7 +50,9 @@ impl Sysroot {
 
     pub fn discover(cargo_toml: &AbsPath) -> Result<Sysroot> {
         log::debug!("Discovering sysroot for {}", cargo_toml.display());
-        let current_dir = cargo_toml.parent().unwrap();
+        let current_dir = cargo_toml.parent().ok_or_else(|| {
+            format_err!("Failed to find the parent directory for {}", cargo_toml.display())
+        })?;
         let sysroot_dir = discover_sysroot_dir(current_dir)?;
         let sysroot_src_dir = discover_sysroot_src_dir(&sysroot_dir, current_dir)?;
         let res = Sysroot::load(&sysroot_src_dir)?;
diff --git a/crates/project_model/src/workspace.rs b/crates/project_model/src/workspace.rs
index 607e62ea59..84990075f7 100644
--- a/crates/project_model/src/workspace.rs
+++ b/crates/project_model/src/workspace.rs
@@ -4,7 +4,7 @@
 
 use std::{collections::VecDeque, fmt, fs, path::Path, process::Command};
 
-use anyhow::{Context, Result};
+use anyhow::{format_err, Context, Result};
 use base_db::{CrateDisplayName, CrateGraph, CrateId, CrateName, Edition, Env, FileId, ProcMacro};
 use cargo_workspace::DepKind;
 use cfg::CfgOptions;
@@ -49,6 +49,18 @@ pub enum ProjectWorkspace {
     },
     /// Project workspace was manually specified using a `rust-project.json` file.
     Json { project: ProjectJson, sysroot: Option<Sysroot>, rustc_cfg: Vec<CfgFlag> },
+
+    // FIXME: The primary limitation of this approach is that the set of detached files needs to be fixed at the beginning.
+    // That's not the end user experience we should strive for.
+    // Ideally, you should be able to just open a random detached file in existing cargo projects, and get the basic features working.
+    // That needs some changes on the salsa-level though.
+    // In particular, we should split the unified CrateGraph (which currently has maximal durability) into proper crate graph, and a set of ad hoc roots (with minimal durability).
+    // Then, we need to hide the graph behind the queries such that most queries look only at the proper crate graph, and fall back to ad hoc roots only if there's no results.
+    // After this, we should be able to tweak the logic in reload.rs to add newly opened files, which don't belong to any existing crates, to the set of the detached files.
+    // //
+    /// Project with a set of disjoint files, not belonging to any particular workspace.
+    /// Backed by basic sysroot crates for basic completion and highlighting.
+    DetachedFiles { files: Vec<AbsPathBuf>, sysroot: Sysroot, rustc_cfg: Vec<CfgFlag> },
 }
 
 impl fmt::Debug for ProjectWorkspace {
@@ -75,6 +87,12 @@ impl fmt::Debug for ProjectWorkspace {
                 debug_struct.field("n_rustc_cfg", &rustc_cfg.len());
                 debug_struct.finish()
             }
+            ProjectWorkspace::DetachedFiles { files, sysroot, rustc_cfg } => f
+                .debug_struct("DetachedFiles")
+                .field("n_files", &files.len())
+                .field("n_sysroot_crates", &sysroot.crates().len())
+                .field("n_rustc_cfg", &rustc_cfg.len())
+                .finish(),
         }
     }
 }
@@ -165,6 +183,14 @@ impl ProjectWorkspace {
         Ok(ProjectWorkspace::Json { project: project_json, sysroot, rustc_cfg })
     }
 
+    pub fn load_detached_files(detached_files: Vec<AbsPathBuf>) -> Result<ProjectWorkspace> {
+        let sysroot = Sysroot::discover(
+            &detached_files.first().ok_or_else(|| format_err!("No detached files to load"))?,
+        )?;
+        let rustc_cfg = rustc_cfg::get(None, None);
+        Ok(ProjectWorkspace::DetachedFiles { files: detached_files, sysroot, rustc_cfg })
+    }
+
     /// 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
@@ -224,6 +250,19 @@ impl ProjectWorkspace {
                     })
                 }))
                 .collect(),
+            ProjectWorkspace::DetachedFiles { files, sysroot, .. } => files
+                .into_iter()
+                .map(|detached_file| PackageRoot {
+                    is_member: true,
+                    include: vec![detached_file.clone()],
+                    exclude: Vec::new(),
+                })
+                .chain(sysroot.crates().map(|krate| PackageRoot {
+                    is_member: false,
+                    include: vec![sysroot[krate].root_dir().to_path_buf()],
+                    exclude: Vec::new(),
+                }))
+                .collect(),
         }
     }
 
@@ -234,6 +273,9 @@ impl ProjectWorkspace {
                 let rustc_package_len = rustc.as_ref().map_or(0, |rc| rc.packages().len());
                 cargo.packages().len() + sysroot.crates().len() + rustc_package_len
             }
+            ProjectWorkspace::DetachedFiles { sysroot, files, .. } => {
+                sysroot.crates().len() + files.len()
+            }
         }
     }
 
@@ -267,6 +309,9 @@ impl ProjectWorkspace {
                 rustc,
                 rustc.as_ref().zip(build_data).and_then(|(it, map)| map.get(it.workspace_root())),
             ),
+            ProjectWorkspace::DetachedFiles { files, sysroot, rustc_cfg } => {
+                detached_files_to_crate_graph(rustc_cfg.clone(), load, files, sysroot)
+            }
         };
         if crate_graph.patch_cfg_if() {
             log::debug!("Patched std to depend on cfg-if")
@@ -474,6 +519,48 @@ fn cargo_to_crate_graph(
     crate_graph
 }
 
+fn detached_files_to_crate_graph(
+    rustc_cfg: Vec<CfgFlag>,
+    load: &mut dyn FnMut(&AbsPath) -> Option<FileId>,
+    detached_files: &[AbsPathBuf],
+    sysroot: &Sysroot,
+) -> CrateGraph {
+    let _p = profile::span("detached_files_to_crate_graph");
+    let mut crate_graph = CrateGraph::default();
+    let (public_deps, _libproc_macro) =
+        sysroot_to_crate_graph(&mut crate_graph, sysroot, rustc_cfg.clone(), load);
+
+    let mut cfg_options = CfgOptions::default();
+    cfg_options.extend(rustc_cfg);
+
+    for detached_file in detached_files {
+        let file_id = match load(&detached_file) {
+            Some(file_id) => file_id,
+            None => {
+                log::error!("Failed to load detached file {:?}", detached_file);
+                continue;
+            }
+        };
+        let display_name = detached_file
+            .file_stem()
+            .and_then(|os_str| os_str.to_str())
+            .map(|file_stem| CrateDisplayName::from_canonical_name(file_stem.to_string()));
+        let detached_file_crate = crate_graph.add_crate_root(
+            file_id,
+            Edition::Edition2018,
+            display_name,
+            cfg_options.clone(),
+            Env::default(),
+            Vec::new(),
+        );
+
+        for (name, krate) in public_deps.iter() {
+            add_dep(&mut crate_graph, detached_file_crate, name.clone(), *krate);
+        }
+    }
+    crate_graph
+}
+
 fn handle_rustc_crates(
     rustc_workspace: &CargoWorkspace,
     load: &mut dyn FnMut(&AbsPath) -> Option<FileId>,
diff --git a/crates/rust-analyzer/src/bin/main.rs b/crates/rust-analyzer/src/bin/main.rs
index f0abb5b15f..6c883dd58b 100644
--- a/crates/rust-analyzer/src/bin/main.rs
+++ b/crates/rust-analyzer/src/bin/main.rs
@@ -199,7 +199,7 @@ fn run_server() -> Result<()> {
             config.update(json);
         }
 
-        if config.linked_projects().is_empty() {
+        if config.linked_projects().is_empty() && config.detached_files().is_empty() {
             let workspace_roots = initialize_params
                 .workspace_folders
                 .map(|workspaces| {
@@ -217,7 +217,6 @@ fn run_server() -> Result<()> {
             if discovered.is_empty() {
                 log::error!("failed to find any projects in {:?}", workspace_roots);
             }
-
             config.discovered_projects = Some(discovered);
         }
 
diff --git a/crates/rust-analyzer/src/config.rs b/crates/rust-analyzer/src/config.rs
index b700d025f1..7c02a507cd 100644
--- a/crates/rust-analyzer/src/config.rs
+++ b/crates/rust-analyzer/src/config.rs
@@ -236,6 +236,7 @@ impl Default for ConfigData {
 pub struct Config {
     caps: lsp_types::ClientCapabilities,
     data: ConfigData,
+    detached_files: Vec<AbsPathBuf>,
     pub discovered_projects: Option<Vec<ProjectManifest>>,
     pub root_path: AbsPathBuf,
 }
@@ -328,13 +329,23 @@ pub struct WorkspaceSymbolConfig {
 
 impl Config {
     pub fn new(root_path: AbsPathBuf, caps: ClientCapabilities) -> Self {
-        Config { caps, data: ConfigData::default(), discovered_projects: None, root_path }
+        Config {
+            caps,
+            data: ConfigData::default(),
+            detached_files: Vec::new(),
+            discovered_projects: None,
+            root_path,
+        }
     }
-    pub fn update(&mut self, json: serde_json::Value) {
+    pub fn update(&mut self, mut json: serde_json::Value) {
         log::info!("updating config from JSON: {:#}", json);
         if json.is_null() || json.as_object().map_or(false, |it| it.is_empty()) {
             return;
         }
+        self.detached_files = get_field::<Vec<PathBuf>>(&mut json, "detachedFiles", None, "[]")
+            .into_iter()
+            .map(AbsPathBuf::assert)
+            .collect();
         self.data = ConfigData::from_json(json);
     }
 
@@ -387,6 +398,10 @@ impl Config {
         }
     }
 
+    pub fn detached_files(&self) -> &[AbsPathBuf] {
+        &self.detached_files
+    }
+
     pub fn did_save_text_document_dynamic_registration(&self) -> bool {
         let caps =
             try_or!(self.caps.text_document.as_ref()?.synchronization.clone()?, Default::default());
diff --git a/crates/rust-analyzer/src/global_state.rs b/crates/rust-analyzer/src/global_state.rs
index 6f2f482c15..6a36d29d4d 100644
--- a/crates/rust-analyzer/src/global_state.rs
+++ b/crates/rust-analyzer/src/global_state.rs
@@ -312,6 +312,7 @@ impl GlobalStateSnapshot {
                 cargo.target_by_root(&path).map(|it| (cargo, it))
             }
             ProjectWorkspace::Json { .. } => None,
+            ProjectWorkspace::DetachedFiles { .. } => None,
         })
     }
 }
diff --git a/crates/rust-analyzer/src/handlers.rs b/crates/rust-analyzer/src/handlers.rs
index aa12fd94bd..f482104248 100644
--- a/crates/rust-analyzer/src/handlers.rs
+++ b/crates/rust-analyzer/src/handlers.rs
@@ -661,19 +661,28 @@ pub(crate) fn handle_runnables(
             }
         }
         None => {
-            res.push(lsp_ext::Runnable {
-                label: "cargo check --workspace".to_string(),
-                location: None,
-                kind: lsp_ext::RunnableKind::Cargo,
-                args: lsp_ext::CargoRunnable {
-                    workspace_root: None,
-                    override_cargo: config.override_cargo,
-                    cargo_args: vec!["check".to_string(), "--workspace".to_string()],
-                    cargo_extra_args: config.cargo_extra_args,
-                    executable_args: Vec::new(),
-                    expect_test: None,
-                },
-            });
+            if !snap.config.linked_projects().is_empty()
+                || !snap
+                    .config
+                    .discovered_projects
+                    .as_ref()
+                    .map(|projects| projects.is_empty())
+                    .unwrap_or(true)
+            {
+                res.push(lsp_ext::Runnable {
+                    label: "cargo check --workspace".to_string(),
+                    location: None,
+                    kind: lsp_ext::RunnableKind::Cargo,
+                    args: lsp_ext::CargoRunnable {
+                        workspace_root: None,
+                        override_cargo: config.override_cargo,
+                        cargo_args: vec!["check".to_string(), "--workspace".to_string()],
+                        cargo_extra_args: config.cargo_extra_args,
+                        executable_args: Vec::new(),
+                        expect_test: None,
+                    },
+                });
+            }
         }
     }
     Ok(res)
diff --git a/crates/rust-analyzer/src/main_loop.rs b/crates/rust-analyzer/src/main_loop.rs
index f837b89ddc..cb002f7009 100644
--- a/crates/rust-analyzer/src/main_loop.rs
+++ b/crates/rust-analyzer/src/main_loop.rs
@@ -103,6 +103,7 @@ impl fmt::Debug for Event {
 impl GlobalState {
     fn run(mut self, inbox: Receiver<lsp_server::Message>) -> Result<()> {
         if self.config.linked_projects().is_empty()
+            && self.config.detached_files().is_empty()
             && self.config.notifications().cargo_toml_not_found
         {
             self.show_message(
diff --git a/crates/rust-analyzer/src/reload.rs b/crates/rust-analyzer/src/reload.rs
index 0ae2758cc7..7a53e4a8b9 100644
--- a/crates/rust-analyzer/src/reload.rs
+++ b/crates/rust-analyzer/src/reload.rs
@@ -147,6 +147,7 @@ impl GlobalState {
 
         self.task_pool.handle.spawn_with_sender({
             let linked_projects = self.config.linked_projects();
+            let detached_files = self.config.detached_files().to_vec();
             let cargo_config = self.config.cargo();
 
             move |sender| {
@@ -161,7 +162,7 @@ impl GlobalState {
 
                 sender.send(Task::FetchWorkspace(ProjectWorkspaceProgress::Begin)).unwrap();
 
-                let workspaces = linked_projects
+                let mut workspaces = linked_projects
                     .iter()
                     .map(|project| match project {
                         LinkedProject::ProjectManifest(manifest) => {
@@ -180,6 +181,11 @@ impl GlobalState {
                     })
                     .collect::<Vec<_>>();
 
+                if !detached_files.is_empty() {
+                    workspaces
+                        .push(project_model::ProjectWorkspace::load_detached_files(detached_files));
+                }
+
                 log::info!("did fetch workspaces {:?}", workspaces);
                 sender
                     .send(Task::FetchWorkspace(ProjectWorkspaceProgress::End(workspaces)))
@@ -407,6 +413,7 @@ impl GlobalState {
                         _ => None,
                     }
                 }
+                ProjectWorkspace::DetachedFiles { .. } => None,
             })
             .map(|(id, root)| {
                 let sender = sender.clone();
diff --git a/editors/code/src/client.ts b/editors/code/src/client.ts
index 116f41df6e..69dbe25353 100644
--- a/editors/code/src/client.ts
+++ b/editors/code/src/client.ts
@@ -4,6 +4,7 @@ import * as ra from '../src/lsp_ext';
 import * as Is from 'vscode-languageclient/lib/common/utils/is';
 import { assert } from './util';
 import { WorkspaceEdit } from 'vscode';
+import { Workspace } from './ctx';
 
 export interface Env {
     [name: string]: string;
@@ -23,7 +24,7 @@ function renderHoverActions(actions: ra.CommandLinkGroup[]): vscode.MarkdownStri
     return result;
 }
 
-export function createClient(serverPath: string, cwd: string, extraEnv: Env): lc.LanguageClient {
+export function createClient(serverPath: string, workspace: Workspace, extraEnv: Env): lc.LanguageClient {
     // '.' Is the fallback if no folder is open
     // TODO?: Workspace folders support Uri's (eg: file://test.txt).
     // It might be a good idea to test if the uri points to a file.
@@ -31,6 +32,11 @@ export function createClient(serverPath: string, cwd: string, extraEnv: Env): lc
     const newEnv = Object.assign({}, process.env);
     Object.assign(newEnv, extraEnv);
 
+    let cwd = undefined;
+    if (workspace.kind === "Workspace Folder") {
+        cwd = workspace.folder.fsPath;
+    };
+
     const run: lc.Executable = {
         command: serverPath,
         options: { cwd, env: newEnv },
@@ -43,9 +49,14 @@ export function createClient(serverPath: string, cwd: string, extraEnv: Env): lc
         'Rust Analyzer Language Server Trace',
     );
 
+    let initializationOptions = vscode.workspace.getConfiguration("rust-analyzer");
+    if (workspace.kind === "Detached Files") {
+        initializationOptions = { "detachedFiles": workspace.files.map(file => file.uri.fsPath), ...initializationOptions };
+    }
+
     const clientOptions: lc.LanguageClientOptions = {
         documentSelector: [{ scheme: 'file', language: 'rust' }],
-        initializationOptions: vscode.workspace.getConfiguration("rust-analyzer"),
+        initializationOptions,
         diagnosticCollectionName: "rustc",
         traceOutputChannel,
         middleware: {
diff --git a/editors/code/src/ctx.ts b/editors/code/src/ctx.ts
index bd023f803e..22c5f62a1c 100644
--- a/editors/code/src/ctx.ts
+++ b/editors/code/src/ctx.ts
@@ -7,6 +7,16 @@ import { createClient } from './client';
 import { isRustEditor, RustEditor } from './util';
 import { ServerStatusParams } from './lsp_ext';
 
+export type Workspace =
+    {
+        kind: 'Workspace Folder';
+        folder: vscode.Uri;
+    }
+    | {
+        kind: 'Detached Files';
+        files: vscode.TextDocument[];
+    };
+
 export class Ctx {
     private constructor(
         readonly config: Config,
@@ -22,9 +32,9 @@ export class Ctx {
         config: Config,
         extCtx: vscode.ExtensionContext,
         serverPath: string,
-        cwd: string,
+        workspace: Workspace,
     ): Promise<Ctx> {
-        const client = createClient(serverPath, cwd, config.serverExtraEnv);
+        const client = createClient(serverPath, workspace, config.serverExtraEnv);
 
         const statusBar = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Left);
         extCtx.subscriptions.push(statusBar);
diff --git a/editors/code/src/main.ts b/editors/code/src/main.ts
index aaedc24314..b735186fe1 100644
--- a/editors/code/src/main.ts
+++ b/editors/code/src/main.ts
@@ -7,7 +7,7 @@ import * as commands from './commands';
 import { activateInlayHints } from './inlay_hints';
 import { Ctx } from './ctx';
 import { Config } from './config';
-import { log, assert, isValidExecutable } from './util';
+import { log, assert, isValidExecutable, isRustDocument } from './util';
 import { PersistentState } from './persistent_state';
 import { fetchRelease, download } from './net';
 import { activateTaskProvider } from './tasks';
@@ -28,26 +28,6 @@ export async function activate(context: vscode.ExtensionContext) {
 }
 
 async function tryActivate(context: vscode.ExtensionContext) {
-    // Register a "dumb" onEnter command for the case where server fails to
-    // start.
-    //
-    // FIXME: refactor command registration code such that commands are
-    // **always** registered, even if the server does not start. Use API like
-    // this perhaps?
-    //
-    // ```TypeScript
-    // registerCommand(
-    //    factory: (Ctx) => ((Ctx) => any),
-    //    fallback: () => any = () => vscode.window.showErrorMessage(
-    //        "rust-analyzer is not available"
-    //    ),
-    // )
-    const defaultOnEnter = vscode.commands.registerCommand(
-        'rust-analyzer.onEnter',
-        () => vscode.commands.executeCommand('default:type', { text: '\n' }),
-    );
-    context.subscriptions.push(defaultOnEnter);
-
     const config = new Config(context);
     const state = new PersistentState(context.globalState);
     const serverPath = await bootstrap(config, state).catch(err => {
@@ -67,14 +47,52 @@ async function tryActivate(context: vscode.ExtensionContext) {
 
     const workspaceFolder = vscode.workspace.workspaceFolders?.[0];
     if (workspaceFolder === undefined) {
-        throw new Error("no folder is opened");
+        const rustDocuments = vscode.workspace.textDocuments.filter(document => isRustDocument(document));
+        if (rustDocuments.length > 0) {
+            ctx = await Ctx.create(config, context, serverPath, { kind: 'Detached Files', files: rustDocuments });
+        } else {
+            throw new Error("no rust files are opened");
+        }
+    } else {
+        // Note: we try to start the server before we activate type hints so that it
+        // registers its `onDidChangeDocument` handler before us.
+        //
+        // This a horribly, horribly wrong way to deal with this problem.
+        ctx = await Ctx.create(config, context, serverPath, { kind: "Workspace Folder", folder: workspaceFolder.uri });
+        ctx.pushCleanup(activateTaskProvider(workspaceFolder, ctx.config));
     }
+    await initCommonContext(context, ctx);
+
+    activateInlayHints(ctx);
+    warnAboutExtensionConflicts();
+
+    vscode.workspace.onDidChangeConfiguration(
+        _ => ctx?.client?.sendNotification('workspace/didChangeConfiguration', { settings: "" }),
+        null,
+        ctx.subscriptions,
+    );
+}
 
-    // Note: we try to start the server before we activate type hints so that it
-    // registers its `onDidChangeDocument` handler before us.
+async function initCommonContext(context: vscode.ExtensionContext, ctx: Ctx) {
+    // Register a "dumb" onEnter command for the case where server fails to
+    // start.
+    //
+    // FIXME: refactor command registration code such that commands are
+    // **always** registered, even if the server does not start. Use API like
+    // this perhaps?
     //
-    // This a horribly, horribly wrong way to deal with this problem.
-    ctx = await Ctx.create(config, context, serverPath, workspaceFolder.uri.fsPath);
+    // ```TypeScript
+    // registerCommand(
+    //    factory: (Ctx) => ((Ctx) => any),
+    //    fallback: () => any = () => vscode.window.showErrorMessage(
+    //        "rust-analyzer is not available"
+    //    ),
+    // )
+    const defaultOnEnter = vscode.commands.registerCommand(
+        'rust-analyzer.onEnter',
+        () => vscode.commands.executeCommand('default:type', { text: '\n' }),
+    );
+    context.subscriptions.push(defaultOnEnter);
 
     await setContextValue(RUST_PROJECT_CONTEXT_NAME, true);
 
@@ -134,17 +152,6 @@ async function tryActivate(context: vscode.ExtensionContext) {
     ctx.registerCommand('resolveCodeAction', commands.resolveCodeAction);
     ctx.registerCommand('applyActionGroup', commands.applyActionGroup);
     ctx.registerCommand('gotoLocation', commands.gotoLocation);
-
-    ctx.pushCleanup(activateTaskProvider(workspaceFolder, ctx.config));
-
-    activateInlayHints(ctx);
-    warnAboutExtensionConflicts();
-
-    vscode.workspace.onDidChangeConfiguration(
-        _ => ctx?.client?.sendNotification('workspace/didChangeConfiguration', { settings: "" }),
-        null,
-        ctx.subscriptions,
-    );
 }
 
 export async function deactivate() {
diff --git a/editors/code/src/snippets.ts b/editors/code/src/snippets.ts
index 58f7aa128f..9561aa345b 100644
--- a/editors/code/src/snippets.ts
+++ b/editors/code/src/snippets.ts
@@ -56,9 +56,6 @@ export async function applySnippetTextEdits(editor: vscode.TextEditor, edits: vs
         }
     });
     if (selections.length > 0) editor.selections = selections;
-    if (selections.length === 1) {
-        editor.revealRange(selections[0], vscode.TextEditorRevealType.InCenterIfOutsideViewport);
-    }
 }
 
 function parseSnippet(snip: string): [string, [number, number]] | undefined {


Test patch:
diff --git a/crates/ide_assists/src/tests.rs b/crates/ide_assists/src/tests.rs
index 2b7c2d581e..6a9231e078 100644
--- a/crates/ide_assists/src/tests.rs
+++ b/crates/ide_assists/src/tests.rs
@@ -215,8 +215,8 @@ fn assist_order_field_struct() {
 
     assert_eq!(assists.next().expect("expected assist").label, "Change visibility to pub(crate)");
     assert_eq!(assists.next().expect("expected assist").label, "Generate `Deref` impl using `bar`");
-    assert_eq!(assists.next().expect("expected assist").label, "Generate a getter method");
     assert_eq!(assists.next().expect("expected assist").label, "Generate a mut getter method");
+    assert_eq!(assists.next().expect("expected assist").label, "Generate a getter method");
     assert_eq!(assists.next().expect("expected assist").label, "Generate a setter method");
     assert_eq!(assists.next().expect("expected assist").label, "Add `#[derive]`");
 }
diff --git a/crates/ide_assists/src/tests/generated.rs b/crates/ide_assists/src/tests/generated.rs
index 8a9b0777c0..4406406a27 100644
--- a/crates/ide_assists/src/tests/generated.rs
+++ b/crates/ide_assists/src/tests/generated.rs
@@ -786,8 +786,8 @@ struct Person {
 
 impl Person {
     /// Get a reference to the person's name.
-    fn $0name(&self) -> &str {
-        self.name.as_str()
+    fn name(&self) -> &String {
+        &self.name
     }
 }
 "#####,
@@ -810,7 +810,7 @@ struct Person {
 
 impl Person {
     /// Get a mutable reference to the person's name.
-    fn $0name_mut(&mut self) -> &mut String {
+    fn name_mut(&mut self) -> &mut String {
         &mut self.name
     }
 }