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..259390147 --- /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 java.io.FileNotFoundException; +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(); + 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 new file mode 100644 index 000000000..0240397e7 --- /dev/null +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/TextDocumentStateManager.java @@ -0,0 +1,213 @@ +/* + * 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.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.function.Function; +import java.util.stream.Collectors; +import org.apache.commons.io.IOUtils; +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.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; +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; + +import io.usethesource.vallang.ISourceLocation; + +/** + * 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 abstract class TextDocumentStateManager implements ITextDocumentStateManager { + + private static final Logger logger = LogManager.getLogger(TextDocumentStateManager.class); + + private final Map files = new ConcurrentHashMap<>(); + private final ColumnMaps columns; + + @SuppressWarnings({"methodref.receiver.bound"}) // this::getContents + protected 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); + 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); + } + catch (IOException e) { + logger.error("Error opening file {} to get contents", file, e); + return ""; + } + } + + @Override + public ColumnMaps getColumnMaps() { + return columns; + } + + @Override + public boolean isManagingFile(ISourceLocation loc) { + return files.containsKey(loc.top()); + } + + @Override + public LineColumnOffsetMap getColumnMap(ISourceLocation loc) { + return columns.get(loc.top()); + } + + /** + * 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. + */ + @Override + public TextDocumentState getEditorState(ISourceLocation loc) throws FileNotFoundException { + loc = loc.top(); + TextDocumentState file = files.get(loc); + if (file == null) { + throw new FileNotFoundException(String.format("Unknown file: %s", loc)); + } + return file; + } + + /** + * 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 { + return getEditorState(loc); + } catch (FileNotFoundException ignored) { + throw new ResponseErrorException(unknownFileError(loc, loc)); + } + } + + protected TextDocumentState openFile(TextDocumentItem doc, Function>> parserGetter, long timestamp, ExecutorService exec) { + return files.computeIfAbsent(Locations.toLoc(doc), + l -> new TextDocumentState(parserGetter.apply(l), l, doc.getVersion(), doc.getText(), timestamp, exec)); + } + + private void invalidateColumnMaps(ISourceLocation loc) { + columns.clear(loc.top()); + } + + /** + * 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 changeParser(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 void updateContents(VersionedTextDocumentIdentifier doc, String newContents, long timestamp) { + logger.trace("New contents for {}", doc); + TextDocumentState file = getFile(Locations.toLoc(doc)); + invalidateColumnMaps(file.getLocation()); + handleParsingErrors(file, file.update(doc.getVersion(), newContents, timestamp)); + } + + protected void handleParsingErrors(TextDocumentState file, CompletableFuture>> diagnosticsAsync) { + diagnosticsAsync.thenAccept(diagnostics -> { + var parseErrors = diagnostics.map(d -> d.stream() + .map(diagnostic -> diagnostic.instantiate(getColumnMaps())) + .collect(Collectors.toList())); + + var loc = file.getLocation(); + logger.trace("Finished parsing tree, reporting new parse errors: {} for: {}", parseErrors, loc); + getDiagnosticsReporter(loc).reportParseErrors(loc, parseErrors); + }); + } + + protected abstract 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 97e85b6e6..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 @@ -26,8 +26,6 @@ */ package org.rascalmpl.vscode.lsp.parametric; -import java.io.IOException; -import java.io.Reader; import java.time.Duration; import java.util.ArrayList; import java.util.Arrays; @@ -50,7 +48,6 @@ 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.nullness.qual.MonotonicNonNull; import org.checkerframework.checker.nullness.qual.Nullable; import org.eclipse.lsp4j.ApplyWorkspaceEditParams; @@ -73,7 +70,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; @@ -129,8 +125,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 +132,8 @@ 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.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; @@ -149,7 +145,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; @@ -177,7 +172,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 +184,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 */ @@ -208,14 +200,11 @@ public class ParametricTextDocumentService implements IBaseTextDocumentService, 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(); this.exec = exec; - this.files = new ConcurrentHashMap<>(); - this.columns = new ColumnMaps(this::getContents); if (dedicatedLanguage == null) { this.dedicatedLanguageName = ""; this.dedicatedLanguage = null; @@ -227,31 +216,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."); @@ -328,6 +292,11 @@ public void initialized() { } } + @Override + protected DiagnosticsReporter getDiagnosticsReporter(ISourceLocation file) { + return facts(file); + } + // LSP interface methods @Override @@ -359,9 +328,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) { - 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. @@ -408,24 +375,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)); - columns.clear(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)) - .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()); @@ -459,7 +408,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 +445,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 +516,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 +596,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 +611,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) { @@ -712,17 +661,7 @@ private ParametricFileFacts facts(ISourceLocation doc) { } 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) { - throw new ResponseErrorException(unknownFileError(loc, loc)); - } - return file; + return openFile(doc, l -> contributions(l)::parsing, timestamp, exec); } public void shutdown() { @@ -771,7 +710,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 +718,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 +829,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 +852,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 +860,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 +869,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 +891,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 +902,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 +915,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,9 +953,10 @@ 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); + logger.trace("File of language {} - updating state: {}", lang.getName(), f); + refreshFileState(f); } } } @@ -1040,12 +980,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 = files.computeIfPresent(f, (loc, currentState) -> currentState.changeParser(contributions(loc)::parsing)); + var state = changeParser(f, contributions(f)::parsing); if (state == null) { logger.debug("Updating the parser of {} failed, since it was closed.", f); return; @@ -1113,23 +1052,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/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/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 a13d62b58..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 @@ -26,15 +26,11 @@ */ package org.rascalmpl.vscode.lsp.rascal; -import java.io.IOException; -import java.io.Reader; 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; @@ -57,7 +53,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; @@ -96,7 +91,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; @@ -106,11 +100,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,10 +110,11 @@ 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.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; @@ -144,7 +136,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,19 +146,14 @@ 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; - @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(); this.exec = exec; - this.documents = new ConcurrentHashMap<>(); - this.columns = new ColumnMaps(this::getContents); FallbackResolver.getInstance().registerTextDocumentService(this); } @@ -199,37 +186,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 +210,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,9 +240,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) { - throw new ResponseErrorException(new ResponseError(ResponseErrorCode.InternalError, "Unknown file: " + loc, params)); - } + closeFile(loc); if (facts != null) { facts.close(loc); } @@ -317,25 +271,9 @@ 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()); - 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(columns)) - .collect(Collectors.toList()); - - logger.trace("Finished parsing tree, reporting new parse errors: {} for: {}", parseErrors, file.getLocation()); - if (facts != null) { - facts.reportParseErrors(file.getLocation(), parseErrors); - } - }); + @Override + protected DiagnosticsReporter getDiagnosticsReporter(ISourceLocation ignored) { + return availableFacts(); } @Override @@ -361,7 +299,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 +346,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 +368,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 +379,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,22 +494,13 @@ 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, l -> 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)); - } - return file; - } - public void shutdown() { exec.shutdown(); } @@ -611,11 +540,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 +622,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 +642,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 +668,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(); } 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..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 @@ -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 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/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"); } 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..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 @@ -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); + } }