Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implemented error recovery support in parametric language servers #511

Open
wants to merge 8 commits into
base: error-recovery/rascal
Choose a base branch
from
Original file line number Diff line number Diff line change
Expand Up @@ -93,8 +93,6 @@
import org.eclipse.lsp4j.jsonrpc.messages.ResponseErrorCode;
import org.eclipse.lsp4j.services.LanguageClient;
import org.eclipse.lsp4j.services.LanguageClientAware;
import org.rascalmpl.exceptions.Throw;
import org.rascalmpl.parser.gtd.exception.ParseError;
import org.rascalmpl.uri.URIResolverRegistry;
import org.rascalmpl.values.IRascalValueFactory;
import org.rascalmpl.values.parsetrees.ITree;
Expand All @@ -107,7 +105,6 @@
import org.rascalmpl.vscode.lsp.parametric.model.ParametricSummary.SummaryLookup;
import org.rascalmpl.vscode.lsp.terminal.ITerminalIDEServer.LanguageParameter;
import org.rascalmpl.vscode.lsp.util.CodeActions;
import org.rascalmpl.vscode.lsp.util.Diagnostics;
import org.rascalmpl.vscode.lsp.util.FoldingRanges;
import org.rascalmpl.vscode.lsp.util.DocumentSymbols;
import org.rascalmpl.vscode.lsp.util.SemanticTokenizer;
Expand Down Expand Up @@ -227,7 +224,8 @@ public void connect(LanguageClient client) {
@Override
public void didOpen(DidOpenTextDocumentParams params) {
logger.debug("Did Open file: {}", params.getTextDocument());
handleParsingErrors(open(params.getTextDocument()));
TextDocumentState file = open(params.getTextDocument());
handleParsingErrors(file, file.getCurrentDiagnosticsAsync()); // No debounce
triggerAnalyzer(params.getTextDocument(), Duration.ofMillis(800));
}

Expand Down Expand Up @@ -279,37 +277,15 @@ private TextDocumentState updateContents(VersionedTextDocumentIdentifier doc, St
TextDocumentState file = getFile(doc);
logger.trace("New contents for {}", doc);
file.update(doc.getVersion(), newContents);
handleParsingErrors(file, file.getCurrentTreeAsync()); // Warning: Might be a later version (when a concurrent update happened)
handleParsingErrors(file, file.getCurrentDiagnosticsAsync(Duration.ofMillis(800))); // Warning: Might be a later version (when a concurrent update happened)
return file;
}

private void handleParsingErrors(TextDocumentState file, CompletableFuture<Versioned<ITree>> futureTree) {
var version = file.getCurrentContent().version();
futureTree.handle((tree, excp) -> {
Diagnostic newParseError = null;
if (excp instanceof CompletionException) {
excp = excp.getCause();
}

if (excp instanceof Throw) {
Throw thrown = (Throw) excp;
newParseError = Diagnostics.translateRascalParseError(thrown.getException(), columns);
}
else if (excp instanceof ParseError) {
newParseError = Diagnostics.translateDiagnostic((ParseError)excp, columns);
}
else if (excp != null) {
logger.error("Parsing crashed", excp);
newParseError = new Diagnostic(
new Range(new Position(0,0), new Position(0,1)),
"Parsing failed: " + excp.getMessage(),
DiagnosticSeverity.Error,
"Rascal Parser");
}
logger.trace("Finished parsing tree, reporting new parse error: {} for: {}", newParseError, file.getLocation());
facts(file.getLocation()).reportParseErrors(file.getLocation(), version,
newParseError == null ? Collections.emptyList() : Collections.singletonList(newParseError));
return null;
private void handleParsingErrors(TextDocumentState file, CompletableFuture<Versioned<List<Diagnostic>>> diagnosticsAsync) {
diagnosticsAsync.thenAccept(diagnostics -> {
List<Diagnostic> parseErrors = diagnostics.get();
logger.trace("Finished parsing tree, reporting new parse errors: {} for: {}", parseErrors, file.getLocation());
facts(file.getLocation()).reportParseErrors(file.getLocation(), diagnostics.version(), parseErrors);
});
}

Expand Down Expand Up @@ -382,12 +358,6 @@ private CodeLens locCommandTupleToCodeLense(String languageName, IValue v) {
return new CodeLens(Locations.toRange(loc, columns), CodeActions.constructorToCommand(dedicatedLanguageName, languageName, command), null);
}



private void handleParsingErrors(TextDocumentState file) {
handleParsingErrors(file, file.getCurrentTreeAsync());
}

private static <T> T last(List<T> l) {
return l.get(l.size() - 1);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -319,20 +319,25 @@ private void sendDiagnostics() {
logger.debug("Cannot send diagnostics since the client hasn't been registered yet");
return;
}
var messages = Lists.union(
unwrap(parserDiagnostics),
unwrap(analyzerDiagnostics),
unwrap(builderDiagnostics));
logger.trace("Sending diagnostics for {}. {} messages", file, messages.size());

// Read the atomic references once for the whole method, to ensure
// that published diagnostics correspond with logged versions
var fromParser = parserDiagnostics.get();
var fromAnalyzer = analyzerDiagnostics.get();
var fromBuilder = builderDiagnostics.get();

var diagnostics = Lists.union(
fromParser.get(),
fromAnalyzer.get(),
fromBuilder.get());

logger.trace(
"Sending {} diagnostic(s) for {} (parser: v{}; analyzer: v{}; builder: v{})",
diagnostics.size(), file, fromParser.version(), fromAnalyzer.version(), fromBuilder.version());

client.publishDiagnostics(new PublishDiagnosticsParams(
file.getURI().toString(),
messages));
}

private List<Diagnostic> unwrap(AtomicReference<Versioned<List<Diagnostic>>> wrappedDiagnostics) {
return wrappedDiagnostics
.get() // Unwrap `AtomicReference`
.get(); // Unwrap `Versioned`
diagnostics));
}

/**
Expand Down
9 changes: 5 additions & 4 deletions rascal-lsp/src/main/rascal/demo/lang/pico/LanguageServer.rsc
PieterOlivier marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ module demo::lang::pico::LanguageServer
import util::LanguageServer;
import util::IDEServices;
import ParseTree;
import util::ErrorRecovery;
import util::Reflective;
import lang::pico::\syntax::Main;

Expand All @@ -46,7 +47,7 @@ Here we group all services such that the LSP server can link them
with the ((Language)) definition later.
}
set[LanguageService] picoLanguageServer() = {
parsing(parser(#start[Program]), usesSpecialCaseHighlighting = false),
parsing(parser(#start[Program], allowRecovery=true, allowAmbiguity=false), usesSpecialCasingHighlighting = false),
documentSymbol(picoDocumentSymbolService),
codeLens(picoCodeLenseService),
execution(picoExecutionService),
Expand All @@ -62,7 +63,7 @@ such that quicky loaded features can be made available while slower to load
tools come in later.
}
set[LanguageService] picoLanguageServerSlowSummary() = {
parsing(parser(#start[Program]), usesSpecialCaseHighlighting = false),
parsing(parser(#start[Program], allowRecovery=true, allowAmbiguity=false), usesSpecialCaseHighlighting = false),
analysis(picoAnalysisService, providesImplementations = false),
build(picoBuildService)
};
Expand All @@ -74,7 +75,7 @@ symbol search in the editor.
}
list[DocumentSymbol] picoDocumentSymbolService(start[Program] input)
= [symbol("<input.src>", DocumentSymbolKind::\file(), input.src, children=[
*[symbol("<var.id>", \variable(), var.src) | /IdType var := input]
*[symbol("<var.id>", \variable(), var.src) | /IdType var := input, !hasErrors(var)]
])];

@synopsis{The analyzer maps pico syntax trees to error messages and references}
Expand All @@ -94,7 +95,7 @@ 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};
rel[str, loc] defs = {<"<var.id>", var.src> | /IdType var := input, !hasErrors(var)};

// uses of identifiers
rel[loc, str] uses = {<id.src, "<id>"> | /Id id := input};
Expand Down
Loading