Skip to main content

module demo::lang::pico::LanguageServer

rascal-0.40.17
rascal-lsp-2.21.0-2

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

http://github.com/usethesource/rascal-language-servers/blob/main/rascal-lsp/src/main/rascal/demo/lang/pico/LanguageServer.rsc

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:

  1. first for fast and cheap contributions
  2. 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.