diff --git a/CHANGELOG.md b/CHANGELOG.md index 87af7fd9..46afee94 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,7 @@ ### Unreleased +* Support VSCode workspaces + ### 0.4.5 - 2018-06-03 * Undo the change to target directory default (unnecessary with Rust 1.26.1) diff --git a/README.md b/README.md index 30d7dba1..6b987f0d 100644 --- a/README.md +++ b/README.md @@ -41,7 +41,7 @@ more details on building and debugging, etc., see [contributing.md](contributing (or by entering `ext install rust` at the command palette). * (Skip this step if you already have Rust projects that you'd like to work on.) Create a new Rust project by following [these instructions](https://doc.rust-lang.org/book/second-edition/ch01-02-hello-world.html#creating-a-project-with-cargo). -* Open a Rust project (`File > Open Folder...`). Open the folder for the whole +* Open a Rust project (`File > Add Folder to Workspace...`). Open the folder for the whole project (i.e., the folder containing 'Cargo.toml'), not the 'src' folder. * You'll be prompted to install the RLS. Once installed, the RLS should start building your project. diff --git a/package.json b/package.json index ae9556bf..5c571d5b 100644 --- a/package.json +++ b/package.json @@ -197,7 +197,8 @@ "null" ], "default": null, - "description": "--sysroot" + "description": "--sysroot", + "scope": "resource" }, "rust.target": { "type": [ @@ -205,7 +206,8 @@ "null" ], "default": null, - "description": "--target" + "description": "--target", + "scope": "resource" }, "rust.rustflags": { "type": [ @@ -213,12 +215,14 @@ "null" ], "default": null, - "description": "Flags added to RUSTFLAGS." + "description": "Flags added to RUSTFLAGS.", + "scope": "resource" }, "rust.clear_env_rust_log": { "type": "boolean", "default": true, - "description": "Clear the RUST_LOG environment variable before running rustc or cargo." + "description": "Clear the RUST_LOG environment variable before running rustc or cargo.", + "scope": "resource" }, "rust.build_lib": { "type": [ @@ -226,7 +230,8 @@ "null" ], "default": null, - "description": "Specify to run analysis as if running `cargo check --lib`. Use `null` to auto-detect. (unstable)" + "description": "Specify to run analysis as if running `cargo check --lib`. Use `null` to auto-detect. (unstable)", + "scope": "resource" }, "rust.build_bin": { "type": [ @@ -234,62 +239,74 @@ "null" ], "default": null, - "description": "Specify to run analysis as if running `cargo check --bin `. Use `null` to auto-detect. (unstable)" + "description": "Specify to run analysis as if running `cargo check --bin `. Use `null` to auto-detect. (unstable)", + "scope": "resource" }, "rust.cfg_test": { "type": "boolean", "default": false, - "description": "Build cfg(test) code. (unstable)" + "description": "Build cfg(test) code. (unstable)", + "scope": "resource" }, "rust.unstable_features": { "type": "boolean", "default": false, - "description": "Enable unstable features." + "description": "Enable unstable features.", + "scope": "resource" }, "rust.wait_to_build": { "type": "number", "default": 1500, - "description": "Time in milliseconds between receiving a change notification and starting build." + "description": "Time in milliseconds between receiving a change notification and starting build.", + "scope": "resource" }, "rust.show_warnings": { "type": "boolean", "default": true, - "description": "Show warnings." - }, - "rust.goto_def_racer_fallback": { - "type": "boolean", - "default": false, - "description": "Use racer as a fallback for goto def." + "description": "Show warnings.", + "scope": "resource" }, "rust.use_crate_blacklist": { "type": "boolean", "default": true, - "description": "Don't index crates on the crate blacklist." + "description": "Don't index crates on the crate blacklist.", + "scope": "resource" }, "rust.build_on_save": { "type": "boolean", "default": false, - "description": "Only index the project when a file is saved and not on change." + "description": "Only index the project when a file is saved and not on change.", + "scope": "resource" }, "rust.features": { "type": "array", "default": [], - "description": "A list of Cargo features to enable." + "description": "A list of Cargo features to enable.", + "scope": "resource" }, "rust.all_features": { "type": "boolean", "default": false, - "description": "Enable all Cargo features." + "description": "Enable all Cargo features.", + "scope": "resource" }, "rust.no_default_features": { "type": "boolean", "default": false, - "description": "Do not enable default Cargo features." + "description": "Do not enable default Cargo features.", + "scope": "resource" + }, + "rust.goto_def_racer_fallback": { + "type": "boolean", + "default": false, + "description": "Use racer as a fallback for goto def.", + "scope": "resource" }, "rust.racer_completion": { "type": "boolean", "default": true, - "description": "Enables code completion using racer." + "description": "Enables code completion using racer.", + "scope": "resource" }, "rust.jobs": { "type": [ @@ -297,12 +314,14 @@ "null" ], "default": null, - "description": "Number of Cargo jobs to be run in parallel." + "description": "Number of Cargo jobs to be run in parallel.", + "scope": "resource" }, "rust.all_targets": { "type": "boolean", "default": true, - "description": "Checks the project as if you were running cargo check --all-targets (I.e., check all targets and integration tests too)." + "description": "Checks the project as if you were running cargo check --all-targets (I.e., check all targets and integration tests too).", + "scope": "resource" }, "rust.target_dir": { "type": [ @@ -310,7 +329,8 @@ "null" ], "default": null, - "description": "When specified, it places the generated analysis files at the specified target directory. By default it is placed target/rls directory." + "description": "When specified, it places the generated analysis files at the specified target directory. By default it is placed target/rls directory.", + "scope": "resource" } } } diff --git a/src/configuration.ts b/src/configuration.ts index d6430640..17bc6d91 100644 --- a/src/configuration.ts +++ b/src/configuration.ts @@ -13,7 +13,7 @@ import { workspace, WorkspaceConfiguration } from 'vscode'; import { RevealOutputChannelOn } from 'vscode-languageclient'; -import { getActiveChannel } from './rustup'; +import { getActiveChannel, RustupConfig } from './rustup'; function fromStringToRevealOutputChannelOn(value: string): RevealOutputChannelOn { switch (value && value.toLowerCase()) { @@ -41,19 +41,18 @@ export class RLSConfiguration { */ public readonly rlsPath: string | null; - public static loadFromWorkspace(): RLSConfiguration { + public static loadFromWorkspace(wsPath: string): RLSConfiguration { const configuration = workspace.getConfiguration(); - - return new RLSConfiguration(configuration); + return new RLSConfiguration(configuration, wsPath); } - private constructor(configuration: WorkspaceConfiguration) { + private constructor(configuration: WorkspaceConfiguration, wsPath: string) { this.rustupPath = configuration.get('rust-client.rustupPath', 'rustup'); this.logToFile = configuration.get('rust-client.logToFile', false); this.revealOutputChannelOn = RLSConfiguration.readRevealOutputChannelOn(configuration); this.updateOnStartup = configuration.get('rust-client.updateOnStartup', true); - this.channel = RLSConfiguration.readChannel(this.rustupPath, configuration); + this.channel = RLSConfiguration.readChannel(this.rustupPath, configuration, wsPath); this.componentName = configuration.get('rust-client.rls-name', 'rls'); // Path to the rls. Prefer `rust-client.rlsPath` if present, otherwise consider @@ -68,6 +67,10 @@ export class RLSConfiguration { } } + public rustupConfig(): RustupConfig { + return new RustupConfig(this.channel, this.rustupPath, this.componentName); + } + private static readRevealOutputChannelOn(configuration: WorkspaceConfiguration) { const setting = configuration.get('rust-client.revealOutputChannelOn', 'never'); return fromStringToRevealOutputChannelOn(setting); @@ -78,13 +81,13 @@ export class RLSConfiguration { * falls back on active toolchain specified by rustup (at `rustupPath`), * finally defaulting to `nightly` if all fails. */ - private static readChannel(rustupPath: string, configuration: WorkspaceConfiguration): string { + private static readChannel(rustupPath: string, configuration: WorkspaceConfiguration, wsPath: string): string { const channel = configuration.get('rust-client.channel', null); if (channel !== null) { return channel; } else { try { - return getActiveChannel(rustupPath); + return getActiveChannel(rustupPath, wsPath); } // rustup might not be installed at the time the configuration is // initially loaded, so silently ignore the error and return a default value diff --git a/src/extension.ts b/src/extension.ts index c8b5a701..b6d8f7eb 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -13,273 +13,417 @@ import { runRlsViaRustup, rustupUpdate } from './rustup'; import { startSpinner, stopSpinner } from './spinner'; import { RLSConfiguration } from './configuration'; -import { activateTaskProvider, deactivateTaskProvider } from './tasks'; +import { activateTaskProvider } from './tasks'; import * as child_process from 'child_process'; import * as fs from 'fs'; import { commands, ExtensionContext, IndentAction, languages, TextEditor, - TextEditorEdit, window, workspace } from 'vscode'; + TextEditorEdit, window, workspace, TextDocument, WorkspaceFolder, Disposable, Uri, + WorkspaceFoldersChangeEvent } from 'vscode'; import { LanguageClient, LanguageClientOptions, Location, NotificationType, ServerOptions } from 'vscode-languageclient'; import { execFile, ExecChildProcessResult } from './utils/child_process'; -// FIXME(#233): Don't only rely on lazily initializing it once on startup, -// handle possible `rust-client.*` value changes while extension is running -export const CONFIGURATION = RLSConfiguration.loadFromWorkspace(); +export async function activate(context: ExtensionContext) { + configureLanguage(context); -async function getSysroot(env: Object): Promise { - let output: ExecChildProcessResult; - try { - output = await execFile( - CONFIGURATION.rustupPath, ['run', CONFIGURATION.channel, 'rustc', '--print', 'sysroot'], { env } - ); - } catch (e) { - throw new Error(`Error getting sysroot from \`rustc\`: ${e}`); - } + workspace.onDidOpenTextDocument((doc) => didOpenTextDocument(doc, context)); + workspace.textDocuments.forEach((doc) => didOpenTextDocument(doc, context)); + workspace.onDidChangeWorkspaceFolders((e) => didChangeWorkspaceFolders(e, context)); +} - if (!output.stdout) { - throw new Error(`Couldn't get sysroot from \`rustc\`: Got no ouput`); +export function deactivate(): Promise { + const promises: Thenable[] = []; + for (const ws of workspaces.values()) { + promises.push(ws.stop()); } - - return output.stdout.replace('\n', '').replace('\r', ''); + return Promise.all(promises).then(() => undefined); } -// Make an evironment to run the RLS. -// Tries to synthesise RUST_SRC_PATH for Racer, if one is not already set. -async function makeRlsEnv(setLibPath = false): Promise { - const env = process.env; - - let sysroot: string | undefined; - try { - sysroot = await getSysroot(env); - } catch (err) { - console.info(err.message); - console.info(`Let's retry with extended $PATH`); - env.PATH = `${env.HOME || '~'}/.cargo/bin:${env.PATH || ''}`; - try { - sysroot = await getSysroot(env); - } catch (e) { - console.warn('Error reading sysroot (second try)', e); - window.showWarningMessage('RLS could not set RUST_SRC_PATH for Racer because it could not read the Rust sysroot.'); - } +// Taken from https://github.com/Microsoft/vscode-extension-samples/blob/master/lsp-multi-server-sample/client/src/extension.ts +function didOpenTextDocument(document: TextDocument, context: ExtensionContext): void { + if (document.languageId !== 'rust' && document.languageId !== 'toml') { + return; } - console.info(`Setting sysroot to`, sysroot); - if (!process.env.RUST_SRC_PATH) { - env.RUST_SRC_PATH = sysroot + '/lib/rustlib/src/rust/src'; - } - if (setLibPath) { - env.DYLD_LIBRARY_PATH = sysroot + '/lib'; - env.LD_LIBRARY_PATH = sysroot + '/lib'; + const uri = document.uri; + let folder = workspace.getWorkspaceFolder(uri); + if (!folder) { + window.showWarningMessage('Startup error: the RLS can only operate on a folder, not a single file'); + return; } - return env; -} + folder = getOuterMostWorkspaceFolder(folder); -async function makeRlsProcess(): Promise { - // Allow to override how RLS is started up. - const rls_path = CONFIGURATION.rlsPath; - - let childProcessPromise: Promise; - if (rls_path) { - const env = await makeRlsEnv(true); - console.info('running ' + rls_path); - childProcessPromise = Promise.resolve(child_process.spawn(rls_path, [], { env })); - } else { - const env = await makeRlsEnv(); - console.info('running with rustup'); - childProcessPromise = runRlsViaRustup(env); + if (!workspaces.has(folder.uri.toString())) { + const workspace = new ClientWorkspace(folder); + workspaces.set(folder.uri.toString(), workspace); + workspace.start(context); } - try { - const childProcess = await childProcessPromise; +} - childProcess.on('error', err => { - if ((err).code == 'ENOENT') { - console.error('Could not spawn RLS process: ', err.message); - window.showWarningMessage('Could not start RLS'); - } else { - throw err; +// This is an intermediate, lazy cache used by `getOuterMostWorkspaceFolder` +// and cleared when VSCode workspaces change. +let _sortedWorkspaceFolders: string[] | undefined; + +function sortedWorkspaceFolders(): string[] { + if (!_sortedWorkspaceFolders && workspace.workspaceFolders) { + _sortedWorkspaceFolders = workspace.workspaceFolders.map(folder => { + let result = folder.uri.toString(); + if (result.charAt(result.length - 1) !== '/') { + result = result + '/'; } - }); + return result; + }).sort( + (a, b) => { + return a.length - b.length; + } + ); + } + return _sortedWorkspaceFolders || []; +} - if (CONFIGURATION.logToFile) { - const logPath = workspace.rootPath + '/rls' + Date.now() + '.log'; - const logStream = fs.createWriteStream(logPath, { flags: 'w+' }); - logStream.on('open', function (_f) { - childProcess.stderr.addListener('data', function (chunk) { - logStream.write(chunk.toString()); - }); - }).on('error', function (err: any) { - console.error("Couldn't write to " + logPath + ' (' + err + ')'); - logStream.end(); - }); +function getOuterMostWorkspaceFolder(folder: WorkspaceFolder): WorkspaceFolder { + const sorted = sortedWorkspaceFolders(); + for (const element of sorted) { + let uri = folder.uri.toString(); + if (uri.charAt(uri.length - 1) !== '/') { + uri = uri + '/'; + } + if (uri.startsWith(element)) { + return workspace.getWorkspaceFolder(Uri.parse(element)) || folder; } - - return childProcess; - } catch (e) { - stopSpinner('RLS could not be started'); - throw new Error('Error starting up rls.'); } + return folder; } -let lc: LanguageClient; +function didChangeWorkspaceFolders(e: WorkspaceFoldersChangeEvent, context: ExtensionContext): void { + _sortedWorkspaceFolders = undefined; -export async function activate(context: ExtensionContext) { - const promise = startLanguageClient(context); - configureLanguage(context); - registerCommands(context); - activateTaskProvider(); - await promise; + // If a VSCode workspace has been added, check to see if it is part of an existing one, and + // if not, and it is a Rust project (i.e., has a Cargo.toml), then create a new client. + for (let folder of e.added) { + folder = getOuterMostWorkspaceFolder(folder); + if (workspaces.has(folder.uri.toString())) { + continue; + } + for (const f of fs.readdirSync(folder.uri.fsPath)) { + if (f === 'Cargo.toml') { + const workspace = new ClientWorkspace(folder); + workspace.start(context); + break; + } + } + } + + // If a workspace is removed which is a Rust workspace, kill the client. + for (const folder of e.removed) { + const ws = workspaces.get(folder.uri.toString()); + if (ws) { + workspaces.delete(folder.uri.toString()); + ws.stop(); + } + } } -async function startLanguageClient(context: ExtensionContext) { - if (workspace.rootPath === undefined) { - window.showWarningMessage('Startup error: the RLS can only operate on a folder, not a single file'); - return; +const workspaces: Map = new Map(); + +// We run one RLS and one corresponding language client per workspace folder +// (VSCode workspace, not Cargo workspace). This class contains all the per-client +// and per-workspace stuff. +class ClientWorkspace { + // FIXME(#233): Don't only rely on lazily initializing it once on startup, + // handle possible `rust-client.*` value changes while extension is running + readonly config: RLSConfiguration; + lc: LanguageClient | null = null; + readonly folder: WorkspaceFolder; + taskProvider: Disposable | null = null; + + constructor(folder: WorkspaceFolder) { + this.config = RLSConfiguration.loadFromWorkspace(folder.uri.fsPath); + this.folder = folder; } - // These methods cannot throw an error, so we can drop it. - warnOnMissingCargoToml(); + async start(context: ExtensionContext) { + // These methods cannot throw an error, so we can drop it. + warnOnMissingCargoToml(); - startSpinner('RLS', 'Starting'); + startSpinner('RLS', 'Starting'); - warnOnRlsToml(); - // Check for deprecated env vars. - if (process.env.RLS_PATH || process.env.RLS_ROOT) { - window.showWarningMessage('Found deprecated environment variables (RLS_PATH or RLS_ROOT). Use `rls.path` or `rls.root` settings.'); + this.warnOnRlsToml(); + // Check for deprecated env vars. + if (process.env.RLS_PATH || process.env.RLS_ROOT) { + window.showWarningMessage( + 'Found deprecated environment variables (RLS_PATH or RLS_ROOT). Use `rls.path` or `rls.root` settings.' + ); + } + + const serverOptions: ServerOptions = async () => { + await this.autoUpdate(); + return this.makeRlsProcess(); + }; + const clientOptions: LanguageClientOptions = { + // Register the server for Rust files + documentSelector: [ + { language: 'rust', scheme: 'file', pattern: `${this.folder.uri.fsPath}/**/*` }, + { language: 'rust', scheme: 'untitled', pattern: `${this.folder.uri.fsPath}/**/*` } + ], + synchronize: { configurationSection: 'rust' }, + // Controls when to focus the channel rather than when to reveal it in the drop-down list + revealOutputChannelOn: this.config.revealOutputChannelOn, + initializationOptions: { omitInitBuild: true }, + workspaceFolder: this.folder, + }; + + // Create the language client and start the client. + this.lc = new LanguageClient('Rust Language Server', serverOptions, clientOptions); + + const promise = this.progressCounter(); + + const disposable = this.lc.start(); + context.subscriptions.push(disposable); + + this.taskProvider = activateTaskProvider(this.folder); + this.registerCommands(context); + + return promise; } - const serverOptions: ServerOptions = async () => { - await autoUpdate(); - return makeRlsProcess(); - }; - const clientOptions: LanguageClientOptions = { - // Register the server for Rust files - documentSelector: [ - { language: 'rust', scheme: 'file' }, - { language: 'rust', scheme: 'untitled' } - ], - synchronize: { configurationSection: 'rust' }, - // Controls when to focus the channel rather than when to reveal it in the drop-down list - revealOutputChannelOn: CONFIGURATION.revealOutputChannelOn, - initializationOptions: { omitInitBuild: true }, - }; - - // Create the language client and start the client. - lc = new LanguageClient('Rust Language Server', serverOptions, clientOptions); - - const promise = progressCounter(); - - const disposable = lc.start(); - context.subscriptions.push(disposable); + registerCommands(context: ExtensionContext) { + if (!this.lc) { + return; + } - return promise; -} + const findImplsDisposable = + commands.registerTextEditorCommand( + 'rls.findImpls', + async (textEditor: TextEditor, _edit: TextEditorEdit) => { + if (!this.lc) { + return; + } + await this.lc.onReady(); + + const params = + this.lc + .code2ProtocolConverter + .asTextDocumentPositionParams(textEditor.document, textEditor.selection.active); + let locations: Location[]; + try { + locations = await this.lc.sendRequest('rustDocument/implementations', params); + } catch (reason) { + window.showWarningMessage('find implementations failed: ' + reason); + return; + } + + return commands.executeCommand( + 'editor.action.showReferences', + textEditor.document.uri, + textEditor.selection.active, + locations.map(this.lc.protocol2CodeConverter.asLocation) + ); + } + ); + context.subscriptions.push(findImplsDisposable); + + const rustupUpdateDisposable = commands.registerCommand('rls.update', () => { + return rustupUpdate(this.config.rustupConfig()); + }); + context.subscriptions.push(rustupUpdateDisposable); -export function deactivate(): Promise { - deactivateTaskProvider(); + const restartServer = commands.registerCommand('rls.restart', async () => { + if (this.lc) { + await this.lc.stop(); + } + return this.start(context); + }); + context.subscriptions.push(restartServer); + } - lc.stop(); + async progressCounter() { + if (!this.lc) { + return; + } - return Promise.resolve(); -} + const runningProgress: Set = new Set(); + const asPercent = (fraction: number): string => `${Math.round(fraction * 100)}%`; + let runningDiagnostics = 0; + await this.lc.onReady(); + stopSpinner('RLS'); -async function warnOnMissingCargoToml() { - const files = await workspace.findFiles('Cargo.toml'); + this.lc.onNotification(new NotificationType('window/progress'), function (progress: any) { + if (progress.done) { + runningProgress.delete(progress.id); + } else { + runningProgress.add(progress.id); + } + if (runningProgress.size) { + let status = ''; + if (typeof progress.percentage === 'number') { + status = asPercent(progress.percentage); + } else if (progress.message) { + status = progress.message; + } else if (progress.title) { + status = `[${progress.title.toLowerCase()}]`; + } + startSpinner('RLS', status); + } else { + stopSpinner('RLS'); + } + }); - if (files.length < 1) { - window.showWarningMessage('A Cargo.toml file must be at the root of the workspace in order to support all features'); + // FIXME these are legacy notifications used by RLS ca jan 2018. + // remove once we're certain we've progress on. + this.lc.onNotification(new NotificationType('rustDocument/beginBuild'), function (_f: any) { + runningDiagnostics++; + startSpinner('RLS', 'working'); + }); + this.lc.onNotification(new NotificationType('rustDocument/diagnosticsEnd'), function (_f: any) { + runningDiagnostics--; + if (runningDiagnostics <= 0) { + stopSpinner('RLS'); + } + }); } -} -function warnOnRlsToml() { - const tomlPath = workspace.rootPath + '/rls.toml'; - fs.access(tomlPath, fs.constants.F_OK, (err) => { - if (!err) { - window.showWarningMessage('Found deprecated rls.toml. Use VSCode user settings instead (File > Preferences > Settings)'); + async stop() { + let promise: Thenable = Promise.resolve(void 0); + if (this.lc) { + promise = this.lc.stop(); } - }); -} - -async function autoUpdate() { - if (CONFIGURATION.updateOnStartup) { - await rustupUpdate(); + return promise.then(() => { + if (this.taskProvider) { + this.taskProvider.dispose(); + } + }); } -} -async function progressCounter() { - const runningProgress: Set = new Set(); - const asPercent = (fraction: number): string => `${Math.round(fraction * 100)}%`; - let runningDiagnostics = 0; - await lc.onReady(); - stopSpinner('RLS'); + async getSysroot(env: Object): Promise { + let output: ExecChildProcessResult; + try { + output = await execFile( + this.config.rustupPath, ['run', this.config.channel, 'rustc', '--print', 'sysroot'], { env } + ); + } catch (e) { + throw new Error(`Error getting sysroot from \`rustc\`: ${e}`); + } - lc.onNotification(new NotificationType('window/progress'), function (progress: any) { - if (progress.done) { - runningProgress.delete(progress.id); - } else { - runningProgress.add(progress.id); + if (!output.stdout) { + throw new Error(`Couldn't get sysroot from \`rustc\`: Got no ouput`); } - if (runningProgress.size) { - let status = ''; - if (typeof progress.percentage === 'number') { - status = asPercent(progress.percentage); - } else if (progress.message) { - status = progress.message; - } else if (progress.title) { - status = `[${progress.title.toLowerCase()}]`; + + return output.stdout.replace('\n', '').replace('\r', ''); + } + + // Make an evironment to run the RLS. + // Tries to synthesise RUST_SRC_PATH for Racer, if one is not already set. + async makeRlsEnv(setLibPath = false): Promise { + const env = process.env; + + let sysroot: string | undefined; + try { + sysroot = await this.getSysroot(env); + } catch (err) { + console.info(err.message); + console.info(`Let's retry with extended $PATH`); + env.PATH = `${env.HOME || '~'}/.cargo/bin:${env.PATH || ''}`; + try { + sysroot = await this.getSysroot(env); + } catch (e) { + console.warn('Error reading sysroot (second try)', e); + window.showWarningMessage( + 'RLS could not set RUST_SRC_PATH for Racer because it could not read the Rust sysroot.' + ); } - startSpinner('RLS', status); - } else { - stopSpinner('RLS'); } - }); - // FIXME these are legacy notifications used by RLS ca jan 2018. - // remove once we're certain we've progress on. - lc.onNotification(new NotificationType('rustDocument/beginBuild'), function (_f: any) { - runningDiagnostics++; - startSpinner('RLS', 'working'); - }); - lc.onNotification(new NotificationType('rustDocument/diagnosticsEnd'), function (_f: any) { - runningDiagnostics--; - if (runningDiagnostics <= 0) { - stopSpinner('RLS'); + console.info(`Setting sysroot to`, sysroot); + if (!process.env.RUST_SRC_PATH) { + env.RUST_SRC_PATH = sysroot + '/lib/rustlib/src/rust/src'; + } + if (setLibPath) { + env.DYLD_LIBRARY_PATH = sysroot + '/lib'; + env.LD_LIBRARY_PATH = sysroot + '/lib'; } - }); -} -function registerCommands(context: ExtensionContext) { - const findImplsDisposable = commands.registerTextEditorCommand('rls.findImpls', async (textEditor: TextEditor, _edit: TextEditorEdit) => { - await lc.onReady(); + return env; + } - const params = lc.code2ProtocolConverter.asTextDocumentPositionParams(textEditor.document, textEditor.selection.active); - let locations: Location[]; + async makeRlsProcess(): Promise { + // Allow to override how RLS is started up. + const rls_path = this.config.rlsPath; + + let childProcessPromise: Promise; + if (rls_path) { + const env = await this.makeRlsEnv(true); + console.info('running ' + rls_path); + childProcessPromise = Promise.resolve(child_process.spawn(rls_path, [], { env })); + } else { + const env = await this.makeRlsEnv(); + console.info('running with rustup'); + childProcessPromise = runRlsViaRustup(env, this.config.rustupConfig()); + } try { - locations = await lc.sendRequest('rustDocument/implementations', params); - } catch (reason) { - window.showWarningMessage('find implementations failed: ' + reason); - return; + const childProcess = await childProcessPromise; + + childProcess.on('error', err => { + if ((err).code == 'ENOENT') { + console.error('Could not spawn RLS process: ', err.message); + window.showWarningMessage('Could not start RLS'); + } else { + throw err; + } + }); + + if (this.config.logToFile) { + const logPath = this.folder.uri.path + '/rls' + Date.now() + '.log'; + const logStream = fs.createWriteStream(logPath, { flags: 'w+' }); + logStream.on('open', function (_f) { + childProcess.stderr.addListener('data', function (chunk) { + logStream.write(chunk.toString()); + }); + }).on('error', function (err: any) { + console.error("Couldn't write to " + logPath + ' (' + err + ')'); + logStream.end(); + }); + } + + return childProcess; + } catch (e) { + stopSpinner('RLS could not be started'); + throw new Error('Error starting up rls.'); } + } + async autoUpdate() { + if (this.config.updateOnStartup) { + await rustupUpdate(this.config.rustupConfig()); + } + } - return commands.executeCommand('editor.action.showReferences', textEditor.document.uri, textEditor.selection.active, locations.map(lc.protocol2CodeConverter.asLocation)); - }); - context.subscriptions.push(findImplsDisposable); + warnOnRlsToml() { + const tomlPath = this.folder.uri.path + '/rls.toml'; + fs.access(tomlPath, fs.constants.F_OK, (err) => { + if (!err) { + window.showWarningMessage( + 'Found deprecated rls.toml. Use VSCode user settings instead (File > Preferences > Settings)' + ); + } + }); + } +} - const rustupUpdateDisposable = commands.registerCommand('rls.update', () => { - return rustupUpdate(); - }); - context.subscriptions.push(rustupUpdateDisposable); +async function warnOnMissingCargoToml() { + const files = await workspace.findFiles('Cargo.toml'); - const restartServer = commands.registerCommand('rls.restart', async () => { - await lc.stop(); - return startLanguageClient(context); - }); - context.subscriptions.push(restartServer); + if (files.length < 1) { + window.showWarningMessage( + 'A Cargo.toml file must be at the root of the workspace in order to support all features' + ); + } } + function configureLanguage(context: ExtensionContext) { const disposable = languages.setLanguageConfiguration('rust', { onEnterRules: [ diff --git a/src/rustup.ts b/src/rustup.ts index f7cac50d..c6c38183 100644 --- a/src/rustup.ts +++ b/src/rustup.ts @@ -11,26 +11,37 @@ 'use strict'; import * as child_process from 'child_process'; -import { window, workspace } from 'vscode'; +import { window } from 'vscode'; import { execChildProcess } from './utils/child_process'; import { startSpinner, stopSpinner } from './spinner'; -import { CONFIGURATION } from './extension'; + +export class RustupConfig { + channel: string; + path: string; + componentName: string; + + constructor(channel: string, path: string, componentName: string) { + this.channel = channel; + this.path = path; + this.componentName = componentName; + } +} // This module handles running the RLS via rustup, including checking that rustup // is installed and installing any required components/toolchains. -export async function runRlsViaRustup(env: any): Promise { - await ensureToolchain(); - await checkForRls(); - return child_process.spawn(CONFIGURATION.rustupPath, ['run', CONFIGURATION.channel, 'rls'], { env }); +export async function runRlsViaRustup(env: any, config: RustupConfig): Promise { + await ensureToolchain(config); + await checkForRls(config); + return child_process.spawn(config.path, ['run', config.channel, 'rls'], { env }); } -export async function rustupUpdate() { +export async function rustupUpdate(config: RustupConfig) { startSpinner('RLS', 'Updating…'); try { - const { stdout } = await execChildProcess(CONFIGURATION.rustupPath + ' update'); + const { stdout } = await execChildProcess(config.path + ' update'); // This test is imperfect because if the user has multiple toolchains installed, they // might have one updated and one unchanged. But I don't want to go too far down the // rabbit hole of parsing rustup's output. @@ -46,25 +57,25 @@ export async function rustupUpdate() { } // Check for the nightly toolchain (and that rustup exists) -async function ensureToolchain(): Promise { - const toolchainInstalled = await hasToolchain(); +async function ensureToolchain(config: RustupConfig): Promise { + const toolchainInstalled = await hasToolchain(config); if (toolchainInstalled) { return; } - const clicked = await window.showInformationMessage(CONFIGURATION.channel + ' toolchain not installed. Install?', 'Yes'); + const clicked = await window.showInformationMessage(config.channel + ' toolchain not installed. Install?', 'Yes'); if (clicked === 'Yes') { - await tryToInstallToolchain(); + await tryToInstallToolchain(config); } else { throw new Error(); } } -async function hasToolchain(): Promise { +async function hasToolchain(config: RustupConfig): Promise { try { - const { stdout } = await execChildProcess(CONFIGURATION.rustupPath + ' toolchain list'); - const hasToolchain = stdout.indexOf(CONFIGURATION.channel) > -1; + const { stdout } = await execChildProcess(config.path + ' toolchain list'); + const hasToolchain = stdout.indexOf(config.channel) > -1; return hasToolchain; } catch (e) { @@ -75,25 +86,25 @@ async function hasToolchain(): Promise { } } -async function tryToInstallToolchain(): Promise { +async function tryToInstallToolchain(config: RustupConfig): Promise { startSpinner('RLS', 'Installing toolchain…'); try { - const { stdout, stderr } = await execChildProcess(CONFIGURATION.rustupPath + ' toolchain install ' + CONFIGURATION.channel); + const { stdout, stderr } = await execChildProcess(config.path + ' toolchain install ' + config.channel); console.log(stdout); console.log(stderr); - stopSpinner(CONFIGURATION.channel + ' toolchain installed successfully'); + stopSpinner(config.channel + ' toolchain installed successfully'); } catch (e) { console.log(e); - window.showErrorMessage('Could not install ' + CONFIGURATION.channel + ' toolchain'); - stopSpinner('Could not install ' + CONFIGURATION.channel + ' toolchain'); + window.showErrorMessage('Could not install ' + config.channel + ' toolchain'); + stopSpinner('Could not install ' + config.channel + ' toolchain'); throw e; } } // Check for rls components. -async function checkForRls(): Promise { - const hasRls = await hasRlsComponents(); +async function checkForRls(config: RustupConfig): Promise { + const hasRls = await hasRlsComponents(config); if (hasRls) { return; } @@ -101,17 +112,17 @@ async function checkForRls(): Promise { // missing component const clicked = await Promise.resolve(window.showInformationMessage('RLS not installed. Install?', 'Yes')); if (clicked === 'Yes') { - await installRls(); + await installRls(config); } else { throw new Error(); } } -async function hasRlsComponents(): Promise { +async function hasRlsComponents(config: RustupConfig): Promise { try { - const { stdout } = await execChildProcess(CONFIGURATION.rustupPath + ' component list --toolchain ' + CONFIGURATION.channel); - const componentName = new RegExp('^' + CONFIGURATION.componentName + '.* \\((default|installed)\\)$', 'm'); + const { stdout } = await execChildProcess(config.path + ' component list --toolchain ' + config.channel); + const componentName = new RegExp('^' + config.componentName + '.* \\((default|installed)\\)$', 'm'); if ( stdout.search(componentName) === -1 || stdout.search(/^rust-analysis.* \((default|installed)\)$/m) === -1 || @@ -130,12 +141,12 @@ async function hasRlsComponents(): Promise { } } -async function installRls(): Promise { +async function installRls(config: RustupConfig): Promise { startSpinner('RLS', 'Installing components…'); const tryFn: (component: string) => Promise<(Error | null)> = async (component: string) => { try { - const { stdout, stderr, } = await execChildProcess(CONFIGURATION.rustupPath + ` component add ${component} --toolchain ` + CONFIGURATION.channel); + const { stdout, stderr, } = await execChildProcess(config.path + ` component add ${component} --toolchain ` + config.channel); console.log(stdout); console.log(stderr); return null; @@ -166,7 +177,7 @@ async function installRls(): Promise { console.log('install rls'); { - const e = await tryFn(CONFIGURATION.componentName); + const e = await tryFn(config.componentName); if (e !== null) { stopSpinner('Could not install RLS'); throw e; @@ -204,11 +215,11 @@ export function parseActiveToolchain(rustupOutput: string): string { * Returns active (including local overrides) toolchain, as specified by rustup. * May throw if rustup at specified path can't be executed. */ -export function getActiveChannel(rustupPath: string, cwd = workspace.rootPath): string { +export function getActiveChannel(rustupPath: string, wsPath: string): string { // rustup info might differ depending on where it's executed // (e.g. when a toolchain is locally overriden), so executing it // under our current workspace root should give us close enough result - const output = child_process.execSync(`${rustupPath} show`, { cwd: cwd }).toString(); + const output = child_process.execSync(`${rustupPath} show`, { cwd: wsPath }).toString(); const activeChannel = parseActiveToolchain(output); console.info(`Detected active channel: ${activeChannel} (since 'rust-client.channel' is unspecified)`); diff --git a/src/tasks.ts b/src/tasks.ts index 610f3cb8..101264e9 100644 --- a/src/tasks.ts +++ b/src/tasks.ts @@ -20,36 +20,24 @@ import { ShellExecution, ShellExecutionOptions, workspace, + WorkspaceFolder, } from 'vscode'; -let taskProvider: Disposable | null = null; - -export function activateTaskProvider(): void { - if (taskProvider !== null) { - console.log('the task provider has been activated'); - return; - } - +export function activateTaskProvider(target: WorkspaceFolder): Disposable { const provider: TaskProvider = { provideTasks: function () { // npm or others parse their task definitions. So they need to provide 'autoDetect' feature. // e,g, https://github.com/Microsoft/vscode/blob/de7e216e9ebcad74f918a025fc5fe7bdbe0d75b2/extensions/npm/src/main.ts // However, cargo.toml does not support to define a new task like them. // So we are not 'autoDetect' feature and the setting for it. - return getCargoTasks(); + return getCargoTasks(target); }, resolveTask(_task: Task): Task | undefined { return undefined; } }; - taskProvider = workspace.registerTaskProvider('rust', provider); -} - -export function deactivateTaskProvider(): void { - if (taskProvider !== null) { - taskProvider.dispose(); - } + return workspace.registerTaskProvider('rust', provider); } interface CargoTaskDefinition extends TaskDefinition { @@ -67,32 +55,27 @@ interface TaskConfigItem { presentationOptions?: TaskPresentationOptions; } -function getCargoTasks(): Array { +function getCargoTasks(target: WorkspaceFolder): Array { const taskList = createTaskConfigItem(); - const rootPath = workspace.rootPath; - if (rootPath === undefined) { - return []; - } - const list = taskList.map((def) => { - const t = createTask(rootPath, def); + const t = createTask(def, target); return t; }); return list; } -function createTask(rootPath: string, { definition, group, presentationOptions, problemMatcher }: TaskConfigItem): Task { +function createTask({ definition, group, presentationOptions, problemMatcher }: TaskConfigItem, target: WorkspaceFolder): Task { const TASK_SOURCE = 'Rust'; const execCmd = `${definition.command} ${definition.args.join(' ')}`; const execOption: ShellExecutionOptions = { - cwd: rootPath, + cwd: target.uri.path, }; const exec = new ShellExecution(execCmd, execOption); - const t = new Task(definition, definition.label, TASK_SOURCE, exec, problemMatcher); + const t = new Task(definition, target, definition.label, TASK_SOURCE, exec, problemMatcher); if (group !== undefined) { t.group = group;