module demo::lang::pico::LanguageServer
Demonstrates the API for defining and registering IDE language services for Programming Languages and Domain Specific Languages.
Usage
import demo::lang::pico::LanguageServer;
Dependencies
import util::LanguageServer;
import util::IDEServices;
import ParseTree;
import util::ParseErrorRecovery;
import util::Reflective;
extend lang::pico::\syntax::Main;
import DateTime;
import Location;
Description
The core functionality of this module is built upon these concepts:
- Register Language for enabling your language services for a given file extension in the current IDE.
- Language is the data-type for defining a language, with meta-data for starting a new LSP server.
- A Language Service is a specific feature for an IDE. Each service comes with one Rascal function that implements it.
syntax IdType
syntax IdType
= function: Id id "(" {IdType ","}* args ")" ":" Type retType ":=" Expression body
;
syntax Expression
syntax Expression
= call: Id id "(" {Expression ","}* args ")"
;
function picoParser
private Tree (str _input, loc _origin) picoParser(bool allowRecovery) {
return ParseTree::parser(#start[Program], allowRecovery=allowRecovery, filters=allowRecovery ? {createParseErrorFilter(false)} : {});
}
function picoLanguageServer
A language server is simply a set of ((LanguageService))s.
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)
};
set[LanguageService] picoLanguageServer() = picoLanguageServer(false);
Each Language Service for pico is implemented as a function. Here we group all services such that the LSP server can link them with the Language definition later.
function picoLanguageServerWithRecovery
set[LanguageService] picoLanguageServerWithRecovery() = picoLanguageServer(true);
function picoLanguageServerSlowSummary
This set of contributions runs slower but provides more detail.
set[LanguageService] picoLanguageServerSlowSummary(bool allowRecovery) = {
parsing(picoParser(allowRecovery), usesSpecialCaseHighlighting = false),
analysis(picoAnalysisService, providesImplementations = false),
build(picoBuildService)
};
set[LanguageService] picoLanguageServerSlowSummary() = picoLanguageServerSlowSummary(false);
Language Services can be registered asynchronously and incrementally, such that quicky loaded features can be made available while slower to load tools come in later.
function picoLanguageServerSlowSummaryWithRecovery
set[LanguageService] picoLanguageServerSlowSummaryWithRecovery() = picoLanguageServerSlowSummary(true);
function picoDocumentSymbolService
The documentSymbol service maps pico syntax trees to lists of DocumentSymbols.
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?]
])];
Here we list the symbols we want in the outline view, and which can be searched using symbol search in the editor.
function picoAnalysisService
The analyzer maps pico syntax trees to error messages and references
Summary picoAnalysisService(loc l, start[Program] input) = picoSummaryService(l, input, analyze());
function picoBuildService
The builder does a more thorough analysis then the analyzer, providing more detail
Summary picoBuildService(loc l, start[Program] input) = picoSummaryService(l, input, build());
data PicoSummarizerMode
A simple "enum" data type for switching between analysis modes
data PicoSummarizerMode
= analyze()
| build()
;
function findDefinitions
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
data Summary (rel[DocumentSymbolKind, loc, Id, str] definitionsByKind = {})
function picoSummaryService
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;
}
function picoDefinitionService
Looks up the declaration for any variable use using a list match into a ((Focus))
set[loc] picoDefinitionService([*_, Id use, *_, start[Program] input]) = { def.src | /IdType def := input, use := def.id};
Pitfalls
This demo actually finds the declaration rather than the definition of a variable in Pico.
function prepareNotDefinedFixes
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)
= [action(title="Change to <existing<0>>", edits=[changed(src.top, [replace(src, existing<0>)])]) | existing <- defs];
function picoCodeActionService
Finds a declaration that the cursor is on and proposes to remove it.
list[CodeAction] picoCodeActionService([*_, IdType x, *_, start[Program] program])
= [action(command=removeDecl(program, x, title="remove <x>"))];
default list[CodeAction] picoCodeActionService(Focus _focus) = [];
data Command
data Command
= renameAtoB(start[Program] program)
| removeDecl(start[Program] program, IdType toBeRemoved)
;
function picoCodeLenseService
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.")>];
function picoInlayHintService
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
];
}
function getAtoBEdits
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])];
function picoExecutionService
Command handler for the renameAtoB command
value picoExecutionService(renameAtoB(start[Program] input)) {
applyDocumentsEdits(getAtoBEdits(input));
return ("result": true);
}
function picoExecutionService
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);
}
function picoRenamePreparingService
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;
}
function picoRenamingService
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)}>;
function picoRenamingService
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
])], {}>;
function picoFileRenameService
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:///|)}>;
}
function picoSelectionRangeService
list[loc] picoSelectionRangeService(Focus focus)
= dup([t@\loc | t <- focus]);
function picoPrepareCallHierarchy
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 _) = [];
function callHierarchyItem
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 CallHierarchyData
= \data(start[Program] prog)
;
function typeOf
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>";
function picoCallsService
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;
}
function main
The main function registers the Pico language with the IDE
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"
)
);
}
Register the Pico language and the contributions that supply the IDE with features.
Register Language is called twice here:
- first for fast and cheap contributions
- 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 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.