]> git.lizzy.rs Git - rust.git/blob - editors/code/src/commands/cargo_watch.ts
45f1dd49f46160363c11b56af4efea74c4be32f4
[rust.git] / editors / code / src / commands / cargo_watch.ts
1 import * as child_process from 'child_process';
2 import * as path from 'path';
3 import * as vscode from 'vscode';
4
5 import { Server } from '../server';
6 import { terminate } from '../utils/processes';
7 import { LineBuffer } from './line_buffer';
8 import { StatusDisplay } from './watch_status';
9
10 import {
11     mapRustDiagnosticToVsCode,
12     RustDiagnostic,
13 } from '../utils/diagnostics/rust';
14 import SuggestedFixCollection from '../utils/diagnostics/SuggestedFixCollection';
15 import { areDiagnosticsEqual } from '../utils/diagnostics/vscode';
16
17 export async function registerCargoWatchProvider(
18     subscriptions: vscode.Disposable[],
19 ): Promise<CargoWatchProvider | undefined> {
20     let cargoExists = false;
21
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);
26
27     if (cargoTomlFileInfo) {
28         cargoExists = true;
29     }
30
31     if (!cargoExists) {
32         vscode.window.showErrorMessage(
33             `Couldn\'t find \'Cargo.toml\' at ${cargoTomlPath}`,
34         );
35         return;
36     }
37
38     const provider = new CargoWatchProvider();
39     subscriptions.push(provider);
40     return provider;
41 }
42
43 export class CargoWatchProvider implements vscode.Disposable {
44     private readonly diagnosticCollection: vscode.DiagnosticCollection;
45     private readonly statusDisplay: StatusDisplay;
46     private readonly outputChannel: vscode.OutputChannel;
47
48     private suggestedFixCollection: SuggestedFixCollection;
49     private codeActionDispose: vscode.Disposable;
50
51     private cargoProcess?: child_process.ChildProcess;
52
53     constructor() {
54         this.diagnosticCollection = vscode.languages.createDiagnosticCollection(
55             'rustc',
56         );
57         this.statusDisplay = new StatusDisplay(
58             Server.config.cargoWatchOptions.command,
59         );
60         this.outputChannel = vscode.window.createOutputChannel(
61             'Cargo Watch Trace',
62         );
63
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,
69             {
70                 providedCodeActionKinds:
71                     SuggestedFixCollection.PROVIDED_CODE_ACTION_KINDS,
72             },
73         );
74     }
75
76     public start() {
77         if (this.cargoProcess) {
78             vscode.window.showInformationMessage(
79                 'Cargo Watch is already running',
80             );
81             return;
82         }
83
84         let args =
85             Server.config.cargoWatchOptions.command +
86             ' --message-format json';
87         if (Server.config.cargoWatchOptions.allTargets) {
88             args += ' --all-targets';
89         }
90         if (Server.config.cargoWatchOptions.command.length > 0) {
91             // Excape the double quote string:
92             args += ' ' + Server.config.cargoWatchOptions.arguments;
93         }
94         // Windows handles arguments differently than the unix-likes, so we need to wrap the args in double quotes
95         if (process.platform === 'win32') {
96             args = '"' + args + '"';
97         }
98
99         const ignoreFlags = Server.config.cargoWatchOptions.ignore.reduce(
100             (flags, pattern) => [...flags, '--ignore', pattern],
101             [] as string[],
102         );
103
104         // Start the cargo watch with json message
105         this.cargoProcess = child_process.spawn(
106             'cargo',
107             ['watch', '-x', args, ...ignoreFlags],
108             {
109                 stdio: ['ignore', 'pipe', 'pipe'],
110                 cwd: vscode.workspace.rootPath,
111                 windowsVerbatimArguments: true,
112             },
113         );
114
115         const stdoutData = new LineBuffer();
116         this.cargoProcess.stdout.on('data', (s: string) => {
117             stdoutData.processOutput(s, line => {
118                 this.logInfo(line);
119                 try {
120                     this.parseLine(line);
121                 } catch (err) {
122                     this.logError(`Failed to parse: ${err}, content : ${line}`);
123                 }
124             });
125         });
126
127         const stderrData = new LineBuffer();
128         this.cargoProcess.stderr.on('data', (s: string) => {
129             stderrData.processOutput(s, line => {
130                 this.logError('Error on cargo-watch : {\n' + line + '}\n');
131             });
132         });
133
134         this.cargoProcess.on('error', (err: Error) => {
135             this.logError(
136                 'Error on cargo-watch process : {\n' + err.message + '}\n',
137             );
138         });
139
140         this.logInfo('cargo-watch started.');
141     }
142
143     public stop() {
144         if (this.cargoProcess) {
145             this.cargoProcess.kill();
146             terminate(this.cargoProcess);
147             this.cargoProcess = undefined;
148         } else {
149             vscode.window.showInformationMessage('Cargo Watch is not running');
150         }
151     }
152
153     public dispose(): void {
154         this.stop();
155
156         this.diagnosticCollection.clear();
157         this.diagnosticCollection.dispose();
158         this.outputChannel.dispose();
159         this.statusDisplay.dispose();
160         this.codeActionDispose.dispose();
161     }
162
163     private logInfo(line: string) {
164         if (Server.config.cargoWatchOptions.trace === 'verbose') {
165             this.outputChannel.append(line);
166         }
167     }
168
169     private logError(line: string) {
170         if (
171             Server.config.cargoWatchOptions.trace === 'error' ||
172             Server.config.cargoWatchOptions.trace === 'verbose'
173         ) {
174             this.outputChannel.append(line);
175         }
176     }
177
178     private parseLine(line: string) {
179         if (line.startsWith('[Running')) {
180             this.diagnosticCollection.clear();
181             this.suggestedFixCollection.clear();
182             this.statusDisplay.show();
183         }
184
185         if (line.startsWith('[Finished running')) {
186             this.statusDisplay.hide();
187         }
188
189         interface CargoArtifact {
190             reason: string;
191             package_id: string;
192         }
193
194         // https://github.com/rust-lang/cargo/blob/master/src/cargo/util/machine_message.rs
195         interface CargoMessage {
196             reason: string;
197             package_id: string;
198             message: RustDiagnostic;
199         }
200
201         // cargo-watch itself output non json format
202         // Ignore these lines
203         let data: CargoMessage;
204         try {
205             data = JSON.parse(line.trim());
206         } catch (error) {
207             this.logError(`Fail to parse to json : { ${error} }`);
208             return;
209         }
210
211         if (data.reason === 'compiler-artifact') {
212             const msg = data as CargoArtifact;
213
214             // The format of the package_id is "{name} {version} ({source_id})",
215             // https://github.com/rust-lang/cargo/blob/37ad03f86e895bb80b474c1c088322634f4725f5/src/cargo/core/package_id.rs#L53
216             this.statusDisplay.packageName = msg.package_id.split(' ')[0];
217         } else if (data.reason === 'compiler-message') {
218             const msg = data.message as RustDiagnostic;
219
220             const mapResult = mapRustDiagnosticToVsCode(msg);
221             if (!mapResult) {
222                 return;
223             }
224
225             const { location, diagnostic, suggestedFixes } = mapResult;
226             const fileUri = location.uri;
227
228             const diagnostics: vscode.Diagnostic[] = [
229                 ...(this.diagnosticCollection!.get(fileUri) || []),
230             ];
231
232             // If we're building multiple targets it's possible we've already seen this diagnostic
233             const isDuplicate = diagnostics.some(d =>
234                 areDiagnosticsEqual(d, diagnostic),
235             );
236             if (isDuplicate) {
237                 return;
238             }
239
240             diagnostics.push(diagnostic);
241             this.diagnosticCollection!.set(fileUri, diagnostics);
242
243             if (suggestedFixes.length) {
244                 for (const suggestedFix of suggestedFixes) {
245                     this.suggestedFixCollection.addSuggestedFixForDiagnostic(
246                         suggestedFix,
247                         diagnostic,
248                     );
249                 }
250
251                 // Have VsCode query us for the code actions
252                 vscode.commands.executeCommand(
253                     'vscode.executeCodeActionProvider',
254                     fileUri,
255                     diagnostic.range,
256                 );
257             }
258         }
259     }
260 }