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; } // Windows handles arguments differently than the unix-likes, so we need to wrap the args in double quotes if (process.platform === 'win32') { args = '"' + 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); try { this.parseLine(line); } catch (err) { this.logError(`Failed to parse: ${err}, content : ${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; } // Reference: // https://github.com/rust-lang/rust/blob/master/src/libsyntax/json.rs interface RustDiagnosticSpan { line_start: number; line_end: number; column_start: number; column_end: number; is_primary: boolean; file_name: string; } interface RustDiagnostic { spans: RustDiagnosticSpan[]; rendered: string; level: string; code?: { code: string; }; } interface CargoArtifact { reason: string; package_id: string; } // https://github.com/rust-lang/cargo/blob/master/src/cargo/util/machine_message.rs interface CargoMessage { reason: string; package_id: string; message: RustDiagnostic; } // cargo-watch itself output non json format // Ignore these lines let data: CargoMessage; try { data = JSON.parse(line.trim()); } catch (error) { this.logError(`Fail to parse to json : { ${error} }`); return; } if (data.reason === 'compiler-artifact') { const msg = data as CargoArtifact; // The format of the package_id is "{name} {version} ({source_id})", // https://github.com/rust-lang/cargo/blob/37ad03f86e895bb80b474c1c088322634f4725f5/src/cargo/core/package_id.rs#L53 this.statusDisplay!.packageName = msg.package_id.split(' ')[0]; } else if (data.reason === 'compiler-message') { const msg = data.message as RustDiagnostic; const spans = msg.spans.filter(o => o.is_primary); // We only handle primary span right now. if (spans.length > 0) { const o = spans[0]; const rendered = msg.rendered; const level = getLevel(msg.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 = msg.code ? msg.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); } } } }