1079: Improve cargo-watch usage in vscode plugin r=matklad a=edwin0cheng

*This PR try to improve current cargo-watch usage in VSCode :*

1. Add Multi-lines error support :
![multilines-error](https://i.imgur.com/gbLEwMG.gif)

2. Add cargo-watch status animation :
![cargo-watch-status](https://i.imgur.com/GbHwzjj.gif)

*Implementation Details*

* Current VSCode `ProblemMatcher` still do not support multiple line parsing.
* However we can, spawn a cargo watch process instead of using vscode.Task to allow more control.
* Use `cargo-check --message-format json` to get json format of compiler-message.
* Use `vscode.DiagnosticCollection` to manage the problems directly, which allow multiple lines diagnostic.

However,
* VSCode use non mono-space font for problems, at this moment i cannot find a good solution about it.
* I am not so good in typescript, please let me know if anything is bad in this PR.





Co-authored-by: Edwin Cheng <edwin0cheng@gmail.com>
Co-authored-by: Edwin Cheng <edwin@m-inverse.com>
This commit is contained in:
bors[bot] 2019-04-02 10:10:26 +00:00
commit bb3b159fb7
11 changed files with 448 additions and 44 deletions

View File

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

View File

@ -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",

View File

@ -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"
}
]
}

View File

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

View File

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

View File

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

View File

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

View File

@ -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<CargoWatchOptions>(
'enableCargoWatchOnStartup',
'ask'
this.cargoWatchOptions.enableOnStartup = config.get<
CargoWatchStartupOptions
>('enableCargoWatchOnStartup', 'ask');
}
if (config.has('trace.cargo-watch')) {
this.cargoWatchOptions.trace = config.get<CargoWatchTraceOptions>(
'trace.cargo-watch',
'off'
);
}
if (config.has('cargo-watch.check-arguments')) {
this.cargoWatchOptions.checkArguments = config.get<string>(
'cargo-watch.check-arguments',
''
);
}
}

View File

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

View File

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

View File

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