diff options
Diffstat (limited to 'editors/code/src/commands')
-rw-r--r-- | editors/code/src/commands/cargo_watch.ts | 264 | ||||
-rw-r--r-- | editors/code/src/commands/runnables.ts | 91 | ||||
-rw-r--r-- | editors/code/src/commands/watch_status.ts | 42 |
3 files changed, 42 insertions, 355 deletions
diff --git a/editors/code/src/commands/cargo_watch.ts b/editors/code/src/commands/cargo_watch.ts deleted file mode 100644 index ac62bdd48..000000000 --- a/editors/code/src/commands/cargo_watch.ts +++ /dev/null | |||
@@ -1,264 +0,0 @@ | |||
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 + ' --message-format json'; | ||
86 | if (Server.config.cargoWatchOptions.allTargets) { | ||
87 | args += ' --all-targets'; | ||
88 | } | ||
89 | if (Server.config.cargoWatchOptions.command.length > 0) { | ||
90 | // Excape the double quote string: | ||
91 | args += ' ' + Server.config.cargoWatchOptions.arguments; | ||
92 | } | ||
93 | // Windows handles arguments differently than the unix-likes, so we need to wrap the args in double quotes | ||
94 | if (process.platform === 'win32') { | ||
95 | args = '"' + args + '"'; | ||
96 | } | ||
97 | |||
98 | const ignoreFlags = Server.config.cargoWatchOptions.ignore.reduce( | ||
99 | (flags, pattern) => [...flags, '--ignore', pattern], | ||
100 | [] as string[], | ||
101 | ); | ||
102 | |||
103 | // Start the cargo watch with json message | ||
104 | this.cargoProcess = child_process.spawn( | ||
105 | 'cargo', | ||
106 | ['watch', '-x', args, ...ignoreFlags], | ||
107 | { | ||
108 | stdio: ['ignore', 'pipe', 'pipe'], | ||
109 | cwd: vscode.workspace.rootPath, | ||
110 | windowsVerbatimArguments: true, | ||
111 | }, | ||
112 | ); | ||
113 | |||
114 | if (!this.cargoProcess) { | ||
115 | vscode.window.showErrorMessage('Cargo Watch failed to start'); | ||
116 | return; | ||
117 | } | ||
118 | |||
119 | const stdoutData = new LineBuffer(); | ||
120 | this.cargoProcess.stdout?.on('data', (s: string) => { | ||
121 | stdoutData.processOutput(s, line => { | ||
122 | this.logInfo(line); | ||
123 | try { | ||
124 | this.parseLine(line); | ||
125 | } catch (err) { | ||
126 | this.logError(`Failed to parse: ${err}, content : ${line}`); | ||
127 | } | ||
128 | }); | ||
129 | }); | ||
130 | |||
131 | const stderrData = new LineBuffer(); | ||
132 | this.cargoProcess.stderr?.on('data', (s: string) => { | ||
133 | stderrData.processOutput(s, line => { | ||
134 | this.logError('Error on cargo-watch : {\n' + line + '}\n'); | ||
135 | }); | ||
136 | }); | ||
137 | |||
138 | this.cargoProcess.on('error', (err: Error) => { | ||
139 | this.logError( | ||
140 | 'Error on cargo-watch process : {\n' + err.message + '}\n', | ||
141 | ); | ||
142 | }); | ||
143 | |||
144 | this.logInfo('cargo-watch started.'); | ||
145 | } | ||
146 | |||
147 | public stop() { | ||
148 | if (this.cargoProcess) { | ||
149 | this.cargoProcess.kill(); | ||
150 | terminate(this.cargoProcess); | ||
151 | this.cargoProcess = undefined; | ||
152 | } else { | ||
153 | vscode.window.showInformationMessage('Cargo Watch is not running'); | ||
154 | } | ||
155 | } | ||
156 | |||
157 | public dispose(): void { | ||
158 | this.stop(); | ||
159 | |||
160 | this.diagnosticCollection.clear(); | ||
161 | this.diagnosticCollection.dispose(); | ||
162 | this.outputChannel.dispose(); | ||
163 | this.statusDisplay.dispose(); | ||
164 | this.codeActionDispose.dispose(); | ||
165 | } | ||
166 | |||
167 | private logInfo(line: string) { | ||
168 | if (Server.config.cargoWatchOptions.trace === 'verbose') { | ||
169 | this.outputChannel.append(line); | ||
170 | } | ||
171 | } | ||
172 | |||
173 | private logError(line: string) { | ||
174 | if ( | ||
175 | Server.config.cargoWatchOptions.trace === 'error' || | ||
176 | Server.config.cargoWatchOptions.trace === 'verbose' | ||
177 | ) { | ||
178 | this.outputChannel.append(line); | ||
179 | } | ||
180 | } | ||
181 | |||
182 | private parseLine(line: string) { | ||
183 | if (line.startsWith('[Running')) { | ||
184 | this.diagnosticCollection.clear(); | ||
185 | this.suggestedFixCollection.clear(); | ||
186 | this.statusDisplay.show(); | ||
187 | } | ||
188 | |||
189 | if (line.startsWith('[Finished running')) { | ||
190 | this.statusDisplay.hide(); | ||
191 | } | ||
192 | |||
193 | interface CargoArtifact { | ||
194 | reason: string; | ||
195 | package_id: string; | ||
196 | } | ||
197 | |||
198 | // https://github.com/rust-lang/cargo/blob/master/src/cargo/util/machine_message.rs | ||
199 | interface CargoMessage { | ||
200 | reason: string; | ||
201 | package_id: string; | ||
202 | message: RustDiagnostic; | ||
203 | } | ||
204 | |||
205 | // cargo-watch itself output non json format | ||
206 | // Ignore these lines | ||
207 | let data: CargoMessage; | ||
208 | try { | ||
209 | data = JSON.parse(line.trim()); | ||
210 | } catch (error) { | ||
211 | this.logError(`Fail to parse to json : { ${error} }`); | ||
212 | return; | ||
213 | } | ||
214 | |||
215 | if (data.reason === 'compiler-artifact') { | ||
216 | const msg = data as CargoArtifact; | ||
217 | |||
218 | // The format of the package_id is "{name} {version} ({source_id})", | ||
219 | // https://github.com/rust-lang/cargo/blob/37ad03f86e895bb80b474c1c088322634f4725f5/src/cargo/core/package_id.rs#L53 | ||
220 | this.statusDisplay.packageName = msg.package_id.split(' ')[0]; | ||
221 | } else if (data.reason === 'compiler-message') { | ||
222 | const msg = data.message as RustDiagnostic; | ||
223 | |||
224 | const mapResult = mapRustDiagnosticToVsCode(msg); | ||
225 | if (!mapResult) { | ||
226 | return; | ||
227 | } | ||
228 | |||
229 | const { location, diagnostic, suggestedFixes } = mapResult; | ||
230 | const fileUri = location.uri; | ||
231 | |||
232 | const diagnostics: vscode.Diagnostic[] = [ | ||
233 | ...(this.diagnosticCollection!.get(fileUri) || []), | ||
234 | ]; | ||
235 | |||
236 | // If we're building multiple targets it's possible we've already seen this diagnostic | ||
237 | const isDuplicate = diagnostics.some(d => | ||
238 | areDiagnosticsEqual(d, diagnostic), | ||
239 | ); | ||
240 | if (isDuplicate) { | ||
241 | return; | ||
242 | } | ||
243 | |||
244 | diagnostics.push(diagnostic); | ||
245 | this.diagnosticCollection!.set(fileUri, diagnostics); | ||
246 | |||
247 | if (suggestedFixes.length) { | ||
248 | for (const suggestedFix of suggestedFixes) { | ||
249 | this.suggestedFixCollection.addSuggestedFixForDiagnostic( | ||
250 | suggestedFix, | ||
251 | diagnostic, | ||
252 | ); | ||
253 | } | ||
254 | |||
255 | // Have VsCode query us for the code actions | ||
256 | vscode.commands.executeCommand( | ||
257 | 'vscode.executeCodeActionProvider', | ||
258 | fileUri, | ||
259 | diagnostic.range, | ||
260 | ); | ||
261 | } | ||
262 | } | ||
263 | } | ||
264 | } | ||
diff --git a/editors/code/src/commands/runnables.ts b/editors/code/src/commands/runnables.ts index cf980e257..7728541de 100644 --- a/editors/code/src/commands/runnables.ts +++ b/editors/code/src/commands/runnables.ts | |||
@@ -1,11 +1,7 @@ | |||
1 | import * as child_process from 'child_process'; | ||
2 | |||
3 | import * as util from 'util'; | ||
4 | import * as vscode from 'vscode'; | 1 | import * as vscode from 'vscode'; |
5 | import * as lc from 'vscode-languageclient'; | 2 | import * as lc from 'vscode-languageclient'; |
6 | 3 | ||
7 | import { Server } from '../server'; | 4 | import { Server } from '../server'; |
8 | import { CargoWatchProvider, registerCargoWatchProvider } from './cargo_watch'; | ||
9 | 5 | ||
10 | interface RunnablesParams { | 6 | interface RunnablesParams { |
11 | textDocument: lc.TextDocumentIdentifier; | 7 | textDocument: lc.TextDocumentIdentifier; |
@@ -131,90 +127,3 @@ export async function handleSingle(runnable: Runnable) { | |||
131 | 127 | ||
132 | return vscode.tasks.executeTask(task); | 128 | return vscode.tasks.executeTask(task); |
133 | } | 129 | } |
134 | |||
135 | /** | ||
136 | * Interactively asks the user whether we should run `cargo check` in order to | ||
137 | * provide inline diagnostics; the user is met with a series of dialog boxes | ||
138 | * that, when accepted, allow us to `cargo install cargo-watch` and then run it. | ||
139 | */ | ||
140 | export async function interactivelyStartCargoWatch( | ||
141 | context: vscode.ExtensionContext, | ||
142 | ): Promise<CargoWatchProvider | undefined> { | ||
143 | if (Server.config.cargoWatchOptions.enableOnStartup === 'disabled') { | ||
144 | return; | ||
145 | } | ||
146 | |||
147 | if (Server.config.cargoWatchOptions.enableOnStartup === 'ask') { | ||
148 | const watch = await vscode.window.showInformationMessage( | ||
149 | 'Start watching changes with cargo? (Executes `cargo watch`, provides inline diagnostics)', | ||
150 | 'yes', | ||
151 | 'no', | ||
152 | ); | ||
153 | if (watch !== 'yes') { | ||
154 | return; | ||
155 | } | ||
156 | } | ||
157 | |||
158 | return startCargoWatch(context); | ||
159 | } | ||
160 | |||
161 | export async function startCargoWatch( | ||
162 | context: vscode.ExtensionContext, | ||
163 | ): Promise<CargoWatchProvider | undefined> { | ||
164 | const execPromise = util.promisify(child_process.exec); | ||
165 | |||
166 | const { stderr, code = 0 } = await execPromise( | ||
167 | 'cargo watch --version', | ||
168 | ).catch(e => e); | ||
169 | |||
170 | if (stderr.includes('no such subcommand: `watch`')) { | ||
171 | const msg = | ||
172 | 'The `cargo-watch` subcommand is not installed. Install? (takes ~1-2 minutes)'; | ||
173 | const install = await vscode.window.showInformationMessage( | ||
174 | msg, | ||
175 | 'yes', | ||
176 | 'no', | ||
177 | ); | ||
178 | if (install !== 'yes') { | ||
179 | return; | ||
180 | } | ||
181 | |||
182 | const label = 'install-cargo-watch'; | ||
183 | const taskFinished = new Promise((resolve, _reject) => { | ||
184 | const disposable = vscode.tasks.onDidEndTask(({ execution }) => { | ||
185 | if (execution.task.name === label) { | ||
186 | disposable.dispose(); | ||
187 | resolve(); | ||
188 | } | ||
189 | }); | ||
190 | }); | ||
191 | |||
192 | vscode.tasks.executeTask( | ||
193 | createTask({ | ||
194 | label, | ||
195 | bin: 'cargo', | ||
196 | args: ['install', 'cargo-watch'], | ||
197 | env: {}, | ||
198 | }), | ||
199 | ); | ||
200 | await taskFinished; | ||
201 | const output = await execPromise('cargo watch --version').catch(e => e); | ||
202 | if (output.stderr !== '') { | ||
203 | vscode.window.showErrorMessage( | ||
204 | `Couldn't install \`cargo-\`watch: ${output.stderr}`, | ||
205 | ); | ||
206 | return; | ||
207 | } | ||
208 | } else if (code !== 0) { | ||
209 | vscode.window.showErrorMessage( | ||
210 | `\`cargo watch\` failed with ${code}: ${stderr}`, | ||
211 | ); | ||
212 | return; | ||
213 | } | ||
214 | |||
215 | const provider = await registerCargoWatchProvider(context.subscriptions); | ||
216 | if (provider) { | ||
217 | provider.start(); | ||
218 | } | ||
219 | return provider; | ||
220 | } | ||
diff --git a/editors/code/src/commands/watch_status.ts b/editors/code/src/commands/watch_status.ts index 8d64394c7..10787b510 100644 --- a/editors/code/src/commands/watch_status.ts +++ b/editors/code/src/commands/watch_status.ts | |||
@@ -57,7 +57,49 @@ export class StatusDisplay implements vscode.Disposable { | |||
57 | this.statusBarItem.dispose(); | 57 | this.statusBarItem.dispose(); |
58 | } | 58 | } |
59 | 59 | ||
60 | public handleProgressNotification(params: ProgressParams) { | ||
61 | const { token, value } = params; | ||
62 | if (token !== 'rustAnalyzer/cargoWatcher') { | ||
63 | return; | ||
64 | } | ||
65 | |||
66 | switch (value.kind) { | ||
67 | case 'begin': | ||
68 | this.show(); | ||
69 | break; | ||
70 | |||
71 | case 'report': | ||
72 | if (value.message) { | ||
73 | this.packageName = value.message; | ||
74 | } | ||
75 | break; | ||
76 | |||
77 | case 'end': | ||
78 | this.hide(); | ||
79 | break; | ||
80 | } | ||
81 | } | ||
82 | |||
60 | private frame() { | 83 | private frame() { |
61 | return spinnerFrames[(this.i = ++this.i % spinnerFrames.length)]; | 84 | return spinnerFrames[(this.i = ++this.i % spinnerFrames.length)]; |
62 | } | 85 | } |
63 | } | 86 | } |
87 | |||
88 | // FIXME: Replace this once vscode-languageclient is updated to LSP 3.15 | ||
89 | interface ProgressParams { | ||
90 | token: string; | ||
91 | value: WorkDoneProgress; | ||
92 | } | ||
93 | |||
94 | enum WorkDoneProgressKind { | ||
95 | Begin = 'begin', | ||
96 | Report = 'report', | ||
97 | End = 'end', | ||
98 | } | ||
99 | |||
100 | interface WorkDoneProgress { | ||
101 | kind: WorkDoneProgressKind; | ||
102 | message?: string; | ||
103 | cancelable?: boolean; | ||
104 | percentage?: string; | ||
105 | } | ||