diff options
Diffstat (limited to 'editors/code/src')
48 files changed, 1208 insertions, 3251 deletions
diff --git a/editors/code/src/client.ts b/editors/code/src/client.ts new file mode 100644 index 000000000..743384bd7 --- /dev/null +++ b/editors/code/src/client.ts | |||
@@ -0,0 +1,90 @@ | |||
1 | import { homedir } from 'os'; | ||
2 | import * as lc from 'vscode-languageclient'; | ||
3 | |||
4 | import { window, workspace } from 'vscode'; | ||
5 | import { Config } from './config'; | ||
6 | |||
7 | export function createClient(config: Config): lc.LanguageClient { | ||
8 | // '.' Is the fallback if no folder is open | ||
9 | // TODO?: Workspace folders support Uri's (eg: file://test.txt). It might be a good idea to test if the uri points to a file. | ||
10 | let folder: string = '.'; | ||
11 | if (workspace.workspaceFolders !== undefined) { | ||
12 | folder = workspace.workspaceFolders[0].uri.fsPath.toString(); | ||
13 | } | ||
14 | |||
15 | const command = expandPathResolving(config.raLspServerPath); | ||
16 | const run: lc.Executable = { | ||
17 | command, | ||
18 | options: { cwd: folder }, | ||
19 | }; | ||
20 | const serverOptions: lc.ServerOptions = { | ||
21 | run, | ||
22 | debug: run, | ||
23 | }; | ||
24 | const traceOutputChannel = window.createOutputChannel( | ||
25 | 'Rust Analyzer Language Server Trace', | ||
26 | ); | ||
27 | const clientOptions: lc.LanguageClientOptions = { | ||
28 | documentSelector: [{ scheme: 'file', language: 'rust' }], | ||
29 | initializationOptions: { | ||
30 | publishDecorations: true, | ||
31 | lruCapacity: config.lruCapacity, | ||
32 | maxInlayHintLength: config.maxInlayHintLength, | ||
33 | cargoWatchEnable: config.cargoWatchOptions.enable, | ||
34 | cargoWatchArgs: config.cargoWatchOptions.arguments, | ||
35 | cargoWatchCommand: config.cargoWatchOptions.command, | ||
36 | cargoWatchAllTargets: | ||
37 | config.cargoWatchOptions.allTargets, | ||
38 | excludeGlobs: config.excludeGlobs, | ||
39 | useClientWatching: config.useClientWatching, | ||
40 | featureFlags: config.featureFlags, | ||
41 | withSysroot: config.withSysroot, | ||
42 | cargoFeatures: config.cargoFeatures, | ||
43 | }, | ||
44 | traceOutputChannel, | ||
45 | }; | ||
46 | |||
47 | const res = new lc.LanguageClient( | ||
48 | 'rust-analyzer', | ||
49 | 'Rust Analyzer Language Server', | ||
50 | serverOptions, | ||
51 | clientOptions, | ||
52 | ); | ||
53 | |||
54 | // HACK: This is an awful way of filtering out the decorations notifications | ||
55 | // However, pending proper support, this is the most effecitve approach | ||
56 | // Proper support for this would entail a change to vscode-languageclient to allow not notifying on certain messages | ||
57 | // Or the ability to disable the serverside component of highlighting (but this means that to do tracing we need to disable hihlighting) | ||
58 | // This also requires considering our settings strategy, which is work which needs doing | ||
59 | // @ts-ignore The tracer is private to vscode-languageclient, but we need access to it to not log publishDecorations requests | ||
60 | res._tracer = { | ||
61 | log: (messageOrDataObject: string | any, data?: string) => { | ||
62 | if (typeof messageOrDataObject === 'string') { | ||
63 | if ( | ||
64 | messageOrDataObject.includes( | ||
65 | 'rust-analyzer/publishDecorations', | ||
66 | ) || | ||
67 | messageOrDataObject.includes( | ||
68 | 'rust-analyzer/decorationsRequest', | ||
69 | ) | ||
70 | ) { | ||
71 | // Don't log publish decorations requests | ||
72 | } else { | ||
73 | // @ts-ignore This is just a utility function | ||
74 | res.logTrace(messageOrDataObject, data); | ||
75 | } | ||
76 | } else { | ||
77 | // @ts-ignore | ||
78 | res.logObjectTrace(messageOrDataObject); | ||
79 | } | ||
80 | }, | ||
81 | }; | ||
82 | res.registerProposedFeatures(); | ||
83 | return res; | ||
84 | } | ||
85 | function expandPathResolving(path: string) { | ||
86 | if (path.startsWith('~/')) { | ||
87 | return path.replace('~', homedir()); | ||
88 | } | ||
89 | return path; | ||
90 | } | ||
diff --git a/editors/code/src/color_theme.ts b/editors/code/src/color_theme.ts new file mode 100644 index 000000000..cbad47f35 --- /dev/null +++ b/editors/code/src/color_theme.ts | |||
@@ -0,0 +1,123 @@ | |||
1 | import * as fs from 'fs'; | ||
2 | import * as jsonc from 'jsonc-parser'; | ||
3 | import * as path from 'path'; | ||
4 | import * as vscode from 'vscode'; | ||
5 | |||
6 | export interface TextMateRuleSettings { | ||
7 | foreground?: string; | ||
8 | background?: string; | ||
9 | fontStyle?: string; | ||
10 | } | ||
11 | |||
12 | export class ColorTheme { | ||
13 | private rules: Map<string, TextMateRuleSettings> = new Map(); | ||
14 | |||
15 | static load(): ColorTheme { | ||
16 | // Find out current color theme | ||
17 | const themeName = vscode.workspace | ||
18 | .getConfiguration('workbench') | ||
19 | .get('colorTheme'); | ||
20 | |||
21 | if (typeof themeName !== 'string') { | ||
22 | // console.warn('workbench.colorTheme is', themeName) | ||
23 | return new ColorTheme(); | ||
24 | } | ||
25 | return loadThemeNamed(themeName); | ||
26 | } | ||
27 | |||
28 | static fromRules(rules: TextMateRule[]): ColorTheme { | ||
29 | const res = new ColorTheme(); | ||
30 | for (const rule of rules) { | ||
31 | const scopes = typeof rule.scope === 'string' | ||
32 | ? [rule.scope] | ||
33 | : rule.scope; | ||
34 | for (const scope of scopes) { | ||
35 | res.rules.set(scope, rule.settings); | ||
36 | } | ||
37 | } | ||
38 | return res; | ||
39 | } | ||
40 | |||
41 | lookup(scopes: string[]): TextMateRuleSettings { | ||
42 | let res: TextMateRuleSettings = {}; | ||
43 | for (const scope of scopes) { | ||
44 | this.rules.forEach((value, key) => { | ||
45 | if (scope.startsWith(key)) { | ||
46 | res = mergeRuleSettings(res, value); | ||
47 | } | ||
48 | }); | ||
49 | } | ||
50 | return res; | ||
51 | } | ||
52 | |||
53 | mergeFrom(other: ColorTheme) { | ||
54 | other.rules.forEach((value, key) => { | ||
55 | const merged = mergeRuleSettings(this.rules.get(key), value); | ||
56 | this.rules.set(key, merged); | ||
57 | }); | ||
58 | } | ||
59 | } | ||
60 | |||
61 | function loadThemeNamed(themeName: string): ColorTheme { | ||
62 | function isTheme(extension: vscode.Extension<any>): boolean { | ||
63 | return ( | ||
64 | extension.extensionKind === vscode.ExtensionKind.UI && | ||
65 | extension.packageJSON.contributes && | ||
66 | extension.packageJSON.contributes.themes | ||
67 | ); | ||
68 | } | ||
69 | |||
70 | let themePaths = vscode.extensions.all | ||
71 | .filter(isTheme) | ||
72 | .flatMap(ext => { | ||
73 | return ext.packageJSON.contributes.themes | ||
74 | .filter((it: any) => (it.id || it.label) === themeName) | ||
75 | .map((it: any) => path.join(ext.extensionPath, it.path)); | ||
76 | }); | ||
77 | |||
78 | const res = new ColorTheme(); | ||
79 | for (const themePath of themePaths) { | ||
80 | res.mergeFrom(loadThemeFile(themePath)); | ||
81 | } | ||
82 | |||
83 | const customizations: any = vscode.workspace.getConfiguration('editor').get('tokenColorCustomizations'); | ||
84 | res.mergeFrom(ColorTheme.fromRules(customizations?.textMateRules ?? [])); | ||
85 | |||
86 | return res; | ||
87 | } | ||
88 | |||
89 | function loadThemeFile(themePath: string): ColorTheme { | ||
90 | let text; | ||
91 | try { | ||
92 | text = fs.readFileSync(themePath, 'utf8'); | ||
93 | } catch { | ||
94 | return new ColorTheme(); | ||
95 | } | ||
96 | const obj = jsonc.parse(text); | ||
97 | const tokenColors = obj?.tokenColors ?? []; | ||
98 | const res = ColorTheme.fromRules(tokenColors); | ||
99 | |||
100 | for (const include in obj?.include ?? []) { | ||
101 | const includePath = path.join(path.dirname(themePath), include); | ||
102 | const tmp = loadThemeFile(includePath); | ||
103 | res.mergeFrom(tmp); | ||
104 | } | ||
105 | |||
106 | return res; | ||
107 | } | ||
108 | |||
109 | interface TextMateRule { | ||
110 | scope: string | string[]; | ||
111 | settings: TextMateRuleSettings; | ||
112 | } | ||
113 | |||
114 | function mergeRuleSettings( | ||
115 | defaultSetting: TextMateRuleSettings | undefined, | ||
116 | override: TextMateRuleSettings, | ||
117 | ): TextMateRuleSettings { | ||
118 | return { | ||
119 | foreground: override.foreground ?? defaultSetting?.foreground, | ||
120 | background: override.background ?? defaultSetting?.background, | ||
121 | fontStyle: override.fontStyle ?? defaultSetting?.fontStyle, | ||
122 | }; | ||
123 | } | ||
diff --git a/editors/code/src/commands/analyzer_status.ts b/editors/code/src/commands/analyzer_status.ts index 2777ced24..cfe7d1af0 100644 --- a/editors/code/src/commands/analyzer_status.ts +++ b/editors/code/src/commands/analyzer_status.ts | |||
@@ -1,45 +1,20 @@ | |||
1 | import * as vscode from 'vscode'; | 1 | import * as vscode from 'vscode'; |
2 | import { Server } from '../server'; | ||
3 | 2 | ||
4 | const statusUri = vscode.Uri.parse('rust-analyzer-status://status'); | 3 | import { Ctx, Cmd } from '../ctx'; |
5 | |||
6 | export class TextDocumentContentProvider | ||
7 | implements vscode.TextDocumentContentProvider { | ||
8 | public eventEmitter = new vscode.EventEmitter<vscode.Uri>(); | ||
9 | public syntaxTree: string = 'Not available'; | ||
10 | |||
11 | public provideTextDocumentContent( | ||
12 | _uri: vscode.Uri, | ||
13 | ): vscode.ProviderResult<string> { | ||
14 | const editor = vscode.window.activeTextEditor; | ||
15 | if (editor == null) { | ||
16 | return ''; | ||
17 | } | ||
18 | return Server.client.sendRequest<string>( | ||
19 | 'rust-analyzer/analyzerStatus', | ||
20 | null, | ||
21 | ); | ||
22 | } | ||
23 | |||
24 | get onDidChange(): vscode.Event<vscode.Uri> { | ||
25 | return this.eventEmitter.event; | ||
26 | } | ||
27 | } | ||
28 | |||
29 | let poller: NodeJS.Timer | null = null; | ||
30 | 4 | ||
31 | // Shows status of rust-analyzer (for debugging) | 5 | // Shows status of rust-analyzer (for debugging) |
6 | export function analyzerStatus(ctx: Ctx): Cmd { | ||
7 | let poller: NodeJS.Timer | null = null; | ||
8 | const tdcp = new TextDocumentContentProvider(ctx); | ||
32 | 9 | ||
33 | export function makeCommand(context: vscode.ExtensionContext) { | 10 | ctx.pushCleanup( |
34 | const textDocumentContentProvider = new TextDocumentContentProvider(); | ||
35 | context.subscriptions.push( | ||
36 | vscode.workspace.registerTextDocumentContentProvider( | 11 | vscode.workspace.registerTextDocumentContentProvider( |
37 | 'rust-analyzer-status', | 12 | 'rust-analyzer-status', |
38 | textDocumentContentProvider, | 13 | tdcp, |
39 | ), | 14 | ), |
40 | ); | 15 | ); |
41 | 16 | ||
42 | context.subscriptions.push({ | 17 | ctx.pushCleanup({ |
43 | dispose() { | 18 | dispose() { |
44 | if (poller != null) { | 19 | if (poller != null) { |
45 | clearInterval(poller); | 20 | clearInterval(poller); |
@@ -49,12 +24,9 @@ export function makeCommand(context: vscode.ExtensionContext) { | |||
49 | 24 | ||
50 | return async function handle() { | 25 | return async function handle() { |
51 | if (poller == null) { | 26 | if (poller == null) { |
52 | poller = setInterval( | 27 | poller = setInterval(() => tdcp.eventEmitter.fire(tdcp.uri), 1000); |
53 | () => textDocumentContentProvider.eventEmitter.fire(statusUri), | ||
54 | 1000, | ||
55 | ); | ||
56 | } | 28 | } |
57 | const document = await vscode.workspace.openTextDocument(statusUri); | 29 | const document = await vscode.workspace.openTextDocument(tdcp.uri); |
58 | return vscode.window.showTextDocument( | 30 | return vscode.window.showTextDocument( |
59 | document, | 31 | document, |
60 | vscode.ViewColumn.Two, | 32 | vscode.ViewColumn.Two, |
@@ -62,3 +34,31 @@ export function makeCommand(context: vscode.ExtensionContext) { | |||
62 | ); | 34 | ); |
63 | }; | 35 | }; |
64 | } | 36 | } |
37 | |||
38 | class TextDocumentContentProvider | ||
39 | implements vscode.TextDocumentContentProvider { | ||
40 | private ctx: Ctx; | ||
41 | uri = vscode.Uri.parse('rust-analyzer-status://status'); | ||
42 | eventEmitter = new vscode.EventEmitter<vscode.Uri>(); | ||
43 | |||
44 | constructor(ctx: Ctx) { | ||
45 | this.ctx = ctx; | ||
46 | } | ||
47 | |||
48 | provideTextDocumentContent( | ||
49 | _uri: vscode.Uri, | ||
50 | ): vscode.ProviderResult<string> { | ||
51 | const editor = vscode.window.activeTextEditor; | ||
52 | const client = this.ctx.client; | ||
53 | if (!editor || !client) return ''; | ||
54 | |||
55 | return client.sendRequest<string>( | ||
56 | 'rust-analyzer/analyzerStatus', | ||
57 | null, | ||
58 | ); | ||
59 | } | ||
60 | |||
61 | get onDidChange(): vscode.Event<vscode.Uri> { | ||
62 | return this.eventEmitter.event; | ||
63 | } | ||
64 | } | ||
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/expand_macro.ts b/editors/code/src/commands/expand_macro.ts index 17c78280a..dcdde78af 100644 --- a/editors/code/src/commands/expand_macro.ts +++ b/editors/code/src/commands/expand_macro.ts | |||
@@ -1,60 +1,23 @@ | |||
1 | import * as vscode from 'vscode'; | 1 | import * as vscode from 'vscode'; |
2 | import { Position, TextDocumentIdentifier } from 'vscode-languageclient'; | 2 | import * as lc from 'vscode-languageclient'; |
3 | import { Server } from '../server'; | ||
4 | 3 | ||
5 | export const expandMacroUri = vscode.Uri.parse( | 4 | import { Ctx, Cmd } from '../ctx'; |
6 | 'rust-analyzer://expandMacro/[EXPANSION].rs', | ||
7 | ); | ||
8 | |||
9 | export class ExpandMacroContentProvider | ||
10 | implements vscode.TextDocumentContentProvider { | ||
11 | public eventEmitter = new vscode.EventEmitter<vscode.Uri>(); | ||
12 | |||
13 | public provideTextDocumentContent( | ||
14 | _uri: vscode.Uri, | ||
15 | ): vscode.ProviderResult<string> { | ||
16 | async function handle() { | ||
17 | const editor = vscode.window.activeTextEditor; | ||
18 | if (editor == null) { | ||
19 | return ''; | ||
20 | } | ||
21 | |||
22 | const position = editor.selection.active; | ||
23 | const request: MacroExpandParams = { | ||
24 | textDocument: { uri: editor.document.uri.toString() }, | ||
25 | position, | ||
26 | }; | ||
27 | const expanded = await Server.client.sendRequest<ExpandedMacro>( | ||
28 | 'rust-analyzer/expandMacro', | ||
29 | request, | ||
30 | ); | ||
31 | |||
32 | if (expanded == null) { | ||
33 | return 'Not available'; | ||
34 | } | ||
35 | |||
36 | return code_format(expanded); | ||
37 | } | ||
38 | |||
39 | return handle(); | ||
40 | } | ||
41 | |||
42 | get onDidChange(): vscode.Event<vscode.Uri> { | ||
43 | return this.eventEmitter.event; | ||
44 | } | ||
45 | } | ||
46 | 5 | ||
47 | // Opens the virtual file that will show the syntax tree | 6 | // Opens the virtual file that will show the syntax tree |
48 | // | 7 | // |
49 | // The contents of the file come from the `TextDocumentContentProvider` | 8 | // The contents of the file come from the `TextDocumentContentProvider` |
50 | export function createHandle(provider: ExpandMacroContentProvider) { | 9 | export function expandMacro(ctx: Ctx): Cmd { |
51 | return async () => { | 10 | const tdcp = new TextDocumentContentProvider(ctx); |
52 | const uri = expandMacroUri; | 11 | ctx.pushCleanup( |
53 | 12 | vscode.workspace.registerTextDocumentContentProvider( | |
54 | const document = await vscode.workspace.openTextDocument(uri); | 13 | 'rust-analyzer', |
55 | 14 | tdcp, | |
56 | provider.eventEmitter.fire(uri); | 15 | ), |
16 | ); | ||
57 | 17 | ||
18 | return async () => { | ||
19 | const document = await vscode.workspace.openTextDocument(tdcp.uri); | ||
20 | tdcp.eventEmitter.fire(tdcp.uri); | ||
58 | return vscode.window.showTextDocument( | 21 | return vscode.window.showTextDocument( |
59 | document, | 22 | document, |
60 | vscode.ViewColumn.Two, | 23 | vscode.ViewColumn.Two, |
@@ -63,11 +26,6 @@ export function createHandle(provider: ExpandMacroContentProvider) { | |||
63 | }; | 26 | }; |
64 | } | 27 | } |
65 | 28 | ||
66 | interface MacroExpandParams { | ||
67 | textDocument: TextDocumentIdentifier; | ||
68 | position: Position; | ||
69 | } | ||
70 | |||
71 | interface ExpandedMacro { | 29 | interface ExpandedMacro { |
72 | name: string; | 30 | name: string; |
73 | expansion: string; | 31 | expansion: string; |
@@ -81,3 +39,38 @@ function code_format(expanded: ExpandedMacro): string { | |||
81 | 39 | ||
82 | return result; | 40 | return result; |
83 | } | 41 | } |
42 | |||
43 | class TextDocumentContentProvider | ||
44 | implements vscode.TextDocumentContentProvider { | ||
45 | private ctx: Ctx; | ||
46 | uri = vscode.Uri.parse('rust-analyzer://expandMacro/[EXPANSION].rs'); | ||
47 | eventEmitter = new vscode.EventEmitter<vscode.Uri>(); | ||
48 | |||
49 | constructor(ctx: Ctx) { | ||
50 | this.ctx = ctx; | ||
51 | } | ||
52 | |||
53 | async provideTextDocumentContent(_uri: vscode.Uri): Promise<string> { | ||
54 | const editor = vscode.window.activeTextEditor; | ||
55 | const client = this.ctx.client; | ||
56 | if (!editor || !client) return ''; | ||
57 | |||
58 | const position = editor.selection.active; | ||
59 | const request: lc.TextDocumentPositionParams = { | ||
60 | textDocument: { uri: editor.document.uri.toString() }, | ||
61 | position, | ||
62 | }; | ||
63 | const expanded = await client.sendRequest<ExpandedMacro>( | ||
64 | 'rust-analyzer/expandMacro', | ||
65 | request, | ||
66 | ); | ||
67 | |||
68 | if (expanded == null) return 'Not available'; | ||
69 | |||
70 | return code_format(expanded); | ||
71 | } | ||
72 | |||
73 | get onDidChange(): vscode.Event<vscode.Uri> { | ||
74 | return this.eventEmitter.event; | ||
75 | } | ||
76 | } | ||
diff --git a/editors/code/src/commands/index.ts b/editors/code/src/commands/index.ts index 13a696758..9a1697dcb 100644 --- a/editors/code/src/commands/index.ts +++ b/editors/code/src/commands/index.ts | |||
@@ -1,23 +1,63 @@ | |||
1 | import * as analyzerStatus from './analyzer_status'; | 1 | import * as vscode from 'vscode'; |
2 | import * as applySourceChange from './apply_source_change'; | 2 | import * as lc from 'vscode-languageclient'; |
3 | import * as expandMacro from './expand_macro'; | 3 | |
4 | import * as inlayHints from './inlay_hints'; | 4 | import { Ctx, Cmd } from '../ctx'; |
5 | import * as joinLines from './join_lines'; | 5 | import * as sourceChange from '../source_change'; |
6 | import * as matchingBrace from './matching_brace'; | 6 | |
7 | import * as onEnter from './on_enter'; | 7 | import { analyzerStatus } from './analyzer_status'; |
8 | import * as parentModule from './parent_module'; | 8 | import { matchingBrace } from './matching_brace'; |
9 | import * as runnables from './runnables'; | 9 | import { joinLines } from './join_lines'; |
10 | import * as syntaxTree from './syntaxTree'; | 10 | import { onEnter } from './on_enter'; |
11 | import { parentModule } from './parent_module'; | ||
12 | import { syntaxTree } from './syntax_tree'; | ||
13 | import { expandMacro } from './expand_macro'; | ||
14 | import { run, runSingle } from './runnables'; | ||
15 | |||
16 | function collectGarbage(ctx: Ctx): Cmd { | ||
17 | return async () => { | ||
18 | ctx.client?.sendRequest<null>('rust-analyzer/collectGarbage', null); | ||
19 | }; | ||
20 | } | ||
21 | |||
22 | function showReferences(ctx: Ctx): Cmd { | ||
23 | return (uri: string, position: lc.Position, locations: lc.Location[]) => { | ||
24 | let client = ctx.client; | ||
25 | if (client) { | ||
26 | vscode.commands.executeCommand( | ||
27 | 'editor.action.showReferences', | ||
28 | vscode.Uri.parse(uri), | ||
29 | client.protocol2CodeConverter.asPosition(position), | ||
30 | locations.map(client.protocol2CodeConverter.asLocation), | ||
31 | ); | ||
32 | } | ||
33 | }; | ||
34 | } | ||
35 | |||
36 | function applySourceChange(ctx: Ctx): Cmd { | ||
37 | return async (change: sourceChange.SourceChange) => { | ||
38 | sourceChange.applySourceChange(ctx, change); | ||
39 | }; | ||
40 | } | ||
41 | |||
42 | function reload(ctx: Ctx): Cmd { | ||
43 | return async () => { | ||
44 | vscode.window.showInformationMessage('Reloading rust-analyzer...'); | ||
45 | await ctx.restartServer(); | ||
46 | }; | ||
47 | } | ||
11 | 48 | ||
12 | export { | 49 | export { |
13 | analyzerStatus, | 50 | analyzerStatus, |
14 | applySourceChange, | ||
15 | expandMacro, | 51 | expandMacro, |
16 | joinLines, | 52 | joinLines, |
17 | matchingBrace, | 53 | matchingBrace, |
18 | parentModule, | 54 | parentModule, |
19 | runnables, | ||
20 | syntaxTree, | 55 | syntaxTree, |
21 | onEnter, | 56 | onEnter, |
22 | inlayHints, | 57 | collectGarbage, |
58 | run, | ||
59 | runSingle, | ||
60 | showReferences, | ||
61 | applySourceChange, | ||
62 | reload | ||
23 | }; | 63 | }; |
diff --git a/editors/code/src/commands/inlay_hints.ts b/editors/code/src/commands/inlay_hints.ts deleted file mode 100644 index ac7dcce60..000000000 --- a/editors/code/src/commands/inlay_hints.ts +++ /dev/null | |||
@@ -1,115 +0,0 @@ | |||
1 | import * as vscode from 'vscode'; | ||
2 | import { Range, TextDocumentChangeEvent, TextEditor } from 'vscode'; | ||
3 | import { TextDocumentIdentifier } from 'vscode-languageclient'; | ||
4 | import { Server } from '../server'; | ||
5 | |||
6 | interface InlayHintsParams { | ||
7 | textDocument: TextDocumentIdentifier; | ||
8 | } | ||
9 | |||
10 | interface InlayHint { | ||
11 | range: Range; | ||
12 | kind: string; | ||
13 | label: string; | ||
14 | } | ||
15 | |||
16 | const typeHintDecorationType = vscode.window.createTextEditorDecorationType({ | ||
17 | after: { | ||
18 | color: new vscode.ThemeColor('ralsp.inlayHint'), | ||
19 | }, | ||
20 | }); | ||
21 | |||
22 | export class HintsUpdater { | ||
23 | private displayHints = true; | ||
24 | |||
25 | public async toggleHintsDisplay(displayHints: boolean): Promise<void> { | ||
26 | if (this.displayHints !== displayHints) { | ||
27 | this.displayHints = displayHints; | ||
28 | return this.refreshVisibleEditorsHints( | ||
29 | displayHints ? undefined : [], | ||
30 | ); | ||
31 | } | ||
32 | } | ||
33 | |||
34 | public async refreshHintsForVisibleEditors( | ||
35 | cause?: TextDocumentChangeEvent, | ||
36 | ): Promise<void> { | ||
37 | if (!this.displayHints) { | ||
38 | return; | ||
39 | } | ||
40 | if ( | ||
41 | cause !== undefined && | ||
42 | (cause.contentChanges.length === 0 || | ||
43 | !this.isRustDocument(cause.document)) | ||
44 | ) { | ||
45 | return; | ||
46 | } | ||
47 | return this.refreshVisibleEditorsHints(); | ||
48 | } | ||
49 | |||
50 | private async refreshVisibleEditorsHints( | ||
51 | newDecorations?: vscode.DecorationOptions[], | ||
52 | ) { | ||
53 | const promises: Array<Promise<void>> = []; | ||
54 | |||
55 | for (const rustEditor of vscode.window.visibleTextEditors.filter( | ||
56 | editor => this.isRustDocument(editor.document), | ||
57 | )) { | ||
58 | if (newDecorations !== undefined) { | ||
59 | promises.push( | ||
60 | Promise.resolve( | ||
61 | rustEditor.setDecorations( | ||
62 | typeHintDecorationType, | ||
63 | newDecorations, | ||
64 | ), | ||
65 | ), | ||
66 | ); | ||
67 | } else { | ||
68 | promises.push(this.updateDecorationsFromServer(rustEditor)); | ||
69 | } | ||
70 | } | ||
71 | |||
72 | for (const promise of promises) { | ||
73 | await promise; | ||
74 | } | ||
75 | } | ||
76 | |||
77 | private isRustDocument(document: vscode.TextDocument): boolean { | ||
78 | return document && document.languageId === 'rust'; | ||
79 | } | ||
80 | |||
81 | private async updateDecorationsFromServer( | ||
82 | editor: TextEditor, | ||
83 | ): Promise<void> { | ||
84 | const newHints = await this.queryHints(editor.document.uri.toString()); | ||
85 | if (newHints !== null) { | ||
86 | const newDecorations = newHints.map(hint => ({ | ||
87 | range: hint.range, | ||
88 | renderOptions: { | ||
89 | after: { | ||
90 | contentText: `: ${hint.label}`, | ||
91 | }, | ||
92 | }, | ||
93 | })); | ||
94 | return editor.setDecorations( | ||
95 | typeHintDecorationType, | ||
96 | newDecorations, | ||
97 | ); | ||
98 | } | ||
99 | } | ||
100 | |||
101 | private async queryHints(documentUri: string): Promise<InlayHint[] | null> { | ||
102 | const request: InlayHintsParams = { | ||
103 | textDocument: { uri: documentUri }, | ||
104 | }; | ||
105 | const client = Server.client; | ||
106 | return client | ||
107 | .onReady() | ||
108 | .then(() => | ||
109 | client.sendRequest<InlayHint[] | null>( | ||
110 | 'rust-analyzer/inlayHints', | ||
111 | request, | ||
112 | ), | ||
113 | ); | ||
114 | } | ||
115 | } | ||
diff --git a/editors/code/src/commands/join_lines.ts b/editors/code/src/commands/join_lines.ts index 134ddc801..7b08c3255 100644 --- a/editors/code/src/commands/join_lines.ts +++ b/editors/code/src/commands/join_lines.ts | |||
@@ -1,29 +1,27 @@ | |||
1 | import * as vscode from 'vscode'; | 1 | import * as lc from 'vscode-languageclient'; |
2 | 2 | ||
3 | import { Range, TextDocumentIdentifier } from 'vscode-languageclient'; | 3 | import { Ctx, Cmd } from '../ctx'; |
4 | import { Server } from '../server'; | 4 | import { applySourceChange, SourceChange } from '../source_change'; |
5 | import { | ||
6 | handle as applySourceChange, | ||
7 | SourceChange, | ||
8 | } from './apply_source_change'; | ||
9 | 5 | ||
10 | interface JoinLinesParams { | 6 | export function joinLines(ctx: Ctx): Cmd { |
11 | textDocument: TextDocumentIdentifier; | 7 | return async () => { |
12 | range: Range; | 8 | const editor = ctx.activeRustEditor; |
13 | } | 9 | const client = ctx.client; |
10 | if (!editor || !client) return; | ||
14 | 11 | ||
15 | export async function handle() { | 12 | const request: JoinLinesParams = { |
16 | const editor = vscode.window.activeTextEditor; | 13 | range: client.code2ProtocolConverter.asRange(editor.selection), |
17 | if (editor == null || editor.document.languageId !== 'rust') { | 14 | textDocument: { uri: editor.document.uri.toString() }, |
18 | return; | 15 | }; |
19 | } | 16 | const change = await client.sendRequest<SourceChange>( |
20 | const request: JoinLinesParams = { | 17 | 'rust-analyzer/joinLines', |
21 | range: Server.client.code2ProtocolConverter.asRange(editor.selection), | 18 | request, |
22 | textDocument: { uri: editor.document.uri.toString() }, | 19 | ); |
20 | await applySourceChange(ctx, change); | ||
23 | }; | 21 | }; |
24 | const change = await Server.client.sendRequest<SourceChange>( | 22 | } |
25 | 'rust-analyzer/joinLines', | 23 | |
26 | request, | 24 | interface JoinLinesParams { |
27 | ); | 25 | textDocument: lc.TextDocumentIdentifier; |
28 | await applySourceChange(change); | 26 | range: lc.Range; |
29 | } | 27 | } |
diff --git a/editors/code/src/commands/line_buffer.ts b/editors/code/src/commands/line_buffer.ts deleted file mode 100644 index fb5b9f7f2..000000000 --- a/editors/code/src/commands/line_buffer.ts +++ /dev/null | |||
@@ -1,16 +0,0 @@ | |||
1 | export 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/matching_brace.ts b/editors/code/src/commands/matching_brace.ts index 364208cc7..7c58bb7e7 100644 --- a/editors/code/src/commands/matching_brace.ts +++ b/editors/code/src/commands/matching_brace.ts | |||
@@ -1,34 +1,36 @@ | |||
1 | import * as vscode from 'vscode'; | 1 | import * as vscode from 'vscode'; |
2 | import * as lc from 'vscode-languageclient'; | ||
2 | 3 | ||
3 | import { Position, TextDocumentIdentifier } from 'vscode-languageclient'; | 4 | import { Ctx, Cmd } from '../ctx'; |
4 | import { Server } from '../server'; | ||
5 | 5 | ||
6 | interface FindMatchingBraceParams { | 6 | export function matchingBrace(ctx: Ctx): Cmd { |
7 | textDocument: TextDocumentIdentifier; | 7 | return async () => { |
8 | offsets: Position[]; | 8 | const editor = ctx.activeRustEditor; |
9 | } | 9 | const client = ctx.client; |
10 | if (!editor || !client) return; | ||
10 | 11 | ||
11 | export async function handle() { | 12 | const request: FindMatchingBraceParams = { |
12 | const editor = vscode.window.activeTextEditor; | 13 | textDocument: { uri: editor.document.uri.toString() }, |
13 | if (editor == null || editor.document.languageId !== 'rust') { | 14 | offsets: editor.selections.map(s => |
14 | return; | 15 | client.code2ProtocolConverter.asPosition(s.active), |
15 | } | 16 | ), |
16 | const request: FindMatchingBraceParams = { | 17 | }; |
17 | textDocument: { uri: editor.document.uri.toString() }, | 18 | const response = await client.sendRequest<lc.Position[]>( |
18 | offsets: editor.selections.map(s => { | 19 | 'rust-analyzer/findMatchingBrace', |
19 | return Server.client.code2ProtocolConverter.asPosition(s.active); | 20 | request, |
20 | }), | ||
21 | }; | ||
22 | const response = await Server.client.sendRequest<Position[]>( | ||
23 | 'rust-analyzer/findMatchingBrace', | ||
24 | request, | ||
25 | ); | ||
26 | editor.selections = editor.selections.map((sel, idx) => { | ||
27 | const active = Server.client.protocol2CodeConverter.asPosition( | ||
28 | response[idx], | ||
29 | ); | 21 | ); |
30 | const anchor = sel.isEmpty ? active : sel.anchor; | 22 | editor.selections = editor.selections.map((sel, idx) => { |
31 | return new vscode.Selection(anchor, active); | 23 | const active = client.protocol2CodeConverter.asPosition( |
32 | }); | 24 | response[idx], |
33 | editor.revealRange(editor.selection); | 25 | ); |
26 | const anchor = sel.isEmpty ? active : sel.anchor; | ||
27 | return new vscode.Selection(anchor, active); | ||
28 | }); | ||
29 | editor.revealRange(editor.selection); | ||
30 | }; | ||
31 | } | ||
32 | |||
33 | interface FindMatchingBraceParams { | ||
34 | textDocument: lc.TextDocumentIdentifier; | ||
35 | offsets: lc.Position[]; | ||
34 | } | 36 | } |
diff --git a/editors/code/src/commands/on_enter.ts b/editors/code/src/commands/on_enter.ts index 772c64b3c..6f61883cd 100644 --- a/editors/code/src/commands/on_enter.ts +++ b/editors/code/src/commands/on_enter.ts | |||
@@ -1,33 +1,28 @@ | |||
1 | import * as vscode from 'vscode'; | ||
2 | import * as lc from 'vscode-languageclient'; | 1 | import * as lc from 'vscode-languageclient'; |
3 | import { Server } from '../server'; | ||
4 | import { | ||
5 | handle as applySourceChange, | ||
6 | SourceChange, | ||
7 | } from './apply_source_change'; | ||
8 | 2 | ||
9 | export async function handle(event: { text: string }): Promise<boolean> { | 3 | import { applySourceChange, SourceChange } from '../source_change'; |
10 | const editor = vscode.window.activeTextEditor; | 4 | import { Cmd, Ctx } from '../ctx'; |
11 | if ( | 5 | |
12 | editor == null || | 6 | export function onEnter(ctx: Ctx): Cmd { |
13 | editor.document.languageId !== 'rust' || | 7 | return async (event: { text: string }) => { |
14 | event.text !== '\n' | 8 | const editor = ctx.activeRustEditor; |
15 | ) { | 9 | const client = ctx.client; |
16 | return false; | 10 | if (!editor || event.text !== '\n') return false; |
17 | } | 11 | if (!client) return false; |
18 | const request: lc.TextDocumentPositionParams = { | 12 | |
19 | textDocument: { uri: editor.document.uri.toString() }, | 13 | const request: lc.TextDocumentPositionParams = { |
20 | position: Server.client.code2ProtocolConverter.asPosition( | 14 | textDocument: { uri: editor.document.uri.toString() }, |
21 | editor.selection.active, | 15 | position: client.code2ProtocolConverter.asPosition( |
22 | ), | 16 | editor.selection.active, |
17 | ), | ||
18 | }; | ||
19 | const change = await client.sendRequest<undefined | SourceChange>( | ||
20 | 'rust-analyzer/onEnter', | ||
21 | request, | ||
22 | ); | ||
23 | if (!change) return false; | ||
24 | |||
25 | await applySourceChange(ctx, change); | ||
26 | return true; | ||
23 | }; | 27 | }; |
24 | const change = await Server.client.sendRequest<undefined | SourceChange>( | ||
25 | 'rust-analyzer/onEnter', | ||
26 | request, | ||
27 | ); | ||
28 | if (!change) { | ||
29 | return false; | ||
30 | } | ||
31 | await applySourceChange(change); | ||
32 | return true; | ||
33 | } | 28 | } |
diff --git a/editors/code/src/commands/parent_module.ts b/editors/code/src/commands/parent_module.ts index ad49e1bdb..bf40b4021 100644 --- a/editors/code/src/commands/parent_module.ts +++ b/editors/code/src/commands/parent_module.ts | |||
@@ -1,32 +1,33 @@ | |||
1 | import * as vscode from 'vscode'; | 1 | import * as vscode from 'vscode'; |
2 | |||
3 | import * as lc from 'vscode-languageclient'; | 2 | import * as lc from 'vscode-languageclient'; |
4 | import { Server } from '../server'; | ||
5 | 3 | ||
6 | export async function handle() { | 4 | import { Ctx, Cmd } from '../ctx'; |
7 | const editor = vscode.window.activeTextEditor; | 5 | |
8 | if (editor == null || editor.document.languageId !== 'rust') { | 6 | export function parentModule(ctx: Ctx): Cmd { |
9 | return; | 7 | return async () => { |
10 | } | 8 | const editor = ctx.activeRustEditor; |
11 | const request: lc.TextDocumentPositionParams = { | 9 | const client = ctx.client; |
12 | textDocument: { uri: editor.document.uri.toString() }, | 10 | if (!editor || !client) return; |
13 | position: Server.client.code2ProtocolConverter.asPosition( | ||
14 | editor.selection.active, | ||
15 | ), | ||
16 | }; | ||
17 | const response = await Server.client.sendRequest<lc.Location[]>( | ||
18 | 'rust-analyzer/parentModule', | ||
19 | request, | ||
20 | ); | ||
21 | const loc = response[0]; | ||
22 | if (loc == null) { | ||
23 | return; | ||
24 | } | ||
25 | const uri = Server.client.protocol2CodeConverter.asUri(loc.uri); | ||
26 | const range = Server.client.protocol2CodeConverter.asRange(loc.range); | ||
27 | 11 | ||
28 | const doc = await vscode.workspace.openTextDocument(uri); | 12 | const request: lc.TextDocumentPositionParams = { |
29 | const e = await vscode.window.showTextDocument(doc); | 13 | textDocument: { uri: editor.document.uri.toString() }, |
30 | e.selection = new vscode.Selection(range.start, range.start); | 14 | position: client.code2ProtocolConverter.asPosition( |
31 | e.revealRange(range, vscode.TextEditorRevealType.InCenter); | 15 | editor.selection.active, |
16 | ), | ||
17 | }; | ||
18 | const response = await client.sendRequest<lc.Location[]>( | ||
19 | 'rust-analyzer/parentModule', | ||
20 | request, | ||
21 | ); | ||
22 | const loc = response[0]; | ||
23 | if (loc == null) return; | ||
24 | |||
25 | const uri = client.protocol2CodeConverter.asUri(loc.uri); | ||
26 | const range = client.protocol2CodeConverter.asRange(loc.range); | ||
27 | |||
28 | const doc = await vscode.workspace.openTextDocument(uri); | ||
29 | const e = await vscode.window.showTextDocument(doc); | ||
30 | e.selection = new vscode.Selection(range.start, range.start); | ||
31 | e.revealRange(range, vscode.TextEditorRevealType.InCenter); | ||
32 | }; | ||
32 | } | 33 | } |
diff --git a/editors/code/src/commands/runnables.ts b/editors/code/src/commands/runnables.ts index cf980e257..7919997ce 100644 --- a/editors/code/src/commands/runnables.ts +++ b/editors/code/src/commands/runnables.ts | |||
@@ -1,11 +1,68 @@ | |||
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 { Ctx, Cmd } from '../ctx'; |
8 | import { CargoWatchProvider, registerCargoWatchProvider } from './cargo_watch'; | 5 | |
6 | export function run(ctx: Ctx): Cmd { | ||
7 | let prevRunnable: RunnableQuickPick | undefined; | ||
8 | |||
9 | return async () => { | ||
10 | const editor = ctx.activeRustEditor; | ||
11 | const client = ctx.client; | ||
12 | if (!editor || !client) return; | ||
13 | |||
14 | const textDocument: lc.TextDocumentIdentifier = { | ||
15 | uri: editor.document.uri.toString(), | ||
16 | }; | ||
17 | const params: RunnablesParams = { | ||
18 | textDocument, | ||
19 | position: client.code2ProtocolConverter.asPosition( | ||
20 | editor.selection.active, | ||
21 | ), | ||
22 | }; | ||
23 | const runnables = await client.sendRequest<Runnable[]>( | ||
24 | 'rust-analyzer/runnables', | ||
25 | params, | ||
26 | ); | ||
27 | const items: RunnableQuickPick[] = []; | ||
28 | if (prevRunnable) { | ||
29 | items.push(prevRunnable); | ||
30 | } | ||
31 | for (const r of runnables) { | ||
32 | if ( | ||
33 | prevRunnable && | ||
34 | JSON.stringify(prevRunnable.runnable) === JSON.stringify(r) | ||
35 | ) { | ||
36 | continue; | ||
37 | } | ||
38 | items.push(new RunnableQuickPick(r)); | ||
39 | } | ||
40 | const item = await vscode.window.showQuickPick(items); | ||
41 | if (!item) return; | ||
42 | |||
43 | item.detail = 'rerun'; | ||
44 | prevRunnable = item; | ||
45 | const task = createTask(item.runnable); | ||
46 | return await vscode.tasks.executeTask(task); | ||
47 | }; | ||
48 | } | ||
49 | |||
50 | export function runSingle(ctx: Ctx): Cmd { | ||
51 | return async (runnable: Runnable) => { | ||
52 | const editor = ctx.activeRustEditor; | ||
53 | if (!editor) return; | ||
54 | |||
55 | const task = createTask(runnable); | ||
56 | task.group = vscode.TaskGroup.Build; | ||
57 | task.presentationOptions = { | ||
58 | reveal: vscode.TaskRevealKind.Always, | ||
59 | panel: vscode.TaskPanelKind.Dedicated, | ||
60 | clear: true, | ||
61 | }; | ||
62 | |||
63 | return vscode.tasks.executeTask(task); | ||
64 | }; | ||
65 | } | ||
9 | 66 | ||
10 | interface RunnablesParams { | 67 | interface RunnablesParams { |
11 | textDocument: lc.TextDocumentIdentifier; | 68 | textDocument: lc.TextDocumentIdentifier; |
@@ -71,150 +128,3 @@ function createTask(spec: Runnable): vscode.Task { | |||
71 | t.presentationOptions.clear = true; | 128 | t.presentationOptions.clear = true; |
72 | return t; | 129 | return t; |
73 | } | 130 | } |
74 | |||
75 | let prevRunnable: RunnableQuickPick | undefined; | ||
76 | export async function handle(): Promise<vscode.TaskExecution | undefined> { | ||
77 | const editor = vscode.window.activeTextEditor; | ||
78 | if (editor == null || editor.document.languageId !== 'rust') { | ||
79 | return; | ||
80 | } | ||
81 | const textDocument: lc.TextDocumentIdentifier = { | ||
82 | uri: editor.document.uri.toString(), | ||
83 | }; | ||
84 | const params: RunnablesParams = { | ||
85 | textDocument, | ||
86 | position: Server.client.code2ProtocolConverter.asPosition( | ||
87 | editor.selection.active, | ||
88 | ), | ||
89 | }; | ||
90 | const runnables = await Server.client.sendRequest<Runnable[]>( | ||
91 | 'rust-analyzer/runnables', | ||
92 | params, | ||
93 | ); | ||
94 | const items: RunnableQuickPick[] = []; | ||
95 | if (prevRunnable) { | ||
96 | items.push(prevRunnable); | ||
97 | } | ||
98 | for (const r of runnables) { | ||
99 | if ( | ||
100 | prevRunnable && | ||
101 | JSON.stringify(prevRunnable.runnable) === JSON.stringify(r) | ||
102 | ) { | ||
103 | continue; | ||
104 | } | ||
105 | items.push(new RunnableQuickPick(r)); | ||
106 | } | ||
107 | const item = await vscode.window.showQuickPick(items); | ||
108 | if (!item) { | ||
109 | return; | ||
110 | } | ||
111 | |||
112 | item.detail = 'rerun'; | ||
113 | prevRunnable = item; | ||
114 | const task = createTask(item.runnable); | ||
115 | return await vscode.tasks.executeTask(task); | ||
116 | } | ||
117 | |||
118 | export async function handleSingle(runnable: Runnable) { | ||
119 | const editor = vscode.window.activeTextEditor; | ||
120 | if (editor == null || editor.document.languageId !== 'rust') { | ||
121 | return; | ||
122 | } | ||
123 | |||
124 | const task = createTask(runnable); | ||
125 | task.group = vscode.TaskGroup.Build; | ||
126 | task.presentationOptions = { | ||
127 | reveal: vscode.TaskRevealKind.Always, | ||
128 | panel: vscode.TaskPanelKind.Dedicated, | ||
129 | clear: true, | ||
130 | }; | ||
131 | |||
132 | return vscode.tasks.executeTask(task); | ||
133 | } | ||
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/syntaxTree.ts b/editors/code/src/commands/syntaxTree.ts deleted file mode 100644 index 89a80550c..000000000 --- a/editors/code/src/commands/syntaxTree.ts +++ /dev/null | |||
@@ -1,76 +0,0 @@ | |||
1 | import * as vscode from 'vscode'; | ||
2 | import { Range, TextDocumentIdentifier } from 'vscode-languageclient'; | ||
3 | |||
4 | import { Server } from '../server'; | ||
5 | |||
6 | export const syntaxTreeUri = vscode.Uri.parse('rust-analyzer://syntaxtree'); | ||
7 | |||
8 | export class SyntaxTreeContentProvider | ||
9 | implements vscode.TextDocumentContentProvider { | ||
10 | public eventEmitter = new vscode.EventEmitter<vscode.Uri>(); | ||
11 | public syntaxTree: string = 'Not available'; | ||
12 | |||
13 | public provideTextDocumentContent( | ||
14 | uri: vscode.Uri, | ||
15 | ): vscode.ProviderResult<string> { | ||
16 | const editor = vscode.window.activeTextEditor; | ||
17 | if (editor == null) { | ||
18 | return ''; | ||
19 | } | ||
20 | |||
21 | let range: Range | undefined; | ||
22 | |||
23 | // When the range based query is enabled we take the range of the selection | ||
24 | if (uri.query === 'range=true') { | ||
25 | range = editor.selection.isEmpty | ||
26 | ? undefined | ||
27 | : Server.client.code2ProtocolConverter.asRange( | ||
28 | editor.selection, | ||
29 | ); | ||
30 | } | ||
31 | |||
32 | const request: SyntaxTreeParams = { | ||
33 | textDocument: { uri: editor.document.uri.toString() }, | ||
34 | range, | ||
35 | }; | ||
36 | return Server.client.sendRequest<SyntaxTreeResult>( | ||
37 | 'rust-analyzer/syntaxTree', | ||
38 | request, | ||
39 | ); | ||
40 | } | ||
41 | |||
42 | get onDidChange(): vscode.Event<vscode.Uri> { | ||
43 | return this.eventEmitter.event; | ||
44 | } | ||
45 | } | ||
46 | |||
47 | interface SyntaxTreeParams { | ||
48 | textDocument: TextDocumentIdentifier; | ||
49 | range?: Range; | ||
50 | } | ||
51 | |||
52 | type SyntaxTreeResult = string; | ||
53 | |||
54 | // Opens the virtual file that will show the syntax tree | ||
55 | // | ||
56 | // The contents of the file come from the `TextDocumentContentProvider` | ||
57 | export function createHandle(provider: SyntaxTreeContentProvider) { | ||
58 | return async () => { | ||
59 | const editor = vscode.window.activeTextEditor; | ||
60 | const rangeEnabled = !!(editor && !editor.selection.isEmpty); | ||
61 | |||
62 | const uri = rangeEnabled | ||
63 | ? vscode.Uri.parse(`${syntaxTreeUri.toString()}?range=true`) | ||
64 | : syntaxTreeUri; | ||
65 | |||
66 | const document = await vscode.workspace.openTextDocument(uri); | ||
67 | |||
68 | provider.eventEmitter.fire(uri); | ||
69 | |||
70 | return vscode.window.showTextDocument( | ||
71 | document, | ||
72 | vscode.ViewColumn.Two, | ||
73 | true, | ||
74 | ); | ||
75 | }; | ||
76 | } | ||
diff --git a/editors/code/src/commands/syntax_tree.ts b/editors/code/src/commands/syntax_tree.ts new file mode 100644 index 000000000..02ea9f166 --- /dev/null +++ b/editors/code/src/commands/syntax_tree.ts | |||
@@ -0,0 +1,104 @@ | |||
1 | import * as vscode from 'vscode'; | ||
2 | import * as lc from 'vscode-languageclient'; | ||
3 | |||
4 | import { Ctx, Cmd } from '../ctx'; | ||
5 | |||
6 | // Opens the virtual file that will show the syntax tree | ||
7 | // | ||
8 | // The contents of the file come from the `TextDocumentContentProvider` | ||
9 | export function syntaxTree(ctx: Ctx): Cmd { | ||
10 | const tdcp = new TextDocumentContentProvider(ctx); | ||
11 | |||
12 | ctx.pushCleanup( | ||
13 | vscode.workspace.registerTextDocumentContentProvider( | ||
14 | 'rust-analyzer', | ||
15 | tdcp, | ||
16 | ), | ||
17 | ); | ||
18 | |||
19 | vscode.workspace.onDidChangeTextDocument( | ||
20 | (event: vscode.TextDocumentChangeEvent) => { | ||
21 | const doc = event.document; | ||
22 | if (doc.languageId !== 'rust') return; | ||
23 | afterLs(() => tdcp.eventEmitter.fire(tdcp.uri)); | ||
24 | }, | ||
25 | ctx.subscriptions, | ||
26 | ); | ||
27 | |||
28 | vscode.window.onDidChangeActiveTextEditor( | ||
29 | (editor: vscode.TextEditor | undefined) => { | ||
30 | if (!editor || editor.document.languageId !== 'rust') return; | ||
31 | tdcp.eventEmitter.fire(tdcp.uri); | ||
32 | }, | ||
33 | ctx.subscriptions, | ||
34 | ); | ||
35 | |||
36 | return async () => { | ||
37 | const editor = vscode.window.activeTextEditor; | ||
38 | const rangeEnabled = !!(editor && !editor.selection.isEmpty); | ||
39 | |||
40 | const uri = rangeEnabled | ||
41 | ? vscode.Uri.parse(`${tdcp.uri.toString()}?range=true`) | ||
42 | : tdcp.uri; | ||
43 | |||
44 | const document = await vscode.workspace.openTextDocument(uri); | ||
45 | |||
46 | tdcp.eventEmitter.fire(uri); | ||
47 | |||
48 | return vscode.window.showTextDocument( | ||
49 | document, | ||
50 | vscode.ViewColumn.Two, | ||
51 | true, | ||
52 | ); | ||
53 | }; | ||
54 | } | ||
55 | |||
56 | // We need to order this after LS updates, but there's no API for that. | ||
57 | // Hence, good old setTimeout. | ||
58 | function afterLs(f: () => any) { | ||
59 | setTimeout(f, 10); | ||
60 | } | ||
61 | |||
62 | interface SyntaxTreeParams { | ||
63 | textDocument: lc.TextDocumentIdentifier; | ||
64 | range?: lc.Range; | ||
65 | } | ||
66 | |||
67 | class TextDocumentContentProvider | ||
68 | implements vscode.TextDocumentContentProvider { | ||
69 | private ctx: Ctx; | ||
70 | uri = vscode.Uri.parse('rust-analyzer://syntaxtree'); | ||
71 | eventEmitter = new vscode.EventEmitter<vscode.Uri>(); | ||
72 | |||
73 | constructor(ctx: Ctx) { | ||
74 | this.ctx = ctx; | ||
75 | } | ||
76 | |||
77 | provideTextDocumentContent(uri: vscode.Uri): vscode.ProviderResult<string> { | ||
78 | const editor = vscode.window.activeTextEditor; | ||
79 | const client = this.ctx.client; | ||
80 | if (!editor || !client) return ''; | ||
81 | |||
82 | let range: lc.Range | undefined; | ||
83 | |||
84 | // When the range based query is enabled we take the range of the selection | ||
85 | if (uri.query === 'range=true') { | ||
86 | range = editor.selection.isEmpty | ||
87 | ? undefined | ||
88 | : client.code2ProtocolConverter.asRange(editor.selection); | ||
89 | } | ||
90 | |||
91 | const request: SyntaxTreeParams = { | ||
92 | textDocument: { uri: editor.document.uri.toString() }, | ||
93 | range, | ||
94 | }; | ||
95 | return client.sendRequest<string>( | ||
96 | 'rust-analyzer/syntaxTree', | ||
97 | request, | ||
98 | ); | ||
99 | } | ||
100 | |||
101 | get onDidChange(): vscode.Event<vscode.Uri> { | ||
102 | return this.eventEmitter.event; | ||
103 | } | ||
104 | } | ||
diff --git a/editors/code/src/commands/watch_status.ts b/editors/code/src/commands/watch_status.ts deleted file mode 100644 index 8d64394c7..000000000 --- a/editors/code/src/commands/watch_status.ts +++ /dev/null | |||
@@ -1,63 +0,0 @@ | |||
1 | import * as vscode from 'vscode'; | ||
2 | |||
3 | const spinnerFrames = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏']; | ||
4 | |||
5 | export class StatusDisplay implements vscode.Disposable { | ||
6 | public packageName?: string; | ||
7 | |||
8 | private i = 0; | ||
9 | private statusBarItem: vscode.StatusBarItem; | ||
10 | private command: string; | ||
11 | private timer?: NodeJS.Timeout; | ||
12 | |||
13 | constructor(command: string) { | ||
14 | this.statusBarItem = vscode.window.createStatusBarItem( | ||
15 | vscode.StatusBarAlignment.Left, | ||
16 | 10, | ||
17 | ); | ||
18 | this.command = command; | ||
19 | this.statusBarItem.hide(); | ||
20 | } | ||
21 | |||
22 | public show() { | ||
23 | this.packageName = undefined; | ||
24 | |||
25 | this.timer = | ||
26 | this.timer || | ||
27 | setInterval(() => { | ||
28 | if (this.packageName) { | ||
29 | this.statusBarItem!.text = `cargo ${this.command} [${ | ||
30 | this.packageName | ||
31 | }] ${this.frame()}`; | ||
32 | } else { | ||
33 | this.statusBarItem!.text = `cargo ${ | ||
34 | this.command | ||
35 | } ${this.frame()}`; | ||
36 | } | ||
37 | }, 300); | ||
38 | |||
39 | this.statusBarItem.show(); | ||
40 | } | ||
41 | |||
42 | public hide() { | ||
43 | if (this.timer) { | ||
44 | clearInterval(this.timer); | ||
45 | this.timer = undefined; | ||
46 | } | ||
47 | |||
48 | this.statusBarItem.hide(); | ||
49 | } | ||
50 | |||
51 | public dispose() { | ||
52 | if (this.timer) { | ||
53 | clearInterval(this.timer); | ||
54 | this.timer = undefined; | ||
55 | } | ||
56 | |||
57 | this.statusBarItem.dispose(); | ||
58 | } | ||
59 | |||
60 | private frame() { | ||
61 | return spinnerFrames[(this.i = ++this.i % spinnerFrames.length)]; | ||
62 | } | ||
63 | } | ||
diff --git a/editors/code/src/config.ts b/editors/code/src/config.ts index e131f09df..ec2790b63 100644 --- a/editors/code/src/config.ts +++ b/editors/code/src/config.ts | |||
@@ -1,18 +1,11 @@ | |||
1 | import * as vscode from 'vscode'; | 1 | import * as vscode from 'vscode'; |
2 | 2 | ||
3 | import { Server } from './server'; | ||
4 | |||
5 | const RA_LSP_DEBUG = process.env.__RA_LSP_SERVER_DEBUG; | 3 | const RA_LSP_DEBUG = process.env.__RA_LSP_SERVER_DEBUG; |
6 | 4 | ||
7 | export type CargoWatchStartupOptions = 'ask' | 'enabled' | 'disabled'; | ||
8 | export type CargoWatchTraceOptions = 'off' | 'error' | 'verbose'; | ||
9 | |||
10 | export interface CargoWatchOptions { | 5 | export interface CargoWatchOptions { |
11 | enableOnStartup: CargoWatchStartupOptions; | 6 | enable: boolean; |
12 | arguments: string; | 7 | arguments: string[]; |
13 | command: string; | 8 | command: string; |
14 | trace: CargoWatchTraceOptions; | ||
15 | ignore: string[]; | ||
16 | allTargets: boolean; | 9 | allTargets: boolean; |
17 | } | 10 | } |
18 | 11 | ||
@@ -23,27 +16,25 @@ export interface CargoFeatures { | |||
23 | } | 16 | } |
24 | 17 | ||
25 | export class Config { | 18 | export class Config { |
26 | public highlightingOn = true; | 19 | highlightingOn = true; |
27 | public rainbowHighlightingOn = false; | 20 | rainbowHighlightingOn = false; |
28 | public enableEnhancedTyping = true; | 21 | enableEnhancedTyping = true; |
29 | public raLspServerPath = RA_LSP_DEBUG || 'ra_lsp_server'; | 22 | raLspServerPath = RA_LSP_DEBUG || 'ra_lsp_server'; |
30 | public lruCapacity: null | number = null; | 23 | lruCapacity: null | number = null; |
31 | public displayInlayHints = true; | 24 | displayInlayHints = true; |
32 | public maxInlayHintLength: null | number = null; | 25 | maxInlayHintLength: null | number = null; |
33 | public excludeGlobs = []; | 26 | excludeGlobs = []; |
34 | public useClientWatching = true; | 27 | useClientWatching = true; |
35 | public featureFlags = {}; | 28 | featureFlags = {}; |
36 | // for internal use | 29 | // for internal use |
37 | public withSysroot: null | boolean = null; | 30 | withSysroot: null | boolean = null; |
38 | public cargoWatchOptions: CargoWatchOptions = { | 31 | cargoWatchOptions: CargoWatchOptions = { |
39 | enableOnStartup: 'ask', | 32 | enable: true, |
40 | trace: 'off', | 33 | arguments: [], |
41 | arguments: '', | ||
42 | command: '', | 34 | command: '', |
43 | ignore: [], | ||
44 | allTargets: true, | 35 | allTargets: true, |
45 | }; | 36 | }; |
46 | public cargoFeatures: CargoFeatures = { | 37 | cargoFeatures: CargoFeatures = { |
47 | noDefaultFeatures: false, | 38 | noDefaultFeatures: false, |
48 | allFeatures: true, | 39 | allFeatures: true, |
49 | features: [], | 40 | features: [], |
@@ -52,15 +43,14 @@ export class Config { | |||
52 | private prevEnhancedTyping: null | boolean = null; | 43 | private prevEnhancedTyping: null | boolean = null; |
53 | private prevCargoFeatures: null | CargoFeatures = null; | 44 | private prevCargoFeatures: null | CargoFeatures = null; |
54 | 45 | ||
55 | constructor() { | 46 | constructor(ctx: vscode.ExtensionContext) { |
56 | vscode.workspace.onDidChangeConfiguration(_ => | 47 | vscode.workspace.onDidChangeConfiguration(_ => this.refresh(), ctx.subscriptions); |
57 | this.userConfigChanged(), | 48 | this.refresh(); |
58 | ); | ||
59 | this.userConfigChanged(); | ||
60 | } | 49 | } |
61 | 50 | ||
62 | public userConfigChanged() { | 51 | private refresh() { |
63 | const config = vscode.workspace.getConfiguration('rust-analyzer'); | 52 | const config = vscode.workspace.getConfiguration('rust-analyzer'); |
53 | |||
64 | let requireReloadMessage = null; | 54 | let requireReloadMessage = null; |
65 | 55 | ||
66 | if (config.has('highlightingOn')) { | 56 | if (config.has('highlightingOn')) { |
@@ -73,10 +63,6 @@ export class Config { | |||
73 | ) as boolean; | 63 | ) as boolean; |
74 | } | 64 | } |
75 | 65 | ||
76 | if (!this.highlightingOn && Server) { | ||
77 | Server.highlighter.removeHighlights(); | ||
78 | } | ||
79 | |||
80 | if (config.has('enableEnhancedTyping')) { | 66 | if (config.has('enableEnhancedTyping')) { |
81 | this.enableEnhancedTyping = config.get( | 67 | this.enableEnhancedTyping = config.get( |
82 | 'enableEnhancedTyping', | 68 | 'enableEnhancedTyping', |
@@ -100,23 +86,17 @@ export class Config { | |||
100 | RA_LSP_DEBUG || (config.get('raLspServerPath') as string); | 86 | RA_LSP_DEBUG || (config.get('raLspServerPath') as string); |
101 | } | 87 | } |
102 | 88 | ||
103 | if (config.has('enableCargoWatchOnStartup')) { | 89 | if (config.has('cargo-watch.enable')) { |
104 | this.cargoWatchOptions.enableOnStartup = config.get< | 90 | this.cargoWatchOptions.enable = config.get<boolean>( |
105 | CargoWatchStartupOptions | 91 | 'cargo-watch.enable', |
106 | >('enableCargoWatchOnStartup', 'ask'); | 92 | true, |
107 | } | ||
108 | |||
109 | if (config.has('trace.cargo-watch')) { | ||
110 | this.cargoWatchOptions.trace = config.get<CargoWatchTraceOptions>( | ||
111 | 'trace.cargo-watch', | ||
112 | 'off', | ||
113 | ); | 93 | ); |
114 | } | 94 | } |
115 | 95 | ||
116 | if (config.has('cargo-watch.arguments')) { | 96 | if (config.has('cargo-watch.arguments')) { |
117 | this.cargoWatchOptions.arguments = config.get<string>( | 97 | this.cargoWatchOptions.arguments = config.get<string[]>( |
118 | 'cargo-watch.arguments', | 98 | 'cargo-watch.arguments', |
119 | '', | 99 | [], |
120 | ); | 100 | ); |
121 | } | 101 | } |
122 | 102 | ||
@@ -127,13 +107,6 @@ export class Config { | |||
127 | ); | 107 | ); |
128 | } | 108 | } |
129 | 109 | ||
130 | if (config.has('cargo-watch.ignore')) { | ||
131 | this.cargoWatchOptions.ignore = config.get<string[]>( | ||
132 | 'cargo-watch.ignore', | ||
133 | [], | ||
134 | ); | ||
135 | } | ||
136 | |||
137 | if (config.has('cargo-watch.allTargets')) { | 110 | if (config.has('cargo-watch.allTargets')) { |
138 | this.cargoWatchOptions.allTargets = config.get<boolean>( | 111 | this.cargoWatchOptions.allTargets = config.get<boolean>( |
139 | 'cargo-watch.allTargets', | 112 | 'cargo-watch.allTargets', |
@@ -190,9 +163,9 @@ export class Config { | |||
190 | (this.cargoFeatures.allFeatures !== | 163 | (this.cargoFeatures.allFeatures !== |
191 | this.prevCargoFeatures.allFeatures || | 164 | this.prevCargoFeatures.allFeatures || |
192 | this.cargoFeatures.noDefaultFeatures !== | 165 | this.cargoFeatures.noDefaultFeatures !== |
193 | this.prevCargoFeatures.noDefaultFeatures || | 166 | this.prevCargoFeatures.noDefaultFeatures || |
194 | this.cargoFeatures.features.length !== | 167 | this.cargoFeatures.features.length !== |
195 | this.prevCargoFeatures.features.length || | 168 | this.prevCargoFeatures.features.length || |
196 | this.cargoFeatures.features.some( | 169 | this.cargoFeatures.features.some( |
197 | (v, i) => v !== this.prevCargoFeatures!.features[i], | 170 | (v, i) => v !== this.prevCargoFeatures!.features[i], |
198 | )) | 171 | )) |
diff --git a/editors/code/src/ctx.ts b/editors/code/src/ctx.ts new file mode 100644 index 000000000..a2a4e42a9 --- /dev/null +++ b/editors/code/src/ctx.ts | |||
@@ -0,0 +1,112 @@ | |||
1 | import * as vscode from 'vscode'; | ||
2 | import * as lc from 'vscode-languageclient'; | ||
3 | import { Config } from './config'; | ||
4 | import { createClient } from './client'; | ||
5 | |||
6 | export class Ctx { | ||
7 | readonly config: Config; | ||
8 | // Because we have "reload server" action, various listeners **will** face a | ||
9 | // situation where the client is not ready yet, and should be prepared to | ||
10 | // deal with it. | ||
11 | // | ||
12 | // Ideally, this should be replaced with async getter though. | ||
13 | client: lc.LanguageClient | null = null; | ||
14 | private extCtx: vscode.ExtensionContext; | ||
15 | private onDidRestartHooks: Array<(client: lc.LanguageClient) => void> = []; | ||
16 | |||
17 | constructor(extCtx: vscode.ExtensionContext) { | ||
18 | this.config = new Config(extCtx); | ||
19 | this.extCtx = extCtx; | ||
20 | } | ||
21 | |||
22 | async restartServer() { | ||
23 | let old = this.client; | ||
24 | if (old) { | ||
25 | await old.stop(); | ||
26 | } | ||
27 | this.client = null; | ||
28 | const client = createClient(this.config); | ||
29 | this.pushCleanup(client.start()); | ||
30 | await client.onReady(); | ||
31 | |||
32 | this.client = client; | ||
33 | for (const hook of this.onDidRestartHooks) { | ||
34 | hook(client); | ||
35 | } | ||
36 | } | ||
37 | |||
38 | get activeRustEditor(): vscode.TextEditor | undefined { | ||
39 | const editor = vscode.window.activeTextEditor; | ||
40 | return editor && editor.document.languageId === 'rust' | ||
41 | ? editor | ||
42 | : undefined; | ||
43 | } | ||
44 | |||
45 | registerCommand(name: string, factory: (ctx: Ctx) => Cmd) { | ||
46 | const fullName = `rust-analyzer.${name}`; | ||
47 | const cmd = factory(this); | ||
48 | const d = vscode.commands.registerCommand(fullName, cmd); | ||
49 | this.pushCleanup(d); | ||
50 | } | ||
51 | |||
52 | overrideCommand(name: string, factory: (ctx: Ctx) => Cmd) { | ||
53 | const defaultCmd = `default:${name}`; | ||
54 | const override = factory(this); | ||
55 | const original = (...args: any[]) => | ||
56 | vscode.commands.executeCommand(defaultCmd, ...args); | ||
57 | try { | ||
58 | const d = vscode.commands.registerCommand( | ||
59 | name, | ||
60 | async (...args: any[]) => { | ||
61 | if (!(await override(...args))) { | ||
62 | return await original(...args); | ||
63 | } | ||
64 | }, | ||
65 | ); | ||
66 | this.pushCleanup(d); | ||
67 | } catch (_) { | ||
68 | vscode.window.showWarningMessage( | ||
69 | 'Enhanced typing feature is disabled because of incompatibility with VIM extension, consider turning off rust-analyzer.enableEnhancedTyping: https://github.com/rust-analyzer/rust-analyzer/blob/master/docs/user/README.md#settings', | ||
70 | ); | ||
71 | } | ||
72 | } | ||
73 | |||
74 | get subscriptions(): { dispose(): any }[] { | ||
75 | return this.extCtx.subscriptions; | ||
76 | } | ||
77 | |||
78 | pushCleanup(d: { dispose(): any }) { | ||
79 | this.extCtx.subscriptions.push(d); | ||
80 | } | ||
81 | |||
82 | onDidRestart(hook: (client: lc.LanguageClient) => void) { | ||
83 | this.onDidRestartHooks.push(hook); | ||
84 | } | ||
85 | } | ||
86 | |||
87 | export type Cmd = (...args: any[]) => any; | ||
88 | |||
89 | export async function sendRequestWithRetry<R>( | ||
90 | client: lc.LanguageClient, | ||
91 | method: string, | ||
92 | param: any, | ||
93 | token?: vscode.CancellationToken, | ||
94 | ): Promise<R> { | ||
95 | for (const delay of [2, 4, 6, 8, 10, null]) { | ||
96 | try { | ||
97 | return await (token ? client.sendRequest(method, param, token) : client.sendRequest(method, param)); | ||
98 | } catch (e) { | ||
99 | if ( | ||
100 | e.code === lc.ErrorCodes.ContentModified && | ||
101 | delay !== null | ||
102 | ) { | ||
103 | await sleep(10 * (1 << delay)); | ||
104 | continue; | ||
105 | } | ||
106 | throw e; | ||
107 | } | ||
108 | } | ||
109 | throw 'unreachable'; | ||
110 | } | ||
111 | |||
112 | const sleep = (ms: number) => new Promise(resolve => setTimeout(resolve, ms)); | ||
diff --git a/editors/code/src/events/change_active_text_editor.ts b/editors/code/src/events/change_active_text_editor.ts deleted file mode 100644 index 74b91bd48..000000000 --- a/editors/code/src/events/change_active_text_editor.ts +++ /dev/null | |||
@@ -1,32 +0,0 @@ | |||
1 | import { TextEditor } from 'vscode'; | ||
2 | import { TextDocumentIdentifier } from 'vscode-languageclient'; | ||
3 | |||
4 | import { | ||
5 | SyntaxTreeContentProvider, | ||
6 | syntaxTreeUri, | ||
7 | } from '../commands/syntaxTree'; | ||
8 | import { Decoration } from '../highlighting'; | ||
9 | import { Server } from '../server'; | ||
10 | |||
11 | export function makeHandler(syntaxTreeProvider: SyntaxTreeContentProvider) { | ||
12 | return async function handle(editor: TextEditor | undefined) { | ||
13 | if (!editor || editor.document.languageId !== 'rust') { | ||
14 | return; | ||
15 | } | ||
16 | |||
17 | syntaxTreeProvider.eventEmitter.fire(syntaxTreeUri); | ||
18 | |||
19 | if (!Server.config.highlightingOn) { | ||
20 | return; | ||
21 | } | ||
22 | |||
23 | const params: TextDocumentIdentifier = { | ||
24 | uri: editor.document.uri.toString(), | ||
25 | }; | ||
26 | const decorations = await Server.client.sendRequest<Decoration[]>( | ||
27 | 'rust-analyzer/decorationsRequest', | ||
28 | params, | ||
29 | ); | ||
30 | Server.highlighter.setHighlights(editor, decorations); | ||
31 | }; | ||
32 | } | ||
diff --git a/editors/code/src/events/change_text_document.ts b/editors/code/src/events/change_text_document.ts deleted file mode 100644 index 2e998e889..000000000 --- a/editors/code/src/events/change_text_document.ts +++ /dev/null | |||
@@ -1,24 +0,0 @@ | |||
1 | import * as vscode from 'vscode'; | ||
2 | |||
3 | import { | ||
4 | SyntaxTreeContentProvider, | ||
5 | syntaxTreeUri, | ||
6 | } from '../commands/syntaxTree'; | ||
7 | |||
8 | export function createHandler(syntaxTreeProvider: SyntaxTreeContentProvider) { | ||
9 | return (event: vscode.TextDocumentChangeEvent) => { | ||
10 | const doc = event.document; | ||
11 | if (doc.languageId !== 'rust') { | ||
12 | return; | ||
13 | } | ||
14 | afterLs(() => { | ||
15 | syntaxTreeProvider.eventEmitter.fire(syntaxTreeUri); | ||
16 | }); | ||
17 | }; | ||
18 | } | ||
19 | |||
20 | // We need to order this after LS updates, but there's no API for that. | ||
21 | // Hence, good old setTimeout. | ||
22 | function afterLs(f: () => any) { | ||
23 | setTimeout(f, 10); | ||
24 | } | ||
diff --git a/editors/code/src/events/index.ts b/editors/code/src/events/index.ts deleted file mode 100644 index 4c154563f..000000000 --- a/editors/code/src/events/index.ts +++ /dev/null | |||
@@ -1,4 +0,0 @@ | |||
1 | import * as changeActiveTextEditor from './change_active_text_editor'; | ||
2 | import * as changeTextDocument from './change_text_document'; | ||
3 | |||
4 | export { changeActiveTextEditor, changeTextDocument }; | ||
diff --git a/editors/code/src/extension.ts b/editors/code/src/extension.ts deleted file mode 100644 index 815f3692c..000000000 --- a/editors/code/src/extension.ts +++ /dev/null | |||
@@ -1,218 +0,0 @@ | |||
1 | import * as vscode from 'vscode'; | ||
2 | import * as lc from 'vscode-languageclient'; | ||
3 | |||
4 | import * as commands from './commands'; | ||
5 | import { CargoWatchProvider } from './commands/cargo_watch'; | ||
6 | import { ExpandMacroContentProvider } from './commands/expand_macro'; | ||
7 | import { HintsUpdater } from './commands/inlay_hints'; | ||
8 | import { | ||
9 | interactivelyStartCargoWatch, | ||
10 | startCargoWatch, | ||
11 | } from './commands/runnables'; | ||
12 | import { SyntaxTreeContentProvider } from './commands/syntaxTree'; | ||
13 | import * as events from './events'; | ||
14 | import * as notifications from './notifications'; | ||
15 | import { Server } from './server'; | ||
16 | |||
17 | export async function activate(context: vscode.ExtensionContext) { | ||
18 | function disposeOnDeactivation(disposable: vscode.Disposable) { | ||
19 | context.subscriptions.push(disposable); | ||
20 | } | ||
21 | |||
22 | function registerCommand(name: string, f: any) { | ||
23 | disposeOnDeactivation(vscode.commands.registerCommand(name, f)); | ||
24 | } | ||
25 | function overrideCommand( | ||
26 | name: string, | ||
27 | f: (...args: any[]) => Promise<boolean>, | ||
28 | ) { | ||
29 | const defaultCmd = `default:${name}`; | ||
30 | const original = (...args: any[]) => | ||
31 | vscode.commands.executeCommand(defaultCmd, ...args); | ||
32 | |||
33 | try { | ||
34 | registerCommand(name, async (...args: any[]) => { | ||
35 | const editor = vscode.window.activeTextEditor; | ||
36 | if ( | ||
37 | !editor || | ||
38 | !editor.document || | ||
39 | editor.document.languageId !== 'rust' | ||
40 | ) { | ||
41 | return await original(...args); | ||
42 | } | ||
43 | if (!(await f(...args))) { | ||
44 | return await original(...args); | ||
45 | } | ||
46 | }); | ||
47 | } catch (_) { | ||
48 | vscode.window.showWarningMessage( | ||
49 | 'Enhanced typing feature is disabled because of incompatibility with VIM extension, consider turning off rust-analyzer.enableEnhancedTyping: https://github.com/rust-analyzer/rust-analyzer/blob/master/docs/user/README.md#settings', | ||
50 | ); | ||
51 | } | ||
52 | } | ||
53 | |||
54 | // Commands are requests from vscode to the language server | ||
55 | registerCommand( | ||
56 | 'rust-analyzer.analyzerStatus', | ||
57 | commands.analyzerStatus.makeCommand(context), | ||
58 | ); | ||
59 | registerCommand('rust-analyzer.collectGarbage', () => | ||
60 | Server.client.sendRequest<null>('rust-analyzer/collectGarbage', null), | ||
61 | ); | ||
62 | registerCommand( | ||
63 | 'rust-analyzer.matchingBrace', | ||
64 | commands.matchingBrace.handle, | ||
65 | ); | ||
66 | registerCommand('rust-analyzer.joinLines', commands.joinLines.handle); | ||
67 | registerCommand('rust-analyzer.parentModule', commands.parentModule.handle); | ||
68 | registerCommand('rust-analyzer.run', commands.runnables.handle); | ||
69 | // Unlike the above this does not send requests to the language server | ||
70 | registerCommand('rust-analyzer.runSingle', commands.runnables.handleSingle); | ||
71 | registerCommand( | ||
72 | 'rust-analyzer.applySourceChange', | ||
73 | commands.applySourceChange.handle, | ||
74 | ); | ||
75 | registerCommand( | ||
76 | 'rust-analyzer.showReferences', | ||
77 | (uri: string, position: lc.Position, locations: lc.Location[]) => { | ||
78 | vscode.commands.executeCommand( | ||
79 | 'editor.action.showReferences', | ||
80 | vscode.Uri.parse(uri), | ||
81 | Server.client.protocol2CodeConverter.asPosition(position), | ||
82 | locations.map(Server.client.protocol2CodeConverter.asLocation), | ||
83 | ); | ||
84 | }, | ||
85 | ); | ||
86 | |||
87 | if (Server.config.enableEnhancedTyping) { | ||
88 | overrideCommand('type', commands.onEnter.handle); | ||
89 | } | ||
90 | |||
91 | // Notifications are events triggered by the language server | ||
92 | const allNotifications: Iterable<[ | ||
93 | string, | ||
94 | lc.GenericNotificationHandler, | ||
95 | ]> = [ | ||
96 | [ | ||
97 | 'rust-analyzer/publishDecorations', | ||
98 | notifications.publishDecorations.handle, | ||
99 | ], | ||
100 | ]; | ||
101 | const syntaxTreeContentProvider = new SyntaxTreeContentProvider(); | ||
102 | const expandMacroContentProvider = new ExpandMacroContentProvider(); | ||
103 | |||
104 | // The events below are plain old javascript events, triggered and handled by vscode | ||
105 | vscode.window.onDidChangeActiveTextEditor( | ||
106 | events.changeActiveTextEditor.makeHandler(syntaxTreeContentProvider), | ||
107 | ); | ||
108 | |||
109 | disposeOnDeactivation( | ||
110 | vscode.workspace.registerTextDocumentContentProvider( | ||
111 | 'rust-analyzer', | ||
112 | syntaxTreeContentProvider, | ||
113 | ), | ||
114 | ); | ||
115 | disposeOnDeactivation( | ||
116 | vscode.workspace.registerTextDocumentContentProvider( | ||
117 | 'rust-analyzer', | ||
118 | expandMacroContentProvider, | ||
119 | ), | ||
120 | ); | ||
121 | |||
122 | registerCommand( | ||
123 | 'rust-analyzer.syntaxTree', | ||
124 | commands.syntaxTree.createHandle(syntaxTreeContentProvider), | ||
125 | ); | ||
126 | registerCommand( | ||
127 | 'rust-analyzer.expandMacro', | ||
128 | commands.expandMacro.createHandle(expandMacroContentProvider), | ||
129 | ); | ||
130 | |||
131 | vscode.workspace.onDidChangeTextDocument( | ||
132 | events.changeTextDocument.createHandler(syntaxTreeContentProvider), | ||
133 | null, | ||
134 | context.subscriptions, | ||
135 | ); | ||
136 | |||
137 | const startServer = () => Server.start(allNotifications); | ||
138 | const reloadCommand = () => reloadServer(startServer); | ||
139 | |||
140 | vscode.commands.registerCommand('rust-analyzer.reload', reloadCommand); | ||
141 | |||
142 | // Executing `cargo watch` provides us with inline diagnostics on save | ||
143 | let provider: CargoWatchProvider | undefined; | ||
144 | interactivelyStartCargoWatch(context).then(p => { | ||
145 | provider = p; | ||
146 | }); | ||
147 | registerCommand('rust-analyzer.startCargoWatch', () => { | ||
148 | if (provider) { | ||
149 | provider.start(); | ||
150 | } else { | ||
151 | startCargoWatch(context).then(p => { | ||
152 | provider = p; | ||
153 | }); | ||
154 | } | ||
155 | }); | ||
156 | registerCommand('rust-analyzer.stopCargoWatch', () => { | ||
157 | if (provider) { | ||
158 | provider.stop(); | ||
159 | } | ||
160 | }); | ||
161 | |||
162 | // Start the language server, finally! | ||
163 | try { | ||
164 | await startServer(); | ||
165 | } catch (e) { | ||
166 | vscode.window.showErrorMessage(e.message); | ||
167 | } | ||
168 | |||
169 | if (Server.config.displayInlayHints) { | ||
170 | const hintsUpdater = new HintsUpdater(); | ||
171 | hintsUpdater.refreshHintsForVisibleEditors().then(() => { | ||
172 | // vscode may ignore top level hintsUpdater.refreshHintsForVisibleEditors() | ||
173 | // so update the hints once when the focus changes to guarantee their presence | ||
174 | let editorChangeDisposable: vscode.Disposable | null = null; | ||
175 | editorChangeDisposable = vscode.window.onDidChangeActiveTextEditor( | ||
176 | _ => { | ||
177 | if (editorChangeDisposable !== null) { | ||
178 | editorChangeDisposable.dispose(); | ||
179 | } | ||
180 | return hintsUpdater.refreshHintsForVisibleEditors(); | ||
181 | }, | ||
182 | ); | ||
183 | |||
184 | disposeOnDeactivation( | ||
185 | vscode.window.onDidChangeVisibleTextEditors(_ => | ||
186 | hintsUpdater.refreshHintsForVisibleEditors(), | ||
187 | ), | ||
188 | ); | ||
189 | disposeOnDeactivation( | ||
190 | vscode.workspace.onDidChangeTextDocument(e => | ||
191 | hintsUpdater.refreshHintsForVisibleEditors(e), | ||
192 | ), | ||
193 | ); | ||
194 | disposeOnDeactivation( | ||
195 | vscode.workspace.onDidChangeConfiguration(_ => | ||
196 | hintsUpdater.toggleHintsDisplay( | ||
197 | Server.config.displayInlayHints, | ||
198 | ), | ||
199 | ), | ||
200 | ); | ||
201 | }); | ||
202 | } | ||
203 | } | ||
204 | |||
205 | export function deactivate(): Thenable<void> { | ||
206 | if (!Server.client) { | ||
207 | return Promise.resolve(); | ||
208 | } | ||
209 | return Server.client.stop(); | ||
210 | } | ||
211 | |||
212 | async function reloadServer(startServer: () => Promise<void>) { | ||
213 | if (Server.client != null) { | ||
214 | vscode.window.showInformationMessage('Reloading rust-analyzer...'); | ||
215 | await Server.client.stop(); | ||
216 | await startServer(); | ||
217 | } | ||
218 | } | ||
diff --git a/editors/code/src/highlighting.ts b/editors/code/src/highlighting.ts index e1b0d13e7..014e96f75 100644 --- a/editors/code/src/highlighting.ts +++ b/editors/code/src/highlighting.ts | |||
@@ -1,10 +1,69 @@ | |||
1 | import seedrandom = require('seedrandom'); | ||
2 | import * as vscode from 'vscode'; | 1 | import * as vscode from 'vscode'; |
3 | import * as lc from 'vscode-languageclient'; | 2 | import * as lc from 'vscode-languageclient'; |
3 | import * as seedrandom_ from 'seedrandom'; | ||
4 | const seedrandom = seedrandom_; // https://github.com/jvandemo/generator-angular2-library/issues/221#issuecomment-355945207 | ||
5 | |||
6 | import { ColorTheme, TextMateRuleSettings } from './color_theme'; | ||
7 | |||
8 | import { Ctx, sendRequestWithRetry } from './ctx'; | ||
9 | |||
10 | export function activateHighlighting(ctx: Ctx) { | ||
11 | const highlighter = new Highlighter(ctx); | ||
12 | ctx.onDidRestart(client => { | ||
13 | client.onNotification( | ||
14 | 'rust-analyzer/publishDecorations', | ||
15 | (params: PublishDecorationsParams) => { | ||
16 | if (!ctx.config.highlightingOn) return; | ||
17 | |||
18 | const targetEditor = vscode.window.visibleTextEditors.find( | ||
19 | editor => { | ||
20 | const unescapedUri = unescape( | ||
21 | editor.document.uri.toString(), | ||
22 | ); | ||
23 | // Unescaped URI looks like: | ||
24 | // file:///c:/Workspace/ra-test/src/main.rs | ||
25 | return unescapedUri === params.uri; | ||
26 | }, | ||
27 | ); | ||
28 | if (!targetEditor) return; | ||
29 | |||
30 | highlighter.setHighlights(targetEditor, params.decorations); | ||
31 | }, | ||
32 | ); | ||
33 | }); | ||
34 | |||
35 | vscode.workspace.onDidChangeConfiguration( | ||
36 | _ => highlighter.removeHighlights(), | ||
37 | ctx.subscriptions, | ||
38 | ); | ||
39 | |||
40 | vscode.window.onDidChangeActiveTextEditor( | ||
41 | async (editor: vscode.TextEditor | undefined) => { | ||
42 | if (!editor || editor.document.languageId !== 'rust') return; | ||
43 | if (!ctx.config.highlightingOn) return; | ||
44 | let client = ctx.client; | ||
45 | if (!client) return; | ||
46 | |||
47 | const params: lc.TextDocumentIdentifier = { | ||
48 | uri: editor.document.uri.toString(), | ||
49 | }; | ||
50 | const decorations = await sendRequestWithRetry<Decoration[]>( | ||
51 | client, | ||
52 | 'rust-analyzer/decorationsRequest', | ||
53 | params, | ||
54 | ); | ||
55 | highlighter.setHighlights(editor, decorations); | ||
56 | }, | ||
57 | ctx.subscriptions, | ||
58 | ); | ||
59 | } | ||
4 | 60 | ||
5 | import { Server } from './server'; | 61 | interface PublishDecorationsParams { |
62 | uri: string; | ||
63 | decorations: Decoration[]; | ||
64 | } | ||
6 | 65 | ||
7 | export interface Decoration { | 66 | interface Decoration { |
8 | range: lc.Range; | 67 | range: lc.Range; |
9 | tag: string; | 68 | tag: string; |
10 | bindingHash?: string; | 69 | bindingHash?: string; |
@@ -23,62 +82,17 @@ function fancify(seed: string, shade: 'light' | 'dark') { | |||
23 | return `hsl(${h},${s}%,${l}%)`; | 82 | return `hsl(${h},${s}%,${l}%)`; |
24 | } | 83 | } |
25 | 84 | ||
26 | export class Highlighter { | 85 | class Highlighter { |
27 | private static initDecorations(): Map< | 86 | private ctx: Ctx; |
28 | string, | ||
29 | vscode.TextEditorDecorationType | ||
30 | > { | ||
31 | const decoration = ( | ||
32 | tag: string, | ||
33 | textDecoration?: string, | ||
34 | ): [string, vscode.TextEditorDecorationType] => { | ||
35 | const color = new vscode.ThemeColor('ralsp.' + tag); | ||
36 | const decor = vscode.window.createTextEditorDecorationType({ | ||
37 | color, | ||
38 | textDecoration, | ||
39 | }); | ||
40 | return [tag, decor]; | ||
41 | }; | ||
42 | |||
43 | const decorations: Iterable<[ | ||
44 | string, | ||
45 | vscode.TextEditorDecorationType, | ||
46 | ]> = [ | ||
47 | decoration('comment'), | ||
48 | decoration('string'), | ||
49 | decoration('keyword'), | ||
50 | decoration('keyword.control'), | ||
51 | decoration('keyword.unsafe'), | ||
52 | decoration('function'), | ||
53 | decoration('parameter'), | ||
54 | decoration('constant'), | ||
55 | decoration('type.builtin'), | ||
56 | decoration('type.generic'), | ||
57 | decoration('type.lifetime'), | ||
58 | decoration('type.param'), | ||
59 | decoration('type.self'), | ||
60 | decoration('type'), | ||
61 | decoration('text'), | ||
62 | decoration('attribute'), | ||
63 | decoration('literal'), | ||
64 | decoration('literal.numeric'), | ||
65 | decoration('literal.char'), | ||
66 | decoration('literal.byte'), | ||
67 | decoration('macro'), | ||
68 | decoration('variable'), | ||
69 | decoration('variable.mut', 'underline'), | ||
70 | decoration('field'), | ||
71 | decoration('module'), | ||
72 | ]; | ||
73 | |||
74 | return new Map<string, vscode.TextEditorDecorationType>(decorations); | ||
75 | } | ||
76 | |||
77 | private decorations: Map< | 87 | private decorations: Map< |
78 | string, | 88 | string, |
79 | vscode.TextEditorDecorationType | 89 | vscode.TextEditorDecorationType |
80 | > | null = null; | 90 | > | null = null; |
81 | 91 | ||
92 | constructor(ctx: Ctx) { | ||
93 | this.ctx = ctx; | ||
94 | } | ||
95 | |||
82 | public removeHighlights() { | 96 | public removeHighlights() { |
83 | if (this.decorations == null) { | 97 | if (this.decorations == null) { |
84 | return; | 98 | return; |
@@ -93,12 +107,14 @@ export class Highlighter { | |||
93 | } | 107 | } |
94 | 108 | ||
95 | public setHighlights(editor: vscode.TextEditor, highlights: Decoration[]) { | 109 | public setHighlights(editor: vscode.TextEditor, highlights: Decoration[]) { |
110 | let client = this.ctx.client; | ||
111 | if (!client) return; | ||
96 | // Initialize decorations if necessary | 112 | // Initialize decorations if necessary |
97 | // | 113 | // |
98 | // Note: decoration objects need to be kept around so we can dispose them | 114 | // Note: decoration objects need to be kept around so we can dispose them |
99 | // if the user disables syntax highlighting | 115 | // if the user disables syntax highlighting |
100 | if (this.decorations == null) { | 116 | if (this.decorations == null) { |
101 | this.decorations = Highlighter.initDecorations(); | 117 | this.decorations = initDecorations(); |
102 | } | 118 | } |
103 | 119 | ||
104 | const byTag: Map<string, vscode.Range[]> = new Map(); | 120 | const byTag: Map<string, vscode.Range[]> = new Map(); |
@@ -106,7 +122,7 @@ export class Highlighter { | |||
106 | string, | 122 | string, |
107 | [vscode.Range[], boolean] | 123 | [vscode.Range[], boolean] |
108 | > = new Map(); | 124 | > = new Map(); |
109 | const rainbowTime = Server.config.rainbowHighlightingOn; | 125 | const rainbowTime = this.ctx.config.rainbowHighlightingOn; |
110 | 126 | ||
111 | for (const tag of this.decorations.keys()) { | 127 | for (const tag of this.decorations.keys()) { |
112 | byTag.set(tag, []); | 128 | byTag.set(tag, []); |
@@ -125,13 +141,13 @@ export class Highlighter { | |||
125 | colorfulIdents | 141 | colorfulIdents |
126 | .get(d.bindingHash)![0] | 142 | .get(d.bindingHash)![0] |
127 | .push( | 143 | .push( |
128 | Server.client.protocol2CodeConverter.asRange(d.range), | 144 | client.protocol2CodeConverter.asRange(d.range), |
129 | ); | 145 | ); |
130 | } else { | 146 | } else { |
131 | byTag | 147 | byTag |
132 | .get(d.tag)! | 148 | .get(d.tag)! |
133 | .push( | 149 | .push( |
134 | Server.client.protocol2CodeConverter.asRange(d.range), | 150 | client.protocol2CodeConverter.asRange(d.range), |
135 | ); | 151 | ); |
136 | } | 152 | } |
137 | } | 153 | } |
@@ -154,3 +170,80 @@ export class Highlighter { | |||
154 | } | 170 | } |
155 | } | 171 | } |
156 | } | 172 | } |
173 | |||
174 | function initDecorations(): Map<string, vscode.TextEditorDecorationType> { | ||
175 | const theme = ColorTheme.load(); | ||
176 | const res = new Map(); | ||
177 | TAG_TO_SCOPES.forEach((scopes, tag) => { | ||
178 | if (!scopes) throw `unmapped tag: ${tag}`; | ||
179 | let rule = theme.lookup(scopes); | ||
180 | const decor = createDecorationFromTextmate(rule); | ||
181 | res.set(tag, decor); | ||
182 | }); | ||
183 | return res; | ||
184 | } | ||
185 | |||
186 | function createDecorationFromTextmate( | ||
187 | themeStyle: TextMateRuleSettings, | ||
188 | ): vscode.TextEditorDecorationType { | ||
189 | const decorationOptions: vscode.DecorationRenderOptions = {}; | ||
190 | decorationOptions.rangeBehavior = vscode.DecorationRangeBehavior.OpenOpen; | ||
191 | |||
192 | if (themeStyle.foreground) { | ||
193 | decorationOptions.color = themeStyle.foreground; | ||
194 | } | ||
195 | |||
196 | if (themeStyle.background) { | ||
197 | decorationOptions.backgroundColor = themeStyle.background; | ||
198 | } | ||
199 | |||
200 | if (themeStyle.fontStyle) { | ||
201 | const parts: string[] = themeStyle.fontStyle.split(' '); | ||
202 | parts.forEach(part => { | ||
203 | switch (part) { | ||
204 | case 'italic': | ||
205 | decorationOptions.fontStyle = 'italic'; | ||
206 | break; | ||
207 | case 'bold': | ||
208 | decorationOptions.fontWeight = 'bold'; | ||
209 | break; | ||
210 | case 'underline': | ||
211 | decorationOptions.textDecoration = 'underline'; | ||
212 | break; | ||
213 | default: | ||
214 | break; | ||
215 | } | ||
216 | }); | ||
217 | } | ||
218 | return vscode.window.createTextEditorDecorationType(decorationOptions); | ||
219 | } | ||
220 | |||
221 | // sync with tags from `syntax_highlighting.rs`. | ||
222 | const TAG_TO_SCOPES = new Map<string, string[]>([ | ||
223 | ["field", ["entity.name.field"]], | ||
224 | ["function", ["entity.name.function"]], | ||
225 | ["module", ["entity.name.module"]], | ||
226 | ["constant", ["entity.name.constant"]], | ||
227 | ["macro", ["entity.name.macro"]], | ||
228 | |||
229 | ["variable", ["variable"]], | ||
230 | ["variable.mut", ["variable", "meta.mutable"]], | ||
231 | |||
232 | ["type", ["entity.name.type"]], | ||
233 | ["type.builtin", ["entity.name.type", "support.type.primitive"]], | ||
234 | ["type.self", ["entity.name.type.parameter.self"]], | ||
235 | ["type.param", ["entity.name.type.parameter"]], | ||
236 | ["type.lifetime", ["entity.name.type.lifetime"]], | ||
237 | |||
238 | ["literal.byte", ["constant.character.byte"]], | ||
239 | ["literal.char", ["constant.character"]], | ||
240 | ["literal.numeric", ["constant.numeric"]], | ||
241 | |||
242 | ["comment", ["comment"]], | ||
243 | ["string", ["string.quoted"]], | ||
244 | ["attribute", ["meta.attribute"]], | ||
245 | |||
246 | ["keyword", ["keyword"]], | ||
247 | ["keyword.unsafe", ["keyword.other.unsafe"]], | ||
248 | ["keyword.control", ["keyword.control"]], | ||
249 | ]); | ||
diff --git a/editors/code/src/inlay_hints.ts b/editors/code/src/inlay_hints.ts new file mode 100644 index 000000000..6dd767d72 --- /dev/null +++ b/editors/code/src/inlay_hints.ts | |||
@@ -0,0 +1,120 @@ | |||
1 | import * as vscode from 'vscode'; | ||
2 | import * as lc from 'vscode-languageclient'; | ||
3 | |||
4 | import { Ctx, sendRequestWithRetry } from './ctx'; | ||
5 | |||
6 | export function activateInlayHints(ctx: Ctx) { | ||
7 | const hintsUpdater = new HintsUpdater(ctx); | ||
8 | vscode.window.onDidChangeVisibleTextEditors(async _ => { | ||
9 | await hintsUpdater.refresh(); | ||
10 | }, ctx.subscriptions); | ||
11 | |||
12 | vscode.workspace.onDidChangeTextDocument(async e => { | ||
13 | if (e.contentChanges.length === 0) return; | ||
14 | if (e.document.languageId !== 'rust') return; | ||
15 | await hintsUpdater.refresh(); | ||
16 | }, ctx.subscriptions); | ||
17 | |||
18 | vscode.workspace.onDidChangeConfiguration(_ => { | ||
19 | hintsUpdater.setEnabled(ctx.config.displayInlayHints); | ||
20 | }, ctx.subscriptions); | ||
21 | |||
22 | ctx.onDidRestart(_ => hintsUpdater.setEnabled(ctx.config.displayInlayHints)); | ||
23 | } | ||
24 | |||
25 | interface InlayHintsParams { | ||
26 | textDocument: lc.TextDocumentIdentifier; | ||
27 | } | ||
28 | |||
29 | interface InlayHint { | ||
30 | range: vscode.Range; | ||
31 | kind: string; | ||
32 | label: string; | ||
33 | } | ||
34 | |||
35 | const typeHintDecorationType = vscode.window.createTextEditorDecorationType({ | ||
36 | after: { | ||
37 | color: new vscode.ThemeColor('rust_analyzer.inlayHint'), | ||
38 | }, | ||
39 | }); | ||
40 | |||
41 | class HintsUpdater { | ||
42 | private pending: Map<string, vscode.CancellationTokenSource> = new Map(); | ||
43 | private ctx: Ctx; | ||
44 | private enabled = true; | ||
45 | |||
46 | constructor(ctx: Ctx) { | ||
47 | this.ctx = ctx; | ||
48 | } | ||
49 | |||
50 | async setEnabled(enabled: boolean) { | ||
51 | if (this.enabled == enabled) return; | ||
52 | this.enabled = enabled; | ||
53 | |||
54 | if (this.enabled) { | ||
55 | await this.refresh(); | ||
56 | } else { | ||
57 | this.allEditors.forEach(it => this.setDecorations(it, [])); | ||
58 | } | ||
59 | } | ||
60 | |||
61 | async refresh() { | ||
62 | if (!this.enabled) return; | ||
63 | const promises = this.allEditors.map(it => this.refreshEditor(it)); | ||
64 | await Promise.all(promises); | ||
65 | } | ||
66 | |||
67 | private async refreshEditor(editor: vscode.TextEditor): Promise<void> { | ||
68 | const newHints = await this.queryHints(editor.document.uri.toString()); | ||
69 | if (newHints == null) return; | ||
70 | const newDecorations = newHints.map(hint => ({ | ||
71 | range: hint.range, | ||
72 | renderOptions: { | ||
73 | after: { | ||
74 | contentText: `: ${hint.label}`, | ||
75 | }, | ||
76 | }, | ||
77 | })); | ||
78 | this.setDecorations(editor, newDecorations); | ||
79 | } | ||
80 | |||
81 | private get allEditors(): vscode.TextEditor[] { | ||
82 | return vscode.window.visibleTextEditors.filter( | ||
83 | editor => editor.document.languageId === 'rust', | ||
84 | ); | ||
85 | } | ||
86 | |||
87 | private setDecorations( | ||
88 | editor: vscode.TextEditor, | ||
89 | decorations: vscode.DecorationOptions[], | ||
90 | ) { | ||
91 | editor.setDecorations( | ||
92 | typeHintDecorationType, | ||
93 | this.enabled ? decorations : [], | ||
94 | ); | ||
95 | } | ||
96 | |||
97 | private async queryHints(documentUri: string): Promise<InlayHint[] | null> { | ||
98 | let client = this.ctx.client; | ||
99 | if (!client) return null; | ||
100 | const request: InlayHintsParams = { | ||
101 | textDocument: { uri: documentUri }, | ||
102 | }; | ||
103 | let tokenSource = new vscode.CancellationTokenSource(); | ||
104 | let prev = this.pending.get(documentUri); | ||
105 | if (prev) prev.cancel(); | ||
106 | this.pending.set(documentUri, tokenSource); | ||
107 | try { | ||
108 | return await sendRequestWithRetry<InlayHint[] | null>( | ||
109 | client, | ||
110 | 'rust-analyzer/inlayHints', | ||
111 | request, | ||
112 | tokenSource.token, | ||
113 | ); | ||
114 | } finally { | ||
115 | if (!tokenSource.token.isCancellationRequested) { | ||
116 | this.pending.delete(documentUri); | ||
117 | } | ||
118 | } | ||
119 | } | ||
120 | } | ||
diff --git a/editors/code/src/main.ts b/editors/code/src/main.ts new file mode 100644 index 000000000..51dedd5ef --- /dev/null +++ b/editors/code/src/main.ts | |||
@@ -0,0 +1,51 @@ | |||
1 | import * as vscode from 'vscode'; | ||
2 | |||
3 | import * as commands from './commands'; | ||
4 | import { activateInlayHints } from './inlay_hints'; | ||
5 | import { activateStatusDisplay } from './status_display'; | ||
6 | import { Ctx } from './ctx'; | ||
7 | import { activateHighlighting } from './highlighting'; | ||
8 | |||
9 | let ctx!: Ctx; | ||
10 | |||
11 | export async function activate(context: vscode.ExtensionContext) { | ||
12 | ctx = new Ctx(context); | ||
13 | |||
14 | // Note: we try to start the server before we register various commands, so | ||
15 | // that it registers its `onDidChangeDocument` handler before us. | ||
16 | // | ||
17 | // This a horribly, horribly wrong way to deal with this problem. | ||
18 | try { | ||
19 | await ctx.restartServer(); | ||
20 | } catch (e) { | ||
21 | vscode.window.showErrorMessage(e.message); | ||
22 | } | ||
23 | |||
24 | |||
25 | // Commands which invokes manually via command pallet, shortcut, etc. | ||
26 | ctx.registerCommand('analyzerStatus', commands.analyzerStatus); | ||
27 | ctx.registerCommand('collectGarbage', commands.collectGarbage); | ||
28 | ctx.registerCommand('matchingBrace', commands.matchingBrace); | ||
29 | ctx.registerCommand('joinLines', commands.joinLines); | ||
30 | ctx.registerCommand('parentModule', commands.parentModule); | ||
31 | ctx.registerCommand('syntaxTree', commands.syntaxTree); | ||
32 | ctx.registerCommand('expandMacro', commands.expandMacro); | ||
33 | ctx.registerCommand('run', commands.run); | ||
34 | ctx.registerCommand('reload', commands.reload); | ||
35 | |||
36 | // Internal commands which are invoked by the server. | ||
37 | ctx.registerCommand('runSingle', commands.runSingle); | ||
38 | ctx.registerCommand('showReferences', commands.showReferences); | ||
39 | ctx.registerCommand('applySourceChange', commands.applySourceChange); | ||
40 | |||
41 | if (ctx.config.enableEnhancedTyping) { | ||
42 | ctx.overrideCommand('type', commands.onEnter); | ||
43 | } | ||
44 | activateStatusDisplay(ctx); | ||
45 | activateHighlighting(ctx); | ||
46 | activateInlayHints(ctx); | ||
47 | } | ||
48 | |||
49 | export async function deactivate() { | ||
50 | await ctx?.client?.stop(); | ||
51 | } | ||
diff --git a/editors/code/src/notifications/index.ts b/editors/code/src/notifications/index.ts deleted file mode 100644 index 74c4c3563..000000000 --- a/editors/code/src/notifications/index.ts +++ /dev/null | |||
@@ -1,3 +0,0 @@ | |||
1 | import * as publishDecorations from './publish_decorations'; | ||
2 | |||
3 | export { publishDecorations }; | ||
diff --git a/editors/code/src/notifications/publish_decorations.ts b/editors/code/src/notifications/publish_decorations.ts deleted file mode 100644 index f23e286ad..000000000 --- a/editors/code/src/notifications/publish_decorations.ts +++ /dev/null | |||
@@ -1,24 +0,0 @@ | |||
1 | import * as vscode from 'vscode'; | ||
2 | |||
3 | import { Decoration } from '../highlighting'; | ||
4 | import { Server } from '../server'; | ||
5 | |||
6 | export interface PublishDecorationsParams { | ||
7 | uri: string; | ||
8 | decorations: Decoration[]; | ||
9 | } | ||
10 | |||
11 | export function handle(params: PublishDecorationsParams) { | ||
12 | const targetEditor = vscode.window.visibleTextEditors.find(editor => { | ||
13 | const unescapedUri = unescape(editor.document.uri.toString()); | ||
14 | // Unescaped URI looks like: | ||
15 | // file:///c:/Workspace/ra-test/src/main.rs | ||
16 | return unescapedUri === params.uri; | ||
17 | }); | ||
18 | |||
19 | if (!Server.config.highlightingOn || !targetEditor) { | ||
20 | return; | ||
21 | } | ||
22 | |||
23 | Server.highlighter.setHighlights(targetEditor, params.decorations); | ||
24 | } | ||
diff --git a/editors/code/src/server.ts b/editors/code/src/server.ts deleted file mode 100644 index 5ace1d0fa..000000000 --- a/editors/code/src/server.ts +++ /dev/null | |||
@@ -1,109 +0,0 @@ | |||
1 | import { lookpath } from 'lookpath'; | ||
2 | import { homedir, platform } from 'os'; | ||
3 | import * as lc from 'vscode-languageclient'; | ||
4 | |||
5 | import { window, workspace } from 'vscode'; | ||
6 | import { Config } from './config'; | ||
7 | import { Highlighter } from './highlighting'; | ||
8 | |||
9 | function expandPathResolving(path: string) { | ||
10 | if (path.startsWith('~/')) { | ||
11 | return path.replace('~', homedir()); | ||
12 | } | ||
13 | return path; | ||
14 | } | ||
15 | |||
16 | export class Server { | ||
17 | public static highlighter = new Highlighter(); | ||
18 | public static config = new Config(); | ||
19 | public static client: lc.LanguageClient; | ||
20 | |||
21 | public static async start( | ||
22 | notificationHandlers: Iterable<[string, lc.GenericNotificationHandler]>, | ||
23 | ) { | ||
24 | // '.' Is the fallback if no folder is open | ||
25 | // TODO?: Workspace folders support Uri's (eg: file://test.txt). It might be a good idea to test if the uri points to a file. | ||
26 | let folder: string = '.'; | ||
27 | if (workspace.workspaceFolders !== undefined) { | ||
28 | folder = workspace.workspaceFolders[0].uri.fsPath.toString(); | ||
29 | } | ||
30 | |||
31 | const command = expandPathResolving(this.config.raLspServerPath); | ||
32 | // FIXME: remove check when the following issue is fixed: | ||
33 | // https://github.com/otiai10/lookpath/issues/4 | ||
34 | if (platform() !== 'win32') { | ||
35 | if (!(await lookpath(command))) { | ||
36 | throw new Error( | ||
37 | `Cannot find rust-analyzer server \`${command}\` in PATH.`, | ||
38 | ); | ||
39 | } | ||
40 | } | ||
41 | const run: lc.Executable = { | ||
42 | command, | ||
43 | options: { cwd: folder }, | ||
44 | }; | ||
45 | const serverOptions: lc.ServerOptions = { | ||
46 | run, | ||
47 | debug: run, | ||
48 | }; | ||
49 | const traceOutputChannel = window.createOutputChannel( | ||
50 | 'Rust Analyzer Language Server Trace', | ||
51 | ); | ||
52 | const clientOptions: lc.LanguageClientOptions = { | ||
53 | documentSelector: [{ scheme: 'file', language: 'rust' }], | ||
54 | initializationOptions: { | ||
55 | publishDecorations: true, | ||
56 | lruCapacity: Server.config.lruCapacity, | ||
57 | maxInlayHintLength: Server.config.maxInlayHintLength, | ||
58 | excludeGlobs: Server.config.excludeGlobs, | ||
59 | useClientWatching: Server.config.useClientWatching, | ||
60 | featureFlags: Server.config.featureFlags, | ||
61 | withSysroot: Server.config.withSysroot, | ||
62 | cargoFeatures: Server.config.cargoFeatures, | ||
63 | }, | ||
64 | traceOutputChannel, | ||
65 | }; | ||
66 | |||
67 | Server.client = new lc.LanguageClient( | ||
68 | 'rust-analyzer', | ||
69 | 'Rust Analyzer Language Server', | ||
70 | serverOptions, | ||
71 | clientOptions, | ||
72 | ); | ||
73 | // HACK: This is an awful way of filtering out the decorations notifications | ||
74 | // However, pending proper support, this is the most effecitve approach | ||
75 | // Proper support for this would entail a change to vscode-languageclient to allow not notifying on certain messages | ||
76 | // Or the ability to disable the serverside component of highlighting (but this means that to do tracing we need to disable hihlighting) | ||
77 | // This also requires considering our settings strategy, which is work which needs doing | ||
78 | // @ts-ignore The tracer is private to vscode-languageclient, but we need access to it to not log publishDecorations requests | ||
79 | Server.client._tracer = { | ||
80 | log: (messageOrDataObject: string | any, data?: string) => { | ||
81 | if (typeof messageOrDataObject === 'string') { | ||
82 | if ( | ||
83 | messageOrDataObject.includes( | ||
84 | 'rust-analyzer/publishDecorations', | ||
85 | ) || | ||
86 | messageOrDataObject.includes( | ||
87 | 'rust-analyzer/decorationsRequest', | ||
88 | ) | ||
89 | ) { | ||
90 | // Don't log publish decorations requests | ||
91 | } else { | ||
92 | // @ts-ignore This is just a utility function | ||
93 | Server.client.logTrace(messageOrDataObject, data); | ||
94 | } | ||
95 | } else { | ||
96 | // @ts-ignore | ||
97 | Server.client.logObjectTrace(messageOrDataObject); | ||
98 | } | ||
99 | }, | ||
100 | }; | ||
101 | Server.client.registerProposedFeatures(); | ||
102 | Server.client.onReady().then(() => { | ||
103 | for (const [type, handler] of notificationHandlers) { | ||
104 | Server.client.onNotification(type, handler); | ||
105 | } | ||
106 | }); | ||
107 | Server.client.start(); | ||
108 | } | ||
109 | } | ||
diff --git a/editors/code/src/commands/apply_source_change.ts b/editors/code/src/source_change.ts index 8167398b1..a336269ba 100644 --- a/editors/code/src/commands/apply_source_change.ts +++ b/editors/code/src/source_change.ts | |||
@@ -1,7 +1,7 @@ | |||
1 | import * as vscode from 'vscode'; | 1 | import * as vscode from 'vscode'; |
2 | import * as lc from 'vscode-languageclient'; | 2 | import * as lc from 'vscode-languageclient'; |
3 | 3 | ||
4 | import { Server } from '../server'; | 4 | import { Ctx } from './ctx'; |
5 | 5 | ||
6 | export interface SourceChange { | 6 | export interface SourceChange { |
7 | label: string; | 7 | label: string; |
@@ -9,8 +9,11 @@ export interface SourceChange { | |||
9 | cursorPosition?: lc.TextDocumentPositionParams; | 9 | cursorPosition?: lc.TextDocumentPositionParams; |
10 | } | 10 | } |
11 | 11 | ||
12 | export async function handle(change: SourceChange) { | 12 | export async function applySourceChange(ctx: Ctx, change: SourceChange) { |
13 | const wsEdit = Server.client.protocol2CodeConverter.asWorkspaceEdit( | 13 | const client = ctx.client; |
14 | if (!client) return; | ||
15 | |||
16 | const wsEdit = client.protocol2CodeConverter.asWorkspaceEdit( | ||
14 | change.workspaceEdit, | 17 | change.workspaceEdit, |
15 | ); | 18 | ); |
16 | let created; | 19 | let created; |
@@ -32,10 +35,10 @@ export async function handle(change: SourceChange) { | |||
32 | const doc = await vscode.workspace.openTextDocument(toOpenUri); | 35 | const doc = await vscode.workspace.openTextDocument(toOpenUri); |
33 | await vscode.window.showTextDocument(doc); | 36 | await vscode.window.showTextDocument(doc); |
34 | } else if (toReveal) { | 37 | } else if (toReveal) { |
35 | const uri = Server.client.protocol2CodeConverter.asUri( | 38 | const uri = client.protocol2CodeConverter.asUri( |
36 | toReveal.textDocument.uri, | 39 | toReveal.textDocument.uri, |
37 | ); | 40 | ); |
38 | const position = Server.client.protocol2CodeConverter.asPosition( | 41 | const position = client.protocol2CodeConverter.asPosition( |
39 | toReveal.position, | 42 | toReveal.position, |
40 | ); | 43 | ); |
41 | const editor = vscode.window.activeTextEditor; | 44 | const editor = vscode.window.activeTextEditor; |
diff --git a/editors/code/src/status_display.ts b/editors/code/src/status_display.ts new file mode 100644 index 000000000..08cdc8bdf --- /dev/null +++ b/editors/code/src/status_display.ts | |||
@@ -0,0 +1,115 @@ | |||
1 | import * as vscode from 'vscode'; | ||
2 | |||
3 | import { Ctx } from './ctx'; | ||
4 | |||
5 | const spinnerFrames = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏']; | ||
6 | |||
7 | export function activateStatusDisplay(ctx: Ctx) { | ||
8 | const statusDisplay = new StatusDisplay(ctx.config.cargoWatchOptions.command); | ||
9 | ctx.pushCleanup(statusDisplay); | ||
10 | ctx.onDidRestart(client => { | ||
11 | client.onNotification('$/progress', params => statusDisplay.handleProgressNotification(params)); | ||
12 | }); | ||
13 | } | ||
14 | |||
15 | class StatusDisplay implements vscode.Disposable { | ||
16 | packageName?: string; | ||
17 | |||
18 | private i = 0; | ||
19 | private statusBarItem: vscode.StatusBarItem; | ||
20 | private command: string; | ||
21 | private timer?: NodeJS.Timeout; | ||
22 | |||
23 | constructor(command: string) { | ||
24 | this.statusBarItem = vscode.window.createStatusBarItem( | ||
25 | vscode.StatusBarAlignment.Left, | ||
26 | 10, | ||
27 | ); | ||
28 | this.command = command; | ||
29 | this.statusBarItem.hide(); | ||
30 | } | ||
31 | |||
32 | show() { | ||
33 | this.packageName = undefined; | ||
34 | |||
35 | this.timer = | ||
36 | this.timer || | ||
37 | setInterval(() => { | ||
38 | if (this.packageName) { | ||
39 | this.statusBarItem!.text = `cargo ${this.command} [${ | ||
40 | this.packageName | ||
41 | }] ${this.frame()}`; | ||
42 | } else { | ||
43 | this.statusBarItem!.text = `cargo ${ | ||
44 | this.command | ||
45 | } ${this.frame()}`; | ||
46 | } | ||
47 | }, 300); | ||
48 | |||
49 | this.statusBarItem.show(); | ||
50 | } | ||
51 | |||
52 | hide() { | ||
53 | if (this.timer) { | ||
54 | clearInterval(this.timer); | ||
55 | this.timer = undefined; | ||
56 | } | ||
57 | |||
58 | this.statusBarItem.hide(); | ||
59 | } | ||
60 | |||
61 | dispose() { | ||
62 | if (this.timer) { | ||
63 | clearInterval(this.timer); | ||
64 | this.timer = undefined; | ||
65 | } | ||
66 | |||
67 | this.statusBarItem.dispose(); | ||
68 | } | ||
69 | |||
70 | handleProgressNotification(params: ProgressParams) { | ||
71 | const { token, value } = params; | ||
72 | if (token !== 'rustAnalyzer/cargoWatcher') { | ||
73 | return; | ||
74 | } | ||
75 | |||
76 | switch (value.kind) { | ||
77 | case 'begin': | ||
78 | this.show(); | ||
79 | break; | ||
80 | |||
81 | case 'report': | ||
82 | if (value.message) { | ||
83 | this.packageName = value.message; | ||
84 | } | ||
85 | break; | ||
86 | |||
87 | case 'end': | ||
88 | this.hide(); | ||
89 | break; | ||
90 | } | ||
91 | } | ||
92 | |||
93 | private frame() { | ||
94 | return spinnerFrames[(this.i = ++this.i % spinnerFrames.length)]; | ||
95 | } | ||
96 | } | ||
97 | |||
98 | // FIXME: Replace this once vscode-languageclient is updated to LSP 3.15 | ||
99 | interface ProgressParams { | ||
100 | token: string; | ||
101 | value: WorkDoneProgress; | ||
102 | } | ||
103 | |||
104 | enum WorkDoneProgressKind { | ||
105 | Begin = 'begin', | ||
106 | Report = 'report', | ||
107 | End = 'end', | ||
108 | } | ||
109 | |||
110 | interface WorkDoneProgress { | ||
111 | kind: WorkDoneProgressKind; | ||
112 | message?: string; | ||
113 | cancelable?: boolean; | ||
114 | percentage?: string; | ||
115 | } | ||
diff --git a/editors/code/src/test/fixtures/rust-diagnostics/clippy/trivially_copy_pass_by_ref.json b/editors/code/src/test/fixtures/rust-diagnostics/clippy/trivially_copy_pass_by_ref.json deleted file mode 100644 index d874e99bc..000000000 --- a/editors/code/src/test/fixtures/rust-diagnostics/clippy/trivially_copy_pass_by_ref.json +++ /dev/null | |||
@@ -1,110 +0,0 @@ | |||
1 | { | ||
2 | "message": "this argument is passed by reference, but would be more efficient if passed by value", | ||
3 | "code": { | ||
4 | "code": "clippy::trivially_copy_pass_by_ref", | ||
5 | "explanation": null | ||
6 | }, | ||
7 | "level": "warning", | ||
8 | "spans": [ | ||
9 | { | ||
10 | "file_name": "compiler/mir/tagset.rs", | ||
11 | "byte_start": 941, | ||
12 | "byte_end": 946, | ||
13 | "line_start": 42, | ||
14 | "line_end": 42, | ||
15 | "column_start": 24, | ||
16 | "column_end": 29, | ||
17 | "is_primary": true, | ||
18 | "text": [ | ||
19 | { | ||
20 | "text": " pub fn is_disjoint(&self, other: Self) -> bool {", | ||
21 | "highlight_start": 24, | ||
22 | "highlight_end": 29 | ||
23 | } | ||
24 | ], | ||
25 | "label": null, | ||
26 | "suggested_replacement": null, | ||
27 | "suggestion_applicability": null, | ||
28 | "expansion": null | ||
29 | } | ||
30 | ], | ||
31 | "children": [ | ||
32 | { | ||
33 | "message": "lint level defined here", | ||
34 | "code": null, | ||
35 | "level": "note", | ||
36 | "spans": [ | ||
37 | { | ||
38 | "file_name": "compiler/lib.rs", | ||
39 | "byte_start": 8, | ||
40 | "byte_end": 19, | ||
41 | "line_start": 1, | ||
42 | "line_end": 1, | ||
43 | "column_start": 9, | ||
44 | "column_end": 20, | ||
45 | "is_primary": true, | ||
46 | "text": [ | ||
47 | { | ||
48 | "text": "#![warn(clippy::all)]", | ||
49 | "highlight_start": 9, | ||
50 | "highlight_end": 20 | ||
51 | } | ||
52 | ], | ||
53 | "label": null, | ||
54 | "suggested_replacement": null, | ||
55 | "suggestion_applicability": null, | ||
56 | "expansion": null | ||
57 | } | ||
58 | ], | ||
59 | "children": [], | ||
60 | "rendered": null | ||
61 | }, | ||
62 | { | ||
63 | "message": "#[warn(clippy::trivially_copy_pass_by_ref)] implied by #[warn(clippy::all)]", | ||
64 | "code": null, | ||
65 | "level": "note", | ||
66 | "spans": [], | ||
67 | "children": [], | ||
68 | "rendered": null | ||
69 | }, | ||
70 | { | ||
71 | "message": "for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#trivially_copy_pass_by_ref", | ||
72 | "code": null, | ||
73 | "level": "help", | ||
74 | "spans": [], | ||
75 | "children": [], | ||
76 | "rendered": null | ||
77 | }, | ||
78 | { | ||
79 | "message": "consider passing by value instead", | ||
80 | "code": null, | ||
81 | "level": "help", | ||
82 | "spans": [ | ||
83 | { | ||
84 | "file_name": "compiler/mir/tagset.rs", | ||
85 | "byte_start": 941, | ||
86 | "byte_end": 946, | ||
87 | "line_start": 42, | ||
88 | "line_end": 42, | ||
89 | "column_start": 24, | ||
90 | "column_end": 29, | ||
91 | "is_primary": true, | ||
92 | "text": [ | ||
93 | { | ||
94 | "text": " pub fn is_disjoint(&self, other: Self) -> bool {", | ||
95 | "highlight_start": 24, | ||
96 | "highlight_end": 29 | ||
97 | } | ||
98 | ], | ||
99 | "label": null, | ||
100 | "suggested_replacement": "self", | ||
101 | "suggestion_applicability": "Unspecified", | ||
102 | "expansion": null | ||
103 | } | ||
104 | ], | ||
105 | "children": [], | ||
106 | "rendered": null | ||
107 | } | ||
108 | ], | ||
109 | "rendered": "warning: this argument is passed by reference, but would be more efficient if passed by value\n --> compiler/mir/tagset.rs:42:24\n |\n42 | pub fn is_disjoint(&self, other: Self) -> bool {\n | ^^^^^ help: consider passing by value instead: `self`\n |\nnote: lint level defined here\n --> compiler/lib.rs:1:9\n |\n1 | #![warn(clippy::all)]\n | ^^^^^^^^^^^\n = note: #[warn(clippy::trivially_copy_pass_by_ref)] implied by #[warn(clippy::all)]\n = help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#trivially_copy_pass_by_ref\n\n" | ||
110 | } | ||
diff --git a/editors/code/src/test/fixtures/rust-diagnostics/error/E0053.json b/editors/code/src/test/fixtures/rust-diagnostics/error/E0053.json deleted file mode 100644 index ea5c976d1..000000000 --- a/editors/code/src/test/fixtures/rust-diagnostics/error/E0053.json +++ /dev/null | |||
@@ -1,42 +0,0 @@ | |||
1 | { | ||
2 | "message": "method `next` has an incompatible type for trait", | ||
3 | "code": { | ||
4 | "code": "E0053", | ||
5 | "explanation": "\nThe parameters of any trait method must match between a trait implementation\nand the trait definition.\n\nHere are a couple examples of this error:\n\n```compile_fail,E0053\ntrait Foo {\n fn foo(x: u16);\n fn bar(&self);\n}\n\nstruct Bar;\n\nimpl Foo for Bar {\n // error, expected u16, found i16\n fn foo(x: i16) { }\n\n // error, types differ in mutability\n fn bar(&mut self) { }\n}\n```\n" | ||
6 | }, | ||
7 | "level": "error", | ||
8 | "spans": [ | ||
9 | { | ||
10 | "file_name": "compiler/ty/list_iter.rs", | ||
11 | "byte_start": 1307, | ||
12 | "byte_end": 1350, | ||
13 | "line_start": 52, | ||
14 | "line_end": 52, | ||
15 | "column_start": 5, | ||
16 | "column_end": 48, | ||
17 | "is_primary": true, | ||
18 | "text": [ | ||
19 | { | ||
20 | "text": " fn next(&self) -> Option<&'list ty::Ref<M>> {", | ||
21 | "highlight_start": 5, | ||
22 | "highlight_end": 48 | ||
23 | } | ||
24 | ], | ||
25 | "label": "types differ in mutability", | ||
26 | "suggested_replacement": null, | ||
27 | "suggestion_applicability": null, | ||
28 | "expansion": null | ||
29 | } | ||
30 | ], | ||
31 | "children": [ | ||
32 | { | ||
33 | "message": "expected type `fn(&mut ty::list_iter::ListIterator<'list, M>) -> std::option::Option<&ty::Ref<M>>`\n found type `fn(&ty::list_iter::ListIterator<'list, M>) -> std::option::Option<&'list ty::Ref<M>>`", | ||
34 | "code": null, | ||
35 | "level": "note", | ||
36 | "spans": [], | ||
37 | "children": [], | ||
38 | "rendered": null | ||
39 | } | ||
40 | ], | ||
41 | "rendered": "error[E0053]: method `next` has an incompatible type for trait\n --> compiler/ty/list_iter.rs:52:5\n |\n52 | fn next(&self) -> Option<&'list ty::Ref<M>> {\n | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ types differ in mutability\n |\n = note: expected type `fn(&mut ty::list_iter::ListIterator<'list, M>) -> std::option::Option<&ty::Ref<M>>`\n found type `fn(&ty::list_iter::ListIterator<'list, M>) -> std::option::Option<&'list ty::Ref<M>>`\n\n" | ||
42 | } | ||
diff --git a/editors/code/src/test/fixtures/rust-diagnostics/error/E0061.json b/editors/code/src/test/fixtures/rust-diagnostics/error/E0061.json deleted file mode 100644 index 3154d1098..000000000 --- a/editors/code/src/test/fixtures/rust-diagnostics/error/E0061.json +++ /dev/null | |||
@@ -1,114 +0,0 @@ | |||
1 | { | ||
2 | "message": "this function takes 2 parameters but 3 parameters were supplied", | ||
3 | "code": { | ||
4 | "code": "E0061", | ||
5 | "explanation": "\nThe number of arguments passed to a function must match the number of arguments\nspecified in the function signature.\n\nFor example, a function like:\n\n```\nfn f(a: u16, b: &str) {}\n```\n\nMust always be called with exactly two arguments, e.g., `f(2, \"test\")`.\n\nNote that Rust does not have a notion of optional function arguments or\nvariadic functions (except for its C-FFI).\n" | ||
6 | }, | ||
7 | "level": "error", | ||
8 | "spans": [ | ||
9 | { | ||
10 | "file_name": "compiler/ty/select.rs", | ||
11 | "byte_start": 8787, | ||
12 | "byte_end": 9241, | ||
13 | "line_start": 219, | ||
14 | "line_end": 231, | ||
15 | "column_start": 5, | ||
16 | "column_end": 6, | ||
17 | "is_primary": false, | ||
18 | "text": [ | ||
19 | { | ||
20 | "text": " pub fn add_evidence(", | ||
21 | "highlight_start": 5, | ||
22 | "highlight_end": 25 | ||
23 | }, | ||
24 | { | ||
25 | "text": " &mut self,", | ||
26 | "highlight_start": 1, | ||
27 | "highlight_end": 19 | ||
28 | }, | ||
29 | { | ||
30 | "text": " target_poly: &ty::Ref<ty::Poly>,", | ||
31 | "highlight_start": 1, | ||
32 | "highlight_end": 41 | ||
33 | }, | ||
34 | { | ||
35 | "text": " evidence_poly: &ty::Ref<ty::Poly>,", | ||
36 | "highlight_start": 1, | ||
37 | "highlight_end": 43 | ||
38 | }, | ||
39 | { | ||
40 | "text": " ) {", | ||
41 | "highlight_start": 1, | ||
42 | "highlight_end": 8 | ||
43 | }, | ||
44 | { | ||
45 | "text": " match target_poly {", | ||
46 | "highlight_start": 1, | ||
47 | "highlight_end": 28 | ||
48 | }, | ||
49 | { | ||
50 | "text": " ty::Ref::Var(tvar, _) => self.add_var_evidence(tvar, evidence_poly),", | ||
51 | "highlight_start": 1, | ||
52 | "highlight_end": 81 | ||
53 | }, | ||
54 | { | ||
55 | "text": " ty::Ref::Fixed(target_ty) => {", | ||
56 | "highlight_start": 1, | ||
57 | "highlight_end": 43 | ||
58 | }, | ||
59 | { | ||
60 | "text": " let evidence_ty = evidence_poly.resolve_to_ty();", | ||
61 | "highlight_start": 1, | ||
62 | "highlight_end": 65 | ||
63 | }, | ||
64 | { | ||
65 | "text": " self.add_evidence_ty(target_ty, evidence_poly, evidence_ty)", | ||
66 | "highlight_start": 1, | ||
67 | "highlight_end": 76 | ||
68 | }, | ||
69 | { | ||
70 | "text": " }", | ||
71 | "highlight_start": 1, | ||
72 | "highlight_end": 14 | ||
73 | }, | ||
74 | { | ||
75 | "text": " }", | ||
76 | "highlight_start": 1, | ||
77 | "highlight_end": 10 | ||
78 | }, | ||
79 | { | ||
80 | "text": " }", | ||
81 | "highlight_start": 1, | ||
82 | "highlight_end": 6 | ||
83 | } | ||
84 | ], | ||
85 | "label": "defined here", | ||
86 | "suggested_replacement": null, | ||
87 | "suggestion_applicability": null, | ||
88 | "expansion": null | ||
89 | }, | ||
90 | { | ||
91 | "file_name": "compiler/ty/select.rs", | ||
92 | "byte_start": 4045, | ||
93 | "byte_end": 4057, | ||
94 | "line_start": 104, | ||
95 | "line_end": 104, | ||
96 | "column_start": 18, | ||
97 | "column_end": 30, | ||
98 | "is_primary": true, | ||
99 | "text": [ | ||
100 | { | ||
101 | "text": " self.add_evidence(target_fixed, evidence_fixed, false);", | ||
102 | "highlight_start": 18, | ||
103 | "highlight_end": 30 | ||
104 | } | ||
105 | ], | ||
106 | "label": "expected 2 parameters", | ||
107 | "suggested_replacement": null, | ||
108 | "suggestion_applicability": null, | ||
109 | "expansion": null | ||
110 | } | ||
111 | ], | ||
112 | "children": [], | ||
113 | "rendered": "error[E0061]: this function takes 2 parameters but 3 parameters were supplied\n --> compiler/ty/select.rs:104:18\n |\n104 | self.add_evidence(target_fixed, evidence_fixed, false);\n | ^^^^^^^^^^^^ expected 2 parameters\n...\n219 | / pub fn add_evidence(\n220 | | &mut self,\n221 | | target_poly: &ty::Ref<ty::Poly>,\n222 | | evidence_poly: &ty::Ref<ty::Poly>,\n... |\n230 | | }\n231 | | }\n | |_____- defined here\n\n" | ||
114 | } | ||
diff --git a/editors/code/src/test/fixtures/rust-diagnostics/error/E0277.json b/editors/code/src/test/fixtures/rust-diagnostics/error/E0277.json deleted file mode 100644 index bfef33c7d..000000000 --- a/editors/code/src/test/fixtures/rust-diagnostics/error/E0277.json +++ /dev/null | |||
@@ -1,261 +0,0 @@ | |||
1 | { | ||
2 | "rendered": "error[E0277]: can't compare `{integer}` with `&str`\n --> src/main.rs:2:5\n |\n2 | assert_eq!(1, \"love\");\n | ^^^^^^^^^^^^^^^^^^^^^^ no implementation for `{integer} == &str`\n |\n = help: the trait `std::cmp::PartialEq<&str>` is not implemented for `{integer}`\n = note: this error originates in a macro outside of the current crate (in Nightly builds, run with -Z external-macro-backtrace for more info)\n\n", | ||
3 | "children": [ | ||
4 | { | ||
5 | "children": [], | ||
6 | "code": null, | ||
7 | "level": "help", | ||
8 | "message": "the trait `std::cmp::PartialEq<&str>` is not implemented for `{integer}`", | ||
9 | "rendered": null, | ||
10 | "spans": [] | ||
11 | } | ||
12 | ], | ||
13 | "code": { | ||
14 | "code": "E0277", | ||
15 | "explanation": "\nYou tried to use a type which doesn't implement some trait in a place which\nexpected that trait. Erroneous code example:\n\n```compile_fail,E0277\n// here we declare the Foo trait with a bar method\ntrait Foo {\n fn bar(&self);\n}\n\n// we now declare a function which takes an object implementing the Foo trait\nfn some_func<T: Foo>(foo: T) {\n foo.bar();\n}\n\nfn main() {\n // we now call the method with the i32 type, which doesn't implement\n // the Foo trait\n some_func(5i32); // error: the trait bound `i32 : Foo` is not satisfied\n}\n```\n\nIn order to fix this error, verify that the type you're using does implement\nthe trait. Example:\n\n```\ntrait Foo {\n fn bar(&self);\n}\n\nfn some_func<T: Foo>(foo: T) {\n foo.bar(); // we can now use this method since i32 implements the\n // Foo trait\n}\n\n// we implement the trait on the i32 type\nimpl Foo for i32 {\n fn bar(&self) {}\n}\n\nfn main() {\n some_func(5i32); // ok!\n}\n```\n\nOr in a generic context, an erroneous code example would look like:\n\n```compile_fail,E0277\nfn some_func<T>(foo: T) {\n println!(\"{:?}\", foo); // error: the trait `core::fmt::Debug` is not\n // implemented for the type `T`\n}\n\nfn main() {\n // We now call the method with the i32 type,\n // which *does* implement the Debug trait.\n some_func(5i32);\n}\n```\n\nNote that the error here is in the definition of the generic function: Although\nwe only call it with a parameter that does implement `Debug`, the compiler\nstill rejects the function: It must work with all possible input types. In\norder to make this example compile, we need to restrict the generic type we're\naccepting:\n\n```\nuse std::fmt;\n\n// Restrict the input type to types that implement Debug.\nfn some_func<T: fmt::Debug>(foo: T) {\n println!(\"{:?}\", foo);\n}\n\nfn main() {\n // Calling the method is still fine, as i32 implements Debug.\n some_func(5i32);\n\n // This would fail to compile now:\n // struct WithoutDebug;\n // some_func(WithoutDebug);\n}\n```\n\nRust only looks at the signature of the called function, as such it must\nalready specify all requirements that will be used for every type parameter.\n" | ||
16 | }, | ||
17 | "level": "error", | ||
18 | "message": "can't compare `{integer}` with `&str`", | ||
19 | "spans": [ | ||
20 | { | ||
21 | "byte_end": 155, | ||
22 | "byte_start": 153, | ||
23 | "column_end": 33, | ||
24 | "column_start": 31, | ||
25 | "expansion": { | ||
26 | "def_site_span": { | ||
27 | "byte_end": 940, | ||
28 | "byte_start": 0, | ||
29 | "column_end": 6, | ||
30 | "column_start": 1, | ||
31 | "expansion": null, | ||
32 | "file_name": "<::core::macros::assert_eq macros>", | ||
33 | "is_primary": false, | ||
34 | "label": null, | ||
35 | "line_end": 36, | ||
36 | "line_start": 1, | ||
37 | "suggested_replacement": null, | ||
38 | "suggestion_applicability": null, | ||
39 | "text": [ | ||
40 | { | ||
41 | "highlight_end": 35, | ||
42 | "highlight_start": 1, | ||
43 | "text": "($ left : expr, $ right : expr) =>" | ||
44 | }, | ||
45 | { | ||
46 | "highlight_end": 3, | ||
47 | "highlight_start": 1, | ||
48 | "text": "({" | ||
49 | }, | ||
50 | { | ||
51 | "highlight_end": 33, | ||
52 | "highlight_start": 1, | ||
53 | "text": " match (& $ left, & $ right)" | ||
54 | }, | ||
55 | { | ||
56 | "highlight_end": 7, | ||
57 | "highlight_start": 1, | ||
58 | "text": " {" | ||
59 | }, | ||
60 | { | ||
61 | "highlight_end": 34, | ||
62 | "highlight_start": 1, | ||
63 | "text": " (left_val, right_val) =>" | ||
64 | }, | ||
65 | { | ||
66 | "highlight_end": 11, | ||
67 | "highlight_start": 1, | ||
68 | "text": " {" | ||
69 | }, | ||
70 | { | ||
71 | "highlight_end": 46, | ||
72 | "highlight_start": 1, | ||
73 | "text": " if ! (* left_val == * right_val)" | ||
74 | }, | ||
75 | { | ||
76 | "highlight_end": 15, | ||
77 | "highlight_start": 1, | ||
78 | "text": " {" | ||
79 | }, | ||
80 | { | ||
81 | "highlight_end": 25, | ||
82 | "highlight_start": 1, | ||
83 | "text": " panic !" | ||
84 | }, | ||
85 | { | ||
86 | "highlight_end": 57, | ||
87 | "highlight_start": 1, | ||
88 | "text": " (r#\"assertion failed: `(left == right)`" | ||
89 | }, | ||
90 | { | ||
91 | "highlight_end": 16, | ||
92 | "highlight_start": 1, | ||
93 | "text": " left: `{:?}`," | ||
94 | }, | ||
95 | { | ||
96 | "highlight_end": 18, | ||
97 | "highlight_start": 1, | ||
98 | "text": " right: `{:?}`\"#," | ||
99 | }, | ||
100 | { | ||
101 | "highlight_end": 47, | ||
102 | "highlight_start": 1, | ||
103 | "text": " & * left_val, & * right_val)" | ||
104 | }, | ||
105 | { | ||
106 | "highlight_end": 15, | ||
107 | "highlight_start": 1, | ||
108 | "text": " }" | ||
109 | }, | ||
110 | { | ||
111 | "highlight_end": 11, | ||
112 | "highlight_start": 1, | ||
113 | "text": " }" | ||
114 | }, | ||
115 | { | ||
116 | "highlight_end": 7, | ||
117 | "highlight_start": 1, | ||
118 | "text": " }" | ||
119 | }, | ||
120 | { | ||
121 | "highlight_end": 42, | ||
122 | "highlight_start": 1, | ||
123 | "text": " }) ; ($ left : expr, $ right : expr,) =>" | ||
124 | }, | ||
125 | { | ||
126 | "highlight_end": 49, | ||
127 | "highlight_start": 1, | ||
128 | "text": "({ $ crate :: assert_eq ! ($ left, $ right) }) ;" | ||
129 | }, | ||
130 | { | ||
131 | "highlight_end": 53, | ||
132 | "highlight_start": 1, | ||
133 | "text": "($ left : expr, $ right : expr, $ ($ arg : tt) +) =>" | ||
134 | }, | ||
135 | { | ||
136 | "highlight_end": 3, | ||
137 | "highlight_start": 1, | ||
138 | "text": "({" | ||
139 | }, | ||
140 | { | ||
141 | "highlight_end": 37, | ||
142 | "highlight_start": 1, | ||
143 | "text": " match (& ($ left), & ($ right))" | ||
144 | }, | ||
145 | { | ||
146 | "highlight_end": 7, | ||
147 | "highlight_start": 1, | ||
148 | "text": " {" | ||
149 | }, | ||
150 | { | ||
151 | "highlight_end": 34, | ||
152 | "highlight_start": 1, | ||
153 | "text": " (left_val, right_val) =>" | ||
154 | }, | ||
155 | { | ||
156 | "highlight_end": 11, | ||
157 | "highlight_start": 1, | ||
158 | "text": " {" | ||
159 | }, | ||
160 | { | ||
161 | "highlight_end": 46, | ||
162 | "highlight_start": 1, | ||
163 | "text": " if ! (* left_val == * right_val)" | ||
164 | }, | ||
165 | { | ||
166 | "highlight_end": 15, | ||
167 | "highlight_start": 1, | ||
168 | "text": " {" | ||
169 | }, | ||
170 | { | ||
171 | "highlight_end": 25, | ||
172 | "highlight_start": 1, | ||
173 | "text": " panic !" | ||
174 | }, | ||
175 | { | ||
176 | "highlight_end": 57, | ||
177 | "highlight_start": 1, | ||
178 | "text": " (r#\"assertion failed: `(left == right)`" | ||
179 | }, | ||
180 | { | ||
181 | "highlight_end": 16, | ||
182 | "highlight_start": 1, | ||
183 | "text": " left: `{:?}`," | ||
184 | }, | ||
185 | { | ||
186 | "highlight_end": 22, | ||
187 | "highlight_start": 1, | ||
188 | "text": " right: `{:?}`: {}\"#," | ||
189 | }, | ||
190 | { | ||
191 | "highlight_end": 72, | ||
192 | "highlight_start": 1, | ||
193 | "text": " & * left_val, & * right_val, $ crate :: format_args !" | ||
194 | }, | ||
195 | { | ||
196 | "highlight_end": 33, | ||
197 | "highlight_start": 1, | ||
198 | "text": " ($ ($ arg) +))" | ||
199 | }, | ||
200 | { | ||
201 | "highlight_end": 15, | ||
202 | "highlight_start": 1, | ||
203 | "text": " }" | ||
204 | }, | ||
205 | { | ||
206 | "highlight_end": 11, | ||
207 | "highlight_start": 1, | ||
208 | "text": " }" | ||
209 | }, | ||
210 | { | ||
211 | "highlight_end": 7, | ||
212 | "highlight_start": 1, | ||
213 | "text": " }" | ||
214 | }, | ||
215 | { | ||
216 | "highlight_end": 6, | ||
217 | "highlight_start": 1, | ||
218 | "text": " }) ;" | ||
219 | } | ||
220 | ] | ||
221 | }, | ||
222 | "macro_decl_name": "assert_eq!", | ||
223 | "span": { | ||
224 | "byte_end": 38, | ||
225 | "byte_start": 16, | ||
226 | "column_end": 27, | ||
227 | "column_start": 5, | ||
228 | "expansion": null, | ||
229 | "file_name": "src/main.rs", | ||
230 | "is_primary": false, | ||
231 | "label": null, | ||
232 | "line_end": 2, | ||
233 | "line_start": 2, | ||
234 | "suggested_replacement": null, | ||
235 | "suggestion_applicability": null, | ||
236 | "text": [ | ||
237 | { | ||
238 | "highlight_end": 27, | ||
239 | "highlight_start": 5, | ||
240 | "text": " assert_eq!(1, \"love\");" | ||
241 | } | ||
242 | ] | ||
243 | } | ||
244 | }, | ||
245 | "file_name": "<::core::macros::assert_eq macros>", | ||
246 | "is_primary": true, | ||
247 | "label": "no implementation for `{integer} == &str`", | ||
248 | "line_end": 7, | ||
249 | "line_start": 7, | ||
250 | "suggested_replacement": null, | ||
251 | "suggestion_applicability": null, | ||
252 | "text": [ | ||
253 | { | ||
254 | "highlight_end": 33, | ||
255 | "highlight_start": 31, | ||
256 | "text": " if ! (* left_val == * right_val)" | ||
257 | } | ||
258 | ] | ||
259 | } | ||
260 | ] | ||
261 | } | ||
diff --git a/editors/code/src/test/fixtures/rust-diagnostics/error/E0308.json b/editors/code/src/test/fixtures/rust-diagnostics/error/E0308.json deleted file mode 100644 index fb23824a3..000000000 --- a/editors/code/src/test/fixtures/rust-diagnostics/error/E0308.json +++ /dev/null | |||
@@ -1,33 +0,0 @@ | |||
1 | { | ||
2 | "message": "mismatched types", | ||
3 | "code": { | ||
4 | "code": "E0308", | ||
5 | "explanation": "\nThis error occurs when the compiler was unable to infer the concrete type of a\nvariable. It can occur for several cases, the most common of which is a\nmismatch in the expected type that the compiler inferred for a variable's\ninitializing expression, and the actual type explicitly assigned to the\nvariable.\n\nFor example:\n\n```compile_fail,E0308\nlet x: i32 = \"I am not a number!\";\n// ~~~ ~~~~~~~~~~~~~~~~~~~~\n// | |\n// | initializing expression;\n// | compiler infers type `&str`\n// |\n// type `i32` assigned to variable `x`\n```\n" | ||
6 | }, | ||
7 | "level": "error", | ||
8 | "spans": [ | ||
9 | { | ||
10 | "file_name": "runtime/compiler_support.rs", | ||
11 | "byte_start": 1589, | ||
12 | "byte_end": 1594, | ||
13 | "line_start": 48, | ||
14 | "line_end": 48, | ||
15 | "column_start": 65, | ||
16 | "column_end": 70, | ||
17 | "is_primary": true, | ||
18 | "text": [ | ||
19 | { | ||
20 | "text": " let layout = alloc::Layout::from_size_align_unchecked(size, align);", | ||
21 | "highlight_start": 65, | ||
22 | "highlight_end": 70 | ||
23 | } | ||
24 | ], | ||
25 | "label": "expected usize, found u32", | ||
26 | "suggested_replacement": null, | ||
27 | "suggestion_applicability": null, | ||
28 | "expansion": null | ||
29 | } | ||
30 | ], | ||
31 | "children": [], | ||
32 | "rendered": "error[E0308]: mismatched types\n --> runtime/compiler_support.rs:48:65\n |\n48 | let layout = alloc::Layout::from_size_align_unchecked(size, align);\n | ^^^^^ expected usize, found u32\n\n" | ||
33 | } | ||
diff --git a/editors/code/src/test/fixtures/rust-diagnostics/warning/unused_variables.json b/editors/code/src/test/fixtures/rust-diagnostics/warning/unused_variables.json deleted file mode 100644 index d1e2be722..000000000 --- a/editors/code/src/test/fixtures/rust-diagnostics/warning/unused_variables.json +++ /dev/null | |||
@@ -1,72 +0,0 @@ | |||
1 | { | ||
2 | "message": "unused variable: `foo`", | ||
3 | "code": { | ||
4 | "code": "unused_variables", | ||
5 | "explanation": null | ||
6 | }, | ||
7 | "level": "warning", | ||
8 | "spans": [ | ||
9 | { | ||
10 | "file_name": "driver/subcommand/repl.rs", | ||
11 | "byte_start": 9228, | ||
12 | "byte_end": 9231, | ||
13 | "line_start": 291, | ||
14 | "line_end": 291, | ||
15 | "column_start": 9, | ||
16 | "column_end": 12, | ||
17 | "is_primary": true, | ||
18 | "text": [ | ||
19 | { | ||
20 | "text": " let foo = 42;", | ||
21 | "highlight_start": 9, | ||
22 | "highlight_end": 12 | ||
23 | } | ||
24 | ], | ||
25 | "label": null, | ||
26 | "suggested_replacement": null, | ||
27 | "suggestion_applicability": null, | ||
28 | "expansion": null | ||
29 | } | ||
30 | ], | ||
31 | "children": [ | ||
32 | { | ||
33 | "message": "#[warn(unused_variables)] on by default", | ||
34 | "code": null, | ||
35 | "level": "note", | ||
36 | "spans": [], | ||
37 | "children": [], | ||
38 | "rendered": null | ||
39 | }, | ||
40 | { | ||
41 | "message": "consider prefixing with an underscore", | ||
42 | "code": null, | ||
43 | "level": "help", | ||
44 | "spans": [ | ||
45 | { | ||
46 | "file_name": "driver/subcommand/repl.rs", | ||
47 | "byte_start": 9228, | ||
48 | "byte_end": 9231, | ||
49 | "line_start": 291, | ||
50 | "line_end": 291, | ||
51 | "column_start": 9, | ||
52 | "column_end": 12, | ||
53 | "is_primary": true, | ||
54 | "text": [ | ||
55 | { | ||
56 | "text": " let foo = 42;", | ||
57 | "highlight_start": 9, | ||
58 | "highlight_end": 12 | ||
59 | } | ||
60 | ], | ||
61 | "label": null, | ||
62 | "suggested_replacement": "_foo", | ||
63 | "suggestion_applicability": "MachineApplicable", | ||
64 | "expansion": null | ||
65 | } | ||
66 | ], | ||
67 | "children": [], | ||
68 | "rendered": null | ||
69 | } | ||
70 | ], | ||
71 | "rendered": "warning: unused variable: `foo`\n --> driver/subcommand/repl.rs:291:9\n |\n291 | let foo = 42;\n | ^^^ help: consider prefixing with an underscore: `_foo`\n |\n = note: #[warn(unused_variables)] on by default\n\n" | ||
72 | } | ||
diff --git a/editors/code/src/test/runTest.ts b/editors/code/src/test/runTest.ts deleted file mode 100644 index d880d47df..000000000 --- a/editors/code/src/test/runTest.ts +++ /dev/null | |||
@@ -1,22 +0,0 @@ | |||
1 | import * as path from 'path'; | ||
2 | |||
3 | import { runTests } from 'vscode-test'; | ||
4 | |||
5 | async function main() { | ||
6 | try { | ||
7 | // The folder containing the Extension Manifest package.json | ||
8 | // Passed to `--extensionDevelopmentPath` | ||
9 | const extensionDevelopmentPath = path.resolve(__dirname, '../../'); | ||
10 | |||
11 | // The path to the extension test runner script | ||
12 | // Passed to --extensionTestsPath | ||
13 | const extensionTestsPath = path.resolve(__dirname, './utils/index'); | ||
14 | |||
15 | // Download VS Code, unzip it and run the integration test | ||
16 | await runTests({ extensionDevelopmentPath, extensionTestsPath }); | ||
17 | } catch (err) { | ||
18 | process.exit(1); | ||
19 | } | ||
20 | } | ||
21 | |||
22 | main(); | ||
diff --git a/editors/code/src/test/utils/diagnotics/SuggestedFix.test.ts b/editors/code/src/test/utils/diagnotics/SuggestedFix.test.ts deleted file mode 100644 index 2b25eb705..000000000 --- a/editors/code/src/test/utils/diagnotics/SuggestedFix.test.ts +++ /dev/null | |||
@@ -1,134 +0,0 @@ | |||
1 | import * as assert from 'assert'; | ||
2 | import * as vscode from 'vscode'; | ||
3 | |||
4 | import { SuggestionApplicability } from '../../../utils/diagnostics/rust'; | ||
5 | import SuggestedFix from '../../../utils/diagnostics/SuggestedFix'; | ||
6 | |||
7 | const location1 = new vscode.Location( | ||
8 | vscode.Uri.file('/file/1'), | ||
9 | new vscode.Range(new vscode.Position(1, 2), new vscode.Position(3, 4)), | ||
10 | ); | ||
11 | |||
12 | const location2 = new vscode.Location( | ||
13 | vscode.Uri.file('/file/2'), | ||
14 | new vscode.Range(new vscode.Position(5, 6), new vscode.Position(7, 8)), | ||
15 | ); | ||
16 | |||
17 | describe('SuggestedFix', () => { | ||
18 | describe('isEqual', () => { | ||
19 | it('should treat identical instances as equal', () => { | ||
20 | const suggestion1 = new SuggestedFix( | ||
21 | 'Replace me!', | ||
22 | location1, | ||
23 | 'With this!', | ||
24 | ); | ||
25 | |||
26 | const suggestion2 = new SuggestedFix( | ||
27 | 'Replace me!', | ||
28 | location1, | ||
29 | 'With this!', | ||
30 | ); | ||
31 | |||
32 | assert(suggestion1.isEqual(suggestion2)); | ||
33 | }); | ||
34 | |||
35 | it('should treat instances with different titles as inequal', () => { | ||
36 | const suggestion1 = new SuggestedFix( | ||
37 | 'Replace me!', | ||
38 | location1, | ||
39 | 'With this!', | ||
40 | ); | ||
41 | |||
42 | const suggestion2 = new SuggestedFix( | ||
43 | 'Not the same title!', | ||
44 | location1, | ||
45 | 'With this!', | ||
46 | ); | ||
47 | |||
48 | assert(!suggestion1.isEqual(suggestion2)); | ||
49 | }); | ||
50 | |||
51 | it('should treat instances with different replacements as inequal', () => { | ||
52 | const suggestion1 = new SuggestedFix( | ||
53 | 'Replace me!', | ||
54 | location1, | ||
55 | 'With this!', | ||
56 | ); | ||
57 | |||
58 | const suggestion2 = new SuggestedFix( | ||
59 | 'Replace me!', | ||
60 | location1, | ||
61 | 'With something else!', | ||
62 | ); | ||
63 | |||
64 | assert(!suggestion1.isEqual(suggestion2)); | ||
65 | }); | ||
66 | |||
67 | it('should treat instances with different locations as inequal', () => { | ||
68 | const suggestion1 = new SuggestedFix( | ||
69 | 'Replace me!', | ||
70 | location1, | ||
71 | 'With this!', | ||
72 | ); | ||
73 | |||
74 | const suggestion2 = new SuggestedFix( | ||
75 | 'Replace me!', | ||
76 | location2, | ||
77 | 'With this!', | ||
78 | ); | ||
79 | |||
80 | assert(!suggestion1.isEqual(suggestion2)); | ||
81 | }); | ||
82 | |||
83 | it('should treat instances with different applicability as inequal', () => { | ||
84 | const suggestion1 = new SuggestedFix( | ||
85 | 'Replace me!', | ||
86 | location1, | ||
87 | 'With this!', | ||
88 | SuggestionApplicability.MachineApplicable, | ||
89 | ); | ||
90 | |||
91 | const suggestion2 = new SuggestedFix( | ||
92 | 'Replace me!', | ||
93 | location2, | ||
94 | 'With this!', | ||
95 | SuggestionApplicability.HasPlaceholders, | ||
96 | ); | ||
97 | |||
98 | assert(!suggestion1.isEqual(suggestion2)); | ||
99 | }); | ||
100 | }); | ||
101 | |||
102 | describe('toCodeAction', () => { | ||
103 | it('should map a simple suggestion', () => { | ||
104 | const suggestion = new SuggestedFix( | ||
105 | 'Replace me!', | ||
106 | location1, | ||
107 | 'With this!', | ||
108 | ); | ||
109 | |||
110 | const codeAction = suggestion.toCodeAction(); | ||
111 | assert.strictEqual(codeAction.kind, vscode.CodeActionKind.QuickFix); | ||
112 | assert.strictEqual(codeAction.title, 'Replace me!'); | ||
113 | assert.strictEqual(codeAction.isPreferred, false); | ||
114 | |||
115 | const edit = codeAction.edit; | ||
116 | if (!edit) { | ||
117 | assert.fail('Code Action edit unexpectedly missing'); | ||
118 | return; | ||
119 | } | ||
120 | |||
121 | const editEntries = edit.entries(); | ||
122 | assert.strictEqual(editEntries.length, 1); | ||
123 | |||
124 | const [[editUri, textEdits]] = editEntries; | ||
125 | assert.strictEqual(editUri.toString(), location1.uri.toString()); | ||
126 | |||
127 | assert.strictEqual(textEdits.length, 1); | ||
128 | const [textEdit] = textEdits; | ||
129 | |||
130 | assert(textEdit.range.isEqual(location1.range)); | ||
131 | assert.strictEqual(textEdit.newText, 'With this!'); | ||
132 | }); | ||
133 | }); | ||
134 | }); | ||
diff --git a/editors/code/src/test/utils/diagnotics/SuggestedFixCollection.test.ts b/editors/code/src/test/utils/diagnotics/SuggestedFixCollection.test.ts deleted file mode 100644 index ef09013f4..000000000 --- a/editors/code/src/test/utils/diagnotics/SuggestedFixCollection.test.ts +++ /dev/null | |||
@@ -1,127 +0,0 @@ | |||
1 | import * as assert from 'assert'; | ||
2 | import * as vscode from 'vscode'; | ||
3 | |||
4 | import SuggestedFix from '../../../utils/diagnostics/SuggestedFix'; | ||
5 | import SuggestedFixCollection from '../../../utils/diagnostics/SuggestedFixCollection'; | ||
6 | |||
7 | const uri1 = vscode.Uri.file('/file/1'); | ||
8 | const uri2 = vscode.Uri.file('/file/2'); | ||
9 | |||
10 | const mockDocument1 = ({ | ||
11 | uri: uri1, | ||
12 | } as unknown) as vscode.TextDocument; | ||
13 | |||
14 | const mockDocument2 = ({ | ||
15 | uri: uri2, | ||
16 | } as unknown) as vscode.TextDocument; | ||
17 | |||
18 | const range1 = new vscode.Range( | ||
19 | new vscode.Position(1, 2), | ||
20 | new vscode.Position(3, 4), | ||
21 | ); | ||
22 | const range2 = new vscode.Range( | ||
23 | new vscode.Position(5, 6), | ||
24 | new vscode.Position(7, 8), | ||
25 | ); | ||
26 | |||
27 | const diagnostic1 = new vscode.Diagnostic(range1, 'First diagnostic'); | ||
28 | const diagnostic2 = new vscode.Diagnostic(range2, 'Second diagnostic'); | ||
29 | |||
30 | // This is a mutable object so return a fresh instance every time | ||
31 | function suggestion1(): SuggestedFix { | ||
32 | return new SuggestedFix( | ||
33 | 'Replace me!', | ||
34 | new vscode.Location(uri1, range1), | ||
35 | 'With this!', | ||
36 | ); | ||
37 | } | ||
38 | |||
39 | describe('SuggestedFixCollection', () => { | ||
40 | it('should add a suggestion then return it as a code action', () => { | ||
41 | const suggestedFixes = new SuggestedFixCollection(); | ||
42 | suggestedFixes.addSuggestedFixForDiagnostic(suggestion1(), diagnostic1); | ||
43 | |||
44 | // Specify the document and range that exactly matches | ||
45 | const codeActions = suggestedFixes.provideCodeActions( | ||
46 | mockDocument1, | ||
47 | range1, | ||
48 | ); | ||
49 | |||
50 | assert.strictEqual(codeActions.length, 1); | ||
51 | const [codeAction] = codeActions; | ||
52 | assert.strictEqual(codeAction.title, suggestion1().title); | ||
53 | |||
54 | const { diagnostics } = codeAction; | ||
55 | if (!diagnostics) { | ||
56 | assert.fail('Diagnostics unexpectedly missing'); | ||
57 | return; | ||
58 | } | ||
59 | |||
60 | assert.strictEqual(diagnostics.length, 1); | ||
61 | assert.strictEqual(diagnostics[0], diagnostic1); | ||
62 | }); | ||
63 | |||
64 | it('should not return code actions for different ranges', () => { | ||
65 | const suggestedFixes = new SuggestedFixCollection(); | ||
66 | suggestedFixes.addSuggestedFixForDiagnostic(suggestion1(), diagnostic1); | ||
67 | |||
68 | const codeActions = suggestedFixes.provideCodeActions( | ||
69 | mockDocument1, | ||
70 | range2, | ||
71 | ); | ||
72 | |||
73 | assert(!codeActions || codeActions.length === 0); | ||
74 | }); | ||
75 | |||
76 | it('should not return code actions for different documents', () => { | ||
77 | const suggestedFixes = new SuggestedFixCollection(); | ||
78 | suggestedFixes.addSuggestedFixForDiagnostic(suggestion1(), diagnostic1); | ||
79 | |||
80 | const codeActions = suggestedFixes.provideCodeActions( | ||
81 | mockDocument2, | ||
82 | range1, | ||
83 | ); | ||
84 | |||
85 | assert(!codeActions || codeActions.length === 0); | ||
86 | }); | ||
87 | |||
88 | it('should not return code actions that have been cleared', () => { | ||
89 | const suggestedFixes = new SuggestedFixCollection(); | ||
90 | suggestedFixes.addSuggestedFixForDiagnostic(suggestion1(), diagnostic1); | ||
91 | suggestedFixes.clear(); | ||
92 | |||
93 | const codeActions = suggestedFixes.provideCodeActions( | ||
94 | mockDocument1, | ||
95 | range1, | ||
96 | ); | ||
97 | |||
98 | assert(!codeActions || codeActions.length === 0); | ||
99 | }); | ||
100 | |||
101 | it('should merge identical suggestions together', () => { | ||
102 | const suggestedFixes = new SuggestedFixCollection(); | ||
103 | |||
104 | // Add the same suggestion for two diagnostics | ||
105 | suggestedFixes.addSuggestedFixForDiagnostic(suggestion1(), diagnostic1); | ||
106 | suggestedFixes.addSuggestedFixForDiagnostic(suggestion1(), diagnostic2); | ||
107 | |||
108 | const codeActions = suggestedFixes.provideCodeActions( | ||
109 | mockDocument1, | ||
110 | range1, | ||
111 | ); | ||
112 | |||
113 | assert.strictEqual(codeActions.length, 1); | ||
114 | const [codeAction] = codeActions; | ||
115 | const { diagnostics } = codeAction; | ||
116 | |||
117 | if (!diagnostics) { | ||
118 | assert.fail('Diagnostics unexpectedly missing'); | ||
119 | return; | ||
120 | } | ||
121 | |||
122 | // We should be associated with both diagnostics | ||
123 | assert.strictEqual(diagnostics.length, 2); | ||
124 | assert.strictEqual(diagnostics[0], diagnostic1); | ||
125 | assert.strictEqual(diagnostics[1], diagnostic2); | ||
126 | }); | ||
127 | }); | ||
diff --git a/editors/code/src/test/utils/diagnotics/rust.test.ts b/editors/code/src/test/utils/diagnotics/rust.test.ts deleted file mode 100644 index 358325cc8..000000000 --- a/editors/code/src/test/utils/diagnotics/rust.test.ts +++ /dev/null | |||
@@ -1,236 +0,0 @@ | |||
1 | import * as assert from 'assert'; | ||
2 | import * as fs from 'fs'; | ||
3 | import * as vscode from 'vscode'; | ||
4 | |||
5 | import { | ||
6 | MappedRustDiagnostic, | ||
7 | mapRustDiagnosticToVsCode, | ||
8 | RustDiagnostic, | ||
9 | SuggestionApplicability, | ||
10 | } from '../../../utils/diagnostics/rust'; | ||
11 | |||
12 | function loadDiagnosticFixture(name: string): RustDiagnostic { | ||
13 | const jsonText = fs | ||
14 | .readFileSync( | ||
15 | // We're actually in our JavaScript output directory, climb out | ||
16 | `${__dirname}/../../../../src/test/fixtures/rust-diagnostics/${name}.json`, | ||
17 | ) | ||
18 | .toString(); | ||
19 | |||
20 | return JSON.parse(jsonText); | ||
21 | } | ||
22 | |||
23 | function mapFixtureToVsCode(name: string): MappedRustDiagnostic { | ||
24 | const rd = loadDiagnosticFixture(name); | ||
25 | const mapResult = mapRustDiagnosticToVsCode(rd); | ||
26 | |||
27 | if (!mapResult) { | ||
28 | return assert.fail('Mapping unexpectedly failed'); | ||
29 | } | ||
30 | return mapResult; | ||
31 | } | ||
32 | |||
33 | describe('mapRustDiagnosticToVsCode', () => { | ||
34 | it('should map an incompatible type for trait error', () => { | ||
35 | const { diagnostic, suggestedFixes } = mapFixtureToVsCode( | ||
36 | 'error/E0053', | ||
37 | ); | ||
38 | |||
39 | assert.strictEqual( | ||
40 | diagnostic.severity, | ||
41 | vscode.DiagnosticSeverity.Error, | ||
42 | ); | ||
43 | assert.strictEqual(diagnostic.source, 'rustc'); | ||
44 | assert.strictEqual( | ||
45 | diagnostic.message, | ||
46 | [ | ||
47 | `method \`next\` has an incompatible type for trait`, | ||
48 | `expected type \`fn(&mut ty::list_iter::ListIterator<'list, M>) -> std::option::Option<&ty::Ref<M>>\``, | ||
49 | ` found type \`fn(&ty::list_iter::ListIterator<'list, M>) -> std::option::Option<&'list ty::Ref<M>>\``, | ||
50 | ].join('\n'), | ||
51 | ); | ||
52 | assert.strictEqual(diagnostic.code, 'E0053'); | ||
53 | assert.deepStrictEqual(diagnostic.tags, []); | ||
54 | |||
55 | // No related information | ||
56 | assert.deepStrictEqual(diagnostic.relatedInformation, []); | ||
57 | |||
58 | // There are no suggested fixes | ||
59 | assert.strictEqual(suggestedFixes.length, 0); | ||
60 | }); | ||
61 | |||
62 | it('should map an unused variable warning', () => { | ||
63 | const { diagnostic, suggestedFixes } = mapFixtureToVsCode( | ||
64 | 'warning/unused_variables', | ||
65 | ); | ||
66 | |||
67 | assert.strictEqual( | ||
68 | diagnostic.severity, | ||
69 | vscode.DiagnosticSeverity.Warning, | ||
70 | ); | ||
71 | assert.strictEqual( | ||
72 | diagnostic.message, | ||
73 | [ | ||
74 | 'unused variable: `foo`', | ||
75 | '#[warn(unused_variables)] on by default', | ||
76 | ].join('\n'), | ||
77 | ); | ||
78 | assert.strictEqual(diagnostic.code, 'unused_variables'); | ||
79 | assert.strictEqual(diagnostic.source, 'rustc'); | ||
80 | assert.deepStrictEqual(diagnostic.tags, [ | ||
81 | vscode.DiagnosticTag.Unnecessary, | ||
82 | ]); | ||
83 | |||
84 | // No related information | ||
85 | assert.deepStrictEqual(diagnostic.relatedInformation, []); | ||
86 | |||
87 | // One suggested fix available to prefix the variable | ||
88 | assert.strictEqual(suggestedFixes.length, 1); | ||
89 | const [suggestedFix] = suggestedFixes; | ||
90 | assert.strictEqual( | ||
91 | suggestedFix.title, | ||
92 | 'consider prefixing with an underscore: `_foo`', | ||
93 | ); | ||
94 | assert.strictEqual( | ||
95 | suggestedFix.applicability, | ||
96 | SuggestionApplicability.MachineApplicable, | ||
97 | ); | ||
98 | }); | ||
99 | |||
100 | it('should map a wrong number of parameters error', () => { | ||
101 | const { diagnostic, suggestedFixes } = mapFixtureToVsCode( | ||
102 | 'error/E0061', | ||
103 | ); | ||
104 | |||
105 | assert.strictEqual( | ||
106 | diagnostic.severity, | ||
107 | vscode.DiagnosticSeverity.Error, | ||
108 | ); | ||
109 | assert.strictEqual( | ||
110 | diagnostic.message, | ||
111 | [ | ||
112 | 'this function takes 2 parameters but 3 parameters were supplied', | ||
113 | 'expected 2 parameters', | ||
114 | ].join('\n'), | ||
115 | ); | ||
116 | assert.strictEqual(diagnostic.code, 'E0061'); | ||
117 | assert.strictEqual(diagnostic.source, 'rustc'); | ||
118 | assert.deepStrictEqual(diagnostic.tags, []); | ||
119 | |||
120 | // One related information for the original definition | ||
121 | const relatedInformation = diagnostic.relatedInformation; | ||
122 | if (!relatedInformation) { | ||
123 | assert.fail('Related information unexpectedly undefined'); | ||
124 | return; | ||
125 | } | ||
126 | assert.strictEqual(relatedInformation.length, 1); | ||
127 | const [related] = relatedInformation; | ||
128 | assert.strictEqual(related.message, 'defined here'); | ||
129 | |||
130 | // There are no suggested fixes | ||
131 | assert.strictEqual(suggestedFixes.length, 0); | ||
132 | }); | ||
133 | |||
134 | it('should map a Clippy copy pass by ref warning', () => { | ||
135 | const { diagnostic, suggestedFixes } = mapFixtureToVsCode( | ||
136 | 'clippy/trivially_copy_pass_by_ref', | ||
137 | ); | ||
138 | |||
139 | assert.strictEqual( | ||
140 | diagnostic.severity, | ||
141 | vscode.DiagnosticSeverity.Warning, | ||
142 | ); | ||
143 | assert.strictEqual(diagnostic.source, 'clippy'); | ||
144 | assert.strictEqual( | ||
145 | diagnostic.message, | ||
146 | [ | ||
147 | 'this argument is passed by reference, but would be more efficient if passed by value', | ||
148 | '#[warn(clippy::trivially_copy_pass_by_ref)] implied by #[warn(clippy::all)]', | ||
149 | 'for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#trivially_copy_pass_by_ref', | ||
150 | ].join('\n'), | ||
151 | ); | ||
152 | assert.strictEqual(diagnostic.code, 'trivially_copy_pass_by_ref'); | ||
153 | assert.deepStrictEqual(diagnostic.tags, []); | ||
154 | |||
155 | // One related information for the lint definition | ||
156 | const relatedInformation = diagnostic.relatedInformation; | ||
157 | if (!relatedInformation) { | ||
158 | assert.fail('Related information unexpectedly undefined'); | ||
159 | return; | ||
160 | } | ||
161 | assert.strictEqual(relatedInformation.length, 1); | ||
162 | const [related] = relatedInformation; | ||
163 | assert.strictEqual(related.message, 'lint level defined here'); | ||
164 | |||
165 | // One suggested fix to pass by value | ||
166 | assert.strictEqual(suggestedFixes.length, 1); | ||
167 | const [suggestedFix] = suggestedFixes; | ||
168 | assert.strictEqual( | ||
169 | suggestedFix.title, | ||
170 | 'consider passing by value instead: `self`', | ||
171 | ); | ||
172 | // Clippy does not mark this with any applicability | ||
173 | assert.strictEqual( | ||
174 | suggestedFix.applicability, | ||
175 | SuggestionApplicability.Unspecified, | ||
176 | ); | ||
177 | }); | ||
178 | |||
179 | it('should map a mismatched type error', () => { | ||
180 | const { diagnostic, suggestedFixes } = mapFixtureToVsCode( | ||
181 | 'error/E0308', | ||
182 | ); | ||
183 | |||
184 | assert.strictEqual( | ||
185 | diagnostic.severity, | ||
186 | vscode.DiagnosticSeverity.Error, | ||
187 | ); | ||
188 | assert.strictEqual( | ||
189 | diagnostic.message, | ||
190 | ['mismatched types', 'expected usize, found u32'].join('\n'), | ||
191 | ); | ||
192 | assert.strictEqual(diagnostic.code, 'E0308'); | ||
193 | assert.strictEqual(diagnostic.source, 'rustc'); | ||
194 | assert.deepStrictEqual(diagnostic.tags, []); | ||
195 | |||
196 | // No related information | ||
197 | assert.deepStrictEqual(diagnostic.relatedInformation, []); | ||
198 | |||
199 | // There are no suggested fixes | ||
200 | assert.strictEqual(suggestedFixes.length, 0); | ||
201 | }); | ||
202 | |||
203 | it('should map a macro invocation location to normal file path', () => { | ||
204 | const { location, diagnostic, suggestedFixes } = mapFixtureToVsCode( | ||
205 | 'error/E0277', | ||
206 | ); | ||
207 | |||
208 | assert.strictEqual( | ||
209 | diagnostic.severity, | ||
210 | vscode.DiagnosticSeverity.Error, | ||
211 | ); | ||
212 | assert.strictEqual( | ||
213 | diagnostic.message, | ||
214 | [ | ||
215 | "can't compare `{integer}` with `&str`", | ||
216 | 'the trait `std::cmp::PartialEq<&str>` is not implemented for `{integer}`', | ||
217 | ].join('\n'), | ||
218 | ); | ||
219 | assert.strictEqual(diagnostic.code, 'E0277'); | ||
220 | assert.strictEqual(diagnostic.source, 'rustc'); | ||
221 | assert.deepStrictEqual(diagnostic.tags, []); | ||
222 | |||
223 | // No related information | ||
224 | assert.deepStrictEqual(diagnostic.relatedInformation, []); | ||
225 | |||
226 | // There are no suggested fixes | ||
227 | assert.strictEqual(suggestedFixes.length, 0); | ||
228 | |||
229 | // The file url should be normal file | ||
230 | // Ignore the first part because it depends on vs workspace location | ||
231 | assert.strictEqual( | ||
232 | location.uri.path.substr(-'src/main.rs'.length), | ||
233 | 'src/main.rs', | ||
234 | ); | ||
235 | }); | ||
236 | }); | ||
diff --git a/editors/code/src/test/utils/diagnotics/vscode.test.ts b/editors/code/src/test/utils/diagnotics/vscode.test.ts deleted file mode 100644 index 4944dd032..000000000 --- a/editors/code/src/test/utils/diagnotics/vscode.test.ts +++ /dev/null | |||
@@ -1,98 +0,0 @@ | |||
1 | import * as assert from 'assert'; | ||
2 | import * as vscode from 'vscode'; | ||
3 | |||
4 | import { areDiagnosticsEqual } from '../../../utils/diagnostics/vscode'; | ||
5 | |||
6 | const range1 = new vscode.Range( | ||
7 | new vscode.Position(1, 2), | ||
8 | new vscode.Position(3, 4), | ||
9 | ); | ||
10 | |||
11 | const range2 = new vscode.Range( | ||
12 | new vscode.Position(5, 6), | ||
13 | new vscode.Position(7, 8), | ||
14 | ); | ||
15 | |||
16 | describe('areDiagnosticsEqual', () => { | ||
17 | it('should treat identical diagnostics as equal', () => { | ||
18 | const diagnostic1 = new vscode.Diagnostic( | ||
19 | range1, | ||
20 | 'Hello, world!', | ||
21 | vscode.DiagnosticSeverity.Error, | ||
22 | ); | ||
23 | |||
24 | const diagnostic2 = new vscode.Diagnostic( | ||
25 | range1, | ||
26 | 'Hello, world!', | ||
27 | vscode.DiagnosticSeverity.Error, | ||
28 | ); | ||
29 | |||
30 | assert(areDiagnosticsEqual(diagnostic1, diagnostic2)); | ||
31 | }); | ||
32 | |||
33 | it('should treat diagnostics with different sources as inequal', () => { | ||
34 | const diagnostic1 = new vscode.Diagnostic( | ||
35 | range1, | ||
36 | 'Hello, world!', | ||
37 | vscode.DiagnosticSeverity.Error, | ||
38 | ); | ||
39 | diagnostic1.source = 'rustc'; | ||
40 | |||
41 | const diagnostic2 = new vscode.Diagnostic( | ||
42 | range1, | ||
43 | 'Hello, world!', | ||
44 | vscode.DiagnosticSeverity.Error, | ||
45 | ); | ||
46 | diagnostic2.source = 'clippy'; | ||
47 | |||
48 | assert(!areDiagnosticsEqual(diagnostic1, diagnostic2)); | ||
49 | }); | ||
50 | |||
51 | it('should treat diagnostics with different ranges as inequal', () => { | ||
52 | const diagnostic1 = new vscode.Diagnostic( | ||
53 | range1, | ||
54 | 'Hello, world!', | ||
55 | vscode.DiagnosticSeverity.Error, | ||
56 | ); | ||
57 | |||
58 | const diagnostic2 = new vscode.Diagnostic( | ||
59 | range2, | ||
60 | 'Hello, world!', | ||
61 | vscode.DiagnosticSeverity.Error, | ||
62 | ); | ||
63 | |||
64 | assert(!areDiagnosticsEqual(diagnostic1, diagnostic2)); | ||
65 | }); | ||
66 | |||
67 | it('should treat diagnostics with different messages as inequal', () => { | ||
68 | const diagnostic1 = new vscode.Diagnostic( | ||
69 | range1, | ||
70 | 'Hello, world!', | ||
71 | vscode.DiagnosticSeverity.Error, | ||
72 | ); | ||
73 | |||
74 | const diagnostic2 = new vscode.Diagnostic( | ||
75 | range1, | ||
76 | 'Goodbye!, world!', | ||
77 | vscode.DiagnosticSeverity.Error, | ||
78 | ); | ||
79 | |||
80 | assert(!areDiagnosticsEqual(diagnostic1, diagnostic2)); | ||
81 | }); | ||
82 | |||
83 | it('should treat diagnostics with different severities as inequal', () => { | ||
84 | const diagnostic1 = new vscode.Diagnostic( | ||
85 | range1, | ||
86 | 'Hello, world!', | ||
87 | vscode.DiagnosticSeverity.Warning, | ||
88 | ); | ||
89 | |||
90 | const diagnostic2 = new vscode.Diagnostic( | ||
91 | range1, | ||
92 | 'Hello, world!', | ||
93 | vscode.DiagnosticSeverity.Error, | ||
94 | ); | ||
95 | |||
96 | assert(!areDiagnosticsEqual(diagnostic1, diagnostic2)); | ||
97 | }); | ||
98 | }); | ||
diff --git a/editors/code/src/test/utils/index.ts b/editors/code/src/test/utils/index.ts deleted file mode 100644 index 9927daaf6..000000000 --- a/editors/code/src/test/utils/index.ts +++ /dev/null | |||
@@ -1,49 +0,0 @@ | |||
1 | // | ||
2 | // PLEASE DO NOT MODIFY / DELETE UNLESS YOU KNOW WHAT YOU ARE DOING | ||
3 | // | ||
4 | // This file is providing the test runner to use when running extension tests. | ||
5 | // By default the test runner in use is Mocha based. | ||
6 | // | ||
7 | // You can provide your own test runner if you want to override it by exporting | ||
8 | // a function run(testRoot: string, clb: (error:Error) => void) that the extension | ||
9 | // host can call to run the tests. The test runner is expected to use console.log | ||
10 | // to report the results back to the caller. When the tests are finished, return | ||
11 | // a possible error to the callback or null if none. | ||
12 | |||
13 | import * as glob from 'glob'; | ||
14 | import * as Mocha from 'mocha'; | ||
15 | import * as path from 'path'; | ||
16 | |||
17 | export function run(): Promise<void> { | ||
18 | // Create the mocha test | ||
19 | const mocha = new Mocha({ | ||
20 | ui: 'bdd', | ||
21 | }); | ||
22 | mocha.useColors(true); | ||
23 | |||
24 | const testsRoot = __dirname; | ||
25 | |||
26 | return new Promise((c, e) => { | ||
27 | glob('**/**.test.js', { cwd: testsRoot }, (err, files) => { | ||
28 | if (err) { | ||
29 | return e(err); | ||
30 | } | ||
31 | |||
32 | // Add files to the test suite | ||
33 | files.forEach(f => mocha.addFile(path.resolve(testsRoot, f))); | ||
34 | |||
35 | try { | ||
36 | // Run the mocha test | ||
37 | mocha.run(failures => { | ||
38 | if (failures > 0) { | ||
39 | e(new Error(`${failures} tests failed.`)); | ||
40 | } else { | ||
41 | c(); | ||
42 | } | ||
43 | }); | ||
44 | } catch (err) { | ||
45 | e(err); | ||
46 | } | ||
47 | }); | ||
48 | }); | ||
49 | } | ||
diff --git a/editors/code/src/utils/diagnostics/SuggestedFix.ts b/editors/code/src/utils/diagnostics/SuggestedFix.ts deleted file mode 100644 index 6e660bb61..000000000 --- a/editors/code/src/utils/diagnostics/SuggestedFix.ts +++ /dev/null | |||
@@ -1,67 +0,0 @@ | |||
1 | import * as vscode from 'vscode'; | ||
2 | |||
3 | import { SuggestionApplicability } from './rust'; | ||
4 | |||
5 | /** | ||
6 | * Model object for text replacements suggested by the Rust compiler | ||
7 | * | ||
8 | * This is an intermediate form between the raw `rustc` JSON and a | ||
9 | * `vscode.CodeAction`. It's optimised for the use-cases of | ||
10 | * `SuggestedFixCollection`. | ||
11 | */ | ||
12 | export default class SuggestedFix { | ||
13 | public readonly title: string; | ||
14 | public readonly location: vscode.Location; | ||
15 | public readonly replacement: string; | ||
16 | public readonly applicability: SuggestionApplicability; | ||
17 | |||
18 | /** | ||
19 | * Diagnostics this suggested fix could resolve | ||
20 | */ | ||
21 | public diagnostics: vscode.Diagnostic[]; | ||
22 | |||
23 | constructor( | ||
24 | title: string, | ||
25 | location: vscode.Location, | ||
26 | replacement: string, | ||
27 | applicability: SuggestionApplicability = SuggestionApplicability.Unspecified, | ||
28 | ) { | ||
29 | this.title = title; | ||
30 | this.location = location; | ||
31 | this.replacement = replacement; | ||
32 | this.applicability = applicability; | ||
33 | this.diagnostics = []; | ||
34 | } | ||
35 | |||
36 | /** | ||
37 | * Determines if this suggested fix is equivalent to another instance | ||
38 | */ | ||
39 | public isEqual(other: SuggestedFix): boolean { | ||
40 | return ( | ||
41 | this.title === other.title && | ||
42 | this.location.range.isEqual(other.location.range) && | ||
43 | this.replacement === other.replacement && | ||
44 | this.applicability === other.applicability | ||
45 | ); | ||
46 | } | ||
47 | |||
48 | /** | ||
49 | * Converts this suggested fix to a VS Code Quick Fix code action | ||
50 | */ | ||
51 | public toCodeAction(): vscode.CodeAction { | ||
52 | const codeAction = new vscode.CodeAction( | ||
53 | this.title, | ||
54 | vscode.CodeActionKind.QuickFix, | ||
55 | ); | ||
56 | |||
57 | const edit = new vscode.WorkspaceEdit(); | ||
58 | edit.replace(this.location.uri, this.location.range, this.replacement); | ||
59 | codeAction.edit = edit; | ||
60 | |||
61 | codeAction.isPreferred = | ||
62 | this.applicability === SuggestionApplicability.MachineApplicable; | ||
63 | |||
64 | codeAction.diagnostics = [...this.diagnostics]; | ||
65 | return codeAction; | ||
66 | } | ||
67 | } | ||
diff --git a/editors/code/src/utils/diagnostics/SuggestedFixCollection.ts b/editors/code/src/utils/diagnostics/SuggestedFixCollection.ts deleted file mode 100644 index 57c9856cf..000000000 --- a/editors/code/src/utils/diagnostics/SuggestedFixCollection.ts +++ /dev/null | |||
@@ -1,77 +0,0 @@ | |||
1 | import * as vscode from 'vscode'; | ||
2 | import SuggestedFix from './SuggestedFix'; | ||
3 | |||
4 | /** | ||
5 | * Collection of suggested fixes across multiple documents | ||
6 | * | ||
7 | * This stores `SuggestedFix` model objects and returns them via the | ||
8 | * `vscode.CodeActionProvider` interface. | ||
9 | */ | ||
10 | export default class SuggestedFixCollection | ||
11 | implements vscode.CodeActionProvider { | ||
12 | public static PROVIDED_CODE_ACTION_KINDS = [vscode.CodeActionKind.QuickFix]; | ||
13 | |||
14 | /** | ||
15 | * Map of document URI strings to suggested fixes | ||
16 | */ | ||
17 | private suggestedFixes: Map<string, SuggestedFix[]>; | ||
18 | |||
19 | constructor() { | ||
20 | this.suggestedFixes = new Map(); | ||
21 | } | ||
22 | |||
23 | /** | ||
24 | * Clears all suggested fixes across all documents | ||
25 | */ | ||
26 | public clear(): void { | ||
27 | this.suggestedFixes = new Map(); | ||
28 | } | ||
29 | |||
30 | /** | ||
31 | * Adds a suggested fix for the given diagnostic | ||
32 | * | ||
33 | * Some suggested fixes will appear in multiple diagnostics. For example, | ||
34 | * forgetting a `mut` on a variable will suggest changing the delaration on | ||
35 | * every mutable usage site. If the suggested fix has already been added | ||
36 | * this method will instead associate the existing fix with the new | ||
37 | * diagnostic. | ||
38 | */ | ||
39 | public addSuggestedFixForDiagnostic( | ||
40 | suggestedFix: SuggestedFix, | ||
41 | diagnostic: vscode.Diagnostic, | ||
42 | ): void { | ||
43 | const fileUriString = suggestedFix.location.uri.toString(); | ||
44 | const fileSuggestions = this.suggestedFixes.get(fileUriString) || []; | ||
45 | |||
46 | const existingSuggestion = fileSuggestions.find(s => | ||
47 | s.isEqual(suggestedFix), | ||
48 | ); | ||
49 | |||
50 | if (existingSuggestion) { | ||
51 | // The existing suggestion also applies to this new diagnostic | ||
52 | existingSuggestion.diagnostics.push(diagnostic); | ||
53 | } else { | ||
54 | // We haven't seen this suggestion before | ||
55 | suggestedFix.diagnostics.push(diagnostic); | ||
56 | fileSuggestions.push(suggestedFix); | ||
57 | } | ||
58 | |||
59 | this.suggestedFixes.set(fileUriString, fileSuggestions); | ||
60 | } | ||
61 | |||
62 | /** | ||
63 | * Filters suggested fixes by their document and range and converts them to | ||
64 | * code actions | ||
65 | */ | ||
66 | public provideCodeActions( | ||
67 | document: vscode.TextDocument, | ||
68 | range: vscode.Range, | ||
69 | ): vscode.CodeAction[] { | ||
70 | const documentUriString = document.uri.toString(); | ||
71 | |||
72 | const suggestedFixes = this.suggestedFixes.get(documentUriString); | ||
73 | return (suggestedFixes || []) | ||
74 | .filter(({ location }) => location.range.intersection(range)) | ||
75 | .map(suggestedEdit => suggestedEdit.toCodeAction()); | ||
76 | } | ||
77 | } | ||
diff --git a/editors/code/src/utils/diagnostics/rust.ts b/editors/code/src/utils/diagnostics/rust.ts deleted file mode 100644 index 1f0c0d3e4..000000000 --- a/editors/code/src/utils/diagnostics/rust.ts +++ /dev/null | |||
@@ -1,299 +0,0 @@ | |||
1 | import * as path from 'path'; | ||
2 | import * as vscode from 'vscode'; | ||
3 | |||
4 | import SuggestedFix from './SuggestedFix'; | ||
5 | |||
6 | export enum SuggestionApplicability { | ||
7 | MachineApplicable = 'MachineApplicable', | ||
8 | HasPlaceholders = 'HasPlaceholders', | ||
9 | MaybeIncorrect = 'MaybeIncorrect', | ||
10 | Unspecified = 'Unspecified', | ||
11 | } | ||
12 | |||
13 | export interface RustDiagnosticSpanMacroExpansion { | ||
14 | span: RustDiagnosticSpan; | ||
15 | macro_decl_name: string; | ||
16 | def_site_span?: RustDiagnosticSpan; | ||
17 | } | ||
18 | |||
19 | // Reference: | ||
20 | // https://github.com/rust-lang/rust/blob/master/src/libsyntax/json.rs | ||
21 | export interface RustDiagnosticSpan { | ||
22 | line_start: number; | ||
23 | line_end: number; | ||
24 | column_start: number; | ||
25 | column_end: number; | ||
26 | is_primary: boolean; | ||
27 | file_name: string; | ||
28 | label?: string; | ||
29 | expansion?: RustDiagnosticSpanMacroExpansion; | ||
30 | suggested_replacement?: string; | ||
31 | suggestion_applicability?: SuggestionApplicability; | ||
32 | } | ||
33 | |||
34 | export interface RustDiagnostic { | ||
35 | spans: RustDiagnosticSpan[]; | ||
36 | rendered: string; | ||
37 | message: string; | ||
38 | level: string; | ||
39 | code?: { | ||
40 | code: string; | ||
41 | }; | ||
42 | children: RustDiagnostic[]; | ||
43 | } | ||
44 | |||
45 | export interface MappedRustDiagnostic { | ||
46 | location: vscode.Location; | ||
47 | diagnostic: vscode.Diagnostic; | ||
48 | suggestedFixes: SuggestedFix[]; | ||
49 | } | ||
50 | |||
51 | interface MappedRustChildDiagnostic { | ||
52 | related?: vscode.DiagnosticRelatedInformation; | ||
53 | suggestedFix?: SuggestedFix; | ||
54 | messageLine?: string; | ||
55 | } | ||
56 | |||
57 | /** | ||
58 | * Converts a Rust level string to a VsCode severity | ||
59 | */ | ||
60 | function mapLevelToSeverity(s: string): vscode.DiagnosticSeverity { | ||
61 | if (s === 'error') { | ||
62 | return vscode.DiagnosticSeverity.Error; | ||
63 | } | ||
64 | if (s.startsWith('warn')) { | ||
65 | return vscode.DiagnosticSeverity.Warning; | ||
66 | } | ||
67 | return vscode.DiagnosticSeverity.Information; | ||
68 | } | ||
69 | |||
70 | /** | ||
71 | * Check whether a file name is from macro invocation | ||
72 | */ | ||
73 | function isFromMacro(fileName: string): boolean { | ||
74 | return fileName.startsWith('<') && fileName.endsWith('>'); | ||
75 | } | ||
76 | |||
77 | /** | ||
78 | * Converts a Rust macro span to a VsCode location recursively | ||
79 | */ | ||
80 | function mapMacroSpanToLocation( | ||
81 | spanMacro: RustDiagnosticSpanMacroExpansion, | ||
82 | ): vscode.Location | undefined { | ||
83 | if (!isFromMacro(spanMacro.span.file_name)) { | ||
84 | return mapSpanToLocation(spanMacro.span); | ||
85 | } | ||
86 | |||
87 | if (spanMacro.span.expansion) { | ||
88 | return mapMacroSpanToLocation(spanMacro.span.expansion); | ||
89 | } | ||
90 | |||
91 | return; | ||
92 | } | ||
93 | |||
94 | /** | ||
95 | * Converts a Rust span to a VsCode location | ||
96 | */ | ||
97 | function mapSpanToLocation(span: RustDiagnosticSpan): vscode.Location { | ||
98 | if (isFromMacro(span.file_name) && span.expansion) { | ||
99 | const macroLoc = mapMacroSpanToLocation(span.expansion); | ||
100 | if (macroLoc) { | ||
101 | return macroLoc; | ||
102 | } | ||
103 | } | ||
104 | |||
105 | const fileName = path.join(vscode.workspace.rootPath || '', span.file_name); | ||
106 | const fileUri = vscode.Uri.file(fileName); | ||
107 | |||
108 | const range = new vscode.Range( | ||
109 | new vscode.Position(span.line_start - 1, span.column_start - 1), | ||
110 | new vscode.Position(span.line_end - 1, span.column_end - 1), | ||
111 | ); | ||
112 | |||
113 | return new vscode.Location(fileUri, range); | ||
114 | } | ||
115 | |||
116 | /** | ||
117 | * Converts a secondary Rust span to a VsCode related information | ||
118 | * | ||
119 | * If the span is unlabelled this will return `undefined`. | ||
120 | */ | ||
121 | function mapSecondarySpanToRelated( | ||
122 | span: RustDiagnosticSpan, | ||
123 | ): vscode.DiagnosticRelatedInformation | undefined { | ||
124 | if (!span.label) { | ||
125 | // Nothing to label this with | ||
126 | return; | ||
127 | } | ||
128 | |||
129 | const location = mapSpanToLocation(span); | ||
130 | return new vscode.DiagnosticRelatedInformation(location, span.label); | ||
131 | } | ||
132 | |||
133 | /** | ||
134 | * Determines if diagnostic is related to unused code | ||
135 | */ | ||
136 | function isUnusedOrUnnecessary(rd: RustDiagnostic): boolean { | ||
137 | if (!rd.code) { | ||
138 | return false; | ||
139 | } | ||
140 | |||
141 | return [ | ||
142 | 'dead_code', | ||
143 | 'unknown_lints', | ||
144 | 'unreachable_code', | ||
145 | 'unused_attributes', | ||
146 | 'unused_imports', | ||
147 | 'unused_macros', | ||
148 | 'unused_variables', | ||
149 | ].includes(rd.code.code); | ||
150 | } | ||
151 | |||
152 | /** | ||
153 | * Determines if diagnostic is related to deprecated code | ||
154 | */ | ||
155 | function isDeprecated(rd: RustDiagnostic): boolean { | ||
156 | if (!rd.code) { | ||
157 | return false; | ||
158 | } | ||
159 | |||
160 | return ['deprecated'].includes(rd.code.code); | ||
161 | } | ||
162 | |||
163 | /** | ||
164 | * Converts a Rust child diagnostic to a VsCode related information | ||
165 | * | ||
166 | * This can have three outcomes: | ||
167 | * | ||
168 | * 1. If this is no primary span this will return a `noteLine` | ||
169 | * 2. If there is a primary span with a suggested replacement it will return a | ||
170 | * `codeAction`. | ||
171 | * 3. If there is a primary span without a suggested replacement it will return | ||
172 | * a `related`. | ||
173 | */ | ||
174 | function mapRustChildDiagnostic(rd: RustDiagnostic): MappedRustChildDiagnostic { | ||
175 | const span = rd.spans.find(s => s.is_primary); | ||
176 | |||
177 | if (!span) { | ||
178 | // `rustc` uses these spanless children as a way to print multi-line | ||
179 | // messages | ||
180 | return { messageLine: rd.message }; | ||
181 | } | ||
182 | |||
183 | // If we have a primary span use its location, otherwise use the parent | ||
184 | const location = mapSpanToLocation(span); | ||
185 | |||
186 | // We need to distinguish `null` from an empty string | ||
187 | if (span && typeof span.suggested_replacement === 'string') { | ||
188 | // Include our replacement in the title unless it's empty | ||
189 | const title = span.suggested_replacement | ||
190 | ? `${rd.message}: \`${span.suggested_replacement}\`` | ||
191 | : rd.message; | ||
192 | |||
193 | return { | ||
194 | suggestedFix: new SuggestedFix( | ||
195 | title, | ||
196 | location, | ||
197 | span.suggested_replacement, | ||
198 | span.suggestion_applicability, | ||
199 | ), | ||
200 | }; | ||
201 | } else { | ||
202 | const related = new vscode.DiagnosticRelatedInformation( | ||
203 | location, | ||
204 | rd.message, | ||
205 | ); | ||
206 | |||
207 | return { related }; | ||
208 | } | ||
209 | } | ||
210 | |||
211 | /** | ||
212 | * Converts a Rust root diagnostic to VsCode form | ||
213 | * | ||
214 | * This flattens the Rust diagnostic by: | ||
215 | * | ||
216 | * 1. Creating a `vscode.Diagnostic` with the root message and primary span. | ||
217 | * 2. Adding any labelled secondary spans to `relatedInformation` | ||
218 | * 3. Categorising child diagnostics as either `SuggestedFix`es, | ||
219 | * `relatedInformation` or additional message lines. | ||
220 | * | ||
221 | * If the diagnostic has no primary span this will return `undefined` | ||
222 | */ | ||
223 | export function mapRustDiagnosticToVsCode( | ||
224 | rd: RustDiagnostic, | ||
225 | ): MappedRustDiagnostic | undefined { | ||
226 | const primarySpan = rd.spans.find(s => s.is_primary); | ||
227 | if (!primarySpan) { | ||
228 | return; | ||
229 | } | ||
230 | |||
231 | const location = mapSpanToLocation(primarySpan); | ||
232 | const secondarySpans = rd.spans.filter(s => !s.is_primary); | ||
233 | |||
234 | const severity = mapLevelToSeverity(rd.level); | ||
235 | let primarySpanLabel = primarySpan.label; | ||
236 | |||
237 | const vd = new vscode.Diagnostic(location.range, rd.message, severity); | ||
238 | |||
239 | let source = 'rustc'; | ||
240 | let code = rd.code && rd.code.code; | ||
241 | if (code) { | ||
242 | // See if this is an RFC #2103 scoped lint (e.g. from Clippy) | ||
243 | const scopedCode = code.split('::'); | ||
244 | if (scopedCode.length === 2) { | ||
245 | [source, code] = scopedCode; | ||
246 | } | ||
247 | } | ||
248 | |||
249 | vd.source = source; | ||
250 | vd.code = code; | ||
251 | vd.relatedInformation = []; | ||
252 | vd.tags = []; | ||
253 | |||
254 | for (const secondarySpan of secondarySpans) { | ||
255 | const related = mapSecondarySpanToRelated(secondarySpan); | ||
256 | if (related) { | ||
257 | vd.relatedInformation.push(related); | ||
258 | } | ||
259 | } | ||
260 | |||
261 | const suggestedFixes = []; | ||
262 | for (const child of rd.children) { | ||
263 | const { related, suggestedFix, messageLine } = mapRustChildDiagnostic( | ||
264 | child, | ||
265 | ); | ||
266 | |||
267 | if (related) { | ||
268 | vd.relatedInformation.push(related); | ||
269 | } | ||
270 | if (suggestedFix) { | ||
271 | suggestedFixes.push(suggestedFix); | ||
272 | } | ||
273 | if (messageLine) { | ||
274 | vd.message += `\n${messageLine}`; | ||
275 | |||
276 | // These secondary messages usually duplicate the content of the | ||
277 | // primary span label. | ||
278 | primarySpanLabel = undefined; | ||
279 | } | ||
280 | } | ||
281 | |||
282 | if (primarySpanLabel) { | ||
283 | vd.message += `\n${primarySpanLabel}`; | ||
284 | } | ||
285 | |||
286 | if (isUnusedOrUnnecessary(rd)) { | ||
287 | vd.tags.push(vscode.DiagnosticTag.Unnecessary); | ||
288 | } | ||
289 | |||
290 | if (isDeprecated(rd)) { | ||
291 | vd.tags.push(vscode.DiagnosticTag.Deprecated); | ||
292 | } | ||
293 | |||
294 | return { | ||
295 | location, | ||
296 | diagnostic: vd, | ||
297 | suggestedFixes, | ||
298 | }; | ||
299 | } | ||
diff --git a/editors/code/src/utils/diagnostics/vscode.ts b/editors/code/src/utils/diagnostics/vscode.ts deleted file mode 100644 index f4a5450e2..000000000 --- a/editors/code/src/utils/diagnostics/vscode.ts +++ /dev/null | |||
@@ -1,14 +0,0 @@ | |||
1 | import * as vscode from 'vscode'; | ||
2 | |||
3 | /** Compares two `vscode.Diagnostic`s for equality */ | ||
4 | export function areDiagnosticsEqual( | ||
5 | left: vscode.Diagnostic, | ||
6 | right: vscode.Diagnostic, | ||
7 | ): boolean { | ||
8 | return ( | ||
9 | left.source === right.source && | ||
10 | left.severity === right.severity && | ||
11 | left.range.isEqual(right.range) && | ||
12 | left.message === right.message | ||
13 | ); | ||
14 | } | ||
diff --git a/editors/code/src/utils/processes.ts b/editors/code/src/utils/processes.ts deleted file mode 100644 index a1d6b7eaf..000000000 --- a/editors/code/src/utils/processes.ts +++ /dev/null | |||
@@ -1,51 +0,0 @@ | |||
1 | 'use strict'; | ||
2 | |||
3 | import * as cp from 'child_process'; | ||
4 | import ChildProcess = cp.ChildProcess; | ||
5 | |||
6 | import { join } from 'path'; | ||
7 | |||
8 | const isWindows = process.platform === 'win32'; | ||
9 | const isMacintosh = process.platform === 'darwin'; | ||
10 | const 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 | |||
18 | export 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 deleted file mode 100644 index 2ec9e1c2e..000000000 --- a/editors/code/src/utils/terminateProcess.sh +++ /dev/null | |||
@@ -1,12 +0,0 @@ | |||
1 | #!/bin/bash | ||
2 | |||
3 | terminateTree() { | ||
4 | for cpid in $(pgrep -P $1); do | ||
5 | terminateTree $cpid | ||
6 | done | ||
7 | kill -9 $1 > /dev/null 2>&1 | ||
8 | } | ||
9 | |||
10 | for pid in $*; do | ||
11 | terminateTree $pid | ||
12 | done \ No newline at end of file | ||