diff --git a/doc/main.md b/doc/main.md index 33b28db..cc64587 100644 --- a/doc/main.md +++ b/doc/main.md @@ -11,6 +11,11 @@ The extension can function in one of two modes: * [Legacy Mode](legacy_mode/main.md) * [Rust Language Server Mode](rls_mode/main.md) +The first mode is called *Legacy* because this mode does its best, but the second one is better. +The second one is recommended and at some point the first one will be removed. + +But Legacy Mode should work just fine and if it doesn't, open an issue. + Each mode is described in detail on its own page. Furthermore, the extension provides: diff --git a/doc/rls_mode/main.md b/doc/rls_mode/main.md index fa4e5ec..3d9d722 100644 --- a/doc/rls_mode/main.md +++ b/doc/rls_mode/main.md @@ -28,6 +28,20 @@ The possible values are: ## Setting up +The recommended way to set RLS up is using rustup. You should use rustup unless rustup does not suit you. + +If you can't answer if rustup suits you, then it suits you. + +### Using rustup + +If rustup is installed on your computer, then when the extension activates it checks if RLS is installed and if it is not, then the extension asks your permission to update rustup and install RLS. + +If you agree with this, the extension will do it and start itself in RLS mode. + +You don't have to specify either settings to make RLS work because the extension will do it automatically. + +### Using source + First of all, you have to download the [RLS](https://github.com/rust-lang-nursery/rls) sources: ```bash diff --git a/package.json b/package.json index 7d5439f..a5648a4 100644 --- a/package.json +++ b/package.json @@ -512,11 +512,13 @@ "typescript": "2.2.1", "vscode": "1.1.0", "@types/node": "7.0.11", - "@types/mocha": "2.2.40" + "@types/mocha": "2.2.40", + "@types/open": "0.0.29" }, "dependencies": { "expand-tilde": "2.0.2", "tmp": "0.0.31", + "open": "0.0.5", "tree-kill": "1.1.0", "find-up": "2.1.0", "elegant-spinner": "1.0.1", diff --git a/src/components/completion/completion_manager.ts b/src/components/completion/completion_manager.ts index d7d3615..30cd07d 100644 --- a/src/components/completion/completion_manager.ts +++ b/src/components/completion/completion_manager.ts @@ -33,6 +33,8 @@ import { fileSync } from 'tmp'; import { Configuration } from '../configuration/Configuration'; +import { Rustup } from '../configuration/Rustup'; + import getDocumentFilter from '../configuration/mod'; import ChildLogger from '../logging/child_logger'; @@ -127,9 +129,7 @@ export default class CompletionManager { return true; } - const rustcSysRoot = this.configuration.getRustcSysRoot(); - - if (rustcSysRoot && rustcSysRoot.includes('.rustup')) { + if (this.configuration.getRustInstallation() instanceof Rustup) { // tslint:disable-next-line const message = 'You are using rustup, but don\'t have installed source code. Do you want to install it?'; window.showErrorMessage(message, 'Yes').then(chosenItem => { diff --git a/src/components/configuration/Configuration.ts b/src/components/configuration/Configuration.ts index e8af1c1..f438779 100644 --- a/src/components/configuration/Configuration.ts +++ b/src/components/configuration/Configuration.ts @@ -1,9 +1,5 @@ import { SpawnOptions } from 'child_process'; -import { access } from 'fs'; - -import { join } from 'path'; - import { WorkspaceConfiguration, workspace } from 'vscode'; import { RevealOutputChannelOn } from 'vscode-languageclient'; @@ -12,9 +8,15 @@ import expandTilde = require('expand-tilde'); import { OutputtingProcess } from '../../OutputtingProcess'; -export interface RlsConfiguration { - executable: string; +import { FileSystem } from '../file_system/FileSystem'; + +import ChildLogger from '../logging/child_logger'; + +import { Rustup } from './Rustup'; + +import { NotRustup } from './NotRustup'; +export interface RlsConfiguration { args?: string[]; env?: any; @@ -28,62 +30,172 @@ export enum ActionOnStartingCommandIfThereIsRunningCommand { ShowDialogToLetUserDecide } +/** + * The main class of the component `Configuration`. + * This class contains code related to Configuration + */ export class Configuration { - private rustcSysRoot: string | undefined; + private logger: ChildLogger; - private rustSourcePath: string | undefined; + private rustInstallation: Rustup | NotRustup | undefined; - public static async create(): Promise { - const rustcSysRoot = await this.loadRustcSysRoot(); + /** + * A path to Rust's source code specified by a user. + * It contains a value of either: + * - the configuration parameter `rust.rustLangSrcPath` + * - the environment variable `RUST_SRC_PATH` + * The path has higher priority than a path to Rust's source code contained within an installation + */ + private pathToRustSourceCodeSpecifiedByUser: string | undefined; - const rustSourcePath = await this.loadRustSourcePath(rustcSysRoot); + /** + * A path to the executable of RLS specified by a user. + * A user can specify it via the configuration parameter `rust.rls.executable` + * The path has higher priority than a path found automatically + */ + private pathToRlsSpecifiedByUser: string | undefined; - return new Configuration(rustcSysRoot, rustSourcePath); + /** + * Creates a new instance of the class. + * This method is asynchronous because it works with the file system + * @param logger a logger to log messages + */ + public static async create(logger: ChildLogger): Promise { + const rustcSysRoot: string | undefined = await this.loadRustcSysRoot(); + + const createRustInstallationPromise = async () => { + if (!rustcSysRoot) { + return undefined; + } + + if (Rustup.doesManageRustcSysRoot(rustcSysRoot)) { + return await Rustup.create(logger.createChildLogger('Rustup: '), rustcSysRoot); + } else { + return new NotRustup(rustcSysRoot); + } + }; + + const rustInstallation: Rustup | NotRustup | undefined = await createRustInstallationPromise(); + + const pathToRustSourceCodeSpecifiedByUser = await this.checkPathToRustSourceCodeSpecifiedByUser(); + + const configuration = new Configuration( + logger, + rustInstallation, + pathToRustSourceCodeSpecifiedByUser, + undefined + ); + + configuration.updatePathToRlsExecutableSpecifiedByUser(); + + return configuration; } - public getRlsConfiguration(): RlsConfiguration | undefined { - const configuration = Configuration.getConfiguration(); + /** + * Returns either a path to the executable of RLS or undefined + */ + public getPathToRlsExecutable(): string | undefined { + if (this.pathToRlsSpecifiedByUser) { + return this.pathToRlsSpecifiedByUser; + } + + if (this.rustInstallation instanceof Rustup) { + const pathToRlsExecutable = this.rustInstallation.getPathToRlsExecutable(); + + if (pathToRlsExecutable) { + return pathToRlsExecutable; + } + } + + return undefined; + } - const rlsConfiguration: any | null = configuration['rls']; + /** + * Returns a list of arguments to spawn RLS with + * Possible values are: + * * A list of arguments specified by a user with the configuration parameter `rust.rls.args` + * * undefined + */ + public getRlsArgs(): string[] | undefined { + const rlsConfiguration = this.getRlsConfiguration(); if (!rlsConfiguration) { return undefined; } - const executable: string = rlsConfiguration.executable; - const args: string[] | null = rlsConfiguration.args; - const env: any | null = rlsConfiguration.env; - const revealOutputChannelOn: string = rlsConfiguration.revealOutputChannelOn; + const rlsArgs = rlsConfiguration.args; + + return rlsArgs; + } - let revealOutputChannelOnEnum: RevealOutputChannelOn; + /** + * Returns an object representing an environment to run RLS in. + * Possible values are: + * * A value of the configuration parameter `rust.rls.env` + * * An empty object + * This method also tries to set RUST_SRC_PATH for any possible value + */ + public getRlsEnv(): object { + const rlsConfiguration: any | undefined = this.getRlsConfiguration(); + + let rlsEnv: any = {}; + + if (rlsConfiguration) { + const rlsEnvSpecifiedByUser = rlsConfiguration.env; + + if (rlsEnvSpecifiedByUser) { + rlsEnv = rlsEnvSpecifiedByUser; + } + } + + if (!rlsEnv.RUST_SRC_PATH) { + rlsEnv.RUST_SRC_PATH = this.getRustSourcePath(); + } + + return rlsEnv; + } + + /** + * Returns a mode specifying for which kinds of messages the RLS output channel should be revealed + * The possible values are (the higher the greater priority): + * * A value specified by a user with the configuration parameter `rust.rls.revealOutputChannelOn` + * * A default value which is on error + */ + public getRlsRevealOutputChannelOn(): RevealOutputChannelOn { + const rlsConfiguration: any | undefined = this.getRlsConfiguration(); - switch (revealOutputChannelOn) { + const defaultValue = RevealOutputChannelOn.Error; + + if (!rlsConfiguration) { + return defaultValue; + } + + const valueSpecifiedByUser = rlsConfiguration.revealOutputChannelOn; + + switch (valueSpecifiedByUser) { case 'info': - revealOutputChannelOnEnum = RevealOutputChannelOn.Info; - break; + return RevealOutputChannelOn.Info; case 'warn': - revealOutputChannelOnEnum = RevealOutputChannelOn.Warn; - break; + return RevealOutputChannelOn.Warn; case 'error': - revealOutputChannelOnEnum = RevealOutputChannelOn.Error; - break; + return RevealOutputChannelOn.Error; case 'never': - revealOutputChannelOnEnum = RevealOutputChannelOn.Never; - break; + return RevealOutputChannelOn.Never; default: - revealOutputChannelOnEnum = RevealOutputChannelOn.Error; + return defaultValue; } + } - return { - executable, - args: args !== null ? args : undefined, - env: env !== null ? env : undefined, - revealOutputChannelOn: revealOutputChannelOnEnum - }; + private getRlsConfiguration(): any | undefined { + const configuration = Configuration.getConfiguration(); + + const rlsConfiguration: any = configuration['rls']; + + return rlsConfiguration; } public shouldExecuteCargoCommandInTerminal(): boolean { @@ -105,8 +217,8 @@ export class Configuration { return actionOnSave; } - public getRustcSysRoot(): string | undefined { - return this.rustcSysRoot; + public getRustInstallation(): Rustup | NotRustup | undefined { + return this.rustInstallation; } public shouldShowRunningCargoTaskOutputChannel(): boolean { @@ -164,7 +276,15 @@ export class Configuration { } public getRustSourcePath(): string | undefined { - return this.rustSourcePath; + if (this.pathToRustSourceCodeSpecifiedByUser) { + return this.pathToRustSourceCodeSpecifiedByUser; + } + + if (this.rustInstallation instanceof Rustup) { + return this.rustInstallation.getPathToRustSourceCode(); + } + + return undefined; } public getActionOnStartingCommandIfThereIsRunningCommand(): ActionOnStartingCommandIfThereIsRunningCommand { @@ -207,63 +327,117 @@ export class Configuration { } /** - * Loads the path of the Rust's source code. - * It tries to load from different places. + * Checks if a user specified a path to Rust's source code in the configuration and if it is, checks if the specified path does really exist + * @return Promise which after resolving contains either a path if the path suits otherwise undefined + */ + private static async checkPathToRustSourceCodeSpecifiedByUserInConfiguration(): Promise { + let configPath: string | undefined = this.getPathConfigParameter('rustLangSrcPath'); + + if (configPath) { + const configPathExists: boolean = await FileSystem.doesFileOrDirectoryExists(configPath); + + if (!configPathExists) { + configPath = undefined; + } + } + + return configPath; + } + + /** + * Tries to find a path to Rust's source code specified by a user. + * The method is asynchronous because it checks if a directory-candidate exists + * It tries to find it in different places. * These places sorted by priority (the first item has the highest priority): * * User/Workspace configuration * * Environment - * * Rustup */ - private static async loadRustSourcePath(rustcSysRoot: string | undefined): Promise { - const configPath: string | undefined = this.getPathConfigParameter('rustLangSrcPath'); - - const configPathExists: boolean = configPath !== undefined && await this.checkPathExists(configPath); + private static async checkPathToRustSourceCodeSpecifiedByUser(): Promise { + const configPath: string | undefined = await this.checkPathToRustSourceCodeSpecifiedByUserInConfiguration(); - if (configPathExists) { + if (configPath) { return configPath; } const envPath: string | undefined = this.getPathEnvParameter('RUST_SRC_PATH'); - const envPathExists: boolean = envPath !== undefined && await this.checkPathExists(envPath); + const envPathExists: boolean = envPath !== undefined && await FileSystem.doesFileOrDirectoryExists(envPath); if (envPathExists) { return envPath; + } else { + return undefined; } + } - if (!rustcSysRoot) { - return undefined; + /** + * Checks if a user specified a path to the executable of RLS via the configuration parameter. + * It assigns either a path specified by a user or undefined, depending on if a user specified a path and the specified path exists. + * This method is asynchronous because it checks if a path specified by a user exists + */ + private async updatePathToRlsExecutableSpecifiedByUser(): Promise { + function getRlsExecutable(): string | undefined { + const configuration = Configuration.getConfiguration(); + + const rlsConfiguration = configuration['rls']; + + if (!rlsConfiguration) { + return undefined; + } + + const pathToRlsExecutable = rlsConfiguration.executable; + + if (!pathToRlsExecutable) { + return undefined; + } + + return expandTilde(pathToRlsExecutable); } - if (!rustcSysRoot.includes('.rustup')) { - return undefined; + const logger = this.logger.createChildLogger('updatePathToRlsSpecifiedByUser: '); + + const pathToRlsExecutable: string | undefined = getRlsExecutable(); + + if (!pathToRlsExecutable) { + this.pathToRlsSpecifiedByUser = undefined; + + return; } - const rustupPath: string = join(rustcSysRoot, 'lib', 'rustlib', 'src', 'rust', 'src'); + const doesPathToRlsExecutableExist: boolean = await FileSystem.doesFileOrDirectoryExists(pathToRlsExecutable); - const rustupPathExists: boolean = await this.checkPathExists(rustupPath); + if (!doesPathToRlsExecutableExist) { + logger.error(`The specified path does not exist. Path=${pathToRlsExecutable}`); - if (rustupPathExists) { - return rustupPath; - } else { - return undefined; + this.pathToRlsSpecifiedByUser = undefined; + + return; } + + this.pathToRlsSpecifiedByUser = pathToRlsExecutable; } - private static checkPathExists(path: string): Promise { - return new Promise(resolve => { - access(path, err => { - const pathExists = !err; + /** + * Creates a new instance of the class. + * The constructor is private because creating a new instance should be done via the method `create` + * @param logger A value for the field `logger` + * @param rustInstallation A value for the field `rustInstallation` + * @param pathToRustSourceCodeSpecifiedByUser A value for the field `pathToRustSourceCodeSpecifiedByUser` + * @param pathToRlsSpecifiedByUser A value for the field `pathToRlsSpecifiedByUser` + */ + private constructor( + logger: ChildLogger, + rustInstallation: Rustup | NotRustup | undefined, + pathToRustSourceCodeSpecifiedByUser: string | undefined, + pathToRlsSpecifiedByUser: string | undefined + ) { + this.logger = logger; - resolve(pathExists); - }); - }); - } + this.rustInstallation = rustInstallation; - private constructor(rustcSysRoot: string | undefined, rustSourcePath: string | undefined) { - this.rustcSysRoot = rustcSysRoot; + this.pathToRustSourceCodeSpecifiedByUser = pathToRustSourceCodeSpecifiedByUser; - this.rustSourcePath = rustSourcePath; + this.pathToRlsSpecifiedByUser = pathToRlsSpecifiedByUser; } private static getStringParameter(parameterName: string): string | null { diff --git a/src/components/configuration/NotRustup.ts b/src/components/configuration/NotRustup.ts new file mode 100644 index 0000000..ef32789 --- /dev/null +++ b/src/components/configuration/NotRustup.ts @@ -0,0 +1,21 @@ +/** + * Configuration of Rust installed not via Rustup, but via other variant + */ +export class NotRustup { + /** + * A path to Rust's installation root. + * It is what `rustc --print=sysroot` returns + */ + private rustcSysRoot: string; + + public constructor(rustcSysRoot: string) { + this.rustcSysRoot = rustcSysRoot; + } + + /** + * Returns Rust's installation root + */ + public getRustcSysRoot(): string { + return this.rustcSysRoot; + } +} diff --git a/src/components/configuration/Rustup.ts b/src/components/configuration/Rustup.ts new file mode 100644 index 0000000..03bd392 --- /dev/null +++ b/src/components/configuration/Rustup.ts @@ -0,0 +1,350 @@ +import { join } from 'path'; + +// import { EOL } from 'os'; + +import { OutputtingProcess } from '../../OutputtingProcess'; + +import { FileSystem } from '../file_system/FileSystem'; + +import ChildLogger from '../logging/child_logger'; + +/** + * Configuration of Rust installed via Rustup + */ +export class Rustup { + /** + * A logger to log messages + */ + private logger: ChildLogger; + + /** + * A path to Rust's installation root. + * It is what `rustc --print=sysroot` returns. + */ + private pathToRustcSysRoot: string; + + /** + * A path to Rust's source code. + * It can be undefined if the component "rust-src" is not installed + */ + private pathToRustSourceCode: string | undefined; + + /** + * A path to the executable of RLS. + * It can be undefined if the component "rls" is not installed + */ + private pathToRlsExecutable: string | undefined; + + /** + * Checks if Rustup manages a specified Rust's installation root + * @param rustcSysRoot a path to Rust's installation root to check + * @returns true if Rustup manages it otherwire false + */ + public static doesManageRustcSysRoot(pathToRustcSysRoot: string): boolean { + // It can be inaccurate since nobody can stop a user from installing Rust not via Rustup, but to `.rustup` directory + return pathToRustcSysRoot.includes('.rustup'); + } + + /** + * Creates a new instance of the class. + * The method is asynchronous because it tries to find Rust's source code + * @param pathToRustcSysRoot A path to Rust's installation root + */ + public static async create(logger: ChildLogger, pathToRustcSysRoot: string): Promise { + const rustup = new Rustup(logger, pathToRustcSysRoot, undefined, undefined); + + await rustup.updatePathToRustSourceCodePath(); + + await rustup.updatePathToRlsExecutable(); + + return rustup; + } + + /** + * Returns the path to Rust's installation root + */ + public getPathToRustcSysRoot(): string { + return this.pathToRustcSysRoot; + } + + /** + * Returns the path to Rust's source code + */ + public getPathToRustSourceCode(): string | undefined { + return this.pathToRustSourceCode; + } + + /** + * Returns either the path to the executable of RLS or undefined + */ + public getPathToRlsExecutable(): string | undefined { + return this.pathToRlsExecutable; + } + + /** + * Requests Rustup update + * @return true if no error occurred otherwise false + */ + public async update(): Promise { + const args = ['self', 'update']; + + const stdoutData: string | undefined = await this.invokeRustup(args); + + if (stdoutData === undefined) { + return false; + } + + return true; + } + + /** + * Requests Rustup install RLS + * @return true if no error occurred and RLS has been installed otherwise false + */ + public async installRls(): Promise { + const logger = this.logger.createChildLogger('installRls: '); + + if (this.pathToRlsExecutable) { + logger.error('RLS is already installed. The method should not have been called'); + + // We return true because RLS is installed, but anyway it is an exceptional situation + return true; + } + + const args = [ + 'component', + 'add', + Rustup.getRlsComponentName() + ]; + + const stdoutData: string | undefined = await this.invokeRustup(args); + + // Some error occurred. It is already logged in the method invokeRustup. + // So we just need to notify a caller that the installation failed + if (stdoutData === undefined) { + return false; + } + + // We need to update the field + await this.updatePathToRlsExecutable(); + + if (!this.pathToRlsExecutable) { + logger.createChildLogger('RLS had been installed successfully, but then Rustup reported that RLS was not installed. This should have not happened'); + + return false; + } + + return true; + } + + /** + * Checks if Rust's source code is installed at the expected path. + * This method assigns either the expected path or undefined to the field `pathToRustSourceCode`, depending on if the expected path exists. + * The method is asynchronous because it checks if the expected path exists + */ + public async updatePathToRustSourceCodePath(): Promise { + const pathToRustSourceCode = join(this.pathToRustcSysRoot, 'lib', 'rustlib', 'src', 'rust', 'src'); + + const isRustSourceCodeInstalled: boolean = await FileSystem.doesFileOrDirectoryExists(pathToRustSourceCode); + + if (isRustSourceCodeInstalled) { + this.pathToRustSourceCode = pathToRustSourceCode; + } else { + this.pathToRustSourceCode = undefined; + } + } + + /** + * Checks if the executable of RLS is installed. + * This method assigns either a path to the executable or undefined to the field `pathToRlsExecutable`, depending on if the executable is found + * This method is asynchronous because it checks if the executable exists + */ + public async updatePathToRlsExecutable(): Promise { + const logger = this.logger.createChildLogger('updatePathToRlsExecutable: '); + + const installedComponents: string[] | undefined = await this.getInstalledComponents(); + + if (!installedComponents) { + this.pathToRlsExecutable = undefined; + + return; + } + + const rlsComponent = installedComponents.find(component => { + return component.startsWith(Rustup.getRlsComponentName()); + }); + + const isRlsInstalled = rlsComponent !== undefined; + + if (!isRlsInstalled) { + logger.debug('RLS is not installed'); + + this.pathToRlsExecutable = undefined; + + return; + } + + const pathToRlsExecutable: string | undefined = await FileSystem.findExecutablePath(Rustup.getRlsComponentName()); + + if (!pathToRlsExecutable) { + // RLS is installed via Rustup, but isn't found. Let a user know about it + logger.error(`Rustup had reported that RLS had been installed, but RLS wasn't found in PATH=${process.env.PATH}`); + + this.pathToRlsExecutable = undefined; + + return; + } + + this.pathToRlsExecutable = pathToRlsExecutable; + + logger.debug(`RLS is installed. Path=${this.pathToRlsExecutable}`); + } + + /** + * Constructs a new instance of the class. + * The constructor is private because creating a new instance should be done via the method `create` + * @param logger A value for the field `logger` + * @param pathToRustcSysRoot A value for the field `pathToRustcSysRoot` + * @param pathToRustSourceCode A value for the field `pathToRustSourceCode` + * @param pathToRlsExecutable A value fo the field `pathToRlsExecutable` + */ + private constructor( + logger: ChildLogger, + pathToRustcSysRoot: string, + pathToRustSourceCode: string | undefined, + pathToRlsExecutable: string | undefined + ) { + this.logger = logger; + + this.pathToRustcSysRoot = pathToRustcSysRoot; + + this.pathToRustSourceCode = pathToRustSourceCode; + + this.pathToRlsExecutable = pathToRlsExecutable; + } + + /** + * Requests Rustup give a list of components, parses it, checks if RLS is present in the list and returns if it is + * @returns true if RLS can be installed otherwise false + */ + public async canInstallRls(): Promise { + const logger = this.logger.createChildLogger('canRlsBeInstalled: '); + + const components: string[] | undefined = await this.getComponents(); + + if (!components) { + return false; + } + + const rlsComponent = components.find(component => component.startsWith(Rustup.getRlsComponentName())); + + if (!rlsComponent) { + return false; + } + + const isRlsComponentAlreadyInstalled = rlsComponent.endsWith(Rustup.getSuffixForInstalledComponent()); + + if (isRlsComponentAlreadyInstalled) { + logger.error('RLS is already installed. The method should not have been called'); + + return false; + } + + return true; + } + + /** + * Requests Rustup give a list of components, parses it and returns it + * This method is asynchronous because it requests Rustup + * @returns a list of components if no error occurred otherwise undefined + */ + private async getComponents(): Promise { + const logger = this.logger.createChildLogger('getComponents: '); + + const stdoutData: string | undefined = await this.invokeRustup(['component', 'list']); + + if (!stdoutData) { + return undefined; + } + + const components: string[] = stdoutData.split('\n'); + + if (components.length === 0) { + // It actually shouldn't happen, but sometimes strange things happen + logger.error(`Rustup returned no output`); + + return undefined; + } + + return components; + } + + /** + * Invokes Rustup with specified arguments, checks it exited successfully and returns its output + * @param args Arguments to invoke Rustup with + * @returns an output if invokation Rustup exited successfully otherwise undefined + */ + private async invokeRustup(args: string[]): Promise { + const logger = this.logger.createChildLogger('invokeRustup: '); + + const rustupExe = Rustup.getRustupExecutable(); + + // We assume that the executable of Rustup can be called since usually both `rustc` and `rustup` are placed in the same directory + const result = await OutputtingProcess.spawn(rustupExe, args, undefined); + + if (!result.success) { + // It actually shouldn't happen. + // If it happens, then there is some problem and we need to know about it + logger.error(`failed to execute ${rustupExe}. This should not have happened`); + + return undefined; + } + + if (result.exitCode !== 0) { + logger.error(`${rustupExe} ${args.join(' ')} exited with code=${result.exitCode}, but zero is expected. This should not have happened. stderrData=${result.stderrData}`); + + return undefined; + } + + return result.stdoutData; + } + + /** + * Requests Rustup give a list of components, parses it, filters only installed and returns it + * @returns a list of installed components if no error occurred otherwise undefined + */ + private async getInstalledComponents(): Promise { + const components: string[] | undefined = await this.getComponents(); + + if (!components) { + return undefined; + } + + const installedComponents = components.filter(component => { + return component.endsWith(Rustup.getSuffixForInstalledComponent()); + }); + + return installedComponents; + } + + /** + * Returns the executable of Rustup + */ + private static getRustupExecutable(): string { + return 'rustup'; + } + + /** + * Returns the name of the component RLS + */ + private static getRlsComponentName(): string { + return 'rls'; + } + + /** + * Returns a suffix which any installed component ends with + */ + private static getSuffixForInstalledComponent(): string { + return ' (installed)'; + } +} diff --git a/src/components/file_system/FileSystem.ts b/src/components/file_system/FileSystem.ts new file mode 100644 index 0000000..e7f8527 --- /dev/null +++ b/src/components/file_system/FileSystem.ts @@ -0,0 +1,54 @@ +import { access } from 'fs'; + +import { delimiter, extname, join } from 'path'; + +/** + * Code related to file system + */ +export class FileSystem { + /** + * Checks if there is a file or a directory at a specified path + * @param path a path to check + * @return true if there is a file or a directory otherwise false + */ + public static doesFileOrDirectoryExists(path: string): Promise { + return new Promise(resolve => { + access(path, err => { + const pathExists = !err; + + resolve(pathExists); + }); + }); + } + + /** + * Looks for a specified executable at paths specified in the environment variable PATH + * @param executable an executable to look for + * @return A path to the executable if it has been found otherwise undefined + */ + public static async findExecutablePath(executable: string): Promise { + if (!process.env.PATH) { + return undefined; + } + + // A executable on Windows ends with ".exe". + // Since this method can be called without the extension we need to add it if it is necessary + if (process.platform === 'win32' && extname(executable).length === 0) { + executable += '.exe'; + } + + const paths: string[] = process.env.PATH.split(delimiter); + + for (const path of paths) { + const possibleExecutablePath = join(path, executable); + + const doesPathExist: boolean = await FileSystem.doesFileOrDirectoryExists(possibleExecutablePath); + + if (doesPathExist) { + return possibleExecutablePath; + } + } + + return undefined; + } +} diff --git a/src/extension.ts b/src/extension.ts index 5e01772..2ea5283 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -1,13 +1,18 @@ +// https://github.com/pwnall/node-open +import open = require('open'); + import { ExtensionContext, window, workspace } from 'vscode'; import { CargoManager, CommandInvocationReason } from './components/cargo/cargo_manager'; -import { RlsConfiguration } from './components/configuration/Configuration'; - import { Configuration } from './components/configuration/Configuration'; import CurrentWorkingDirectoryManager from './components/configuration/current_working_directory_manager'; +import { NotRustup } from './components/configuration/NotRustup'; + +import { Rustup } from './components/configuration/Rustup'; + import { Manager as LanguageClientManager } from './components/language_client/manager'; import LoggingManager from './components/logging/logging_manager'; @@ -16,12 +21,174 @@ import RootLogger from './components/logging/root_logger'; import LegacyModeManager from './legacy_mode_manager'; +enum UserDecisionAboutInstallingRlsViaRustup { + ReadAboutRls, + InstallRls, + Decline +} + +/** + * Checks if Rust is installed via Rustup, then asks a user to install it if it is possible + * @param logger A logger + * @param configuration A configuration + */ +async function askForInstallingRlsViaRustup( + logger: RootLogger, + configuration: Configuration +): Promise { + const methodLogger = logger.createChildLogger('askForInstallingRlsViaRustup: '); + + const rustInstallation: Rustup | NotRustup | undefined = configuration.getRustInstallation(); + + if (!(rustInstallation instanceof Rustup)) { + methodLogger.error('Rust is either not installed or installed not via Rustup. The method should not have been called'); + + return UserDecisionAboutInstallingRlsViaRustup.Decline; + } + + const readAboutRlsChoice = 'Read about RLS'; + + const installRlsChoice = 'Install RLS'; + + const choice: string | undefined = await window.showInformationMessage( + 'You use Rustup, but RLS was not found. RLS provides a good user experience', + readAboutRlsChoice, + installRlsChoice + ); + + switch (choice) { + case readAboutRlsChoice: + methodLogger.debug('A user decided to read about RLS'); + + return UserDecisionAboutInstallingRlsViaRustup.ReadAboutRls; + + case installRlsChoice: + methodLogger.debug('A user decided to install RLS'); + + return UserDecisionAboutInstallingRlsViaRustup.InstallRls; + + default: + methodLogger.debug('A user declined'); + + return UserDecisionAboutInstallingRlsViaRustup.Decline; + } +} + +/** + * Asks a user if the user agrees to update Rustup + * @param updatePurpose A reason to update which is shown to a user + * @returns true if a user agreed to update otherwise false + */ +async function askUserToUpdateRustup(updatePurpose: string): Promise { + const updateChoice = 'Update'; + + const choice = await window.showInformationMessage(updatePurpose, updateChoice); + + return choice === updateChoice; +} + +/** + * Checks if Rustup can install (because older versions of Rustup cannot) and installs it if it Rustup can do it + * @param logger A logger + * @param rustup A rustup + * @returns true if RLS has been installed otherwise false + */ +async function handleUserDecisionToInstallRls(logger: RootLogger, rustup: Rustup): Promise { + const methodLogger = logger.createChildLogger('handleUserDecisionToInstallRls: '); + + const didUserAgreeToUpdateRustup = await askUserToUpdateRustup('Before installing RLS it would be good to update Rustup. If you decline to update, RLS will not be installed'); + + if (!didUserAgreeToUpdateRustup) { + methodLogger.debug('A user declined to update rustup'); + + return false; + } + + methodLogger.debug('A user agreed to update rustup'); + + const didRustupUpdateSuccessfully: boolean = await rustup.update(); + + if (!didRustupUpdateSuccessfully) { + methodLogger.error('Rustup failed to update'); + + return false; + } + + methodLogger.debug('Rustup has updated successfully'); + + const canRustupInstallRls = await rustup.canInstallRls(); + + if (!canRustupInstallRls) { + methodLogger.error('Rustup cannot install RLS'); + + return false; + } + + methodLogger.debug('Rustup can install RLS'); + + return await rustup.installRls(); +} + export async function activate(ctx: ExtensionContext): Promise { const loggingManager = new LoggingManager(); const logger = loggingManager.getLogger(); - const configuration = await Configuration.create(); + const configuration = await Configuration.create(logger.createChildLogger('Configuration: ')); + + // The following if statement does the following: + // * It checks if RLS is installed via any way + // * If it is, then it stops + // * Otherwise it checks if Rust is installed via Rustup + // * If it is, then it asks a user if the user wants to install RLS + // * If a user agrees to install RLS + // * It installs RLS + // * Otherwise it shows an error message + if (!configuration.getPathToRlsExecutable()) { + const rustInstallation = configuration.getRustInstallation(); + + if (rustInstallation instanceof Rustup) { + // Asking a user if the user wants to install RLS until the user declines it or agrees to install it. + // A user can decide to install RLS, then we install it. + // A user can decide to read about RLS, then we open a link to the repository of RLS and ask again after + + let shouldStop = false; + + while (!shouldStop) { + const userDecision: UserDecisionAboutInstallingRlsViaRustup = await askForInstallingRlsViaRustup(logger, configuration); + + switch (userDecision) { + case UserDecisionAboutInstallingRlsViaRustup.Decline: + shouldStop = true; + + break; + + case UserDecisionAboutInstallingRlsViaRustup.InstallRls: { + const isRlsInstalled: boolean = await handleUserDecisionToInstallRls(logger, rustInstallation); + + if (isRlsInstalled) { + await window.showInformationMessage('RLS has been installed successfully'); + } else { + await window.showErrorMessage('RLS has not been installed. Check the output channel "Rust Logging"'); + } + + shouldStop = true; + + break; + } + + case UserDecisionAboutInstallingRlsViaRustup.ReadAboutRls: + open('https://github.com/rust-lang-nursery/rls'); + + break; + } + } + } else { + logger.debug('Rust is either not installed or installed not via Rustup'); + + await window.showInformationMessage('You do not use Rustup. Rustup is a preffered way to install Rust and its components'); + } + } const currentWorkingDirectoryManager = new CurrentWorkingDirectoryManager(); @@ -37,35 +204,55 @@ export async function activate(ctx: ExtensionContext): Promise { addExecutingActionOnSave(ctx, configuration, cargoManager); } -function chooseModeAndRun( +/** + * Starts the extension in RLS mode + * @param context An extension context to use + * @param logger A logger to log messages + * @param configuration A configuration + * @param pathToRlsExecutable A path to the executable of RLS + */ +function runInRlsMode( context: ExtensionContext, logger: RootLogger, configuration: Configuration, - currentWorkingDirectoryManager: CurrentWorkingDirectoryManager + pathToRlsExecutable: string ): void { - const rlsConfiguration: RlsConfiguration | undefined = configuration.getRlsConfiguration(); + const methodLogger = logger.createChildLogger('runInRlsMode: '); - if (rlsConfiguration !== undefined) { - let { executable, args, env, revealOutputChannelOn } = rlsConfiguration; + const env = configuration.getRlsEnv(); - if (!env) { - env = {}; - } + methodLogger.debug(`env=${JSON.stringify(env)}`); - if (!env.RUST_SRC_PATH) { - env.RUST_SRC_PATH = configuration.getRustSourcePath(); - } + const args = configuration.getRlsArgs(); - const languageClientManager = new LanguageClientManager( - context, - logger.createChildLogger('Language Client Manager: '), - executable, - args, - env, - revealOutputChannelOn - ); + methodLogger.debug(`args=${JSON.stringify(args)}`); + + let revealOutputChannelOn = configuration.getRlsRevealOutputChannelOn(); + + methodLogger.debug(`revealOutputChannelOn=${revealOutputChannelOn}`); + + const languageClientManager = new LanguageClientManager( + context, + logger.createChildLogger('Language Client Manager: '), + pathToRlsExecutable, + args, + env, + revealOutputChannelOn + ); + + languageClientManager.initialStart(); +} + +function chooseModeAndRun( + context: ExtensionContext, + logger: RootLogger, + configuration: Configuration, + currentWorkingDirectoryManager: CurrentWorkingDirectoryManager +): void { + const pathToRlsExecutable = configuration.getPathToRlsExecutable(); - languageClientManager.initialStart(); + if (pathToRlsExecutable) { + runInRlsMode(context, logger, configuration, pathToRlsExecutable); } else { const legacyModeManager = new LegacyModeManager( context, diff --git a/tslint.json b/tslint.json index 6fea8f2..180425c 100644 --- a/tslint.json +++ b/tslint.json @@ -20,10 +20,6 @@ "interface-name": false, "jsdoc-format": true, "label-position": true, - "max-line-length": [ - true, - 140 - ], "member-access": true, "member-ordering": [ false, @@ -109,4 +105,4 @@ "check-type" ] } -} \ No newline at end of file +}