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 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.

Co-authored-by: Kirill Bulatov <mail4score@gmail.com>
This commit is contained in:
bors[bot]
2021-05-24 12:40:25 +00:00
committed by GitHub
11 changed files with 210 additions and 61 deletions

View File

@@ -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: {

View File

@@ -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);

View File

@@ -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);
// Note: we try to start the server before we activate type hints so that it
// registers its `onDidChangeDocument` handler before us.
activateInlayHints(ctx);
warnAboutExtensionConflicts();
vscode.workspace.onDidChangeConfiguration(
_ => ctx?.client?.sendNotification('workspace/didChangeConfiguration', { settings: "" }),
null,
ctx.subscriptions,
);
}
async function initCommonContext(context: vscode.ExtensionContext, ctx: Ctx) {
// Register a "dumb" onEnter command for the case where server fails to
// start.
//
// This a horribly, horribly wrong way to deal with this problem.
ctx = await Ctx.create(config, context, serverPath, workspaceFolder.uri.fsPath);
// 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);
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() {