Skip to content

Commit 05fc97e

Browse files
Merge #8955
8955: feature: Support standalone Rust files r=matklad a=SomeoneToIgnore ![standalone](https://user-images.githubusercontent.com/2690773/119277037-0b579380-bc26-11eb-8d77-20d46ab4916a.gif) Closes #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. Co-authored-by: Kirill Bulatov <[email protected]>
2 parents 31a1914 + 5c0369b commit 05fc97e

File tree

11 files changed

+210
-61
lines changed

11 files changed

+210
-61
lines changed

crates/project_model/src/sysroot.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,9 @@ impl Sysroot {
5050

5151
pub fn discover(cargo_toml: &AbsPath) -> Result<Sysroot> {
5252
log::debug!("Discovering sysroot for {}", cargo_toml.display());
53-
let current_dir = cargo_toml.parent().unwrap();
53+
let current_dir = cargo_toml.parent().ok_or_else(|| {
54+
format_err!("Failed to find the parent directory for {}", cargo_toml.display())
55+
})?;
5456
let sysroot_dir = discover_sysroot_dir(current_dir)?;
5557
let sysroot_src_dir = discover_sysroot_src_dir(&sysroot_dir, current_dir)?;
5658
let res = Sysroot::load(&sysroot_src_dir)?;

crates/project_model/src/workspace.rs

Lines changed: 88 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
55
use std::{collections::VecDeque, fmt, fs, path::Path, process::Command};
66

7-
use anyhow::{Context, Result};
7+
use anyhow::{format_err, Context, Result};
88
use base_db::{CrateDisplayName, CrateGraph, CrateId, CrateName, Edition, Env, FileId, ProcMacro};
99
use cargo_workspace::DepKind;
1010
use cfg::CfgOptions;
@@ -49,6 +49,18 @@ pub enum ProjectWorkspace {
4949
},
5050
/// Project workspace was manually specified using a `rust-project.json` file.
5151
Json { project: ProjectJson, sysroot: Option<Sysroot>, rustc_cfg: Vec<CfgFlag> },
52+
53+
// FIXME: The primary limitation of this approach is that the set of detached files needs to be fixed at the beginning.
54+
// That's not the end user experience we should strive for.
55+
// Ideally, you should be able to just open a random detached file in existing cargo projects, and get the basic features working.
56+
// That needs some changes on the salsa-level though.
57+
// 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).
58+
// 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.
59+
// 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.
60+
// //
61+
/// Project with a set of disjoint files, not belonging to any particular workspace.
62+
/// Backed by basic sysroot crates for basic completion and highlighting.
63+
DetachedFiles { files: Vec<AbsPathBuf>, sysroot: Sysroot, rustc_cfg: Vec<CfgFlag> },
5264
}
5365

5466
impl fmt::Debug for ProjectWorkspace {
@@ -75,6 +87,12 @@ impl fmt::Debug for ProjectWorkspace {
7587
debug_struct.field("n_rustc_cfg", &rustc_cfg.len());
7688
debug_struct.finish()
7789
}
90+
ProjectWorkspace::DetachedFiles { files, sysroot, rustc_cfg } => f
91+
.debug_struct("DetachedFiles")
92+
.field("n_files", &files.len())
93+
.field("n_sysroot_crates", &sysroot.crates().len())
94+
.field("n_rustc_cfg", &rustc_cfg.len())
95+
.finish(),
7896
}
7997
}
8098
}
@@ -165,6 +183,14 @@ impl ProjectWorkspace {
165183
Ok(ProjectWorkspace::Json { project: project_json, sysroot, rustc_cfg })
166184
}
167185

186+
pub fn load_detached_files(detached_files: Vec<AbsPathBuf>) -> Result<ProjectWorkspace> {
187+
let sysroot = Sysroot::discover(
188+
&detached_files.first().ok_or_else(|| format_err!("No detached files to load"))?,
189+
)?;
190+
let rustc_cfg = rustc_cfg::get(None, None);
191+
Ok(ProjectWorkspace::DetachedFiles { files: detached_files, sysroot, rustc_cfg })
192+
}
193+
168194
/// Returns the roots for the current `ProjectWorkspace`
169195
/// The return type contains the path and whether or not
170196
/// the root is a member of the current workspace
@@ -224,6 +250,19 @@ impl ProjectWorkspace {
224250
})
225251
}))
226252
.collect(),
253+
ProjectWorkspace::DetachedFiles { files, sysroot, .. } => files
254+
.into_iter()
255+
.map(|detached_file| PackageRoot {
256+
is_member: true,
257+
include: vec![detached_file.clone()],
258+
exclude: Vec::new(),
259+
})
260+
.chain(sysroot.crates().map(|krate| PackageRoot {
261+
is_member: false,
262+
include: vec![sysroot[krate].root_dir().to_path_buf()],
263+
exclude: Vec::new(),
264+
}))
265+
.collect(),
227266
}
228267
}
229268

