]> git.lizzy.rs Git - rust.git/blob - editors/code/src/commands/cargo_watch.ts
00b24dbced4a68fae5c63c6ea0d839a68c7afdac
[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             ' --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;
90         }
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 + '"';
94         }
95
96         // Start the cargo watch with json message
97         this.cargoProcess = child_process.spawn(
98             'cargo',
99             ['watch', '-x', args],
100             {
101                 stdio: ['ignore', 'pipe', 'pipe'],
102                 cwd: vscode.workspace.rootPath,
103                 windowsVerbatimArguments: true
104             }
105         );
106
107         const stdoutData = new LineBuffer();
108         this.cargoProcess.stdout.on('data', (s: string) => {
109             stdoutData.processOutput(s, line => {
110                 this.logInfo(line);
111                 try {
112                     this.parseLine(line);
113                 } catch (err) {
114                     this.logError(`Failed to parse: ${err}, content : ${line}`);
115                 }
116             });
117         });
118
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');
123             });
124         });
125
126         this.cargoProcess.on('error', (err: Error) => {
127             this.logError(
128                 'Error on cargo-watch process : {\n' + err.message + '}\n'
129             );
130         });
131
132         this.logInfo('cargo-watch started.');
133     }
134
135     public stop() {
136         if (this.cargoProcess) {
137             this.cargoProcess.kill();
138             terminate(this.cargoProcess);
139             this.cargoProcess = undefined;
140         } else {
141             vscode.window.showInformationMessage('Cargo Watch is not running');
142         }
143     }
144
145     public dispose(): void {
146         this.stop();
147
148         this.diagnosticCollection.clear();
149         this.diagnosticCollection.dispose();
150         this.outputChannel.dispose();
151         this.statusDisplay.dispose();
152         this.codeActionDispose.dispose();
153     }
154
155     private logInfo(line: string) {
156         if (Server.config.cargoWatchOptions.trace === 'verbose') {
157             this.outputChannel.append(line);
158         }
159     }
160
161     private logError(line: string) {
162         if (
163             Server.config.cargoWatchOptions.trace === 'error' ||
164             Server.config.cargoWatchOptions.trace === 'verbose'
165         ) {
166             this.outputChannel.append(line);
167         }
168     }
169
170     private parseLine(line: string) {
171         if (line.startsWith('[Running')) {
172             this.diagnosticCollection.clear();
173             this.suggestedFixCollection.clear();
174             this.statusDisplay.show();
175         }
176
177         if (line.startsWith('[Finished running')) {
178             this.statusDisplay.hide();
179         }
180
181         interface CargoArtifact {
182             reason: string;
183             package_id: string;
184         }
185
186         // https://github.com/rust-lang/cargo/blob/master/src/cargo/util/machine_message.rs
187         interface CargoMessage {
188             reason: string;
189             package_id: string;
190             message: RustDiagnostic;
191         }
192
193         // cargo-watch itself output non json format
194         // Ignore these lines
195         let data: CargoMessage;
196         try {
197             data = JSON.parse(line.trim());
198         } catch (error) {
199             this.logError(`Fail to parse to json : { ${error} }`);
200             return;
201         }
202
203         if (data.reason === 'compiler-artifact') {
204             const msg = data as CargoArtifact;
205
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;
211
212             const mapResult = mapRustDiagnosticToVsCode(msg);
213             if (!mapResult) {
214                 return;
215             }
216
217             const { location, diagnostic, suggestedFixes } = mapResult;
218             const fileUri = location.uri;
219
220             const diagnostics: vscode.Diagnostic[] = [
221                 ...(this.diagnosticCollection!.get(fileUri) || [])
222             ];
223
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)
227             );
228             if (isDuplicate) {
229                 return;
230             }
231
232             diagnostics.push(diagnostic);
233             this.diagnosticCollection!.set(fileUri, diagnostics);
234
235             if (suggestedFixes.length) {
236                 for (const suggestedFix of suggestedFixes) {
237                     this.suggestedFixCollection.addSuggestedFixForDiagnostic(
238                         suggestedFix,
239                         diagnostic
240                     );
241                 }
242
243                 // Have VsCode query us for the code actions
244                 vscode.commands.executeCommand(
245                     'vscode.executeCodeActionProvider',
246                     fileUri,
247                     diagnostic.range
248                 );
249             }
250         }
251     }
252 }