diff --git a/rascal-lsp/.vscode/launch.json b/rascal-lsp/.vscode/launch.json index 9dde3567f..b1df648ea 100644 --- a/rascal-lsp/.vscode/launch.json +++ b/rascal-lsp/.vscode/launch.json @@ -11,7 +11,8 @@ "vmArgs": [ "-Dlog4j2.level=TRACE", "-Drascal.compilerClasspath=${workspaceFolder}/target/lib/rascal.jar", - "-Drascal.fallbackResolver=org.rascalmpl.vscode.lsp.uri.FallbackResolver" + "-Drascal.remoteResolverRegistryPort=8889", + "-Drascal.specializedRemoteResolverRegistryClass=org.rascalmpl.vscode.lsp.uri.jsonrpc.VSCodeFileSystemInRascal" ] }, { @@ -24,7 +25,8 @@ "vmArgs": [ "-Dlog4j2.level=TRACE", "-Drascal.compilerClasspath=${workspaceFolder}/target/lib/rascal.jar", - "-Drascal.fallbackResolver=org.rascalmpl.vscode.lsp.uri.FallbackResolver" + "-Drascal.remoteResolverRegistryPort=8889", + "-Drascal.specializedRemoteResolverRegistryClass=org.rascalmpl.vscode.lsp.uri.jsonrpc.VSCodeFileSystemInRascal" ] } ] diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/BaseLanguageServer.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/BaseLanguageServer.java index 60890c16f..eb77b2cf7 100644 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/BaseLanguageServer.java +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/BaseLanguageServer.java @@ -43,6 +43,7 @@ import java.util.concurrent.ExecutorService; import java.util.function.BiFunction; import java.util.function.Function; + import org.apache.logging.log4j.Level; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; @@ -60,12 +61,14 @@ import org.eclipse.lsp4j.services.LanguageClientAware; import org.rascalmpl.ideservices.GsonUtils; import org.rascalmpl.library.util.PathConfig; +import org.rascalmpl.uri.URIResolverRegistry; +import org.rascalmpl.uri.remote.jsonrpc.RemoteIOError; import org.rascalmpl.vscode.lsp.log.LogRedirectConfiguration; import org.rascalmpl.vscode.lsp.parametric.LanguageRegistry.LanguageParameter; import org.rascalmpl.vscode.lsp.terminal.RemoteIDEServicesThread; -import org.rascalmpl.vscode.lsp.uri.jsonrpc.impl.VSCodeVFSClient; +import org.rascalmpl.vscode.lsp.uri.jsonrpc.RascalFileSystemInVSCode; import org.rascalmpl.vscode.lsp.uri.jsonrpc.messages.PathConfigParameter; -import org.rascalmpl.vscode.lsp.uri.jsonrpc.messages.VFSRegister; +import org.rascalmpl.vscode.lsp.util.Sets; import org.rascalmpl.vscode.lsp.util.concurrent.CompletableFutureUtils; import org.rascalmpl.vscode.lsp.util.locations.Locations; @@ -73,7 +76,7 @@ import io.usethesource.vallang.ISourceLocation; /** -* The main language server class for Rascal is build on top of the Eclipse lsp4j library +* The main language server class for Rascal is built on top of the Eclipse lsp4j library */ @SuppressWarnings("java:S106") // we are using system.in/system.out correctly in this class public abstract class BaseLanguageServer { @@ -119,6 +122,7 @@ private static Launcher constructLSPClient(InputStream in, .setOutput(out) .configureGson(GsonUtils.complexAsJsonObject()) .setExecutorService(threadPool) + .setExceptionHandler(t -> RemoteIOError.translate((Exception) t).getResponseError()) .create(); server.connect(clientLauncher.getRemoteProxy()); @@ -193,7 +197,7 @@ private static void startLSP(Launcher server) { } } } - private static class ActualLanguageServer implements IBaseLanguageServerExtensions, LanguageClientAware { + private static class ActualLanguageServer extends RascalFileSystemInVSCode implements IBaseLanguageServerExtensions, LanguageClientAware { static final Logger logger = LogManager.getLogger(ActualLanguageServer.class); private final IBaseTextDocumentService lspDocumentService; private final BaseWorkspaceService lspWorkspaceService; @@ -307,15 +311,11 @@ public void connect(LanguageClient client) { var actualClient = (IBaseLanguageClient) client; lspDocumentService.connect(actualClient); lspWorkspaceService.connect(actualClient); + provideClient(actualClient); remoteIDEServicesConfiguration = RemoteIDEServicesThread.startRemoteIDEServicesServer(client, lspDocumentService, executor); logger.debug("Remote IDE Services Port {}", remoteIDEServicesConfiguration); } - @Override - public void registerVFS(VFSRegister registration) { - VSCodeVFSClient.buildAndRegister(registration.getPort()); - } - @Override public void cancelProgress(WorkDoneProgressCancelParams params) { lspDocumentService.cancelProgress(params.getToken().getLeft()); @@ -326,5 +326,14 @@ public void setMinimumLogLevel(String level) { final var l = Level.toLevel(level, Level.DEBUG); // fall back to debug when the string cannot be mapped Configurator.setRootLevel(l); } + + @Override + public CompletableFuture fileSystemSchemes() { + var reg = URIResolverRegistry.getInstance(); + var inputs = reg.getRegisteredInputSchemes(); + var logicals = reg.getRegisteredLogicalSchemes(); + + return CompletableFuture.completedFuture(Sets.union(inputs, logicals).toArray(String[]::new)); + } } } diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/IBaseLanguageClient.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/IBaseLanguageClient.java index 5a437a739..a5a0d068b 100644 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/IBaseLanguageClient.java +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/IBaseLanguageClient.java @@ -31,35 +31,38 @@ import org.checkerframework.checker.nullness.qual.Nullable; import org.eclipse.lsp4j.Range; import org.eclipse.lsp4j.jsonrpc.services.JsonNotification; +import org.eclipse.lsp4j.jsonrpc.services.JsonSegment; import org.eclipse.lsp4j.services.LanguageClient; +import org.rascalmpl.uri.vfs.IRemoteResolverRegistryClient; import org.rascalmpl.vscode.lsp.parametric.LanguageRegistry.LanguageParameter; import io.usethesource.vallang.IInteger; import io.usethesource.vallang.IString; -public interface IBaseLanguageClient extends LanguageClient { - @JsonNotification("rascal/showContent") +@JsonSegment("rascal") +public interface IBaseLanguageClient extends LanguageClient, IRemoteResolverRegistryClient { + @JsonNotification void showContent(URI uri, IString title, IInteger viewColumn); - @JsonNotification("rascal/receiveRegisterLanguage") + @JsonNotification void receiveRegisterLanguage(LanguageParameter lang); - @JsonNotification("rascal/receiveUnregisterLanguage") + @JsonNotification void receiveUnregisterLanguage(LanguageParameter lang); - @JsonNotification("rascal/editDocument") + @JsonNotification void editDocument(URI uri, @Nullable Range range, int viewColumn); /** * Notification sent to the vscode client to start a debugging session on the given debug adapter port */ - @JsonNotification("rascal/startDebuggingSession") + @JsonNotification void startDebuggingSession(int serverPort); /** * Notification sent to the vscode client to register the port on which the debug adapter server is listening * It is then used to make the link between a terminal process ID and the corresponding debug server port */ - @JsonNotification("rascal/registerDebugServerPort") + @JsonNotification void registerDebugServerPort(int processID, int serverPort); } diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/IBaseLanguageServerExtensions.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/IBaseLanguageServerExtensions.java index 47693efef..c9790e831 100644 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/IBaseLanguageServerExtensions.java +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/IBaseLanguageServerExtensions.java @@ -32,35 +32,36 @@ import org.eclipse.lsp4j.jsonrpc.messages.Tuple.Two; import org.eclipse.lsp4j.jsonrpc.services.JsonNotification; import org.eclipse.lsp4j.jsonrpc.services.JsonRequest; +import org.eclipse.lsp4j.jsonrpc.services.JsonSegment; import org.eclipse.lsp4j.services.LanguageServer; import org.rascalmpl.vscode.lsp.parametric.LanguageRegistry.LanguageParameter; import org.rascalmpl.vscode.lsp.uri.jsonrpc.messages.PathConfigParameter; -import org.rascalmpl.vscode.lsp.uri.jsonrpc.messages.VFSRegister; -public interface IBaseLanguageServerExtensions extends LanguageServer, IRascalFileSystemServices { - @JsonNotification("rascal/supplyRemoteIDEServicesConfiguration") +@JsonSegment("rascal") +public interface IBaseLanguageServerExtensions extends LanguageServer { + @JsonNotification default CompletableFuture supplyRemoteIDEServicesConfiguration() { throw new UnsupportedOperationException(); } - @JsonRequest("rascal/sendRegisterLanguage") + @JsonRequest default CompletableFuture sendRegisterLanguage(LanguageParameter lang) { throw new UnsupportedOperationException(); } - @JsonRequest("rascal/sendUnregisterLanguage") + @JsonRequest default CompletableFuture sendUnregisterLanguage(LanguageParameter lang) { throw new UnsupportedOperationException(); } - @JsonRequest("rascal/supplyPathConfig") + @JsonRequest default CompletableFuture[]> supplyPathConfig(PathConfigParameter projectFolder) { throw new UnsupportedOperationException(); } - @JsonNotification("rascal/vfs/register") - void registerVFS(VFSRegister registration); - - @JsonNotification("rascal/logLevel") + @JsonNotification("logLevel") void setMinimumLogLevel(String level); + + @JsonRequest("vfs/schemes") + CompletableFuture fileSystemSchemes(); } 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 deleted file mode 100644 index f450ed621..000000000 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/IRascalFileSystemServices.java +++ /dev/null @@ -1,682 +0,0 @@ -/* - * 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.InputStream; -import java.io.OutputStream; -import java.net.URISyntaxException; -import java.nio.charset.StandardCharsets; -import java.nio.file.FileAlreadyExistsException; -import java.nio.file.NotDirectoryException; -import java.util.Arrays; -import java.util.Base64; -import java.util.Set; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.CompletionException; -import java.util.concurrent.ExecutorService; -import java.util.stream.Stream; -import org.checkerframework.checker.nullness.qual.EnsuresNonNullIf; -import org.checkerframework.checker.nullness.qual.Nullable; -import org.apache.commons.codec.binary.Base64InputStream; -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; -import org.eclipse.lsp4j.jsonrpc.ResponseErrorException; -import org.eclipse.lsp4j.jsonrpc.messages.ResponseError; -import org.eclipse.lsp4j.jsonrpc.services.JsonNotification; -import org.eclipse.lsp4j.jsonrpc.services.JsonRequest; -import org.eclipse.lsp4j.jsonrpc.validation.NonNull; -import org.rascalmpl.library.Prelude; -import org.rascalmpl.uri.ISourceLocationWatcher.ISourceLocationChangeType; -import org.rascalmpl.uri.ISourceLocationWatcher.ISourceLocationChanged; -import org.rascalmpl.uri.URIResolverRegistry; -import org.rascalmpl.uri.URIUtil; -import org.rascalmpl.uri.UnsupportedSchemeException; -import org.rascalmpl.values.IRascalValueFactory; -import org.rascalmpl.vscode.lsp.util.NamedThreadPool; -import org.rascalmpl.vscode.lsp.util.locations.Locations; -import io.usethesource.vallang.ISourceLocation; -import io.usethesource.vallang.IValueFactory; - -public interface IRascalFileSystemServices { - static final URIResolverRegistry reg = URIResolverRegistry.getInstance(); - static final Logger IRascalFileSystemServices__logger = LogManager.getLogger(IRascalFileSystemServices.class); - static final ExecutorService executor = NamedThreadPool.cachedDaemon("rascal-vfs"); - - @JsonRequest("rascal/filesystem/resolveLocation") - default CompletableFuture resolveLocation(SourceLocation loc) { - return CompletableFuture.supplyAsync(() -> { - try { - ISourceLocation tmp = loc.toRascalLocation(); - - ISourceLocation resolved = Locations.toClientLocation(tmp); - - if (resolved == null) { - return loc; - } - - return SourceLocation.fromRascalLocation(resolved); - } catch (Exception e) { - IRascalFileSystemServices__logger.warn("Could not resolve location {}", loc, e); - return loc; - } - }, executor); - } - - @JsonRequest("rascal/filesystem/watch") - default CompletableFuture watch(WatchParameters params) { - return CompletableFuture.runAsync(() -> { - try { - ISourceLocation loc = params.getLocation(); - - URIResolverRegistry.getInstance().watch(loc, params.isRecursive(), changed -> { - try { - onDidChangeFile(convertChangeEvent(changed)); - } catch (IOException e) { - throw new RuntimeException(e); - } - }); - } catch (IOException | URISyntaxException | RuntimeException e) { - throw new VSCodeFSError(e); - } - }, executor); - } - - static FileChangeEvent convertChangeEvent(ISourceLocationChanged changed) throws IOException { - return new FileChangeEvent(convertFileChangeType(changed.getChangeType()), - Locations.toUri(changed.getLocation()).toASCIIString()); - } - - static FileChangeType convertFileChangeType(ISourceLocationChangeType changeType) throws IOException { - switch (changeType) { - case CREATED: - return FileChangeType.Created; - case DELETED: - return FileChangeType.Deleted; - case MODIFIED: - return FileChangeType.Changed; - default: - throw new IOException("unknown change type: " + changeType); - } - } - - private static boolean readonly(ISourceLocation loc) throws IOException { - if (reg.getRegisteredOutputSchemes().contains(loc.getScheme())) { - return false; - } - if (reg.getRegisteredLogicalSchemes().contains(loc.getScheme())) { - var resolved = Locations.toClientLocation(loc); - if (resolved != null && resolved != loc) { - return readonly(resolved); - } - return true; - } - return true; - } - - @JsonRequest("rascal/filesystem/stat") - default CompletableFuture stat(URIParameter uri) { - return CompletableFuture.supplyAsync(() -> { - try { - ISourceLocation loc = uri.getLocation(); - if (!reg.exists(loc)) { - throw new FileNotFoundException(); - } - var created = reg.created(loc); - var lastModified = reg.lastModified(loc); - if (reg.isDirectory(loc)) { - return new FileStat(FileType.Directory, created, lastModified, 0, null); - } - long size = 0; - if (reg.supportsReadableFileChannel(loc)) { - try (var c = reg.getReadableFileChannel(loc)) { - size = c.size(); - } - } - else { - size = Prelude.__getFileSize(IRascalValueFactory.getInstance(), loc).longValue(); - } - return new FileStat(FileType.File, created, lastModified, size, readonly(loc) ? FilePermission.Readonly : null); - } catch (IOException | URISyntaxException | RuntimeException e) { - throw new VSCodeFSError(e); - } - }, executor); - } - - @JsonRequest("rascal/filesystem/readDirectory") - default CompletableFuture readDirectory(URIParameter uri) { - return CompletableFuture.supplyAsync(() -> { - try { - ISourceLocation loc = uri.getLocation(); - if (!reg.isDirectory(loc)) { - throw VSCodeFSError.notADirectory(loc); - } - return Arrays.stream(reg.list(loc)).map(l -> new FileWithType(URIUtil.getLocationName(l), - reg.isDirectory(l) ? FileType.Directory : FileType.File)).toArray(FileWithType[]::new); - } catch (IOException | URISyntaxException | RuntimeException e) { - throw new VSCodeFSError(e); - } - }, executor); - } - - @JsonRequest("rascal/filesystem/createDirectory") - default CompletableFuture createDirectory(URIParameter uri) { - return CompletableFuture.runAsync(() -> { - try { - reg.mkDirectory(uri.getLocation()); - } catch (IOException | URISyntaxException | RuntimeException e) { - throw new VSCodeFSError(e); - } - }, executor); - } - - @JsonRequest("rascal/filesystem/readFile") - default CompletableFuture readFile(URIParameter uri) { - return CompletableFuture.supplyAsync(() -> { - try (InputStream source = new Base64InputStream(reg.getInputStream(uri.getLocation()), true)) { - return new LocationContent(new String(source.readAllBytes(), StandardCharsets.US_ASCII)); - } catch (IOException | URISyntaxException | RuntimeException e) { - throw new VSCodeFSError(e); - } - }, executor); - } - - @JsonRequest("rascal/filesystem/writeFile") - default CompletableFuture writeFile(WriteFileParameters params) { - return CompletableFuture.runAsync(() -> { - try { - ISourceLocation loc = params.getLocation(); - - boolean fileExists = reg.exists(loc); - if (!fileExists && !params.isCreate()) { - throw new FileNotFoundException(loc.toString()); - } - if (fileExists && reg.isDirectory(loc)) { - throw VSCodeFSError.isADirectory(loc); - } - - ISourceLocation parentFolder = URIUtil.getParentLocation(loc); - if (!reg.exists(parentFolder) && params.isCreate()) { - throw new FileNotFoundException(parentFolder.toString()); - } - - if (fileExists && params.isCreate() && !params.isOverwrite()) { - throw new FileAlreadyExistsException(loc.toString()); - } - try (OutputStream target = reg.getOutputStream(loc, false)) { - target.write(Base64.getDecoder().decode(params.getContent())); - } - } catch (IOException | URISyntaxException | RuntimeException e) { - throw new VSCodeFSError(e); - } - }, executor); - } - - @JsonRequest("rascal/filesystem/delete") - default CompletableFuture delete(DeleteParameters params) { - return CompletableFuture.runAsync(() -> { - try { - ISourceLocation loc = params.getLocation(); - reg.remove(loc, params.isRecursive()); - } catch (IOException | URISyntaxException e) { - throw new CompletionException(e); - } - }, executor); - } - - @JsonRequest("rascal/filesystem/rename") - default CompletableFuture rename(RenameParameters params) { - return CompletableFuture.runAsync(() -> { - try { - ISourceLocation oldLoc = params.getOldLocation(); - ISourceLocation newLoc = params.getNewLocation(); - reg.rename(oldLoc, newLoc, params.isOverwrite()); - } catch (IOException | URISyntaxException e) { - throw new CompletionException(e); - } - }, executor); - } - - @JsonRequest("rascal/filesystem/schemes") - default CompletableFuture fileSystemSchemes() { - Set inputs = reg.getRegisteredInputSchemes(); - Set logicals = reg.getRegisteredLogicalSchemes(); - - return CompletableFuture - .completedFuture(Stream.concat(inputs.stream(), logicals.stream()).toArray(String[]::new)); - } - - @JsonNotification("rascal/filesystem/onDidChangeFile") - default void onDidChangeFile(FileChangeEvent event) { } - - - public static class DeleteParameters { - private final String uri; - private final boolean recursive; - - public DeleteParameters(String uri, boolean recursive) { - this.uri = uri; - this.recursive = recursive; - } - - public ISourceLocation getLocation() throws URISyntaxException { - return new URIParameter(uri).getLocation(); - } - - public boolean isRecursive() { - return recursive; - } - } - - public static class RenameParameters { - private final String oldUri; - private final String newUri; - private final boolean overwrite; - - public RenameParameters(String oldUri, String newUri, boolean overwrite) { - this.oldUri = oldUri; - this.newUri = newUri; - this.overwrite = overwrite; - } - - public ISourceLocation getOldLocation() throws URISyntaxException { - return new URIParameter(oldUri).getLocation(); - } - - public ISourceLocation getNewLocation() throws URISyntaxException { - return new URIParameter(newUri).getLocation(); - } - - public boolean isOverwrite() { - return overwrite; - } - } - - public static class WatchParameters { - private final String uri; - private final boolean recursive; - private final String[] excludes; - - public WatchParameters(String uri, boolean recursive, String[] excludes) { - this.uri = uri; - this.recursive = recursive; - this.excludes = excludes; - } - - public ISourceLocation getLocation() throws URISyntaxException { - return new URIParameter(uri).getLocation(); - } - - public String[] getExcludes() { - return excludes; - } - - public boolean isRecursive() { - return recursive; - } - } - - public static class SourceLocation { - @NonNull private final String uri; - private final int @Nullable[] offsetLength; - private final int @Nullable[] beginLineColumn; - private final int @Nullable[] endLineColumn; - - public static SourceLocation fromRascalLocation(ISourceLocation loc) { - if (loc.hasOffsetLength()) { - if (loc.hasLineColumn()) { - return new SourceLocation(Locations.toUri(loc).toString(), loc.getOffset(), loc.getLength(), loc.getBeginLine(), loc.getBeginColumn(), loc.getEndLine(), loc.getEndColumn()); - } - else { - return new SourceLocation(Locations.toUri(loc).toString(), loc.getOffset(), loc.getLength()); - } - } - else { - return new SourceLocation(Locations.toUri(loc).toString()); - } - } - - public ISourceLocation toRascalLocation() throws URISyntaxException { - final IValueFactory VF = IRascalValueFactory.getInstance(); - ISourceLocation tmp = Locations.toCheckedLoc(uri); - - if (hasOffsetLength()) { - if (hasLineColumn()) { - tmp = VF.sourceLocation(tmp,getOffset(), getLength(), getBeginLine(), getEndLine(), getBeginColumn(), getEndColumn()); - } - else { - tmp = VF.sourceLocation(tmp, getOffset(), getLength()); - } - } - - return tmp; - } - - private SourceLocation(String uri, int offset, int length, int beginLine, int beginColumn, int endLine, int endColumn) { - this.uri = uri; - this.offsetLength = new int[] {offset, length}; - this.beginLineColumn = new int [] {beginLine, beginColumn}; - this.endLineColumn = new int [] {endLine, endColumn}; - } - - private SourceLocation(String uri, int offset, int length) { - this.uri = uri; - this.offsetLength = new int[] {offset, length}; - this.beginLineColumn = null; - this.endLineColumn = null; - } - - private SourceLocation(String uri) { - this.uri = uri; - this.offsetLength = null; - this.beginLineColumn = null; - this.endLineColumn = null; - } - - public String getUri() { - return uri; - } - - @EnsuresNonNullIf(expression = "this.offsetLength", result = true) - public boolean hasOffsetLength() { - return offsetLength != null; - } - - @EnsuresNonNullIf(expression = "this.endLineColumn", result = true) - @EnsuresNonNullIf(expression = "this.beginLineColumn", result = true) - public boolean hasLineColumn() { - return beginLineColumn != null && endLineColumn != null; - } - - public int getOffset() { - if (!hasOffsetLength()) { - throw new IllegalStateException("This location has no offset"); - } - return offsetLength[0]; - } - - public int getLength() { - if (!hasOffsetLength()) { - throw new IllegalStateException("This location has no length"); - } - return offsetLength[1]; - } - - public int getBeginLine() { - if (!hasLineColumn()) { - throw new IllegalStateException("This location has no line and columns"); - } - return beginLineColumn[0]; - } - - public int getBeginColumn() { - if (!hasLineColumn()) { - throw new IllegalStateException("This location has no line and columns"); - } - return beginLineColumn[1]; - } - - public int getEndLine() { - if (!hasLineColumn()) { - throw new IllegalStateException("This location has no line and columns"); - } - return endLineColumn[0]; - } - - public int getEndColumn() { - if (!hasLineColumn()) { - throw new IllegalStateException("This location has no line and columns"); - } - return endLineColumn[1]; - } - } - - public static class FileChangeEvent { - @NonNull private final FileChangeType type; - @NonNull private final String uri; - - public FileChangeEvent(FileChangeType type, @NonNull String uri) { - this.type = type; - this.uri = uri; - } - - public FileChangeType getType() { - return type; - } - - public ISourceLocation getLocation() throws URISyntaxException { - return new URIParameter(uri).getLocation(); - } - } - - public enum FileChangeType { - Changed(1), Created(2), Deleted(3); - - private final int value; - - private FileChangeType(int val) { - assert val == 1 || val == 2 || val == 3; - this.value = val; - } - - public int getValue() { - return value; - } - } - - // The fields of are only used on the TS side - @SuppressWarnings("unused") - public static class FileStat { - private final FileType type; - private final long ctime; - private final long mtime; - private final long size; - - private @Nullable FilePermission permissions; - - public FileStat(FileType type, long ctime, long mtime, long size, @Nullable FilePermission permissions) { - this.type = type; - this.ctime = ctime; - this.mtime = mtime; - this.size = size; - this.permissions = permissions; - } - - } - - public enum FileType { - Unknown(0), File(1), Directory(2), SymbolicLink(64); - - private final int value; - - private FileType(int val) { - assert val == 0 || val == 1 || val == 2 || val == 64; - this.value = val; - } - - public int getValue() { - return value; - } - } - - // this enum models the enum inside vscode, which in the future might become an enum flag - // in that case we have to solve that - public enum FilePermission { - Readonly(1); - private final int value; - private FilePermission(int val) { - assert val == 1; - this.value = val; - } - - public int getValue() { - return value; - } - } - - public static class FileWithType { - @NonNull private final String name; - @NonNull private final FileType type; - - public FileWithType(@NonNull String name, @NonNull FileType type) { - this.name = name; - this.type = type; - } - - public String getName() { - return name; - } - - public FileType getType() { - return type; - } - } - - public static class LocationContent { - @NonNull private final String content; - - public LocationContent(@NonNull String content) { - this.content = content; - } - - public String getContent() { - return content; - } - } - - public static class URIParameter { - @NonNull private final String uri; - - public URIParameter(@NonNull String uri) { - this.uri = uri; - } - - public String getUri() { - return uri; - } - - public ISourceLocation getLocation() throws URISyntaxException { - return Locations.toCheckedLoc(uri); - } - } - - public static class WriteFileParameters { - @NonNull private final String uri; - @NonNull private final String content; - private final boolean create; - private final boolean overwrite; - - public WriteFileParameters(@NonNull String uri, @NonNull String content, boolean create, boolean overwrite) { - this.uri = uri; - this.content = content; - this.create = create; - this.overwrite = overwrite; - } - - public String getUri() { - return uri; - } - - public ISourceLocation getLocation() throws URISyntaxException { - return new URIParameter(uri).getLocation(); - } - - public String getContent() { - return content; - } - - public boolean isCreate() { - return create; - } - - public boolean isOverwrite() { - return overwrite; - } - } - - /** Maps common exceptions to FileSystemError in VS Code */ - public static class VSCodeFSError extends ResponseErrorException { - public VSCodeFSError(Exception original) { - super(translate(original)); - } - - private static ResponseError fileExists(Object data) { - return new ResponseError(-1, "File exists", data); - } - private static ResponseError fileIsADirectory(Object data) { - return new ResponseError(-2, "File is a directory", data); - } - private static ResponseError fileNotADirectory(Object data) { - return new ResponseError(-3, "File is not a directory", data); - } - private static ResponseError fileNotFound(Object data) { - return new ResponseError(-4, "File is not found", data); - } - private static ResponseError noPermissions(Object data) { - return new ResponseError(-5, "No permissions", data); - } - @SuppressWarnings("unused") - private static ResponseError unavailable(Object data) { - return new ResponseError(-6, "Unavailable", data); - } - - private static ResponseError generic(@Nullable String message, Object data) { - return new ResponseError(-99, message == null ? "no error message was provided" : message, data); - } - - public static ResponseErrorException notADirectory(Object data) { - return new ResponseErrorException(fileNotADirectory(data)); - } - - public static ResponseErrorException isADirectory(Object data) { - return new ResponseErrorException(fileIsADirectory(data)); - } - - private static ResponseError translate(Exception original) { - if (original instanceof FileNotFoundException - || original instanceof UnsupportedSchemeException - || original instanceof URISyntaxException - ) { - return fileNotFound(original); - } - else if (original instanceof FileAlreadyExistsException) { - return fileExists(original); - } - else if (original instanceof NotDirectoryException) { - return fileNotADirectory(original); - } - else if (original instanceof SecurityException) { - return noPermissions(original); - } - else if (original instanceof ResponseErrorException) { - return ((ResponseErrorException)original).getResponseError(); - } - return generic(original.getMessage(), original); - } - } -} diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/LanguageRegistry.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/LanguageRegistry.java index 27720ff3b..8df1b9786 100644 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/LanguageRegistry.java +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/LanguageRegistry.java @@ -33,6 +33,7 @@ import org.checkerframework.checker.nullness.qual.Nullable; import org.eclipse.lsp4j.jsonrpc.services.JsonRequest; +import org.eclipse.lsp4j.jsonrpc.services.JsonSegment; import org.rascalmpl.values.IRascalValueFactory; import io.usethesource.vallang.IConstructor; @@ -46,11 +47,12 @@ /** * Interface of the Language Registry */ +@JsonSegment("rascal") public interface LanguageRegistry { - @JsonRequest("rascal/receiveRegisterLanguage") + @JsonRequest("receiveRegisterLanguage") CompletableFuture registerLanguage(LanguageParameter lang); - @JsonRequest("rascal/receiveUnregisterLanguage") + @JsonRequest("receiveUnregisterLanguage") CompletableFuture unregisterLanguage(LanguageParameter lang); public static class LanguageParameter { diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/ParametricLanguageServer.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/ParametricLanguageServer.java index 5913f1a12..dec7c378c 100644 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/ParametricLanguageServer.java +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/ParametricLanguageServer.java @@ -27,9 +27,9 @@ package org.rascalmpl.vscode.lsp.parametric; +import org.rascalmpl.util.NamedThreadPool; import org.rascalmpl.vscode.lsp.BaseLanguageServer; import org.rascalmpl.vscode.lsp.parametric.LanguageRegistry.LanguageParameter; -import org.rascalmpl.vscode.lsp.util.NamedThreadPool; import com.google.gson.GsonBuilder; 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..7515e1ba0 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 @@ -156,7 +156,7 @@ import org.rascalmpl.vscode.lsp.rascal.conversion.KeywordParameter; import org.rascalmpl.vscode.lsp.rascal.conversion.SelectionRanges; import org.rascalmpl.vscode.lsp.rascal.conversion.SemanticTokenizer; -import org.rascalmpl.vscode.lsp.uri.FallbackResolver; +import org.rascalmpl.vscode.lsp.uri.LSPOpenFileRedirector; import org.rascalmpl.vscode.lsp.util.Maps; import org.rascalmpl.vscode.lsp.util.Versioned; import org.rascalmpl.vscode.lsp.util.concurrent.CompletableFutureUtils; @@ -210,9 +210,6 @@ public class ParametricTextDocumentService implements IBaseTextDocumentService, @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); @@ -224,7 +221,7 @@ public ParametricTextDocumentService(ExecutorService exec, @Nullable LanguagePar this.dedicatedLanguageName = dedicatedLanguage.getName(); this.dedicatedLanguage = dedicatedLanguage; } - FallbackResolver.getInstance().registerTextDocumentService(this); + LSPOpenFileRedirector.getInstance().registerTextDocumentService(this); } @Override diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/ParserOnlyContribution.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/ParserOnlyContribution.java index 72dc57b03..9cdef62d9 100644 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/ParserOnlyContribution.java +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/ParserOnlyContribution.java @@ -32,6 +32,7 @@ import java.io.Writer; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ExecutorService; + import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.checkerframework.checker.nullness.qual.Nullable; diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/RascalInterface.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/RascalInterface.java index e9fb65553..5a303d22d 100644 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/RascalInterface.java +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/RascalInterface.java @@ -37,9 +37,9 @@ import org.rascalmpl.debug.IRascalMonitor; import org.rascalmpl.exceptions.RuntimeExceptionFactory; import org.rascalmpl.uri.URIUtil; +import org.rascalmpl.util.NamedThreadPool; import org.rascalmpl.values.parsetrees.ITree; import org.rascalmpl.vscode.lsp.parametric.LanguageRegistry.LanguageParameter; -import org.rascalmpl.vscode.lsp.util.NamedThreadPool; import org.rascalmpl.vscode.lsp.util.locations.impl.TreeSearch; import io.usethesource.vallang.IConstructor; diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/model/ParametricSummary.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/model/ParametricSummary.java index 6ec7c57f7..bbfd6adb4 100644 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/model/ParametricSummary.java +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/model/ParametricSummary.java @@ -43,6 +43,7 @@ import org.eclipse.lsp4j.Position; import org.eclipse.lsp4j.Range; import org.eclipse.lsp4j.jsonrpc.messages.Either; +import org.rascalmpl.util.Lazy; import org.rascalmpl.util.locations.ColumnMaps; import org.rascalmpl.values.IRascalValueFactory; import org.rascalmpl.values.parsetrees.ITree; @@ -54,7 +55,6 @@ import org.rascalmpl.vscode.lsp.parametric.model.RascalADTs.SummaryFields; import org.rascalmpl.vscode.lsp.rascal.conversion.Diagnostics; import org.rascalmpl.vscode.lsp.rascal.conversion.KeywordParameter; -import org.rascalmpl.vscode.lsp.util.Lazy; import org.rascalmpl.vscode.lsp.util.Versioned; import org.rascalmpl.vscode.lsp.util.concurrent.InterruptibleFuture; import org.rascalmpl.vscode.lsp.util.locations.IRangeMap; diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/rascal/RascalLanguageServer.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/rascal/RascalLanguageServer.java index e36353df8..8cb387529 100644 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/rascal/RascalLanguageServer.java +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/rascal/RascalLanguageServer.java @@ -29,8 +29,8 @@ import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; +import org.rascalmpl.util.NamedThreadPool; import org.rascalmpl.vscode.lsp.BaseLanguageServer; -import org.rascalmpl.vscode.lsp.util.NamedThreadPool; public class RascalLanguageServer extends BaseLanguageServer { public static void main(String[] args) { 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..ed0c1daef 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 @@ -130,7 +130,7 @@ import org.rascalmpl.vscode.lsp.rascal.conversion.SemanticTokenizer; import org.rascalmpl.vscode.lsp.rascal.model.FileFacts; import org.rascalmpl.vscode.lsp.rascal.model.SummaryBridge; -import org.rascalmpl.vscode.lsp.uri.FallbackResolver; +import org.rascalmpl.vscode.lsp.uri.LSPOpenFileRedirector; import org.rascalmpl.vscode.lsp.util.Versioned; import org.rascalmpl.vscode.lsp.util.concurrent.CompletableFutureUtils; import org.rascalmpl.vscode.lsp.util.locations.Locations; @@ -161,13 +161,10 @@ public class RascalTextDocumentService implements IBaseTextDocumentService, Lang @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); + LSPOpenFileRedirector.getInstance().registerTextDocumentService(this); } diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/rascal/conversion/Diagnostics.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/rascal/conversion/Diagnostics.java index c5ba8b86f..39e5c2f64 100644 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/rascal/conversion/Diagnostics.java +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/rascal/conversion/Diagnostics.java @@ -277,7 +277,7 @@ private static boolean hasValidLocation(IConstructor d) { return isValidLocation(getMessageLocation(d), d); } - private static boolean isValidLocation( ISourceLocation loc, IValue m) { + private static boolean isValidLocation(ISourceLocation loc, IValue m) { if (loc == null || loc.getScheme().equals("unknown")) { logger.trace("Dropping diagnostic due to incorrect location on message: {}", m); return false; @@ -291,7 +291,7 @@ private static boolean isValidLocation( ISourceLocation loc, IValue m) { if (loc.getPath().endsWith("/pom.xml")) { return true; } - logger.error("Filtering diagnostic as it's an unsupported file to report diagnostics on: {}", m); + logger.error("Filtering diagnostic as it's an unsupported file to report diagnostics on ({}): {}", loc, m); return false; } } diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/rascal/model/SummaryBridge.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/rascal/model/SummaryBridge.java index a5021c7c7..f9ee2103e 100644 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/rascal/model/SummaryBridge.java +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/rascal/model/SummaryBridge.java @@ -35,11 +35,11 @@ import org.eclipse.lsp4j.Location; import org.eclipse.lsp4j.Position; import org.eclipse.lsp4j.Range; +import org.rascalmpl.util.Lazy; import org.rascalmpl.util.locations.ColumnMaps; import org.rascalmpl.values.IRascalValueFactory; import org.rascalmpl.values.ValueFactoryFactory; import org.rascalmpl.vscode.lsp.rascal.conversion.KeywordParameter; -import org.rascalmpl.vscode.lsp.util.Lazy; import org.rascalmpl.vscode.lsp.util.locations.IRangeMap; import org.rascalmpl.vscode.lsp.util.locations.Locations; import org.rascalmpl.vscode.lsp.util.locations.impl.TreeMapLookup; diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/terminal/LSPTerminalREPL.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/terminal/LSPTerminalREPL.java deleted file mode 100644 index eda3eba58..000000000 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/terminal/LSPTerminalREPL.java +++ /dev/null @@ -1,62 +0,0 @@ -/* - * 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.terminal; - -import java.io.IOException; -import java.util.ArrayList; - -import org.rascalmpl.repl.rascal.RascalInterpreterREPL; -import org.rascalmpl.shell.RascalShell; -import org.rascalmpl.vscode.lsp.uri.jsonrpc.impl.VSCodeVFSClient; - -/** - * This class runs a Rascal terminal REPL that - * connects to a running LSP server instance to - * provide IDE feature to the user of a terminal instance. - */ -public class LSPTerminalREPL extends RascalInterpreterREPL { - public static void main(String[] args) throws IOException { - int vfsPort = -1; - - var rascalShellArgs = new ArrayList(); - - for (int i = 0; i < args.length; i++) { - if (args[i].equals("--vfsPort")) { - vfsPort = Integer.parseInt(args[++i]); - } else { - rascalShellArgs.add(args[i]); - } - } - - if (vfsPort != -1) { - VSCodeVFSClient.buildAndRegister(vfsPort); - } - - RascalShell.main(rascalShellArgs.toArray(new String[rascalShellArgs.size()])); - } -} - 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 deleted file mode 100644 index a722f327c..000000000 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/uri/FallbackResolver.java +++ /dev/null @@ -1,388 +0,0 @@ -/* - * 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.uri; - -import java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.net.URISyntaxException; -import java.nio.charset.StandardCharsets; -import java.nio.file.AccessDeniedException; -import java.nio.file.FileAlreadyExistsException; -import java.nio.file.NoSuchFileException; -import java.nio.file.NotDirectoryException; -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.checkerframework.checker.initialization.qual.UnderInitialization; -import org.checkerframework.checker.nullness.qual.MonotonicNonNull; -import org.eclipse.lsp4j.jsonrpc.ResponseErrorException; -import org.rascalmpl.uri.FileAttributes; -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; -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 com.google.gson.JsonPrimitive; -import io.usethesource.vallang.ISourceLocation; - -public class FallbackResolver implements ISourceLocationInputOutput, ISourceLocationWatcher, ILogicalSourceLocationResolver { - - private static @MonotonicNonNull 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(); - if (result == null) { - throw new IOException("Missing VFS file server"); - } - return result; - } - - private static VSCodeUriResolverClient getClient() throws IOException { - var result = VSCodeVFS.INSTANCE.getClient(); - if (result == null) { - throw new IOException("Missing VFS file client"); - } - return result; - } - - private static T call(Function> target) throws IOException { - try { - return target.apply(getServer()).get(5, TimeUnit.MINUTES); - } - catch (TimeoutException te) { - throw new IOException("VSCode took too long to reply, interruption to avoid deadlocks"); - } - catch(InterruptedException ie) { - Thread.currentThread().interrupt(); - throw new UnsupportedOperationException("Thread should have been interrupted"); - } - catch (CompletionException | ExecutionException ce) { - var cause = ce.getCause(); - if (cause != null) { - if (cause instanceof ResponseErrorException) { - throw translateException((ResponseErrorException)cause); - } - throw new IOException(cause); - } - throw new IOException(ce); - } - } - - - private ISourceLocationRequest param(ISourceLocation uri) { - return new ISourceLocationRequest(uri); - } - - @Override - public InputStream getInputStream(ISourceLocation uri) throws IOException { - var fileBody = call(s -> s.readFile(param(uri))).getContents(); - - // TODO: do the decoding in a stream, to avoid the extra intermediate - // byte array - return Base64.getDecoder().wrap( - new ByteArrayInputStream( - fileBody.getBytes(StandardCharsets.ISO_8859_1))); - } - - @Override - public boolean exists(ISourceLocation uri) { - try { - return call(s -> s.exists(param(uri))).getResult(); - } catch (IOException e) { - return false; - } - } - - @Override - public long lastModified(ISourceLocation uri) throws IOException { - return TimeUnit.SECONDS.toMillis(call(s -> s.lastModified(param(uri))).getTimestamp()); - } - - @Override - public long created(ISourceLocation uri) throws IOException { - return TimeUnit.SECONDS.toMillis(call(s -> s.created(param(uri))).getTimestamp()); - } - - @Override - public boolean isDirectory(ISourceLocation uri) { - try { - var cached = cachedDirectoryListing.getIfPresent(URIUtil.getParentLocation(uri)); - if (cached != null) { - var result = cached.get().get(URIUtil.getLocationName(uri)); - if (result != null) { - return result; - } - } - return call(s -> s.isDirectory(param(uri))).getResult(); - } catch (IOException e) { - return false; - } - } - - @Override - public boolean isFile(ISourceLocation uri) { - try { - var cached = cachedDirectoryListing.getIfPresent(URIUtil.getParentLocation(uri)); - if (cached != null) { - var result = cached.get().get(URIUtil.getLocationName(uri)); - if (result != null) { - return !result; - } - } - return call(s -> s.isFile(param(uri))).getResult(); - } catch (IOException e) { - return false; - } - } - - /** - * Rascal's current implementions sometimes ask for a directory listing - * and then iterate over all the entries checking if they are a directory. - * This is super slow for this jsonrcp, so we store tha last directory listing - * and check insid - */ - private final Cache>> cachedDirectoryListing - = Caffeine.newBuilder() - .expireAfterWrite(Duration.ofSeconds(5)) - .maximumSize(1000) - .build(); - - @Override - public String[] list(ISourceLocation uri) throws IOException { - var result = call(s -> s.list(param(uri))); - // we store the entries in a cache, for consecutive isDirectory/isFile calls - cachedDirectoryListing.put(uri, Lazy.defer(() -> { - var entries = result.getEntries(); - var areDirs = result.getAreDirectory(); - Map lookup = new HashMap<>(entries.length); - assert entries.length == areDirs.length; - for (int i = 0; i < entries.length; i++) { - lookup.put(entries[i], areDirs[i]); - } - return lookup; - })); - return result.getEntries(); - } - - @Override - public String scheme() { - throw new UnsupportedOperationException("Scheme not supported on fallback resolver"); - } - - @Override - public boolean supportsHost() { - return false; - } - - @Override - public OutputStream getOutputStream(ISourceLocation uri, boolean append) throws IOException { - // we have to collect all bytes into memory, there exist no streaming Base64 encoder in java jre - // otherwise we could just store that base64 string. - // when done with the outputstream, we can generate the base64 string and send it towards the LSP client - return new ByteArrayOutputStream() { - private boolean closed = false; - - @Override - public void close() throws IOException { - if (closed) { - return; - } - closed = true; - var contents = Base64.getEncoder().encodeToString(this.toByteArray()); - call(s -> s.writeFile(new WriteFileRequest(uri, contents, append))); - cachedDirectoryListing.invalidate(URIUtil.getParentLocation(uri)); - } - }; - } - - @Override - public void mkDirectory(ISourceLocation uri) throws IOException { - call(s -> s.mkDirectory(param(uri))); - cachedDirectoryListing.invalidate(URIUtil.getParentLocation(uri)); - } - - @Override - public void remove(ISourceLocation uri) throws IOException { - call(s -> s.remove(param(uri))); - cachedDirectoryListing.invalidate(uri); - cachedDirectoryListing.invalidate(URIUtil.getParentLocation(uri)); - } - - @Override - public void setLastModified(ISourceLocation uri, long timestamp) throws IOException { - throw new IOException("setLastModified not supported by vscode"); - } - - @Override - public void watch(ISourceLocation root, Consumer watcher, boolean recursive) throws IOException { - getClient().addWatcher(root, recursive, watcher, getServer()); - } - - @Override - public void unwatch(ISourceLocation root, Consumer watcher, boolean recursive) throws IOException { - getClient().removeWatcher(root, recursive, watcher, getServer()); - } - - @Override - public boolean supportsRecursiveWatch() { - return true; - } - - 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<>(); - - @SuppressWarnings({"initialization", "argument"}) - public void registerTextDocumentService(@UnderInitialization 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"); - } - - @Override - public long size(ISourceLocation uri) throws IOException { - return call(s -> s.size(param(uri))).getResult(); - } - - @Override - public boolean isReadable(ISourceLocation uri) throws IOException { - return call(s -> s.isReadable(param(uri))).getResult(); - } - @Override - public boolean isWritable(ISourceLocation uri) throws IOException { - return call(s -> s.isWritable(param(uri))).getResult(); - } - - @Override - public FileAttributes stat(ISourceLocation uri) throws IOException { - return call(s -> s.stat(param(uri))).getFileAttributes(); - } - - private static IOException translateException(ResponseErrorException cause) { - var error = cause.getResponseError(); - switch (error.getCode()) { - case -1: return new IOException("Generic error: " + error.getMessage()); - case -2: { - if (error.getData() instanceof JsonPrimitive) { - var data = (JsonPrimitive)error.getData(); - if (data.isString()) { - switch (data.getAsString()) { - case "FileExists": // fall-through - case "EntryExists": - return new FileAlreadyExistsException(error.getMessage()); - case "FileNotFound": // fall-through - case "EntryNotFound": - return new NoSuchFileException(error.getMessage()); - case "FileNotADirectory": // fall-through - case "EntryNotADirectory": - return new NotDirectoryException(error.getMessage()); - case "FileIsADirectory": // fall-through - case "EntryIsADirectory": - return new IOException("File is a directory: " + error.getMessage()); - case "NoPermissions": - return new AccessDeniedException(error.getMessage()); - } - } - } - return new IOException("File system error: " + error.getMessage() + " data: " + error.getData()); - } - case -3: return new IOException("Rascal native scheme's should not be forwarded to VS Code"); - default: return new IOException("Missing case for: " + error); - } - } - -} diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/uri/LSPOpenFileRedirector.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/uri/LSPOpenFileRedirector.java new file mode 100644 index 000000000..b255e22d5 --- /dev/null +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/uri/LSPOpenFileRedirector.java @@ -0,0 +1,93 @@ +/* + * 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.uri; + +import java.io.IOException; +import java.net.URISyntaxException; +import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; + +import org.checkerframework.checker.initialization.qual.UnderInitialization; +import org.checkerframework.checker.nullness.qual.MonotonicNonNull; +import org.rascalmpl.uri.URIUtil; +import org.rascalmpl.vscode.lsp.IBaseTextDocumentService; +import org.rascalmpl.vscode.lsp.TextDocumentState; + +import io.usethesource.vallang.ISourceLocation; + +public class LSPOpenFileRedirector { + private static @MonotonicNonNull LSPOpenFileRedirector instance = null; + + public static LSPOpenFileRedirector getInstance() { + if (instance == null) { + instance = new LSPOpenFileRedirector(); + } + return instance; + } + + private LSPOpenFileRedirector() { } + + public boolean isFileManaged(ISourceLocation file) { + for (var service : textDocumentServices) { + if (service.isManagingFile(file)) { + return true; + } + } + return false; + } + + 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; + } + + private final List textDocumentServices = new CopyOnWriteArrayList<>(); + + @SuppressWarnings({"initialization", "argument"}) + public void registerTextDocumentService(@UnderInitialization 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 index 00efecc12..67a2070e0 100644 --- 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 @@ -34,6 +34,7 @@ import org.rascalmpl.uri.FileAttributes; import org.rascalmpl.uri.ISourceLocationInput; +import org.rascalmpl.uri.URIResolverRegistry; import org.rascalmpl.uri.URIUtil; import org.rascalmpl.vscode.lsp.TextDocumentState; import org.rascalmpl.vscode.lsp.util.Versioned; @@ -42,7 +43,7 @@ public class LSPOpenFileResolver implements ISourceLocationInput { private TextDocumentState getEditorState(ISourceLocation uri) throws IOException { - return FallbackResolver.getInstance().getDocumentState(stripLspPrefix(uri)); + return LSPOpenFileRedirector.getInstance().getDocumentState(stripLspPrefix(uri)); } @Override @@ -57,7 +58,7 @@ public Charset getCharset(ISourceLocation uri) throws IOException { @Override public boolean exists(ISourceLocation uri) { - return FallbackResolver.getInstance().isFileManaged(stripLspPrefix(uri)); + return LSPOpenFileRedirector.getInstance().isFileManaged(stripLspPrefix(uri)); } @Override @@ -112,7 +113,7 @@ private int size(Versioned s) { @Override public boolean isReadable(ISourceLocation uri) throws IOException { - return FallbackResolver.getInstance().isFileManaged(stripLspPrefix(uri)); + return LSPOpenFileRedirector.getInstance().isFileManaged(stripLspPrefix(uri)); } @Override @@ -122,7 +123,7 @@ public FileAttributes stat(ISourceLocation uri) throws IOException { } var current = getEditorState(uri).getCurrentContent(); var modified = current.getTimestamp(); - var isWritable = FallbackResolver.getInstance().isWritable(stripLspPrefix(uri)); + var isWritable = URIResolverRegistry.getInstance().isWritable(stripLspPrefix(uri)); return new FileAttributes( true, true, modified, modified, //We fix the creation timestamp to be equal to the last modified time diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/uri/jsonrpc/RascalFileSystemInVSCode.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/uri/jsonrpc/RascalFileSystemInVSCode.java new file mode 100644 index 000000000..13de77144 --- /dev/null +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/uri/jsonrpc/RascalFileSystemInVSCode.java @@ -0,0 +1,130 @@ +/* + * 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.uri.jsonrpc; + +import java.util.concurrent.CompletableFuture; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.eclipse.lsp4j.jsonrpc.ResponseErrorException; +import org.eclipse.lsp4j.jsonrpc.messages.ResponseError; +import org.rascalmpl.uri.FileAttributes; +import org.rascalmpl.uri.URIResolverRegistry; +import org.rascalmpl.uri.UnsupportedSchemeException; +import org.rascalmpl.uri.remote.RascalFileSystemServices; +import org.rascalmpl.uri.remote.jsonrpc.CopyRequest; +import org.rascalmpl.uri.remote.jsonrpc.DirectoryListingResponse; +import org.rascalmpl.uri.remote.jsonrpc.ISourceLocationRequest; +import org.rascalmpl.uri.remote.jsonrpc.LocationContentResponse; +import org.rascalmpl.uri.remote.jsonrpc.RemoteIOError; +import org.rascalmpl.uri.remote.jsonrpc.RemoveRequest; +import org.rascalmpl.uri.remote.jsonrpc.RenameRequest; +import org.rascalmpl.uri.remote.jsonrpc.SourceLocationResponse; +import org.rascalmpl.uri.remote.jsonrpc.WatchRequest; +import org.rascalmpl.uri.remote.jsonrpc.WriteFileRequest; +import org.rascalmpl.vscode.lsp.util.locations.Locations; + +/** + * Wrapper for RascalFileSystemServices handling LSP-specifics. + * In particular, locations from LSP are mapped to Rascal-friendly locations, and Rascal exceptions are mapped to exceptions LSP expects. + */ +public class RascalFileSystemInVSCode extends RascalFileSystemServices { + private static final Logger logger = LogManager.getLogger(RascalFileSystemServices.class); + private static final URIResolverRegistry reg = URIResolverRegistry.getInstance(); + + @Override + public CompletableFuture resolveLocation(ISourceLocationRequest req) { + logger.trace("resolveLocation: {}", req.getLocation()); + return super.resolveLocation(new ISourceLocationRequest(Locations.toClientLocation(req.getLocation()))); + } + + @Override + public CompletableFuture watch(WatchRequest req) { + var loc = req.getLocation(); + logger.trace("watch: {}", loc); + if (Locations.isWrappedOpaque(loc)) { + throw RemoteIOError.translate(new UnsupportedSchemeException("Opaque locations are not supported by Rascal: " + loc.getScheme())); + } + return super.watch(req); + } + + @Override + public CompletableFuture stat(ISourceLocationRequest req) { + var loc = req.getLocation(); + logger.trace("stat: {}", loc); + return super.stat(new ISourceLocationRequest(Locations.toClientLocation(loc))); + } + + @Override + public CompletableFuture list(ISourceLocationRequest req) { + logger.trace("list: {}", req.getLocation()); + return super.list(new ISourceLocationRequest(Locations.toClientLocation(req.getLocation()))); + } + + @Override + public CompletableFuture mkDirectory(ISourceLocationRequest req) { + var loc = req.getLocation(); + logger.trace("mkDirectory: {}", loc); + return super.mkDirectory(new ISourceLocationRequest(Locations.toClientLocation(loc))); + } + + @Override + public CompletableFuture readFile(ISourceLocationRequest req) { + var loc = req.getLocation(); + logger.trace("readFile: {}", loc); + return super.readFile(new ISourceLocationRequest(Locations.toClientLocation(loc))); + } + + @Override + public CompletableFuture writeFile(WriteFileRequest req) { + var loc = req.getLocation(); + logger.info("writeFile: {}", loc); + if (reg.exists(loc) && reg.isDirectory(loc)) { + throw new ResponseErrorException(new ResponseError(RemoteIOError.IsADirectory.code, "Is a directory: " + loc, req)); + } + return super.writeFile(new WriteFileRequest(Locations.toClientLocation(loc), req.getContent(), req.isAppend())); + } + + @Override + public CompletableFuture remove(RemoveRequest req) { + var loc = req.getLocation(); + logger.trace("remove: {}", loc); + return super.remove(new RemoveRequest(Locations.toClientLocation(loc), req.isRecursive())); + } + + @Override + public CompletableFuture rename(RenameRequest req) { + logger.trace("rename: {} to {}", req.getFrom(), req.getTo()); + return super.rename(new RenameRequest(Locations.toClientLocation(req.getFrom()), Locations.toClientLocation(req.getTo()), req.isOverwrite())); + } + + @Override + public CompletableFuture copy(CopyRequest req) { + logger.trace("copy: {} to {}", req.getFrom(), req.getTo()); + return super.copy(new CopyRequest(Locations.toClientLocation(req.getFrom()), Locations.toClientLocation(req.getTo()), req.isRecursive(), req.isOverwrite())); + } +} diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/uri/jsonrpc/VSCodeUriResolverClient.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/uri/jsonrpc/VSCodeFileSystemInRascal.java similarity index 67% rename from rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/uri/jsonrpc/VSCodeUriResolverClient.java rename to rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/uri/jsonrpc/VSCodeFileSystemInRascal.java index 15b89078d..93f37f763 100644 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/uri/jsonrpc/VSCodeUriResolverClient.java +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/uri/jsonrpc/VSCodeFileSystemInRascal.java @@ -27,17 +27,23 @@ package org.rascalmpl.vscode.lsp.uri.jsonrpc; import java.io.IOException; -import java.util.function.Consumer; -import org.eclipse.lsp4j.jsonrpc.services.JsonNotification; -import org.rascalmpl.uri.ISourceLocationWatcher; -import org.rascalmpl.vscode.lsp.uri.jsonrpc.messages.ISourceLocationChanged; -import io.usethesource.vallang.ISourceLocation; -public interface VSCodeUriResolverClient { +import org.rascalmpl.uri.remote.RemoteExternalResolverRegistry; +import org.rascalmpl.vscode.lsp.uri.LSPOpenFileRedirector; + +import io.usethesource.vallang.ISourceLocation; - @JsonNotification("rascal/vfs/watcher/emitWatch") - void emitWatch(ISourceLocationChanged event); +/** + * Wrapper for RemoteExternalResolverRegistry handling LSP-specifics. + * In particular, locations from LSP are mapped to Rascal-friendly locations. + */ +public class VSCodeFileSystemInRascal extends RemoteExternalResolverRegistry { + public VSCodeFileSystemInRascal(int remoteResolverRegistryPort) { + super(remoteResolverRegistryPort); + } - void addWatcher(ISourceLocation loc, boolean recursive, Consumer callback, VSCodeUriResolverServer server) throws IOException; - void removeWatcher(ISourceLocation loc, boolean recursive, Consumer callback, VSCodeUriResolverServer server) throws IOException; + @Override + public ISourceLocation resolve(ISourceLocation input) throws IOException { + return LSPOpenFileRedirector.getInstance().resolve(super.resolve(input)); + } } diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/uri/jsonrpc/VSCodeUriResolverServer.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/uri/jsonrpc/VSCodeUriResolverServer.java deleted file mode 100644 index 722d68259..000000000 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/uri/jsonrpc/VSCodeUriResolverServer.java +++ /dev/null @@ -1,130 +0,0 @@ -/* - * 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.uri.jsonrpc; - -import java.util.concurrent.CompletableFuture; - -import org.eclipse.lsp4j.jsonrpc.services.JsonRequest; -import org.rascalmpl.vscode.lsp.uri.jsonrpc.messages.BooleanResult; -import org.rascalmpl.vscode.lsp.uri.jsonrpc.messages.DirectoryListingResult; -import org.rascalmpl.vscode.lsp.uri.jsonrpc.messages.FileAttributesResult; -import org.rascalmpl.vscode.lsp.uri.jsonrpc.messages.ISourceLocationRequest; -import org.rascalmpl.vscode.lsp.uri.jsonrpc.messages.NumberResult; -import org.rascalmpl.vscode.lsp.uri.jsonrpc.messages.ReadFileResult; -import org.rascalmpl.vscode.lsp.uri.jsonrpc.messages.RenameRequest; -import org.rascalmpl.vscode.lsp.uri.jsonrpc.messages.TimestampResult; -import org.rascalmpl.vscode.lsp.uri.jsonrpc.messages.WatchRequest; -import org.rascalmpl.vscode.lsp.uri.jsonrpc.messages.WriteFileRequest; - -public interface VSCodeUriResolverServer { - @JsonRequest("rascal/vfs/input/readFile") - default CompletableFuture readFile(ISourceLocationRequest req) { - throw new UnsupportedOperationException(); - } - - @JsonRequest("rascal/vfs/input/exists") - default CompletableFuture exists(ISourceLocationRequest req) { - throw new UnsupportedOperationException(); - } - - @JsonRequest("rascal/vfs/input/lastModified") - default CompletableFuture lastModified(ISourceLocationRequest req) { - throw new UnsupportedOperationException(); - } - - @JsonRequest("rascal/vfs/input/created") - default CompletableFuture created(ISourceLocationRequest req) { - throw new UnsupportedOperationException(); - } - - @JsonRequest("rascal/vfs/input/isDirectory") - default CompletableFuture isDirectory(ISourceLocationRequest req) { - throw new UnsupportedOperationException(); - } - - @JsonRequest("rascal/vfs/input/isFile") - default CompletableFuture isFile(ISourceLocationRequest req) { - throw new UnsupportedOperationException(); - } - - @JsonRequest("rascal/vfs/input/list") - default CompletableFuture list(ISourceLocationRequest req) { - throw new UnsupportedOperationException(); - } - - @JsonRequest("rascal/vfs/input/size") - default CompletableFuture size(ISourceLocationRequest req) { - throw new UnsupportedOperationException(); - } - - @JsonRequest("rascal/vfs/input/stat") - default CompletableFuture stat(ISourceLocationRequest req) { - throw new UnsupportedOperationException(); - } - - @JsonRequest("rascal/vfs/input/isReadable") - default CompletableFuture isReadable(ISourceLocationRequest req) { - throw new UnsupportedOperationException(); - } - - @JsonRequest("rascal/vfs/input/isWritable") - default CompletableFuture isWritable(ISourceLocationRequest req) { - throw new UnsupportedOperationException(); - } - - - @JsonRequest("rascal/vfs/output/writeFile") - default CompletableFuture writeFile(WriteFileRequest req) { - throw new UnsupportedOperationException(); - } - - @JsonRequest("rascal/vfs/output/mkDirectory") - default CompletableFuture mkDirectory(ISourceLocationRequest req) { - throw new UnsupportedOperationException(); - } - - @JsonRequest("rascal/vfs/output/remove") - default CompletableFuture remove(ISourceLocationRequest req) { - throw new UnsupportedOperationException(); - } - - @JsonRequest("rascal/vfs/output/rename") - default CompletableFuture rename(RenameRequest req) { - throw new UnsupportedOperationException(); - } - - @JsonRequest("rascal/vfs/watcher/watch") - default CompletableFuture watch(WatchRequest req) { - throw new UnsupportedOperationException(); - } - - @JsonRequest("rascal/vfs/watcher/unwatch") - default CompletableFuture unwatch(WatchRequest req) { - throw new UnsupportedOperationException(); - } - -} diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/uri/jsonrpc/VSCodeVFS.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/uri/jsonrpc/VSCodeVFS.java deleted file mode 100644 index 4bcefbad1..000000000 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/uri/jsonrpc/VSCodeVFS.java +++ /dev/null @@ -1,64 +0,0 @@ -/* - * 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.uri.jsonrpc; - -import org.checkerframework.checker.nullness.qual.EnsuresNonNull; -import org.checkerframework.checker.nullness.qual.MonotonicNonNull; -import org.checkerframework.checker.nullness.qual.Nullable; - -/** - * This singleton keeps track of the current VFS server instance - * - * The FallbackResolvers uses this, and the LSP client should make sure to setup - * the right connection - */ -public enum VSCodeVFS { - INSTANCE; - - private volatile @MonotonicNonNull VSCodeUriResolverServer server = null; - private volatile @MonotonicNonNull VSCodeUriResolverClient client = null; - - public @Nullable VSCodeUriResolverServer getServer() { - return server; - } - - @EnsuresNonNull("this.server") - public void provideServer(VSCodeUriResolverServer server) { - this.server = server; - } - - public @Nullable VSCodeUriResolverClient getClient() { - return client; - } - - @EnsuresNonNull("this.client") - public void provideClient(VSCodeUriResolverClient client) { - this.client = client; - } - - -} diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/uri/jsonrpc/impl/VSCodeVFSClient.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/uri/jsonrpc/impl/VSCodeVFSClient.java deleted file mode 100644 index 02a5186fb..000000000 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/uri/jsonrpc/impl/VSCodeVFSClient.java +++ /dev/null @@ -1,228 +0,0 @@ -/* - * 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.uri.jsonrpc.impl; - -import java.io.IOException; -import java.net.InetAddress; -import java.net.Socket; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.UUID; -import java.util.concurrent.CompletionException; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.CopyOnWriteArrayList; -import java.util.concurrent.ExecutorService; -import java.util.function.Consumer; -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; -import org.checkerframework.checker.nullness.qual.Nullable; -import org.eclipse.lsp4j.jsonrpc.Launcher; -import org.rascalmpl.uri.ISourceLocationWatcher; -import org.rascalmpl.vscode.lsp.IRascalFileSystemServices; -import org.rascalmpl.vscode.lsp.uri.jsonrpc.VSCodeUriResolverClient; -import org.rascalmpl.vscode.lsp.uri.jsonrpc.VSCodeUriResolverServer; -import org.rascalmpl.vscode.lsp.uri.jsonrpc.VSCodeVFS; -import org.rascalmpl.vscode.lsp.uri.jsonrpc.messages.ISourceLocationChanged; -import org.rascalmpl.vscode.lsp.uri.jsonrpc.messages.WatchRequest; -import org.rascalmpl.vscode.lsp.util.NamedThreadPool; -import io.usethesource.vallang.ISourceLocation; - -public class VSCodeVFSClient implements VSCodeUriResolverClient, AutoCloseable { - private static final Logger logger = LogManager.getLogger(VSCodeVFSClient.class); - - private final Map watchers = new ConcurrentHashMap<>(); - private final Map watchersById = new ConcurrentHashMap<>(); - private final Socket socket; - - private VSCodeVFSClient(Socket socket) { - this.socket = socket; - } - - @Override - public void close() { - try { - this.socket.close(); - } catch (IOException e) { - logger.debug("Closing failed", e); - } - } - - @Override - public void emitWatch(ISourceLocationChanged event) { - logger.trace("emitWatch: {}", event); - var watch = watchersById.get(event.getWatchId()); - if (watch != null) { - watch.publish(event.translate()); - } - } - - @Override - public void addWatcher(ISourceLocation loc, boolean recursive, Consumer callback, VSCodeUriResolverServer server) throws IOException { - logger.trace("addWatcher: {}", loc); - try { - var watch = watchers.computeIfAbsent(new WatchSubscriptionKey(loc, recursive), k -> { - logger.trace("Fresh watch, setting up request to server"); - var result = new Watchers(); - result.addNewWatcher(callback); - watchersById.put(result.id, result); - server.watch(new WatchRequest(loc, recursive, result.id)).join(); - return result; - }); - watch.addNewWatcher(callback); - } catch (CompletionException ce) { - logger.error("Error setting up watch", ce.getCause()); - throw new IOException(ce.getCause()); - } - } - - @Override - public void removeWatcher(ISourceLocation loc, boolean recursive, - Consumer callback, - VSCodeUriResolverServer server) throws IOException { - logger.trace("removeWatcher: {}", loc); - var watchKey = new WatchSubscriptionKey(loc, recursive); - var watch = watchers.get(watchKey); - if (watch != null && watch.removeWatcher(callback)) { - logger.trace("No other watchers registered, so unregistering at server"); - watchers.remove(watchKey); - if (!watch.callbacks.isEmpty()) { - logger.trace("Raced by another thread, canceling unregister"); - watchers.put(watchKey, watch); - return; - } - watchersById.remove(watch.id); - try { - server.unwatch(new WatchRequest(loc, recursive, watch.id)).join(); - } catch (CompletionException ce) { - logger.error("Error removing watch", ce.getCause()); - throw new IOException(ce.getCause()); - } - } - } - - private static class WatchSubscriptionKey { - private final ISourceLocation loc; - private final boolean recursive; - public WatchSubscriptionKey(ISourceLocation loc, boolean recursive) { - this.loc = loc; - this.recursive = recursive; - } - - @Override - public int hashCode() { - return Objects.hash(loc, recursive); - } - - @Override - public boolean equals(@Nullable Object obj) { - if (this == obj) { - return true; - } - if ((obj instanceof WatchSubscriptionKey)) { - WatchSubscriptionKey other = (WatchSubscriptionKey) obj; - return recursive == other.recursive - && Objects.equals(loc, other.loc) - ; - } - return false; - } - - } - - - - private static final ExecutorService exec = NamedThreadPool.cachedDaemon("FallbackResolver-watcher"); - - /** - * The watch api in rascal uses closures identity to keep track of watches. - * Since we cannot share the instance via the json-rpc bridge, we keep the - * closure around in this collection class. - * If there are no more callbacks registered, we unregister the watch at the - * VSCode side. - */ - private static class Watchers { - private final String id; - private final List> callbacks = new CopyOnWriteArrayList<>(); - - public Watchers() { - this.id = UUID.randomUUID().toString(); - } - - public void addNewWatcher(Consumer watcher) { - this.callbacks.add(watcher); - } - - public boolean removeWatcher(Consumer watcher) { - this.callbacks.remove(watcher); - return this.callbacks.isEmpty(); - } - - public void publish(ISourceLocationWatcher.ISourceLocationChanged changed) { - for (Consumer c : callbacks) { - // schedule callbacks on different thread - exec.submit(() -> c.accept(changed)); - } - } - - - } - - public static void buildAndRegister(int port) { - try { - var existingClient = VSCodeVFS.INSTANCE.getClient(); - if (existingClient instanceof AutoCloseable) { - try { - ((AutoCloseable)existingClient).close(); - } catch (Exception e) { - logger.error("Error closing old client", e); - } - } - - logger.debug("Connecting to VFS: {}", port); - @SuppressWarnings("java:S2095") // we don't have to close the socket, we are passing it off to the lsp4j framework - var socket = new Socket(InetAddress.getLoopbackAddress(), port); - socket.setTcpNoDelay(true); - @SuppressWarnings("java:S2095") // we don't have to close the client, we are passing it off to the VSCodeVFS singleton - var newClient = new VSCodeVFSClient(socket); - Launcher clientLauncher = new Launcher.Builder() - .setRemoteInterface(VSCodeUriResolverServer.class) - .setLocalService(newClient) - .setInput(socket.getInputStream()) - .setOutput(socket.getOutputStream()) - .setExecutorService(IRascalFileSystemServices.executor) - .create(); - - clientLauncher.startListening(); - - VSCodeVFS.INSTANCE.provideServer(clientLauncher.getRemoteProxy()); - VSCodeVFS.INSTANCE.provideClient(newClient); - } catch (Throwable e) { - logger.error("Error setting up VFS connection", e); - } - } -} diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/uri/jsonrpc/messages/BooleanResult.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/uri/jsonrpc/messages/BooleanResult.java deleted file mode 100644 index 9d6cb6019..000000000 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/uri/jsonrpc/messages/BooleanResult.java +++ /dev/null @@ -1,62 +0,0 @@ -/* - * 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.uri.jsonrpc.messages; - -import org.checkerframework.checker.nullness.qual.Nullable; - -public class BooleanResult { - private boolean result; - - public BooleanResult(boolean result) { - this.result = result; - } - - public BooleanResult() {} - - public boolean getResult() { - return result; - } - - @Override - public boolean equals(@Nullable Object obj) { - if (obj instanceof BooleanResult) { - return result == ((BooleanResult)obj).result; - } - return false; - } - - @Override - public int hashCode() { - return result ? 11 : 3; - } - - @Override - public String toString() { - return "BooleanResult [result=" + result + "]"; - } - -} diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/uri/jsonrpc/messages/DirectoryListingResult.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/uri/jsonrpc/messages/DirectoryListingResult.java deleted file mode 100644 index ae0efc5dc..000000000 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/uri/jsonrpc/messages/DirectoryListingResult.java +++ /dev/null @@ -1,73 +0,0 @@ -/* - * 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.uri.jsonrpc.messages; - -import java.util.Arrays; -import java.util.Objects; -import org.checkerframework.checker.nullness.qual.Nullable; -import org.eclipse.lsp4j.jsonrpc.validation.NonNull; - -public class DirectoryListingResult { - - - @NonNull private String[] entries; - @NonNull private boolean[] areDirectory; - - public DirectoryListingResult(@NonNull String [] entries, @NonNull boolean [] areDirectory) { - this.entries = entries; - this.areDirectory = areDirectory; - } - - public String [] getEntries() { - return entries; - } - - public boolean [] getAreDirectory() { - return areDirectory; - } - - @Override - public boolean equals(@Nullable Object obj) { - if (obj instanceof DirectoryListingResult) { - return Objects.deepEquals(entries, ((DirectoryListingResult)obj).entries) - && Objects.deepEquals(areDirectory, ((DirectoryListingResult)obj).areDirectory) - ; - } - return false; - } - - @Override - public int hashCode() { - return (Arrays.deepHashCode(entries) + 1) + 19 * (Arrays.hashCode(areDirectory) + 1); - } - - @Override - public String toString() { - return "DirectoryListingResult [entries=" + Arrays.toString(entries) + "areDirectory=" +Arrays.toString(areDirectory) + "]"; - } - -} diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/uri/jsonrpc/messages/FileAttributesResult.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/uri/jsonrpc/messages/FileAttributesResult.java deleted file mode 100644 index ee5effcf3..000000000 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/uri/jsonrpc/messages/FileAttributesResult.java +++ /dev/null @@ -1,81 +0,0 @@ -/* - * 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.uri.jsonrpc.messages; - -import java.util.Objects; - -import org.checkerframework.checker.nullness.qual.Nullable; -import org.rascalmpl.uri.FileAttributes; - -public class FileAttributesResult { - private boolean exists; - private int type; - private long ctime; - private long mtime; - private int size; - private int permissions; - - public FileAttributesResult(boolean exists, int type, long ctime, long mtime, int size, int permissions) { - this.exists = exists; - this.type = type; - this.ctime = ctime; - this.mtime = mtime; - this.size = size; - this.permissions = permissions; - } - - public FileAttributesResult() {} - - public FileAttributes getFileAttributes() { - return new FileAttributes(exists, (type & 1) == 1, ctime, mtime, true, (permissions & 1) == 0, size); - } - - @Override - public boolean equals(@Nullable Object obj) { - if (obj instanceof FileAttributesResult) { - var other = (FileAttributesResult)obj; - return exists == other.exists - && type == other.type - && ctime == other.ctime - && mtime == other.mtime - && size == other.size - && permissions == other.permissions; - } - return false; - } - - @Override - public int hashCode() { - return Objects.hash(exists, type, ctime, mtime, size, permissions); - } - - @Override - public String toString() { - return "FileStatResult [exists="+ exists + " type=" + type + " ctime=" + ctime + " mtime=" + mtime + " size=" + size + " permissions=" + permissions + "]"; - } - -} diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/uri/jsonrpc/messages/ISourceLocationChangeType.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/uri/jsonrpc/messages/ISourceLocationChangeType.java deleted file mode 100644 index 6258b89a0..000000000 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/uri/jsonrpc/messages/ISourceLocationChangeType.java +++ /dev/null @@ -1,71 +0,0 @@ -/* - * 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.uri.jsonrpc.messages; - -import org.rascalmpl.uri.ISourceLocationWatcher; - -/** - * @see ISourceLocationWatcher.ISourceLocationChangeType this code is mirroring this type for serialization purposes - */ -public enum ISourceLocationChangeType { - CREATED(1), - DELETED(2), - MODIFIED(3); - - - private final int value; - - ISourceLocationChangeType(int value) { - this.value = value; - } - - public int getValue() { - return value; - } - - public static ISourceLocationChangeType forValue(int value) { - var allValues = ISourceLocationChangeType.values(); - if (value < 1 || value > allValues.length) { - throw new IllegalArgumentException("Illegal enum value: " + value); - } - return allValues[value - 1]; - } - - public static ISourceLocationWatcher.ISourceLocationChangeType translate( - ISourceLocationChangeType lsp) { - switch (lsp) { - case CREATED: - return ISourceLocationWatcher.ISourceLocationChangeType.CREATED; - case DELETED: - return ISourceLocationWatcher.ISourceLocationChangeType.DELETED; - case MODIFIED: - return ISourceLocationWatcher.ISourceLocationChangeType.MODIFIED; - default: - throw new RuntimeException("Forgotten type: " + lsp); - } - } -} diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/uri/jsonrpc/messages/ISourceLocationChanged.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/uri/jsonrpc/messages/ISourceLocationChanged.java deleted file mode 100644 index 89a61cbf2..000000000 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/uri/jsonrpc/messages/ISourceLocationChanged.java +++ /dev/null @@ -1,97 +0,0 @@ -/* - * 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.uri.jsonrpc.messages; - -import java.util.Objects; -import org.checkerframework.checker.nullness.qual.Nullable; -import org.eclipse.lsp4j.jsonrpc.validation.NonNull; -import org.rascalmpl.uri.ISourceLocationWatcher; -import org.rascalmpl.vscode.lsp.util.locations.Locations; -import io.usethesource.vallang.ISourceLocation; - -public class ISourceLocationChanged { - @NonNull - private String watchId; - @NonNull - private String location; - @NonNull - private ISourceLocationChangeType changeType; - - public ISourceLocationChanged(@NonNull String watchId, @NonNull String location, @NonNull ISourceLocationChangeType changeType) { - this.watchId = watchId; - this.location = location; - this.changeType = changeType; - } - - public ISourceLocationChangeType getChangeType() { - return changeType; - } - public String getLocation() { - return location; - } - public ISourceLocation getSourceLocation() { - return Locations.toLoc(location); - } - - public String getWatchId() { - return watchId; - } - - @Override - public boolean equals(@Nullable Object obj) { - if (obj instanceof ISourceLocationChanged) { - var other = (ISourceLocationChanged)obj; - return Objects.equals(watchId, other.watchId) - && Objects.equals(location, other.location) - && Objects.equals(changeType, other.changeType) - ; - } - return false; - } - - @Override - public int hashCode() { - return Objects.hash(watchId, location, changeType); - } - - public ISourceLocationWatcher.ISourceLocationChanged translate() { - return ISourceLocationWatcher.makeChange( - getSourceLocation(), - ISourceLocationChangeType.translate(changeType) - ); - } - - @Override - public String toString() { - return "ISourceLocationChanged [changeType=" + changeType + ", location=" + location + ", watchId=" + watchId + "]"; - } - - - - - -} diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/uri/jsonrpc/messages/ISourceLocationRequest.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/uri/jsonrpc/messages/ISourceLocationRequest.java deleted file mode 100644 index f2242e93f..000000000 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/uri/jsonrpc/messages/ISourceLocationRequest.java +++ /dev/null @@ -1,64 +0,0 @@ -/* - * 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.uri.jsonrpc.messages; - -import org.checkerframework.checker.nullness.qual.Nullable; -import org.eclipse.lsp4j.jsonrpc.validation.NonNull; -import org.rascalmpl.vscode.lsp.util.locations.Locations; - -import io.usethesource.vallang.ISourceLocation; - -public class ISourceLocationRequest { - @NonNull - private String uri; - - public ISourceLocationRequest(@NonNull String uri) { - this.uri = uri; - } - - public ISourceLocationRequest(ISourceLocation loc) { - this(Locations.toUri(loc).toString()); - } - - @Override - public boolean equals(@Nullable Object obj) { - if (obj instanceof ISourceLocationRequest) { - return uri.equals(((ISourceLocationRequest)obj).uri); - } - return false; - } - - @Override - public int hashCode() { - return 7 * uri.hashCode(); - } - - public String getUri() { - return uri; - } - -} diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/uri/jsonrpc/messages/NumberResult.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/uri/jsonrpc/messages/NumberResult.java deleted file mode 100644 index 4062faf0d..000000000 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/uri/jsonrpc/messages/NumberResult.java +++ /dev/null @@ -1,62 +0,0 @@ -/* - * 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.uri.jsonrpc.messages; - -import org.checkerframework.checker.nullness.qual.Nullable; - -public class NumberResult { - private int result; - - public NumberResult(int result) { - this.result = result; - } - - public NumberResult() {} - - public int getResult() { - return result; - } - - @Override - public boolean equals(@Nullable Object obj) { - if (obj instanceof NumberResult) { - return result == ((NumberResult)obj).result; - } - return false; - } - - @Override - public int hashCode() { - return result * 11; - } - - @Override - public String toString() { - return "NumberResult [result=" + result + "]"; - } - -} diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/uri/jsonrpc/messages/ReadFileResult.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/uri/jsonrpc/messages/ReadFileResult.java deleted file mode 100644 index 2c8d925db..000000000 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/uri/jsonrpc/messages/ReadFileResult.java +++ /dev/null @@ -1,62 +0,0 @@ -/* - * 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.uri.jsonrpc.messages; - -import org.checkerframework.checker.nullness.qual.Nullable; -import org.eclipse.lsp4j.jsonrpc.validation.NonNull; - -public class ReadFileResult { - - @NonNull private String contents; - - public ReadFileResult(@NonNull String contents) { - this.contents = contents; - } - - public String getContents() { - return contents; - } - - @Override - public boolean equals(@Nullable Object obj) { - if (obj instanceof ReadFileResult) { - return contents.equals(((ReadFileResult)obj).contents); - } - return false; - } - - @Override - public int hashCode() { - return contents.hashCode(); - } - - @Override - public String toString() { - return "ReadFileResult [contents=" + contents + "]"; - } - -} diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/uri/jsonrpc/messages/RenameRequest.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/uri/jsonrpc/messages/RenameRequest.java deleted file mode 100644 index f8ab7e1a5..000000000 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/uri/jsonrpc/messages/RenameRequest.java +++ /dev/null @@ -1,89 +0,0 @@ -/* - * 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.uri.jsonrpc.messages; - -import java.util.Objects; -import org.checkerframework.checker.nullness.qual.Nullable; -import org.eclipse.lsp4j.jsonrpc.validation.NonNull; -import org.rascalmpl.vscode.lsp.util.locations.Locations; - -import io.usethesource.vallang.ISourceLocation; - -public class RenameRequest { - @NonNull - private String from; - @NonNull - private String to; - - private boolean overwrite; - - @SuppressWarnings("initialization.fields.uninitialized") - public RenameRequest() { - } - - public RenameRequest(String from, String to, boolean overwrite) { - this.from = from; - this.to = to; - this.overwrite = overwrite; - } - - public RenameRequest(ISourceLocation from, ISourceLocation to, boolean overwrite) { - this.from = Locations.toUri(from).toString(); - this.to = Locations.toUri(to).toString(); - this.overwrite = overwrite; - } - - public String getFrom() { - return from; - } - - public String getTo() { - return to; - } - - public boolean getOverwrite() { - return overwrite; - } - - @Override - public boolean equals(@Nullable Object obj) { - if (obj instanceof RenameRequest) { - var other = (RenameRequest)obj; - return Objects.equals(from, other.from) - && Objects.equals(to, other.to) - && overwrite == other.overwrite - ; - } - return false; - } - - @Override - public int hashCode() { - return Objects.hash(from, to, overwrite); - } - -} diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/uri/jsonrpc/messages/TimestampResult.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/uri/jsonrpc/messages/TimestampResult.java deleted file mode 100644 index b4f24d3dd..000000000 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/uri/jsonrpc/messages/TimestampResult.java +++ /dev/null @@ -1,63 +0,0 @@ -/* - * 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.uri.jsonrpc.messages; - -import org.checkerframework.checker.nullness.qual.Nullable; - -public class TimestampResult { - private long timestamp; - - public TimestampResult(long timestamp) { - this.timestamp = timestamp; - } - - public TimestampResult() {} - - public long getTimestamp() { - return timestamp; - } - - @Override - public int hashCode() { - return Long.hashCode(timestamp); - } - - @Override - public boolean equals(@Nullable Object obj) { - if (obj instanceof TimestampResult) { - return timestamp == ((TimestampResult)obj).timestamp; - } - return false; - } - - @Override - public String toString() { - return "TimestampResult [timestamp=" + timestamp + "]"; - } - - -} diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/uri/jsonrpc/messages/VFSRegister.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/uri/jsonrpc/messages/VFSRegister.java deleted file mode 100644 index fddafd73e..000000000 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/uri/jsonrpc/messages/VFSRegister.java +++ /dev/null @@ -1,60 +0,0 @@ -/* - * 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.uri.jsonrpc.messages; - -import org.checkerframework.checker.nullness.qual.Nullable; -import org.eclipse.lsp4j.jsonrpc.validation.NonNull; - -public class VFSRegister { - @NonNull - private int port; - - public VFSRegister() { - } - - public VFSRegister(int port) { - this.port = port; - } - - public int getPort() { - return port; - } - - @Override - public boolean equals(@Nullable Object obj) { - if (obj instanceof VFSRegister) { - return port == ((VFSRegister)obj).port; - } - return false; - } - - @Override - public int hashCode() { - return port; - } - -} diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/uri/jsonrpc/messages/WatchRequest.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/uri/jsonrpc/messages/WatchRequest.java deleted file mode 100644 index aceeabb08..000000000 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/uri/jsonrpc/messages/WatchRequest.java +++ /dev/null @@ -1,77 +0,0 @@ -/* - * 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.uri.jsonrpc.messages; - -import java.util.Objects; -import org.checkerframework.checker.nullness.qual.Nullable; -import org.eclipse.lsp4j.jsonrpc.validation.NonNull; -import io.usethesource.vallang.ISourceLocation; - -public class WatchRequest extends ISourceLocationRequest { - - @NonNull - private String watcher; - - private boolean recursive; - - public WatchRequest(ISourceLocation loc, boolean recursive, String watcher) { - super(loc); - this.recursive = recursive; - this.watcher = watcher; - } - - public WatchRequest(@NonNull String uri, @NonNull boolean recursive, @NonNull String watcher) { - super(uri); - this.recursive = recursive; - this.watcher = watcher; - } - - public String getWatcher() { - return watcher; - } - - public boolean isRecursive() { - return recursive; - } - - @Override - public boolean equals(@Nullable Object obj) { - if (obj instanceof WatchRequest) { - var other = (WatchRequest)obj; - return super.equals(other) - && other.recursive == recursive - && Objects.equals(watcher, other.watcher); - } - return false; - } - - @Override - public int hashCode() { - return Objects.hash(super.hashCode(), watcher, recursive); - } - -} diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/uri/jsonrpc/messages/WriteFileRequest.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/uri/jsonrpc/messages/WriteFileRequest.java deleted file mode 100644 index 969fb92cf..000000000 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/uri/jsonrpc/messages/WriteFileRequest.java +++ /dev/null @@ -1,77 +0,0 @@ -/* - * 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.uri.jsonrpc.messages; - -import java.util.Objects; -import org.checkerframework.checker.nullness.qual.Nullable; -import org.eclipse.lsp4j.jsonrpc.validation.NonNull; -import io.usethesource.vallang.ISourceLocation; - -public class WriteFileRequest extends ISourceLocationRequest { - - @NonNull - private String content; - - private boolean append; - - public WriteFileRequest(@NonNull String uri, @NonNull String content, @NonNull boolean append) { - super(uri); - this.content = content; - this.append = append; - } - - public WriteFileRequest(ISourceLocation loc, String content, boolean append) { - super(loc); - this.content = content; - this.append = append; - } - - public String getContent() { - return content; - } - - public boolean getAppend() { - return append; - } - - @Override - public boolean equals(@Nullable Object obj) { - if (obj instanceof WriteFileRequest) { - var other = (WriteFileRequest)obj; - return super.equals(obj) - && content.equals(other.content) - && append == other.append; - } - return false; - } - - @Override - public int hashCode() { - return Objects.hash(super.hashCode(), content, append); - } - -} diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/util/Lazy.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/util/Lazy.java deleted file mode 100644 index 2f6622a95..000000000 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/util/Lazy.java +++ /dev/null @@ -1,50 +0,0 @@ -/* - * 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.util; - -import java.util.function.Supplier; -import org.checkerframework.checker.nullness.qual.MonotonicNonNull; -import org.checkerframework.checker.nullness.qual.NonNull; - -public interface Lazy extends Supplier { - public static Lazy defer(Supplier generator) { - return new Lazy(){ - private volatile @MonotonicNonNull T result = null; - - @Override - public T get() { - if (result == null) { - result = generator.get(); - } - return result; - } - - }; - - } - -} diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/util/NamedThreadPool.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/util/NamedThreadPool.java deleted file mode 100644 index c37b7d0c7..000000000 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/util/NamedThreadPool.java +++ /dev/null @@ -1,63 +0,0 @@ -/* - * 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.util; - -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; -import java.util.concurrent.ThreadFactory; -import java.util.concurrent.atomic.AtomicInteger; - -public class NamedThreadPool { - private NamedThreadPool() {} - - public static ExecutorService single(String name) { - return Executors.newSingleThreadExecutor(factory(name, false)); - } - - public static ExecutorService singleDaemon(String name) { - return Executors.newSingleThreadExecutor(factory(name, true)); - } - - public static ExecutorService cached(String name) { - return Executors.newCachedThreadPool(factory(name, false)); - } - - public static ExecutorService cachedDaemon(String name) { - return Executors.newCachedThreadPool(factory(name, true)); - } - - private static ThreadFactory factory(String name, boolean daemon) { - AtomicInteger counter = new AtomicInteger(0); - ThreadGroup group = new ThreadGroup(name); - return r -> { - var t = new Thread(group, r, name + "-" + counter.incrementAndGet()); - t.setDaemon(daemon); - return t; - }; - } - -} 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 0724749f6..234c026c6 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 @@ -29,6 +29,7 @@ import java.io.IOException; import java.net.URI; import java.net.URISyntaxException; + import org.apache.commons.lang3.tuple.Pair; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; @@ -169,6 +170,10 @@ public static URI toUri(ISourceLocation loc) { return uri; } + public static boolean isWrappedOpaque(ISourceLocation loc) { + return OPAQUE_SCHEME.equals(loc.getURI().getScheme()); + } + public static Location mapValueToLocation(IValue v, ColumnMaps cm) { if (v instanceof ISourceLocation) { return Locations.toLocation((ISourceLocation)v, cm); diff --git a/rascal-vscode-extension/src/RascalExtension.ts b/rascal-vscode-extension/src/RascalExtension.ts index 23af93e0d..0107d9eff 100644 --- a/rascal-vscode-extension/src/RascalExtension.ts +++ b/rascal-vscode-extension/src/RascalExtension.ts @@ -33,20 +33,20 @@ import { checkForJVMUpdate, getJavaExecutable } from './auto-jvm/JavaLookup'; import { RascalLanguageServer } from './lsp/RascalLanguageServer'; import { LanguageParameter, ParameterizedLanguageServer } from './lsp/ParameterizedLanguageServer'; import { RascalTerminalLinkProvider } from './RascalTerminalLinkProvider'; -import { VSCodeUriResolverServer } from './fs/VSCodeURIResolver'; +import { VSCodeFileSystemInRascal } from './fs/VSCodeFileSystemInRascal'; import { RascalLibraryProvider } from './ux/LibraryNavigator'; import { FileType } from 'vscode'; import { RascalDebugViewProvider } from './dap/RascalDebugView'; export class RascalExtension implements vscode.Disposable { - private readonly vfsServer: VSCodeUriResolverServer; + private readonly vfsServer: VSCodeFileSystemInRascal; private readonly dsls:ParameterizedLanguageServer; private readonly rascal: RascalLanguageServer; private readonly log: vscode.LogOutputChannel = vscode.window.createOutputChannel("Rascal Extension", {log: true}); constructor(private readonly context: vscode.ExtensionContext, private readonly jarRootPath: string, private readonly icon: vscode.Uri, private readonly isDeploy = true) { - this.vfsServer = new VSCodeUriResolverServer(!isDeploy, this.log); + this.vfsServer = new VSCodeFileSystemInRascal(!isDeploy, this.log); this.dsls = new ParameterizedLanguageServer(context, this.vfsServer, jarRootPath, isDeploy); this.rascal = new RascalLanguageServer(context, this.vfsServer, jarRootPath, this.dsls, this.log, isDeploy); @@ -266,15 +266,14 @@ export class RascalExtension implements vscode.Disposable { ); } shellArgs.push(); + const vfsServerPort = await this.vfsServer.serverPort; shellArgs.push( '-Dfile.encoding=UTF8' - , '-Drascal.fallbackResolver=org.rascalmpl.vscode.lsp.uri.FallbackResolver' , `-Drascal.languageRegistryPort=${await this.rascal.languageRegistry.serverPort}` - , 'org.rascalmpl.vscode.lsp.terminal.LSPTerminalREPL' + , `-Drascal.remoteResolverRegistryPort=${vfsServerPort}` + , 'org.rascalmpl.shell.RascalShell' , '--remoteIDEServicesPort' , '' + remoteIDEServicesConfiguration.port - , '--vfsPort' - , '' + await this.vfsServer.serverPort ); return shellArgs.concat(extraArgs || []); } diff --git a/rascal-vscode-extension/src/RascalTerminalLinkProvider.ts b/rascal-vscode-extension/src/RascalTerminalLinkProvider.ts index 16f87cb09..1dbab6208 100644 --- a/rascal-vscode-extension/src/RascalTerminalLinkProvider.ts +++ b/rascal-vscode-extension/src/RascalTerminalLinkProvider.ts @@ -74,7 +74,7 @@ export class RascalTerminalLinkProvider implements TerminalLinkProvider"; + ret += "<" + sloc.endLineColumn![0] + "," + sloc.endLineColumn![1] + ">"; + } + ret += ")"; + } + return ret; + } + + fromRascalLocationString(loc: string): SourceLocation { + const match: RegExpExecArray | null = this.linkDetector().exec(loc); + if (match !== null) { + return buildLocation(match); + } + throw Error(`Invalid location ${loc}`); + } } function translateRange(sloc: SourceLocation, td: vscode.TextDocument): vscode.Range | undefined { @@ -112,9 +133,8 @@ function translateRange(sloc: SourceLocation, td: vscode.TextDocument): vscode.R return undefined; } -function buildLink(match: RegExpExecArray): ExtendedLink { +function buildLocation(match: RegExpExecArray): SourceLocation { const linkMatch = match[0]; - const linkOffset = match.index + 1; const linkLength = linkMatch.indexOf('|', 2); const sloc = { uri: linkMatch.substring(1, linkLength) }; const numbers = linkMatch.substring(linkLength).match(/\d+/g,); @@ -126,10 +146,14 @@ function buildLink(match: RegExpExecArray): ExtendedLink { sloc.endLineColumn = [Number(numbers[4]), Number(numbers[5])]; } } + return sloc; +} +function buildLink(match: RegExpExecArray): ExtendedLink { + const sloc = buildLocation(match); return { - startIndex: linkOffset - 1, - length: linkMatch.length, + startIndex: match.index, + length: match[0].length, loc: sloc }; } diff --git a/rascal-vscode-extension/src/fs/RascalFileSystemInVSCode.ts b/rascal-vscode-extension/src/fs/RascalFileSystemInVSCode.ts new file mode 100644 index 000000000..46ee4d0c3 --- /dev/null +++ b/rascal-vscode-extension/src/fs/RascalFileSystemInVSCode.ts @@ -0,0 +1,193 @@ +/* + * 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. + */ +import path from 'path'; +import * as vscode from 'vscode'; +import { BaseLanguageClient, ResponseError } from 'vscode-languageclient'; +import { CopyRequest, DirectoryListingResponse, FileAttributes, ISourceLocationRequest, LocationContentResponse, RemoveRequest, RenameRequest, WatchRequest, WriteFileRequest } from './VSCodeFileSystemInRascal'; +import { RemoteIOError } from './RemoteIOError'; + +export class RascalFileSystemInVSCode implements vscode.FileSystemProvider { + readonly client: BaseLanguageClient; + private readonly _emitter = new vscode.EventEmitter(); + readonly onDidChangeFile: vscode.Event = this._emitter.event; + private readonly protectedSchemes: string[] = ["file", "http", "https", "unknown"]; + + /** + * Registers a single FileSystemProvider for every URI scheme that Rascal supports, except + * for file, http and https. + * + * @param client to use as a server for the file system provider methods + */ + constructor (client: BaseLanguageClient, private readonly logger: vscode.LogOutputChannel) { + this.client = client; + + client.onNotification("rascal/vfs/watcher/fileChanged", (event: vscode.FileChangeEvent) => { + this._emitter.fire([event]); + }); + } + + // VS Code omits the leading two slashes from URIs if the autority is empty *and* the scheme is not equal to "file" + // Rascal does not support this style of URIs, so we add the slashes before sending the URI over + toRascalUri(uri: vscode.Uri | string): string { + if (typeof(uri) === "string") { + return uri; + } + const uriString = uri.toString(); + if (uri.authority === "" && uri.scheme !== "file") { + const colon = uri.scheme.length + 1; + return `${uriString.slice(0, colon)}//${uriString.slice(colon)}`; + } + return uriString; + } + + sendRequest(uri: vscode.Uri | string, method: string): Promise; + sendRequest(uri: vscode.Uri | string, method: string, param: A): Promise; + sendRequest(uri: vscode.Uri | string, method: string, param?: A): Promise { + return this.client.sendRequest(method, param ?? { loc: this.toRascalUri(uri) } ) + .catch((r: ResponseError) => { + throw RemoteIOError.translateResponseError(r, uri, this.logger); + }); + } + + /** + * Attemptes to register all schemes. + * @param schemes The list of schemes to register for this provider + */ + tryRegisterSchemes(schemes: string[]) { + schemes + .filter(s => !this.protectedSchemes.includes(s)) + // we add support for schemes that look inside a jar + .concat(schemes + .filter(s => s !== "jar" && s !== "zip" && s !== "compressed") + .map(s => "jar+" + s)) + .filter(isUnknownFileSystem) + .forEach(s => { + try { + vscode.workspace.registerFileSystemProvider(s, this); + this.logger.debug(`Rascal VFS registered scheme: ${s}`); + } catch (error) { + if (isUnknownFileSystem(s)) { + this.logger.error(`Unable to register scheme: ${s}\n${error}`); + } + else { + this.logger.debug(`Rascal VFS lost the race to register scheme: ${s}, which in most cases is fine`); + } + } + }); + } + + watch(uri: vscode.Uri, options: { recursive: boolean; excludes: string[]; }): vscode.Disposable { + this.logger.trace("[RascalFileSystemInVSCode] watch: ", uri); + this.sendRequest(uri, "rascal/vfs/watcher/watch", { + loc: this.toRascalUri(uri), + recursive: options.recursive + }); + + return new vscode.Disposable(() => { + this.sendRequest(uri, "rascal/vfs/watcher/unwatch", { + loc: this.toRascalUri(uri), + recursive: options.recursive + }); + }); + } + + stat(uri: vscode.Uri): vscode.FileStat | Thenable { + this.logger.trace("[RascalFileSystemInVSCode] stat: ", uri); + return this.sendRequest(uri, "rascal/vfs/input/stat").then(a => + { + type: a.isFile ? vscode.FileType.File : vscode.FileType.Directory, + ctime: a.created, + mtime: a.lastModified, + size: a.size, + permissions: a.isWritable ? undefined : vscode.FilePermission.Readonly + } + ); + } + + readDirectory(uri: vscode.Uri): [string, vscode.FileType][] | Thenable<[string, vscode.FileType][]> { + this.logger.trace("[RascalFileSystemInVSCode] readDirectory: ", uri); + return this.sendRequest(uri, "rascal/vfs/input/list") + .then(c => c.entries.map(ft => [ft.name, ft.types.reduce((a, i) => a | i)])); + } + + createDirectory(uri: vscode.Uri): void | Thenable { + this.logger.trace("[RascalFileSystemInVSCode] createDirectory: ", uri); + return this.sendRequest(uri, "rascal/vfs/output/mkDirectory"); + } + + readFile(uri: vscode.Uri): Uint8Array | Thenable { + this.logger.trace("[RascalFileSystemInVSCode] readFile: ", uri); + return this.sendRequest(uri, "rascal/vfs/input/readFile") + .then(r => Buffer.from(r.content, "base64")); + } + + writeFile(uri: vscode.Uri, content: Uint8Array, options: { create: boolean; overwrite: boolean; }): void | Thenable { + // The `create` and `overwrite` options are handled on this side + this.logger.trace("[RascalFileSystemInVSCode] writeFile: ", uri); + const parentUri = uri.with({ path: path.dirname(uri.path) }); + Promise.all([ + this.sendRequest(uri, "rascal/vfs/input/stat"), + this.sendRequest(parentUri, "rascal/vfs/input/stat")] + ).then(r => { + const fileStat = r[0]; + const parentStat = r[1]; + if (!fileStat.exists && !options.create) { + throw vscode.FileSystemError.FileNotFound(`File ${uri} does not exist and \`create\` was not set`); + } + if (!parentStat.exists && options.create) { + throw vscode.FileSystemError.FileNotFound(`Parent of ${uri} does not exist but \`create\` was set`); + } + if (fileStat.exists && options.create && !options.overwrite) { + throw vscode.FileSystemError.FileExists(`File ${uri} exists and \`create\` was set, but \`override\` was not set`); + } + return this.sendRequest(uri, "rascal/vfs/output/writeFile", { + loc: this.toRascalUri(uri), + content: Buffer.from(content).toString("base64"), + append: false + }); + }); + } + + delete(uri: vscode.Uri, options: { recursive: boolean; }): void | Thenable { + this.logger.trace("[RascalFileSystemInVSCode] delete: ", uri); + return this.sendRequest(uri, "rascal/vfs/output/remove", { loc: this.toRascalUri(uri), recursive: options.recursive }); + } + + rename(oldUri: vscode.Uri, newUri: vscode.Uri, options: { overwrite: boolean; }): void | Thenable { + this.logger.trace("[RascalFileSystemInVSCode] rename: ", oldUri, newUri); + return this.sendRequest(oldUri, "rascal/vfs/output/rename", { from: this.toRascalUri(oldUri), to: this.toRascalUri(newUri), overwrite: options.overwrite }); + } + + copy(source: vscode.Uri, target: vscode.Uri, options?: { overwrite?: boolean; }): Thenable { + this.logger.trace("[RascalFileSystemInVSCode] copy: ", source, target); + return this.sendRequest(source, "rascal/vfs/output/copy", { from: this.toRascalUri(source), to: this.toRascalUri(target), recursive: true, overwrite: (options && options.overwrite) ?? false }); + } +} + +function isUnknownFileSystem(scheme: string): boolean { + return vscode.workspace.fs.isWritableFileSystem(scheme) === undefined; +} diff --git a/rascal-vscode-extension/src/fs/RascalFileSystemProviders.ts b/rascal-vscode-extension/src/fs/RascalFileSystemProviders.ts deleted file mode 100644 index 321287416..000000000 --- a/rascal-vscode-extension/src/fs/RascalFileSystemProviders.ts +++ /dev/null @@ -1,165 +0,0 @@ -/* - * 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. - */ -import * as vscode from 'vscode'; -import {BaseLanguageClient, ResponseError } from 'vscode-languageclient'; - -export class RascalFileSystemProvider implements vscode.FileSystemProvider { - readonly client: BaseLanguageClient; - private readonly _emitter = new vscode.EventEmitter(); - readonly onDidChangeFile: vscode.Event = this._emitter.event; - private readonly protectedSchemes:string[] = ["file", "http", "https", "unknown"]; - - /** - * Registers a single FileSystemProvider for every URI scheme that Rascal supports, except - * for file, http and https. - * - * @param client to use as a server for the file system provider methods - */ - constructor (client:BaseLanguageClient, private readonly logger: vscode.LogOutputChannel) { - this.client = client; - - client.onNotification("rascal/filesystem/onDidChangeFile", (event:vscode.FileChangeEvent) => { - this._emitter.fire([event]); - }); - } - - sendRequest(uri : vscode.Uri, method: string): Promise; - sendRequest(uri : vscode.Uri, method: string, param: A): Promise; - sendRequest(uri : vscode.Uri, method: string, param?: A): Promise { - return this.client.sendRequest(method, param ?? { uri: uri.toString()} ) - .catch((r: ResponseError) => { - if (r !== undefined) { - this.logger.debug("Got response error from the file system: ", r); - switch (r.code) { - case -1: throw vscode.FileSystemError.FileExists(uri); - case -2: throw vscode.FileSystemError.FileIsADirectory(uri); - case -3: throw vscode.FileSystemError.FileNotADirectory(uri); - case -4: throw vscode.FileSystemError.FileNotFound(uri); - case -5: throw vscode.FileSystemError.NoPermissions(uri); - case -6: throw vscode.FileSystemError.Unavailable(uri); - default: throw new vscode.FileSystemError(uri); - } - } - throw r; - }); - } - - /** - * Attemptes to register all schemes. - * @param schemes The list of schemes to register for this provider - */ - tryRegisterSchemes(schemes: string[]) { - schemes - .filter(s => !this.protectedSchemes.includes(s)) - // we add support for schemes that look inside a jar - .concat(schemes - .filter(s => s !== "jar" && s !== "zip" && s !== "compressed") - .map(s => "jar+" + s)) - .filter(isUnknownFileSystem) - .forEach(s => { - try { - vscode.workspace.registerFileSystemProvider(s, this); - this.logger.debug(`Rascal VFS registered scheme: ${s}`); - } catch (error) { - if (isUnknownFileSystem(s)) { - this.logger.error(`Unable to register scheme: ${s}\n${error}`); - } - else { - this.logger.debug(`Rascal VFS lost the race to register scheme: ${s}, which in most cases is fine`); - } - } - }); - } - - watch(uri: vscode.Uri, options: { recursive: boolean; excludes: string[]; }): vscode.Disposable { - this.sendRequest(uri, "rascal/filesystem/watch", { - uri: uri.toString(), - recursive:options.recursive, - excludes: options.excludes - }); - - return new vscode.Disposable(() => { - this.sendRequest(uri, "rascal/filesystem/unwatch", { - uri: uri.toString() - }); - }); - } - - stat(uri: vscode.Uri): vscode.FileStat | Thenable { - return this.sendRequest(uri, "rascal/filesystem/stat"); - } - - readDirectory(uri: vscode.Uri): [string, vscode.FileType][] | Thenable<[string, vscode.FileType][]> { - return this.sendRequest(uri, "rascal/filesystem/readDirectory") - .then(c => c.map(ft => [ft.name, ft.type])); - } - - createDirectory(uri: vscode.Uri): void | Thenable { - return this.sendRequest(uri, "rascal/filesystem/createDirectory", {uri: uri.toString()}); - } - - readFile(uri: vscode.Uri): Uint8Array | Thenable { - return this.sendRequest(uri, "rascal/filesystem/readFile") - .then(content => content.content) - .then(str => Buffer.from(str, "base64")); - } - - writeFile(uri: vscode.Uri, content: Uint8Array, options: { create: boolean; overwrite: boolean; }): void | Thenable { - return this.sendRequest(uri, "rascal/filesystem/writeFile", { - uri: uri.toString(), - create:options.create, - overwrite:options.overwrite, - content: Buffer.from(content).toString("base64") - }); - } - - delete(uri: vscode.Uri, options: { recursive: boolean; }): void | Thenable { - return this.sendRequest(uri, "rascal/filesystem/delete", {uri: uri.toString(), recursive: options.recursive}); - } - - rename(oldUri: vscode.Uri, newUri: vscode.Uri, options: { overwrite: boolean; }): void | Thenable { - return this.sendRequest(oldUri, "rascal/filesystem/rename", {oldUri: oldUri.toString(), newUri: newUri.toString(), overwrite: options.overwrite}); - } -} - -function isUnknownFileSystem(scheme : string) : boolean { - return vscode.workspace.fs.isWritableFileSystem(scheme) === undefined; -} -interface LocationContent { - content: string; -} - -interface WatchParameters { - uri: string; - recursive: boolean; - excludes:Array; -} - -interface FileWithType { - name: string; - type: vscode.FileType -} diff --git a/rascal-vscode-extension/src/fs/RemoteIOError.ts b/rascal-vscode-extension/src/fs/RemoteIOError.ts new file mode 100644 index 000000000..61e3813b7 --- /dev/null +++ b/rascal-vscode-extension/src/fs/RemoteIOError.ts @@ -0,0 +1,93 @@ +/* + * 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. + */ +import * as vscode from 'vscode'; +import * as rpc from 'vscode-jsonrpc/node'; +import { ResponseError } from "vscode-languageclient"; + +export class RemoteIOError { + static readonly fileExists = -1; + static readonly fileNotFound = -2; + static readonly isADirectory = -3; + static readonly isNotADirectory = -4; + static readonly directoryIsNotEmpty = -5; + static readonly permissionDenied = -6; + static readonly unsupportedScheme = -7; + static readonly illegalSyntax = -8; + + static readonly watchAlreadyDefined = -10; + static readonly watchNotDefined = -11; + + static readonly fileSystemError = -20; + + static readonly isRascalNative = -30; + + static readonly jsonRpcError = -40; + + static readonly unknown = -100; + + static translateResponseError(r: ResponseError | undefined, uri: vscode.Uri | string, logger: vscode.LogOutputChannel) : vscode.FileSystemError | undefined { + if (r !== undefined) { + logger.debug("Received error from Rascal file system", r); + switch (r.code) { + case RemoteIOError.fileExists: return vscode.FileSystemError.FileExists(uri); + case RemoteIOError.isADirectory: return vscode.FileSystemError.FileIsADirectory(uri); + case RemoteIOError.isNotADirectory: return vscode.FileSystemError.FileNotADirectory(uri); + case RemoteIOError.fileNotFound: return vscode.FileSystemError.FileNotFound(uri); + case RemoteIOError.permissionDenied: return vscode.FileSystemError.NoPermissions(uri); + default: return new vscode.FileSystemError(uri); + } + } + return r; + } + + static translateFileSystemError(e: vscode.FileSystemError | ResponseError | unknown, logger: vscode.LogOutputChannel) : ResponseError { + logger.debug("Received error from VS Code file system", e); + if (e instanceof vscode.FileSystemError) { + switch (e.code) { + case "FileExists": + case "EntryExists": + return new rpc.ResponseError(RemoteIOError.fileExists, e.message); + case "FileNotFound": + case "EntryNotFound": + return new rpc.ResponseError(RemoteIOError.fileNotFound, e.message); + case "FileNotADirectory": + case "EntryNotADirectory": + return new rpc.ResponseError(RemoteIOError.isADirectory, e.message); + case "FileIsADirectory": + case "EntryIsADirectory": + return new rpc.ResponseError(RemoteIOError.isNotADirectory, e.message); + case "NoPermissions": + return new rpc.ResponseError(RemoteIOError.permissionDenied, e.message); + } + return new rpc.ResponseError(RemoteIOError.fileSystemError, e.message); + } + if (e instanceof rpc.ResponseError) { + return e; + } + return new rpc.ResponseError(RemoteIOError.unknown, "Unknown error occurred"); + } +} diff --git a/rascal-vscode-extension/src/fs/VSCodeFileSystemInRascal.ts b/rascal-vscode-extension/src/fs/VSCodeFileSystemInRascal.ts new file mode 100644 index 000000000..f69b04423 --- /dev/null +++ b/rascal-vscode-extension/src/fs/VSCodeFileSystemInRascal.ts @@ -0,0 +1,514 @@ +/* + * 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. + */ +import * as vscode from 'vscode'; +import { Disposable } from "vscode"; +import * as rpc from 'vscode-jsonrpc/node'; +import { URI } from "vscode-languageclient"; +import { JsonRpcServer } from "../util/JsonRpcServer"; +import { RemoteIOError } from './RemoteIOError'; + +export declare type ISourceLocation = URI; + +/** + * VS Code implements this and offers it to the rascal-lsp server + */ +interface VSCodeResolverServer extends ISourceLocationInput, ISourceLocationOutput, ISourceLocationWatcher { } + +/** + * Rascal side should implement this on the other side of the stream + */ +//interface VSCodeResolverClient extends WatchEventReceiver {} + + +// Rascal's interface reduced to a subset we can support +interface ISourceLocationInput { + readFile(req: ISourceLocationRequest): Promise; + exists(req: ISourceLocationRequest): Promise; + lastModified(req: ISourceLocationRequest): Promise; + created(req: ISourceLocationRequest): Promise; + isDirectory(req: ISourceLocationRequest): Promise; + isFile(req: ISourceLocationRequest): Promise; + list(req: ISourceLocationRequest): Promise; + size(req: ISourceLocationRequest): Promise; + fileStat(req: ISourceLocationRequest): Promise; + isReadable(req: ISourceLocationRequest): Promise; +} + +function connectInputHandler(connection: rpc.MessageConnection, handler: ISourceLocationInput, toClear: Disposable[]) { + function req (method: string, h: rpc.RequestHandler1) { + toClear.push(connection.onRequest( + new rpc.RequestType1("rascal/vfs/input/" + method), + h.bind(handler))); + } + req("readFile", handler.readFile); + req("exists", handler.exists); + req("lastModified", handler.lastModified); + req("created", handler.created); + req("isDirectory", handler.isDirectory); + req("isFile", handler.isFile); + req("list", handler.list); + req("size", handler.size); + req("stat", handler.fileStat); + req("isReadable", handler.isReadable); +} + +// Rascal's interface reduced to a subset we can support +interface ISourceLocationOutput { + writeFile(req: WriteFileRequest): Promise; + mkDirectory(req: ISourceLocationRequest): Promise; + remove(req: RemoveRequest): Promise; + rename(req: RenameRequest): Promise; + copy(req: CopyRequest): Promise; + isWritable(req: ISourceLocationRequest): Promise; +} + +function connectOutputHandler(connection: rpc.MessageConnection, handler: ISourceLocationOutput, toClear: Disposable[]) { + function req (method: string, h: rpc.RequestHandler1) { + toClear.push(connection.onRequest( + new rpc.RequestType1("rascal/vfs/output/" + method), + h.bind(handler))); + } + + req("writeFile", handler.writeFile); + req("mkDirectory", handler.mkDirectory); + req("remove", handler.remove); + req("rename", handler.rename); + req("isWritable", handler.isWritable); +} + +// Rascal's interface reduced to a subset we can support +interface ISourceLocationWatcher { + watch(newWatch: WatchRequest): Promise; + unwatch(removeWatch: WatchRequest): Promise; +} + +function connectWatchHandler(connection: rpc.MessageConnection, handler: ISourceLocationWatcher, toClear: Disposable[]) { + function req (method: string, h: rpc.RequestHandler1) { + toClear.push(connection.onRequest( + new rpc.RequestType1("rascal/vfs/watcher/" + method), + h.bind(handler))); + } + req("watch", handler.watch); + req("unwatch", handler.unwatch); +} + +interface ILogicalSourceLocationResolver { + resolve(req: ISourceLocationRequest): Promise +} + +function connectLogicalResolver(connection: rpc.MessageConnection, handler: ILogicalSourceLocationResolver, toClear: Disposable[]) { + toClear.push(connection.onRequest( + new rpc.RequestType1("rascal/vfs/logical/resolveLocation"), handler.resolve.bind(handler) + )); +} + +// client side implementation receiving watch events +export interface WatchEventReceiver { + emitWatch(event: ISourceLocationChanged): void; +} + +function buildWatchReceiver(connection: rpc.MessageConnection): WatchEventReceiver { + return { + emitWatch: (e) => { + connection.sendNotification(new rpc.NotificationType1("rascal/vfs/watcher/sourceLocationChanged"), e); + } + }; +} + +// Messages (requests and responses) + +export interface ISourceLocationRequest { + loc: ISourceLocation; +} + +export interface WriteFileRequest extends ISourceLocationRequest { + content: string; + append: boolean; +} + +export interface RenameRequest { + from: ISourceLocation; + to: ISourceLocation; + overwrite: boolean; +} + +export interface CopyRequest { + from: ISourceLocation; + to: ISourceLocation; + recursive: boolean; + overwrite: boolean; +} + +export interface RemoveRequest extends ISourceLocationRequest { + recursive: boolean; +} + +export interface WatchRequest { + loc: ISourceLocation; + /** + * subscription id, this helps the calling in linking up to the original request + * as the watches are recursive + */ + watchId: string; + recursive: boolean; +} + +export interface FileAttributes { + exists: boolean; + isFile: boolean; + created: number; + lastModified: number; + isWritable: boolean; + isReadable: boolean; + size: number; +} + +export enum ISourceLocationChangeType { + created = 1, + deleted = 2, + modified = 3 +} + +export interface ISourceLocationChanged { + root: ISourceLocation; + type: ISourceLocationChangeType; + watchId: string; +} + +export interface LocationContentResponse { + /** + * Base64-encoded content of a location + */ + content: string +} + +interface BooleanResponse { + value: boolean +} + +interface NumberResponse { + value: number +} + +interface TimestampResponse { + value: number +} + +interface SourceLocationResponse { + loc: ISourceLocation +} + +export interface DirectoryListingResponse { + entries: DirectoryEntry[] +} + +export interface DirectoryEntry { + name: string; + types: vscode.FileType[] +} + +export class VSCodeFileSystemInRascal extends JsonRpcServer { + private rascalNativeSchemes: Set = new Set(); + constructor(debug: boolean, private readonly logger: vscode.LogOutputChannel) { + super("VFS", debug, connection => new ResolverClient(connection, debug, this.rascalNativeSchemes, this.logger), logger); + } + + ignoreSchemes(toIgnore: string[]) { + toIgnore.forEach(v => this.rascalNativeSchemes.add(v)); + } +} + +class ResolverClient implements VSCodeResolverServer, Disposable { + private readonly connection: rpc.MessageConnection; + private readonly watchListener: WatchEventReceiver; + private readonly fs: vscode.FileSystem; + private readonly rascalNativeSchemes: Set; + private toClear: Disposable[] = []; + constructor(connection: rpc.MessageConnection, debug: boolean, rascalNativeSchemes: Set, private readonly logger: vscode.LogOutputChannel){ + this.rascalNativeSchemes = rascalNativeSchemes; + this.fs = vscode.workspace.fs; + this.connection = connection; + if (debug) { + connection.trace(rpc.Trace.Verbose, { + log: (a) => { + this.logger.debug("[VSCodeFileSystemInRascal]: " + a); + } + }); + } + this.watchListener = buildWatchReceiver(connection); + connectInputHandler(connection, this, this.toClear); + connectOutputHandler(connection, this, this.toClear); + connectWatchHandler(connection, this, this.toClear); + connectLogicalResolver(connection, this, this.toClear); + } + + async asyncCatcher(build: () => Thenable): Promise { + try { + return await build(); + } + catch (e: unknown) { + throw RemoteIOError.translateFileSystemError(e, this.logger); + } + } + + async asyncVoidCatcher(run: (() => Promise) | Thenable): Promise { + return this.asyncCatcher(() => { + if (typeof run === "function") { + return run(); + } + else { + return run; + } + }); + } + + toUri(loc: ISourceLocation): vscode.Uri { + const uri = vscode.Uri.parse(loc); + if (this.isRascalNative(uri)) { + throw new rpc.ResponseError(RemoteIOError.isRascalNative, `Cannot request VS jobs on native Rascal locations: ${loc}`); + } + return uri; + } + + async readFile(req: ISourceLocationRequest): Promise { + this.logger.trace("[VSCodeFileSystemInRascal] readFile: ", req.loc); + return this.asyncCatcher(async () => { + content: Buffer.from(await this.fs.readFile(this.toUri(req.loc))).toString("base64") + }); + } + + isRascalNative(loc: ISourceLocation | vscode.Uri): boolean { + // Note that `ISourceLocation` === `URI` === `string` !== `vscode.Uri` + const scheme = typeof(loc) === "string" ? loc.substring(0, loc.indexOf(":")) : loc.scheme; + return this.rascalNativeSchemes.has(scheme); + } + + async exists(req: ISourceLocationRequest): Promise { + this.logger.trace("[VSCodeFileSystemInRascal] exists: ", req.loc); + try { + await this.stat(req.loc); + return { value: true }; + } + catch (_e) { + return { value: false }; + } + } + + async fileStat(req: ISourceLocationRequest): Promise { + return this.asyncCatcher(async () => { + const fileInfo = await this.stat(req.loc); + return { + exists: true, + isFile: (fileInfo.type | vscode.FileType.File) > 0, + created: fileInfo.ctime, + lastModified: fileInfo.mtime, + isReadable: true, + isWritable: true, + permissions: fileInfo.permissions && (fileInfo.permissions | vscode.FilePermission.Readonly) > 0, + size: fileInfo.size, + }; + }); + } + + private async stat(loc: ISourceLocation): Promise { + this.logger.trace("[VSCodeFileSystemInRascal] stat: ", loc); + return this.fs.stat(this.toUri(loc)); + } + + private async numberResult(loc: ISourceLocation, mapper: (s: vscode.FileStat) => number): Promise { + return this.asyncCatcher(async () => { value: mapper((await this.stat(loc))) }); + } + + lastModified(req: ISourceLocationRequest): Promise { + this.logger.trace("[VSCodeFileSystemInRascal] lastModified: ", req.loc); + return this.numberResult(req.loc, f => f.mtime); + } + + created(req: ISourceLocationRequest): Promise { + this.logger.trace("[VSCodeFileSystemInRascal] created: ", req.loc); + return this.numberResult(req.loc, f => f.ctime); + } + + size(req: ISourceLocationRequest): Promise { + this.logger.trace("[VSCodeFileSystemInRascal] size: ", req.loc); + return this.numberResult(req.loc, f => f.size); + } + + private async boolResult(loc: ISourceLocation, mapper: (s: vscode.FileStat) => boolean): Promise { + return this.asyncCatcher(async () => { value: mapper((await this.stat(loc))) }); + } + + isDirectory(req: ISourceLocationRequest): Promise { + this.logger.trace("[VSCodeFileSystemInRascal] isDirectory: ", req.loc); + return this.boolResult(req.loc, f => (f.type & vscode.FileType.Directory) !== 0); + } + + isFile(req: ISourceLocationRequest): Promise { + this.logger.trace("[VSCodeFileSystemInRascal] isFile: ", req.loc); + // TODO: figure out how to handle vscode.FileType.Symlink + return this.boolResult(req.loc, f => (f.type & vscode.FileType.File) !== 0); + } + + isReadable(req: ISourceLocationRequest): Promise { + this.logger.trace("[VSCodeFileSystemInRascal] isReadable: ", req.loc); + // if we can do a stat, we can read + return this.boolResult(req.loc, _ => true); + } + + async isWritable(req: ISourceLocationRequest): Promise { + this.logger.trace("[VSCodeFileSystemInRascal] isWritable: ", req.loc); + const scheme = this.toUri(req.loc).scheme; + const writable = this.fs.isWritableFileSystem(scheme); + if (writable === undefined) { + throw new rpc.ResponseError(RemoteIOError.unsupportedScheme, `Unsupported scheme: ${scheme}`); + } + if (!writable) { + // not a writable file system, so no need to check the uri + return { value: false }; + } + return this.boolResult(req.loc, f => f.permissions === undefined || (f.permissions & vscode.FilePermission.Readonly) === 0); + } + + async list(req: ISourceLocationRequest): Promise { + this.logger.trace("[VSCodeFileSystemInRascal] list: ", req.loc); + return this.asyncCatcher(async () => { entries: + (await this.fs.readDirectory(this.toUri(req.loc))).map(entry => { name: entry[0], types: this.decodeFileTypeBitmask(entry[1]) }) + }); + } + + decodeFileTypeBitmask(input: number): vscode.FileType[] { + return input === 0 ? [vscode.FileType.Unknown] : [vscode.FileType.File, vscode.FileType.Directory, vscode.FileType.SymbolicLink].filter(t => t === (t & input)); + } + + async writeFile(req: WriteFileRequest): Promise { + this.logger.trace("[VSCodeFileSystemInRascal] writeFile: ", req.loc); + const loc = this.toUri(req.loc); + let prefix: Buffer = Buffer.of(); + if (await this.exists(req) && req.append) { + prefix = Buffer.from(await this.fs.readFile(loc)); + } + return this.asyncVoidCatcher( + this.fs.writeFile(loc, Buffer.concat([prefix, Buffer.from(req.content, "base64")])) + ); + } + async mkDirectory(req: ISourceLocationRequest): Promise { + this.logger.trace("[VSCodeFileSystemInRascal] mkDirectory: ", req.loc); + return this.asyncVoidCatcher(this.fs.createDirectory(this.toUri(req.loc))); + } + async remove(req: RemoveRequest): Promise { + this.logger.trace("[VSCodeFileSystemInRascal] remove: ", req.loc); + return this.asyncVoidCatcher(this.fs.delete(this.toUri(req.loc), { recursive: req.recursive })); + } + async rename(req: RenameRequest): Promise { + this.logger.trace("[VSCodeFileSystemInRascal] rename: ", req.from, req.to); + return this.asyncVoidCatcher(this.fs.rename(this.toUri(req.from), this.toUri(req.to), { overwrite: req.overwrite })); + } + + async copy(req: CopyRequest): Promise { + this.logger.trace("[VSCodeFileSystemInRascal] copy: ", req.from, req.to); + if (req.recursive && await this.isDirectory({ loc: req.from })) { + throw new rpc.ResponseError(RemoteIOError.isADirectory, 'Non-recursive copy requested on a directory', req); + } + return this.asyncVoidCatcher(this.fs.copy(this.toUri(req.from), this.toUri(req.to), { overwrite: req.overwrite })); + } + + private readonly activeWatches = new Map(); + + async watch(newWatch: WatchRequest): Promise { + this.logger.trace("[VSCodeFileSystemInRascal] watch: ", newWatch.loc); + const watchKey = newWatch.loc + newWatch.recursive; + if (!this.activeWatches.has(watchKey)) { + const watcher = new WatcherCallbacks(this.toUri(newWatch.loc), newWatch.recursive, this.watchListener, newWatch.watchId); + this.activeWatches.set(watchKey, watcher); + this.toClear.push(watcher); + return; + } + throw new rpc.ResponseError(RemoteIOError.watchAlreadyDefined, `Watch already defined: ${newWatch.loc}`); + } + + async unwatch(removeWatch: WatchRequest): Promise { + this.logger.trace("[VSCodeFileSystemInRascal] unwatch: ", removeWatch.loc); + const watchKey = removeWatch.loc + removeWatch.recursive; + const watcher = this.activeWatches.get(watchKey); + if (watcher) { + this.activeWatches.delete(watchKey); + watcher.dispose(); + const index = this.toClear.indexOf(watcher); + if (index >= 0) { + this.toClear.splice(index, 1); + } + return; + } + throw new rpc.ResponseError(RemoteIOError.watchNotDefined, `Watch not defined: ${removeWatch.loc}`); + } + + async resolve(req: ISourceLocationRequest): Promise { + this.logger.trace("[VSCodeFileSystemInRascal] resolve: ", req.loc); + return { loc: req.loc }; + } + + dispose() { + this.activeWatches.clear(); + this.toClear.forEach(c => c.dispose()); + try { + this.connection.end(); + } catch (_e: unknown) { + // ignore errors here, ase we are disposing anyway + } + } +} + +class WatcherCallbacks implements Disposable { + private readonly watchId: string; + private readonly toClear: Disposable[] = []; + private readonly watchListener: WatchEventReceiver; + constructor(uri: vscode.Uri, recursive: boolean, watchListener: WatchEventReceiver, watchId: string) { + this.watchId = watchId; + this.watchListener = watchListener; + const newWatcher = vscode.workspace.createFileSystemWatcher( + new vscode.RelativePattern(uri, recursive ? '**/*' : '*') + ); + this.toClear.push(newWatcher); + newWatcher.onDidCreate(e => this.sendWatchEvent(e, ISourceLocationChangeType.created), this.toClear); + newWatcher.onDidChange(e => this.sendWatchEvent(e, ISourceLocationChangeType.modified), this.toClear); + newWatcher.onDidDelete(e => this.sendWatchEvent(e, ISourceLocationChangeType.deleted), this.toClear); + + } + + private async sendWatchEvent(uri: vscode.Uri, changeType: ISourceLocationChangeType) { + this.watchListener.emitWatch({ + root: uri.toString(), + type: changeType, + watchId: this.watchId + }); + } + + dispose() { + this.toClear.forEach(c => c.dispose()); + this.toClear.splice(0); + } +} diff --git a/rascal-vscode-extension/src/fs/VSCodeURIResolver.ts b/rascal-vscode-extension/src/fs/VSCodeURIResolver.ts deleted file mode 100644 index e776ec377..000000000 --- a/rascal-vscode-extension/src/fs/VSCodeURIResolver.ts +++ /dev/null @@ -1,507 +0,0 @@ -/* - * 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. - */ -import * as vscode from 'vscode'; -import { Disposable } from "vscode"; -import * as rpc from 'vscode-jsonrpc/node'; -import { URI } from "vscode-languageclient"; -import { JsonRpcServer } from "../util/JsonRpcServer"; - -declare type ISourceLocation = URI; - -/** - * VSCode implements this and offers it to the rascal-lsp server - */ -interface VSCodeResolverServer extends ISourceLocationInput, ISourceLocationOutput, ISourceLocationWatcher { } - -/** - * Rascal side should implement this on the other side of the stream - */ -//interface VSCodeResolverClient extends WatchEventReceiver {} - - -// Rascal's interface reduce to a subset we can support -interface ISourceLocationInput { - readFile(req: ISourceLocationRequest): Promise; - exists(req: ISourceLocationRequest): Promise; - lastModified(req: ISourceLocationRequest): Promise; - created(req: ISourceLocationRequest): Promise; - isDirectory(req: ISourceLocationRequest): Promise; - isFile(req: ISourceLocationRequest): Promise; - list(req: ISourceLocationRequest): Promise; - size(req: ISourceLocationRequest): Promise; - fileStat(req: ISourceLocationRequest): Promise; - isReadable(req: ISourceLocationRequest): Promise; - isWritable(req: ISourceLocationRequest): Promise; -} - - -function connectInputHandler(connection: rpc.MessageConnection, handler: ISourceLocationInput, toClear: Disposable[]) { - function req (method: string, h: rpc.RequestHandler1) { - toClear.push(connection.onRequest( - new rpc.RequestType1("rascal/vfs/input/" + method), - h.bind(handler))); - } - req("readFile", handler.readFile); - req("exists", handler.exists); - req("lastModified", handler.lastModified); - req("created", handler.created); - req("isDirectory", handler.isDirectory); - req("isFile", handler.isFile); - req("list", handler.list); - req("size", handler.size); - req("stat", handler.fileStat); - req("isReadable", handler.isReadable); - req("isWritable", handler.isWritable); -} - -// Rascal's interface reduce to a subset we can support -interface ISourceLocationOutput { - writeFile(req: WriteFileRequest ): Promise; - mkDirectory(req: ISourceLocationRequest): Promise; - remove(req: ISourceLocationRequest): Promise; - rename(req: RenameRequest): Promise; -} - -function connectOutputHandler(connection: rpc.MessageConnection, handler: ISourceLocationOutput, toClear: Disposable[]) { - function req (method: string, h: rpc.RequestHandler1) { - toClear.push(connection.onRequest( - new rpc.RequestType1("rascal/vfs/output/" + method), - h.bind(handler))); - } - req("writeFile", handler.writeFile); - req("mkDirectory", handler.mkDirectory); - req("remove", handler.remove); - req("rename", handler.rename); -} - -// Rascal's interface reduce to a subset we can support -interface ISourceLocationWatcher { - watch(newWatch: WatchRequest): Promise; - unwatch(removeWatch: WatchRequest): Promise; -} - -function connectWatchHandler(connection: rpc.MessageConnection, handler: ISourceLocationWatcher, toClear: Disposable[]) { - function req (method: string, h: rpc.RequestHandler1) { - toClear.push(connection.onRequest( - new rpc.RequestType1("rascal/vfs/watcher/" + method), - h.bind(handler))); - } - req("watch", handler.watch); - req("unwatch", handler.unwatch); -} - -// client side implementation receiving watch events -export interface WatchEventReceiver { - emitWatch(event: ISourceLocationChanged): void; -} - -function buildWatchReceiver(connection: rpc.MessageConnection) : WatchEventReceiver { - return { - emitWatch : (e) => { - connection.sendNotification(new rpc.NotificationType1("rascal/vfs/watcher/emitWatch"), e); - } - }; -} - -// Messages (requests and responses) - -interface ISourceLocationRequest { - uri: ISourceLocation; -} - -interface ReadFileResult { - /** - * base64 encoding of file - */ - contents: string; -} - -export interface BooleanResult { - result: boolean; -} - - -export interface TimestampResult { - /** - * Epoch seconds - */ - timestamp: number; -} - -export interface DirectoryListingResult { - entries: string[]; - areDirectory: boolean[] -} - -export interface NumberResult { - result: number; -} - -export interface FileAttributesResult { - exists : boolean; - type: vscode.FileType; - ctime: number; - mtime: number; - size: number; - permissions: vscode.FilePermission; -} - -export interface WriteFileRequest extends ISourceLocationRequest { - content: string; - append: boolean; -} - -export interface RenameRequest { - from: ISourceLocation; - to: ISourceLocation; - overwrite: boolean; -} - - -export interface WatchRequest extends ISourceLocationRequest { - /** - * subscription id, this helps the calling in linking up to the original request - * as the watches are recursive - */ - watcher: string; - recursive: boolean; -} - - -export enum ISourceLocationChangeType { - created = 1, - deleted = 2, - modified = 3 -} - -export interface ISourceLocationChanged { - watchId: string; - location: ISourceLocation; - changeType: ISourceLocationChangeType; -} - -enum ErrorCodes { - generic = -1, - fileSystem = -2, - nativeRascal = -3 - -} - - -export class VSCodeUriResolverServer extends JsonRpcServer { - private rascalNativeSchemes: Set = new Set(); - constructor(debug: boolean, private readonly logger: vscode.LogOutputChannel) { - super("VFS", connection => new ResolverClient(connection, debug, this.rascalNativeSchemes, this.logger), logger); - } - - ignoreSchemes(toIgnore: string[]) { - toIgnore.forEach(v => this.rascalNativeSchemes.add(v)); - } -} - - -async function asyncCatcher(build: () => Thenable): Promise { - try { - return await build(); - } - catch (e: unknown) { - if (e instanceof vscode.FileSystemError) { - throw new rpc.ResponseError(ErrorCodes.fileSystem, e.message, e.code); - } - if (e instanceof rpc.ResponseError) { - throw e; - } - throw new rpc.ResponseError(ErrorCodes.generic, "" + e); - } -} - -async function asyncVoidCatcher(run: (() => Promise) | Thenable): Promise { - return asyncCatcher(() => { - if (typeof run === "function") { - return run(); - } - else { - return run; - } - }); -} - - -class ResolverClient implements VSCodeResolverServer, Disposable { - private readonly connection: rpc.MessageConnection; - private readonly watchListener: WatchEventReceiver; - private readonly fs: vscode.FileSystem; - private readonly rascalNativeSchemes: Set; - private toClear: Disposable[] = []; - constructor(connection: rpc.MessageConnection, debug: boolean, rascalNativeSchemes: Set, private readonly logger: vscode.LogOutputChannel){ - this.rascalNativeSchemes = rascalNativeSchemes; - this.fs = vscode.workspace.fs; - this.connection = connection; - if (debug) { - connection.trace(rpc.Trace.Verbose, { - log: (a) => { - this.logger.debug("[VFS]: " + a); - } - }); - } - this.watchListener = buildWatchReceiver(connection); - connectInputHandler(connection, this, this.toClear); - connectOutputHandler(connection, this, this.toClear); - connectWatchHandler(connection, this, this.toClear); - } - - toUri(req: ISourceLocationRequest | ISourceLocation): vscode.Uri { - if (typeof req !== 'string') { - req = req.uri; - } - const uri = vscode.Uri.parse(req); - if (this.isRascalNative(uri)) { - throw new rpc.ResponseError(ErrorCodes.nativeRascal, "Cannot request VFS jobs on native rascal URIs: " + req); - } - return uri; - } - - - - async readFile(req: ISourceLocationRequest): Promise { - this.logger.trace("[VFS] readFile: ", req.uri); - return asyncCatcher(async () => { - errorCode: 0, - contents: Buffer.from( - await this.fs.readFile(this.toUri(req)) - ).toString("base64") - }); - } - - isRascalNative(req: ISourceLocationRequest | vscode.Uri) : boolean { - //this.rascalNativeSchemes.has(uri.scheme) - const scheme = "scheme" in req ? req.scheme : req.uri.substring(0, req.uri.indexOf(":")); - return this.rascalNativeSchemes.has(scheme); - } - - async exists(req: ISourceLocationRequest): Promise { - this.logger.trace("[VFS] exists: ", req.uri); - try { - await this.stat(req); - return { result: true }; - } - catch (_e) { - return { result: false }; - } - } - - async fileStat(req: ISourceLocationRequest): Promise { - return asyncCatcher(async () => { - const fileInfo = await this.stat(req); - return { - exists: true, - type: fileInfo.type.valueOf(), - ctime: fileInfo.ctime, - mtime: fileInfo.mtime, - size: fileInfo.size, - permissions: fileInfo.permissions ? fileInfo.permissions.valueOf() : 0 - }; - }); - } - - private async stat(req: ISourceLocationRequest): Promise { - this.logger.trace("[VFS] stat: ", req.uri); - return this.fs.stat(this.toUri(req)); - } - - private async timeStampResult(req: ISourceLocationRequest, mapper: (s :vscode.FileStat) => number): Promise { - return asyncCatcher(async () => { - timestamp: mapper((await this.stat(req))) - }); - } - - lastModified(req: ISourceLocationRequest): Promise { - this.logger.trace("[VFS] lastModified: ", req.uri); - return this.timeStampResult(req, f => f.mtime); - } - created(req: ISourceLocationRequest): Promise { - this.logger.trace("[VFS] created: ", req.uri); - return this.timeStampResult(req, f => f.ctime); - } - - private async numberResult(req: ISourceLocationRequest, mapper: (s: vscode.FileStat) => number): Promise { - return asyncCatcher(async () => { - result: mapper((await this.stat(req))) - }); - } - - size(req: ISourceLocationRequest): Promise { - this.logger.trace("[VFS] size: ", req.uri); - return this.numberResult(req, f => f.size); - } - - private async boolResult(req: ISourceLocationRequest, mapper: (s :vscode.FileStat) => boolean): Promise { - return asyncCatcher(async () => { - result: mapper((await this.stat(req))) - }); - } - - isDirectory(req: ISourceLocationRequest): Promise { - this.logger.trace("[VFS] isDirectory: ", req.uri); - return this.boolResult(req, f => f.type === vscode.FileType.Directory); - } - - isFile(req: ISourceLocationRequest): Promise { - this.logger.trace("[VFS] isFile: ", req.uri); - // TODO: figure out how to handle vscode.FileType.Symlink - return this.boolResult(req, f => f.type === vscode.FileType.File); - } - - isReadable(req: ISourceLocationRequest): Promise { - this.logger.trace("[VFS] isReadable: ", req.uri); - // if we can do a stat, we can read - return this.boolResult(req, _ => true); - } - - async isWritable(req: ISourceLocationRequest): Promise { - this.logger.trace("[VFS] isWritable: ", req.uri); - const scheme = this.toUri(req).scheme; - const writable = this.fs.isWritableFileSystem(scheme); - if (writable === undefined) { - throw new rpc.ResponseError(ErrorCodes.fileSystem, "Unsupported scheme: " + scheme, "Unsupported file system"); - } - if (!writable) { - // not a writable file system, so no need to check the uri - return {result : false }; - } - return this.boolResult(req, f => f.permissions === undefined || (f.permissions & vscode.FilePermission.Readonly) === 0); - } - - async list(req: ISourceLocationRequest): Promise { - this.logger.trace("[VFS] list: ", req.uri); - return asyncCatcher(async () => { - const entries = await this.fs.readDirectory(this.toUri(req)); - return { - entries: entries.map(([entry, _type], _index) => entry), - areDirectory: entries.map(([_entry, type], _index) => type === vscode.FileType.Directory) - }; - }); - } - - - - async writeFile(req: WriteFileRequest): Promise { - this.logger.trace("[VFS] writeFile: ", req.uri); - return asyncVoidCatcher( - this.fs.writeFile(this.toUri(req), Buffer.from(req.content, "base64")) - ); - } - async mkDirectory(req: ISourceLocationRequest): Promise { - this.logger.trace("[VFS] mkDirectory: ", req.uri); - return asyncVoidCatcher(this.fs.createDirectory(this.toUri(req))); - } - async remove(req: ISourceLocationRequest): Promise { - this.logger.trace("[VFS] remove: ", req.uri); - return asyncVoidCatcher(this.fs.delete(this.toUri(req))); - } - async rename(req: RenameRequest): Promise { - this.logger.trace("[VFS] rename: ", req.from, req.to); - const from = this.toUri(req.from); - const to = this.toUri(req.to); - return asyncVoidCatcher(this.fs.rename(from, to, { overwrite: req.overwrite })); - } - - private readonly activeWatches = new Map(); - - async watch(newWatch: WatchRequest): Promise { - this.logger.trace("[VFS] watch: ", newWatch.uri); - const watchKey = newWatch.uri + newWatch.recursive; - if (!this.activeWatches.has(watchKey)) { - const watcher = new WatcherCallbacks(this.toUri(newWatch.uri), newWatch.recursive, this.watchListener, newWatch.watcher); - this.activeWatches.set(watchKey, watcher); - this.toClear.push(watcher); - return; - } - throw new rpc.ResponseError(ErrorCodes.fileSystem, 'Watch already defined for: ' + newWatch.uri, 'AlreadyDefined'); - } - - - - async unwatch(removeWatch: WatchRequest): Promise { - this.logger.trace("[VFS] unwatch: ", removeWatch.uri); - const watchKey = removeWatch.uri + removeWatch.recursive; - const watcher = this.activeWatches.get(watchKey); - if (watcher) { - this.activeWatches.delete(watchKey); - watcher.dispose(); - const index = this.toClear.indexOf(watcher); - if (index >= 0) { - this.toClear.splice(index, 1); - } - return; - } - throw new rpc.ResponseError(ErrorCodes.fileSystem, 'Watch not defined for: ' + removeWatch.uri, 'NotDefined'); - } - - dispose() { - this.activeWatches.clear(); - this.toClear.forEach(c => c.dispose()); - try { - this.connection.end(); - } catch (_e: unknown) { - // ignore errors here, ase we are disposing anyway - } - } - - -} - -class WatcherCallbacks implements Disposable { - private readonly watchId: string; - private readonly toClear: Disposable[] = []; - private readonly watchListener: WatchEventReceiver; - constructor(uri: vscode.Uri, recursive: boolean, watchListener: WatchEventReceiver, watchId: string) { - this.watchId = watchId; - this.watchListener = watchListener; - const newWatcher = vscode.workspace.createFileSystemWatcher( - new vscode.RelativePattern(uri, recursive ? '**/*' : '*') - ); - this.toClear.push(newWatcher); - newWatcher.onDidCreate(e => this.sendWatchEvent(e, ISourceLocationChangeType.created), this.toClear); - newWatcher.onDidChange(e => this.sendWatchEvent(e, ISourceLocationChangeType.modified), this.toClear); - newWatcher.onDidDelete(e => this.sendWatchEvent(e, ISourceLocationChangeType.deleted), this.toClear); - - } - - private async sendWatchEvent(uri: vscode.Uri, changeType: ISourceLocationChangeType) { - this.watchListener.emitWatch({ - watchId: this.watchId, - changeType: changeType, - location: uri.toString() - }); - } - - dispose() { - this.toClear.forEach(c => c.dispose()); - this.toClear.splice(0); - } -} diff --git a/rascal-vscode-extension/src/lsp/LanguageRegistry.ts b/rascal-vscode-extension/src/lsp/LanguageRegistry.ts index 53e565a41..8ea8f9454 100644 --- a/rascal-vscode-extension/src/lsp/LanguageRegistry.ts +++ b/rascal-vscode-extension/src/lsp/LanguageRegistry.ts @@ -30,11 +30,11 @@ import { Disposable, LogOutputChannel } from 'vscode'; import { JsonRpcServer } from '../util/JsonRpcServer'; /** - * Json-rpc server that handles registration and unregistration of languages + * JSON-RPC server that handles registration and unregistration of languages */ export class LanguageRegistry extends JsonRpcServer { constructor(dslLSP: ParameterizedLanguageServer, logger: LogOutputChannel) { - super("LanguageRegistry", connection => Disposable.from( + super("LanguageRegistry", false, connection => Disposable.from( connection.onRequest(new rpc.RequestType1("rascal/receiveRegisterLanguage"), lang => { logger.info("LanguageRegistry: registerLanguage", lang); return dslLSP.registerLanguage(lang); diff --git a/rascal-vscode-extension/src/lsp/ParameterizedLanguageServer.ts b/rascal-vscode-extension/src/lsp/ParameterizedLanguageServer.ts index 26d513f9d..592d033f4 100644 --- a/rascal-vscode-extension/src/lsp/ParameterizedLanguageServer.ts +++ b/rascal-vscode-extension/src/lsp/ParameterizedLanguageServer.ts @@ -29,7 +29,7 @@ import * as vscode from 'vscode'; import { BaseLanguageClient } from 'vscode-languageclient'; import { activateLanguageClient } from './RascalLSPConnection'; -import { VSCodeUriResolverServer } from '../fs/VSCodeURIResolver'; +import { VSCodeFileSystemInRascal } from '../fs/VSCodeFileSystemInRascal'; export class ParameterizedLanguageServer implements vscode.Disposable { private readonly registeredFileExtensions:Map> = new Map(); @@ -37,7 +37,7 @@ export class ParameterizedLanguageServer implements vscode.Disposable { constructor( context: vscode.ExtensionContext, - private readonly vfsServer: VSCodeUriResolverServer, + private readonly vfsServer: VSCodeFileSystemInRascal, private readonly absoluteJarPath: string, private readonly deployMode = true, private readonly languageId = 'parametric-rascalmpl', diff --git a/rascal-vscode-extension/src/lsp/RascalLSPConnection.ts b/rascal-vscode-extension/src/lsp/RascalLSPConnection.ts index 9ab434b37..714c56b90 100644 --- a/rascal-vscode-extension/src/lsp/RascalLSPConnection.ts +++ b/rascal-vscode-extension/src/lsp/RascalLSPConnection.ts @@ -31,17 +31,17 @@ import * as vscode from 'vscode'; import { integer, LanguageClient, LanguageClientOptions, ServerOptions, StreamInfo } from 'vscode-languageclient/node'; import { getJavaExecutable } from '../auto-jvm/JavaLookup'; -import { RascalFileSystemProvider } from '../fs/RascalFileSystemProviders'; -import { VSCodeUriResolverServer } from '../fs/VSCodeURIResolver'; +import { RascalFileSystemInVSCode } from '../fs/RascalFileSystemInVSCode'; +import { VSCodeFileSystemInRascal } from '../fs/VSCodeFileSystemInRascal'; import { JsonParserOutputChannel } from './JsonOutputChannel'; export async function activateLanguageClient( { language, title, jarPath, vfsServer, isParametricServer = false, deployMode = true, devPort = -1, dedicated = false, lspArg = "" } : - {language: string, title: string, jarPath: string, vfsServer: VSCodeUriResolverServer, isParametricServer: boolean, deployMode: boolean, devPort: integer, dedicated: boolean, lspArg: string | undefined} ) + {language: string, title: string, jarPath: string, vfsServer: VSCodeFileSystemInRascal, isParametricServer: boolean, deployMode: boolean, devPort: integer, dedicated: boolean, lspArg: string | undefined} ) : Promise { const logger = new JsonParserOutputChannel(title); const serverOptions: ServerOptions = deployMode - ? await buildRascalServerOptions(jarPath, isParametricServer, dedicated, lspArg, logger) + ? await buildRascalServerOptions(jarPath, isParametricServer, dedicated, lspArg, await vfsServer.serverPort, logger) : () => connectToRascalLanguageServerSocket(devPort) // we assume a server is running in debug mode .then((socket) => { writer: socket, reader: socket}); @@ -54,24 +54,23 @@ export async function activateLanguageClient( await client.start(); logger.setClient(client); - client.sendNotification("rascal/vfs/register", { - port: await vfsServer.serverPort - }); client.onNotification("rascal/showContent", (uri: string, title: string, viewColumn: integer) => { + logger.trace(`[RascalLSPConnection] showContent: ${uri}`); showContentPanel(uri, title, viewColumn); }); client.onNotification("rascal/editDocument", (uri: string, range: vscode.Range | undefined, viewColumn: integer) => { + logger.trace(`[RascalLSPConnection] editDocument: ${uri}`); openEditor(uri, range, viewColumn); }); - const schemesReply = client.sendRequest("rascal/filesystem/schemes"); + const schemesReply = client.sendRequest("rascal/vfs/schemes"); schemesReply.then( schemes => { vfsServer.ignoreSchemes(schemes); - new RascalFileSystemProvider(client, logger).tryRegisterSchemes(schemes); + new RascalFileSystemInVSCode(client, logger).tryRegisterSchemes(schemes); }); return client; @@ -157,14 +156,15 @@ function loadURLintoPanel(panel:vscode.WebviewPanel, url:string): void { `; } -async function buildRascalServerOptions(jarPath: string, isParametricServer: boolean, dedicated: boolean, lspArg: string | undefined, logger: vscode.LogOutputChannel): Promise { +async function buildRascalServerOptions(jarPath: string, isParametricServer: boolean, dedicated: boolean, lspArg: string | undefined, remoteResolverRegistryPort : number, logger: vscode.LogOutputChannel): Promise { const classpath = buildCompilerJVMPath(jarPath); const commandArgs = [ '-Dlog4j2.configurationFactory=org.rascalmpl.vscode.lsp.log.LogJsonConfiguration' , '-Dlog4j2.level=DEBUG' - , '-Drascal.fallbackResolver=org.rascalmpl.vscode.lsp.uri.FallbackResolver' , '-Drascal.lsp.deploy=true' , '-Drascal.compilerClasspath=' + classpath + , '-Drascal.remoteResolverRegistryPort=' + remoteResolverRegistryPort + , '-Drascal.specializedRemoteResolverRegistryClass=org.rascalmpl.vscode.lsp.uri.jsonrpc.VSCodeFileSystemInRascal' ]; let mainClass: string; if (isParametricServer) { diff --git a/rascal-vscode-extension/src/lsp/RascalLanguageServer.ts b/rascal-vscode-extension/src/lsp/RascalLanguageServer.ts index de40a85c4..fbb77e1f2 100644 --- a/rascal-vscode-extension/src/lsp/RascalLanguageServer.ts +++ b/rascal-vscode-extension/src/lsp/RascalLanguageServer.ts @@ -27,7 +27,7 @@ import * as vscode from 'vscode'; import { BaseLanguageClient } from 'vscode-languageclient'; -import { VSCodeUriResolverServer } from '../fs/VSCodeURIResolver'; +import { VSCodeFileSystemInRascal } from '../fs/VSCodeFileSystemInRascal'; import { activateLanguageClient } from './RascalLSPConnection'; import { ParameterizedLanguageServer } from './ParameterizedLanguageServer'; import { RascalDebugClient } from '../dap/RascalDebugClient'; @@ -41,7 +41,7 @@ export class RascalLanguageServer implements vscode.Disposable { constructor( _context: vscode.ExtensionContext, - vfsServer: VSCodeUriResolverServer, + vfsServer: VSCodeFileSystemInRascal, absoluteJarPath: string, dslLSP: ParameterizedLanguageServer, logger: vscode.LogOutputChannel, diff --git a/rascal-vscode-extension/src/lsp/library.ts b/rascal-vscode-extension/src/lsp/library.ts index aad7e839f..2120becca 100644 --- a/rascal-vscode-extension/src/lsp/library.ts +++ b/rascal-vscode-extension/src/lsp/library.ts @@ -27,4 +27,4 @@ // make sure that everything you might need to run `ParameterizedLanguageServer` is exported in this file export { ParameterizedLanguageServer } from "./ParameterizedLanguageServer"; export type { LanguageParameter, ParserSpecification } from "./ParameterizedLanguageServer"; -export { VSCodeUriResolverServer } from "../fs/VSCodeURIResolver"; +export { VSCodeFileSystemInRascal as VSCodeFileSystemInRascal } from "../fs/VSCodeFileSystemInRascal"; diff --git a/rascal-vscode-extension/src/test/vscode-suite/repl.test.ts b/rascal-vscode-extension/src/test/vscode-suite/repl.test.ts index 461b0f201..db280a47c 100644 --- a/rascal-vscode-extension/src/test/vscode-suite/repl.test.ts +++ b/rascal-vscode-extension/src/test/vscode-suite/repl.test.ts @@ -80,7 +80,7 @@ describe('REPL', function () { expect(repl.lastOutput).is.equal("5\nint: 0"); }).timeout(Delays.extremelySlow * 3); - it("edit call module via repl", async() => { + it("open module editor via repl", async() => { const repl = new RascalREPL(bench, driver); await repl.start(); await repl.execute(":edit demo::lang::pico::LanguageServer", true, Delays.extremelySlow); @@ -88,6 +88,14 @@ describe('REPL', function () { await driver.wait(async () => await (await bench.getEditorView().getActiveTab())?.getTitle() === "LanguageServer.rsc", Delays.slow, "LanguageServer should be opened"); }); + it("open stdlib module editor via repl", async() => { + const repl = new RascalREPL(bench, driver); + await repl.start(); + await repl.execute(":edit IO", true, Delays.extremelySlow); + + await driver.wait(async () => await (await bench.getEditorView().getActiveTab())?.getTitle() === "IO.rsc", Delays.slow, "IO should be opened"); + }); + it("VFS works", async() => { const repl = new RascalREPL(bench, driver); await repl.start(); diff --git a/rascal-vscode-extension/src/util/JsonRpcServer.ts b/rascal-vscode-extension/src/util/JsonRpcServer.ts index 3b11130b4..91f9e9af3 100644 --- a/rascal-vscode-extension/src/util/JsonRpcServer.ts +++ b/rascal-vscode-extension/src/util/JsonRpcServer.ts @@ -29,14 +29,14 @@ import * as net from 'net'; import { Disposable, LogOutputChannel } from 'vscode'; /** - * Json-rpc server that starts a server on a dynamic port + * JSON-RPC server that starts a server on a dynamic port */ export class JsonRpcServer implements Disposable { readonly serverPort: Promise; private readonly server: net.Server; private activeClients: net.Socket[] = []; - constructor(name: string, connectHandlers: (connection: rpc.MessageConnection) => Disposable, logger: LogOutputChannel) { + constructor(name: string, debug: boolean, connectHandlers: (connection: rpc.MessageConnection) => Disposable, logger: LogOutputChannel) { this.server = net.createServer({noDelay: true}, newClient => { logger.info(`${name}: new connection ${JSON.stringify(newClient.address())}`); newClient.setNoDelay(true); @@ -64,12 +64,13 @@ export class JsonRpcServer implements Disposable { }); this.server.on('error', e => logger.error(`${name} (server): ${e}`)); this.serverPort = new Promise((r, e) => { - this.server.listen(0, "localhost", undefined, () => { + this.server.listen(debug ? 8889 : 0, "localhost", undefined, () => { const address = this.server.address(); if (address && typeof(address) !== "string" && 'port' in address) { logger.debug(`${name}: listening on ${JSON.stringify(address)}`); r(address.port); } else { + logger.error(`${name}: failed to start listening on ${JSON.stringify(address)}`); e(new Error(`${name}: server address not valid: ${JSON.stringify(address)}`)); } });