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;
Source code
Dependencies
import util::LanguageServer;
import util::IDEServices;
import ParseTree;
import util::Reflective;
import lang::pico::\syntax::Main;
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.
function picoLanguageServer
A language server is simply a set of Language Services.
set[LanguageService] picoLanguageServer() = {
parsing(parser(#start[Program]), usesSpecialCaseHighlighting = false),
documentSymbol(picoDocumentSymbolService),
codeLens(picoCodeLenseService),
execution(picoExecutionService),
inlayHint(picoInlayHintService),
definition(picoDefinitionService),
codeAction(picoCodeActionService)
};
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 picoLanguageServerSlowSummary
This set of contributions runs slower but provides more detail.
set[LanguageService] picoLanguageServerSlowSummary() = {
parsing(parser(#start[Program]), usesSpecialCaseHighlighting = false),
analysis(picoAnalysisService, providesImplementations = false),
build(picoBuildService)
};
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 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>", \variable(), var.src) | /IdType var := input]
])];
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 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
rel[str, loc] defs = {<"<var.id>", var.src> | /IdType var := input};
// uses of identifiers
rel[loc, str] uses = {<id.src, "<id>"> | /Id id := input};
// 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)> | <id, src> <- defs, 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 main
The main function registers the Pico language with the IDE.
void main() {
registerLanguage(
language(
pathConfig(),
"Pico",
{"pico", "pico-new"},
"demo::lang::pico::LanguageServer",
"picoLanguageServer"
)
);
registerLanguage(
language(
pathConfig(),
"Pico",
{"pico", "pico-new"},
"demo::lang::pico::LanguageServer",
"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
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.