From c5a72f922db6c252be0f7ec09339ade079060fb4 Mon Sep 17 00:00:00 2001 From: Toine Hartman Date: Wed, 29 Apr 2026 13:53:12 +0200 Subject: [PATCH 01/15] Factor out text document management. --- .../vscode/lsp/TextDocumentStateManager.java | 145 ++++++++++++++++++ .../ParametricTextDocumentService.java | 119 +++++--------- .../lsp/rascal/RascalTextDocumentService.java | 96 +++--------- 3 files changed, 204 insertions(+), 156 deletions(-) create mode 100644 rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/TextDocumentStateManager.java diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/TextDocumentStateManager.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/TextDocumentStateManager.java new file mode 100644 index 000000000..db5a398fb --- /dev/null +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/TextDocumentStateManager.java @@ -0,0 +1,145 @@ +/* + * Copyright (c) 2018-2025, NWO-I CWI and Swat.engineering + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ +package org.rascalmpl.vscode.lsp; + +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.Reader; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ExecutorService; +import java.util.function.BiFunction; +import org.apache.commons.io.IOUtils; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.checkerframework.checker.initialization.qual.UnknownInitialization; +import org.checkerframework.checker.nullness.qual.KeyFor; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.eclipse.lsp4j.TextDocumentItem; +import org.eclipse.lsp4j.jsonrpc.messages.ResponseError; +import org.eclipse.lsp4j.jsonrpc.messages.ResponseErrorCode; +import org.rascalmpl.uri.URIResolverRegistry; +import org.rascalmpl.util.locations.ColumnMaps; +import org.rascalmpl.util.locations.LineColumnOffsetMap; +import org.rascalmpl.values.parsetrees.ITree; +import org.rascalmpl.vscode.lsp.util.locations.Locations; + +import io.usethesource.vallang.ISourceLocation; + +/** + * Management of {{@link TextDocumentState}} associated with locations. + * + * + */ +public class TextDocumentStateManager { + + private static final Logger logger = LogManager.getLogger(TextDocumentStateManager.class); + + private final Map files = new ConcurrentHashMap<>(); + private final ColumnMaps columns; + + public TextDocumentStateManager() { + this.columns = new ColumnMaps(l -> getContents(l)); + } + + public String getContents(@UnknownInitialization TextDocumentStateManager this, ISourceLocation file) { + file = file.top(); + try { + return getFile(file).getCurrentContent().get(); + } catch (FileNotFoundException ignored) {} + + try (Reader src = URIResolverRegistry.getInstance().getCharacterReader(file)) { + return IOUtils.toString(src); + } + catch (IOException e) { + logger.error("Error opening file {} to get contents", file, e); + return ""; + } + } + + public ColumnMaps getColumnMaps() { + return columns; + } + + public boolean isManagingFile(ISourceLocation loc) { + return files.containsKey(loc.top()); + } + + public @Nullable TextDocumentState getDocumentState(ISourceLocation file) { + return files.get(file.top()); + } + + public LineColumnOffsetMap getColumnMap(ISourceLocation loc) { + return columns.get(loc.top()); + } + + protected TextDocumentState getFile(@UnknownInitialization TextDocumentStateManager this, ISourceLocation loc) throws FileNotFoundException { + loc = loc.top(); + TextDocumentState file = files.get(loc); + if (file == null) { + throw new FileNotFoundException(String.format("Unknown file: {}", loc)); + } + return file; + } + + protected TextDocumentState openFile(TextDocumentItem doc, BiFunction> parser, long timestamp, ExecutorService exec) { + return files.computeIfAbsent(Locations.toLoc(doc), + l -> new TextDocumentState(parser, l, doc.getVersion(), doc.getText(), timestamp, exec)); + } + + protected void updateFile(ISourceLocation loc) { + columns.clear(loc.top()); + } + + protected boolean removeFile(ISourceLocation loc) { + updateFile(loc); + return files.remove(loc.top()) != null; + } + + protected @Nullable TextDocumentState updateFileState(ISourceLocation f, BiFunction> parser) { + f = f.top(); + logger.trace("Updating state: {}", f); + + // Since we cannot know what happened to this file before we were called, we need to be careful about races. + // It might have been closed in the meantime, so we compute the new value if the key still exists, based on the current value. + var state = files.computeIfPresent(f, (loc, currentState) -> currentState.changeParser(parser)); + if (state == null) { + logger.debug("Updating the parser of {} failed, since it was closed.", f); + } + return state; + } + + protected Set<@KeyFor("this.files") ISourceLocation> getOpenFiles() { + return files.keySet(); + } + + protected static ResponseError unknownFileError(ISourceLocation loc, @Nullable Object data) { + return new ResponseError(ResponseErrorCode.RequestFailed, "Unknown file: " + loc, data); + } +} diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/ParametricTextDocumentService.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/ParametricTextDocumentService.java index 97e85b6e6..f35859588 100644 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/ParametricTextDocumentService.java +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/ParametricTextDocumentService.java @@ -26,8 +26,7 @@ */ package org.rascalmpl.vscode.lsp.parametric; -import java.io.IOException; -import java.io.Reader; +import java.io.FileNotFoundException; import java.time.Duration; import java.util.ArrayList; import java.util.Arrays; @@ -50,7 +49,7 @@ import org.apache.logging.log4j.Level; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; -import org.apache.logging.log4j.core.util.IOUtils; +import org.checkerframework.checker.initialization.qual.UnknownInitialization; import org.checkerframework.checker.nullness.qual.MonotonicNonNull; import org.checkerframework.checker.nullness.qual.Nullable; import org.eclipse.lsp4j.ApplyWorkspaceEditParams; @@ -129,8 +128,6 @@ import org.eclipse.lsp4j.services.LanguageClientAware; import org.rascalmpl.uri.URIResolverRegistry; import org.rascalmpl.uri.URIUtil; -import org.rascalmpl.util.locations.ColumnMaps; -import org.rascalmpl.util.locations.LineColumnOffsetMap; import org.rascalmpl.values.IRascalValueFactory; import org.rascalmpl.values.parsetrees.ITree; import org.rascalmpl.values.parsetrees.TreeAdapter; @@ -138,6 +135,7 @@ import org.rascalmpl.vscode.lsp.IBaseLanguageClient; import org.rascalmpl.vscode.lsp.IBaseTextDocumentService; import org.rascalmpl.vscode.lsp.TextDocumentState; +import org.rascalmpl.vscode.lsp.TextDocumentStateManager; import org.rascalmpl.vscode.lsp.parametric.LanguageRegistry.LanguageParameter; import org.rascalmpl.vscode.lsp.parametric.capabilities.CapabilityRegistration; import org.rascalmpl.vscode.lsp.parametric.capabilities.CompletionCapability; @@ -177,7 +175,7 @@ import io.usethesource.vallang.type.TypeFactory; import io.usethesource.vallang.type.TypeStore; -public class ParametricTextDocumentService implements IBaseTextDocumentService, LanguageClientAware { +public class ParametricTextDocumentService extends TextDocumentStateManager implements IBaseTextDocumentService, LanguageClientAware { private static final IValueFactory VF = IRascalValueFactory.getInstance(); private static final Logger logger = LogManager.getLogger(ParametricTextDocumentService.class); @@ -189,9 +187,6 @@ public class ParametricTextDocumentService implements IBaseTextDocumentService, private @MonotonicNonNull BaseWorkspaceService workspaceService; private @MonotonicNonNull CapabilityRegistration dynamicCapabilities; - private final Map files; - private final ColumnMaps columns; - /** extension to language */ private final Map registeredExtensions = new ConcurrentHashMap<>(); /** language to facts */ @@ -214,8 +209,6 @@ public ParametricTextDocumentService(ExecutorService exec, @Nullable LanguagePar URIResolverRegistry.getInstance(); this.exec = exec; - this.files = new ConcurrentHashMap<>(); - this.columns = new ColumnMaps(this::getContents); if (dedicatedLanguage == null) { this.dedicatedLanguageName = ""; this.dedicatedLanguage = null; @@ -227,31 +220,6 @@ public ParametricTextDocumentService(ExecutorService exec, @Nullable LanguagePar FallbackResolver.getInstance().registerTextDocumentService(this); } - @Override - public ColumnMaps getColumnMaps() { - return columns; - } - - @Override - public LineColumnOffsetMap getColumnMap(ISourceLocation file) { - return columns.get(file); - } - - public String getContents(ISourceLocation file) { - file = file.top(); - TextDocumentState ideState = files.get(file); - if (ideState != null) { - return ideState.getCurrentContent().get(); - } - try (Reader src = URIResolverRegistry.getInstance().getCharacterReader(file)) { - return IOUtils.toString(src); - } - catch (IOException e) { - logger.error("Error opening file {} to get contents", file, e); - return ""; - } - } - private CapabilityRegistration availableCapabilities() { if (dynamicCapabilities == null) { throw new IllegalStateException("Dynamic capabilities are `null` - the document service did not yet connect to a client."); @@ -359,7 +327,7 @@ public void didSave(DidSaveTextDocumentParams params) { public void didClose(DidCloseTextDocumentParams params) { logger.debug("Did Close file: {}", params.getTextDocument()); var loc = Locations.toLoc(params.getTextDocument()); - if (files.remove(loc) == null) { + if (!removeFile(loc)) { throw new ResponseErrorException(unknownFileError(loc, params)); } facts(loc).close(loc); @@ -411,14 +379,14 @@ private void triggerBuilder(TextDocumentIdentifier doc) { private void updateContents(VersionedTextDocumentIdentifier doc, String newContents, long timestamp) { logger.trace("New contents for {}", doc); TextDocumentState file = getFile(Locations.toLoc(doc)); - columns.clear(file.getLocation()); + updateFile(file.getLocation()); handleParsingErrors(file, file.update(doc.getVersion(), newContents, timestamp)); } private void handleParsingErrors(TextDocumentState file, CompletableFuture>> diagnosticsAsync) { diagnosticsAsync.thenAccept(diagnostics -> { List parseErrors = diagnostics.get().stream() - .map(diagnostic -> diagnostic.instantiate(columns)) + .map(diagnostic -> diagnostic.instantiate(getColumnMaps())) .collect(Collectors.toList()); logger.trace("Finished parsing tree, reporting new parse errors: {} for: {}", parseErrors, file.getLocation()); @@ -459,7 +427,7 @@ public CompletableFuture computeRenameRange(final ILanguageCon @Override public CompletableFuture rename(RenameParams params) { logger.trace("rename for: {}, new name: {}", params.getTextDocument().getUri(), params.getNewName()); - ISourceLocation loc = Locations.setPosition(Locations.toLoc(params.getTextDocument()), params.getPosition(), columns); + ISourceLocation loc = Locations.setPosition(Locations.toLoc(params.getTextDocument()), params.getPosition(), getColumnMaps()); ILanguageContributions contribs = contributions(loc); return getFile(loc) .getCurrentTreeAsync(true) @@ -496,7 +464,7 @@ private CompletableFuture computeRename(final ILanguageContributi .thenApply(tuple -> { IList documentEdits = (IList) tuple.get(0); showMessages(availableClient(), (ISet) tuple.get(1)); - return DocumentChanges.translateDocumentChanges(documentEdits, columns); + return DocumentChanges.translateDocumentChanges(documentEdits, getColumnMaps()); }) .get(); } @@ -567,7 +535,7 @@ public void didRenameFiles(RenameFilesParams params, List works return; } - WorkspaceEdit changes = DocumentChanges.translateDocumentChanges(edits, columns); + WorkspaceEdit changes = DocumentChanges.translateDocumentChanges(edits, getColumnMaps()); client.applyEdit(new ApplyWorkspaceEditParams(changes, "Rename files")).thenAccept(editResponse -> { if (!editResponse.isApplied()) { throw new RuntimeException("didRenameFiles resulted in a list of edits but applying them failed" @@ -647,7 +615,7 @@ private InlayHint rowToInlayHint(IValue v) { var atEnd = KeywordParameter.get("atEnd", tKW, false); // translate to lsp - var result = new InlayHint(Locations.toPosition(loc, columns, atEnd), Either.forLeft(label.trim())); + var result = new InlayHint(Locations.toPosition(loc, getColumnMaps(), atEnd), Either.forLeft(label.trim())); result.setKind(kind.getName().equals("type") ? InlayHintKind.Type : InlayHintKind.Parameter); result.setPaddingLeft(label.startsWith(" ")); result.setPaddingRight(label.endsWith(" ")); @@ -662,7 +630,7 @@ private CodeLens locCommandTupleToCodeLense(String languageName, IValue v) { ISourceLocation loc = (ISourceLocation) t.get(0); IConstructor command = (IConstructor) t.get(1); - return new CodeLens(Locations.toRange(loc, columns), CodeActions.constructorToCommand(dedicatedLanguageName, languageName, command), null); + return new CodeLens(Locations.toRange(loc, getColumnMaps()), CodeActions.constructorToCommand(dedicatedLanguageName, languageName, command), null); } private static T last(List l) { @@ -711,18 +679,17 @@ private ParametricFileFacts facts(ISourceLocation doc) { return fact; } - private TextDocumentState open(TextDocumentItem doc, long timestamp) { - return files.computeIfAbsent(Locations.toLoc(doc), - l -> new TextDocumentState(contributions(l)::parsing, l, doc.getVersion(), doc.getText(), timestamp, exec)); - } - - private TextDocumentState getFile(ISourceLocation loc) { - loc = loc.top(); - TextDocumentState file = files.get(loc); - if (file == null) { + @Override + protected TextDocumentState getFile(@UnknownInitialization ParametricTextDocumentService this, ISourceLocation loc) { + try { + return super.getFile(loc); + } catch (FileNotFoundException e) { throw new ResponseErrorException(unknownFileError(loc, loc)); } - return file; + } + + private TextDocumentState open(TextDocumentItem doc, long timestamp) { + return openFile(doc, contributions(Locations.toLoc(doc))::parsing, timestamp, exec); } public void shutdown() { @@ -771,7 +738,7 @@ public CompletableFuture>> docume .thenApply(Versioned::get) .thenApply(contrib::documentSymbol) .thenCompose(InterruptibleFuture::get) - .thenApply(documentSymbols -> DocumentSymbols.toLSP(documentSymbols, columns.get(file.getLocation()))) + .thenApply(documentSymbols -> DocumentSymbols.toLSP(documentSymbols, getColumnMap(file.getLocation()))) , Collections::emptyList); } @@ -779,7 +746,7 @@ public CompletableFuture>> docume public CompletableFuture>> codeAction(CodeActionParams params) { logger.debug("codeAction: {}", params); - var location = Locations.setPosition(Locations.toLoc(params.getTextDocument()), params.getRange().getStart(), columns); + var location = Locations.setPosition(Locations.toLoc(params.getTextDocument()), params.getRange().getStart(), getColumnMaps()); final ILanguageContributions contribs = contributions(location); // first we make a future stream for filtering out the "fixes" that were optionally sent along with earlier diagnostics @@ -890,17 +857,17 @@ public CompletableFuture> selectionRange(SelectionRangePara return recoverExceptions(file.getCurrentTreeAsync(true) .thenApply(Versioned::get) .thenCompose(t -> CompletableFutureUtils.reduce(params.getPositions().stream() - .map(p -> Locations.setPosition(loc, p, columns)) + .map(p -> Locations.setPosition(loc, p, getColumnMaps())) .map(p -> computeSelection .thenCompose(compute -> compute.apply(TreeSearch.computeFocusList(t, p.getBeginLine(), p.getBeginColumn()))) - .thenApply(selection -> SelectionRanges.toSelectionRange(p, selection, columns))) + .thenApply(selection -> SelectionRanges.toSelectionRange(p, selection, getColumnMaps()))) .collect(Collectors.toUnmodifiableList()), exec)), Collections::emptyList); } @Override public CompletableFuture> prepareCallHierarchy(CallHierarchyPrepareParams params) { - final var loc = Locations.setPosition(Locations.toLoc(params.getTextDocument()), params.getPosition(), columns); + final var loc = Locations.setPosition(Locations.toLoc(params.getTextDocument()), params.getPosition(), getColumnMaps()); final var contrib = contributions(loc); final var file = getFile(loc); @@ -913,7 +880,7 @@ public CompletableFuture> prepareCallHierarchy(CallHiera var ch = new CallHierarchy(exec); return items.stream() .map(IConstructor.class::cast) - .map(ci -> ch.toLSP(ci, columns)) + .map(ci -> ch.toLSP(ci, getColumnMaps())) .collect(Collectors.toList()); })), Collections::emptyList); } @@ -921,7 +888,7 @@ public CompletableFuture> prepareCallHierarchy(CallHiera private CompletableFuture> incomingOutgoingCalls(BiFunction, T> constructor, CallHierarchyItem source, CallHierarchy.Direction direction) { final var contrib = contributions(Locations.toLoc(source.getUri())); var ch = new CallHierarchy(exec); - return ch.toRascal(source, contrib::parseCallHierarchyData, columns) + return ch.toRascal(source, contrib::parseCallHierarchyData, getColumnMaps()) .thenCompose(sourceItem -> contrib.incomingOutgoingCalls(sourceItem, ch.direction(direction)).get()) .thenApply(callRel -> { // we need to maintain the order @@ -930,10 +897,10 @@ private CompletableFuture> incomingOutgoingCalls(BiFunction new ArrayList<>()); var callSite = (ISourceLocation)((ITuple)entry).get(1); - sites.add(Locations.toRange(callSite, columns)); + sites.add(Locations.toRange(callSite, getColumnMaps())); } return orderedEdges.entrySet().stream() - .map(entry -> constructor.apply(ch.toLSP(entry.getKey(), columns), entry.getValue())) + .map(entry -> constructor.apply(ch.toLSP(entry.getKey(), getColumnMaps()), entry.getValue())) .collect(Collectors.toList()); }); } @@ -952,7 +919,7 @@ public CompletableFuture> callHierarchyOutgoingC public CompletableFuture, CompletionList>> completion(CompletionParams params) { logger.debug("Completion: {} at {} with {}", params.getTextDocument(), params.getPosition(), params.getContext()); - var loc = Locations.setPosition(Locations.toLoc(params.getTextDocument()), params.getPosition(), columns); + var loc = Locations.setPosition(Locations.toLoc(params.getTextDocument()), params.getPosition(), getColumnMaps()); var contrib = contributions(loc); var file = getFile(loc); @@ -963,7 +930,7 @@ public CompletableFuture, CompletionList>> completio var focus = TreeSearch.computeFocusList(t, loc.getBeginLine(), loc.getBeginColumn()); var cursorOffset = loc.getBeginColumn() - TreeAdapter.getLocation((ITree) focus.get(0)).getBeginColumn(); return contrib.completion(focus, VF.integer(cursorOffset), completion.triggerKindToRascal(params.getContext())).get() - .thenApply(ci -> completion.toLSP(this, ci, dedicatedLanguageName, contrib.getName(), loc.getBeginLine(), columns.get(loc))); + .thenApply(ci -> completion.toLSP(this, ci, dedicatedLanguageName, contrib.getName(), loc.getBeginLine(), getColumnMaps().get(loc))); }) .thenApply(Either::forLeft), () -> Either.forLeft(Collections.emptyList())); } @@ -976,7 +943,7 @@ public synchronized void registerLanguage(LanguageParameter lang) { t -> new LanguageContributionsMultiplexer(lang.getName(), exec) ); var fact = facts.computeIfAbsent(lang.getName(), t -> - new ParametricFileFacts(exec, columns, multiplexer) + new ParametricFileFacts(exec, getColumnMaps(), multiplexer) ); var parserConfig = lang.getPrecompiledParser(); @@ -1014,7 +981,7 @@ public synchronized void registerLanguage(LanguageParameter lang) { // If we opened any files with this extension before, now associate them with contributions var extensions = Arrays.asList(lang.getExtensions()); - for (var f : files.keySet()) { + for (var f : getOpenFiles()) { if (extensions.contains(extension(f))) { updateFileState(lang, f); } @@ -1045,7 +1012,7 @@ private void updateFileState(LanguageParameter lang, ISourceLocation f) { logger.trace("File of language {} - updating state: {}", lang.getName(), f); // Since we cannot know what happened to this file before we were called, we need to be careful about races. // It might have been closed in the meantime, so we compute the new value if the key still exists, based on the current value. - var state = files.computeIfPresent(f, (loc, currentState) -> currentState.changeParser(contributions(loc)::parsing)); + var state = updateFileState(f, contributions(f)::parsing); if (state == null) { logger.debug("Updating the parser of {} failed, since it was closed.", f); return; @@ -1113,23 +1080,9 @@ public CompletableFuture executeCommand(String languageName, String comm } } - @Override - public boolean isManagingFile(ISourceLocation file) { - return files.containsKey(file.top()); - } - - @Override - public @Nullable TextDocumentState getDocumentState(ISourceLocation file) { - return files.get(file.top()); - } - @Override public void cancelProgress(String progressId) { contributions.values().forEach(plex -> plex.cancelProgress(progressId)); } - - private ResponseError unknownFileError(ISourceLocation loc, Object data) { - return new ResponseError(ResponseErrorCode.RequestFailed, "Unknown file: " + loc, data); - } } diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/rascal/RascalTextDocumentService.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/rascal/RascalTextDocumentService.java index a13d62b58..eca9d6b90 100644 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/rascal/RascalTextDocumentService.java +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/rascal/RascalTextDocumentService.java @@ -26,15 +26,12 @@ */ package org.rascalmpl.vscode.lsp.rascal; -import java.io.IOException; -import java.io.Reader; +import java.io.FileNotFoundException; import java.util.Collections; import java.util.List; -import java.util.Map; import java.util.Optional; import java.util.Set; import java.util.concurrent.CompletableFuture; -import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ExecutorService; import java.util.function.Consumer; import java.util.function.Supplier; @@ -43,6 +40,7 @@ import org.apache.logging.log4j.Level; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; +import org.checkerframework.checker.initialization.qual.UnknownInitialization; import org.checkerframework.checker.nullness.qual.MonotonicNonNull; import org.checkerframework.checker.nullness.qual.Nullable; import org.eclipse.lsp4j.ApplyWorkspaceEditParams; @@ -106,11 +104,8 @@ import org.eclipse.lsp4j.jsonrpc.messages.ResponseErrorCode; import org.eclipse.lsp4j.services.LanguageClient; import org.eclipse.lsp4j.services.LanguageClientAware; -import org.rascalmpl.library.Prelude; import org.rascalmpl.library.util.PathConfig; import org.rascalmpl.uri.URIResolverRegistry; -import org.rascalmpl.util.locations.ColumnMaps; -import org.rascalmpl.util.locations.LineColumnOffsetMap; import org.rascalmpl.values.IRascalValueFactory; import org.rascalmpl.values.parsetrees.ITree; import org.rascalmpl.values.parsetrees.ProductionAdapter; @@ -119,6 +114,7 @@ import org.rascalmpl.vscode.lsp.IBaseLanguageClient; import org.rascalmpl.vscode.lsp.IBaseTextDocumentService; import org.rascalmpl.vscode.lsp.TextDocumentState; +import org.rascalmpl.vscode.lsp.TextDocumentStateManager; import org.rascalmpl.vscode.lsp.parametric.LanguageRegistry.LanguageParameter; import org.rascalmpl.vscode.lsp.rascal.RascalLanguageServices.CodeLensSuggestion; import org.rascalmpl.vscode.lsp.rascal.conversion.CodeActions; @@ -144,7 +140,7 @@ import io.usethesource.vallang.IValue; import io.usethesource.vallang.IValueFactory; -public class RascalTextDocumentService implements IBaseTextDocumentService, LanguageClientAware { +public class RascalTextDocumentService extends TextDocumentStateManager implements IBaseTextDocumentService, LanguageClientAware { private static final IValueFactory VF = IRascalValueFactory.getInstance(); private static final Logger logger = LogManager.getLogger(RascalTextDocumentService.class); @@ -154,8 +150,6 @@ public class RascalTextDocumentService implements IBaseTextDocumentService, Lang private final SemanticTokenizer tokenizer = new SemanticTokenizer(true); private @MonotonicNonNull LanguageClient client; - private final Map documents; - private final ColumnMaps columns; private @MonotonicNonNull FileFacts facts; private @MonotonicNonNull BaseWorkspaceService workspaceService; @@ -165,8 +159,6 @@ public RascalTextDocumentService(ExecutorService exec) { URIResolverRegistry.getInstance(); this.exec = exec; - this.documents = new ConcurrentHashMap<>(); - this.columns = new ColumnMaps(this::getContents); FallbackResolver.getInstance().registerTextDocumentService(this); } @@ -199,37 +191,6 @@ private BaseWorkspaceService availableWorkspaceServices() { return workspaceService; } - @Override - public ColumnMaps getColumnMaps() { - return columns; - } - - @Override - public LineColumnOffsetMap getColumnMap(ISourceLocation file) { - return columns.get(file); - } - - public String getContents(ISourceLocation file) { - file = file.top(); - TextDocumentState ideState = documents.get(file); - if (ideState != null) { - return ideState.getCurrentContent().get(); - } - - if (!URIResolverRegistry.getInstance().isFile(file)) { - logger.error("Trying to get the contents of a directory: {}", file); - return ""; - } - - try (Reader src = URIResolverRegistry.getInstance().getCharacterReader(file)) { - return Prelude.consumeInputStream(src); - } - catch (IOException e) { - logger.error("Error opening file {} to get contents", file, e); - return ""; - } - } - public void initializeServerCapabilities(ClientCapabilities clientCapabilities, ServerCapabilities result) { result.setDefinitionProvider(true); result.setTextDocumentSync(TextDocumentSyncKind.Full); @@ -254,7 +215,7 @@ public void pair(BaseWorkspaceService workspaceService) { public void connect(LanguageClient client) { this.client = client; this.rascalServices = new RascalLanguageServices(this, availableWorkspaceServices(), (IBaseLanguageClient) client, exec); - this.facts = new FileFacts(exec, rascalServices, client, columns); + this.facts = new FileFacts(exec, rascalServices, client, getColumnMaps()); } @Override @@ -284,7 +245,7 @@ public void didChange(DidChangeTextDocumentParams params) { public void didClose(DidCloseTextDocumentParams params) { logger.debug("Close: {}", params.getTextDocument()); var loc = Locations.toLoc(params.getTextDocument()); - if (documents.remove(loc) == null) { + if (!removeFile(loc)) { throw new ResponseErrorException(new ResponseError(ResponseErrorCode.InternalError, "Unknown file: " + loc, params)); } if (facts != null) { @@ -320,7 +281,7 @@ public void didSave(DidSaveTextDocumentParams params) { private TextDocumentState updateContents(VersionedTextDocumentIdentifier doc, String newContents, long timestamp) { TextDocumentState file = getFile(doc); logger.trace("New contents for {}", doc); - columns.clear(file.getLocation()); + updateFile(file.getLocation()); handleParsingErrors(file, file.update(doc.getVersion(), newContents, timestamp)); return file; } @@ -328,7 +289,7 @@ private TextDocumentState updateContents(VersionedTextDocumentIdentifier doc, St private void handleParsingErrors(TextDocumentState file, CompletableFuture>> diagnosticsAsync) { diagnosticsAsync.thenAccept(diagnostics -> { List parseErrors = diagnostics.get().stream() - .map(diagnostic -> diagnostic.instantiate(columns)) + .map(diagnostic -> diagnostic.instantiate(getColumnMaps())) .collect(Collectors.toList()); logger.trace("Finished parsing tree, reporting new parse errors: {} for: {}", parseErrors, file.getLocation()); @@ -361,7 +322,7 @@ public CompletableFuture, List availableRascalServices().getDocumentSymbols(tr).get()) - .thenApply(documentSymbols -> DocumentSymbols.toLSP(documentSymbols, columns.get(file.getLocation()))) + .thenApply(documentSymbols -> DocumentSymbols.toLSP(documentSymbols, getColumnMap(file.getLocation()))) ); } @@ -408,11 +369,11 @@ public CompletableFuture { - ISourceLocation rascalCursorPos = Locations.setPosition(file.getLocation(), params.getPosition(), columns); + ISourceLocation rascalCursorPos = Locations.setPosition(file.getLocation(), params.getPosition(), getColumnMaps()); IList focus = TreeSearch.computeFocusList(tr, rascalCursorPos.getBeginLine(), rascalCursorPos.getBeginColumn()); return findQualifiedNameUnderCursor(focus); }) - .thenApply(cur -> Locations.toRange(TreeAdapter.getLocation(cur), columns)) + .thenApply(cur -> Locations.toRange(TreeAdapter.getLocation(cur), getColumnMaps())) .thenApply(Either3::forFirst), () -> null); } @@ -430,7 +391,7 @@ public CompletableFuture rename(RenameParams params) { return t; }) .thenCompose(tr -> { - ISourceLocation rascalCursorPos = Locations.setPosition(file.getLocation(), params.getPosition(), columns); + ISourceLocation rascalCursorPos = Locations.setPosition(file.getLocation(), params.getPosition(), getColumnMaps()); var focus = TreeSearch.computeFocusList(tr, rascalCursorPos.getBeginLine(), rascalCursorPos.getBeginColumn()); var cursorTree = findQualifiedNameUnderCursor(focus); var workspaceFolders = availableWorkspaceServices().workspaceFolders() @@ -441,7 +402,7 @@ public CompletableFuture rename(RenameParams params) { }) .thenApply(t -> { showMessages((ISet) t.get(1)); - return DocumentChanges.translateDocumentChanges((IList) t.get(0), columns); + return DocumentChanges.translateDocumentChanges((IList) t.get(0), getColumnMaps()); }); } @@ -556,20 +517,19 @@ private static T last(List l) { } private TextDocumentState open(TextDocumentItem doc, long timestamp) { - return documents.computeIfAbsent(Locations.toLoc(doc), - l -> new TextDocumentState(availableRascalServices()::parseSourceFile, l, doc.getVersion(), doc.getText(), timestamp, exec)); + return openFile(doc, availableRascalServices()::parseSourceFile, timestamp, exec); } private TextDocumentState getFile(TextDocumentIdentifier doc) { return getFile(Locations.toLoc(doc)); } - protected TextDocumentState getFile(ISourceLocation loc) { - TextDocumentState file = documents.get(loc); - if (file == null) { - throw new ResponseErrorException(new ResponseError(-1, "Unknown file: " + loc, loc)); + protected TextDocumentState getFile(@UnknownInitialization RascalTextDocumentService this, ISourceLocation loc) { + try { + return super.getFile(loc); + } catch (FileNotFoundException e) { + throw new ResponseErrorException(unknownFileError(loc, loc)); } - return file; } public void shutdown() { @@ -611,11 +571,11 @@ public CompletableFuture> selectionRange(SelectionRangePara return recoverExceptions(file.getCurrentTreeAsync(true) .thenApply(Versioned::get) .thenApply(tr -> params.getPositions().stream() - .map(p -> Locations.setPosition(file.getLocation(), p, columns)) + .map(p -> Locations.setPosition(file.getLocation(), p, getColumnMaps())) .map(p -> { var focus = TreeSearch.computeFocusList(tr, p.getBeginLine(), p.getBeginColumn()); var locs = SelectionRanges.uniqueTreeLocations(focus); - return SelectionRanges.toSelectionRange(p, locs, columns); + return SelectionRanges.toSelectionRange(p, locs, getColumnMaps()); }) .collect(Collectors.toList()))); } @@ -693,7 +653,7 @@ public CompletableFuture>> codeAction(CodeActio .getCurrentTreeAsync(true) .thenApply(Versioned::get) .thenCompose((ITree tree) -> { - var loc = Locations.setPosition(Locations.toLoc(params.getTextDocument()), params.getRange().getStart(), columns); + var loc = Locations.setPosition(Locations.toLoc(params.getTextDocument()), params.getRange().getStart(), getColumnMaps()); return computeCodeActions(loc.getBeginLine(), loc.getBeginColumn(), tree, availableFacts().getPathConfig(loc)); }) .thenApply(IList::stream) @@ -713,7 +673,7 @@ private CompletableFuture computeCodeActions(final int startLine, final i private CodeLens makeRunCodeLens(CodeLensSuggestion detected) { return new CodeLens( - Locations.toRange(detected.getLine(), columns), + Locations.toRange(detected.getLine(), getColumnMaps()), new Command(detected.getShortName(), detected.getCommandName(), detected.getArguments()), null ); @@ -739,16 +699,6 @@ private static CompletableFuture> recoverExceptions(CompletableFutur return recoverExceptions(future, Collections::emptyList); } - @Override - public boolean isManagingFile(ISourceLocation file) { - return documents.containsKey(file.top()); - } - - @Override - public @Nullable TextDocumentState getDocumentState(ISourceLocation file) { - return documents.get(file.top()); - } - public FileFacts getFileFacts() { return availableFacts(); } From 765f030e76ea46d549751bb186dd18a75e0c90ed Mon Sep 17 00:00:00 2001 From: Toine Hartman Date: Wed, 29 Apr 2026 15:01:43 +0200 Subject: [PATCH 02/15] Factor out parse error handling. --- .../vscode/lsp/TextDocumentStateManager.java | 30 ++++++++++++++++ .../ParametricTextDocumentService.java | 34 +++++++------------ .../lsp/rascal/RascalTextDocumentService.java | 32 ++++++----------- 3 files changed, 52 insertions(+), 44 deletions(-) diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/TextDocumentStateManager.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/TextDocumentStateManager.java index db5a398fb..f3532f378 100644 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/TextDocumentStateManager.java +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/TextDocumentStateManager.java @@ -29,25 +29,32 @@ import java.io.FileNotFoundException; import java.io.IOException; import java.io.Reader; +import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ExecutorService; import java.util.function.BiFunction; +import java.util.stream.Collectors; import org.apache.commons.io.IOUtils; +import org.apache.commons.lang3.function.TriConsumer; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.checkerframework.checker.initialization.qual.UnknownInitialization; import org.checkerframework.checker.nullness.qual.KeyFor; import org.checkerframework.checker.nullness.qual.Nullable; +import org.eclipse.lsp4j.Diagnostic; import org.eclipse.lsp4j.TextDocumentItem; +import org.eclipse.lsp4j.VersionedTextDocumentIdentifier; import org.eclipse.lsp4j.jsonrpc.messages.ResponseError; import org.eclipse.lsp4j.jsonrpc.messages.ResponseErrorCode; import org.rascalmpl.uri.URIResolverRegistry; import org.rascalmpl.util.locations.ColumnMaps; import org.rascalmpl.util.locations.LineColumnOffsetMap; import org.rascalmpl.values.parsetrees.ITree; +import org.rascalmpl.vscode.lsp.rascal.conversion.Diagnostics; +import org.rascalmpl.vscode.lsp.util.Versioned; import org.rascalmpl.vscode.lsp.util.locations.Locations; import io.usethesource.vallang.ISourceLocation; @@ -142,4 +149,27 @@ protected boolean removeFile(ISourceLocation loc) { protected static ResponseError unknownFileError(ISourceLocation loc, @Nullable Object data) { return new ResponseError(ResponseErrorCode.RequestFailed, "Unknown file: " + loc, data); } + + protected static ResponseError unknownFileError(VersionedTextDocumentIdentifier doc, @Nullable Object data) { + return unknownFileError(Locations.toLoc(doc), data); + } + + protected void updateContents(VersionedTextDocumentIdentifier doc, String newContents, long timestamp, TriConsumer>, List> reportDiagnostics) throws FileNotFoundException { + logger.trace("New contents for {}", doc); + TextDocumentState file = getFile(Locations.toLoc(doc)); + updateFile(file.getLocation()); + handleParsingErrors(file, file.update(doc.getVersion(), newContents, timestamp), reportDiagnostics); + } + + protected void handleParsingErrors(TextDocumentState file, CompletableFuture>> diagnosticsAsync, TriConsumer>, List> reportDiagnostics) { + diagnosticsAsync.thenAccept(diagnostics -> { + List parseErrors = diagnostics.get().stream() + .map(diagnostic -> diagnostic.instantiate(getColumnMaps())) + .collect(Collectors.toList()); + + logger.trace("Finished parsing tree, reporting new parse errors: {} for: {}", parseErrors, file.getLocation()); + reportDiagnostics.accept(file.getLocation(), diagnostics, parseErrors); + }); + } + } diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/ParametricTextDocumentService.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/ParametricTextDocumentService.java index f35859588..7abc43611 100644 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/ParametricTextDocumentService.java +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/ParametricTextDocumentService.java @@ -296,6 +296,10 @@ public void initialized() { } } + private void reportDiagnostics(ISourceLocation file, Versioned> diagnostics, List parseErrors) { + facts(file).reportParseErrors(file, diagnostics.version(), parseErrors); + } + // LSP interface methods @Override @@ -303,7 +307,7 @@ public void didOpen(DidOpenTextDocumentParams params) { var timestamp = System.currentTimeMillis(); logger.debug("Did Open file: {}", params.getTextDocument()); TextDocumentState file = open(params.getTextDocument(), timestamp); - handleParsingErrors(file, file.getCurrentDiagnosticsAsync()); + handleParsingErrors(file, file.getCurrentDiagnosticsAsync(), this::reportDiagnostics); triggerAnalyzer(params.getTextDocument(), NORMAL_DEBOUNCE); } @@ -311,8 +315,12 @@ public void didOpen(DidOpenTextDocumentParams params) { public void didChange(DidChangeTextDocumentParams params) { var timestamp = System.currentTimeMillis(); logger.debug("Did Change file: {}", params.getTextDocument().getUri()); - updateContents(params.getTextDocument(), last(params.getContentChanges()).getText(), timestamp); - triggerAnalyzer(params.getTextDocument(), NORMAL_DEBOUNCE); + try { + updateContents(params.getTextDocument(), last(params.getContentChanges()).getText(), timestamp, this::reportDiagnostics); + triggerAnalyzer(params.getTextDocument(), NORMAL_DEBOUNCE); + } catch (FileNotFoundException ignored) { + throw new ResponseErrorException(unknownFileError(params.getTextDocument(), params)); + } } @Override @@ -376,24 +384,6 @@ private void triggerBuilder(TextDocumentIdentifier doc) { fileFacts.calculateBuilder(location, getFile(location).getCurrentTreeAsync(true)); } - private void updateContents(VersionedTextDocumentIdentifier doc, String newContents, long timestamp) { - logger.trace("New contents for {}", doc); - TextDocumentState file = getFile(Locations.toLoc(doc)); - updateFile(file.getLocation()); - handleParsingErrors(file, file.update(doc.getVersion(), newContents, timestamp)); - } - - private void handleParsingErrors(TextDocumentState file, CompletableFuture>> diagnosticsAsync) { - diagnosticsAsync.thenAccept(diagnostics -> { - List parseErrors = diagnostics.get().stream() - .map(diagnostic -> diagnostic.instantiate(getColumnMaps())) - .collect(Collectors.toList()); - - logger.trace("Finished parsing tree, reporting new parse errors: {} for: {}", parseErrors, file.getLocation()); - facts(file.getLocation()).reportParseErrors(file.getLocation(), diagnostics.version(), parseErrors); - }); - } - @Override public CompletableFuture> codeLens(CodeLensParams params) { logger.trace("codeLens for: {}", params.getTextDocument().getUri()); @@ -1018,7 +1008,7 @@ private void updateFileState(LanguageParameter lang, ISourceLocation f) { return; } // Update open editor - handleParsingErrors(state, state.getCurrentDiagnosticsAsync()); + handleParsingErrors(state, state.getCurrentDiagnosticsAsync(), this::reportDiagnostics); triggerAnalyzer(f, state.getCurrentContent().version(), NORMAL_DEBOUNCE); } diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/rascal/RascalTextDocumentService.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/rascal/RascalTextDocumentService.java index eca9d6b90..44abea5d0 100644 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/rascal/RascalTextDocumentService.java +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/rascal/RascalTextDocumentService.java @@ -94,7 +94,6 @@ import org.eclipse.lsp4j.TextDocumentIdentifier; import org.eclipse.lsp4j.TextDocumentItem; import org.eclipse.lsp4j.TextDocumentSyncKind; -import org.eclipse.lsp4j.VersionedTextDocumentIdentifier; import org.eclipse.lsp4j.WorkspaceEdit; import org.eclipse.lsp4j.WorkspaceFolder; import org.eclipse.lsp4j.jsonrpc.ResponseErrorException; @@ -231,14 +230,18 @@ public void didOpen(DidOpenTextDocumentParams params) { var timestamp = System.currentTimeMillis(); logger.debug("Open: {}", params.getTextDocument()); TextDocumentState file = open(params.getTextDocument(), timestamp); - handleParsingErrors(file, file.getCurrentDiagnosticsAsync()); + handleParsingErrors(file, file.getCurrentDiagnosticsAsync(), this::reportDiagnostics); } @Override public void didChange(DidChangeTextDocumentParams params) { var timestamp = System.currentTimeMillis(); logger.trace("Change: {}", params.getTextDocument()); - updateContents(params.getTextDocument(), last(params.getContentChanges()).getText(), timestamp); + try { + updateContents(params.getTextDocument(), last(params.getContentChanges()).getText(), timestamp, this::reportDiagnostics); + } catch (FileNotFoundException ignored) { + throw new ResponseErrorException(unknownFileError(params.getTextDocument(), params)); + } } @Override @@ -278,25 +281,10 @@ public void didSave(DidSaveTextDocumentParams params) { } } - private TextDocumentState updateContents(VersionedTextDocumentIdentifier doc, String newContents, long timestamp) { - TextDocumentState file = getFile(doc); - logger.trace("New contents for {}", doc); - updateFile(file.getLocation()); - handleParsingErrors(file, file.update(doc.getVersion(), newContents, timestamp)); - return file; - } - - private void handleParsingErrors(TextDocumentState file, CompletableFuture>> diagnosticsAsync) { - diagnosticsAsync.thenAccept(diagnostics -> { - List parseErrors = diagnostics.get().stream() - .map(diagnostic -> diagnostic.instantiate(getColumnMaps())) - .collect(Collectors.toList()); - - logger.trace("Finished parsing tree, reporting new parse errors: {} for: {}", parseErrors, file.getLocation()); - if (facts != null) { - facts.reportParseErrors(file.getLocation(), parseErrors); - } - }); + private void reportDiagnostics(ISourceLocation file, Versioned> diagnostics, List parseErrors) { + if (facts != null) { + facts.reportParseErrors(file, parseErrors); + } } @Override From 2e1a26f7f23e51a5a15021e28e78d8a739ff4b15 Mon Sep 17 00:00:00 2001 From: Toine Hartman Date: Wed, 29 Apr 2026 15:07:17 +0200 Subject: [PATCH 03/15] Fix editor contents retrieval. --- .../vscode/lsp/TextDocumentStateManager.java | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/TextDocumentStateManager.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/TextDocumentStateManager.java index f3532f378..6591cba9b 100644 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/TextDocumentStateManager.java +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/TextDocumentStateManager.java @@ -77,10 +77,14 @@ public TextDocumentStateManager() { public String getContents(@UnknownInitialization TextDocumentStateManager this, ISourceLocation file) { file = file.top(); - try { - return getFile(file).getCurrentContent().get(); - } catch (FileNotFoundException ignored) {} - + var ideState = files.get(file); + if (ideState != null) { + return ideState.getCurrentContent().get(); + } + if (!URIResolverRegistry.getInstance().isFile(file)) { + logger.error("Trying to get the contents of a directory: {}", file); + return ""; + } try (Reader src = URIResolverRegistry.getInstance().getCharacterReader(file)) { return IOUtils.toString(src); } From 0c19310088322d3a2b0aeea46b98f3fa1a22155d Mon Sep 17 00:00:00 2001 From: Toine Hartman Date: Wed, 29 Apr 2026 15:39:25 +0200 Subject: [PATCH 04/15] Fix warnings. --- .../org/rascalmpl/vscode/lsp/TextDocumentStateManager.java | 4 ++-- .../vscode/lsp/rascal/RascalTextDocumentService.java | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/TextDocumentStateManager.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/TextDocumentStateManager.java index 6591cba9b..4285f7a56 100644 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/TextDocumentStateManager.java +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/TextDocumentStateManager.java @@ -72,7 +72,7 @@ public class TextDocumentStateManager { private final ColumnMaps columns; public TextDocumentStateManager() { - this.columns = new ColumnMaps(l -> getContents(l)); + this.columns = new ColumnMaps(this::getContents); } public String getContents(@UnknownInitialization TextDocumentStateManager this, ISourceLocation file) { @@ -114,7 +114,7 @@ protected TextDocumentState getFile(@UnknownInitialization TextDocumentStateMana loc = loc.top(); TextDocumentState file = files.get(loc); if (file == null) { - throw new FileNotFoundException(String.format("Unknown file: {}", loc)); + throw new FileNotFoundException(String.format("Unknown file: %s", loc)); } return file; } diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/rascal/RascalTextDocumentService.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/rascal/RascalTextDocumentService.java index 44abea5d0..c7f8d956b 100644 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/rascal/RascalTextDocumentService.java +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/rascal/RascalTextDocumentService.java @@ -512,6 +512,7 @@ private TextDocumentState getFile(TextDocumentIdentifier doc) { return getFile(Locations.toLoc(doc)); } + @Override protected TextDocumentState getFile(@UnknownInitialization RascalTextDocumentService this, ISourceLocation loc) { try { return super.getFile(loc); From 8a4d2e6040be95c6e8868c28a1e32b7f7efc46d1 Mon Sep 17 00:00:00 2001 From: Toine Hartman Date: Thu, 30 Apr 2026 10:15:46 +0200 Subject: [PATCH 05/15] Extract document state functions to separate interface. --- .../vscode/lsp/IBaseTextDocumentService.java | 10 +---- .../vscode/lsp/ITextDocumentStateManager.java | 40 +++++++++++++++++++ .../vscode/lsp/TextDocumentStateManager.java | 6 ++- 3 files changed, 46 insertions(+), 10 deletions(-) create mode 100644 rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/ITextDocumentStateManager.java diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/IBaseTextDocumentService.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/IBaseTextDocumentService.java index d7fb14096..e733873d9 100644 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/IBaseTextDocumentService.java +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/IBaseTextDocumentService.java @@ -29,7 +29,6 @@ import java.time.Duration; import java.util.List; import java.util.concurrent.CompletableFuture; -import org.checkerframework.checker.nullness.qual.Nullable; import org.eclipse.lsp4j.ClientCapabilities; import org.eclipse.lsp4j.CreateFilesParams; import org.eclipse.lsp4j.DeleteFilesParams; @@ -38,14 +37,12 @@ import org.eclipse.lsp4j.WorkspaceFolder; import org.eclipse.lsp4j.services.LanguageClient; import org.eclipse.lsp4j.services.TextDocumentService; -import org.rascalmpl.util.locations.ColumnMaps; -import org.rascalmpl.util.locations.LineColumnOffsetMap; import org.rascalmpl.vscode.lsp.parametric.LanguageRegistry.LanguageParameter; import io.usethesource.vallang.ISourceLocation; import io.usethesource.vallang.IValue; -public interface IBaseTextDocumentService extends TextDocumentService { +public interface IBaseTextDocumentService extends TextDocumentService, ITextDocumentStateManager { static final Duration NO_DEBOUNCE = Duration.ZERO; static final Duration NORMAL_DEBOUNCE = Duration.ofMillis(800); @@ -61,11 +58,6 @@ public interface IBaseTextDocumentService extends TextDocumentService { void projectRemoved(String name, ISourceLocation projectRoot); CompletableFuture executeCommand(String languageName, String command); - LineColumnOffsetMap getColumnMap(ISourceLocation file); - ColumnMaps getColumnMaps(); - @Nullable TextDocumentState getDocumentState(ISourceLocation file); - - boolean isManagingFile(ISourceLocation file); void didCreateFiles(CreateFilesParams params); void didRenameFiles(RenameFilesParams params, List workspaceFolders); diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/ITextDocumentStateManager.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/ITextDocumentStateManager.java new file mode 100644 index 000000000..db69bf993 --- /dev/null +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/ITextDocumentStateManager.java @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2018-2025, NWO-I CWI and Swat.engineering + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ +package org.rascalmpl.vscode.lsp; + +import org.checkerframework.checker.nullness.qual.Nullable; +import org.rascalmpl.util.locations.ColumnMaps; +import org.rascalmpl.util.locations.LineColumnOffsetMap; + +import io.usethesource.vallang.ISourceLocation; + +public interface ITextDocumentStateManager { + LineColumnOffsetMap getColumnMap(ISourceLocation file); + ColumnMaps getColumnMaps(); + @Nullable TextDocumentState getDocumentState(ISourceLocation file); + boolean isManagingFile(ISourceLocation file); +} diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/TextDocumentStateManager.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/TextDocumentStateManager.java index 4285f7a56..035f0c6cc 100644 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/TextDocumentStateManager.java +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/TextDocumentStateManager.java @@ -64,7 +64,7 @@ * * */ -public class TextDocumentStateManager { +public class TextDocumentStateManager implements ITextDocumentStateManager { private static final Logger logger = LogManager.getLogger(TextDocumentStateManager.class); @@ -94,18 +94,22 @@ public String getContents(@UnknownInitialization TextDocumentStateManager this, } } + @Override public ColumnMaps getColumnMaps() { return columns; } + @Override public boolean isManagingFile(ISourceLocation loc) { return files.containsKey(loc.top()); } + @Override public @Nullable TextDocumentState getDocumentState(ISourceLocation file) { return files.get(file.top()); } + @Override public LineColumnOffsetMap getColumnMap(ISourceLocation loc) { return columns.get(loc.top()); } From ee5ac8edadb4cf9aa4bff93b17c4121ce9e71c16 Mon Sep 17 00:00:00 2001 From: Toine Hartman Date: Thu, 30 Apr 2026 11:31:09 +0200 Subject: [PATCH 06/15] Refactor file getters to unchecked (LSP) and checked (elsewhere). --- .../vscode/lsp/TextDocumentStateManager.java | 24 ++++++++++++++++++- .../ParametricTextDocumentService.java | 11 --------- .../lsp/rascal/RascalLanguageServices.java | 5 ++-- .../lsp/rascal/RascalTextDocumentService.java | 11 --------- 4 files changed, 26 insertions(+), 25 deletions(-) diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/TextDocumentStateManager.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/TextDocumentStateManager.java index 035f0c6cc..3d93e0a9e 100644 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/TextDocumentStateManager.java +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/TextDocumentStateManager.java @@ -47,6 +47,7 @@ import org.eclipse.lsp4j.Diagnostic; import org.eclipse.lsp4j.TextDocumentItem; import org.eclipse.lsp4j.VersionedTextDocumentIdentifier; +import org.eclipse.lsp4j.jsonrpc.ResponseErrorException; import org.eclipse.lsp4j.jsonrpc.messages.ResponseError; import org.eclipse.lsp4j.jsonrpc.messages.ResponseErrorCode; import org.rascalmpl.uri.URIResolverRegistry; @@ -114,7 +115,13 @@ public LineColumnOffsetMap getColumnMap(ISourceLocation loc) { return columns.get(loc.top()); } - protected TextDocumentState getFile(@UnknownInitialization TextDocumentStateManager this, ISourceLocation loc) throws FileNotFoundException { + /** + * Get open file state. + * @param loc The location of the file. + * @return The current state in the editor. + * @throws FileNotFoundException If the file is not open. + */ + public TextDocumentState getEditorState(ISourceLocation loc) throws FileNotFoundException { loc = loc.top(); TextDocumentState file = files.get(loc); if (file == null) { @@ -123,6 +130,21 @@ protected TextDocumentState getFile(@UnknownInitialization TextDocumentStateMana return file; } + /** + * Get open file state. If the file is not open, throws an (unchecked) {@link ResponseErrorException}. + * + * Intentionally protected function, only to be used from LSP endpoints. Users outside of the LSP context should call {@link TextDocumentStateManager#getEditorState}. + * @param loc The location of the file. + * @return The current state in the editor. + */ + protected TextDocumentState getFile(ISourceLocation loc) { + try { + return getEditorState(loc); + } catch (FileNotFoundException ignored) { + throw new ResponseErrorException(unknownFileError(loc, loc)); + } + } + protected TextDocumentState openFile(TextDocumentItem doc, BiFunction> parser, long timestamp, ExecutorService exec) { return files.computeIfAbsent(Locations.toLoc(doc), l -> new TextDocumentState(parser, l, doc.getVersion(), doc.getText(), timestamp, exec)); diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/ParametricTextDocumentService.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/ParametricTextDocumentService.java index 7abc43611..389b8357a 100644 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/ParametricTextDocumentService.java +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/ParametricTextDocumentService.java @@ -49,7 +49,6 @@ import org.apache.logging.log4j.Level; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; -import org.checkerframework.checker.initialization.qual.UnknownInitialization; import org.checkerframework.checker.nullness.qual.MonotonicNonNull; import org.checkerframework.checker.nullness.qual.Nullable; import org.eclipse.lsp4j.ApplyWorkspaceEditParams; @@ -203,7 +202,6 @@ public class ParametricTextDocumentService extends TextDocumentStateManager impl tf.abstractDataType(typeStore, "FileSystemChange"), "renamed", tf.sourceLocationType(), "from", tf.sourceLocationType(), "to"); - @SuppressWarnings({"initialization", "methodref.receiver.bound"}) // this::getContents public ParametricTextDocumentService(ExecutorService exec, @Nullable LanguageParameter dedicatedLanguage) { // The following call ensures that URIResolverRegistry is initialized before FallbackResolver is accessed URIResolverRegistry.getInstance(); @@ -669,15 +667,6 @@ private ParametricFileFacts facts(ISourceLocation doc) { return fact; } - @Override - protected TextDocumentState getFile(@UnknownInitialization ParametricTextDocumentService this, ISourceLocation loc) { - try { - return super.getFile(loc); - } catch (FileNotFoundException e) { - throw new ResponseErrorException(unknownFileError(loc, loc)); - } - } - private TextDocumentState open(TextDocumentItem doc, long timestamp) { return openFile(doc, contributions(Locations.toLoc(doc))::parsing, timestamp, exec); } diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/rascal/RascalLanguageServices.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/rascal/RascalLanguageServices.java index b796576e3..dc0d62e3a 100644 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/rascal/RascalLanguageServices.java +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/rascal/RascalLanguageServices.java @@ -29,6 +29,7 @@ import static org.rascalmpl.vscode.lsp.util.EvaluatorUtil.makeFutureEvaluator; import static org.rascalmpl.vscode.lsp.util.EvaluatorUtil.runEvaluator; +import java.io.FileNotFoundException; import java.io.IOException; import java.io.StringReader; import java.net.URISyntaxException; @@ -240,11 +241,11 @@ IFunction makeParseTreeGetter(Evaluator e) { ISourceLocation resolvedLocation = Locations.toClientLocation((ISourceLocation) t[0]); try { // although we cannot type-check modules with errors, we prefer to get the errors here instead of retrying the parse and still failing after this try-block - var tree = rascalTextDocumentService.getFile(resolvedLocation).getCurrentTreeAsync(true).get(); + var tree = rascalTextDocumentService.getEditorState(resolvedLocation).getCurrentTreeAsync(true).get(); if (tree != null) { return tree.get(); } - } catch (ResponseErrorException | ExecutionException e1) { + } catch (FileNotFoundException | ExecutionException e1) { // File is not open in the IDE | Parse threw an exception // In either case, fall through and try a direct parse } catch (InterruptedException e1) { diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/rascal/RascalTextDocumentService.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/rascal/RascalTextDocumentService.java index c7f8d956b..02fd9cff6 100644 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/rascal/RascalTextDocumentService.java +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/rascal/RascalTextDocumentService.java @@ -40,7 +40,6 @@ import org.apache.logging.log4j.Level; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; -import org.checkerframework.checker.initialization.qual.UnknownInitialization; import org.checkerframework.checker.nullness.qual.MonotonicNonNull; import org.checkerframework.checker.nullness.qual.Nullable; import org.eclipse.lsp4j.ApplyWorkspaceEditParams; @@ -152,7 +151,6 @@ public class RascalTextDocumentService extends TextDocumentStateManager implemen private @MonotonicNonNull FileFacts facts; private @MonotonicNonNull BaseWorkspaceService workspaceService; - @SuppressWarnings({"initialization", "methodref.receiver.bound"}) // this::getContents public RascalTextDocumentService(ExecutorService exec) { // The following call ensures that URIResolverRegistry is initialized before FallbackResolver is accessed URIResolverRegistry.getInstance(); @@ -512,15 +510,6 @@ private TextDocumentState getFile(TextDocumentIdentifier doc) { return getFile(Locations.toLoc(doc)); } - @Override - protected TextDocumentState getFile(@UnknownInitialization RascalTextDocumentService this, ISourceLocation loc) { - try { - return super.getFile(loc); - } catch (FileNotFoundException e) { - throw new ResponseErrorException(unknownFileError(loc, loc)); - } - } - public void shutdown() { exec.shutdown(); } From 7f11b2ea50ec5461b65accb58c5d14d1ef5ef683 Mon Sep 17 00:00:00 2001 From: Toine Hartman Date: Thu, 30 Apr 2026 11:46:51 +0200 Subject: [PATCH 07/15] Clean up CF annotations. --- .../org/rascalmpl/vscode/lsp/TextDocumentStateManager.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/TextDocumentStateManager.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/TextDocumentStateManager.java index 3d93e0a9e..c6b376ab0 100644 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/TextDocumentStateManager.java +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/TextDocumentStateManager.java @@ -41,7 +41,6 @@ import org.apache.commons.lang3.function.TriConsumer; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; -import org.checkerframework.checker.initialization.qual.UnknownInitialization; import org.checkerframework.checker.nullness.qual.KeyFor; import org.checkerframework.checker.nullness.qual.Nullable; import org.eclipse.lsp4j.Diagnostic; @@ -72,11 +71,12 @@ public class TextDocumentStateManager implements ITextDocumentStateManager { private final Map files = new ConcurrentHashMap<>(); private final ColumnMaps columns; + @SuppressWarnings({"methodref.receiver.bound"}) // this::getContents public TextDocumentStateManager() { this.columns = new ColumnMaps(this::getContents); } - public String getContents(@UnknownInitialization TextDocumentStateManager this, ISourceLocation file) { + public String getContents(ISourceLocation file) { file = file.top(); var ideState = files.get(file); if (ideState != null) { From 7e38a026303f8e799cc58c4a07744b652410ebf6 Mon Sep 17 00:00:00 2001 From: Toine Hartman Date: Thu, 30 Apr 2026 11:58:31 +0200 Subject: [PATCH 08/15] Rename & document document management methods. --- .../vscode/lsp/TextDocumentStateManager.java | 38 +++++++++++-------- .../ParametricTextDocumentService.java | 12 +++--- .../lsp/rascal/RascalTextDocumentService.java | 4 +- 3 files changed, 29 insertions(+), 25 deletions(-) diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/TextDocumentStateManager.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/TextDocumentStateManager.java index c6b376ab0..275901171 100644 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/TextDocumentStateManager.java +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/TextDocumentStateManager.java @@ -76,6 +76,14 @@ public TextDocumentStateManager() { this.columns = new ColumnMaps(this::getContents); } + protected static ResponseError unknownFileError(ISourceLocation loc, @Nullable Object data) { + return new ResponseError(ResponseErrorCode.RequestFailed, "Unknown file: " + loc, data); + } + + protected static ResponseError unknownFileError(VersionedTextDocumentIdentifier doc, @Nullable Object data) { + return unknownFileError(Locations.toLoc(doc), data); + } + public String getContents(ISourceLocation file) { file = file.top(); var ideState = files.get(file); @@ -131,11 +139,12 @@ public TextDocumentState getEditorState(ISourceLocation loc) throws FileNotFound } /** - * Get open file state. If the file is not open, throws an (unchecked) {@link ResponseErrorException}. + * Get open file state. * * Intentionally protected function, only to be used from LSP endpoints. Users outside of the LSP context should call {@link TextDocumentStateManager#getEditorState}. * @param loc The location of the file. * @return The current state in the editor. + * @throws ResponseErrorException If the file is not open. */ protected TextDocumentState getFile(ISourceLocation loc) { try { @@ -150,16 +159,23 @@ protected TextDocumentState openFile(TextDocumentItem doc, BiFunction new TextDocumentState(parser, l, doc.getVersion(), doc.getText(), timestamp, exec)); } - protected void updateFile(ISourceLocation loc) { + private void invalidateColumnMaps(ISourceLocation loc) { columns.clear(loc.top()); } - protected boolean removeFile(ISourceLocation loc) { - updateFile(loc); - return files.remove(loc.top()) != null; + /** + * Close a file/editor. + * @param loc The location of the file. + * @throws ResponseErrorException If the file was not open. + */ + protected void closeFile(ISourceLocation loc) { + invalidateColumnMaps(loc); + if (files.remove(loc.top()) == null) { + throw new ResponseErrorException(unknownFileError(loc, loc)); + } } - protected @Nullable TextDocumentState updateFileState(ISourceLocation f, BiFunction> parser) { + protected @Nullable TextDocumentState changeParser(ISourceLocation f, BiFunction> parser) { f = f.top(); logger.trace("Updating state: {}", f); @@ -176,18 +192,10 @@ protected boolean removeFile(ISourceLocation loc) { return files.keySet(); } - protected static ResponseError unknownFileError(ISourceLocation loc, @Nullable Object data) { - return new ResponseError(ResponseErrorCode.RequestFailed, "Unknown file: " + loc, data); - } - - protected static ResponseError unknownFileError(VersionedTextDocumentIdentifier doc, @Nullable Object data) { - return unknownFileError(Locations.toLoc(doc), data); - } - protected void updateContents(VersionedTextDocumentIdentifier doc, String newContents, long timestamp, TriConsumer>, List> reportDiagnostics) throws FileNotFoundException { logger.trace("New contents for {}", doc); TextDocumentState file = getFile(Locations.toLoc(doc)); - updateFile(file.getLocation()); + invalidateColumnMaps(file.getLocation()); handleParsingErrors(file, file.update(doc.getVersion(), newContents, timestamp), reportDiagnostics); } diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/ParametricTextDocumentService.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/ParametricTextDocumentService.java index 389b8357a..67a911311 100644 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/ParametricTextDocumentService.java +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/ParametricTextDocumentService.java @@ -333,9 +333,7 @@ public void didSave(DidSaveTextDocumentParams params) { public void didClose(DidCloseTextDocumentParams params) { logger.debug("Did Close file: {}", params.getTextDocument()); var loc = Locations.toLoc(params.getTextDocument()); - if (!removeFile(loc)) { - throw new ResponseErrorException(unknownFileError(loc, params)); - } + closeFile(loc); facts(loc).close(loc); // If the closed file no longer exists (e.g., if an untitled file is closed without ever having been saved), // we mimic a delete event to ensure all diagnostics are cleared. @@ -962,7 +960,8 @@ public synchronized void registerLanguage(LanguageParameter lang) { var extensions = Arrays.asList(lang.getExtensions()); for (var f : getOpenFiles()) { if (extensions.contains(extension(f))) { - updateFileState(lang, f); + logger.trace("File of language {} - updating state: {}", lang.getName(), f); + refreshFileState(f); } } } @@ -986,12 +985,11 @@ public Set fileExtensions() { }).collect(Collectors.toSet()); } - private void updateFileState(LanguageParameter lang, ISourceLocation f) { + private void refreshFileState(ISourceLocation f) { f = f.top(); - logger.trace("File of language {} - updating state: {}", lang.getName(), f); // Since we cannot know what happened to this file before we were called, we need to be careful about races. // It might have been closed in the meantime, so we compute the new value if the key still exists, based on the current value. - var state = updateFileState(f, contributions(f)::parsing); + var state = changeParser(f, contributions(f)::parsing); if (state == null) { logger.debug("Updating the parser of {} failed, since it was closed.", f); return; diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/rascal/RascalTextDocumentService.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/rascal/RascalTextDocumentService.java index 02fd9cff6..fa503efef 100644 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/rascal/RascalTextDocumentService.java +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/rascal/RascalTextDocumentService.java @@ -246,9 +246,7 @@ public void didChange(DidChangeTextDocumentParams params) { public void didClose(DidCloseTextDocumentParams params) { logger.debug("Close: {}", params.getTextDocument()); var loc = Locations.toLoc(params.getTextDocument()); - if (!removeFile(loc)) { - throw new ResponseErrorException(new ResponseError(ResponseErrorCode.InternalError, "Unknown file: " + loc, params)); - } + closeFile(loc); if (facts != null) { facts.close(loc); } From fd31fa5fdd877595ebe6fcc39df70f13a0453ec5 Mon Sep 17 00:00:00 2001 From: Toine Hartman Date: Thu, 30 Apr 2026 12:55:00 +0200 Subject: [PATCH 09/15] Collapse two state getters. --- .../rascalmpl/vscode/lsp/ITextDocumentStateManager.java | 4 ++-- .../rascalmpl/vscode/lsp/TextDocumentStateManager.java | 6 +----- .../org/rascalmpl/vscode/lsp/uri/FallbackResolver.java | 8 ++++---- 3 files changed, 7 insertions(+), 11 deletions(-) diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/ITextDocumentStateManager.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/ITextDocumentStateManager.java index db69bf993..259390147 100644 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/ITextDocumentStateManager.java +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/ITextDocumentStateManager.java @@ -26,7 +26,7 @@ */ package org.rascalmpl.vscode.lsp; -import org.checkerframework.checker.nullness.qual.Nullable; +import java.io.FileNotFoundException; import org.rascalmpl.util.locations.ColumnMaps; import org.rascalmpl.util.locations.LineColumnOffsetMap; @@ -35,6 +35,6 @@ public interface ITextDocumentStateManager { LineColumnOffsetMap getColumnMap(ISourceLocation file); ColumnMaps getColumnMaps(); - @Nullable TextDocumentState getDocumentState(ISourceLocation file); + TextDocumentState getEditorState(ISourceLocation file) throws FileNotFoundException; boolean isManagingFile(ISourceLocation file); } diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/TextDocumentStateManager.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/TextDocumentStateManager.java index 275901171..cff235253 100644 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/TextDocumentStateManager.java +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/TextDocumentStateManager.java @@ -113,11 +113,6 @@ public boolean isManagingFile(ISourceLocation loc) { return files.containsKey(loc.top()); } - @Override - public @Nullable TextDocumentState getDocumentState(ISourceLocation file) { - return files.get(file.top()); - } - @Override public LineColumnOffsetMap getColumnMap(ISourceLocation loc) { return columns.get(loc.top()); @@ -129,6 +124,7 @@ public LineColumnOffsetMap getColumnMap(ISourceLocation loc) { * @return The current state in the editor. * @throws FileNotFoundException If the file is not open. */ + @Override public TextDocumentState getEditorState(ISourceLocation loc) throws FileNotFoundException { loc = loc.top(); TextDocumentState file = files.get(loc); diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/uri/FallbackResolver.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/uri/FallbackResolver.java index a722f327c..2d56cb33d 100644 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/uri/FallbackResolver.java +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/uri/FallbackResolver.java @@ -28,6 +28,7 @@ import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; +import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; @@ -325,10 +326,9 @@ public void registerTextDocumentService(@UnderInitialization IBaseTextDocumentSe public TextDocumentState getDocumentState(ISourceLocation file) throws IOException { for (var service : textDocumentServices) { - var state = service.getDocumentState(file); - if (state != null) { - return state; - } + try { + return service.getEditorState(file); + } catch (FileNotFoundException ignored) { /* try the next service */} } throw new IOException("File is not managed by lsp"); } From 00ed55709ed64c6c06fae1cd2dc44fda20f968b4 Mon Sep 17 00:00:00 2001 From: Toine Hartman Date: Thu, 30 Apr 2026 13:07:44 +0200 Subject: [PATCH 10/15] Update documentation. --- .../org/rascalmpl/vscode/lsp/TextDocumentStateManager.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/TextDocumentStateManager.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/TextDocumentStateManager.java index cff235253..2c984e457 100644 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/TextDocumentStateManager.java +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/TextDocumentStateManager.java @@ -60,9 +60,10 @@ import io.usethesource.vallang.ISourceLocation; /** - * Management of {{@link TextDocumentState}} associated with locations. - * + * Manages open files and their contents. * + * This class maintains a set of open files, their state, and information derived from their contents, like column maps. + * This functionality is shared by implementations of {@link IBaseTextDocumentService}. */ public class TextDocumentStateManager implements ITextDocumentStateManager { From 777f1fd9e5216692436caa88e3f9e03d2cefccf9 Mon Sep 17 00:00:00 2001 From: Toine Hartman Date: Thu, 30 Apr 2026 13:40:15 +0200 Subject: [PATCH 11/15] Factor out parse error reporting. --- .../vscode/lsp/TextDocumentStateManager.java | 22 +++++----- .../vscode/lsp/model/DiagnosticsReporter.java | 44 +++++++++++++++++++ .../ParametricTextDocumentService.java | 14 +++--- .../parametric/model/ParametricFileFacts.java | 28 ++++++------ .../lsp/rascal/RascalTextDocumentService.java | 14 +++--- .../vscode/lsp/rascal/model/FileFacts.java | 25 ++++++----- .../rascalmpl/vscode/lsp/util/Versioned.java | 5 +++ 7 files changed, 103 insertions(+), 49 deletions(-) create mode 100644 rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/model/DiagnosticsReporter.java diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/TextDocumentStateManager.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/TextDocumentStateManager.java index 2c984e457..41680655d 100644 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/TextDocumentStateManager.java +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/TextDocumentStateManager.java @@ -38,12 +38,10 @@ import java.util.function.BiFunction; import java.util.stream.Collectors; import org.apache.commons.io.IOUtils; -import org.apache.commons.lang3.function.TriConsumer; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.checkerframework.checker.nullness.qual.KeyFor; import org.checkerframework.checker.nullness.qual.Nullable; -import org.eclipse.lsp4j.Diagnostic; import org.eclipse.lsp4j.TextDocumentItem; import org.eclipse.lsp4j.VersionedTextDocumentIdentifier; import org.eclipse.lsp4j.jsonrpc.ResponseErrorException; @@ -53,6 +51,7 @@ import org.rascalmpl.util.locations.ColumnMaps; import org.rascalmpl.util.locations.LineColumnOffsetMap; import org.rascalmpl.values.parsetrees.ITree; +import org.rascalmpl.vscode.lsp.model.DiagnosticsReporter; import org.rascalmpl.vscode.lsp.rascal.conversion.Diagnostics; import org.rascalmpl.vscode.lsp.util.Versioned; import org.rascalmpl.vscode.lsp.util.locations.Locations; @@ -65,7 +64,7 @@ * This class maintains a set of open files, their state, and information derived from their contents, like column maps. * This functionality is shared by implementations of {@link IBaseTextDocumentService}. */ -public class TextDocumentStateManager implements ITextDocumentStateManager { +public abstract class TextDocumentStateManager implements ITextDocumentStateManager { private static final Logger logger = LogManager.getLogger(TextDocumentStateManager.class); @@ -189,22 +188,25 @@ protected void closeFile(ISourceLocation loc) { return files.keySet(); } - protected void updateContents(VersionedTextDocumentIdentifier doc, String newContents, long timestamp, TriConsumer>, List> reportDiagnostics) throws FileNotFoundException { + protected void updateContents(VersionedTextDocumentIdentifier doc, String newContents, long timestamp) throws FileNotFoundException { logger.trace("New contents for {}", doc); TextDocumentState file = getFile(Locations.toLoc(doc)); invalidateColumnMaps(file.getLocation()); - handleParsingErrors(file, file.update(doc.getVersion(), newContents, timestamp), reportDiagnostics); + handleParsingErrors(file, file.update(doc.getVersion(), newContents, timestamp)); } - protected void handleParsingErrors(TextDocumentState file, CompletableFuture>> diagnosticsAsync, TriConsumer>, List> reportDiagnostics) { + protected void handleParsingErrors(TextDocumentState file, CompletableFuture>> diagnosticsAsync) { diagnosticsAsync.thenAccept(diagnostics -> { - List parseErrors = diagnostics.get().stream() + var parseErrors = diagnostics.map(d -> d.stream() .map(diagnostic -> diagnostic.instantiate(getColumnMaps())) - .collect(Collectors.toList()); + .collect(Collectors.toList())); - logger.trace("Finished parsing tree, reporting new parse errors: {} for: {}", parseErrors, file.getLocation()); - reportDiagnostics.accept(file.getLocation(), diagnostics, parseErrors); + var loc = file.getLocation(); + logger.trace("Finished parsing tree, reporting new parse errors: {} for: {}", parseErrors, loc); + getDiagnosticsReporter(loc).reportParseErrors(loc, parseErrors); }); } + abstract protected DiagnosticsReporter getDiagnosticsReporter(ISourceLocation file); + } diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/model/DiagnosticsReporter.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/model/DiagnosticsReporter.java new file mode 100644 index 000000000..12d257fdd --- /dev/null +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/model/DiagnosticsReporter.java @@ -0,0 +1,44 @@ +/* + * Copyright (c) 2018-2025, NWO-I CWI and Swat.engineering + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ +package org.rascalmpl.vscode.lsp.model; + +import java.util.List; +import org.eclipse.lsp4j.Diagnostic; +import org.rascalmpl.vscode.lsp.parametric.model.ParametricFileFacts; +import org.rascalmpl.vscode.lsp.rascal.model.FileFacts; +import org.rascalmpl.vscode.lsp.util.Versioned; + +import io.usethesource.vallang.ISourceLocation; + +/** + * Interface for objects that can report diagnostics on files. + * + * Encapsulates common behavior of {@link FileFacts} and {@link ParametricFileFacts}. + */ +public interface DiagnosticsReporter { + void reportParseErrors(ISourceLocation file, Versioned> msgs); +} diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/ParametricTextDocumentService.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/ParametricTextDocumentService.java index 67a911311..08a122ef5 100644 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/ParametricTextDocumentService.java +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/ParametricTextDocumentService.java @@ -71,7 +71,6 @@ import org.eclipse.lsp4j.CreateFilesParams; import org.eclipse.lsp4j.DefinitionParams; import org.eclipse.lsp4j.DeleteFilesParams; -import org.eclipse.lsp4j.Diagnostic; import org.eclipse.lsp4j.DidChangeTextDocumentParams; import org.eclipse.lsp4j.DidCloseTextDocumentParams; import org.eclipse.lsp4j.DidOpenTextDocumentParams; @@ -135,6 +134,7 @@ import org.rascalmpl.vscode.lsp.IBaseTextDocumentService; import org.rascalmpl.vscode.lsp.TextDocumentState; import org.rascalmpl.vscode.lsp.TextDocumentStateManager; +import org.rascalmpl.vscode.lsp.model.DiagnosticsReporter; import org.rascalmpl.vscode.lsp.parametric.LanguageRegistry.LanguageParameter; import org.rascalmpl.vscode.lsp.parametric.capabilities.CapabilityRegistration; import org.rascalmpl.vscode.lsp.parametric.capabilities.CompletionCapability; @@ -146,7 +146,6 @@ import org.rascalmpl.vscode.lsp.rascal.conversion.CallHierarchy; import org.rascalmpl.vscode.lsp.rascal.conversion.CodeActions; import org.rascalmpl.vscode.lsp.rascal.conversion.Completion; -import org.rascalmpl.vscode.lsp.rascal.conversion.Diagnostics; import org.rascalmpl.vscode.lsp.rascal.conversion.DocumentChanges; import org.rascalmpl.vscode.lsp.rascal.conversion.DocumentSymbols; import org.rascalmpl.vscode.lsp.rascal.conversion.FoldingRanges; @@ -294,8 +293,9 @@ public void initialized() { } } - private void reportDiagnostics(ISourceLocation file, Versioned> diagnostics, List parseErrors) { - facts(file).reportParseErrors(file, diagnostics.version(), parseErrors); + @Override + protected DiagnosticsReporter getDiagnosticsReporter(ISourceLocation file) { + return facts(file); } // LSP interface methods @@ -305,7 +305,7 @@ public void didOpen(DidOpenTextDocumentParams params) { var timestamp = System.currentTimeMillis(); logger.debug("Did Open file: {}", params.getTextDocument()); TextDocumentState file = open(params.getTextDocument(), timestamp); - handleParsingErrors(file, file.getCurrentDiagnosticsAsync(), this::reportDiagnostics); + handleParsingErrors(file, file.getCurrentDiagnosticsAsync()); triggerAnalyzer(params.getTextDocument(), NORMAL_DEBOUNCE); } @@ -314,7 +314,7 @@ public void didChange(DidChangeTextDocumentParams params) { var timestamp = System.currentTimeMillis(); logger.debug("Did Change file: {}", params.getTextDocument().getUri()); try { - updateContents(params.getTextDocument(), last(params.getContentChanges()).getText(), timestamp, this::reportDiagnostics); + updateContents(params.getTextDocument(), last(params.getContentChanges()).getText(), timestamp); triggerAnalyzer(params.getTextDocument(), NORMAL_DEBOUNCE); } catch (FileNotFoundException ignored) { throw new ResponseErrorException(unknownFileError(params.getTextDocument(), params)); @@ -995,7 +995,7 @@ private void refreshFileState(ISourceLocation f) { return; } // Update open editor - handleParsingErrors(state, state.getCurrentDiagnosticsAsync(), this::reportDiagnostics); + handleParsingErrors(state, state.getCurrentDiagnosticsAsync()); triggerAnalyzer(f, state.getCurrentContent().version(), NORMAL_DEBOUNCE); } diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/model/ParametricFileFacts.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/model/ParametricFileFacts.java index 3e1a59fb0..88c147c75 100644 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/model/ParametricFileFacts.java +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/model/ParametricFileFacts.java @@ -38,7 +38,6 @@ import java.util.concurrent.atomic.AtomicReference; import java.util.function.Function; import java.util.function.Supplier; - import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.checkerframework.checker.nullness.qual.MonotonicNonNull; @@ -50,6 +49,7 @@ import org.rascalmpl.uri.URIResolverRegistry; import org.rascalmpl.util.locations.ColumnMaps; import org.rascalmpl.values.parsetrees.ITree; +import org.rascalmpl.vscode.lsp.model.DiagnosticsReporter; import org.rascalmpl.vscode.lsp.parametric.ILanguageContributions; import org.rascalmpl.vscode.lsp.parametric.model.ParametricSummary.SummaryLookup; import org.rascalmpl.vscode.lsp.util.Lists; @@ -59,7 +59,7 @@ import io.usethesource.vallang.ISourceLocation; -public class ParametricFileFacts { +public class ParametricFileFacts implements DiagnosticsReporter { private static final Logger logger = LogManager.getLogger(ParametricFileFacts.class); private final Executor exec; @@ -109,8 +109,9 @@ public void setClient(LanguageClient client) { this.client = client; } - public void reportParseErrors(ISourceLocation file, int version, List msgs) { - getFile(file).reportParseErrors(version, msgs); + @Override + public void reportParseErrors(ISourceLocation file, Versioned> msgs) { + getFile(file).reportParseErrors(msgs); } private FileFact getFile(ISourceLocation file) { @@ -168,11 +169,11 @@ private interface FileFact { void close(); void calculateAnalyzer(CompletableFuture> tree, int version, Duration delay); void calculateBuilder(CompletableFuture> tree); - void reportParseErrors(int version, List messages); + void reportParseErrors(Versioned> messages); void clearDiagnostics(); CompletableFuture> lookupInSummaries(SummaryLookup lookup, Versioned tree, Position cursor); } - + @SuppressWarnings("java:S3077") // Reads/writes to fields of this class happen sequentially private class ActualFileFact implements FileFact { private final ISourceLocation file; @@ -200,9 +201,8 @@ public ActualFileFact(ISourceLocation file) { this.file = file; } - private void reportDiagnostics(AtomicReference> current, int version, T messages) { - var maybeNewer = new Versioned<>(version, messages); - if (Versioned.replaceIfNewer(current, maybeNewer)) { + private void reportDiagnostics(AtomicReference> current, Versioned messages) { + if (Versioned.replaceIfNewer(current, messages)) { sendDiagnostics(); } } @@ -302,7 +302,7 @@ public void calculateAnalyzer(CompletableFuture> tree, int vers .thenApply(f -> f.createFullSummary(file, tree)) .thenCompose(Function.identity()); ParametricSummary.getMessages(summary, exec) - .thenAcceptIfUninterrupted(ms -> reportDiagnostics(analyzerDiagnostics, version, ms)); + .thenAcceptIfUninterrupted(ms -> reportDiagnostics(analyzerDiagnostics, new Versioned<>(version, ms))); return summary; }); } @@ -342,13 +342,13 @@ public void calculateBuilder(CompletableFuture> tree) { var builderMessages = ParametricSummary.getMessages(latestBuilderBuild, exec); analyzerMessages.thenAcceptBothIfUninterrupted(builderMessages, (aMessages, bMessages) -> { bMessages.removeAll(aMessages); - tree.thenAccept(t -> reportDiagnostics(builderDiagnostics, t.version(), bMessages)); + tree.thenAccept(t -> reportDiagnostics(builderDiagnostics, new Versioned<>(t.version(), bMessages))); }); } @Override - public void reportParseErrors(int version, List messages) { - reportDiagnostics(parserDiagnostics, version, messages); + public void reportParseErrors(Versioned> messages) { + reportDiagnostics(parserDiagnostics, messages); } @Override @@ -469,7 +469,7 @@ public void calculateBuilder(CompletableFuture> tree) { } @Override - public void reportParseErrors(int version, List messages) { + public void reportParseErrors(Versioned> messages) { // NOP } diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/rascal/RascalTextDocumentService.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/rascal/RascalTextDocumentService.java index fa503efef..c25dc7bae 100644 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/rascal/RascalTextDocumentService.java +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/rascal/RascalTextDocumentService.java @@ -54,7 +54,6 @@ import org.eclipse.lsp4j.CreateFilesParams; import org.eclipse.lsp4j.DefinitionParams; import org.eclipse.lsp4j.DeleteFilesParams; -import org.eclipse.lsp4j.Diagnostic; import org.eclipse.lsp4j.DidChangeTextDocumentParams; import org.eclipse.lsp4j.DidCloseTextDocumentParams; import org.eclipse.lsp4j.DidOpenTextDocumentParams; @@ -113,10 +112,10 @@ import org.rascalmpl.vscode.lsp.IBaseTextDocumentService; import org.rascalmpl.vscode.lsp.TextDocumentState; import org.rascalmpl.vscode.lsp.TextDocumentStateManager; +import org.rascalmpl.vscode.lsp.model.DiagnosticsReporter; import org.rascalmpl.vscode.lsp.parametric.LanguageRegistry.LanguageParameter; import org.rascalmpl.vscode.lsp.rascal.RascalLanguageServices.CodeLensSuggestion; import org.rascalmpl.vscode.lsp.rascal.conversion.CodeActions; -import org.rascalmpl.vscode.lsp.rascal.conversion.Diagnostics; import org.rascalmpl.vscode.lsp.rascal.conversion.DocumentChanges; import org.rascalmpl.vscode.lsp.rascal.conversion.DocumentSymbols; import org.rascalmpl.vscode.lsp.rascal.conversion.FoldingRanges; @@ -228,7 +227,7 @@ public void didOpen(DidOpenTextDocumentParams params) { var timestamp = System.currentTimeMillis(); logger.debug("Open: {}", params.getTextDocument()); TextDocumentState file = open(params.getTextDocument(), timestamp); - handleParsingErrors(file, file.getCurrentDiagnosticsAsync(), this::reportDiagnostics); + handleParsingErrors(file, file.getCurrentDiagnosticsAsync()); } @Override @@ -236,7 +235,7 @@ public void didChange(DidChangeTextDocumentParams params) { var timestamp = System.currentTimeMillis(); logger.trace("Change: {}", params.getTextDocument()); try { - updateContents(params.getTextDocument(), last(params.getContentChanges()).getText(), timestamp, this::reportDiagnostics); + updateContents(params.getTextDocument(), last(params.getContentChanges()).getText(), timestamp); } catch (FileNotFoundException ignored) { throw new ResponseErrorException(unknownFileError(params.getTextDocument(), params)); } @@ -277,10 +276,9 @@ public void didSave(DidSaveTextDocumentParams params) { } } - private void reportDiagnostics(ISourceLocation file, Versioned> diagnostics, List parseErrors) { - if (facts != null) { - facts.reportParseErrors(file, parseErrors); - } + @Override + protected DiagnosticsReporter getDiagnosticsReporter(ISourceLocation ignored) { + return facts; } @Override diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/rascal/model/FileFacts.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/rascal/model/FileFacts.java index 88061ba0c..e8777c62a 100644 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/rascal/model/FileFacts.java +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/rascal/model/FileFacts.java @@ -32,6 +32,7 @@ import java.util.concurrent.CompletableFuture; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.Executor; +import java.util.concurrent.atomic.AtomicReference; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.checkerframework.checker.nullness.qual.Nullable; @@ -41,9 +42,11 @@ import org.rascalmpl.library.util.PathConfig; import org.rascalmpl.uri.URIResolverRegistry; import org.rascalmpl.util.locations.ColumnMaps; +import org.rascalmpl.vscode.lsp.model.DiagnosticsReporter; import org.rascalmpl.vscode.lsp.rascal.RascalLanguageServices; import org.rascalmpl.vscode.lsp.rascal.conversion.Diagnostics; import org.rascalmpl.vscode.lsp.util.Lists; +import org.rascalmpl.vscode.lsp.util.Versioned; import org.rascalmpl.vscode.lsp.util.concurrent.CompletableFutureUtils; import org.rascalmpl.vscode.lsp.util.concurrent.InterruptibleFuture; import org.rascalmpl.vscode.lsp.util.concurrent.LazyUpdateableReference; @@ -53,7 +56,7 @@ import io.usethesource.vallang.IConstructor; import io.usethesource.vallang.ISourceLocation; -public class FileFacts { +public class FileFacts implements DiagnosticsReporter { private static final Logger logger = LogManager.getLogger(FileFacts.class); private final Executor exec; private final RascalLanguageServices rascal; @@ -70,7 +73,7 @@ public FileFacts(Executor exec, RascalLanguageServices rascal, LanguageClient cl this.cm = cm; this.confs = new PathConfigs(exec, new PathConfigDiagnostics(client, cm)); this.nopFact = new FileFact() { - @Override public void reportParseErrors(List msgs) { /* NOP */} + @Override public void reportParseErrors(Versioned> msgs) { /* NOP */} @Override public void reportTypeCheckerErrors(List msgs) { /* NOP */ } @Override public void invalidate() { /* NOP */ } @Override public void close() { /* NOP */ } @@ -95,7 +98,8 @@ public void invalidate(ISourceLocation file) { return getFile(file).getSummary(); } - public void reportParseErrors(ISourceLocation file, List msgs) { + @Override + public void reportParseErrors(ISourceLocation file, Versioned> msgs) { getFile(file).reportParseErrors(msgs); } @@ -134,7 +138,7 @@ public void close(ISourceLocation file) { } private interface FileFact { - void reportParseErrors(List msgs); + void reportParseErrors(Versioned> msgs); void reportTypeCheckerErrors(List msgs); CompletableFuture<@Nullable SummaryBridge> getSummary(); void invalidate(); @@ -145,7 +149,7 @@ private interface FileFact { private class ActualFileFact implements FileFact { private final ISourceLocation file; private final LazyUpdateableReference> summary; - private volatile List parseMessages = Collections.emptyList(); + private volatile AtomicReference>> parseMessages = Versioned.atomic(-1, Collections.emptyList()); private volatile List typeCheckerMessages = Collections.emptyList(); private final ReplaceableFuture>> typeCheckResults; @@ -165,9 +169,10 @@ public ActualFileFact(ISourceLocation file, Executor exec) { } @Override - public void reportParseErrors(List msgs) { - parseMessages = msgs; - sendDiagnostics(); + public void reportParseErrors(Versioned> msgs) { + if (Versioned.replaceIfNewer(parseMessages, msgs)) { + sendDiagnostics(); + } } @Override @@ -184,7 +189,7 @@ private void sendDiagnostics() { logger.trace("Sending diagnostics for: {}", file); client.publishDiagnostics(new PublishDiagnosticsParams( Locations.toUri(file).toString(), - Lists.union(typeCheckerMessages, parseMessages))); + Lists.union(typeCheckerMessages, parseMessages.get().get()))); } @Override @@ -204,7 +209,7 @@ public void invalidate() { @Override public void close() { - if ((parseMessages.isEmpty() && typeCheckerMessages.isEmpty()) || !URIResolverRegistry.getInstance().exists(file)) { + if ((parseMessages.get().get().isEmpty() && typeCheckerMessages.isEmpty()) || !URIResolverRegistry.getInstance().exists(file)) { // If there are no messages for this file or the file has been deleted, can we remove it // else VS Code comes back and we've dropped the messages in our internal data files.remove(file); diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/util/Versioned.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/util/Versioned.java index 7e8aac0de..4bb120b09 100644 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/util/Versioned.java +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/util/Versioned.java @@ -27,6 +27,7 @@ package org.rascalmpl.vscode.lsp.util; import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Function; import org.checkerframework.checker.nullness.qual.PolyNull; public class Versioned { @@ -77,4 +78,8 @@ public static boolean replaceIfNewer(AtomicReference<@PolyNull Versioned> } } } + + public Versioned map(Function func) { + return new Versioned(this.version, func.apply(this.object), this.timestamp); + } } From 4b370beb9987bccc834c36d5af4db844d6fc9b5b Mon Sep 17 00:00:00 2001 From: Toine Hartman Date: Thu, 30 Apr 2026 13:48:26 +0200 Subject: [PATCH 12/15] Fix CF error. --- .../rascalmpl/vscode/lsp/rascal/RascalTextDocumentService.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/rascal/RascalTextDocumentService.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/rascal/RascalTextDocumentService.java index c25dc7bae..4c44eb2b3 100644 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/rascal/RascalTextDocumentService.java +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/rascal/RascalTextDocumentService.java @@ -278,7 +278,7 @@ public void didSave(DidSaveTextDocumentParams params) { @Override protected DiagnosticsReporter getDiagnosticsReporter(ISourceLocation ignored) { - return facts; + return availableFacts(); } @Override From 1aa2fa4d137e5795fcb95ec68b5f92d21342daf5 Mon Sep 17 00:00:00 2001 From: Toine Hartman Date: Thu, 30 Apr 2026 13:57:20 +0200 Subject: [PATCH 13/15] Fix checkstyle. --- .../java/org/rascalmpl/vscode/lsp/TextDocumentStateManager.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/TextDocumentStateManager.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/TextDocumentStateManager.java index 41680655d..0c339cc23 100644 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/TextDocumentStateManager.java +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/TextDocumentStateManager.java @@ -207,6 +207,6 @@ protected void handleParsingErrors(TextDocumentState file, CompletableFuture Date: Thu, 30 Apr 2026 16:26:06 +0200 Subject: [PATCH 14/15] Fix SQ warnings. --- .../rascalmpl/vscode/lsp/TextDocumentStateManager.java | 4 ++-- .../lsp/parametric/ParametricTextDocumentService.java | 9 ++------- .../vscode/lsp/rascal/RascalTextDocumentService.java | 7 +------ .../org/rascalmpl/vscode/lsp/rascal/model/FileFacts.java | 2 +- .../java/org/rascalmpl/vscode/lsp/util/Versioned.java | 2 +- 5 files changed, 7 insertions(+), 17 deletions(-) diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/TextDocumentStateManager.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/TextDocumentStateManager.java index 0c339cc23..28fe4572a 100644 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/TextDocumentStateManager.java +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/TextDocumentStateManager.java @@ -72,7 +72,7 @@ public abstract class TextDocumentStateManager implements ITextDocumentStateMana private final ColumnMaps columns; @SuppressWarnings({"methodref.receiver.bound"}) // this::getContents - public TextDocumentStateManager() { + protected TextDocumentStateManager() { this.columns = new ColumnMaps(this::getContents); } @@ -188,7 +188,7 @@ protected void closeFile(ISourceLocation loc) { return files.keySet(); } - protected void updateContents(VersionedTextDocumentIdentifier doc, String newContents, long timestamp) throws FileNotFoundException { + protected void updateContents(VersionedTextDocumentIdentifier doc, String newContents, long timestamp) { logger.trace("New contents for {}", doc); TextDocumentState file = getFile(Locations.toLoc(doc)); invalidateColumnMaps(file.getLocation()); diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/ParametricTextDocumentService.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/ParametricTextDocumentService.java index 08a122ef5..9df7abca8 100644 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/ParametricTextDocumentService.java +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/ParametricTextDocumentService.java @@ -26,7 +26,6 @@ */ package org.rascalmpl.vscode.lsp.parametric; -import java.io.FileNotFoundException; import java.time.Duration; import java.util.ArrayList; import java.util.Arrays; @@ -313,12 +312,8 @@ public void didOpen(DidOpenTextDocumentParams params) { public void didChange(DidChangeTextDocumentParams params) { var timestamp = System.currentTimeMillis(); logger.debug("Did Change file: {}", params.getTextDocument().getUri()); - try { - updateContents(params.getTextDocument(), last(params.getContentChanges()).getText(), timestamp); - triggerAnalyzer(params.getTextDocument(), NORMAL_DEBOUNCE); - } catch (FileNotFoundException ignored) { - throw new ResponseErrorException(unknownFileError(params.getTextDocument(), params)); - } + updateContents(params.getTextDocument(), last(params.getContentChanges()).getText(), timestamp); + triggerAnalyzer(params.getTextDocument(), NORMAL_DEBOUNCE); } @Override diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/rascal/RascalTextDocumentService.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/rascal/RascalTextDocumentService.java index 4c44eb2b3..9a9de9286 100644 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/rascal/RascalTextDocumentService.java +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/rascal/RascalTextDocumentService.java @@ -26,7 +26,6 @@ */ package org.rascalmpl.vscode.lsp.rascal; -import java.io.FileNotFoundException; import java.util.Collections; import java.util.List; import java.util.Optional; @@ -234,11 +233,7 @@ public void didOpen(DidOpenTextDocumentParams params) { public void didChange(DidChangeTextDocumentParams params) { var timestamp = System.currentTimeMillis(); logger.trace("Change: {}", params.getTextDocument()); - try { - updateContents(params.getTextDocument(), last(params.getContentChanges()).getText(), timestamp); - } catch (FileNotFoundException ignored) { - throw new ResponseErrorException(unknownFileError(params.getTextDocument(), params)); - } + updateContents(params.getTextDocument(), last(params.getContentChanges()).getText(), timestamp); } @Override diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/rascal/model/FileFacts.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/rascal/model/FileFacts.java index e8777c62a..ae9cee664 100644 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/rascal/model/FileFacts.java +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/rascal/model/FileFacts.java @@ -149,7 +149,7 @@ private interface FileFact { private class ActualFileFact implements FileFact { private final ISourceLocation file; private final LazyUpdateableReference> summary; - private volatile AtomicReference>> parseMessages = Versioned.atomic(-1, Collections.emptyList()); + private AtomicReference>> parseMessages = Versioned.atomic(-1, Collections.emptyList()); private volatile List typeCheckerMessages = Collections.emptyList(); private final ReplaceableFuture>> typeCheckResults; diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/util/Versioned.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/util/Versioned.java index 4bb120b09..295de4893 100644 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/util/Versioned.java +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/util/Versioned.java @@ -80,6 +80,6 @@ public static boolean replaceIfNewer(AtomicReference<@PolyNull Versioned> } public Versioned map(Function func) { - return new Versioned(this.version, func.apply(this.object), this.timestamp); + return new Versioned<>(this.version, func.apply(this.object), this.timestamp); } } From c6187f173d2df2acd4d39186a612cf339f5f5849 Mon Sep 17 00:00:00 2001 From: Toine Hartman Date: Thu, 30 Apr 2026 18:08:22 +0200 Subject: [PATCH 15/15] Lazy contribution lookup. --- .../org/rascalmpl/vscode/lsp/TextDocumentStateManager.java | 5 +++-- .../vscode/lsp/parametric/ParametricTextDocumentService.java | 2 +- .../vscode/lsp/rascal/RascalTextDocumentService.java | 2 +- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/TextDocumentStateManager.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/TextDocumentStateManager.java index 28fe4572a..0240397e7 100644 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/TextDocumentStateManager.java +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/TextDocumentStateManager.java @@ -36,6 +36,7 @@ import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ExecutorService; import java.util.function.BiFunction; +import java.util.function.Function; import java.util.stream.Collectors; import org.apache.commons.io.IOUtils; import org.apache.logging.log4j.LogManager; @@ -150,9 +151,9 @@ protected TextDocumentState getFile(ISourceLocation loc) { } } - protected TextDocumentState openFile(TextDocumentItem doc, BiFunction> parser, long timestamp, ExecutorService exec) { + protected TextDocumentState openFile(TextDocumentItem doc, Function>> parserGetter, long timestamp, ExecutorService exec) { return files.computeIfAbsent(Locations.toLoc(doc), - l -> new TextDocumentState(parser, l, doc.getVersion(), doc.getText(), timestamp, exec)); + l -> new TextDocumentState(parserGetter.apply(l), l, doc.getVersion(), doc.getText(), timestamp, exec)); } private void invalidateColumnMaps(ISourceLocation loc) { diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/ParametricTextDocumentService.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/ParametricTextDocumentService.java index 9df7abca8..59593b749 100644 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/ParametricTextDocumentService.java +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/ParametricTextDocumentService.java @@ -661,7 +661,7 @@ private ParametricFileFacts facts(ISourceLocation doc) { } private TextDocumentState open(TextDocumentItem doc, long timestamp) { - return openFile(doc, contributions(Locations.toLoc(doc))::parsing, timestamp, exec); + return openFile(doc, l -> contributions(l)::parsing, timestamp, exec); } public void shutdown() { diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/rascal/RascalTextDocumentService.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/rascal/RascalTextDocumentService.java index 9a9de9286..36d6ddda2 100644 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/rascal/RascalTextDocumentService.java +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/rascal/RascalTextDocumentService.java @@ -494,7 +494,7 @@ private static T last(List l) { } private TextDocumentState open(TextDocumentItem doc, long timestamp) { - return openFile(doc, availableRascalServices()::parseSourceFile, timestamp, exec); + return openFile(doc, l -> availableRascalServices()::parseSourceFile, timestamp, exec); } private TextDocumentState getFile(TextDocumentIdentifier doc) {