Skip to main content

module demo::lang::pico::LanguageServer

rascal-0.41.2
org.rascalmpl.rascal-lsp-2.22.0

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:

  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 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.