-
Notifications
You must be signed in to change notification settings - Fork 14
Expand file tree
/
Copy pathLanguageServer.rsc
More file actions
382 lines (320 loc) · 16.2 KB
/
LanguageServer.rsc
File metadata and controls
382 lines (320 loc) · 16.2 KB
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
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
@license{
Copyright (c) 2018-2025, NWO-I CWI and Swat.engineering
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice,
this list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
POSSIBILITY OF SUCH DAMAGE.
}
@synopsis{Demonstrates the API for defining and registering IDE language services for Programming Languages and Domain Specific Languages.}
@description{
The core functionality of this module is built upon these concepts:
* ((registerLanguage)) for enabling your language services for a given file extension _in the current IDE_.
* ((LanguageServer-Language)) is the data-type for defining a language, with meta-data for starting a new LSP server.
* A ((LanguageService)) is a specific feature for an IDE. Each service comes with one Rascal function that implements it.
}
module demo::lang::pico::LanguageServer
import util::LanguageServer;
import util::IDEServices;
import ParseTree;
import util::ParseErrorRecovery;
import util::Reflective;
extend lang::pico::\syntax::Main;
import DateTime;
import IO;
import List;
import Location;
import String;
// We extend the grammar with functions and calls, so we can demo call hierarchy functionality.
// For most use-cases, one should not extend the grammar in the language server implementation
syntax IdType
= function: Id id "(" {IdType ","}* args ")" ":" Type retType ":=" Expression body
;
syntax Expression
= call: Id id "(" {Expression ","}* args ")"
;
private Tree (str _input, loc _origin) picoParser(bool allowRecovery) {
return ParseTree::parser(#start[Program], allowRecovery=allowRecovery, filters=allowRecovery ? {createParseErrorFilter(false)} : {});
}
@synopsis{A language server is simply a set of ((LanguageService))s.}
@description{
Each ((LanguageService)) for pico is implemented as a function.
Here we group all services such that the LSP server can link them
with the ((LanguageServer-Language)) definition later.
}
set[LanguageService] picoLanguageServer(bool allowRecovery) = {
parsing(picoParser(allowRecovery), usesSpecialCaseHighlighting = false),
documentSymbol(picoDocumentSymbolService),
codeLens(picoCodeLenseService),
execution(picoExecutionService),
inlayHint(picoInlayHintService),
definition(picoDefinitionService),
codeAction(picoCodeActionService),
rename(picoRenamingService, prepareRenameService = picoRenamePreparingService),
didRenameFiles(picoFileRenameService),
selectionRange(picoSelectionRangeService),
callHierarchy(picoPrepareCallHierarchy, picoCallsService),
completion(picoCompletionService, additionalTriggerCharacters = ["="])
};
set[LanguageService] picoLanguageServer() = picoLanguageServer(false);
set[LanguageService] picoLanguageServerWithRecovery() = picoLanguageServer(true);
@synopsis{This set of contributions runs slower but provides more detail.}
@description{
((LanguageService))s can be registered asynchronously and incrementally,
such that quicky loaded features can be made available while slower to load
tools come in later.
}
set[LanguageService] picoLanguageServerSlowSummary(bool allowRecovery) = {
parsing(picoParser(allowRecovery), usesSpecialCaseHighlighting = false),
analysis(picoAnalysisService, providesImplementations = false),
build(picoBuildService)
};
set[LanguageService] picoLanguageServerSlowSummary() = picoLanguageServerSlowSummary(false);
set[LanguageService] picoLanguageServerSlowSummaryWithRecovery() = picoLanguageServerSlowSummary(true);
@synopsis{The documentSymbol service maps pico syntax trees to lists of DocumentSymbols.}
@description{
Here we list the symbols we want in the outline view, and which can be searched using
symbol search in the editor.
}
list[DocumentSymbol] picoDocumentSymbolService(start[Program] input)
= [symbol("<input.src>", DocumentSymbolKind::\file(), input.src, children=[
*[symbol("<var.id>", var is function ? \function() : \variable(), var.src) | /IdType var := input, var.id?]
])];
@synopsis{The analyzer maps pico syntax trees to error messages and references}
Summary picoAnalysisService(loc l, start[Program] input) = picoSummaryService(l, input, analyze());
@synopsis{The builder does a more thorough analysis then the analyzer, providing more detail}
Summary picoBuildService(loc l, start[Program] input) = picoSummaryService(l, input, build());
@synopsis{A simple "enum" data type for switching between analysis modes}
data PicoSummarizerMode
= analyze()
| build()
;
rel[DocumentSymbolKind, loc, Id, str] findDefinitions(Tree input, bool funcScope = false) {
rel[DocumentSymbolKind, loc, Id, str] defs = {};
top-down-break visit (input) {
case var:(IdType) `<Id id>: <Type _>`: defs += <funcScope ? constant() : variable(), var.src, id, typeOf(var)>;
case func:(IdType) `<Id id>(<{IdType ","}* args>): <Type _> := <Expression _>`: {
defs += <function(), func.src, id, typeOf(func)>;
defs += findDefinitions(args, funcScope = true);
}
}
return defs;
}
data Summary(rel[DocumentSymbolKind, loc, Id, str] definitionsByKind = {});
@synopsis{Translates a pico syntax tree to a model (Summary) of everything we need to know about the program in the IDE.}
Summary picoSummaryService(loc l, start[Program] input, PicoSummarizerMode mode) {
Summary s = summary(l);
// definitions of variables
s.definitionsByKind = findDefinitions(input);
rel[str, loc] defs = {<"<id>", d> | <d, id> <- s.definitionsByKind<1, 2>};
// uses of identifiers
rel[loc, str] uses = {<id.src, "<id>"> | /Id id := input, id notin s.definitionsByKind<2>};
// documentation strings for identifier uses
rel[loc, str] docs = {<var.src, "*variable* <var>"> | /IdType var := input};
// Provide errors (cheap to compute) both in analyze mode and in build mode.
s.messages += {<src, error("<id> is not defined", src, fixes=prepareNotDefinedFixes(src, defs))>
| <src, id> <- uses, id notin defs<0>};
// "references" are links for loc to loc (from def to use)
s.references += (uses o defs)<1,0>;
// "definitions" are also links from loc to loc (from use to def)
s.definitions += uses o defs;
// "documentation" maps locations to strs
s.documentation += (uses o defs) o docs;
// Provide warnings (expensive to compute) only in build mode
if (build() := mode) {
rel[loc, str] asgn = {<id.src, "<id>"> | /Statement stmt := input, (Statement) `<Id id> := <Expression _>` := stmt};
s.messages += {<src, warning("<id> is not assigned", src)> | <src, id, _> <- s.definitionsByKind[variable()], "<id>" notin asgn<1>};
}
return s;
}
@synopsis{Looks up the declaration for any variable use using a list match into a ((Focus))}
@pitfalls{
This demo actually finds the declaration rather than the definition of a variable in Pico.
}
set[loc] picoDefinitionService([*_, Id use, *_, start[Program] input]) = { def.src | /IdType def := input, use := def.id};
@synopsis{If a variable is not defined, we list a fix of fixes to replace it with a defined variable instead.}
list[CodeAction] prepareNotDefinedFixes(loc src, rel[str, loc] defs)
= [CodeAction::action(title="Change to <existing<0>>", edits=[changed(src.top, [replace(src, existing<0>)])]) | existing <- defs];
@synopsis{Finds a declaration that the cursor is on and proposes to remove it.}
list[CodeAction] picoCodeActionService([*_, IdType x, *_, start[Program] program])
= [CodeAction::action(command=removeDecl(program, x, title="remove <x>"))];
default list[CodeAction] picoCodeActionService(Focus _focus) = [];
@synsopsis{Defines example commands that can be triggered by the user (from a code lens, from a diagnostic, or just from the cursor position)}
data Command
= renameAtoB(start[Program] program)
| removeDecl(start[Program] program, IdType toBeRemoved)
| testValueEncoding()
;
@synopsis{Adds an example lense to the entire program.}
lrel[loc,Command] picoCodeLenseService(start[Program] input)
= [<input@\loc, renameAtoB(input, title="Rename variables a to b.")>];
@synopsis{Generates inlay hints that explain the type of each variable usage.}
list[InlayHint] picoInlayHintService(start[Program] input) {
typeLookup = ( "<name>" : "<tp>" | /(IdType)`<Id name> : <Type tp>` := input);
return [
hint(name.src, " : <typeLookup["<name>"]>", \type(), atEnd = true)
| /(Expression)`<Id name>` := input
, "<name>" in typeLookup
];
}
@synopsis{Helper function to generate actual edit actions for the renameAtoB command}
list[DocumentEdit] getAtoBEdits(start[Program] input)
= [changed(input@\loc.top, [replace(id@\loc, "b") | /id:(Id) `a` := input])];
@synopsis{Command handler for the renameAtoB command}
value picoExecutionService(renameAtoB(start[Program] input)) {
applyDocumentsEdits(getAtoBEdits(input));
return ("result": true);
}
@synopsis{Command handler to test JSON serialization of various Rascal value types.}
value picoExecutionService(testValueEncoding()) = (
"result": [ // list
("a": true), // map, str, bool
{8, 1r2, 3.14, 10e3}, // set, int, rat, real
char(0), // ADT constructor
reposition(parse(#IdType, "x: string"), file = |test:///expectation|), // Tree
|memory://authority/file.ext|, // loc
$2026-03-19T11:55:54.121+0100$, // datetime
<[1..3], #int> // tuple, range, reified type
]
);
@synopsis{Command handler for the removeDecl command}
value picoExecutionService(removeDecl(start[Program] program, IdType toBeRemoved)) {
applyDocumentsEdits([changed(program@\loc.top, [replace(toBeRemoved@\loc, "")])]);
return ("result": true);
}
@synopsis{Prepares the rename service by checking if the id can be renamed}
loc picoRenamePreparingService(Focus _:[Id id, *_]) {
if ("<id>" == "fixed") {
throw "Cannot rename id <id>";
}
return id.src;
}
@synopsis{Renaming service implementation, unhappy flow.}
tuple[list[DocumentEdit], set[Message]] picoRenamingService(Focus focus, "error") = <[], {error("Test of error detection during renaming.", focus[0].src.top)}>;
@synopsis{Renaming service implementation, happy flow.}
default tuple[list[DocumentEdit], set[Message]] picoRenamingService(Focus focus, str newName) = <[changed(focus[0].src.top, [
replace(id.src, newName)
| cursor := focus[0]
, /Id id := focus[-1]
, id := cursor
])], {}>;
@synposis{Handle renames of files in the IDE.}
tuple[list[DocumentEdit],set[Message]] picoFileRenameService(list[DocumentEdit] fileRenames) {
// Iterate over fileRenames
list[DocumentEdit] edits = [];
for (renamed(loc from, loc to) <- fileRenames) {
// Surely there is a better way to do this?
toBegin = to[offset=0][length=0][begin=<1,0>][end=<1,0>];
edits = edits + changed(to, [insertBefore(toBegin, "%% File moved from <from> to <to> at <now()>\n", separator="")]);
}
return <edits, {info("<size(edits)> moves succeeded!", |unknown:///|)}>;
}
list[loc] picoSelectionRangeService(Focus focus)
= dup([t@\loc | t <- focus]);
list[CallHierarchyItem] picoPrepareCallHierarchy(Focus focus: [*_, e:(Expression) `<Id callId>(<{Expression ","}* _>)`, *_, start[Program] prog]) {
s = picoSummaryService(prog.src.top, prog, analyze());
return [ callHierarchyItem(prog, id, d, tp)
| d <- s.definitions[callId.src]
, <id, tp> <- s.definitionsByKind[function(), d]
];
}
list[CallHierarchyItem] picoPrepareCallHierarchy(Focus _: [*_, d:(IdType) `<Id _>(<{IdType ","}* _>): <Type _> := <Expression _>`, *_, start[Program] prog])
= [callHierarchyItem(prog, d)];
default list[CallHierarchyItem] picoPrepareCallHierarchy(Focus _) = [];
CallHierarchyItem callHierarchyItem(start[Program] prog, Id id, loc decl, str tp)
= callHierarchyItem("<id>", function(), decl, id.src, detail = tp, \data = \data(prog));
CallHierarchyItem callHierarchyItem(start[Program] prog, d:(IdType) `<Id id>(<{IdType ","}* _>): <Type _> := <Expression _>`)
= callHierarchyItem("<id>", function(), d.src, id.src, detail = typeOf(d), \data = \data(prog));
data CallHierarchyData = \data(start[Program] prog);
str typeOf((IdType) `<Id _>: <Type t>`) = "<t>";
str typeOf((IdType) `<Id id>(<{IdType ","}* args>): <Type retType> := <Expression body>`)
= "<id>(<intercalate(", ", [typeOf(a) | a <- args])>): <retType>";
lrel[CallHierarchyItem, loc] picoCallsService(CallHierarchyItem ci, CallDirection dir) {
s = picoSummaryService(ci.\data.prog.src.top, ci.\data.prog, analyze());
calls = [];
for (<d, id, t> <- s.definitionsByKind[function()]) {
newItem = callHierarchyItem(ci.\data.prog, id, d, t);
<caller, callee> = dir is incoming
? <newItem, ci>
: <ci, newItem>
;
for (use <- s.references[callee.src], isContainedIn(use, caller.src)) {
calls += <newItem, use>;
}
};
return calls;
}
list[CompletionItem] picoCompletionService(Focus focus, int cursorOffset, CompletionTrigger trigger) {
t = focus[0];
str prefix = "<t>"[..cursorOffset];
cc = t.src.begin.column + cursorOffset;
list[CompletionItem] items = [];
isTypingId = false;
try {
if (prefix != "" && trim(prefix) == prefix) {
parse(#Id, prefix);
isTypingId = true;
}
} catch ParseError(_): {;}
top-down-break visit (focus[-1]) {
case IdType def: {
name = "<def.id>";
if (!isTypingId || startsWith(name, prefix)) {
e = isTypingId && !trigger is character
? completionEdit(t.src.begin.column, cc, t.src.end.column, name)
: completionEdit(cc, cc, cc, name);
items += completionItem(def is function ? function() : variable(), e, name, labelDetail = ": <typeOf(def)>");
}
}
}
return sort(items, bool(CompletionItem i1, CompletionItem i2) {return i1.label < i2.label; });
}
@synopsis{The main function registers the Pico language with the IDE}
@description{
Register the Pico language and the contributions that supply the IDE with features.
((registerLanguage)) is called twice here:
1. first for fast and cheap contributions
2. asynchronously for the full monty that loads slower
The `errorRecovery` parameter can be set to `true` to enable error recovery in the parser.
When enabled, all the contributions in this file will mostly work when parse errors are
present in the input because the contributions are written to be robust
in the presence of error trees. See ((util::LanguageServer)) for more details.
}
@benefits{
* You can run each contribution on an example in the terminal to test it first.
Any feedback (errors and exceptions) is faster and more clearly printed in the terminal.
}
void main(bool errorRecovery=false) {
registerLanguage(
language(
pathConfig(),
"Pico",
{"pico", "pico-new"},
"demo::lang::pico::LanguageServer",
errorRecovery ? "picoLanguageServerWithRecovery" : "picoLanguageServer"
)
);
registerLanguage(
language(
pathConfig(),
"Pico",
{"pico", "pico-new"},
"demo::lang::pico::LanguageServer",
errorRecovery ? "picoLanguageServerSlowSummaryWithRecovery" : "picoLanguageServerSlowSummary"
)
);
}