diff --git a/docs/user/README.md b/docs/user/README.md index 439c4e6ae5c..33dd4f9950b 100644 --- a/docs/user/README.md +++ b/docs/user/README.md @@ -59,7 +59,10 @@ for details. * `rust-analyzer.raLspServerPath`: path to `ra_lsp_server` executable * `rust-analyzer.enableCargoWatchOnStartup`: prompt to install & enable `cargo watch` for live error highlighting (note, this **does not** use rust-analyzer) +* `rust-analyzer.cargo-watch.check-arguments`: cargo-watch check arguments. + (e.g: `--features="shumway,pdf"` will run as `cargo watch -x "check --features="shumway,pdf""` ) * `rust-analyzer.trace.server`: enables internal logging +* `rust-analyzer.trace.cargo-watch`: enables cargo-watch logging ## Emacs diff --git a/editors/code/package-lock.json b/editors/code/package-lock.json index 5a0d21e788b..008df6f52a7 100644 --- a/editors/code/package-lock.json +++ b/editors/code/package-lock.json @@ -607,6 +607,12 @@ "integrity": "sha512-f2LZMYl1Fzu7YSBKg+RoROelpOaNrcGmE9AZubeDfrCEia483oW4MI4VyFd5VNHIgQ/7qm1I0wUHK1eJnn2y2w==", "dev": true }, + "es6-object-assign": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/es6-object-assign/-/es6-object-assign-1.1.0.tgz", + "integrity": "sha1-wsNYJlYkfDnqEHyx5mUrb58kUjw=", + "dev": true + }, "escape-string-regexp": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", @@ -1121,6 +1127,12 @@ "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", "dev": true }, + "interpret": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/interpret/-/interpret-1.2.0.tgz", + "integrity": "sha512-mT34yGKMNceBQUoVn7iCDKDntA7SC6gycMAWzGx1z/CMCTV7b2AAtXlo3nRyHZ1FelRkQbQjprHSYGwzLtkVbw==", + "dev": true + }, "is": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/is/-/is-3.3.0.tgz", @@ -1791,6 +1803,15 @@ "util-deprecate": "^1.0.1" } }, + "rechoir": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.6.2.tgz", + "integrity": "sha1-hSBLVNuoLVdC4oyWdW70OvUOM4Q=", + "dev": true, + "requires": { + "resolve": "^1.1.6" + } + }, "remove-bom-buffer": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/remove-bom-buffer/-/remove-bom-buffer-3.0.0.tgz", @@ -1902,6 +1923,36 @@ "resolved": "https://registry.npmjs.org/semver/-/semver-5.6.0.tgz", "integrity": "sha512-RS9R6R35NYgQn++fkDWaOmqGoj4Ek9gGs+DPxNUZKuwE183xjJroKvyo1IzVFeXvUrvmALy6FWD5xrdJT25gMg==" }, + "shelljs": { + "version": "0.8.3", + "resolved": "https://registry.npmjs.org/shelljs/-/shelljs-0.8.3.tgz", + "integrity": "sha512-fc0BKlAWiLpwZljmOvAOTE/gXawtCoNrP5oaY7KIaQbbyHeQVg01pSEuEGvGh3HEdBU4baCD7wQBwADmM/7f7A==", + "dev": true, + "requires": { + "glob": "^7.0.0", + "interpret": "^1.0.0", + "rechoir": "^0.6.2" + } + }, + "shx": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/shx/-/shx-0.3.2.tgz", + "integrity": "sha512-aS0mWtW3T2sHAenrSrip2XGv39O9dXIFUqxAEWHEOS1ePtGIBavdPJY1kE2IHl14V/4iCbUiNDPGdyYTtmhSoA==", + "dev": true, + "requires": { + "es6-object-assign": "^1.0.3", + "minimist": "^1.2.0", + "shelljs": "^0.8.1" + }, + "dependencies": { + "minimist": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", + "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", + "dev": true + } + } + }, "source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", diff --git a/editors/code/package.json b/editors/code/package.json index facb633d9ad..1c8caaa60cb 100644 --- a/editors/code/package.json +++ b/editors/code/package.json @@ -18,7 +18,7 @@ "scripts": { "vscode:prepublish": "npm run compile", "package": "vsce package", - "compile": "tsc -p ./", + "compile": "tsc -p ./ && shx cp src/utils/terminateProcess.sh out/utils/terminateProcess.sh", "watch": "tsc -watch -p ./", "postinstall": "node ./node_modules/vscode/bin/install", "fix": "prettier **/*.{json,ts} --write && tslint --project . --fix", @@ -41,7 +41,8 @@ "tslint-config-prettier": "^1.18.0", "typescript": "^3.3.1", "vsce": "^1.57.0", - "vscode": "^1.1.29" + "vscode": "^1.1.29", + "shx": "^0.3.1" }, "activationEvents": [ "onLanguage:rust", @@ -183,6 +184,11 @@ ], "description": "Whether to run `cargo watch` on startup" }, + "rust-analyzer.cargo-watch.check-arguments": { + "type": "string", + "description": "`cargo-watch` check arguments. (e.g: `--features=\"shumway,pdf\"` will run as `cargo watch -x \"check --features=\"shumway,pdf\"\"` )", + "default": "" + }, "rust-analyzer.trace.server": { "type": "string", "scope": "window", @@ -191,8 +197,24 @@ "messages", "verbose" ], + "enumDescriptions": [ + "No traces", + "Error only", + "Full log" + ], "default": "off", "description": "Trace requests to the ra_lsp_server" + }, + "rust-analyzer.trace.cargo-watch": { + "type": "string", + "scope": "window", + "enum": [ + "off", + "error", + "verbose" + ], + "default": "off", + "description": "Trace output of cargo-watch" } } }, @@ -223,18 +245,6 @@ "${workspaceRoot}" ], "pattern": "$rustc" - }, - { - "name": "rustc-watch", - "fileLocation": [ - "relative", - "${workspaceRoot}" - ], - "background": { - "beginsPattern": "^\\[Running\\b", - "endsPattern": "^\\[Finished running\\b" - }, - "pattern": "$rustc" } ] } diff --git a/editors/code/src/commands/cargo_watch.ts b/editors/code/src/commands/cargo_watch.ts new file mode 100644 index 00000000000..6d8e4d88511 --- /dev/null +++ b/editors/code/src/commands/cargo_watch.ts @@ -0,0 +1,211 @@ +import * as child_process from 'child_process'; +import * as fs from 'fs'; +import * as path from 'path'; +import * as vscode from 'vscode'; +import { Server } from '../server'; +import { terminate } from '../utils/processes'; +import { LineBuffer } from './line_buffer'; +import { StatusDisplay } from './watch_status'; + +export class CargoWatchProvider { + private diagnosticCollection?: vscode.DiagnosticCollection; + private cargoProcess?: child_process.ChildProcess; + private outBuffer: string = ''; + private statusDisplay?: StatusDisplay; + private outputChannel?: vscode.OutputChannel; + + public activate(subscriptions: vscode.Disposable[]) { + let cargoExists = false; + const cargoTomlFile = path.join( + vscode.workspace.rootPath!, + 'Cargo.toml' + ); + // Check if the working directory is valid cargo root path + try { + if (fs.existsSync(cargoTomlFile)) { + cargoExists = true; + } + } catch (err) { + cargoExists = false; + } + + if (!cargoExists) { + vscode.window.showErrorMessage( + `Couldn\'t find \'Cargo.toml\' in ${cargoTomlFile}` + ); + return; + } + + subscriptions.push(this); + this.diagnosticCollection = vscode.languages.createDiagnosticCollection( + 'rustc' + ); + + this.statusDisplay = new StatusDisplay(subscriptions); + this.outputChannel = vscode.window.createOutputChannel( + 'Cargo Watch Trace' + ); + + let args = '"check --message-format json'; + if (Server.config.cargoWatchOptions.checkArguments.length > 0) { + // Excape the double quote string: + args += ' ' + Server.config.cargoWatchOptions.checkArguments; + } + args += '"'; + + // Start the cargo watch with json message + this.cargoProcess = child_process.spawn( + 'cargo', + ['watch', '-x', args], + { + stdio: ['ignore', 'pipe', 'pipe'], + cwd: vscode.workspace.rootPath, + windowsVerbatimArguments: true + } + ); + + const stdoutData = new LineBuffer(); + this.cargoProcess.stdout.on('data', (s: string) => { + stdoutData.processOutput(s, line => { + this.logInfo(line); + this.parseLine(line); + }); + }); + + const stderrData = new LineBuffer(); + this.cargoProcess.stderr.on('data', (s: string) => { + stderrData.processOutput(s, line => { + this.logError('Error on cargo-watch : {\n' + line + '}\n'); + }); + }); + + this.cargoProcess.on('error', (err: Error) => { + this.logError( + 'Error on cargo-watch process : {\n' + err.message + '}\n' + ); + }); + + this.logInfo('cargo-watch started.'); + } + + public dispose(): void { + if (this.diagnosticCollection) { + this.diagnosticCollection.clear(); + this.diagnosticCollection.dispose(); + } + + if (this.cargoProcess) { + this.cargoProcess.kill(); + terminate(this.cargoProcess); + } + + if (this.outputChannel) { + this.outputChannel.dispose(); + } + } + + private logInfo(line: string) { + if (Server.config.cargoWatchOptions.trace === 'verbose') { + this.outputChannel!.append(line); + } + } + + private logError(line: string) { + if ( + Server.config.cargoWatchOptions.trace === 'error' || + Server.config.cargoWatchOptions.trace === 'verbose' + ) { + this.outputChannel!.append(line); + } + } + + private parseLine(line: string) { + if (line.startsWith('[Running')) { + this.diagnosticCollection!.clear(); + this.statusDisplay!.show(); + } + + if (line.startsWith('[Finished running')) { + this.statusDisplay!.hide(); + } + + function getLevel(s: string): vscode.DiagnosticSeverity { + if (s === 'error') { + return vscode.DiagnosticSeverity.Error; + } + + if (s.startsWith('warn')) { + return vscode.DiagnosticSeverity.Warning; + } + + return vscode.DiagnosticSeverity.Information; + } + + interface ErrorSpan { + line_start: number; + line_end: number; + column_start: number; + column_end: number; + } + + interface ErrorMessage { + reason: string; + message: { + spans: ErrorSpan[]; + rendered: string; + level: string; + code?: { + code: string; + }; + }; + } + + // cargo-watch itself output non json format + // Ignore these lines + let data: ErrorMessage; + try { + data = JSON.parse(line.trim()); + } catch (error) { + this.logError(`Fail to pass to json : { ${error} }`); + return; + } + + // Only handle compiler-message now + if (data.reason !== 'compiler-message') { + return; + } + + let spans: any[] = data.message.spans; + spans = spans.filter(o => o.is_primary); + + // We only handle primary span right now. + if (spans.length > 0) { + const o = spans[0]; + + const rendered = data.message.rendered; + const level = getLevel(data.message.level); + const range = new vscode.Range( + new vscode.Position(o.line_start - 1, o.column_start - 1), + new vscode.Position(o.line_end - 1, o.column_end - 1) + ); + + const fileName = path.join(vscode.workspace.rootPath!, o.file_name); + const diagnostic = new vscode.Diagnostic(range, rendered, level); + + diagnostic.source = 'rustc'; + diagnostic.code = data.message.code + ? data.message.code.code + : undefined; + diagnostic.relatedInformation = []; + + const fileUrl = vscode.Uri.file(fileName!); + + const diagnostics: vscode.Diagnostic[] = [ + ...(this.diagnosticCollection!.get(fileUrl) || []) + ]; + diagnostics.push(diagnostic); + + this.diagnosticCollection!.set(fileUrl, diagnostics); + } + } +} diff --git a/editors/code/src/commands/line_buffer.ts b/editors/code/src/commands/line_buffer.ts new file mode 100644 index 00000000000..fb5b9f7f2b2 --- /dev/null +++ b/editors/code/src/commands/line_buffer.ts @@ -0,0 +1,16 @@ +export class LineBuffer { + private outBuffer: string = ''; + + public processOutput(chunk: string, cb: (line: string) => void) { + this.outBuffer += chunk; + let eolIndex = this.outBuffer.indexOf('\n'); + while (eolIndex >= 0) { + // line includes the EOL + const line = this.outBuffer.slice(0, eolIndex + 1); + cb(line); + this.outBuffer = this.outBuffer.slice(eolIndex + 1); + + eolIndex = this.outBuffer.indexOf('\n'); + } + } +} diff --git a/editors/code/src/commands/runnables.ts b/editors/code/src/commands/runnables.ts index 4187ef4d157..3589edceecd 100644 --- a/editors/code/src/commands/runnables.ts +++ b/editors/code/src/commands/runnables.ts @@ -1,9 +1,11 @@ import * as child_process from 'child_process'; + import * as util from 'util'; import * as vscode from 'vscode'; import * as lc from 'vscode-languageclient'; import { Server } from '../server'; +import { CargoWatchProvider } from './cargo_watch'; interface RunnablesParams { textDocument: lc.TextDocumentIdentifier; @@ -127,37 +129,19 @@ export async function handleSingle(runnable: Runnable) { return vscode.tasks.executeTask(task); } -export const autoCargoWatchTask: vscode.Task = { - name: 'cargo watch', - source: 'rust-analyzer', - definition: { - type: 'watch' - }, - execution: new vscode.ShellExecution('cargo', ['watch'], { cwd: '.' }), - - isBackground: true, - problemMatchers: ['$rustc-watch'], - presentationOptions: { - clear: true - }, - // Not yet exposed in the vscode.d.ts - // https://github.com/Microsoft/vscode/blob/ea7c31d770e04b51d586b0d3944f3a7feb03afb9/src/vs/workbench/contrib/tasks/common/tasks.ts#L444-L456 - runOptions: ({ - runOn: 2 // RunOnOptions.folderOpen - } as unknown) as vscode.RunOptions -}; - /** * Interactively asks the user whether we should run `cargo check` in order to * provide inline diagnostics; the user is met with a series of dialog boxes * that, when accepted, allow us to `cargo install cargo-watch` and then run it. */ -export async function interactivelyStartCargoWatch() { - if (Server.config.enableCargoWatchOnStartup === 'disabled') { +export async function interactivelyStartCargoWatch( + context: vscode.ExtensionContext +) { + if (Server.config.cargoWatchOptions.enableOnStartup === 'disabled') { return; } - if (Server.config.enableCargoWatchOnStartup === 'ask') { + if (Server.config.cargoWatchOptions.enableOnStartup === 'ask') { const watch = await vscode.window.showInformationMessage( 'Start watching changes with cargo? (Executes `cargo watch`, provides inline diagnostics)', 'yes', @@ -212,5 +196,6 @@ export async function interactivelyStartCargoWatch() { } } - vscode.tasks.executeTask(autoCargoWatchTask); + const validater = new CargoWatchProvider(); + validater.activate(context.subscriptions); } diff --git a/editors/code/src/commands/watch_status.ts b/editors/code/src/commands/watch_status.ts new file mode 100644 index 00000000000..f027d7bbc27 --- /dev/null +++ b/editors/code/src/commands/watch_status.ts @@ -0,0 +1,41 @@ +import * as vscode from 'vscode'; + +const spinnerFrames = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏']; + +export class StatusDisplay { + private i = 0; + private statusBarItem: vscode.StatusBarItem; + private timer?: NodeJS.Timeout; + + constructor(subscriptions: vscode.Disposable[]) { + this.statusBarItem = vscode.window.createStatusBarItem( + vscode.StatusBarAlignment.Left, + 10 + ); + subscriptions.push(this.statusBarItem); + this.statusBarItem.hide(); + } + + public show() { + this.timer = + this.timer || + setInterval(() => { + this.statusBarItem!.text = 'cargo check ' + this.frame(); + }, 300); + + this.statusBarItem!.show(); + } + + public hide() { + if (this.timer) { + clearInterval(this.timer); + this.timer = undefined; + } + + this.statusBarItem!.hide(); + } + + private frame() { + return spinnerFrames[(this.i = ++this.i % spinnerFrames.length)]; + } +} diff --git a/editors/code/src/config.ts b/editors/code/src/config.ts index 42058906845..481a5e5f18f 100644 --- a/editors/code/src/config.ts +++ b/editors/code/src/config.ts @@ -4,14 +4,25 @@ import { Server } from './server'; const RA_LSP_DEBUG = process.env.__RA_LSP_SERVER_DEBUG; -export type CargoWatchOptions = 'ask' | 'enabled' | 'disabled'; +export type CargoWatchStartupOptions = 'ask' | 'enabled' | 'disabled'; +export type CargoWatchTraceOptions = 'off' | 'error' | 'verbose'; + +export interface CargoWatchOptions { + enableOnStartup: CargoWatchStartupOptions; + checkArguments: string; + trace: CargoWatchTraceOptions; +} export class Config { public highlightingOn = true; public enableEnhancedTyping = true; public raLspServerPath = RA_LSP_DEBUG || 'ra_lsp_server'; public showWorkspaceLoadedNotification = true; - public enableCargoWatchOnStartup: CargoWatchOptions = 'ask'; + public cargoWatchOptions: CargoWatchOptions = { + enableOnStartup: 'ask', + trace: 'off', + checkArguments: '' + }; private prevEnhancedTyping: null | boolean = null; @@ -73,9 +84,22 @@ export class Config { } if (config.has('enableCargoWatchOnStartup')) { - this.enableCargoWatchOnStartup = config.get( - 'enableCargoWatchOnStartup', - 'ask' + this.cargoWatchOptions.enableOnStartup = config.get< + CargoWatchStartupOptions + >('enableCargoWatchOnStartup', 'ask'); + } + + if (config.has('trace.cargo-watch')) { + this.cargoWatchOptions.trace = config.get( + 'trace.cargo-watch', + 'off' + ); + } + + if (config.has('cargo-watch.check-arguments')) { + this.cargoWatchOptions.checkArguments = config.get( + 'cargo-watch.check-arguments', + '' ); } } diff --git a/editors/code/src/extension.ts b/editors/code/src/extension.ts index 2e13c87de16..ef83c0b8b21 100644 --- a/editors/code/src/extension.ts +++ b/editors/code/src/extension.ts @@ -121,7 +121,7 @@ export function activate(context: vscode.ExtensionContext) { ); // Executing `cargo watch` provides us with inline diagnostics on save - interactivelyStartCargoWatch(); + interactivelyStartCargoWatch(context); // Start the language server, finally! Server.start(allNotifications); diff --git a/editors/code/src/utils/processes.ts b/editors/code/src/utils/processes.ts new file mode 100644 index 00000000000..da8be9eb179 --- /dev/null +++ b/editors/code/src/utils/processes.ts @@ -0,0 +1,51 @@ +'use strict'; + +import * as cp from 'child_process'; +import ChildProcess = cp.ChildProcess; + +import { join } from 'path'; + +const isWindows = process.platform === 'win32'; +const isMacintosh = process.platform === 'darwin'; +const isLinux = process.platform === 'linux'; + +// this is very complex, but is basically copy-pased from VSCode implementation here: +// https://github.com/Microsoft/vscode-languageserver-node/blob/dbfd37e35953ad0ee14c4eeced8cfbc41697b47e/client/src/utils/processes.ts#L15 + +// And see discussion at +// https://github.com/rust-analyzer/rust-analyzer/pull/1079#issuecomment-478908109 + +export function terminate(process: ChildProcess, cwd?: string): boolean { + if (isWindows) { + try { + // This we run in Atom execFileSync is available. + // Ignore stderr since this is otherwise piped to parent.stderr + // which might be already closed. + const options: any = { + stdio: ['pipe', 'pipe', 'ignore'] + }; + if (cwd) { + options.cwd = cwd; + } + cp.execFileSync( + 'taskkill', + ['/T', '/F', '/PID', process.pid.toString()], + options + ); + return true; + } catch (err) { + return false; + } + } else if (isLinux || isMacintosh) { + try { + const cmd = join(__dirname, 'terminateProcess.sh'); + const result = cp.spawnSync(cmd, [process.pid.toString()]); + return result.error ? false : true; + } catch (err) { + return false; + } + } else { + process.kill('SIGKILL'); + return true; + } +} diff --git a/editors/code/src/utils/terminateProcess.sh b/editors/code/src/utils/terminateProcess.sh new file mode 100644 index 00000000000..2ec9e1c2ec3 --- /dev/null +++ b/editors/code/src/utils/terminateProcess.sh @@ -0,0 +1,12 @@ +#!/bin/bash + +terminateTree() { + for cpid in $(pgrep -P $1); do + terminateTree $cpid + done + kill -9 $1 > /dev/null 2>&1 +} + +for pid in $*; do + terminateTree $pid +done \ No newline at end of file