-
Notifications
You must be signed in to change notification settings - Fork 57
/
Copy pathextension.ts
366 lines (323 loc) · 12.3 KB
/
extension.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
import * as path from "path";
import {
workspace,
ExtensionContext,
commands,
languages,
window,
StatusBarAlignment,
Uri,
Range,
Position,
CodeAction,
WorkspaceEdit,
CodeActionKind,
Diagnostic,
} from "vscode";
import {
LanguageClient,
LanguageClientOptions,
ServerOptions,
State,
TransportKind,
} from "vscode-languageclient/node";
import * as customCommands from "./commands";
import {
DiagnosticsResultCodeActionsMap,
statusBarItem,
} from "./commands/code_analysis";
let client: LanguageClient;
// let taskProvider = tasks.registerTaskProvider('Run ReScript build', {
// provideTasks: () => {
// // if (!rakePromise) {
// // rakePromise = getRakeTasks();
// // }
// // return rakePromise;
// // taskDefinition: TaskDefinition,
// // scope: WorkspaceFolder | TaskScope.Global | TaskScope.Workspace,
// // name: string,
// // source: string,
// // execution ?: ProcessExecution | ShellExecution | CustomExecution,
// // problemMatchers ?: string | string[]
// return [
// new Task(
// {
// type: 'bsb',
// },
// TaskScope.Workspace,
// // definition.task,
// 'build and watch',
// 'bsb',
// new ShellExecution(
// // `./node_modules/.bin/bsb -make-world -w`
// `pwd`
// ),
// "Hello"
// )
// ]
// },
// resolveTask(_task: Task): Task | undefined {
// // const task = _task.definition.task;
// // // A Rake task consists of a task and an optional file as specified in RakeTaskDefinition
// // // Make sure that this looks like a Rake task by checking that there is a task.
// // if (task) {
// // // resolveTask requires that the same definition object be used.
// // const definition: RakeTaskDefinition = <any>_task.definition;
// // return new Task(
// // definition,
// // definition.task,
// // 'rake',
// // new vscode.ShellExecution(`rake ${definition.task}`)
// // );
// // }
// return undefined;
// }
// });
export function activate(context: ExtensionContext) {
let outputChannel = window.createOutputChannel(
"ReScript Language Server",
"rescript"
);
function createLanguageClient() {
// The server is implemented in node
let serverModule = context.asAbsolutePath(
path.join("server", "out", "cli.js")
);
// The debug options for the server
// --inspect=6009: runs the server in Node's Inspector mode so VS Code can attach to the server for debugging
let debugOptions = { execArgv: ["--nolazy", "--inspect=6009"] };
// If the extension is launched in debug mode then the debug server options are used
// Otherwise the run options are used
let serverOptions: ServerOptions = {
run: {
module: serverModule,
args: ["--node-ipc"],
transport: TransportKind.ipc,
},
debug: {
module: serverModule,
args: ["--node-ipc"],
transport: TransportKind.ipc,
options: debugOptions,
},
};
// Options to control the language client
let clientOptions: LanguageClientOptions = {
documentSelector: [{ scheme: "file", language: "rescript" }],
// We'll send the initial configuration in here, but this might be
// problematic because every consumer of the LS will need to mimic this.
// We'll leave it like this for now, but might be worth revisiting later on.
initializationOptions: {
extensionConfiguration: workspace.getConfiguration("rescript.settings"),
// Keep this in sync with the `extensionClientCapabilities` type in the
// server.
extensionClientCapabilities: {
supportsMarkdownLinks: true,
},
},
outputChannel,
markdown: {
isTrusted: true,
},
};
const client = new LanguageClient(
"ReScriptLSP",
"ReScript Language Server",
serverOptions,
clientOptions
);
// This sets up a listener that, if we're in code analysis mode, triggers
// code analysis as the LS server reports that ReScript compilation has
// finished. This is needed because code analysis must wait until
// compilation has finished, and the most reliable source for that is the LS
// server, that already keeps track of when the compiler finishes in order to
// other provide fresh diagnostics.
context.subscriptions.push(
client.onDidChangeState(({ newState }) => {
if (newState === State.Running) {
context.subscriptions.push(
client.onNotification("rescript/compilationFinished", () => {
if (inCodeAnalysisState.active === true) {
customCommands.codeAnalysisWithReanalyze(
inCodeAnalysisState.activatedFromDirectory,
diagnosticsCollection,
diagnosticsResultCodeActions,
outputChannel,
codeAnalysisRunningStatusBarItem
);
}
})
);
}
})
);
return client;
}
// Create the language client and start the client.
client = createLanguageClient();
// Create a custom diagnostics collection, for cases where we want to report
// diagnostics programatically from inside of the extension. The reason this
// is separate from the diagnostics provided by the LS server itself is that
// this should be possible to clear independently of the other diagnostics
// coming from the ReScript compiler.
let diagnosticsCollection = languages.createDiagnosticCollection("rescript");
// This map will hold code actions produced by the code analysis, in a
// format that's cheap to look up.
let diagnosticsResultCodeActions: DiagnosticsResultCodeActionsMap = new Map();
let codeAnalysisRunningStatusBarItem = window.createStatusBarItem(
StatusBarAlignment.Right
);
let debugDumpStatusBarItem = window.createStatusBarItem(
StatusBarAlignment.Right
);
let inCodeAnalysisState: {
active: boolean;
activatedFromDirectory: string | null;
} = { active: false, activatedFromDirectory: null };
// This code actions provider yields the code actions potentially extracted
// from the code analysis to the editor.
languages.registerCodeActionsProvider("rescript", {
async provideCodeActions(document, rangeOrSelection) {
let availableActions =
diagnosticsResultCodeActions.get(document.uri.fsPath) ?? [];
const allRemoveActionEdits = availableActions.filter(
({ codeAction }) => codeAction.title === "Remove unused"
);
const actions: CodeAction[] = availableActions
.filter(
({ range }) =>
range.contains(rangeOrSelection) || range.isEqual(rangeOrSelection)
)
.map(({ codeAction }) => codeAction);
if (allRemoveActionEdits.length > 0) {
const removeAllCodeAction = new CodeAction("Remove all unused in file");
const edit = new WorkspaceEdit();
allRemoveActionEdits.forEach((subEdit) => {
subEdit.codeAction.edit.entries().forEach(([uri, [textEdit]]) => {
edit.replace(uri, textEdit.range, textEdit.newText);
});
});
removeAllCodeAction.kind = CodeActionKind.RefactorRewrite;
removeAllCodeAction.edit = edit;
actions.push(removeAllCodeAction);
}
return actions;
},
});
// Register custom commands
commands.registerCommand("rescript-vscode.create_interface", () => {
customCommands.createInterface(client);
});
commands.registerCommand(
"rescript-vscode.clear_diagnostic",
(diagnostic: Diagnostic) => {
const editor = window.activeTextEditor;
if (!editor) {
return;
}
const document = editor.document;
const diagnostics = diagnosticsCollection.get(document.uri);
const newDiagnostics = diagnostics.filter((d) => d !== diagnostic);
diagnosticsCollection.set(document.uri, newDiagnostics);
}
);
commands.registerCommand("rescript-vscode.open_compiled", () => {
customCommands.openCompiled(client);
});
commands.registerCommand("rescript-vscode.debug-dump-start", () => {
customCommands.dumpDebug(context, debugDumpStatusBarItem);
});
commands.registerCommand("rescript-vscode.debug-dump-retrigger", () => {
customCommands.dumpDebugRetrigger();
});
commands.registerCommand(
"rescript-vscode.go_to_location",
async (fileUri: string, startLine: number, startCol: number) => {
await window.showTextDocument(Uri.parse(fileUri), {
selection: new Range(
new Position(startLine, startCol),
new Position(startLine, startCol)
),
});
}
);
// Starts the code analysis mode.
commands.registerCommand("rescript-vscode.start_code_analysis", () => {
// Save the directory this first ran from, and re-use that when continuously
// running the analysis. This is so that the target of the analysis does not
// change on subsequent runs, if there are multiple ReScript projects open
// in the editor.
let currentDocument = window.activeTextEditor.document;
inCodeAnalysisState.active = true;
// Pointing reanalyze to the dir of the current file path is fine, because
// reanalyze will walk upwards looking for a bsconfig.json in order to find
// the correct project root.
inCodeAnalysisState.activatedFromDirectory = path.dirname(
currentDocument.uri.fsPath
);
codeAnalysisRunningStatusBarItem.command =
"rescript-vscode.stop_code_analysis";
codeAnalysisRunningStatusBarItem.show();
statusBarItem.setToStopText(codeAnalysisRunningStatusBarItem);
customCommands.codeAnalysisWithReanalyze(
inCodeAnalysisState.activatedFromDirectory,
diagnosticsCollection,
diagnosticsResultCodeActions,
outputChannel,
codeAnalysisRunningStatusBarItem
);
});
commands.registerCommand("rescript-vscode.stop_code_analysis", () => {
inCodeAnalysisState.active = false;
inCodeAnalysisState.activatedFromDirectory = null;
diagnosticsCollection.clear();
diagnosticsResultCodeActions.clear();
codeAnalysisRunningStatusBarItem.hide();
});
commands.registerCommand("rescript-vscode.switch-impl-intf", () => {
customCommands.switchImplIntf(client);
});
commands.registerCommand("rescript-vscode.restart_language_server", () => {
client.stop().then(() => {
client = createLanguageClient();
client.start();
});
});
// Start the client. This will also launch the server
client.start();
// Restart the language client automatically when certain configuration
// changes. These are typically settings that affect the capabilities of the
// language client, and because of that requires a full restart.
context.subscriptions.push(
workspace.onDidChangeConfiguration(({ affectsConfiguration }) => {
// Put any configuration that, when changed, requires a full restart of
// the server here. That will typically be any configuration that affects
// the capabilities declared by the server, since those cannot be updated
// on the fly, and require a full restart with new capabilities set when
// initializing.
if (
affectsConfiguration("rescript.settings.inlayHints") ||
affectsConfiguration("rescript.settings.codeLens") ||
affectsConfiguration("rescript.settings.signatureHelp") ||
affectsConfiguration("rescript.settings.incrementalTypechecking") ||
affectsConfiguration("rescript.settings.cache")
) {
commands.executeCommand("rescript-vscode.restart_language_server");
} else {
// Send a general message that configuration has updated. Clients
// interested can then pull the new configuration as they see fit.
client
.sendNotification("workspace/didChangeConfiguration")
.catch((err) => {
window.showErrorMessage(String(err));
});
}
})
);
}
export function deactivate(): Thenable<void> | undefined {
if (!client) {
return undefined;
}
return client.stop();
}