aboutsummaryrefslogtreecommitdiff
path: root/editors/code/src/commands
diff options
context:
space:
mode:
Diffstat (limited to 'editors/code/src/commands')
-rw-r--r--editors/code/src/commands/cargo_watch.ts238
-rw-r--r--editors/code/src/commands/line_buffer.ts16
-rw-r--r--editors/code/src/commands/runnables.ts33
-rw-r--r--editors/code/src/commands/watch_status.ts51
4 files changed, 314 insertions, 24 deletions
diff --git a/editors/code/src/commands/cargo_watch.ts b/editors/code/src/commands/cargo_watch.ts
new file mode 100644
index 000000000..32bd38a1c
--- /dev/null
+++ b/editors/code/src/commands/cargo_watch.ts
@@ -0,0 +1,238 @@
1import * as child_process from 'child_process';
2import * as fs from 'fs';
3import * as path from 'path';
4import * as vscode from 'vscode';
5import { Server } from '../server';
6import { terminate } from '../utils/processes';
7import { LineBuffer } from './line_buffer';
8import { StatusDisplay } from './watch_status';
9
10export class CargoWatchProvider {
11 private diagnosticCollection?: vscode.DiagnosticCollection;
12 private cargoProcess?: child_process.ChildProcess;
13 private outBuffer: string = '';
14 private statusDisplay?: StatusDisplay;
15 private outputChannel?: vscode.OutputChannel;
16
17 public activate(subscriptions: vscode.Disposable[]) {
18 let cargoExists = false;
19 const cargoTomlFile = path.join(
20 vscode.workspace.rootPath!,
21 'Cargo.toml'
22 );
23 // Check if the working directory is valid cargo root path
24 try {
25 if (fs.existsSync(cargoTomlFile)) {
26 cargoExists = true;
27 }
28 } catch (err) {
29 cargoExists = false;
30 }
31
32 if (!cargoExists) {
33 vscode.window.showErrorMessage(
34 `Couldn\'t find \'Cargo.toml\' in ${cargoTomlFile}`
35 );
36 return;
37 }
38
39 subscriptions.push(this);
40 this.diagnosticCollection = vscode.languages.createDiagnosticCollection(
41 'rustc'
42 );
43
44 this.statusDisplay = new StatusDisplay(subscriptions);
45 this.outputChannel = vscode.window.createOutputChannel(
46 'Cargo Watch Trace'
47 );
48
49 let args = 'check --message-format json';
50 if (Server.config.cargoWatchOptions.checkArguments.length > 0) {
51 // Excape the double quote string:
52 args += ' ' + Server.config.cargoWatchOptions.checkArguments;
53 }
54 // Windows handles arguments differently than the unix-likes, so we need to wrap the args in double quotes
55 if (process.platform === 'win32') {
56 args = '"' + args + '"';
57 }
58
59 // Start the cargo watch with json message
60 this.cargoProcess = child_process.spawn(
61 'cargo',
62 ['watch', '-x', args],
63 {
64 stdio: ['ignore', 'pipe', 'pipe'],
65 cwd: vscode.workspace.rootPath,
66 windowsVerbatimArguments: true
67 }
68 );
69
70 const stdoutData = new LineBuffer();
71 this.cargoProcess.stdout.on('data', (s: string) => {
72 stdoutData.processOutput(s, line => {
73 this.logInfo(line);
74 try {
75 this.parseLine(line);
76 } catch (err) {
77 this.logError(`Failed to parse: ${err}, content : ${line}`);
78 }
79 });
80 });
81
82 const stderrData = new LineBuffer();
83 this.cargoProcess.stderr.on('data', (s: string) => {
84 stderrData.processOutput(s, line => {
85 this.logError('Error on cargo-watch : {\n' + line + '}\n');
86 });
87 });
88
89 this.cargoProcess.on('error', (err: Error) => {
90 this.logError(
91 'Error on cargo-watch process : {\n' + err.message + '}\n'
92 );
93 });
94
95 this.logInfo('cargo-watch started.');
96 }
97
98 public dispose(): void {
99 if (this.diagnosticCollection) {
100 this.diagnosticCollection.clear();
101 this.diagnosticCollection.dispose();
102 }
103
104 if (this.cargoProcess) {
105 this.cargoProcess.kill();
106 terminate(this.cargoProcess);
107 }
108
109 if (this.outputChannel) {
110 this.outputChannel.dispose();
111 }
112 }
113
114 private logInfo(line: string) {
115 if (Server.config.cargoWatchOptions.trace === 'verbose') {
116 this.outputChannel!.append(line);
117 }
118 }
119
120 private logError(line: string) {
121 if (
122 Server.config.cargoWatchOptions.trace === 'error' ||
123 Server.config.cargoWatchOptions.trace === 'verbose'
124 ) {
125 this.outputChannel!.append(line);
126 }
127 }
128
129 private parseLine(line: string) {
130 if (line.startsWith('[Running')) {
131 this.diagnosticCollection!.clear();
132 this.statusDisplay!.show();
133 }
134
135 if (line.startsWith('[Finished running')) {
136 this.statusDisplay!.hide();
137 }
138
139 function getLevel(s: string): vscode.DiagnosticSeverity {
140 if (s === 'error') {
141 return vscode.DiagnosticSeverity.Error;
142 }
143 if (s.startsWith('warn')) {
144 return vscode.DiagnosticSeverity.Warning;
145 }
146 return vscode.DiagnosticSeverity.Information;
147 }
148
149 // Reference:
150 // https://github.com/rust-lang/rust/blob/master/src/libsyntax/json.rs
151 interface RustDiagnosticSpan {
152 line_start: number;
153 line_end: number;
154 column_start: number;
155 column_end: number;
156 is_primary: boolean;
157 file_name: string;
158 }
159
160 interface RustDiagnostic {
161 spans: RustDiagnosticSpan[];
162 rendered: string;
163 level: string;
164 code?: {
165 code: string;
166 };
167 }
168
169 interface CargoArtifact {
170 reason: string;
171 package_id: string;
172 }
173
174 // https://github.com/rust-lang/cargo/blob/master/src/cargo/util/machine_message.rs
175 interface CargoMessage {
176 reason: string;
177 package_id: string;
178 message: RustDiagnostic;
179 }
180
181 // cargo-watch itself output non json format
182 // Ignore these lines
183 let data: CargoMessage;
184 try {
185 data = JSON.parse(line.trim());
186 } catch (error) {
187 this.logError(`Fail to parse to json : { ${error} }`);
188 return;
189 }
190
191 if (data.reason === 'compiler-artifact') {
192 const msg = data as CargoArtifact;
193
194 // The format of the package_id is "{name} {version} ({source_id})",
195 // https://github.com/rust-lang/cargo/blob/37ad03f86e895bb80b474c1c088322634f4725f5/src/cargo/core/package_id.rs#L53
196 this.statusDisplay!.packageName = msg.package_id.split(' ')[0];
197 } else if (data.reason === 'compiler-message') {
198 const msg = data.message as RustDiagnostic;
199
200 const spans = msg.spans.filter(o => o.is_primary);
201
202 // We only handle primary span right now.
203 if (spans.length > 0) {
204 const o = spans[0];
205
206 const rendered = msg.rendered;
207 const level = getLevel(msg.level);
208 const range = new vscode.Range(
209 new vscode.Position(o.line_start - 1, o.column_start - 1),
210 new vscode.Position(o.line_end - 1, o.column_end - 1)
211 );
212
213 const fileName = path.join(
214 vscode.workspace.rootPath!,
215 o.file_name
216 );
217 const diagnostic = new vscode.Diagnostic(
218 range,
219 rendered,
220 level
221 );
222
223 diagnostic.source = 'rustc';
224 diagnostic.code = msg.code ? msg.code.code : undefined;
225 diagnostic.relatedInformation = [];
226
227 const fileUrl = vscode.Uri.file(fileName!);
228
229 const diagnostics: vscode.Diagnostic[] = [
230 ...(this.diagnosticCollection!.get(fileUrl) || [])
231 ];
232 diagnostics.push(diagnostic);
233
234 this.diagnosticCollection!.set(fileUrl, diagnostics);
235 }
236 }
237 }
238}
diff --git a/editors/code/src/commands/line_buffer.ts b/editors/code/src/commands/line_buffer.ts
new file mode 100644
index 000000000..fb5b9f7f2
--- /dev/null
+++ b/editors/code/src/commands/line_buffer.ts
@@ -0,0 +1,16 @@
1export class LineBuffer {
2 private outBuffer: string = '';
3
4 public processOutput(chunk: string, cb: (line: string) => void) {
5 this.outBuffer += chunk;
6 let eolIndex = this.outBuffer.indexOf('\n');
7 while (eolIndex >= 0) {
8 // line includes the EOL
9 const line = this.outBuffer.slice(0, eolIndex + 1);
10 cb(line);
11 this.outBuffer = this.outBuffer.slice(eolIndex + 1);
12
13 eolIndex = this.outBuffer.indexOf('\n');
14 }
15 }
16}
diff --git a/editors/code/src/commands/runnables.ts b/editors/code/src/commands/runnables.ts
index 4187ef4d1..3589edcee 100644
--- a/editors/code/src/commands/runnables.ts
+++ b/editors/code/src/commands/runnables.ts
@@ -1,9 +1,11 @@
1import * as child_process from 'child_process'; 1import * as child_process from 'child_process';
2
2import * as util from 'util'; 3import * as util from 'util';
3import * as vscode from 'vscode'; 4import * as vscode from 'vscode';
4import * as lc from 'vscode-languageclient'; 5import * as lc from 'vscode-languageclient';
5 6
6import { Server } from '../server'; 7import { Server } from '../server';
8import { CargoWatchProvider } from './cargo_watch';
7 9
8interface RunnablesParams { 10interface RunnablesParams {
9 textDocument: lc.TextDocumentIdentifier; 11 textDocument: lc.TextDocumentIdentifier;
@@ -127,37 +129,19 @@ export async function handleSingle(runnable: Runnable) {
127 return vscode.tasks.executeTask(task); 129 return vscode.tasks.executeTask(task);
128} 130}
129 131
130export const autoCargoWatchTask: vscode.Task = {
131 name: 'cargo watch',
132 source: 'rust-analyzer',
133 definition: {
134 type: 'watch'
135 },
136 execution: new vscode.ShellExecution('cargo', ['watch'], { cwd: '.' }),
137
138 isBackground: true,
139 problemMatchers: ['$rustc-watch'],
140 presentationOptions: {
141 clear: true
142 },
143 // Not yet exposed in the vscode.d.ts
144 // https://github.com/Microsoft/vscode/blob/ea7c31d770e04b51d586b0d3944f3a7feb03afb9/src/vs/workbench/contrib/tasks/common/tasks.ts#L444-L456
145 runOptions: ({
146 runOn: 2 // RunOnOptions.folderOpen
147 } as unknown) as vscode.RunOptions
148};
149
150/** 132/**
151 * Interactively asks the user whether we should run `cargo check` in order to 133 * Interactively asks the user whether we should run `cargo check` in order to
152 * provide inline diagnostics; the user is met with a series of dialog boxes 134 * provide inline diagnostics; the user is met with a series of dialog boxes
153 * that, when accepted, allow us to `cargo install cargo-watch` and then run it. 135 * that, when accepted, allow us to `cargo install cargo-watch` and then run it.
154 */ 136 */
155export async function interactivelyStartCargoWatch() { 137export async function interactivelyStartCargoWatch(
156 if (Server.config.enableCargoWatchOnStartup === 'disabled') { 138 context: vscode.ExtensionContext
139) {
140 if (Server.config.cargoWatchOptions.enableOnStartup === 'disabled') {
157 return; 141 return;
158 } 142 }
159 143
160 if (Server.config.enableCargoWatchOnStartup === 'ask') { 144 if (Server.config.cargoWatchOptions.enableOnStartup === 'ask') {
161 const watch = await vscode.window.showInformationMessage( 145 const watch = await vscode.window.showInformationMessage(
162 'Start watching changes with cargo? (Executes `cargo watch`, provides inline diagnostics)', 146 'Start watching changes with cargo? (Executes `cargo watch`, provides inline diagnostics)',
163 'yes', 147 'yes',
@@ -212,5 +196,6 @@ export async function interactivelyStartCargoWatch() {
212 } 196 }
213 } 197 }
214 198
215 vscode.tasks.executeTask(autoCargoWatchTask); 199 const validater = new CargoWatchProvider();
200 validater.activate(context.subscriptions);
216} 201}
diff --git a/editors/code/src/commands/watch_status.ts b/editors/code/src/commands/watch_status.ts
new file mode 100644
index 000000000..86ae821de
--- /dev/null
+++ b/editors/code/src/commands/watch_status.ts
@@ -0,0 +1,51 @@
1import * as vscode from 'vscode';
2
3const spinnerFrames = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
4
5export class StatusDisplay {
6 public packageName?: string;
7
8 private i = 0;
9 private statusBarItem: vscode.StatusBarItem;
10 private timer?: NodeJS.Timeout;
11
12 constructor(subscriptions: vscode.Disposable[]) {
13 this.statusBarItem = vscode.window.createStatusBarItem(
14 vscode.StatusBarAlignment.Left,
15 10
16 );
17 subscriptions.push(this.statusBarItem);
18 this.statusBarItem.hide();
19 }
20
21 public show() {
22 this.packageName = undefined;
23
24 this.timer =
25 this.timer ||
26 setInterval(() => {
27 if (this.packageName) {
28 this.statusBarItem!.text = `cargo check [${
29 this.packageName
30 }] ${this.frame()}`;
31 } else {
32 this.statusBarItem!.text = `cargo check ${this.frame()}`;
33 }
34 }, 300);
35
36 this.statusBarItem!.show();
37 }
38
39 public hide() {
40 if (this.timer) {
41 clearInterval(this.timer);
42 this.timer = undefined;
43 }
44
45 this.statusBarItem!.hide();
46 }
47
48 private frame() {
49 return spinnerFrames[(this.i = ++this.i % spinnerFrames.length)];
50 }
51}