@@ -234,6 +273,9 @@ impl ProjectWorkspace {
234273
let rustc_package_len = rustc.as_ref().map_or(0, |rc| rc.packages().len());
235274
cargo.packages().len() + sysroot.crates().len() + rustc_package_len
236275
}
276+
ProjectWorkspace::DetachedFiles { sysroot, files, .. } => {
277+
sysroot.crates().len() + files.len()
278+
}
237279
}
238280
}
239281

@@ -267,6 +309,9 @@ impl ProjectWorkspace {
267309
rustc,
268310
rustc.as_ref().zip(build_data).and_then(|(it, map)| map.get(it.workspace_root())),
269311
),
312+
ProjectWorkspace::DetachedFiles { files, sysroot, rustc_cfg } => {
313+
detached_files_to_crate_graph(rustc_cfg.clone(), load, files, sysroot)
314+
}
270315
};
271316
if crate_graph.patch_cfg_if() {
272317
log::debug!("Patched std to depend on cfg-if")
@@ -474,6 +519,48 @@ fn cargo_to_crate_graph(
474519
crate_graph
475520
}
476521

522+
fn detached_files_to_crate_graph(
523+
rustc_cfg: Vec<CfgFlag>,
524+
load: &mut dyn FnMut(&AbsPath) -> Option<FileId>,
525+
detached_files: &[AbsPathBuf],
526+
sysroot: &Sysroot,
527+
) -> CrateGraph {
528+
let _p = profile::span("detached_files_to_crate_graph");
529+
let mut crate_graph = CrateGraph::default();
530+
let (public_deps, _libproc_macro) =
531+
sysroot_to_crate_graph(&mut crate_graph, sysroot, rustc_cfg.clone(), load);
532+
533+
let mut cfg_options = CfgOptions::default();
534+
cfg_options.extend(rustc_cfg);
535+
536+
for detached_file in detached_files {
537+
let file_id = match load(&detached_file) {
538+
Some(file_id) => file_id,
539+
None => {
540+
log::error!("Failed to load detached file {:?}", detached_file);
541+
continue;
542+
}
543+
};
544+
let display_name = detached_file
545+
.file_stem()
546+
.and_then(|os_str| os_str.to_str())
547+
.map(|file_stem| CrateDisplayName::from_canonical_name(file_stem.to_string()));
548+
let detached_file_crate = crate_graph.add_crate_root(
549+
file_id,
550+
Edition::Edition2018,
551+
display_name,
552+
cfg_options.clone(),
553+
Env::default(),
554+
Vec::new(),
555+
);
556+
557+
for (name, krate) in public_deps.iter() {
558+
add_dep(&mut crate_graph, detached_file_crate, name.clone(), *krate);
559+
}
560+
}
561+
crate_graph
562+
}
563+
477564
fn handle_rustc_crates(
478565
rustc_workspace: &CargoWorkspace,
479566
load: &mut dyn FnMut(&AbsPath) -> Option<FileId>,

crates/rust-analyzer/src/bin/main.rs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -199,7 +199,7 @@ fn run_server() -> Result<()> {
199199
config.update(json);
200200
}
201201

202-
if config.linked_projects().is_empty() {
202+
if config.linked_projects().is_empty() && config.detached_files().is_empty() {
203203
let workspace_roots = initialize_params
204204
.workspace_folders
205205
.map(|workspaces| {
@@ -217,7 +217,6 @@ fn run_server() -> Result<()> {
217217
if discovered.is_empty() {
218218
log::error!("failed to find any projects in {:?}", workspace_roots);
219219
}
220-
221220
config.discovered_projects = Some(discovered);
222221
}
223222

crates/rust-analyzer/src/config.rs

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -236,6 +236,7 @@ impl Default for ConfigData {
236236
pub struct Config {
237237
caps: lsp_types::ClientCapabilities,
238238
data: ConfigData,
239+
detached_files: Vec<AbsPathBuf>,
239240
pub discovered_projects: Option<Vec<ProjectManifest>>,
240241
pub root_path: AbsPathBuf,
241242
}
@@ -328,13 +329,23 @@ pub struct WorkspaceSymbolConfig {
328329

329330
impl Config {
330331
pub fn new(root_path: AbsPathBuf, caps: ClientCapabilities) -> Self {
331-
Config { caps, data: ConfigData::default(), discovered_projects: None, root_path }
332+
Config {
333+
caps,
334+
data: ConfigData::default(),
335+
detached_files: Vec::new(),
336+
discovered_projects: None,
337+
root_path,
338+
}
332339
}
333-
pub fn update(&mut self, json: serde_json::Value) {
340+
pub fn update(&mut self, mut json: serde_json::Value) {
334341
log::info!("updating config from JSON: {:#}", json);
335342
if json.is_null() || json.as_object().map_or(false, |it| it.is_empty()) {
336343
return;
337344
}
345+
self.detached_files = get_field::<Vec<PathBuf>>(&mut json, "detachedFiles", None, "[]")
346+
.into_iter()
347+
.map(AbsPathBuf::assert)
348+
.collect();
338349
self.data = ConfigData::from_json(json);
339350
}
340351

@@ -387,6 +398,10 @@ impl Config {
387398
}
388399
}
389400

401+
pub fn detached_files(&self) -> &[AbsPathBuf] {
402+
&self.detached_files
403+
}
404+
390405
pub fn did_save_text_document_dynamic_registration(&self) -> bool {
391406
let caps =
392407
try_or!(self.caps.text_document.as_ref()?.synchronization.clone()?, Default::default());

crates/rust-analyzer/src/global_state.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -312,6 +312,7 @@ impl GlobalStateSnapshot {
312312
cargo.target_by_root(&path).map(|it| (cargo, it))
313313
}
314314
ProjectWorkspace::Json { .. } => None,
315+
ProjectWorkspace::DetachedFiles { .. } => None,
315316
})
316317
}
317318
}

crates/rust-analyzer/src/handlers.rs

Lines changed: 22 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -661,19 +661,28 @@ pub(crate) fn handle_runnables(
661661
}
662662
}
663663
None => {
664-
res.push(lsp_ext::Runnable {
665-
label: "cargo check --workspace".to_string(),
666-
location: None,
667-
kind: lsp_ext::RunnableKind::Cargo,
668-
args: lsp_ext::CargoRunnable {
669-
workspace_root: None,
670-
override_cargo: config.override_cargo,
671-
cargo_args: vec!["check".to_string(), "--workspace".to_string()],
672-
cargo_extra_args: config.cargo_extra_args,
673-
executable_args: Vec::new(),
674-
expect_test: None,
675-
},
676-
});
664+
if !snap.config.linked_projects().is_empty()
665+
|| !snap
666+
.config
667+
.discovered_projects
668+
.as_ref()
669+
.map(|projects| projects.is_empty())
670+
.unwrap_or(true)
671+
{
672+
res.push(lsp_ext::Runnable {
673+
label: "cargo check --workspace".to_string(),
674+
location: None,
675+
kind: lsp_ext::RunnableKind::Cargo,
676+
args: lsp_ext::CargoRunnable {
677+
workspace_root: None,
678+
override_cargo: config.override_cargo,
679+
cargo_args: vec!["check".to_string(), "--workspace".to_string()],
680+
cargo_extra_args: config.cargo_extra_args,
681+
executable_args: Vec::new(),
682+
expect_test: None,
683+
},
684+
});
685+
}
677686
}
678687
}
679688
Ok(res)

crates/rust-analyzer/src/main_loop.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,7 @@ impl fmt::Debug for Event {
103103
impl GlobalState {
104104
fn run(mut self, inbox: Receiver<lsp_server::Message>) -> Result<()> {
105105
if self.config.linked_projects().is_empty()
106+
&& self.config.detached_files().is_empty()
106107
&& self.config.notifications().cargo_toml_not_found
107108
{
108109
self.show_message(

crates/rust-analyzer/src/reload.rs

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,7 @@ impl GlobalState {
147147

148148
self.task_pool.handle.spawn_with_sender({
149149
let linked_projects = self.config.linked_projects();
150+
let detached_files = self.config.detached_files().to_vec();
150151
let cargo_config = self.config.cargo();
151152

152153
move |sender| {
@@ -161,7 +162,7 @@ impl GlobalState {
161162

162163
sender.send(Task::FetchWorkspace(ProjectWorkspaceProgress::Begin)).unwrap();
163164

164-
let workspaces = linked_projects
165+
let mut workspaces = linked_projects
165166
.iter()
166167
.map(|project| match project {
167168
LinkedProject::ProjectManifest(manifest) => {
@@ -180,6 +181,11 @@ impl GlobalState {
180181
})
181182
.collect::<Vec<_>>();
182183

184+
if !detached_files.is_empty() {
185+
workspaces
186+
.push(project_model::ProjectWorkspace::load_detached_files(detached_files));
187+
}
188+
183189
log::info!("did fetch workspaces {:?}", workspaces);
184190
sender
185191
.send(Task::FetchWorkspace(ProjectWorkspaceProgress::End(workspaces)))
@@ -407,6 +413,7 @@ impl GlobalState {
407413
_ => None,
408414
}
409415
}
416+
ProjectWorkspace::DetachedFiles { .. } => None,
410417
})
411418
.map(|(id, root)| {
412419
let sender = sender.clone();

editors/code/src/client.ts

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import * as ra from '../src/lsp_ext';
44
import * as Is from 'vscode-languageclient/lib/common/utils/is';
55
import { assert } from './util';
66
import { WorkspaceEdit } from 'vscode';
7+
import { Workspace } from './ctx';
78

89
export interface Env {
910
[name: string]: string;
@@ -23,14 +24,19 @@ function renderHoverActions(actions: ra.CommandLinkGroup[]): vscode.MarkdownStri
2324
return result;
2425
}
2526

26-
export function createClient(serverPath: string, cwd: string, extraEnv: Env): lc.LanguageClient {
27+
export function createClient(serverPath: string, workspace: Workspace, extraEnv: Env): lc.LanguageClient {
2728
// '.' Is the fallback if no folder is open
2829
// TODO?: Workspace folders support Uri's (eg: file://test.txt).
2930
// It might be a good idea to test if the uri points to a file.
3031

3132
const newEnv = Object.assign({}, process.env);
3233
Object.assign(newEnv, extraEnv);
3334

35+
let cwd = undefined;
36+
if (workspace.kind === "Workspace Folder") {
37+
cwd = workspace.folder.fsPath;
38+
};
39+
3440
const run: lc.Executable = {
3541
command: serverPath,
3642
options: { cwd, env: newEnv },
@@ -43,9 +49,14 @@ export function createClient(serverPath: string, cwd: string, extraEnv: Env): lc
4349
'Rust Analyzer Language Server Trace',
4450
);
4551

52+
let initializationOptions = vscode.workspace.getConfiguration("rust-analyzer");
53+
if (workspace.kind === "Detached Files") {
54+
initializationOptions = { "detachedFiles": workspace.files.map(file => file.uri.fsPath), ...initializationOptions };
55+
}
56+
4657
const clientOptions: lc.LanguageClientOptions = {
4758
documentSelector: [{ scheme: 'file', language: 'rust' }],
48-
initializationOptions: vscode.workspace.getConfiguration("rust-analyzer"),
59+
initializationOptions,
4960
diagnosticCollectionName: "rustc",
5061
traceOutputChannel,
5162
middleware: {

editors/code/src/ctx.ts

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,16 @@ import { createClient } from './client';
77
import { isRustEditor, RustEditor } from './util';
88
import { ServerStatusParams } from './lsp_ext';
99

10+
export type Workspace =
11+
{
12+
kind: 'Workspace Folder';
13+
folder: vscode.Uri;
14+
}
15+
| {
16+
kind: 'Detached Files';
17+
files: vscode.TextDocument[];
18+
};
19+
1020
export class Ctx {
1121
private constructor(
1222
readonly config: Config,
@@ -22,9 +32,9 @@ export class Ctx {
2232
config: Config,
2333
extCtx: vscode.ExtensionContext,
2434
serverPath: string,
25-
cwd: string,
35+
workspace: Workspace,
2636
): Promise<Ctx> {
27-
const client = createClient(serverPath, cwd, config.serverExtraEnv);
37+
const client = createClient(serverPath, workspace, config.serverExtraEnv);
2838

2939
const statusBar = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Left);
3040
extCtx.subscriptions.push(statusBar);

0 commit comments

Comments
 (0)