aboutsummaryrefslogtreecommitdiff
path: root/editors/code/src
diff options
context:
space:
mode:
Diffstat (limited to 'editors/code/src')
-rw-r--r--editors/code/src/commands/cargo_watch.ts211
-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.ts41
-rw-r--r--editors/code/src/config.ts34
-rw-r--r--editors/code/src/extension.ts2
-rw-r--r--editors/code/src/utils/processes.ts51
-rw-r--r--editors/code/src/utils/terminateProcess.sh12
8 files changed, 370 insertions, 30 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..6d8e4d885
--- /dev/null
+++ b/editors/code/src/commands/cargo_watch.ts
@@ -0,0 +1,211 @@
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 args += '"';
55
56 // Start the cargo watch with json message
57 this.cargoProcess = child_process.spawn(
58 'cargo',
59 ['watch', '-x', args],
60 {
61 stdio: ['ignore', 'pipe', 'pipe'],
62 cwd: vscode.workspace.rootPath,
63 windowsVerbatimArguments: true
64 }
65 );
66
67 const stdoutData = new LineBuffer();
68 this.cargoProcess.stdout.on('data', (s: string) => {
69 stdoutData.processOutput(s, line => {
70 this.logInfo(line);
71 this.parseLine(line);
72 });
73 });
74
75 const stderrData = new LineBuffer();
76 this.cargoProcess.stderr.on('data', (s: string) => {
77 stderrData.processOutput(s, line => {
78 this.logError('Error on cargo-watch : {\n' + line + '}\n');
79 });
80 });
81
82 this.cargoProcess.on('error', (err: Error) => {
83 this.logError(
84 'Error on cargo-watch process : {\n' + err.message + '}\n'
85 );
86 });
87
88 this.logInfo('cargo-watch started.');
89 }
90
91 public dispose(): void {
92 if (this.diagnosticCollection) {
93 this.diagnosticCollection.clear();
94 this.diagnosticCollection.dispose();
95 }
96
97 if (this.cargoProcess) {
98 this.cargoProcess.kill();
99 terminate(this.cargoProcess);
100 }
101
102 if (this.outputChannel) {
103 this.outputChannel.dispose();
104 }
105 }
106
107 private logInfo(line: string) {
108 if (Server.config.cargoWatchOptions.trace === 'verbose') {
109 this.outputChannel!.append(line);
110 }
111 }
112
113 private logError(line: string) {
114 if (
115 Server.config.cargoWatchOptions.trace === 'error' ||
116 Server.config.cargoWatchOptions.trace === 'verbose'
117 ) {
118 this.outputChannel!.append(line);
119 }
120 }
121
122 private parseLine(line: string) {
123 if (line.startsWith('[Running')) {
124 this.diagnosticCollection!.clear();
125 this.statusDisplay!.show();
126 }
127
128 if (line.startsWith('[Finished running')) {
129 this.statusDisplay!.hide();
130 }
131
132 function getLevel(s: string): vscode.DiagnosticSeverity {
133 if (s === 'error') {
134 return vscode.DiagnosticSeverity.Error;
135 }
136
137 if (s.startsWith('warn')) {
138 return vscode.DiagnosticSeverity.Warning;
139 }
140
141 return vscode.DiagnosticSeverity.Information;
142 }
143
144 interface ErrorSpan {
145 line_start: number;
146 line_end: number;
147 column_start: number;
148 column_end: number;
149 }
150
151 interface ErrorMessage {
152 reason: string;
153 message: {
154 spans: ErrorSpan[];
155 rendered: string;
156 level: string;
157 code?: {
158 code: string;
159 };
160 };
161 }
162
163 // cargo-watch itself output non json format
164 // Ignore these lines
165 let data: ErrorMessage;
166 try {
167 data = JSON.parse(line.trim());
168 } catch (error) {
169 this.logError(`Fail to pass to json : { ${error} }`);
170 return;
171 }
172
173 // Only handle compiler-message now
174 if (data.reason !== 'compiler-message') {
175 return;
176 }
177
178 let spans: any[] = data.message.spans;
179 spans = spans.filter(o => o.is_primary);
180
181 // We only handle primary span right now.
182 if (spans.length > 0) {
183 const o = spans[0];
184
185 const rendered = data.message.rendered;
186 const level = getLevel(data.message.level);
187 const range = new vscode.Range(
188 new vscode.Position(o.line_start - 1, o.column_start - 1),
189 new vscode.Position(o.line_end - 1, o.column_end - 1)
190 );
191
192 const fileName = path.join(vscode.workspace.rootPath!, o.file_name);
193 const diagnostic = new vscode.Diagnostic(range, rendered, level);
194
195 diagnostic.source = 'rustc';
196 diagnostic.code = data.message.code
197 ? data.message.code.code
198 : undefined;
199 diagnostic.relatedInformation = [];
200
201 const fileUrl = vscode.Uri.file(fileName!);
202
203 const diagnostics: vscode.Diagnostic[] = [
204 ...(this.diagnosticCollection!.get(fileUrl) || [])
205 ];
206 diagnostics.push(diagnostic);
207
208 this.diagnosticCollection!.set(fileUrl, diagnostics);
209 }
210 }
211}
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..f027d7bbc
--- /dev/null
+++ b/editors/code/src/commands/watch_status.ts
@@ -0,0 +1,41 @@
1import * as vscode from 'vscode';
2
3const spinnerFrames = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
4
5export class StatusDisplay {
6 private i = 0;
7 private statusBarItem: vscode.StatusBarItem;
8 private timer?: NodeJS.Timeout;
9
10 constructor(subscriptions: vscode.Disposable[]) {
11 this.statusBarItem = vscode.window.createStatusBarItem(
12 vscode.StatusBarAlignment.Left,
13 10
14 );
15 subscriptions.push(this.statusBarItem);
16 this.statusBarItem.hide();
17 }
18
19 public show() {
20 this.timer =
21 this.timer ||
22 setInterval(() => {
23 this.statusBarItem!.text = 'cargo check ' + this.frame();
24 }, 300);
25
26 this.statusBarItem!.show();
27 }
28
29 public hide() {
30 if (this.timer) {
31 clearInterval(this.timer);
32 this.timer = undefined;
33 }
34
35 this.statusBarItem!.hide();
36 }
37
38 private frame() {
39 return spinnerFrames[(this.i = ++this.i % spinnerFrames.length)];
40 }
41}
diff --git a/editors/code/src/config.ts b/editors/code/src/config.ts
index 420589068..481a5e5f1 100644
--- a/editors/code/src/config.ts
+++ b/editors/code/src/config.ts
@@ -4,14 +4,25 @@ import { Server } from './server';
4 4
5const RA_LSP_DEBUG = process.env.__RA_LSP_SERVER_DEBUG; 5const RA_LSP_DEBUG = process.env.__RA_LSP_SERVER_DEBUG;
6 6
7export type CargoWatchOptions = 'ask' | 'enabled' | 'disabled'; 7export type CargoWatchStartupOptions = 'ask' | 'enabled' | 'disabled';
8export type CargoWatchTraceOptions = 'off' | 'error' | 'verbose';
9
10export interface CargoWatchOptions {
11 enableOnStartup: CargoWatchStartupOptions;
12 checkArguments: string;
13 trace: CargoWatchTraceOptions;
14}
8 15
9export class Config { 16export class Config {
10 public highlightingOn = true; 17 public highlightingOn = true;
11 public enableEnhancedTyping = true; 18 public enableEnhancedTyping = true;
12 public raLspServerPath = RA_LSP_DEBUG || 'ra_lsp_server'; 19 public raLspServerPath = RA_LSP_DEBUG || 'ra_lsp_server';
13 public showWorkspaceLoadedNotification = true; 20 public showWorkspaceLoadedNotification = true;
14 public enableCargoWatchOnStartup: CargoWatchOptions = 'ask'; 21 public cargoWatchOptions: CargoWatchOptions = {
22 enableOnStartup: 'ask',
23 trace: 'off',
24 checkArguments: ''
25 };
15 26
16 private prevEnhancedTyping: null | boolean = null; 27 private prevEnhancedTyping: null | boolean = null;
17 28
@@ -73,9 +84,22 @@ export class Config {
73 } 84 }
74 85
75 if (config.has('enableCargoWatchOnStartup')) { 86 if (config.has('enableCargoWatchOnStartup')) {
76 this.enableCargoWatchOnStartup = config.get<CargoWatchOptions>( 87 this.cargoWatchOptions.enableOnStartup = config.get<
77 'enableCargoWatchOnStartup', 88 CargoWatchStartupOptions
78 'ask' 89 >('enableCargoWatchOnStartup', 'ask');
90 }
91
92 if (config.has('trace.cargo-watch')) {
93 this.cargoWatchOptions.trace = config.get<CargoWatchTraceOptions>(
94 'trace.cargo-watch',
95 'off'
96 );
97 }
98
99 if (config.has('cargo-watch.check-arguments')) {
100 this.cargoWatchOptions.checkArguments = config.get<string>(
101 'cargo-watch.check-arguments',
102 ''
79 ); 103 );
80 } 104 }
81 } 105 }
diff --git a/editors/code/src/extension.ts b/editors/code/src/extension.ts
index 2e13c87de..ef83c0b8b 100644
--- a/editors/code/src/extension.ts
+++ b/editors/code/src/extension.ts
@@ -121,7 +121,7 @@ export function activate(context: vscode.ExtensionContext) {
121 ); 121 );
122 122
123 // Executing `cargo watch` provides us with inline diagnostics on save 123 // Executing `cargo watch` provides us with inline diagnostics on save
124 interactivelyStartCargoWatch(); 124 interactivelyStartCargoWatch(context);
125 125
126 // Start the language server, finally! 126 // Start the language server, finally!
127 Server.start(allNotifications); 127 Server.start(allNotifications);
diff --git a/editors/code/src/utils/processes.ts b/editors/code/src/utils/processes.ts
new file mode 100644
index 000000000..da8be9eb1
--- /dev/null
+++ b/editors/code/src/utils/processes.ts
@@ -0,0 +1,51 @@
1'use strict';
2
3import * as cp from 'child_process';
4import ChildProcess = cp.ChildProcess;
5
6import { join } from 'path';
7
8const isWindows = process.platform === 'win32';
9const isMacintosh = process.platform === 'darwin';
10const isLinux = process.platform === 'linux';
11
12// this is very complex, but is basically copy-pased from VSCode implementation here:
13// https://github.com/Microsoft/vscode-languageserver-node/blob/dbfd37e35953ad0ee14c4eeced8cfbc41697b47e/client/src/utils/processes.ts#L15
14
15// And see discussion at
16// https://github.com/rust-analyzer/rust-analyzer/pull/1079#issuecomment-478908109
17
18export function terminate(process: ChildProcess, cwd?: string): boolean {
19 if (isWindows) {
20 try {
21 // This we run in Atom execFileSync is available.
22 // Ignore stderr since this is otherwise piped to parent.stderr
23 // which might be already closed.
24 const options: any = {
25 stdio: ['pipe', 'pipe', 'ignore']
26 };
27 if (cwd) {
28 options.cwd = cwd;
29 }
30 cp.execFileSync(
31 'taskkill',
32 ['/T', '/F', '/PID', process.pid.toString()],
33 options
34 );
35 return true;
36 } catch (err) {
37 return false;
38 }
39 } else if (isLinux || isMacintosh) {
40 try {
41 const cmd = join(__dirname, 'terminateProcess.sh');
42 const result = cp.spawnSync(cmd, [process.pid.toString()]);
43 return result.error ? false : true;
44 } catch (err) {
45 return false;
46 }
47 } else {
48 process.kill('SIGKILL');
49 return true;
50 }
51}
diff --git a/editors/code/src/utils/terminateProcess.sh b/editors/code/src/utils/terminateProcess.sh
new file mode 100644
index 000000000..2ec9e1c2e
--- /dev/null
+++ b/editors/code/src/utils/terminateProcess.sh
@@ -0,0 +1,12 @@
1#!/bin/bash
2
3terminateTree() {
4 for cpid in $(pgrep -P $1); do
5 terminateTree $cpid
6 done
7 kill -9 $1 > /dev/null 2>&1
8}
9
10for pid in $*; do
11 terminateTree $pid
12done \ No newline at end of file