1 import * as child_process from 'child_process';
2 import * as path from 'path';
3 import * as vscode from 'vscode';
5 import { Server } from '../server';
6 import { terminate } from '../utils/processes';
7 import { LineBuffer } from './line_buffer';
8 import { StatusDisplay } from './watch_status';
11 mapRustDiagnosticToVsCode,
13 } from '../utils/diagnostics/rust';
14 import SuggestedFixCollection from '../utils/diagnostics/SuggestedFixCollection';
15 import { areDiagnosticsEqual } from '../utils/diagnostics/vscode';
17 export async function registerCargoWatchProvider(
18 subscriptions: vscode.Disposable[]
19 ): Promise<CargoWatchProvider | undefined> {
20 let cargoExists = false;
22 // Check if the working directory is valid cargo root path
23 const cargoTomlPath = path.join(vscode.workspace.rootPath!, 'Cargo.toml');
24 const cargoTomlUri = vscode.Uri.file(cargoTomlPath);
25 const cargoTomlFileInfo = await vscode.workspace.fs.stat(cargoTomlUri);
27 if (cargoTomlFileInfo) {
32 vscode.window.showErrorMessage(
33 `Couldn\'t find \'Cargo.toml\' at ${cargoTomlPath}`
38 const provider = new CargoWatchProvider();
39 subscriptions.push(provider);
43 export class CargoWatchProvider implements vscode.Disposable {
44 private readonly diagnosticCollection: vscode.DiagnosticCollection;
45 private readonly statusDisplay: StatusDisplay;
46 private readonly outputChannel: vscode.OutputChannel;
48 private suggestedFixCollection: SuggestedFixCollection;
49 private codeActionDispose: vscode.Disposable;
51 private cargoProcess?: child_process.ChildProcess;
54 this.diagnosticCollection = vscode.languages.createDiagnosticCollection(
57 this.statusDisplay = new StatusDisplay(
58 Server.config.cargoWatchOptions.command
60 this.outputChannel = vscode.window.createOutputChannel(
64 // Track `rustc`'s suggested fixes so we can convert them to code actions
65 this.suggestedFixCollection = new SuggestedFixCollection();
66 this.codeActionDispose = vscode.languages.registerCodeActionsProvider(
67 [{ scheme: 'file', language: 'rust' }],
68 this.suggestedFixCollection,
70 providedCodeActionKinds:
71 SuggestedFixCollection.PROVIDED_CODE_ACTION_KINDS
77 if (this.cargoProcess) {
78 vscode.window.showInformationMessage(
79 'Cargo Watch is already running'
85 Server.config.cargoWatchOptions.command +
86 ' --all-targets --message-format json';
87 if (Server.config.cargoWatchOptions.command.length > 0) {
88 // Excape the double quote string:
89 args += ' ' + Server.config.cargoWatchOptions.arguments;
91 // Windows handles arguments differently than the unix-likes, so we need to wrap the args in double quotes
92 if (process.platform === 'win32') {
93 args = '"' + args + '"';
96 // Start the cargo watch with json message
97 this.cargoProcess = child_process.spawn(
99 ['watch', '-x', args],
101 stdio: ['ignore', 'pipe', 'pipe'],
102 cwd: vscode.workspace.rootPath,
103 windowsVerbatimArguments: true
107 const stdoutData = new LineBuffer();
108 this.cargoProcess.stdout.on('data', (s: string) => {
109 stdoutData.processOutput(s, line => {
112 this.parseLine(line);
114 this.logError(`Failed to parse: ${err}, content : ${line}`);
119 const stderrData = new LineBuffer();
120 this.cargoProcess.stderr.on('data', (s: string) => {
121 stderrData.processOutput(s, line => {
122 this.logError('Error on cargo-watch : {\n' + line + '}\n');
126 this.cargoProcess.on('error', (err: Error) => {
128 'Error on cargo-watch process : {\n' + err.message + '}\n'
132 this.logInfo('cargo-watch started.');
136 if (this.cargoProcess) {
137 this.cargoProcess.kill();
138 terminate(this.cargoProcess);
139 this.cargoProcess = undefined;
141 vscode.window.showInformationMessage('Cargo Watch is not running');
145 public dispose(): void {
148 this.diagnosticCollection.clear();
149 this.diagnosticCollection.dispose();
150 this.outputChannel.dispose();
151 this.statusDisplay.dispose();
152 this.codeActionDispose.dispose();
155 private logInfo(line: string) {
156 if (Server.config.cargoWatchOptions.trace === 'verbose') {
157 this.outputChannel.append(line);
161 private logError(line: string) {
163 Server.config.cargoWatchOptions.trace === 'error' ||
164 Server.config.cargoWatchOptions.trace === 'verbose'
166 this.outputChannel.append(line);
170 private parseLine(line: string) {
171 if (line.startsWith('[Running')) {
172 this.diagnosticCollection.clear();
173 this.suggestedFixCollection.clear();
174 this.statusDisplay.show();
177 if (line.startsWith('[Finished running')) {
178 this.statusDisplay.hide();
181 interface CargoArtifact {
186 // https://github.com/rust-lang/cargo/blob/master/src/cargo/util/machine_message.rs
187 interface CargoMessage {
190 message: RustDiagnostic;
193 // cargo-watch itself output non json format
194 // Ignore these lines
195 let data: CargoMessage;
197 data = JSON.parse(line.trim());
199 this.logError(`Fail to parse to json : { ${error} }`);
203 if (data.reason === 'compiler-artifact') {
204 const msg = data as CargoArtifact;
206 // The format of the package_id is "{name} {version} ({source_id})",
207 // https://github.com/rust-lang/cargo/blob/37ad03f86e895bb80b474c1c088322634f4725f5/src/cargo/core/package_id.rs#L53
208 this.statusDisplay.packageName = msg.package_id.split(' ')[0];
209 } else if (data.reason === 'compiler-message') {
210 const msg = data.message as RustDiagnostic;
212 const mapResult = mapRustDiagnosticToVsCode(msg);
217 const { location, diagnostic, suggestedFixes } = mapResult;
218 const fileUri = location.uri;
220 const diagnostics: vscode.Diagnostic[] = [
221 ...(this.diagnosticCollection!.get(fileUri) || [])
224 // If we're building multiple targets it's possible we've already seen this diagnostic
225 const isDuplicate = diagnostics.some(d =>
226 areDiagnosticsEqual(d, diagnostic)
232 diagnostics.push(diagnostic);
233 this.diagnosticCollection!.set(fileUri, diagnostics);
235 if (suggestedFixes.length) {
236 for (const suggestedFix of suggestedFixes) {
237 this.suggestedFixCollection.addSuggestedFixForDiagnostic(
243 // Have VsCode query us for the code actions
244 vscode.commands.executeCommand(
245 'vscode.executeCodeActionProvider',