Skip to content

feature: Support standalone Rust files #8955

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 10 commits into from
May 24, 2021
4 changes: 3 additions & 1 deletion crates/project_model/src/sysroot.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)?;
Expand Down
89 changes: 88 additions & 1 deletion crates/project_model/src/workspace.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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 {
Expand All @@ -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(),
}
}
}
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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(),
}
}

Expand All @@ -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()
}
}
}

Expand Down Expand Up @@ -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")
Expand Down Expand Up @@ -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>,
Expand Down
3 changes: 1 addition & 2 deletions crates/rust-analyzer/src/bin/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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| {
Expand All @@ -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);
}

Expand Down
19 changes: 17 additions & 2 deletions crates/rust-analyzer/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
}
Expand Down Expand Up @@ -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);
}

Expand Down Expand Up @@ -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());
Expand Down
1 change: 1 addition & 0 deletions crates/rust-analyzer/src/global_state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -312,6 +312,7 @@ impl GlobalStateSnapshot {
cargo.target_by_root(&path).map(|it| (cargo, it))
}
ProjectWorkspace::Json { .. } => None,
ProjectWorkspace::DetachedFiles { .. } => None,
})
}
}
Expand Down
35 changes: 22 additions & 13 deletions crates/rust-analyzer/src/handlers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
1 change: 1 addition & 0 deletions crates/rust-analyzer/src/main_loop.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
9 changes: 8 additions & 1 deletion crates/rust-analyzer/src/reload.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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| {
Expand All @@ -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) => {
Expand All @@ -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)))
Expand Down Expand Up @@ -407,6 +413,7 @@ impl GlobalState {
_ => None,
}
}
ProjectWorkspace::DetachedFiles { .. } => None,
})
.map(|(id, root)| {
let sender = sender.clone();
Expand Down
15 changes: 13 additions & 2 deletions editors/code/src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -23,14 +24,19 @@ 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.

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 },
Expand All @@ -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: {
Expand Down
14 changes: 12 additions & 2 deletions editors/code/src/ctx.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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);
Expand Down
Loading