diff --git a/rascal-lsp/.vscode/launch.json b/rascal-lsp/.vscode/launch.json index 361401f1e..9dde3567f 100644 --- a/rascal-lsp/.vscode/launch.json +++ b/rascal-lsp/.vscode/launch.json @@ -10,7 +10,8 @@ "console": "internalConsole", "vmArgs": [ "-Dlog4j2.level=TRACE", - "-Drascal.compilerClasspath=${workspaceFolder}/target/lib/rascal.jar" + "-Drascal.compilerClasspath=${workspaceFolder}/target/lib/rascal.jar", + "-Drascal.fallbackResolver=org.rascalmpl.vscode.lsp.uri.FallbackResolver" ] }, { @@ -22,7 +23,8 @@ "console": "internalConsole", "vmArgs": [ "-Dlog4j2.level=TRACE", - "-Drascal.compilerClasspath=${workspaceFolder}/target/lib/rascal.jar" + "-Drascal.compilerClasspath=${workspaceFolder}/target/lib/rascal.jar", + "-Drascal.fallbackResolver=org.rascalmpl.vscode.lsp.uri.FallbackResolver" ] } ] diff --git a/rascal-lsp/pom.xml b/rascal-lsp/pom.xml index 857255f18..a96edd7d0 100644 --- a/rascal-lsp/pom.xml +++ b/rascal-lsp/pom.xml @@ -65,7 +65,7 @@ org.rascalmpl rascal - 0.40.17 + 0.41.0-RC7 org.rascalmpl diff --git a/rascal-lsp/src/main/java/org/rascalmpl/uri/resolvers.config b/rascal-lsp/src/main/java/org/rascalmpl/uri/resolvers.config new file mode 100644 index 000000000..a7f9a3c3f --- /dev/null +++ b/rascal-lsp/src/main/java/org/rascalmpl/uri/resolvers.config @@ -0,0 +1 @@ +org.rascalmpl.vscode.lsp.uri.LSPOpenFileResolver 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 443e28924..6f2d6fc55 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 @@ -48,6 +48,9 @@ public interface IBaseTextDocumentService extends TextDocumentService { void unregisterLanguage(LanguageParameter lang); CompletableFuture executeCommand(String languageName, String command); LineColumnOffsetMap getColumnMap(ISourceLocation file); + TextDocumentState getDocumentState(ISourceLocation file); + + boolean isManagingFile(ISourceLocation file); default void didRenameFiles(RenameFilesParams params, Set workspaceFolders) {} } diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/IRascalFileSystemServices.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/IRascalFileSystemServices.java index 084a4425e..5e0c73273 100644 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/IRascalFileSystemServices.java +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/IRascalFileSystemServices.java @@ -51,6 +51,7 @@ import org.rascalmpl.uri.URIResolverRegistry; import org.rascalmpl.uri.URIUtil; import org.rascalmpl.values.IRascalValueFactory; +import org.rascalmpl.vscode.lsp.util.locations.Locations; import io.usethesource.vallang.ISourceLocation; import io.usethesource.vallang.IValueFactory; @@ -65,7 +66,7 @@ default CompletableFuture resolveLocation(SourceLocation loc) { try { ISourceLocation tmp = loc.toRascalLocation(); - ISourceLocation resolved = reg.logicalToPhysical(tmp); + ISourceLocation resolved = Locations.toClientLocation(tmp); if (resolved == null) { return loc; @@ -77,10 +78,6 @@ default CompletableFuture resolveLocation(SourceLocation loc) { IRascalFileSystemServices__logger.warn("Could not resolve location {} due to {}.", loc, e.getMessage()); return loc; } - catch (IOException e) { - // This is normal behavior (when its not a logical scheme) - return loc; - } catch (Throwable e) { IRascalFileSystemServices__logger.warn("Could not resolve location {} due to {}.", loc, e.getMessage()); return loc; @@ -130,7 +127,7 @@ private static boolean readonly(ISourceLocation loc) throws IOException { return false; } if (reg.getRegisteredLogicalSchemes().contains(loc.getScheme())) { - var resolved = reg.logicalToPhysical(loc); + var resolved = Locations.toClientLocation(loc); if (resolved != null && resolved != loc) { return readonly(resolved); } diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/LSPIDEServices.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/LSPIDEServices.java index 8d1a2dac1..1d446d6a5 100644 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/LSPIDEServices.java +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/LSPIDEServices.java @@ -92,7 +92,7 @@ public void browse(URI uri, String title, int viewColumn) { @Override public void edit(ISourceLocation path) { try { - ISourceLocation physical = URIResolverRegistry.getInstance().logicalToPhysical(path); + ISourceLocation physical = Locations.toClientLocation(path); ShowDocumentParams params = new ShowDocumentParams(physical.getURI().toASCIIString()); params.setTakeFocus(true); diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/TextDocumentState.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/TextDocumentState.java index 73690847b..e143b4d27 100644 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/TextDocumentState.java +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/TextDocumentState.java @@ -55,10 +55,10 @@ public class TextDocumentState { @SuppressWarnings("java:S3077") // we are use volatile correctly private volatile CompletableFuture> currentTree; - public TextDocumentState(BiFunction> parser, ISourceLocation file, int initialVersion, String initialContent) { + public TextDocumentState(BiFunction> parser, ISourceLocation file, int initialVersion, String initialContent, long timestamp) { this.parser = parser; this.file = file; - this.currentContent = new Versioned<>(initialVersion, initialContent); + this.currentContent = new Versioned<>(initialVersion, initialContent, timestamp); this.currentTree = newTreeAsync(initialVersion, initialContent); } @@ -72,8 +72,8 @@ public TextDocumentState(BiFunction pair. */ - public CompletableFuture> update(int version, String content) { - currentContent = new Versioned<>(version, content); + public CompletableFuture> update(int version, String content, long timestamp) { + currentContent = new Versioned<>(version, content, timestamp); var newTree = newTreeAsync(version, content); currentTree = newTree; return newTree; @@ -105,4 +105,8 @@ public ISourceLocation getLocation() { public Versioned getCurrentContent() { return currentContent; } + + public long getLastModified() { + return currentContent.getTimestamp(); + } } 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 7c7bbb648..31dec798c 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 @@ -106,6 +106,7 @@ import org.rascalmpl.vscode.lsp.parametric.model.ParametricSummary; import org.rascalmpl.vscode.lsp.parametric.model.ParametricSummary.SummaryLookup; import org.rascalmpl.vscode.lsp.terminal.ITerminalIDEServer.LanguageParameter; +import org.rascalmpl.vscode.lsp.uri.FallbackResolver; import org.rascalmpl.vscode.lsp.util.CodeActions; import org.rascalmpl.vscode.lsp.util.Diagnostics; import org.rascalmpl.vscode.lsp.util.FoldingRanges; @@ -149,6 +150,9 @@ public class ParametricTextDocumentService implements IBaseTextDocumentService, private final @Nullable LanguageParameter dedicatedLanguage; public ParametricTextDocumentService(ExecutorService exec, @Nullable LanguageParameter dedicatedLanguage) { + // The following call ensures that URIResolverRegistry is initialized before FallbackResolver is accessed + URIResolverRegistry.getInstance(); + this.ownExecuter = exec; this.files = new ConcurrentHashMap<>(); this.columns = new ColumnMaps(this::getContents); @@ -160,6 +164,7 @@ public ParametricTextDocumentService(ExecutorService exec, @Nullable LanguagePar this.dedicatedLanguageName = dedicatedLanguage.getName(); this.dedicatedLanguage = dedicatedLanguage; } + FallbackResolver.getInstance().registerTextDocumentService(this); } @Override @@ -226,15 +231,17 @@ public void connect(LanguageClient client) { @Override public void didOpen(DidOpenTextDocumentParams params) { + var timestamp = System.currentTimeMillis(); logger.debug("Did Open file: {}", params.getTextDocument()); - handleParsingErrors(open(params.getTextDocument())); + handleParsingErrors(open(params.getTextDocument(), timestamp)); triggerAnalyzer(params.getTextDocument(), Duration.ofMillis(800)); } @Override public void didChange(DidChangeTextDocumentParams params) { + var timestamp = System.currentTimeMillis(); logger.debug("Did Change file: {}", params.getTextDocument().getUri()); - updateContents(params.getTextDocument(), last(params.getContentChanges()).getText()); + updateContents(params.getTextDocument(), last(params.getContentChanges()).getText(), timestamp); triggerAnalyzer(params.getTextDocument(), Duration.ofMillis(800)); } @@ -275,10 +282,10 @@ private void triggerBuilder(TextDocumentIdentifier doc) { fileFacts.calculateBuilder(location, getFile(doc).getCurrentTreeAsync()); } - private TextDocumentState updateContents(VersionedTextDocumentIdentifier doc, String newContents) { + private TextDocumentState updateContents(VersionedTextDocumentIdentifier doc, String newContents, long timestamp) { TextDocumentState file = getFile(doc); logger.trace("New contents for {}", doc); - handleParsingErrors(file, file.update(doc.getVersion(), newContents)); + handleParsingErrors(file, file.update(doc.getVersion(), newContents, timestamp)); return file; } @@ -439,9 +446,9 @@ private ParametricFileFacts facts(String doc) { throw new UnsupportedOperationException("Rascal Parametric LSP has no support for this file: " + doc); } - private TextDocumentState open(TextDocumentItem doc) { + private TextDocumentState open(TextDocumentItem doc, long timestamp) { return files.computeIfAbsent(Locations.toLoc(doc), - l -> new TextDocumentState(contributions(doc)::parsing, l, doc.getVersion(), doc.getText()) + l -> new TextDocumentState(contributions(doc)::parsing, l, doc.getVersion(), doc.getText(), timestamp) ); } @@ -683,4 +690,14 @@ public CompletableFuture executeCommand(String languageName, String comm return CompletableFuture.completedFuture(null); } } + + @Override + public boolean isManagingFile(ISourceLocation file) { + return files.containsKey(file.top()); + } + + @Override + public TextDocumentState getDocumentState(ISourceLocation file) { + return files.get(file.top()); + } } 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 ea0d7e413..5daa0ae5e 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 @@ -102,6 +102,7 @@ import org.rascalmpl.library.util.PathConfig; import org.rascalmpl.parser.gtd.exception.ParseError; import org.rascalmpl.uri.URIResolverRegistry; +import org.rascalmpl.uri.URIUtil; import org.rascalmpl.values.parsetrees.ITree; import org.rascalmpl.values.parsetrees.ProductionAdapter; import org.rascalmpl.values.parsetrees.TreeAdapter; @@ -113,6 +114,7 @@ import org.rascalmpl.vscode.lsp.rascal.model.FileFacts; import org.rascalmpl.vscode.lsp.rascal.model.SummaryBridge; import org.rascalmpl.vscode.lsp.terminal.ITerminalIDEServer.LanguageParameter; +import org.rascalmpl.vscode.lsp.uri.FallbackResolver; import org.rascalmpl.vscode.lsp.util.CodeActions; import org.rascalmpl.vscode.lsp.util.Diagnostics; import org.rascalmpl.vscode.lsp.util.DocumentChanges; @@ -143,9 +145,13 @@ public class RascalTextDocumentService implements IBaseTextDocumentService, Lang private @MonotonicNonNull BaseWorkspaceService workspaceService; public RascalTextDocumentService(ExecutorService exec) { + // The following call ensures that URIResolverRegistry is initialized before FallbackResolver is accessed + URIResolverRegistry.getInstance(); + this.ownExecuter = exec; this.documents = new ConcurrentHashMap<>(); this.columns = new ColumnMaps(this::getContents); + FallbackResolver.getInstance().registerTextDocumentService(this); } @Override @@ -200,15 +206,17 @@ public void connect(LanguageClient client) { @Override public void didOpen(DidOpenTextDocumentParams params) { + var timestamp = System.currentTimeMillis(); logger.debug("Open: {}", params.getTextDocument()); - TextDocumentState file = open(params.getTextDocument()); + TextDocumentState file = open(params.getTextDocument(), timestamp); handleParsingErrors(file); } @Override public void didChange(DidChangeTextDocumentParams params) { + var timestamp = System.currentTimeMillis(); logger.trace("Change: {}", params.getTextDocument()); - updateContents(params.getTextDocument(), last(params.getContentChanges()).getText()); + updateContents(params.getTextDocument(), last(params.getContentChanges()).getText(), timestamp); } @Override @@ -230,10 +238,10 @@ public void didSave(DidSaveTextDocumentParams params) { } } - private TextDocumentState updateContents(VersionedTextDocumentIdentifier doc, String newContents) { + private TextDocumentState updateContents(VersionedTextDocumentIdentifier doc, String newContents, long timestamp) { TextDocumentState file = getFile(doc); logger.trace("New contents for {}", doc); - handleParsingErrors(file, file.update(doc.getVersion(), newContents)); + handleParsingErrors(file, file.update(doc.getVersion(), newContents, timestamp)); return file; } @@ -419,9 +427,9 @@ private static T last(List l) { return l.get(l.size() - 1); } - private TextDocumentState open(TextDocumentItem doc) { + private TextDocumentState open(TextDocumentItem doc, long timestamp) { return documents.computeIfAbsent(Locations.toLoc(doc), - l -> new TextDocumentState((loc, input) -> rascalServices.parseSourceFile(loc, input), l, doc.getVersion(), doc.getText())); + l -> new TextDocumentState((loc, input) -> rascalServices.parseSourceFile(loc, input), l, doc.getVersion(), doc.getText(), timestamp)); } private TextDocumentState getFile(TextDocumentIdentifier doc) { @@ -567,6 +575,16 @@ private static CompletableFuture recoverExceptions(CompletableFuture f }); } + @Override + public boolean isManagingFile(ISourceLocation file) { + return documents.containsKey(file.top()); + } + + @Override + public TextDocumentState getDocumentState(ISourceLocation file) { + return documents.get(file.top()); + } + public @MonotonicNonNull FileFacts getFileFacts() { return facts; } 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 dafb3fd04..f11da45bf 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 @@ -50,6 +50,8 @@ import org.rascalmpl.vscode.lsp.util.concurrent.LazyUpdateableReference; import org.rascalmpl.vscode.lsp.util.concurrent.ReplaceableFuture; import org.rascalmpl.vscode.lsp.util.locations.ColumnMaps; +import org.rascalmpl.vscode.lsp.util.locations.Locations; + import io.usethesource.vallang.ISourceLocation; public class FileFacts { @@ -87,7 +89,7 @@ public void reportParseErrors(ISourceLocation file, List msgs) { private FileFact getFile(ISourceLocation l) { ISourceLocation resolved = null; try { - resolved = URIResolverRegistry.getInstance().logicalToPhysical(l); + resolved = Locations.toClientLocation(l); if (resolved == null) { resolved = l; } diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/terminal/TerminalIDEClient.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/terminal/TerminalIDEClient.java index a9430d9b7..a270dd5f2 100644 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/terminal/TerminalIDEClient.java +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/terminal/TerminalIDEClient.java @@ -97,7 +97,7 @@ public void browse(URI uri, String title, int viewColumn) { @Override public void edit(ISourceLocation path) { try { - ISourceLocation physical = URIResolverRegistry.getInstance().logicalToPhysical(path); + ISourceLocation physical = Locations.toClientLocation(path); ShowDocumentParams params = new ShowDocumentParams(physical.getURI().toASCIIString()); params.setTakeFocus(true); 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 ee1da2e3e..4bc53da55 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 @@ -31,21 +31,28 @@ import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; +import java.net.URISyntaxException; import java.nio.charset.StandardCharsets; import java.time.Duration; import java.util.Base64; import java.util.HashMap; +import java.util.List; import java.util.Map; import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletionException; +import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; import java.util.function.Consumer; import java.util.function.Function; + +import org.rascalmpl.uri.ILogicalSourceLocationResolver; import org.rascalmpl.uri.ISourceLocationInputOutput; import org.rascalmpl.uri.ISourceLocationWatcher; import org.rascalmpl.uri.URIUtil; +import org.rascalmpl.vscode.lsp.IBaseTextDocumentService; +import org.rascalmpl.vscode.lsp.TextDocumentState; import org.rascalmpl.vscode.lsp.uri.jsonrpc.VSCodeUriResolverClient; import org.rascalmpl.vscode.lsp.uri.jsonrpc.VSCodeUriResolverServer; import org.rascalmpl.vscode.lsp.uri.jsonrpc.VSCodeVFS; @@ -53,11 +60,28 @@ import org.rascalmpl.vscode.lsp.uri.jsonrpc.messages.ISourceLocationRequest; import org.rascalmpl.vscode.lsp.uri.jsonrpc.messages.WriteFileRequest; import org.rascalmpl.vscode.lsp.util.Lazy; + import com.github.benmanes.caffeine.cache.Cache; import com.github.benmanes.caffeine.cache.Caffeine; + import io.usethesource.vallang.ISourceLocation; -public class FallbackResolver implements ISourceLocationInputOutput, ISourceLocationWatcher { +public class FallbackResolver implements ISourceLocationInputOutput, ISourceLocationWatcher, ILogicalSourceLocationResolver { + + private static FallbackResolver instance = null; + + // The FallbackResolver is dynamically instantiated by URIResolverRegistry. By implementing it as a singleton and + // making it avaible through this method, we allow the IBaseTextDocumentService implementations to interact with it. + public static FallbackResolver getInstance() { + if (instance == null) { + throw new IllegalStateException("FallbackResolver accessed before initialization"); + } + return instance; + } + + public FallbackResolver() { + instance = this; + } private static VSCodeUriResolverServer getServer() throws IOException { var result = VSCodeVFS.INSTANCE.getServer(); @@ -250,5 +274,49 @@ public void unwatch(ISourceLocation root, Consumer watch getClient().removeWatcher(root, watcher, getServer()); } + + public boolean isFileManaged(ISourceLocation file) { + for (var service : textDocumentServices) { + if (service.isManagingFile(file)) { + return true; + } + } + return false; + } + @Override + public ISourceLocation resolve(ISourceLocation input) throws IOException { + if (isFileManaged(input)) { + try { + // The offset/length part of the source location is stripped off here. + // This is reinstated by `URIResolverRegistry::resolveAndFixOffsets` + // during logical resolution + return URIUtil.changeScheme(input.top(), "lsp+" + input.getScheme()); + } catch (URISyntaxException e) { + // fall through + } + } + return input; + } + + @Override + public String authority() { + throw new UnsupportedOperationException("'authority' not supported by fallback resolver"); + } + + private final List textDocumentServices = new CopyOnWriteArrayList<>(); + + public void registerTextDocumentService(IBaseTextDocumentService service) { + textDocumentServices.add(service); + } + + public TextDocumentState getDocumentState(ISourceLocation file) throws IOException { + for (var service : textDocumentServices) { + var state = service.getDocumentState(file); + if (state != null) { + return state; + } + } + throw new IOException("File is not managed by lsp"); + } } diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/uri/LSPOpenFileResolver.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/uri/LSPOpenFileResolver.java new file mode 100644 index 000000000..e13a10b13 --- /dev/null +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/uri/LSPOpenFileResolver.java @@ -0,0 +1,101 @@ +/* + * Copyright (c) 2018-2023, 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.uri; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.net.URISyntaxException; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; + +import org.rascalmpl.uri.ISourceLocationInput; +import org.rascalmpl.uri.URIUtil; + +import io.usethesource.vallang.ISourceLocation; + +public class LSPOpenFileResolver implements ISourceLocationInput { + + @Override + public InputStream getInputStream(ISourceLocation uri) throws IOException { + var fallbackResolver = FallbackResolver.getInstance(); + uri = stripLspPrefix(uri); + return new ByteArrayInputStream(fallbackResolver.getDocumentState(uri).getCurrentContent().get().getBytes(StandardCharsets.UTF_16)); + } + + @Override + public Charset getCharset(ISourceLocation uri) throws IOException { + return StandardCharsets.UTF_16; + } + + @Override + public boolean exists(ISourceLocation uri) { + return FallbackResolver.getInstance().isFileManaged(stripLspPrefix(uri)); + } + + @Override + public long lastModified(ISourceLocation uri) throws IOException { + return FallbackResolver.getInstance().getDocumentState(stripLspPrefix(uri)).getLastModified(); + } + + @Override + public boolean isDirectory(ISourceLocation uri) { + return false; + } + + @Override + public boolean isFile(ISourceLocation uri) { + return exists(uri); + } + + private static ISourceLocation stripLspPrefix(ISourceLocation uri) { + if (uri.getScheme().startsWith("lsp+")) { + try { + return URIUtil.changeScheme(uri, uri.getScheme().substring("lsp+".length())); + } catch (URISyntaxException e) { + // fall through + } + } + return uri; + } + + @Override + public String[] list(ISourceLocation uri) throws IOException { + throw new IOException("`list` is not supported on files"); + } + + @Override + public String scheme() { + return "lsp"; + } + + @Override + public boolean supportsHost() { + return false; + } + +} diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/util/Diagnostics.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/util/Diagnostics.java index f6d9e07a5..a9bf4f887 100644 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/util/Diagnostics.java +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/util/Diagnostics.java @@ -168,7 +168,7 @@ public static Map> translateMessages(IList mes } private static ISourceLocation getMessageLocation(IConstructor message) { - return Locations.toPhysicalIfPossible(((ISourceLocation) message.get("at"))); + return Locations.toClientLocationIfPossible(((ISourceLocation) message.get("at"))); } private static boolean hasValidLocation(IConstructor d, ISourceLocation 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 c9a7b7189..b4f66b357 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 @@ -31,10 +31,16 @@ public class Versioned { private final int version; private final T object; + private final long timestamp; public Versioned(int version, T object) { + this(version, object, System.currentTimeMillis()); + } + + public Versioned(int version, T object, long timestamp) { this.version = version; this.object = object; + this.timestamp = timestamp; } public int version() { @@ -45,6 +51,10 @@ public T get() { return object; } + public long getTimestamp() { + return timestamp; + } + @Override public String toString() { return String.format("%s [version %d]", object, version); diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/util/locations/Locations.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/util/locations/Locations.java index 00b50aadd..0b6c3bff1 100644 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/util/locations/Locations.java +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/util/locations/Locations.java @@ -35,12 +35,41 @@ import org.eclipse.lsp4j.TextDocumentItem; import org.rascalmpl.uri.URIResolverRegistry; import org.rascalmpl.uri.URIUtil; -import org.rascalmpl.values.IRascalValueFactory; import io.usethesource.vallang.ISourceLocation; import io.usethesource.vallang.IValue; +/** + * The Locations class provides utility methods related to source locations and VS Code locations. + * + * Source locations of files that are opened in the IDE are redirected by prefixing the scheme with "lsp+". + * These lsp-redirected source locations must not leak outside of the LSP server. The `toClientLocation` methods + * strip source locations of their "lsp+" prefix. + */ public class Locations { + public static ISourceLocation toClientLocation(ISourceLocation loc) throws IOException { + var result = URIResolverRegistry.getInstance().logicalToPhysical(loc); + if (result.getScheme().startsWith("lsp+")) { + try { + result = URIUtil.changeScheme(result, result.getScheme().substring("lsp+".length())); + } catch (URISyntaxException e) { + // fall through + } + } + return result; + } + + public static ISourceLocation toClientLocationIfPossible(ISourceLocation loc) { + var result = toPhysicalIfPossible(loc); + if (result.getScheme().startsWith("lsp+")) { + try { + return URIUtil.changeScheme(result, result.getScheme().substring("lsp+".length())); + } catch (URISyntaxException e) { + // fall through + } + } + return result; + } public static ISourceLocation toPhysicalIfPossible(ISourceLocation loc) { ISourceLocation physical; try { diff --git a/rascal-vscode-extension/src/test/vscode-suite/ide.test.ts b/rascal-vscode-extension/src/test/vscode-suite/ide.test.ts index 90b75ba3e..d5b4f4a2e 100644 --- a/rascal-vscode-extension/src/test/vscode-suite/ide.test.ts +++ b/rascal-vscode-extension/src/test/vscode-suite/ide.test.ts @@ -270,4 +270,15 @@ describe('IDE', function () { await ide.revertOpenChanges(); } }); + + it("editor contents used for open files", async() => { + const importerEditor = await ide.openModule(TestWorkspace.importerFile); + const importeeEditor = await ide.openModule(TestWorkspace.importeeFile); + + await importeeEditor.typeTextAt(3, 1, "public str foo;"); + await ide.openModule(TestWorkspace.importerFile); + + await ide.triggerTypeChecker(importerEditor, {waitForFinish : true}); + await ide.hasErrorSquiggly(importerEditor); + }); }); diff --git a/rascal-vscode-extension/src/test/vscode-suite/utils.ts b/rascal-vscode-extension/src/test/vscode-suite/utils.ts index 27f77047b..82e615dd7 100644 --- a/rascal-vscode-extension/src/test/vscode-suite/utils.ts +++ b/rascal-vscode-extension/src/test/vscode-suite/utils.ts @@ -61,6 +61,9 @@ export class TestWorkspace { public static readonly libFile = path.join(src(this.libProject), 'Lib.rsc'); public static readonly libFileTpl = path.join(target(this.libProject),'$Lib.tpl'); + public static readonly importerFile = path.join(src(this.testProject), 'Importer.rsc'); + public static readonly importeeFile = path.join(src(this.testProject), 'Importee.rsc'); + public static readonly picoFile = path.join(src(this.testProject, 'pico'), 'testing.pico'); public static readonly picoNewFile = path.join(src(this.testProject, 'pico'), 'testing.pico-new'); } diff --git a/rascal-vscode-extension/test-workspace/test-project/src/main/rascal/Importee.rsc b/rascal-vscode-extension/test-workspace/test-project/src/main/rascal/Importee.rsc new file mode 100644 index 000000000..18e4a45b9 --- /dev/null +++ b/rascal-vscode-extension/test-workspace/test-project/src/main/rascal/Importee.rsc @@ -0,0 +1,3 @@ +module Importee + + diff --git a/rascal-vscode-extension/test-workspace/test-project/src/main/rascal/Importer.rsc b/rascal-vscode-extension/test-workspace/test-project/src/main/rascal/Importer.rsc new file mode 100644 index 000000000..6a24cb67b --- /dev/null +++ b/rascal-vscode-extension/test-workspace/test-project/src/main/rascal/Importer.rsc @@ -0,0 +1,6 @@ +module Importer + +import Importee; + +int x = foo; + \ No newline at end of file