From 51b63934ffcc6e826b6ae6ee28ce7bc40e9c8b17 Mon Sep 17 00:00:00 2001 From: Rodin Aarssen Date: Wed, 7 Jan 2026 15:43:03 +0100 Subject: [PATCH 01/98] Added an interface for a remote resolver registry --- .../uri/vfs/IRemoteResolverRegistry.java | 391 ++++++++++++++++++ 1 file changed, 391 insertions(+) create mode 100644 src/org/rascalmpl/uri/vfs/IRemoteResolverRegistry.java diff --git a/src/org/rascalmpl/uri/vfs/IRemoteResolverRegistry.java b/src/org/rascalmpl/uri/vfs/IRemoteResolverRegistry.java new file mode 100644 index 00000000000..94640ee4f48 --- /dev/null +++ b/src/org/rascalmpl/uri/vfs/IRemoteResolverRegistry.java @@ -0,0 +1,391 @@ +/* + * Copyright (c) 2018-2026, 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.uri.vfs; + +import java.net.URI; +import java.net.URISyntaxException; +import java.util.Arrays; +import java.util.Objects; +import java.util.concurrent.CompletableFuture; + +import org.checkerframework.checker.nullness.qual.EnsuresNonNullIf; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.eclipse.lsp4j.jsonrpc.services.JsonRequest; +import org.eclipse.lsp4j.jsonrpc.validation.NonNull; +import org.rascalmpl.uri.vfs.FileAttributesResult.FileType; +import org.rascalmpl.values.IRascalValueFactory; +import org.rascalmpl.values.ValueFactoryFactory; + +import io.usethesource.vallang.ISourceLocation; +import io.usethesource.vallang.IValueFactory; + +public interface IRemoteResolverRegistry { + @JsonRequest("rascal/vfs/input/readFile") + default CompletableFuture readFile(ISourceLocation loc) { + throw new UnsupportedOperationException(); + } + + @JsonRequest("rascal/vfs/input/exists") + default CompletableFuture exists(ISourceLocation loc) { + throw new UnsupportedOperationException(); + } + + @JsonRequest("rascal/vfs/input/lastModified") + default CompletableFuture lastModified(ISourceLocation loc) { + throw new UnsupportedOperationException(); + } + + @JsonRequest("rascal/vfs/input/created") + default CompletableFuture created(ISourceLocation loc) { + throw new UnsupportedOperationException(); + } + + @JsonRequest("rascal/vfs/input/isDirectory") + default CompletableFuture isDirectory(ISourceLocation loc) { + throw new UnsupportedOperationException(); + } + + @JsonRequest("rascal/vfs/input/isFile") + default CompletableFuture isFile(ISourceLocation loc) { + throw new UnsupportedOperationException(); + } + + @JsonRequest("rascal/vfs/input/list") + //TODO (Rodin): return type not "the same" as in ISourceLocationInput + default CompletableFuture list(ISourceLocation loc) { + throw new UnsupportedOperationException(); + } + + @JsonRequest("rascal/vfs/input/size") + default CompletableFuture size(ISourceLocation loc) { + throw new UnsupportedOperationException(); + } + + @JsonRequest("rascal/vfs/input/stat") + //TODO (Rodin): merge/replace FileAttributesResult with FileAttributes + default CompletableFuture stat(ISourceLocation loc) { + throw new UnsupportedOperationException(); + } + + @JsonRequest("rascal/vfs/input/isReadable") + default CompletableFuture isReadable(ISourceLocation loc) { + throw new UnsupportedOperationException(); + } + + @JsonRequest("rascal/vfs/input/isWritable") + default CompletableFuture isWritable(ISourceLocation loc) { + throw new UnsupportedOperationException(); + } + + @JsonRequest("rascal/vfs/output/writeFile") + //TODO (Rodin): terug naar loc, content, append; create+overwrite in TS oplossen + default CompletableFuture writeFile(ISourceLocation loc, String content, boolean append, boolean create, boolean overwrite) { + throw new UnsupportedOperationException(); + } + + @JsonRequest("rascal/vfs/output/mkDirectory") + default CompletableFuture mkDirectory(ISourceLocation loc) { + throw new UnsupportedOperationException(); + } + + @JsonRequest("rascal/vfs/output/remove") + //TODO (Rodin): ISourceLocationOutput heeft geen `recursive`, URIResolverRegistry wel + default CompletableFuture remove(ISourceLocation loc, boolean recursive) { + throw new UnsupportedOperationException(); + } + + @JsonRequest("rascal/vfs/output/rename") + default CompletableFuture rename(URI from, URI to, boolean overwrite) { + throw new UnsupportedOperationException(); + } + + @JsonRequest("rascal/vfs/watcher/watch") + //TODO (Rodin): uitzoeken + default CompletableFuture watch(WatchRequest req) { + throw new UnsupportedOperationException(); + } + + @JsonRequest("rascal/vfs/watcher/unwatch") + //TODO (Rodin): uitzoeken + default CompletableFuture unwatch(WatchRequest req) { + throw new UnsupportedOperationException(); + } + + @JsonRequest("rascal/vfs/logical/resolveLocation") + default CompletableFuture resolveLocation(ISourceLocation loc) { + throw new UnsupportedOperationException(); + } + + //TODO (Rodin): @JsonRequest tag, plus wat betekent dit binnen Rascal? + default CompletableFuture fileSystemSchemes() { + throw new UnsupportedOperationException(); + } + + //TODO (Rodin): @JsonRequest tag, plus wat betekent dit binnen Rascal? + default void onDidChangeFile(FileChangeEvent event) { + throw new UnsupportedOperationException(); + } + + public static class WatchRequest { + @NonNull private ISourceLocation loc; + @NonNull private String watcher; + + private boolean recursive; + + private final String[] excludes; + + public WatchRequest(ISourceLocation loc, boolean recursive, String watcher) { + this.loc = loc; + this.recursive = recursive; + this.watcher = watcher; + this.excludes = new String[0]; + } + + public WatchRequest(@NonNull String uri, boolean recursive, @NonNull String watcher) { + //TODO (RA): make defensive + this.loc = ValueFactoryFactory.getValueFactory().sourceLocation(uri); + this.recursive = recursive; + this.watcher = watcher; + this.excludes = new String[0]; + } + + public WatchRequest(String uri, boolean recursive, String[] excludes) { + //TODO (RA): make defensive + this.loc = ValueFactoryFactory.getValueFactory().sourceLocation(uri); + this.recursive = recursive; + this.watcher = ""; + this.excludes = excludes; + } + + public ISourceLocation getLocation() { + //TODO (RA): make defensive + return loc; + } + + public String getWatcher() { + return watcher; + } + + public boolean isRecursive() { + return recursive; + } + + public String[] getExcludes() { + return excludes; + } + + @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) + && Arrays.equals(excludes, other.excludes); + } + return false; + } + + @Override + public int hashCode() { + return Objects.hash(super.hashCode(), watcher, recursive, excludes); + } + } + + 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 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) { + //TODO (RA): hier stond iets defensievers + // var uri = Locations.toUri(loc).toString(); + var uri = loc.getURI().toASCIIString(); + if (loc.hasOffsetLength()) { + if (loc.hasLineColumn()) { + return new SourceLocation(uri, loc.getOffset(), loc.getLength(), loc.getBeginLine(), loc.getBeginColumn(), loc.getEndLine(), loc.getEndColumn()); + } + else { + return new SourceLocation(uri, loc.getOffset(), loc.getLength()); + } + } + else { + return new SourceLocation(uri); + } + } + + public ISourceLocation toRascalLocation() throws URISyntaxException { + final IValueFactory VF = IRascalValueFactory.getInstance(); + //TODO (RA): hier stond iets defensievers + // ISourceLocation tmp = Locations.toCheckedLoc(uri); + var tmp = ValueFactoryFactory.getValueFactory().sourceLocation(URI.create(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 { + //TODO (RA): hier stond iets defensievers + // return Locations.toCheckedLoc(uri); + return ValueFactoryFactory.getValueFactory().sourceLocation(URI.create(uri)); + } + } + + 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; + } + } +} From 8adb46efb30611afc2d25c7e8a7d2695ad12eb5f Mon Sep 17 00:00:00 2001 From: Rodin Aarssen Date: Wed, 7 Jan 2026 15:47:37 +0100 Subject: [PATCH 02/98] Ported over FileAttributesResult from rascal-lsp --- .../uri/vfs/FileAttributesResult.java | 132 ++++++++++++++++++ 1 file changed, 132 insertions(+) create mode 100644 src/org/rascalmpl/uri/vfs/FileAttributesResult.java diff --git a/src/org/rascalmpl/uri/vfs/FileAttributesResult.java b/src/org/rascalmpl/uri/vfs/FileAttributesResult.java new file mode 100644 index 00000000000..7616cc83d70 --- /dev/null +++ b/src/org/rascalmpl/uri/vfs/FileAttributesResult.java @@ -0,0 +1,132 @@ +/* + * 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.uri.vfs; + +import java.util.Objects; + +import org.checkerframework.checker.nullness.qual.Nullable; +import org.rascalmpl.uri.FileAttributes; + +public class FileAttributesResult { + private boolean exists; + private FileType type; + private long ctime; + private long mtime; + private long size; + private @Nullable FilePermission permissions; + + public FileAttributesResult(boolean exists, int type, long ctime, long mtime, long size, int permissions) { + this.exists = exists; + this.type = FileType.fromValue(type); + this.ctime = ctime; + this.mtime = mtime; + this.size = size; + this.permissions = FilePermission.fromValue(permissions); + } + + public FileAttributesResult(boolean exists, FileType type, long ctime, long mtime, long size, @Nullable FilePermission permissions) { + this.exists = exists; + this.type = type; + this.ctime = ctime; + this.mtime = mtime; + this.size = size; + this.permissions = permissions; + } + + public FileAttributes getFileAttributes() { + return new FileAttributes(exists, type == FileType.File, ctime, mtime, true, permissions != null, 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 + "]"; + } + + 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; + } + + public static FileType fromValue(int val) { + switch (val) { + case 0: return Unknown; + case 1: return File; + case 2: return Directory; + case 64: return SymbolicLink; + default: throw new IllegalArgumentException("Unknown FileType value " + val); + } + } + } + + 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 @Nullable FilePermission fromValue(int val) { + switch (val) { + case 1: return Readonly; + default: return null; + } + } + } +} From f300250c2f3edbb69899129e1deb6667d6b2a792 Mon Sep 17 00:00:00 2001 From: Rodin Aarssen Date: Fri, 16 Jan 2026 12:27:08 +0100 Subject: [PATCH 03/98] Renamed fallback to external in WatchRegistry --- src/org/rascalmpl/uri/watch/WatchRegistry.java | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/org/rascalmpl/uri/watch/WatchRegistry.java b/src/org/rascalmpl/uri/watch/WatchRegistry.java index 1c4251376e3..f3be99d176a 100644 --- a/src/org/rascalmpl/uri/watch/WatchRegistry.java +++ b/src/org/rascalmpl/uri/watch/WatchRegistry.java @@ -73,7 +73,7 @@ public class WatchRegistry { private final URIResolverRegistry reg; public final ReferenceQueue> clearedReferences = new ReferenceQueue<>(); private final UnaryOperator resolver; - private volatile @Nullable ISourceLocationWatcher fallback; + private volatile @Nullable ISourceLocationWatcher externalRegistry; public WatchRegistry(URIResolverRegistry reg, UnaryOperator resolver) { this.reg = reg; @@ -85,12 +85,12 @@ public WatchRegistry(URIResolverRegistry reg, UnaryOperator res public void registerNative(String scheme, ISourceLocationWatcher watcher) { watchers.put(scheme, watcher); } - public void setFallback(ISourceLocationWatcher fallback) { - this.fallback = fallback; + public void setExternalRegistry(ISourceLocationWatcher externalRegistry) { + this.externalRegistry = externalRegistry; } - public boolean hasFallback() { - return fallback != null; + public boolean hasExternalRegistry() { + return externalRegistry != null; } private ISourceLocation safeResolve(ISourceLocation loc) { @@ -122,8 +122,8 @@ public void watch(ISourceLocation loc, boolean recursive, Predicate p.isRecursive() == recursive && p.getHandler() == finalCallback); } } - else if (fallback != null) { - fallback.unwatch(loc, callback, recursive); + else if (externalRegistry != null) { + externalRegistry.unwatch(loc, callback, recursive); } } From 052ae18d8776372d5d3bb5cb1ab370d592e2d65a Mon Sep 17 00:00:00 2001 From: Rodin Aarssen Date: Fri, 16 Jan 2026 12:27:26 +0100 Subject: [PATCH 04/98] Fixed layout issues --- src/org/rascalmpl/uri/ISourceLocationInput.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/org/rascalmpl/uri/ISourceLocationInput.java b/src/org/rascalmpl/uri/ISourceLocationInput.java index 8995aa94837..4433a44297b 100644 --- a/src/org/rascalmpl/uri/ISourceLocationInput.java +++ b/src/org/rascalmpl/uri/ISourceLocationInput.java @@ -33,10 +33,10 @@ default long created(ISourceLocation uri) throws IOException { /** In bytes !only for internal use! */ long size(ISourceLocation uri) throws IOException; boolean isDirectory(ISourceLocation uri); - boolean isFile(ISourceLocation uri) ; + boolean isFile(ISourceLocation uri); boolean isReadable(ISourceLocation uri) throws IOException; - String[] list(ISourceLocation uri) throws IOException; + String[] list(ISourceLocation uri) throws IOException; String scheme(); boolean supportsHost(); default boolean supportsReadableFileChannel() { From 513ffb33c37489c95e1faa1bf072a2c0d2917690 Mon Sep 17 00:00:00 2001 From: Rodin Aarssen Date: Fri, 16 Jan 2026 12:30:50 +0100 Subject: [PATCH 05/98] Changed URI to ISourceLocation in remote resolver interface --- src/org/rascalmpl/uri/vfs/IRemoteResolverRegistry.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/org/rascalmpl/uri/vfs/IRemoteResolverRegistry.java b/src/org/rascalmpl/uri/vfs/IRemoteResolverRegistry.java index 94640ee4f48..e92b42ec7d1 100644 --- a/src/org/rascalmpl/uri/vfs/IRemoteResolverRegistry.java +++ b/src/org/rascalmpl/uri/vfs/IRemoteResolverRegistry.java @@ -119,7 +119,7 @@ default CompletableFuture remove(ISourceLocation loc, boolean recursive) { } @JsonRequest("rascal/vfs/output/rename") - default CompletableFuture rename(URI from, URI to, boolean overwrite) { + default CompletableFuture rename(ISourceLocation from, ISourceLocation to, boolean overwrite) { throw new UnsupportedOperationException(); } From a144fe554b39cdc0ce121721faf2a737f73aec70 Mon Sep 17 00:00:00 2001 From: Rodin Aarssen Date: Fri, 16 Jan 2026 12:39:33 +0100 Subject: [PATCH 06/98] Removed FallbackResolver in favor of external resolver registry in URIResolverRegistry --- .../rascalmpl/uri/URIResolverRegistry.java | 69 ++++--------------- 1 file changed, 12 insertions(+), 57 deletions(-) diff --git a/src/org/rascalmpl/uri/URIResolverRegistry.java b/src/org/rascalmpl/uri/URIResolverRegistry.java index ef5c9904c90..532382623ed 100644 --- a/src/org/rascalmpl/uri/URIResolverRegistry.java +++ b/src/org/rascalmpl/uri/URIResolverRegistry.java @@ -65,12 +65,7 @@ public class URIResolverRegistry { private final Map> logicalResolvers = new ConcurrentHashMap<>(); private final Map classloaderResolvers = new ConcurrentHashMap<>(); - // we allow the user to define (using -Drascal.fallbackResolver=fully.qualified.classname) a single class that will handle - // scheme's not statically registered. That class should implement at least one of these interfaces - private volatile @Nullable ISourceLocationInput fallbackInputResolver; - private volatile @Nullable ISourceLocationOutput fallbackOutputResolver; - private volatile @Nullable ILogicalSourceLocationResolver fallbackLogicalResolver; - private volatile @Nullable IClassloaderLocationResolver fallbackClassloaderResolver; + public volatile @Nullable IExternalResolverRegistry externalRegistry; private static class InstanceHolder { static URIResolverRegistry sInstance = new URIResolverRegistry(); @@ -116,16 +111,17 @@ private void loadServices() { try { Enumeration resources = getClass().getClassLoader().getResources(RESOLVERS_CONFIG); Collections.list(resources).forEach(f -> loadServices(f)); - var fallbackResolverClassName = System.getProperty("rascal.fallbackResolver"); - if (fallbackResolverClassName != null) { - loadFallback(fallbackResolverClassName); - } } catch (IOException e) { throw new Error("WARNING: Could not load URIResolverRegistry extensions from " + RESOLVERS_CONFIG, e); } } + public void setExternalResolverRegistry(IExternalResolverRegistry externalRegistry) { + this.externalRegistry = externalRegistry; + watchers.setExternalRegistry(externalRegistry); + } + public Set getRegisteredInputSchemes() { return Collections.unmodifiableSet(inputResolvers.keySet()); } @@ -153,46 +149,6 @@ private Object constructService(String name) throws ClassNotFoundException, Inst } } - private void loadFallback(String fallbackClass) { - try { - Object instance = constructService(fallbackClass); - boolean ok = false; - if (instance instanceof ILogicalSourceLocationResolver) { - fallbackLogicalResolver = (ILogicalSourceLocationResolver) instance; - ok = true; - } - - if (instance instanceof ISourceLocationInput) { - fallbackInputResolver = (ISourceLocationInput) instance; - ok = true; - } - - if (instance instanceof ISourceLocationOutput) { - fallbackOutputResolver = (ISourceLocationOutput) instance; - ok = true; - } - - if (instance instanceof IClassloaderLocationResolver) { - fallbackClassloaderResolver = (IClassloaderLocationResolver) instance; - ok = true; - } - - if (instance instanceof ISourceLocationWatcher) { - watchers.setFallback((ISourceLocationWatcher) instance); - } - if (!ok) { - System.err.println("WARNING: could not load fallback resolver " + fallbackClass - + " because it does not implement ISourceLocationInput or ISourceLocationOutput or ILogicalSourceLocationResolver"); - } - } - catch (ClassNotFoundException | InstantiationException | IllegalAccessException | ClassCastException - | IllegalArgumentException | InvocationTargetException | SecurityException e) { - System.err.println("WARNING: could not load resolver due to " + e.getMessage()); - e.printStackTrace(); - } - - } - private void loadServices(URL nextElement) { try { for (String name : readConfigFile(nextElement)) { @@ -402,9 +358,9 @@ private ISourceLocation physicalLocation(ISourceLocation loc) throws IOException loc = resolveAndFixOffsets(loc, resolver, map.values()); } - if (fallbackLogicalResolver != null) { - var fallbackResult = resolveAndFixOffsets(loc == null ? original : loc, fallbackLogicalResolver, Collections.emptyList()); - return fallbackResult == null ? loc : fallbackResult; + if (externalRegistry != null) { + var externalResult = resolveAndFixOffsets(loc == null ? original : loc, externalRegistry, Collections.emptyList()); + return externalResult == null ? loc : externalResult; } return loc; } @@ -464,7 +420,7 @@ private ISourceLocationInput getInputResolver(String scheme) { return result; } } - return fallbackInputResolver; + return externalRegistry; } return result; } @@ -480,7 +436,6 @@ private IClassloaderLocationResolver getClassloaderResolver(String scheme) { return result; } } - return fallbackClassloaderResolver; } return result; } @@ -496,7 +451,7 @@ private ISourceLocationOutput getOutputResolver(String scheme) { return result; } } - return fallbackOutputResolver; + return externalRegistry; } return result; } @@ -1113,7 +1068,7 @@ public boolean hasLogicalResolver(ISourceLocation loc) { } public boolean hasNativelyWatchableResolver(ISourceLocation loc) { - return watchers.hasNativeSupport(loc.getScheme()) || watchers.hasNativeSupport(safeResolve(loc).getScheme()) || watchers.hasFallback(); + return watchers.hasNativeSupport(loc.getScheme()) || watchers.hasNativeSupport(safeResolve(loc).getScheme()) || watchers.hasExternalRegistry(); } public FileAttributes stat(ISourceLocation loc) throws IOException { From 842881a8ced55d69843e0883248bdf63c807ab91 Mon Sep 17 00:00:00 2001 From: Rodin Aarssen Date: Fri, 16 Jan 2026 12:42:32 +0100 Subject: [PATCH 07/98] Removed method from remote interface --- src/org/rascalmpl/uri/vfs/IRemoteResolverRegistry.java | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/org/rascalmpl/uri/vfs/IRemoteResolverRegistry.java b/src/org/rascalmpl/uri/vfs/IRemoteResolverRegistry.java index e92b42ec7d1..8979b1f33c9 100644 --- a/src/org/rascalmpl/uri/vfs/IRemoteResolverRegistry.java +++ b/src/org/rascalmpl/uri/vfs/IRemoteResolverRegistry.java @@ -140,11 +140,6 @@ default CompletableFuture resolveLocation(ISourceLocation loc) throw new UnsupportedOperationException(); } - //TODO (Rodin): @JsonRequest tag, plus wat betekent dit binnen Rascal? - default CompletableFuture fileSystemSchemes() { - throw new UnsupportedOperationException(); - } - //TODO (Rodin): @JsonRequest tag, plus wat betekent dit binnen Rascal? default void onDidChangeFile(FileChangeEvent event) { throw new UnsupportedOperationException(); From 3284a32a1beb64f58b0695a10da5dd9cb3a58a80 Mon Sep 17 00:00:00 2001 From: Rodin Aarssen Date: Fri, 16 Jan 2026 12:44:27 +0100 Subject: [PATCH 08/98] Added IExternalResolverRegistry interface --- .../uri/IExternalResolverRegistry.java | 41 +++++++++++++++++++ 1 file changed, 41 insertions(+) create mode 100644 src/org/rascalmpl/uri/IExternalResolverRegistry.java diff --git a/src/org/rascalmpl/uri/IExternalResolverRegistry.java b/src/org/rascalmpl/uri/IExternalResolverRegistry.java new file mode 100644 index 00000000000..76f3479d669 --- /dev/null +++ b/src/org/rascalmpl/uri/IExternalResolverRegistry.java @@ -0,0 +1,41 @@ +/* + * Copyright (c) 2018-2026, 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.uri; + +public interface IExternalResolverRegistry extends ISourceLocationInputOutput, ILogicalSourceLocationResolver, ISourceLocationWatcher { + @Override + default String scheme() { + throw new UnsupportedOperationException("'scheme' is not supported for external resolvers"); + } + + @Override + default String authority() { + throw new UnsupportedOperationException("`authority` is not supported for external resolvers"); + } + + +} From 16c4bb28cebdb5a3263dfc5a11220b87b3040391 Mon Sep 17 00:00:00 2001 From: Rodin Aarssen Date: Fri, 16 Jan 2026 12:45:37 +0100 Subject: [PATCH 09/98] Added client side for remote resolver registry --- .../RemoteExternalResolverRegistry.java | 252 ++++++++++++++++++ 1 file changed, 252 insertions(+) create mode 100644 src/org/rascalmpl/uri/remote/RemoteExternalResolverRegistry.java diff --git a/src/org/rascalmpl/uri/remote/RemoteExternalResolverRegistry.java b/src/org/rascalmpl/uri/remote/RemoteExternalResolverRegistry.java new file mode 100644 index 00000000000..96e69ad63e0 --- /dev/null +++ b/src/org/rascalmpl/uri/remote/RemoteExternalResolverRegistry.java @@ -0,0 +1,252 @@ +/* + * Copyright (c) 2018-2026, 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.uri.remote; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.InetAddress; +import java.net.Socket; +import java.nio.charset.StandardCharsets; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import java.util.function.Consumer; +import java.util.stream.Stream; + +import org.eclipse.lsp4j.jsonrpc.Launcher; +import org.rascalmpl.exceptions.RuntimeExceptionFactory; +import org.rascalmpl.ideservices.GsonUtils; +import org.rascalmpl.uri.FileAttributes; +import org.rascalmpl.uri.IExternalResolverRegistry; +import org.rascalmpl.uri.vfs.IRemoteResolverRegistry; +import org.rascalmpl.uri.vfs.IRemoteResolverRegistry.WatchRequest; + +import engineering.swat.watch.DaemonThreadPool; +import io.usethesource.vallang.ISourceLocation; + +public class RemoteExternalResolverRegistry implements IExternalResolverRegistry { + private final IRemoteResolverRegistry remote; + + public RemoteExternalResolverRegistry(int remoteResolverRegistryPort) { + this.remote = startClient(remoteResolverRegistryPort); + } + + private IRemoteResolverRegistry startClient(int remoteResolverRegistryPort) { + try { + @SuppressWarnings("resource") + var socket = new Socket(InetAddress.getLoopbackAddress(), remoteResolverRegistryPort); + socket.setTcpNoDelay(true); + Launcher clientLauncher = new Launcher.Builder() + .setRemoteInterface(IRemoteResolverRegistry.class) + .setLocalService(this) + .setInput(socket.getInputStream()) + .setOutput(socket.getOutputStream()) + .configureGson(GsonUtils::configureGson) + .setExecutorService(DaemonThreadPool.buildConstrainedCached("rascal-remote-resolver-registry", Math.max(2, Math.min(6, Runtime.getRuntime().availableProcessors() - 2)))) + .create(); + + clientLauncher.startListening(); + return clientLauncher.getRemoteProxy(); + } catch (Throwable e) { + System.err.println("Error setting up remote resolver registry connection: " + e.getMessage()); + return null; + } + } + + @Override + public InputStream getInputStream(ISourceLocation loc) throws IOException { + try { + var contents = remote.readFile(loc).get(1, TimeUnit.MINUTES); + return new ByteArrayInputStream(contents.getBytes(StandardCharsets.UTF_16)); + } catch (InterruptedException | ExecutionException | TimeoutException e) { + throw RuntimeExceptionFactory.io("Error during remote for `getInputStream` on " + loc + ": " + e.getMessage()); + } + } + + @Override + public boolean exists(ISourceLocation loc) { + try { + return remote.exists(loc).get(1, TimeUnit.MINUTES); + } catch (InterruptedException | ExecutionException | TimeoutException e) { + throw RuntimeExceptionFactory.io("Error during remote `exists` on " + loc + ": " + e.getMessage()); + } + } + + @Override + public long lastModified(ISourceLocation loc) throws IOException { + try { + return remote.lastModified(loc).get(1, TimeUnit.MINUTES); + } catch (InterruptedException | ExecutionException | TimeoutException e) { + throw RuntimeExceptionFactory.io("Error during remote `lastModified` on " + loc + ": " + e.getMessage()); + } + } + + @Override + public long size(ISourceLocation loc) throws IOException { + try { + return remote.size(loc).get(1, TimeUnit.MINUTES); + } catch (InterruptedException | ExecutionException | TimeoutException e) { + throw RuntimeExceptionFactory.io("Error during remote `size` on " + loc + ": " + e.getMessage()); + } + } + + @Override + public boolean isDirectory(ISourceLocation loc) { + try { + return remote.isDirectory(loc).get(1, TimeUnit.MINUTES); + } catch (InterruptedException | ExecutionException | TimeoutException e) { + throw RuntimeExceptionFactory.io("Error during remote `isDirectory` on " + loc + ": " + e.getMessage()); + } + } + + @Override + public boolean isFile(ISourceLocation loc) { + try { + return remote.isFile(loc).get(1, TimeUnit.MINUTES); + } catch (InterruptedException | ExecutionException | TimeoutException e) { + throw RuntimeExceptionFactory.io("Error during remote `isFile` on " + loc + ": " + e.getMessage()); + } + } + + @Override + public boolean isReadable(ISourceLocation loc) throws IOException { + try { + return remote.isReadable(loc).get(1, TimeUnit.MINUTES); + } catch (InterruptedException | ExecutionException | TimeoutException e) { + throw RuntimeExceptionFactory.io("Error during remote `isReadable` on " + loc + ": " + e.getMessage()); + } + } + + @Override + public String[] list(ISourceLocation loc) throws IOException { + try { + return Stream.of(remote.list(loc).get(1, TimeUnit.MINUTES)).map(fwt -> fwt.getName()).toArray(String[]::new); + } catch (InterruptedException | ExecutionException | TimeoutException e) { + throw RuntimeExceptionFactory.io("Error during remote `list` on " + loc + ": " + e.getMessage()); + } + } + + @Override + public boolean supportsHost() { + return false; + } + + @Override + public FileAttributes stat(ISourceLocation loc) throws IOException { + try { + return remote.stat(loc).get(1, TimeUnit.MINUTES).getFileAttributes(); + } catch (InterruptedException | ExecutionException | TimeoutException e) { + throw RuntimeExceptionFactory.io("Error during remote `stat` on " + loc + ": " + e.getMessage()); + } + } + + @Override + public OutputStream getOutputStream(ISourceLocation loc, boolean append) throws IOException { + return new ByteArrayOutputStream() { + private boolean closed = false; + + @Override + public void close() throws IOException { + if (closed) { + return; + } + closed = true; + var contents = this.toString(StandardCharsets.UTF_16); + remote.writeFile(loc, contents, append, true, true); + } + }; + } + + @Override + public void mkDirectory(ISourceLocation loc) throws IOException { + try { + remote.mkDirectory(loc).get(1, TimeUnit.MINUTES); + } catch (InterruptedException | ExecutionException | TimeoutException e) { + throw RuntimeExceptionFactory.io("Error during remote `mkDirectory` on " + loc + ": " + e.getMessage()); + } + } + + @Override + public void remove(ISourceLocation loc) throws IOException { + try { + remote.remove(loc, false).get(1, TimeUnit.MINUTES); + } catch (InterruptedException | ExecutionException | TimeoutException e) { + throw RuntimeExceptionFactory.io("Error during remote `remove` on " + loc + ": " + e.getMessage()); + } + } + + @Override + public void setLastModified(ISourceLocation loc, long timestamp) throws IOException { + throw new IOException("setLastModified is not supported remotely"); + } + + @Override + public boolean isWritable(ISourceLocation loc) throws IOException { + try { + return remote.isWritable(loc).get(1, TimeUnit.MINUTES); + } catch (InterruptedException | ExecutionException | TimeoutException e) { + throw RuntimeExceptionFactory.io("Error during remote `isWritable` on " + loc + ": " + e.getMessage()); + } + } + + @Override + public ISourceLocation resolve(ISourceLocation input) throws IOException { + try { + return remote.resolveLocation(input).get(1, TimeUnit.MINUTES); + } catch (InterruptedException | ExecutionException | TimeoutException e) { + System.err.println("Error during remote `resolve` on " + input + ": " + e.getMessage()); + } + return input; + } + + @Override + public void watch(ISourceLocation root, Consumer watcher, boolean recursive) throws IOException { + try { + remote.watch(new WatchRequest(root, recursive, "")).get(1, TimeUnit.MINUTES); + } catch (InterruptedException | ExecutionException | TimeoutException e) { + throw RuntimeExceptionFactory.io("Error during remote `watch` on " + root + ": " + e.getMessage()); + } + } + + @Override + public void unwatch(ISourceLocation root, Consumer watcher, boolean recursive) throws IOException { + try { + //TODO: arguments are currently not correct + remote.unwatch(new WatchRequest(root.toString(), true, "")).get(1, TimeUnit.MINUTES); + } catch (InterruptedException | ExecutionException | TimeoutException e) { + throw RuntimeExceptionFactory.io("Error during remote `unwatch` on " + root + ": " + e.getMessage()); + } + } + + @Override + public boolean supportsRecursiveWatch() { + return true; + } +} From 9fe54a63f6e81b4251ce0414e7f4c925e79ccd40 Mon Sep 17 00:00:00 2001 From: Rodin Aarssen Date: Fri, 16 Jan 2026 12:47:27 +0100 Subject: [PATCH 10/98] RascalShell connects to external resolver registry when provided with a port during start-up --- src/org/rascalmpl/shell/RascalShell.java | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/src/org/rascalmpl/shell/RascalShell.java b/src/org/rascalmpl/shell/RascalShell.java index f965058e152..e0ef0d2d74f 100644 --- a/src/org/rascalmpl/shell/RascalShell.java +++ b/src/org/rascalmpl/shell/RascalShell.java @@ -14,8 +14,8 @@ package org.rascalmpl.shell; import java.io.IOException; -import java.io.Writer; import java.io.PrintWriter; +import java.io.Writer; import java.nio.charset.StandardCharsets; import java.util.Arrays; @@ -24,12 +24,15 @@ import org.jline.utils.OSUtils; import org.rascalmpl.debug.IRascalMonitor; import org.rascalmpl.repl.streams.StreamUtil; +import org.rascalmpl.uri.URIResolverRegistry; +import org.rascalmpl.uri.remote.RemoteExternalResolverRegistry; public class RascalShell { public static void main(String[] args) throws IOException { int ideServicesPort = -1; + int vfsPort = -1; checkIfHelp(args); var term = connectToTerminal(); @@ -39,8 +42,8 @@ public static void main(String[] args) throws IOException { if (args[i].equals("--remoteIDEServicesPort")) { ideServicesPort = Integer.parseInt(args[++i]); } else if (args[i].equals("--vfsPort")) { - System.err.println("Ignored parameter --vfsPort and its argument"); - i++; // skip the argument + vfsPort = Integer.parseInt(args[++i]); + System.err.println("Found --vfsPort " + vfsPort); } else if (args[i].startsWith("--")) { // Currently unknown named argument, skipping over this System.err.println("Ignored parameter " + args[i]); @@ -50,6 +53,11 @@ public static void main(String[] args) throws IOException { } } + if (vfsPort != -1) { + URIResolverRegistry.getInstance().setExternalResolverRegistry(new RemoteExternalResolverRegistry(vfsPort)); + } + + ShellRunner runner; if (args.length > i) { var monitor = IRascalMonitor.buildConsoleMonitor(term); From d494609a1b78ec8177ba7530ca619f0373af03a4 Mon Sep 17 00:00:00 2001 From: Rodin Aarssen Date: Fri, 23 Jan 2026 13:13:00 +0100 Subject: [PATCH 11/98] Using new GsonUtils configuration function --- .../rascalmpl/uri/remote/RemoteExternalResolverRegistry.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/org/rascalmpl/uri/remote/RemoteExternalResolverRegistry.java b/src/org/rascalmpl/uri/remote/RemoteExternalResolverRegistry.java index 96e69ad63e0..f1cb09289fd 100644 --- a/src/org/rascalmpl/uri/remote/RemoteExternalResolverRegistry.java +++ b/src/org/rascalmpl/uri/remote/RemoteExternalResolverRegistry.java @@ -68,7 +68,7 @@ private IRemoteResolverRegistry startClient(int remoteResolverRegistryPort) { .setLocalService(this) .setInput(socket.getInputStream()) .setOutput(socket.getOutputStream()) - .configureGson(GsonUtils::configureGson) + .configureGson(GsonUtils.complexAsJsonObject()) .setExecutorService(DaemonThreadPool.buildConstrainedCached("rascal-remote-resolver-registry", Math.max(2, Math.min(6, Runtime.getRuntime().availableProcessors() - 2)))) .create(); From 8cebae8bf436428b6b1da69fc28285ab4532c999 Mon Sep 17 00:00:00 2001 From: Rodin Aarssen Date: Fri, 6 Feb 2026 15:14:09 +0100 Subject: [PATCH 12/98] RemoteExternalResolverRegistry now correctly throws plain IOExceptions --- .../RemoteExternalResolverRegistry.java | 32 +++++++++---------- 1 file changed, 15 insertions(+), 17 deletions(-) diff --git a/src/org/rascalmpl/uri/remote/RemoteExternalResolverRegistry.java b/src/org/rascalmpl/uri/remote/RemoteExternalResolverRegistry.java index f1cb09289fd..96f6ee1fd54 100644 --- a/src/org/rascalmpl/uri/remote/RemoteExternalResolverRegistry.java +++ b/src/org/rascalmpl/uri/remote/RemoteExternalResolverRegistry.java @@ -41,7 +41,6 @@ import java.util.stream.Stream; import org.eclipse.lsp4j.jsonrpc.Launcher; -import org.rascalmpl.exceptions.RuntimeExceptionFactory; import org.rascalmpl.ideservices.GsonUtils; import org.rascalmpl.uri.FileAttributes; import org.rascalmpl.uri.IExternalResolverRegistry; @@ -86,7 +85,7 @@ public InputStream getInputStream(ISourceLocation loc) throws IOException { var contents = remote.readFile(loc).get(1, TimeUnit.MINUTES); return new ByteArrayInputStream(contents.getBytes(StandardCharsets.UTF_16)); } catch (InterruptedException | ExecutionException | TimeoutException e) { - throw RuntimeExceptionFactory.io("Error during remote for `getInputStream` on " + loc + ": " + e.getMessage()); + throw new IOException("Error during remote for `getInputStream` on " + loc + ": " + e.getMessage()); } } @@ -95,7 +94,7 @@ public boolean exists(ISourceLocation loc) { try { return remote.exists(loc).get(1, TimeUnit.MINUTES); } catch (InterruptedException | ExecutionException | TimeoutException e) { - throw RuntimeExceptionFactory.io("Error during remote `exists` on " + loc + ": " + e.getMessage()); + return false; } } @@ -104,7 +103,7 @@ public long lastModified(ISourceLocation loc) throws IOException { try { return remote.lastModified(loc).get(1, TimeUnit.MINUTES); } catch (InterruptedException | ExecutionException | TimeoutException e) { - throw RuntimeExceptionFactory.io("Error during remote `lastModified` on " + loc + ": " + e.getMessage()); + throw new IOException("Error during remote `lastModified` on " + loc + ": " + e.getMessage()); } } @@ -113,7 +112,7 @@ public long size(ISourceLocation loc) throws IOException { try { return remote.size(loc).get(1, TimeUnit.MINUTES); } catch (InterruptedException | ExecutionException | TimeoutException e) { - throw RuntimeExceptionFactory.io("Error during remote `size` on " + loc + ": " + e.getMessage()); + throw new IOException("Error during remote `size` on " + loc + ": " + e.getMessage()); } } @@ -122,7 +121,7 @@ public boolean isDirectory(ISourceLocation loc) { try { return remote.isDirectory(loc).get(1, TimeUnit.MINUTES); } catch (InterruptedException | ExecutionException | TimeoutException e) { - throw RuntimeExceptionFactory.io("Error during remote `isDirectory` on " + loc + ": " + e.getMessage()); + return false; } } @@ -131,7 +130,7 @@ public boolean isFile(ISourceLocation loc) { try { return remote.isFile(loc).get(1, TimeUnit.MINUTES); } catch (InterruptedException | ExecutionException | TimeoutException e) { - throw RuntimeExceptionFactory.io("Error during remote `isFile` on " + loc + ": " + e.getMessage()); + return false; } } @@ -140,7 +139,7 @@ public boolean isReadable(ISourceLocation loc) throws IOException { try { return remote.isReadable(loc).get(1, TimeUnit.MINUTES); } catch (InterruptedException | ExecutionException | TimeoutException e) { - throw RuntimeExceptionFactory.io("Error during remote `isReadable` on " + loc + ": " + e.getMessage()); + throw new IOException("Error during remote `isReadable` on " + loc + ": " + e.getMessage()); } } @@ -149,7 +148,7 @@ public String[] list(ISourceLocation loc) throws IOException { try { return Stream.of(remote.list(loc).get(1, TimeUnit.MINUTES)).map(fwt -> fwt.getName()).toArray(String[]::new); } catch (InterruptedException | ExecutionException | TimeoutException e) { - throw RuntimeExceptionFactory.io("Error during remote `list` on " + loc + ": " + e.getMessage()); + throw new IOException("Error during remote `list` on " + loc + ": " + e.getMessage()); } } @@ -163,7 +162,7 @@ public FileAttributes stat(ISourceLocation loc) throws IOException { try { return remote.stat(loc).get(1, TimeUnit.MINUTES).getFileAttributes(); } catch (InterruptedException | ExecutionException | TimeoutException e) { - throw RuntimeExceptionFactory.io("Error during remote `stat` on " + loc + ": " + e.getMessage()); + throw new IOException("Error during remote `stat` on " + loc + ": " + e.getMessage()); } } @@ -189,7 +188,7 @@ public void mkDirectory(ISourceLocation loc) throws IOException { try { remote.mkDirectory(loc).get(1, TimeUnit.MINUTES); } catch (InterruptedException | ExecutionException | TimeoutException e) { - throw RuntimeExceptionFactory.io("Error during remote `mkDirectory` on " + loc + ": " + e.getMessage()); + throw new IOException("Error during remote `mkDirectory` on " + loc + ": " + e.getMessage()); } } @@ -198,7 +197,7 @@ public void remove(ISourceLocation loc) throws IOException { try { remote.remove(loc, false).get(1, TimeUnit.MINUTES); } catch (InterruptedException | ExecutionException | TimeoutException e) { - throw RuntimeExceptionFactory.io("Error during remote `remove` on " + loc + ": " + e.getMessage()); + throw new IOException("Error during remote `remove` on " + loc + ": " + e.getMessage()); } } @@ -212,7 +211,7 @@ public boolean isWritable(ISourceLocation loc) throws IOException { try { return remote.isWritable(loc).get(1, TimeUnit.MINUTES); } catch (InterruptedException | ExecutionException | TimeoutException e) { - throw RuntimeExceptionFactory.io("Error during remote `isWritable` on " + loc + ": " + e.getMessage()); + throw new IOException("Error during remote `isWritable` on " + loc + ": " + e.getMessage()); } } @@ -221,9 +220,8 @@ public ISourceLocation resolve(ISourceLocation input) throws IOException { try { return remote.resolveLocation(input).get(1, TimeUnit.MINUTES); } catch (InterruptedException | ExecutionException | TimeoutException e) { - System.err.println("Error during remote `resolve` on " + input + ": " + e.getMessage()); + throw new IOException("Error during remote `resolve` on " + input + ": " + e.getMessage()); } - return input; } @Override @@ -231,7 +229,7 @@ public void watch(ISourceLocation root, Consumer watcher try { remote.watch(new WatchRequest(root, recursive, "")).get(1, TimeUnit.MINUTES); } catch (InterruptedException | ExecutionException | TimeoutException e) { - throw RuntimeExceptionFactory.io("Error during remote `watch` on " + root + ": " + e.getMessage()); + throw new IOException("Could not watch `" + root + "` remotely: " + e.getMessage()); } } @@ -241,7 +239,7 @@ public void unwatch(ISourceLocation root, Consumer watch //TODO: arguments are currently not correct remote.unwatch(new WatchRequest(root.toString(), true, "")).get(1, TimeUnit.MINUTES); } catch (InterruptedException | ExecutionException | TimeoutException e) { - throw RuntimeExceptionFactory.io("Error during remote `unwatch` on " + root + ": " + e.getMessage()); + throw new IOException("Could not unwatch `" + root + "` remotely: " + e.getMessage()); } } From b25f48c2fbe98876b3359285344bbfc813777eee Mon Sep 17 00:00:00 2001 From: Rodin Aarssen Date: Tue, 10 Feb 2026 09:29:28 +0100 Subject: [PATCH 13/98] Ported over NamedThreadPool from rascal-lsp --- src/org/rascalmpl/util/NamedThreadPool.java | 63 +++++++++++++++++++++ 1 file changed, 63 insertions(+) create mode 100644 src/org/rascalmpl/util/NamedThreadPool.java diff --git a/src/org/rascalmpl/util/NamedThreadPool.java b/src/org/rascalmpl/util/NamedThreadPool.java new file mode 100644 index 00000000000..efd38a3bd6e --- /dev/null +++ b/src/org/rascalmpl/util/NamedThreadPool.java @@ -0,0 +1,63 @@ +/* + * 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.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; + }; + } + +} From 387b85389d2e7e1e695750a38c0e0419aeef9e59 Mon Sep 17 00:00:00 2001 From: Rodin Aarssen Date: Tue, 10 Feb 2026 09:30:02 +0100 Subject: [PATCH 14/98] Ported over Watch-related classes from rascal-lsp --- .../RemoteExternalResolverRegistry.java | 73 ++++++++++++++++++- 1 file changed, 72 insertions(+), 1 deletion(-) diff --git a/src/org/rascalmpl/uri/remote/RemoteExternalResolverRegistry.java b/src/org/rascalmpl/uri/remote/RemoteExternalResolverRegistry.java index 96f6ee1fd54..08ab256ec8b 100644 --- a/src/org/rascalmpl/uri/remote/RemoteExternalResolverRegistry.java +++ b/src/org/rascalmpl/uri/remote/RemoteExternalResolverRegistry.java @@ -46,6 +46,7 @@ import org.rascalmpl.uri.IExternalResolverRegistry; import org.rascalmpl.uri.vfs.IRemoteResolverRegistry; import org.rascalmpl.uri.vfs.IRemoteResolverRegistry.WatchRequest; +import org.rascalmpl.util.NamedThreadPool; import engineering.swat.watch.DaemonThreadPool; import io.usethesource.vallang.ISourceLocation; @@ -246,5 +247,75 @@ public void unwatch(ISourceLocation root, Consumer watch @Override public boolean supportsRecursiveWatch() { return true; - } + } + + private static final ExecutorService exec = NamedThreadPool.cachedDaemon("RemoteExternalResolverRegistry-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. + */ + public 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 String getId() { + return id; + } + + public List> getCallbacks() { + return callbacks; + } + } + + public 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; + } + } } From f0de47ff8a9fd2e0563e72515c507aee385c1c79 Mon Sep 17 00:00:00 2001 From: Rodin Aarssen Date: Tue, 10 Feb 2026 09:30:54 +0100 Subject: [PATCH 15/98] Working on implementing remote watches --- .../RemoteExternalResolverRegistry.java | 52 ++++++++++++++++--- 1 file changed, 45 insertions(+), 7 deletions(-) diff --git a/src/org/rascalmpl/uri/remote/RemoteExternalResolverRegistry.java b/src/org/rascalmpl/uri/remote/RemoteExternalResolverRegistry.java index 08ab256ec8b..df318a854f4 100644 --- a/src/org/rascalmpl/uri/remote/RemoteExternalResolverRegistry.java +++ b/src/org/rascalmpl/uri/remote/RemoteExternalResolverRegistry.java @@ -34,16 +34,26 @@ import java.net.InetAddress; import java.net.Socket; import java.nio.charset.StandardCharsets; +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.ExecutionException; +import java.util.concurrent.ExecutorService; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; import java.util.function.Consumer; import java.util.stream.Stream; +import org.checkerframework.checker.nullness.qual.Nullable; import org.eclipse.lsp4j.jsonrpc.Launcher; import org.rascalmpl.ideservices.GsonUtils; import org.rascalmpl.uri.FileAttributes; import org.rascalmpl.uri.IExternalResolverRegistry; +import org.rascalmpl.uri.ISourceLocationWatcher; import org.rascalmpl.uri.vfs.IRemoteResolverRegistry; import org.rascalmpl.uri.vfs.IRemoteResolverRegistry.WatchRequest; import org.rascalmpl.util.NamedThreadPool; @@ -54,6 +64,9 @@ public class RemoteExternalResolverRegistry implements IExternalResolverRegistry { private final IRemoteResolverRegistry remote; + private final Map watchers = new ConcurrentHashMap<>(); + private final Map watchersById = new ConcurrentHashMap<>(); + public RemoteExternalResolverRegistry(int remoteResolverRegistryPort) { this.remote = startClient(remoteResolverRegistryPort); } @@ -228,19 +241,44 @@ public ISourceLocation resolve(ISourceLocation input) throws IOException { @Override public void watch(ISourceLocation root, Consumer watcher, boolean recursive) throws IOException { try { - remote.watch(new WatchRequest(root, recursive, "")).get(1, TimeUnit.MINUTES); - } catch (InterruptedException | ExecutionException | TimeoutException e) { - throw new IOException("Could not watch `" + root + "` remotely: " + e.getMessage()); + var watch = watchers.computeIfAbsent(new WatchSubscriptionKey(root, recursive), k -> { + System.err.println("Fresh watch, setting up request to server"); + var result = new Watchers(); + result.addNewWatcher(watcher); + watchersById.put(result.getId(), result); + try { + remote.watch(new WatchRequest(root, recursive, result.getId())).get(1, TimeUnit.MINUTES); + } catch (InterruptedException | ExecutionException | TimeoutException e) { + System.err.println("Could not watch `" + root + "` remotely: " + e.getCause().getMessage()); + // throw new IOException("Could not watch `" + root + "` remotely: " + e.getCause().getMessage()); + } + return result; + }); + watch.addNewWatcher(watcher); + } catch (CompletionException ce) { + throw new IOException("Could not watch `" + root + "` remotely: " + ce.getCause().getMessage()); } } @Override public void unwatch(ISourceLocation root, Consumer watcher, boolean recursive) throws IOException { + var watchKey = new WatchSubscriptionKey(root, recursive); + var watch = watchers.get(watchKey); + if (watch != null && watch.removeWatcher(watcher)) { + System.err.println("No other watchers registered, so unregistering at server"); + watchers.remove(watchKey); + if (!watch.getCallbacks().isEmpty()) { + System.err.println("Raced by another thread, canceling unregister"); + watchers.put(watchKey, watch); + return; + } + watchersById.remove(watch.getId()); + } try { - //TODO: arguments are currently not correct - remote.unwatch(new WatchRequest(root.toString(), true, "")).get(1, TimeUnit.MINUTES); - } catch (InterruptedException | ExecutionException | TimeoutException e) { - throw new IOException("Could not unwatch `" + root + "` remotely: " + e.getMessage()); + remote.unwatch(new WatchRequest(root, recursive, watch.getId())).join(); + } catch (CompletionException ce) { + System.err.println("Error removing watch: " + ce.getCause().getMessage()); + throw new IOException(ce.getCause()); } } From a57d1c3f67983660c2a563a3e5d03164b59016e2 Mon Sep 17 00:00:00 2001 From: Rodin Aarssen Date: Wed, 11 Feb 2026 13:27:59 +0100 Subject: [PATCH 16/98] Ported over Lazy from rascal-lsp --- src/org/rascalmpl/util/Lazy.java | 50 ++++++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) create mode 100644 src/org/rascalmpl/util/Lazy.java diff --git a/src/org/rascalmpl/util/Lazy.java b/src/org/rascalmpl/util/Lazy.java new file mode 100644 index 00000000000..8253d6e8328 --- /dev/null +++ b/src/org/rascalmpl/util/Lazy.java @@ -0,0 +1,50 @@ +/* + * 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.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; + } + + }; + + } + +} From 88a66969b80133ab2e0ad71015b68854dc1d9672 Mon Sep 17 00:00:00 2001 From: Rodin Aarssen Date: Wed, 11 Feb 2026 13:28:21 +0100 Subject: [PATCH 17/98] Reinstated directory listing cache for jsonrpc that was present before in rascal-lsp --- .../RemoteExternalResolverRegistry.java | 44 ++++++++++++++++++- 1 file changed, 43 insertions(+), 1 deletion(-) diff --git a/src/org/rascalmpl/uri/remote/RemoteExternalResolverRegistry.java b/src/org/rascalmpl/uri/remote/RemoteExternalResolverRegistry.java index df318a854f4..1d665a6db19 100644 --- a/src/org/rascalmpl/uri/remote/RemoteExternalResolverRegistry.java +++ b/src/org/rascalmpl/uri/remote/RemoteExternalResolverRegistry.java @@ -34,6 +34,7 @@ import java.net.InetAddress; import java.net.Socket; import java.nio.charset.StandardCharsets; +import java.time.Duration; import java.util.List; import java.util.Map; import java.util.Objects; @@ -46,6 +47,7 @@ import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; import java.util.function.Consumer; +import java.util.stream.Collectors; import java.util.stream.Stream; import org.checkerframework.checker.nullness.qual.Nullable; @@ -54,10 +56,17 @@ import org.rascalmpl.uri.FileAttributes; import org.rascalmpl.uri.IExternalResolverRegistry; import org.rascalmpl.uri.ISourceLocationWatcher; +import org.rascalmpl.uri.URIUtil; +import org.rascalmpl.uri.vfs.FileAttributesResult.FileType; import org.rascalmpl.uri.vfs.IRemoteResolverRegistry; +import org.rascalmpl.uri.vfs.IRemoteResolverRegistry.FileWithType; import org.rascalmpl.uri.vfs.IRemoteResolverRegistry.WatchRequest; +import org.rascalmpl.util.Lazy; import org.rascalmpl.util.NamedThreadPool; +import com.github.benmanes.caffeine.cache.Cache; +import com.github.benmanes.caffeine.cache.Caffeine; + import engineering.swat.watch.DaemonThreadPool; import io.usethesource.vallang.ISourceLocation; @@ -133,6 +142,13 @@ public long size(ISourceLocation loc) throws IOException { @Override public boolean isDirectory(ISourceLocation loc) { try { + var cached = cachedDirectoryListing.getIfPresent(URIUtil.getParentLocation(loc)); + if (cached != null) { + var result = cached.get().get(URIUtil.getLocationName(loc)); + if (result != null) { + return result; + } + } return remote.isDirectory(loc).get(1, TimeUnit.MINUTES); } catch (InterruptedException | ExecutionException | TimeoutException e) { return false; @@ -142,6 +158,13 @@ public boolean isDirectory(ISourceLocation loc) { @Override public boolean isFile(ISourceLocation loc) { try { + var cached = cachedDirectoryListing.getIfPresent(URIUtil.getParentLocation(loc)); + if (cached != null) { + var result = cached.get().get(URIUtil.getLocationName(loc)); + if (result != null) { + return !result; + } + } return remote.isFile(loc).get(1, TimeUnit.MINUTES); } catch (InterruptedException | ExecutionException | TimeoutException e) { return false; @@ -157,10 +180,25 @@ public boolean isReadable(ISourceLocation loc) throws IOException { } } + /** + * Rascal's current implementions sometimes ask for a directory listing and then iterate over all entries + * checking whether they are a directory. This is very slow for jsonrcp, so we store the last directory listing + * and check the cache first + */ + private final Cache>> cachedDirectoryListing + = Caffeine.newBuilder() + .expireAfterWrite(Duration.ofSeconds(5)) + .maximumSize(1000) + .build(); + @Override public String[] list(ISourceLocation loc) throws IOException { try { - return Stream.of(remote.list(loc).get(1, TimeUnit.MINUTES)).map(fwt -> fwt.getName()).toArray(String[]::new); + var result = remote.list(loc).get(1, TimeUnit.MINUTES); + cachedDirectoryListing.put(loc, Lazy.defer(() -> { + return Stream.of(result).collect(Collectors.toMap(FileWithType::getName, e -> e.getType() == FileType.Directory)); + })); + return Stream.of(result).map(FileWithType::getName).toArray(String[]::new); } catch (InterruptedException | ExecutionException | TimeoutException e) { throw new IOException("Error during remote `list` on " + loc + ": " + e.getMessage()); } @@ -193,6 +231,7 @@ public void close() throws IOException { closed = true; var contents = this.toString(StandardCharsets.UTF_16); remote.writeFile(loc, contents, append, true, true); + cachedDirectoryListing.invalidate(URIUtil.getParentLocation(loc)); } }; } @@ -201,6 +240,7 @@ public void close() throws IOException { public void mkDirectory(ISourceLocation loc) throws IOException { try { remote.mkDirectory(loc).get(1, TimeUnit.MINUTES); + cachedDirectoryListing.invalidate(URIUtil.getParentLocation(loc)); } catch (InterruptedException | ExecutionException | TimeoutException e) { throw new IOException("Error during remote `mkDirectory` on " + loc + ": " + e.getMessage()); } @@ -210,6 +250,8 @@ public void mkDirectory(ISourceLocation loc) throws IOException { public void remove(ISourceLocation loc) throws IOException { try { remote.remove(loc, false).get(1, TimeUnit.MINUTES); + cachedDirectoryListing.invalidate(loc); + cachedDirectoryListing.invalidate(URIUtil.getParentLocation(loc)); } catch (InterruptedException | ExecutionException | TimeoutException e) { throw new IOException("Error during remote `remove` on " + loc + ": " + e.getMessage()); } From 2d1c2cc3460f1e23ad3e1476d0f2c022394cfed1 Mon Sep 17 00:00:00 2001 From: Rodin Aarssen Date: Wed, 11 Feb 2026 14:54:57 +0100 Subject: [PATCH 18/98] Reinstated handling of native jsonrpc error codes from rascal-lsp --- .../RemoteExternalResolverRegistry.java | 172 ++++++++++-------- 1 file changed, 93 insertions(+), 79 deletions(-) diff --git a/src/org/rascalmpl/uri/remote/RemoteExternalResolverRegistry.java b/src/org/rascalmpl/uri/remote/RemoteExternalResolverRegistry.java index 1d665a6db19..94445f41b9c 100644 --- a/src/org/rascalmpl/uri/remote/RemoteExternalResolverRegistry.java +++ b/src/org/rascalmpl/uri/remote/RemoteExternalResolverRegistry.java @@ -34,11 +34,17 @@ import java.net.InetAddress; import java.net.Socket; 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.List; import java.util.Map; import java.util.Objects; import java.util.UUID; +import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletionException; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.CopyOnWriteArrayList; @@ -47,11 +53,13 @@ import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; import java.util.function.Consumer; +import java.util.function.Function; import java.util.stream.Collectors; import java.util.stream.Stream; import org.checkerframework.checker.nullness.qual.Nullable; import org.eclipse.lsp4j.jsonrpc.Launcher; +import org.eclipse.lsp4j.jsonrpc.ResponseErrorException; import org.rascalmpl.ideservices.GsonUtils; import org.rascalmpl.uri.FileAttributes; import org.rascalmpl.uri.IExternalResolverRegistry; @@ -66,6 +74,7 @@ import com.github.benmanes.caffeine.cache.Cache; import com.github.benmanes.caffeine.cache.Caffeine; +import com.google.gson.JsonPrimitive; import engineering.swat.watch.DaemonThreadPool; import io.usethesource.vallang.ISourceLocation; @@ -102,41 +111,84 @@ private IRemoteResolverRegistry startClient(int remoteResolverRegistryPort) { } } - @Override - public InputStream getInputStream(ISourceLocation loc) throws IOException { + private static U call(Function> function, T argument) throws IOException { try { - var contents = remote.readFile(loc).get(1, TimeUnit.MINUTES); - return new ByteArrayInputStream(contents.getBytes(StandardCharsets.UTF_16)); - } catch (InterruptedException | ExecutionException | TimeoutException e) { - throw new IOException("Error during remote for `getInputStream` on " + loc + ": " + e.getMessage()); + return function.apply(argument).get(1, TimeUnit.MINUTES); + } catch (TimeoutException e) { + throw new IOException("Remote resolver took too long to reply; interrupted to avoid deadlocks"); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new UnsupportedOperationException("Thread should have been interrupted"); + } catch (CompletionException | ExecutionException e) { + var cause = e.getCause(); + if (cause != null) { + if (cause instanceof ResponseErrorException) { + throw translateException((ResponseErrorException) cause); + } + throw new IOException(cause); + } + throw new IOException(e); + } + } + + 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 schemes should not be forwarded"); + default: + return new IOException("Missing case for: " + error); } } + @Override + public InputStream getInputStream(ISourceLocation loc) throws IOException { + return new ByteArrayInputStream(call(remote::readFile, loc).getBytes(StandardCharsets.UTF_16)); + } + @Override public boolean exists(ISourceLocation loc) { try { - return remote.exists(loc).get(1, TimeUnit.MINUTES); - } catch (InterruptedException | ExecutionException | TimeoutException e) { + return call(remote::exists, loc); + } catch (IOException e) { return false; } } @Override public long lastModified(ISourceLocation loc) throws IOException { - try { - return remote.lastModified(loc).get(1, TimeUnit.MINUTES); - } catch (InterruptedException | ExecutionException | TimeoutException e) { - throw new IOException("Error during remote `lastModified` on " + loc + ": " + e.getMessage()); - } + return call(remote::lastModified, loc); } @Override public long size(ISourceLocation loc) throws IOException { - try { - return remote.size(loc).get(1, TimeUnit.MINUTES); - } catch (InterruptedException | ExecutionException | TimeoutException e) { - throw new IOException("Error during remote `size` on " + loc + ": " + e.getMessage()); - } + return call(remote::size, loc); } @Override @@ -149,8 +201,8 @@ public boolean isDirectory(ISourceLocation loc) { return result; } } - return remote.isDirectory(loc).get(1, TimeUnit.MINUTES); - } catch (InterruptedException | ExecutionException | TimeoutException e) { + return call(remote::isDirectory, loc); + } catch (IOException e) { return false; } } @@ -165,19 +217,15 @@ public boolean isFile(ISourceLocation loc) { return !result; } } - return remote.isFile(loc).get(1, TimeUnit.MINUTES); - } catch (InterruptedException | ExecutionException | TimeoutException e) { + return call(remote::isFile, loc); + } catch (IOException e) { return false; } } @Override public boolean isReadable(ISourceLocation loc) throws IOException { - try { - return remote.isReadable(loc).get(1, TimeUnit.MINUTES); - } catch (InterruptedException | ExecutionException | TimeoutException e) { - throw new IOException("Error during remote `isReadable` on " + loc + ": " + e.getMessage()); - } + return call(remote::isReadable, loc); } /** @@ -193,15 +241,11 @@ public boolean isReadable(ISourceLocation loc) throws IOException { @Override public String[] list(ISourceLocation loc) throws IOException { - try { - var result = remote.list(loc).get(1, TimeUnit.MINUTES); - cachedDirectoryListing.put(loc, Lazy.defer(() -> { - return Stream.of(result).collect(Collectors.toMap(FileWithType::getName, e -> e.getType() == FileType.Directory)); - })); - return Stream.of(result).map(FileWithType::getName).toArray(String[]::new); - } catch (InterruptedException | ExecutionException | TimeoutException e) { - throw new IOException("Error during remote `list` on " + loc + ": " + e.getMessage()); - } + var result = call(remote::list, loc); + cachedDirectoryListing.put(loc, Lazy.defer(() -> { + return Stream.of(result).collect(Collectors.toMap(FileWithType::getName, e -> e.getType() == FileType.Directory)); + })); + return Stream.of(result).map(FileWithType::getName).toArray(String[]::new); } @Override @@ -211,11 +255,7 @@ public boolean supportsHost() { @Override public FileAttributes stat(ISourceLocation loc) throws IOException { - try { - return remote.stat(loc).get(1, TimeUnit.MINUTES).getFileAttributes(); - } catch (InterruptedException | ExecutionException | TimeoutException e) { - throw new IOException("Error during remote `stat` on " + loc + ": " + e.getMessage()); - } + return call(remote::stat, loc).getFileAttributes(); } @Override @@ -229,8 +269,8 @@ public void close() throws IOException { return; } closed = true; - var contents = this.toString(StandardCharsets.UTF_16); - remote.writeFile(loc, contents, append, true, true); + var contents = Base64.getEncoder().encodeToString(this.toByteArray()); + call(l -> remote.writeFile(l, contents, append, true, true), loc); cachedDirectoryListing.invalidate(URIUtil.getParentLocation(loc)); } }; @@ -238,46 +278,30 @@ public void close() throws IOException { @Override public void mkDirectory(ISourceLocation loc) throws IOException { - try { - remote.mkDirectory(loc).get(1, TimeUnit.MINUTES); - cachedDirectoryListing.invalidate(URIUtil.getParentLocation(loc)); - } catch (InterruptedException | ExecutionException | TimeoutException e) { - throw new IOException("Error during remote `mkDirectory` on " + loc + ": " + e.getMessage()); - } + call(remote::mkDirectory, loc); + cachedDirectoryListing.invalidate(URIUtil.getParentLocation(loc)); } @Override public void remove(ISourceLocation loc) throws IOException { - try { - remote.remove(loc, false).get(1, TimeUnit.MINUTES); - cachedDirectoryListing.invalidate(loc); - cachedDirectoryListing.invalidate(URIUtil.getParentLocation(loc)); - } catch (InterruptedException | ExecutionException | TimeoutException e) { - throw new IOException("Error during remote `remove` on " + loc + ": " + e.getMessage()); - } + call(l -> remote.remove(l, true), loc); + cachedDirectoryListing.invalidate(loc); + cachedDirectoryListing.invalidate(URIUtil.getParentLocation(loc)); } @Override public void setLastModified(ISourceLocation loc, long timestamp) throws IOException { - throw new IOException("setLastModified is not supported remotely"); + throw new IOException("Remote `setLastModified` is not supported"); } @Override public boolean isWritable(ISourceLocation loc) throws IOException { - try { - return remote.isWritable(loc).get(1, TimeUnit.MINUTES); - } catch (InterruptedException | ExecutionException | TimeoutException e) { - throw new IOException("Error during remote `isWritable` on " + loc + ": " + e.getMessage()); - } + return call(remote::isWritable, loc); } @Override public ISourceLocation resolve(ISourceLocation input) throws IOException { - try { - return remote.resolveLocation(input).get(1, TimeUnit.MINUTES); - } catch (InterruptedException | ExecutionException | TimeoutException e) { - throw new IOException("Error during remote `resolve` on " + input + ": " + e.getMessage()); - } + return call(remote::resolveLocation, input); } @Override @@ -288,12 +312,7 @@ public void watch(ISourceLocation root, Consumer watcher var result = new Watchers(); result.addNewWatcher(watcher); watchersById.put(result.getId(), result); - try { - remote.watch(new WatchRequest(root, recursive, result.getId())).get(1, TimeUnit.MINUTES); - } catch (InterruptedException | ExecutionException | TimeoutException e) { - System.err.println("Could not watch `" + root + "` remotely: " + e.getCause().getMessage()); - // throw new IOException("Could not watch `" + root + "` remotely: " + e.getCause().getMessage()); - } + remote.watch(new WatchRequest(root, recursive, result.getId())).join(); return result; }); watch.addNewWatcher(watcher); @@ -316,12 +335,7 @@ public void unwatch(ISourceLocation root, Consumer watch } watchersById.remove(watch.getId()); } - try { - remote.unwatch(new WatchRequest(root, recursive, watch.getId())).join(); - } catch (CompletionException ce) { - System.err.println("Error removing watch: " + ce.getCause().getMessage()); - throw new IOException(ce.getCause()); - } + call(remote::unwatch, new WatchRequest(root, recursive, watch.getId())); } @Override From 674c6d834ed8277391375133a002ef60df224690 Mon Sep 17 00:00:00 2001 From: Rodin Aarssen Date: Fri, 20 Feb 2026 10:35:39 +0100 Subject: [PATCH 19/98] Using NamedThreadPool instead of DaemonThreadPool --- .../rascalmpl/uri/remote/RemoteExternalResolverRegistry.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/org/rascalmpl/uri/remote/RemoteExternalResolverRegistry.java b/src/org/rascalmpl/uri/remote/RemoteExternalResolverRegistry.java index 94445f41b9c..cb5b2962a68 100644 --- a/src/org/rascalmpl/uri/remote/RemoteExternalResolverRegistry.java +++ b/src/org/rascalmpl/uri/remote/RemoteExternalResolverRegistry.java @@ -76,7 +76,6 @@ import com.github.benmanes.caffeine.cache.Caffeine; import com.google.gson.JsonPrimitive; -import engineering.swat.watch.DaemonThreadPool; import io.usethesource.vallang.ISourceLocation; public class RemoteExternalResolverRegistry implements IExternalResolverRegistry { @@ -100,7 +99,7 @@ private IRemoteResolverRegistry startClient(int remoteResolverRegistryPort) { .setInput(socket.getInputStream()) .setOutput(socket.getOutputStream()) .configureGson(GsonUtils.complexAsJsonObject()) - .setExecutorService(DaemonThreadPool.buildConstrainedCached("rascal-remote-resolver-registry", Math.max(2, Math.min(6, Runtime.getRuntime().availableProcessors() - 2)))) + .setExecutorService(NamedThreadPool.cachedDaemon("rascal-remote-resolver-registry")) .create(); clientLauncher.startListening(); From 8364a67571aa0a35fc50f6826f622fae2f866f0f Mon Sep 17 00:00:00 2001 From: Rodin Aarssen Date: Fri, 20 Feb 2026 10:36:07 +0100 Subject: [PATCH 20/98] Introduced constants instead of magic numbers for errors --- .../uri/remote/RemoteExternalResolverRegistry.java | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/org/rascalmpl/uri/remote/RemoteExternalResolverRegistry.java b/src/org/rascalmpl/uri/remote/RemoteExternalResolverRegistry.java index cb5b2962a68..0408438d91e 100644 --- a/src/org/rascalmpl/uri/remote/RemoteExternalResolverRegistry.java +++ b/src/org/rascalmpl/uri/remote/RemoteExternalResolverRegistry.java @@ -130,12 +130,16 @@ private static U call(Function> function, T argum } } + private static final int JsonRpcErrorCode_Generic = -1; + private static final int JsonRpcErrorCode_FileSystem = -2; + private static final int JsonRpcErrorCode_NativeRascal = -3; + private static IOException translateException(ResponseErrorException cause) { var error = cause.getResponseError(); switch (error.getCode()) { - case -1: + case JsonRpcErrorCode_Generic: return new IOException("Generic error: " + error.getMessage()); - case -2: { + case JsonRpcErrorCode_FileSystem: { if (error.getData() instanceof JsonPrimitive) { var data = (JsonPrimitive) error.getData(); if (data.isString()) { @@ -159,7 +163,7 @@ private static IOException translateException(ResponseErrorException cause) { } return new IOException("File system error: " + error.getMessage() + " data: " + error.getData()); } - case -3: + case JsonRpcErrorCode_NativeRascal: return new IOException("Rascal native schemes should not be forwarded"); default: return new IOException("Missing case for: " + error); From 32b542af24f106124d68257e144c2c6c30c89f99 Mon Sep 17 00:00:00 2001 From: Rodin Aarssen Date: Fri, 20 Feb 2026 10:36:48 +0100 Subject: [PATCH 21/98] RemoteExternalResolverRegistry now forwards setLastModified to the remote --- .../rascalmpl/uri/remote/RemoteExternalResolverRegistry.java | 2 +- src/org/rascalmpl/uri/vfs/IRemoteResolverRegistry.java | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/src/org/rascalmpl/uri/remote/RemoteExternalResolverRegistry.java b/src/org/rascalmpl/uri/remote/RemoteExternalResolverRegistry.java index 0408438d91e..4be4f47c285 100644 --- a/src/org/rascalmpl/uri/remote/RemoteExternalResolverRegistry.java +++ b/src/org/rascalmpl/uri/remote/RemoteExternalResolverRegistry.java @@ -294,7 +294,7 @@ public void remove(ISourceLocation loc) throws IOException { @Override public void setLastModified(ISourceLocation loc, long timestamp) throws IOException { - throw new IOException("Remote `setLastModified` is not supported"); + call(l -> remote.setLastModified(l, timestamp), loc); } @Override diff --git a/src/org/rascalmpl/uri/vfs/IRemoteResolverRegistry.java b/src/org/rascalmpl/uri/vfs/IRemoteResolverRegistry.java index 8979b1f33c9..140dcdacde4 100644 --- a/src/org/rascalmpl/uri/vfs/IRemoteResolverRegistry.java +++ b/src/org/rascalmpl/uri/vfs/IRemoteResolverRegistry.java @@ -96,6 +96,11 @@ default CompletableFuture isReadable(ISourceLocation loc) { throw new UnsupportedOperationException(); } + @JsonRequest("rascal/vfs/output/setLastModified") + default CompletableFuture setLastModified(ISourceLocation loc, long timestamp) { + throw new UnsupportedOperationException(); + } + @JsonRequest("rascal/vfs/input/isWritable") default CompletableFuture isWritable(ISourceLocation loc) { throw new UnsupportedOperationException(); From 1d728fd719d1686dbf5825ec89f3170d3237f95b Mon Sep 17 00:00:00 2001 From: Rodin Aarssen Date: Fri, 20 Feb 2026 10:37:03 +0100 Subject: [PATCH 22/98] RemoteExternalResolverRegistry now forwards supportsRecursiveWatch to the remote --- .../uri/remote/RemoteExternalResolverRegistry.java | 6 +++++- src/org/rascalmpl/uri/vfs/IRemoteResolverRegistry.java | 5 +++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/src/org/rascalmpl/uri/remote/RemoteExternalResolverRegistry.java b/src/org/rascalmpl/uri/remote/RemoteExternalResolverRegistry.java index 4be4f47c285..165822203de 100644 --- a/src/org/rascalmpl/uri/remote/RemoteExternalResolverRegistry.java +++ b/src/org/rascalmpl/uri/remote/RemoteExternalResolverRegistry.java @@ -343,7 +343,11 @@ public void unwatch(ISourceLocation root, Consumer watch @Override public boolean supportsRecursiveWatch() { - return true; + try { + return call(n -> remote.supportsRecursiveWatch(), null); + } catch (IOException e) { + return false; + } } private static final ExecutorService exec = NamedThreadPool.cachedDaemon("RemoteExternalResolverRegistry-watcher"); diff --git a/src/org/rascalmpl/uri/vfs/IRemoteResolverRegistry.java b/src/org/rascalmpl/uri/vfs/IRemoteResolverRegistry.java index 140dcdacde4..6a1ff095224 100644 --- a/src/org/rascalmpl/uri/vfs/IRemoteResolverRegistry.java +++ b/src/org/rascalmpl/uri/vfs/IRemoteResolverRegistry.java @@ -140,6 +140,11 @@ default CompletableFuture unwatch(WatchRequest req) { throw new UnsupportedOperationException(); } + @JsonRequest("rascal/vfs/watcher/supportsRecursiveWatch") + default CompletableFuture supportsRecursiveWatch() { + throw new UnsupportedOperationException(); + } + @JsonRequest("rascal/vfs/logical/resolveLocation") default CompletableFuture resolveLocation(ISourceLocation loc) { throw new UnsupportedOperationException(); From cfc7cf6248811d6aa97746e3b25bc7714033a980 Mon Sep 17 00:00:00 2001 From: Rodin Aarssen Date: Fri, 20 Feb 2026 14:34:16 +0100 Subject: [PATCH 23/98] Using streaming Base64 for remote getOutputStream --- .../uri/remote/RemoteExternalResolverRegistry.java | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/org/rascalmpl/uri/remote/RemoteExternalResolverRegistry.java b/src/org/rascalmpl/uri/remote/RemoteExternalResolverRegistry.java index 165822203de..0c700bce5dd 100644 --- a/src/org/rascalmpl/uri/remote/RemoteExternalResolverRegistry.java +++ b/src/org/rascalmpl/uri/remote/RemoteExternalResolverRegistry.java @@ -71,6 +71,7 @@ import org.rascalmpl.uri.vfs.IRemoteResolverRegistry.WatchRequest; import org.rascalmpl.util.Lazy; import org.rascalmpl.util.NamedThreadPool; +import org.rascalmpl.util.base64.StreamingBase64; import com.github.benmanes.caffeine.cache.Cache; import com.github.benmanes.caffeine.cache.Caffeine; @@ -272,8 +273,11 @@ public void close() throws IOException { return; } closed = true; - var contents = Base64.getEncoder().encodeToString(this.toByteArray()); - call(l -> remote.writeFile(l, contents, append, true, true), loc); + var content = new StringBuilder(); + try (var input = new ByteArrayInputStream(this.toByteArray())) { + StreamingBase64.encode(input, content, true); + } + call(l -> remote.writeFile(l, content.toString(), append, true, true), loc); cachedDirectoryListing.invalidate(URIUtil.getParentLocation(loc)); } }; From 4f975d3b983aa6d73bf754110c3fa55887c3445e Mon Sep 17 00:00:00 2001 From: Rodin Aarssen Date: Fri, 20 Feb 2026 14:34:43 +0100 Subject: [PATCH 24/98] Fixed remote getInputStream: using (streaming) Base64 decoding --- .../rascalmpl/uri/remote/RemoteExternalResolverRegistry.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/org/rascalmpl/uri/remote/RemoteExternalResolverRegistry.java b/src/org/rascalmpl/uri/remote/RemoteExternalResolverRegistry.java index 0c700bce5dd..1d6fb842ae3 100644 --- a/src/org/rascalmpl/uri/remote/RemoteExternalResolverRegistry.java +++ b/src/org/rascalmpl/uri/remote/RemoteExternalResolverRegistry.java @@ -173,7 +173,7 @@ private static IOException translateException(ResponseErrorException cause) { @Override public InputStream getInputStream(ISourceLocation loc) throws IOException { - return new ByteArrayInputStream(call(remote::readFile, loc).getBytes(StandardCharsets.UTF_16)); + return StreamingBase64.decode(call(remote::readFile, loc)); } @Override From fa96e916b4f43a96f39ecfaa5bb81abc61295432 Mon Sep 17 00:00:00 2001 From: Rodin Aarssen Date: Fri, 20 Feb 2026 14:35:06 +0100 Subject: [PATCH 25/98] Removed SourceLocation wrapper class --- .../uri/vfs/IRemoteResolverRegistry.java | 128 ------------------ 1 file changed, 128 deletions(-) diff --git a/src/org/rascalmpl/uri/vfs/IRemoteResolverRegistry.java b/src/org/rascalmpl/uri/vfs/IRemoteResolverRegistry.java index 6a1ff095224..cc7d6b5fea2 100644 --- a/src/org/rascalmpl/uri/vfs/IRemoteResolverRegistry.java +++ b/src/org/rascalmpl/uri/vfs/IRemoteResolverRegistry.java @@ -75,7 +75,6 @@ default CompletableFuture isFile(ISourceLocation loc) { } @JsonRequest("rascal/vfs/input/list") - //TODO (Rodin): return type not "the same" as in ISourceLocationInput default CompletableFuture list(ISourceLocation loc) { throw new UnsupportedOperationException(); } @@ -129,13 +128,11 @@ default CompletableFuture rename(ISourceLocation from, ISourceLocation to, } @JsonRequest("rascal/vfs/watcher/watch") - //TODO (Rodin): uitzoeken default CompletableFuture watch(WatchRequest req) { throw new UnsupportedOperationException(); } @JsonRequest("rascal/vfs/watcher/unwatch") - //TODO (Rodin): uitzoeken default CompletableFuture unwatch(WatchRequest req) { throw new UnsupportedOperationException(); } @@ -171,7 +168,6 @@ public WatchRequest(ISourceLocation loc, boolean recursive, String watcher) { } public WatchRequest(@NonNull String uri, boolean recursive, @NonNull String watcher) { - //TODO (RA): make defensive this.loc = ValueFactoryFactory.getValueFactory().sourceLocation(uri); this.recursive = recursive; this.watcher = watcher; @@ -179,7 +175,6 @@ public WatchRequest(@NonNull String uri, boolean recursive, @NonNull String watc } public WatchRequest(String uri, boolean recursive, String[] excludes) { - //TODO (RA): make defensive this.loc = ValueFactoryFactory.getValueFactory().sourceLocation(uri); this.recursive = recursive; this.watcher = ""; @@ -187,7 +182,6 @@ public WatchRequest(String uri, boolean recursive, String[] excludes) { } public ISourceLocation getLocation() { - //TODO (RA): make defensive return loc; } @@ -239,126 +233,6 @@ public FileType getType() { } } - 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) { - //TODO (RA): hier stond iets defensievers - // var uri = Locations.toUri(loc).toString(); - var uri = loc.getURI().toASCIIString(); - if (loc.hasOffsetLength()) { - if (loc.hasLineColumn()) { - return new SourceLocation(uri, loc.getOffset(), loc.getLength(), loc.getBeginLine(), loc.getBeginColumn(), loc.getEndLine(), loc.getEndColumn()); - } - else { - return new SourceLocation(uri, loc.getOffset(), loc.getLength()); - } - } - else { - return new SourceLocation(uri); - } - } - - public ISourceLocation toRascalLocation() throws URISyntaxException { - final IValueFactory VF = IRascalValueFactory.getInstance(); - //TODO (RA): hier stond iets defensievers - // ISourceLocation tmp = Locations.toCheckedLoc(uri); - var tmp = ValueFactoryFactory.getValueFactory().sourceLocation(URI.create(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; @@ -373,8 +247,6 @@ public FileChangeType getType() { } public ISourceLocation getLocation() throws URISyntaxException { - //TODO (RA): hier stond iets defensievers - // return Locations.toCheckedLoc(uri); return ValueFactoryFactory.getValueFactory().sourceLocation(URI.create(uri)); } } From f74831f2c64a18799786d194c15af9e4880fb12b Mon Sep 17 00:00:00 2001 From: Rodin Aarssen Date: Fri, 20 Feb 2026 14:35:57 +0100 Subject: [PATCH 26/98] Replaced --vfsPort argument with an environment variable such that URIResolverRegistry can configure itself properly --- src/org/rascalmpl/shell/RascalShell.java | 11 ----------- .../rascalmpl/uri/IExternalResolverRegistry.java | 2 -- src/org/rascalmpl/uri/URIResolverRegistry.java | 15 +++++++++++---- 3 files changed, 11 insertions(+), 17 deletions(-) diff --git a/src/org/rascalmpl/shell/RascalShell.java b/src/org/rascalmpl/shell/RascalShell.java index e0ef0d2d74f..70d23b5f820 100644 --- a/src/org/rascalmpl/shell/RascalShell.java +++ b/src/org/rascalmpl/shell/RascalShell.java @@ -24,15 +24,12 @@ import org.jline.utils.OSUtils; import org.rascalmpl.debug.IRascalMonitor; import org.rascalmpl.repl.streams.StreamUtil; -import org.rascalmpl.uri.URIResolverRegistry; -import org.rascalmpl.uri.remote.RemoteExternalResolverRegistry; public class RascalShell { public static void main(String[] args) throws IOException { int ideServicesPort = -1; - int vfsPort = -1; checkIfHelp(args); var term = connectToTerminal(); @@ -41,9 +38,6 @@ public static void main(String[] args) throws IOException { for (; i < args.length; i++) { if (args[i].equals("--remoteIDEServicesPort")) { ideServicesPort = Integer.parseInt(args[++i]); - } else if (args[i].equals("--vfsPort")) { - vfsPort = Integer.parseInt(args[++i]); - System.err.println("Found --vfsPort " + vfsPort); } else if (args[i].startsWith("--")) { // Currently unknown named argument, skipping over this System.err.println("Ignored parameter " + args[i]); @@ -53,11 +47,6 @@ public static void main(String[] args) throws IOException { } } - if (vfsPort != -1) { - URIResolverRegistry.getInstance().setExternalResolverRegistry(new RemoteExternalResolverRegistry(vfsPort)); - } - - ShellRunner runner; if (args.length > i) { var monitor = IRascalMonitor.buildConsoleMonitor(term); diff --git a/src/org/rascalmpl/uri/IExternalResolverRegistry.java b/src/org/rascalmpl/uri/IExternalResolverRegistry.java index 76f3479d669..34f233155b8 100644 --- a/src/org/rascalmpl/uri/IExternalResolverRegistry.java +++ b/src/org/rascalmpl/uri/IExternalResolverRegistry.java @@ -36,6 +36,4 @@ default String scheme() { default String authority() { throw new UnsupportedOperationException("`authority` is not supported for external resolvers"); } - - } diff --git a/src/org/rascalmpl/uri/URIResolverRegistry.java b/src/org/rascalmpl/uri/URIResolverRegistry.java index a02fd2126bd..30aa9b9a9f1 100644 --- a/src/org/rascalmpl/uri/URIResolverRegistry.java +++ b/src/org/rascalmpl/uri/URIResolverRegistry.java @@ -44,6 +44,7 @@ import org.rascalmpl.unicode.UnicodeOutputStreamWriter; import org.rascalmpl.uri.ISourceLocationWatcher.ISourceLocationChanged; import org.rascalmpl.uri.classloaders.IClassloaderLocationResolver; +import org.rascalmpl.uri.remote.RemoteExternalResolverRegistry; import org.rascalmpl.uri.watch.WatchRegistry; import org.rascalmpl.values.IRascalValueFactory; import org.rascalmpl.values.ValueFactoryFactory; @@ -115,11 +116,17 @@ private void loadServices() { catch (IOException e) { throw new Error("WARNING: Could not load URIResolverRegistry extensions from " + RESOLVERS_CONFIG, e); } - } - public void setExternalResolverRegistry(IExternalResolverRegistry externalRegistry) { - this.externalRegistry = externalRegistry; - watchers.setExternalRegistry(externalRegistry); + var remoteResolverRegistryPortProperty = System.getProperty("rascal.remoteResolverRegistryPort"); + if (remoteResolverRegistryPortProperty != null) { + try { + var remoteResolverRegistryPort = Integer.parseInt(remoteResolverRegistryPortProperty); + this.externalRegistry = new RemoteExternalResolverRegistry(remoteResolverRegistryPort); + watchers.setExternalRegistry(this.externalRegistry); + } catch (NumberFormatException e) { + System.err.println("WARNING: Invalid remoteResolverRegistryPort environment variable: " + remoteResolverRegistryPortProperty + " is not parseable as integer"); + } + } } public Set getRegisteredInputSchemes() { From 169d811cfccc2747f2bc347909266ccc9f557031 Mon Sep 17 00:00:00 2001 From: Rodin Aarssen Date: Fri, 20 Feb 2026 15:25:44 +0100 Subject: [PATCH 27/98] Removed the plain join() from the remote watch call --- .../RemoteExternalResolverRegistry.java | 22 ++++++++++--------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/src/org/rascalmpl/uri/remote/RemoteExternalResolverRegistry.java b/src/org/rascalmpl/uri/remote/RemoteExternalResolverRegistry.java index 1d6fb842ae3..c0281ac9061 100644 --- a/src/org/rascalmpl/uri/remote/RemoteExternalResolverRegistry.java +++ b/src/org/rascalmpl/uri/remote/RemoteExternalResolverRegistry.java @@ -314,16 +314,18 @@ public ISourceLocation resolve(ISourceLocation input) throws IOException { @Override public void watch(ISourceLocation root, Consumer watcher, boolean recursive) throws IOException { try { - var watch = watchers.computeIfAbsent(new WatchSubscriptionKey(root, recursive), k -> { - System.err.println("Fresh watch, setting up request to server"); - var result = new Watchers(); - result.addNewWatcher(watcher); - watchersById.put(result.getId(), result); - remote.watch(new WatchRequest(root, recursive, result.getId())).join(); - return result; - }); - watch.addNewWatcher(watcher); - } catch (CompletionException ce) { + synchronized (watchers) { + var key = new WatchSubscriptionKey(root, recursive); + if (!watchers.containsKey(key)) { + System.err.println("Fresh watch, setting up request to server"); + var result = new Watchers(); + result.addNewWatcher(watcher); + watchersById.put(result.getId(), result); + remote.watch(new WatchRequest(root, recursive, result.getId())).get(1, TimeUnit.MINUTES); + } + watchers.get(key).addNewWatcher(watcher); + } + } catch (CompletionException | InterruptedException | ExecutionException | TimeoutException ce) { throw new IOException("Could not watch `" + root + "` remotely: " + ce.getCause().getMessage()); } } From 269b8f1919478f33ae663836e30aa378a7da909a Mon Sep 17 00:00:00 2001 From: Rodin Aarssen Date: Fri, 20 Feb 2026 15:25:48 +0100 Subject: [PATCH 28/98] Imports --- .../rascalmpl/uri/remote/RemoteExternalResolverRegistry.java | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/org/rascalmpl/uri/remote/RemoteExternalResolverRegistry.java b/src/org/rascalmpl/uri/remote/RemoteExternalResolverRegistry.java index c0281ac9061..49c5682a7d9 100644 --- a/src/org/rascalmpl/uri/remote/RemoteExternalResolverRegistry.java +++ b/src/org/rascalmpl/uri/remote/RemoteExternalResolverRegistry.java @@ -33,13 +33,11 @@ import java.io.OutputStream; import java.net.InetAddress; import java.net.Socket; -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.List; import java.util.Map; import java.util.Objects; From 1b2f38c7f959fb6daa03455773dc74ea2aa5f187 Mon Sep 17 00:00:00 2001 From: Rodin Aarssen Date: Wed, 4 Mar 2026 11:10:57 +0100 Subject: [PATCH 29/98] Removed FileChangeEvent and FileChangeType from IRemoveResolverRegistry --- .../uri/vfs/IRemoteResolverRegistry.java | 43 ++----------------- 1 file changed, 3 insertions(+), 40 deletions(-) diff --git a/src/org/rascalmpl/uri/vfs/IRemoteResolverRegistry.java b/src/org/rascalmpl/uri/vfs/IRemoteResolverRegistry.java index cc7d6b5fea2..0e3d56450be 100644 --- a/src/org/rascalmpl/uri/vfs/IRemoteResolverRegistry.java +++ b/src/org/rascalmpl/uri/vfs/IRemoteResolverRegistry.java @@ -26,22 +26,18 @@ */ package org.rascalmpl.uri.vfs; -import java.net.URI; -import java.net.URISyntaxException; import java.util.Arrays; import java.util.Objects; import java.util.concurrent.CompletableFuture; -import org.checkerframework.checker.nullness.qual.EnsuresNonNullIf; import org.checkerframework.checker.nullness.qual.Nullable; +import org.eclipse.lsp4j.jsonrpc.services.JsonNotification; import org.eclipse.lsp4j.jsonrpc.services.JsonRequest; import org.eclipse.lsp4j.jsonrpc.validation.NonNull; import org.rascalmpl.uri.vfs.FileAttributesResult.FileType; -import org.rascalmpl.values.IRascalValueFactory; import org.rascalmpl.values.ValueFactoryFactory; import io.usethesource.vallang.ISourceLocation; -import io.usethesource.vallang.IValueFactory; public interface IRemoteResolverRegistry { @JsonRequest("rascal/vfs/input/readFile") @@ -147,8 +143,8 @@ default CompletableFuture resolveLocation(ISourceLocation loc) throw new UnsupportedOperationException(); } - //TODO (Rodin): @JsonRequest tag, plus wat betekent dit binnen Rascal? - default void onDidChangeFile(FileChangeEvent event) { + @JsonNotification("rascal/vfs/watcher/fileChanged") + default void onDidChangeFile(ISourceLocation loc, int type, String watchId) { throw new UnsupportedOperationException(); } @@ -232,37 +228,4 @@ public FileType getType() { return type; } } - - 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 ValueFactoryFactory.getValueFactory().sourceLocation(URI.create(uri)); - } - } - - 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; - } - } } From 9d2862c52e726a52b95ce4229452736c48ce7d2a Mon Sep 17 00:00:00 2001 From: Rodin Aarssen Date: Wed, 4 Mar 2026 11:17:58 +0100 Subject: [PATCH 30/98] Added value to ISourceLocationChangeType enum --- .../rascalmpl/uri/ISourceLocationWatcher.java | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/src/org/rascalmpl/uri/ISourceLocationWatcher.java b/src/org/rascalmpl/uri/ISourceLocationWatcher.java index 3ba04e8bbe5..1139d97f462 100644 --- a/src/org/rascalmpl/uri/ISourceLocationWatcher.java +++ b/src/org/rascalmpl/uri/ISourceLocationWatcher.java @@ -44,7 +44,6 @@ public interface ISourceLocationWatcher { */ boolean supportsRecursiveWatch(); - public interface ISourceLocationChanged { ISourceLocation getLocation(); ISourceLocationChangeType getChangeType(); @@ -63,9 +62,19 @@ default boolean isChanged() { } public enum ISourceLocationChangeType { - CREATED(), - DELETED(), - MODIFIED() + CREATED(1), + DELETED(2), + MODIFIED(3); + + int value; + + public int getValue() { + return value; + } + + ISourceLocationChangeType(int value) { + this.value = value; + } } static ISourceLocationChanged created(ISourceLocation loc) { From ebc14b27fb2255cd243227e46ed802313915be18 Mon Sep 17 00:00:00 2001 From: Rodin Aarssen Date: Wed, 4 Mar 2026 11:21:36 +0100 Subject: [PATCH 31/98] Implemented remote watch callbacks --- .../RemoteExternalResolverRegistry.java | 42 +++++++++++++++---- 1 file changed, 33 insertions(+), 9 deletions(-) diff --git a/src/org/rascalmpl/uri/remote/RemoteExternalResolverRegistry.java b/src/org/rascalmpl/uri/remote/RemoteExternalResolverRegistry.java index 49c5682a7d9..83beac023b2 100644 --- a/src/org/rascalmpl/uri/remote/RemoteExternalResolverRegistry.java +++ b/src/org/rascalmpl/uri/remote/RemoteExternalResolverRegistry.java @@ -58,6 +58,7 @@ import org.checkerframework.checker.nullness.qual.Nullable; import org.eclipse.lsp4j.jsonrpc.Launcher; import org.eclipse.lsp4j.jsonrpc.ResponseErrorException; +import org.eclipse.lsp4j.jsonrpc.services.JsonNotification; import org.rascalmpl.ideservices.GsonUtils; import org.rascalmpl.uri.FileAttributes; import org.rascalmpl.uri.IExternalResolverRegistry; @@ -316,10 +317,11 @@ public void watch(ISourceLocation root, Consumer watcher var key = new WatchSubscriptionKey(root, recursive); if (!watchers.containsKey(key)) { System.err.println("Fresh watch, setting up request to server"); - var result = new Watchers(); - result.addNewWatcher(watcher); - watchersById.put(result.getId(), result); - remote.watch(new WatchRequest(root, recursive, result.getId())).get(1, TimeUnit.MINUTES); + var freshWatchers = new Watchers(); + freshWatchers.addNewWatcher(watcher); + watchersById.put(freshWatchers.getId(), freshWatchers); + remote.watch(new WatchRequest(root, recursive, freshWatchers.getId())).get(1, TimeUnit.MINUTES); + watchers.put(key, freshWatchers); } watchers.get(key).addNewWatcher(watcher); } @@ -354,14 +356,36 @@ public boolean supportsRecursiveWatch() { } } + private static final int FileChangeType_Changed = 1; + private static final int FileChangeType_Created = 2; + private static final int FileChangeType_Deleted = 3; + + @JsonNotification("rascal/vfs/watcher/fileChanged") + public void emitWatch(ISourceLocation root, int type, String watchId) throws IOException { + synchronized (watchers) { + var watcher = watchersById.get(watchId); + switch (type) { + case FileChangeType_Changed: + watcher.publish(ISourceLocationWatcher.modified(root)); + break; + case FileChangeType_Created: + watcher.publish(ISourceLocationWatcher.created(root)); + break; + case FileChangeType_Deleted: + watcher.publish(ISourceLocationWatcher.deleted(root)); + break; + default: + throw new IOException("Unexpected FileChangeType " + type); + } + } + } + private static final ExecutorService exec = NamedThreadPool.cachedDaemon("RemoteExternalResolverRegistry-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. + * 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 remote side. */ public static class Watchers { private final String id; From a2e62341e682948eb04677939d46c0ce1445b93f Mon Sep 17 00:00:00 2001 From: Rodin Aarssen Date: Fri, 6 Mar 2026 09:47:33 +0100 Subject: [PATCH 32/98] Moved default server implementation for remote Rascal FS access to Rascal --- .../uri/remote/IRascalFileSystemServices.java | 317 ++++++++++++++++++ 1 file changed, 317 insertions(+) create mode 100644 src/org/rascalmpl/uri/remote/IRascalFileSystemServices.java diff --git a/src/org/rascalmpl/uri/remote/IRascalFileSystemServices.java b/src/org/rascalmpl/uri/remote/IRascalFileSystemServices.java new file mode 100644 index 00000000000..b2d429631bc --- /dev/null +++ b/src/org/rascalmpl/uri/remote/IRascalFileSystemServices.java @@ -0,0 +1,317 @@ +/* + * 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.uri.remote; + +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.concurrent.CompletableFuture; +import java.util.concurrent.CompletionException; +import java.util.concurrent.ExecutorService; + +import org.apache.commons.codec.binary.Base64InputStream; +import org.checkerframework.checker.nullness.qual.NonNull; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.eclipse.lsp4j.jsonrpc.ResponseErrorException; +import org.eclipse.lsp4j.jsonrpc.messages.ResponseError; +import org.rascalmpl.uri.FileAttributes; +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.uri.vfs.IRemoteResolverRegistry; +import org.rascalmpl.util.NamedThreadPool; + +import io.usethesource.vallang.ISourceLocation; + +public interface IRascalFileSystemServices extends IRemoteResolverRegistry { + static final URIResolverRegistry reg = URIResolverRegistry.getInstance(); + public static final ExecutorService executor = NamedThreadPool.cachedDaemon("rascal-vfs"); + + @Override + default public CompletableFuture resolveLocation(ISourceLocation loc) { + return CompletableFuture.supplyAsync(() -> { + try { + ISourceLocation resolved = reg.logicalToPhysical(loc); + + if (resolved == null) { + return loc; + } + + return resolved; + } catch (Exception e) { + return loc; + } + }, executor); + } + + @Override + default public CompletableFuture watch(WatchRequest params) { + return CompletableFuture.runAsync(() -> { + try { + ISourceLocation loc = params.getLocation(); + + URIResolverRegistry.getInstance().watch(loc, params.isRecursive(), changed -> { + try { + onDidChangeFile(changed.getLocation(), changed.getChangeType().getValue(), null); + } catch (/*IO*/Exception e) { + throw new RuntimeException(e); + } + }); + } catch (IOException | RuntimeException e) { + throw new VSCodeFSError(e); + } + }, executor); + } + + static FileChangeEvent convertChangeEvent(ISourceLocationChanged changed) throws IOException { + return new FileChangeEvent(convertFileChangeType(changed.getChangeType()), changed.getLocation().getURI().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); + } + } + + @Override + default public CompletableFuture stat(ISourceLocation loc) { + return CompletableFuture.supplyAsync(() -> { + try { + if (!reg.exists(loc)) { + throw new FileNotFoundException(); + } + return reg.stat(loc); + } catch (IOException | RuntimeException e) { + throw new VSCodeFSError(e); + } + }, executor); + } + + @Override + default public CompletableFuture list(ISourceLocation loc) { + return CompletableFuture.supplyAsync(() -> { + try { + 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 | RuntimeException e) { + throw new VSCodeFSError(e); + } + }, executor); + } + + @Override + default public CompletableFuture mkDirectory(ISourceLocation loc) { + return CompletableFuture.runAsync(() -> { + try { + reg.mkDirectory(loc); + } catch (IOException | RuntimeException e) { + throw new VSCodeFSError(e); + } + }, executor); + } + + @Override + default public CompletableFuture readFile(ISourceLocation loc) { + return CompletableFuture.supplyAsync(() -> { + try (InputStream source = new Base64InputStream(reg.getInputStream(loc), true)) { + return new String(source.readAllBytes(), StandardCharsets.US_ASCII); + } catch (IOException | RuntimeException e) { + throw new VSCodeFSError(e); + } + }, executor); + } + + @Override + default public CompletableFuture writeFile(ISourceLocation loc, String content, boolean append, boolean create, boolean overwrite) { + return CompletableFuture.runAsync(() -> { + try { + boolean fileExists = reg.exists(loc); + if (!fileExists && !create) { + throw new FileNotFoundException(loc.toString()); + } + if (fileExists && reg.isDirectory(loc)) { + throw VSCodeFSError.isADirectory(loc); + } + + ISourceLocation parentFolder = URIUtil.getParentLocation(loc); + if (!reg.exists(parentFolder) && create) { + throw new FileNotFoundException(parentFolder.toString()); + } + + if (fileExists && create && !overwrite) { + throw new FileAlreadyExistsException(loc.toString()); + } + try (OutputStream target = reg.getOutputStream(loc, false)) { + target.write(Base64.getDecoder().decode(content)); + } + } catch (IOException | RuntimeException e) { + throw new VSCodeFSError(e); + } + }, executor); + } + + @Override + default public CompletableFuture remove(ISourceLocation loc, boolean recursive) { + return CompletableFuture.runAsync(() -> { + try { + reg.remove(loc, recursive); + } catch (IOException e) { + throw new CompletionException(e); + } + }, executor); + } + + @Override + default public CompletableFuture rename(ISourceLocation from, ISourceLocation to, boolean overwrite) { + return CompletableFuture.runAsync(() -> { + try { + reg.rename(from, to, overwrite); + } catch (IOException e) { + throw new CompletionException(e); + } + }, executor); + } + + @Override + default public void onDidChangeFile(ISourceLocation loc, int type, String watchId) { + // reg.watch(loc, false, null); + } + + 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 null; + } + } + + 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; + } + } + + /** Maps common exceptions to FileSystemError in VS Code */ + public static class VSCodeFSError extends ResponseErrorException { + //TODO (RA): zit ook in Rascal + 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); + } + } +} From 65555f2985c190d4dd19a0177faf7d82b427f7bd Mon Sep 17 00:00:00 2001 From: Rodin Aarssen Date: Fri, 6 Mar 2026 09:48:37 +0100 Subject: [PATCH 33/98] Got rid of FileAttributesResult in favor of FileAttributes --- .../RemoteExternalResolverRegistry.java | 4 +- .../uri/vfs/FileAttributesResult.java | 132 ------------------ .../uri/vfs/IRemoteResolverRegistry.java | 20 ++- 3 files changed, 19 insertions(+), 137 deletions(-) delete mode 100644 src/org/rascalmpl/uri/vfs/FileAttributesResult.java diff --git a/src/org/rascalmpl/uri/remote/RemoteExternalResolverRegistry.java b/src/org/rascalmpl/uri/remote/RemoteExternalResolverRegistry.java index 83beac023b2..526f7ab9a1a 100644 --- a/src/org/rascalmpl/uri/remote/RemoteExternalResolverRegistry.java +++ b/src/org/rascalmpl/uri/remote/RemoteExternalResolverRegistry.java @@ -64,8 +64,8 @@ import org.rascalmpl.uri.IExternalResolverRegistry; import org.rascalmpl.uri.ISourceLocationWatcher; import org.rascalmpl.uri.URIUtil; -import org.rascalmpl.uri.vfs.FileAttributesResult.FileType; import org.rascalmpl.uri.vfs.IRemoteResolverRegistry; +import org.rascalmpl.uri.vfs.IRemoteResolverRegistry.FileType; import org.rascalmpl.uri.vfs.IRemoteResolverRegistry.FileWithType; import org.rascalmpl.uri.vfs.IRemoteResolverRegistry.WatchRequest; import org.rascalmpl.util.Lazy; @@ -258,7 +258,7 @@ public boolean supportsHost() { @Override public FileAttributes stat(ISourceLocation loc) throws IOException { - return call(remote::stat, loc).getFileAttributes(); + return call(remote::stat, loc); } @Override diff --git a/src/org/rascalmpl/uri/vfs/FileAttributesResult.java b/src/org/rascalmpl/uri/vfs/FileAttributesResult.java deleted file mode 100644 index 7616cc83d70..00000000000 --- a/src/org/rascalmpl/uri/vfs/FileAttributesResult.java +++ /dev/null @@ -1,132 +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.uri.vfs; - -import java.util.Objects; - -import org.checkerframework.checker.nullness.qual.Nullable; -import org.rascalmpl.uri.FileAttributes; - -public class FileAttributesResult { - private boolean exists; - private FileType type; - private long ctime; - private long mtime; - private long size; - private @Nullable FilePermission permissions; - - public FileAttributesResult(boolean exists, int type, long ctime, long mtime, long size, int permissions) { - this.exists = exists; - this.type = FileType.fromValue(type); - this.ctime = ctime; - this.mtime = mtime; - this.size = size; - this.permissions = FilePermission.fromValue(permissions); - } - - public FileAttributesResult(boolean exists, FileType type, long ctime, long mtime, long size, @Nullable FilePermission permissions) { - this.exists = exists; - this.type = type; - this.ctime = ctime; - this.mtime = mtime; - this.size = size; - this.permissions = permissions; - } - - public FileAttributes getFileAttributes() { - return new FileAttributes(exists, type == FileType.File, ctime, mtime, true, permissions != null, 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 + "]"; - } - - 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; - } - - public static FileType fromValue(int val) { - switch (val) { - case 0: return Unknown; - case 1: return File; - case 2: return Directory; - case 64: return SymbolicLink; - default: throw new IllegalArgumentException("Unknown FileType value " + val); - } - } - } - - 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 @Nullable FilePermission fromValue(int val) { - switch (val) { - case 1: return Readonly; - default: return null; - } - } - } -} diff --git a/src/org/rascalmpl/uri/vfs/IRemoteResolverRegistry.java b/src/org/rascalmpl/uri/vfs/IRemoteResolverRegistry.java index 0e3d56450be..5f0572c12a1 100644 --- a/src/org/rascalmpl/uri/vfs/IRemoteResolverRegistry.java +++ b/src/org/rascalmpl/uri/vfs/IRemoteResolverRegistry.java @@ -34,7 +34,7 @@ import org.eclipse.lsp4j.jsonrpc.services.JsonNotification; import org.eclipse.lsp4j.jsonrpc.services.JsonRequest; import org.eclipse.lsp4j.jsonrpc.validation.NonNull; -import org.rascalmpl.uri.vfs.FileAttributesResult.FileType; +import org.rascalmpl.uri.FileAttributes; import org.rascalmpl.values.ValueFactoryFactory; import io.usethesource.vallang.ISourceLocation; @@ -81,8 +81,7 @@ default CompletableFuture size(ISourceLocation loc) { } @JsonRequest("rascal/vfs/input/stat") - //TODO (Rodin): merge/replace FileAttributesResult with FileAttributes - default CompletableFuture stat(ISourceLocation loc) { + default CompletableFuture stat(ISourceLocation loc) { throw new UnsupportedOperationException(); } @@ -228,4 +227,19 @@ public FileType getType() { return type; } } + + 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; + } + } } From 7a901fc3f138ab9a79f39d57746cf8a7d8b4fe47 Mon Sep 17 00:00:00 2001 From: Rodin Aarssen Date: Fri, 6 Mar 2026 15:07:42 +0100 Subject: [PATCH 34/98] Changed remote resolver interfaces to align the writeFile signature towards the Rascal side --- .../uri/remote/IRascalFileSystemServices.java | 16 ++-------------- .../remote/RemoteExternalResolverRegistry.java | 2 +- .../uri/vfs/IRemoteResolverRegistry.java | 3 +-- 3 files changed, 4 insertions(+), 17 deletions(-) diff --git a/src/org/rascalmpl/uri/remote/IRascalFileSystemServices.java b/src/org/rascalmpl/uri/remote/IRascalFileSystemServices.java index b2d429631bc..dacac038422 100644 --- a/src/org/rascalmpl/uri/remote/IRascalFileSystemServices.java +++ b/src/org/rascalmpl/uri/remote/IRascalFileSystemServices.java @@ -165,25 +165,13 @@ default public CompletableFuture readFile(ISourceLocation loc) { } @Override - default public CompletableFuture writeFile(ISourceLocation loc, String content, boolean append, boolean create, boolean overwrite) { + default public CompletableFuture writeFile(ISourceLocation loc, String content, boolean append) { return CompletableFuture.runAsync(() -> { try { - boolean fileExists = reg.exists(loc); - if (!fileExists && !create) { - throw new FileNotFoundException(loc.toString()); - } - if (fileExists && reg.isDirectory(loc)) { + if (reg.exists(loc) && reg.isDirectory(loc)) { throw VSCodeFSError.isADirectory(loc); } - ISourceLocation parentFolder = URIUtil.getParentLocation(loc); - if (!reg.exists(parentFolder) && create) { - throw new FileNotFoundException(parentFolder.toString()); - } - - if (fileExists && create && !overwrite) { - throw new FileAlreadyExistsException(loc.toString()); - } try (OutputStream target = reg.getOutputStream(loc, false)) { target.write(Base64.getDecoder().decode(content)); } diff --git a/src/org/rascalmpl/uri/remote/RemoteExternalResolverRegistry.java b/src/org/rascalmpl/uri/remote/RemoteExternalResolverRegistry.java index 526f7ab9a1a..3097bde3b8e 100644 --- a/src/org/rascalmpl/uri/remote/RemoteExternalResolverRegistry.java +++ b/src/org/rascalmpl/uri/remote/RemoteExternalResolverRegistry.java @@ -276,7 +276,7 @@ public void close() throws IOException { try (var input = new ByteArrayInputStream(this.toByteArray())) { StreamingBase64.encode(input, content, true); } - call(l -> remote.writeFile(l, content.toString(), append, true, true), loc); + call(l -> remote.writeFile(l, content.toString(), append), loc); cachedDirectoryListing.invalidate(URIUtil.getParentLocation(loc)); } }; diff --git a/src/org/rascalmpl/uri/vfs/IRemoteResolverRegistry.java b/src/org/rascalmpl/uri/vfs/IRemoteResolverRegistry.java index 5f0572c12a1..0c7de72406d 100644 --- a/src/org/rascalmpl/uri/vfs/IRemoteResolverRegistry.java +++ b/src/org/rascalmpl/uri/vfs/IRemoteResolverRegistry.java @@ -101,8 +101,7 @@ default CompletableFuture isWritable(ISourceLocation loc) { } @JsonRequest("rascal/vfs/output/writeFile") - //TODO (Rodin): terug naar loc, content, append; create+overwrite in TS oplossen - default CompletableFuture writeFile(ISourceLocation loc, String content, boolean append, boolean create, boolean overwrite) { + default CompletableFuture writeFile(ISourceLocation loc, String content, boolean append) { throw new UnsupportedOperationException(); } From f6eb9ab7e72288087cbc692db3686bbf83e4104d Mon Sep 17 00:00:00 2001 From: Rodin Aarssen Date: Wed, 11 Mar 2026 13:35:01 +0100 Subject: [PATCH 35/98] Added option to JsonValueWriter whether or not to encode file locations with their path only --- .../library/lang/json/internal/JsonValueWriter.java | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/org/rascalmpl/library/lang/json/internal/JsonValueWriter.java b/src/org/rascalmpl/library/lang/json/internal/JsonValueWriter.java index ad7e3eea891..3ec2e3d1fab 100644 --- a/src/org/rascalmpl/library/lang/json/internal/JsonValueWriter.java +++ b/src/org/rascalmpl/library/lang/json/internal/JsonValueWriter.java @@ -67,6 +67,7 @@ public class JsonValueWriter { private IFunction formatters; private boolean explicitConstructorNames = false; private boolean explicitDataTypes; + private boolean fileLocationsAsPathOnly = true; /** helper class for number serialization without quotes */ private static class RascalNumber extends Number { @@ -163,6 +164,11 @@ public JsonValueWriter setExplicitDataTypes(boolean setting) { return this; } + public JsonValueWriter setFileLocationsAsPathOnly(boolean setting) { + this.fileLocationsAsPathOnly = setting; + return this; + } + public void write(JsonWriter out, IValue value) throws IOException { value.accept(new IValueVisitor() { @@ -262,7 +268,7 @@ public Void visitSourceLocation(ISourceLocation o) throws IOException { } else { if (!o.hasOffsetLength()) { - if ("file".equals(o.getScheme())) { + if (fileLocationsAsPathOnly && "file".equals(o.getScheme())) { out.value(o.getPath()); } else { From 78de993da64f4ecb8076827b69b3cc474b941884 Mon Sep 17 00:00:00 2001 From: Rodin Aarssen Date: Wed, 11 Mar 2026 13:35:17 +0100 Subject: [PATCH 36/98] Using new JsonValueWriter option in GsonUtils --- src/org/rascalmpl/ideservices/GsonUtils.java | 1 + 1 file changed, 1 insertion(+) diff --git a/src/org/rascalmpl/ideservices/GsonUtils.java b/src/org/rascalmpl/ideservices/GsonUtils.java index 5946c97c3d2..93ae77b571b 100644 --- a/src/org/rascalmpl/ideservices/GsonUtils.java +++ b/src/org/rascalmpl/ideservices/GsonUtils.java @@ -84,6 +84,7 @@ public class GsonUtils { */ static { writer.setRationalsAsString(true); + writer.setFileLocationsAsPathOnly(false); typeMappings = List.of( new TypeMapping(IBool.class, tf.boolType()), new TypeMapping(ICollection.class), // IList, IMap, ISet From 4c87e06264a499d37b2de8a6da0f7da932ca7e71 Mon Sep 17 00:00:00 2001 From: Rodin Aarssen Date: Wed, 11 Mar 2026 13:39:42 +0100 Subject: [PATCH 37/98] Update src/org/rascalmpl/uri/remote/RemoteExternalResolverRegistry.java Co-authored-by: Toine Hartman --- src/org/rascalmpl/uri/remote/RemoteExternalResolverRegistry.java | 1 - 1 file changed, 1 deletion(-) diff --git a/src/org/rascalmpl/uri/remote/RemoteExternalResolverRegistry.java b/src/org/rascalmpl/uri/remote/RemoteExternalResolverRegistry.java index 3097bde3b8e..1a6e847a1c3 100644 --- a/src/org/rascalmpl/uri/remote/RemoteExternalResolverRegistry.java +++ b/src/org/rascalmpl/uri/remote/RemoteExternalResolverRegistry.java @@ -124,7 +124,6 @@ private static U call(Function> function, T argum if (cause instanceof ResponseErrorException) { throw translateException((ResponseErrorException) cause); } - throw new IOException(cause); } throw new IOException(e); } From e94041505a76188f8f429a81ea492e543e1df084 Mon Sep 17 00:00:00 2001 From: Rodin Aarssen Date: Fri, 13 Mar 2026 14:48:00 +0100 Subject: [PATCH 38/98] Forwarding watch id --- src/org/rascalmpl/uri/remote/IRascalFileSystemServices.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/org/rascalmpl/uri/remote/IRascalFileSystemServices.java b/src/org/rascalmpl/uri/remote/IRascalFileSystemServices.java index dacac038422..2981d6cf7a8 100644 --- a/src/org/rascalmpl/uri/remote/IRascalFileSystemServices.java +++ b/src/org/rascalmpl/uri/remote/IRascalFileSystemServices.java @@ -85,7 +85,7 @@ default public CompletableFuture watch(WatchRequest params) { URIResolverRegistry.getInstance().watch(loc, params.isRecursive(), changed -> { try { - onDidChangeFile(changed.getLocation(), changed.getChangeType().getValue(), null); + onDidChangeFile(changed.getLocation(), changed.getChangeType().getValue(), params.getWatcher()); } catch (/*IO*/Exception e) { throw new RuntimeException(e); } @@ -205,7 +205,7 @@ default public CompletableFuture rename(ISourceLocation from, ISourceLocat @Override default public void onDidChangeFile(ISourceLocation loc, int type, String watchId) { - // reg.watch(loc, false, null); + //TODO } public static class FileChangeEvent { From eb7e4de0cbf750d5b9ff0c3b9a1e6c645a2136f8 Mon Sep 17 00:00:00 2001 From: Rodin Aarssen Date: Fri, 13 Mar 2026 14:48:35 +0100 Subject: [PATCH 39/98] Removed public qualifier from interface field --- src/org/rascalmpl/uri/remote/IRascalFileSystemServices.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/org/rascalmpl/uri/remote/IRascalFileSystemServices.java b/src/org/rascalmpl/uri/remote/IRascalFileSystemServices.java index 2981d6cf7a8..09adef121dc 100644 --- a/src/org/rascalmpl/uri/remote/IRascalFileSystemServices.java +++ b/src/org/rascalmpl/uri/remote/IRascalFileSystemServices.java @@ -58,7 +58,7 @@ public interface IRascalFileSystemServices extends IRemoteResolverRegistry { static final URIResolverRegistry reg = URIResolverRegistry.getInstance(); - public static final ExecutorService executor = NamedThreadPool.cachedDaemon("rascal-vfs"); + static final ExecutorService executor = NamedThreadPool.cachedDaemon("rascal-vfs"); @Override default public CompletableFuture resolveLocation(ISourceLocation loc) { From e44cb59fa7c143b42f2b114a10f4862c406c18d8 Mon Sep 17 00:00:00 2001 From: Rodin Aarssen Date: Fri, 13 Mar 2026 17:02:58 +0100 Subject: [PATCH 40/98] RemoteExternalResolverRegistry now detects disconnections and automatically tries to reconnect to the remote --- .../RemoteExternalResolverRegistry.java | 148 +++++++++++++++++- 1 file changed, 141 insertions(+), 7 deletions(-) diff --git a/src/org/rascalmpl/uri/remote/RemoteExternalResolverRegistry.java b/src/org/rascalmpl/uri/remote/RemoteExternalResolverRegistry.java index 1a6e847a1c3..00b2a6e5631 100644 --- a/src/org/rascalmpl/uri/remote/RemoteExternalResolverRegistry.java +++ b/src/org/rascalmpl/uri/remote/RemoteExternalResolverRegistry.java @@ -33,6 +33,7 @@ import java.io.OutputStream; import java.net.InetAddress; import java.net.Socket; +import java.net.SocketException; import java.nio.file.AccessDeniedException; import java.nio.file.FileAlreadyExistsException; import java.nio.file.NoSuchFileException; @@ -79,16 +80,149 @@ import io.usethesource.vallang.ISourceLocation; public class RemoteExternalResolverRegistry implements IExternalResolverRegistry { - private final IRemoteResolverRegistry remote; + private volatile IRemoteResolverRegistry remote = null; private final Map watchers = new ConcurrentHashMap<>(); private final Map watchersById = new ConcurrentHashMap<>(); + private final int remoteResolverRegistryPort; + public RemoteExternalResolverRegistry(int remoteResolverRegistryPort) { - this.remote = startClient(remoteResolverRegistryPort); + this.remoteResolverRegistryPort = remoteResolverRegistryPort; + connect(); + } + + private static final Duration LONGEST_TIMEOUT = Duration.ofMinutes(1); + + private void connect() { + var timeout = Duration.ZERO; + while (true) { + try { + Thread.sleep(timeout.toMillis()); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + var remote = startClient(); + if (remote != null) { + this.remote = remote; + return; + } else { + timeout = timeout.plusMillis(10); + if (timeout.compareTo(LONGEST_TIMEOUT) >= 0) { + timeout = LONGEST_TIMEOUT; + } + } + } + } + + private void scheduleReconnect() { + CompletableFuture.runAsync(() -> connect()); } + + private InputStream errorDetectingInputStream(InputStream original) { + return new InputStream() { + @Override + public int read() throws IOException { + try { + return original.read(); + } catch (SocketException e) { + scheduleReconnect(); + throw e; + } + } - private IRemoteResolverRegistry startClient(int remoteResolverRegistryPort) { + @Override + public int read(byte[] b, int off, int len) throws IOException { + try { + return original.read(b, off, len); + } catch (SocketException e) { + scheduleReconnect(); + throw e; + } + } + + @Override + public int available() throws IOException { + return original.available(); + } + + @Override + public long skip(long n) throws IOException { + try { + return original.skip(n); + } catch (SocketException e) { + scheduleReconnect(); + throw e; + } + } + + @Override + public void close() throws IOException { + original.close(); + } + + @Override + public byte[] readNBytes(int len) throws IOException { + try { + return original.readNBytes(len); + } catch (SocketException e) { + scheduleReconnect(); + throw e; + } + } + + @Override + public int readNBytes(byte[] b, int off, int len) throws IOException { + try { + return original.readNBytes(b, off, len); + } catch (SocketException e) { + scheduleReconnect(); + throw e; + } + } + }; + } + + private OutputStream errorDetectingOutputStream(OutputStream original) { + return new OutputStream() { + @Override + public void write(int b) throws IOException { + try { + original.write(b); + } catch (SocketException e) { + scheduleReconnect(); + throw e; + } + } + + @Override + public void write(byte[] b, int off, int len) throws IOException { + try { + original.write(b, off, len); + } catch (SocketException e) { + scheduleReconnect(); + throw e; + } + } + + @Override + public void flush() throws IOException { + try { + original.flush(); + } catch (SocketException e) { + scheduleReconnect(); + throw e; + } + } + + @Override + public void close() throws IOException { + original.close(); + } + }; + } + + private IRemoteResolverRegistry startClient() { try { @SuppressWarnings("resource") var socket = new Socket(InetAddress.getLoopbackAddress(), remoteResolverRegistryPort); @@ -96,16 +230,16 @@ private IRemoteResolverRegistry startClient(int remoteResolverRegistryPort) { Launcher clientLauncher = new Launcher.Builder() .setRemoteInterface(IRemoteResolverRegistry.class) .setLocalService(this) - .setInput(socket.getInputStream()) - .setOutput(socket.getOutputStream()) + .setInput(errorDetectingInputStream(socket.getInputStream())) + .setOutput(errorDetectingOutputStream(socket.getOutputStream())) .configureGson(GsonUtils.complexAsJsonObject()) .setExecutorService(NamedThreadPool.cachedDaemon("rascal-remote-resolver-registry")) .create(); clientLauncher.startListening(); return clientLauncher.getRemoteProxy(); - } catch (Throwable e) { - System.err.println("Error setting up remote resolver registry connection: " + e.getMessage()); + } catch (RuntimeException | IOException e) { + System.err.println("Error setting up remote resolver registry connection, will reconnect: " + e.getMessage()); return null; } } From c7c087c8ed71f38d3f5c1b961a3005fe15663946 Mon Sep 17 00:00:00 2001 From: Rodin Aarssen Date: Wed, 18 Mar 2026 14:48:02 +0100 Subject: [PATCH 41/98] Removed VS Code-specific logic and exception handling from IRascalFileSystemServices --- .../uri/remote/IRascalFileSystemServices.java | 95 ++----------------- 1 file changed, 8 insertions(+), 87 deletions(-) diff --git a/src/org/rascalmpl/uri/remote/IRascalFileSystemServices.java b/src/org/rascalmpl/uri/remote/IRascalFileSystemServices.java index 09adef121dc..45ea9ed6840 100644 --- a/src/org/rascalmpl/uri/remote/IRascalFileSystemServices.java +++ b/src/org/rascalmpl/uri/remote/IRascalFileSystemServices.java @@ -26,13 +26,11 @@ */ package org.rascalmpl.uri.remote; -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; @@ -42,15 +40,11 @@ import org.apache.commons.codec.binary.Base64InputStream; import org.checkerframework.checker.nullness.qual.NonNull; -import org.checkerframework.checker.nullness.qual.Nullable; -import org.eclipse.lsp4j.jsonrpc.ResponseErrorException; -import org.eclipse.lsp4j.jsonrpc.messages.ResponseError; import org.rascalmpl.uri.FileAttributes; 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.uri.vfs.IRemoteResolverRegistry; import org.rascalmpl.util.NamedThreadPool; @@ -84,14 +78,10 @@ default public CompletableFuture watch(WatchRequest params) { ISourceLocation loc = params.getLocation(); URIResolverRegistry.getInstance().watch(loc, params.isRecursive(), changed -> { - try { - onDidChangeFile(changed.getLocation(), changed.getChangeType().getValue(), params.getWatcher()); - } catch (/*IO*/Exception e) { - throw new RuntimeException(e); - } + onDidChangeFile(changed.getLocation(), changed.getChangeType().getValue(), params.getWatcher()); }); } catch (IOException | RuntimeException e) { - throw new VSCodeFSError(e); + throw new CompletionException(e); } }, executor); } @@ -117,12 +107,9 @@ static FileChangeType convertFileChangeType(ISourceLocationChangeType changeType default public CompletableFuture stat(ISourceLocation loc) { return CompletableFuture.supplyAsync(() -> { try { - if (!reg.exists(loc)) { - throw new FileNotFoundException(); - } return reg.stat(loc); } catch (IOException | RuntimeException e) { - throw new VSCodeFSError(e); + throw new CompletionException(e); } }, executor); } @@ -132,12 +119,12 @@ default public CompletableFuture list(ISourceLocation loc) { return CompletableFuture.supplyAsync(() -> { try { if (!reg.isDirectory(loc)) { - throw VSCodeFSError.notADirectory(loc); + throw new NotDirectoryException(loc.toString()); } 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 | RuntimeException e) { - throw new VSCodeFSError(e); + throw new CompletionException(e); } }, executor); } @@ -148,7 +135,7 @@ default public CompletableFuture mkDirectory(ISourceLocation loc) { try { reg.mkDirectory(loc); } catch (IOException | RuntimeException e) { - throw new VSCodeFSError(e); + throw new CompletionException(e); } }, executor); } @@ -159,7 +146,7 @@ default public CompletableFuture readFile(ISourceLocation loc) { try (InputStream source = new Base64InputStream(reg.getInputStream(loc), true)) { return new String(source.readAllBytes(), StandardCharsets.US_ASCII); } catch (IOException | RuntimeException e) { - throw new VSCodeFSError(e); + throw new CompletionException(e); } }, executor); } @@ -168,15 +155,11 @@ default public CompletableFuture readFile(ISourceLocation loc) { default public CompletableFuture writeFile(ISourceLocation loc, String content, boolean append) { return CompletableFuture.runAsync(() -> { try { - if (reg.exists(loc) && reg.isDirectory(loc)) { - throw VSCodeFSError.isADirectory(loc); - } - try (OutputStream target = reg.getOutputStream(loc, false)) { target.write(Base64.getDecoder().decode(content)); } } catch (IOException | RuntimeException e) { - throw new VSCodeFSError(e); + throw new CompletionException(e); } }, executor); } @@ -240,66 +223,4 @@ public int getValue() { return value; } } - - /** Maps common exceptions to FileSystemError in VS Code */ - public static class VSCodeFSError extends ResponseErrorException { - //TODO (RA): zit ook in Rascal - 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); - } - } } From 26a6c047a5e0b78c98d3ed7bb731dd77d23fe8d8 Mon Sep 17 00:00:00 2001 From: Rodin Aarssen Date: Wed, 18 Mar 2026 14:59:04 +0100 Subject: [PATCH 42/98] Made field private --- src/org/rascalmpl/uri/URIResolverRegistry.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/org/rascalmpl/uri/URIResolverRegistry.java b/src/org/rascalmpl/uri/URIResolverRegistry.java index 30aa9b9a9f1..e5f9a24e1f3 100644 --- a/src/org/rascalmpl/uri/URIResolverRegistry.java +++ b/src/org/rascalmpl/uri/URIResolverRegistry.java @@ -66,7 +66,7 @@ public class URIResolverRegistry { private final Map> logicalResolvers = new ConcurrentHashMap<>(); private final Map classloaderResolvers = new ConcurrentHashMap<>(); - public volatile @Nullable IExternalResolverRegistry externalRegistry; + private volatile @Nullable IExternalResolverRegistry externalRegistry; private static class InstanceHolder { static URIResolverRegistry sInstance = new URIResolverRegistry(); From 5135a40e9b0a6ffa4fc32839f10533b1561caf2b Mon Sep 17 00:00:00 2001 From: Rodin Aarssen Date: Tue, 24 Mar 2026 09:42:21 +0100 Subject: [PATCH 43/98] Renamed IRemoteResolverRegistry to IRemoteResolverRegistryServer --- ...erRegistry.java => IRemoteResolverRegistryServer.java} | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) rename src/org/rascalmpl/uri/vfs/{IRemoteResolverRegistry.java => IRemoteResolverRegistryServer.java} (96%) diff --git a/src/org/rascalmpl/uri/vfs/IRemoteResolverRegistry.java b/src/org/rascalmpl/uri/vfs/IRemoteResolverRegistryServer.java similarity index 96% rename from src/org/rascalmpl/uri/vfs/IRemoteResolverRegistry.java rename to src/org/rascalmpl/uri/vfs/IRemoteResolverRegistryServer.java index 0c7de72406d..c27a7c41d8b 100644 --- a/src/org/rascalmpl/uri/vfs/IRemoteResolverRegistry.java +++ b/src/org/rascalmpl/uri/vfs/IRemoteResolverRegistryServer.java @@ -31,7 +31,6 @@ import java.util.concurrent.CompletableFuture; import org.checkerframework.checker.nullness.qual.Nullable; -import org.eclipse.lsp4j.jsonrpc.services.JsonNotification; import org.eclipse.lsp4j.jsonrpc.services.JsonRequest; import org.eclipse.lsp4j.jsonrpc.validation.NonNull; import org.rascalmpl.uri.FileAttributes; @@ -39,7 +38,7 @@ import io.usethesource.vallang.ISourceLocation; -public interface IRemoteResolverRegistry { +public interface IRemoteResolverRegistryServer { @JsonRequest("rascal/vfs/input/readFile") default CompletableFuture readFile(ISourceLocation loc) { throw new UnsupportedOperationException(); @@ -141,11 +140,6 @@ default CompletableFuture resolveLocation(ISourceLocation loc) throw new UnsupportedOperationException(); } - @JsonNotification("rascal/vfs/watcher/fileChanged") - default void onDidChangeFile(ISourceLocation loc, int type, String watchId) { - throw new UnsupportedOperationException(); - } - public static class WatchRequest { @NonNull private ISourceLocation loc; @NonNull private String watcher; From 2b67b3676fa47ade02aad5e5fd8ecf0885cbf903 Mon Sep 17 00:00:00 2001 From: Rodin Aarssen Date: Tue, 24 Mar 2026 09:42:38 +0100 Subject: [PATCH 44/98] Added IRemoteResolverRegistryClient interface --- .../uri/vfs/IRemoteResolverRegistryClient.java | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 src/org/rascalmpl/uri/vfs/IRemoteResolverRegistryClient.java diff --git a/src/org/rascalmpl/uri/vfs/IRemoteResolverRegistryClient.java b/src/org/rascalmpl/uri/vfs/IRemoteResolverRegistryClient.java new file mode 100644 index 00000000000..ea5e1181400 --- /dev/null +++ b/src/org/rascalmpl/uri/vfs/IRemoteResolverRegistryClient.java @@ -0,0 +1,11 @@ +package org.rascalmpl.uri.vfs; + +import org.eclipse.lsp4j.jsonrpc.services.JsonNotification; +import org.rascalmpl.uri.ISourceLocationWatcher.ISourceLocationChangeType; + +import io.usethesource.vallang.ISourceLocation; + +public interface IRemoteResolverRegistryClient { + @JsonNotification("rascal/vfs/watcher/sourceLocationChanged") + void sourceLocationChanged(ISourceLocation root, ISourceLocationChangeType type, String watchId); +} From dc272280e371c22f8dfd9b82e7e0841392af49b1 Mon Sep 17 00:00:00 2001 From: Rodin Aarssen Date: Tue, 24 Mar 2026 09:45:04 +0100 Subject: [PATCH 45/98] Using new IRemoveResolverServer and -Client interfaces --- .../uri/remote/IRascalFileSystemServices.java | 32 ++++++++++++------- .../RemoteExternalResolverRegistry.java | 25 ++++++++------- 2 files changed, 35 insertions(+), 22 deletions(-) diff --git a/src/org/rascalmpl/uri/remote/IRascalFileSystemServices.java b/src/org/rascalmpl/uri/remote/IRascalFileSystemServices.java index 45ea9ed6840..5c7574fbff3 100644 --- a/src/org/rascalmpl/uri/remote/IRascalFileSystemServices.java +++ b/src/org/rascalmpl/uri/remote/IRascalFileSystemServices.java @@ -39,23 +39,33 @@ import java.util.concurrent.ExecutorService; import org.apache.commons.codec.binary.Base64InputStream; +import org.checkerframework.checker.nullness.qual.EnsuresNonNull; +import org.checkerframework.checker.nullness.qual.MonotonicNonNull; import org.checkerframework.checker.nullness.qual.NonNull; import org.rascalmpl.uri.FileAttributes; 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.vfs.IRemoteResolverRegistry; +import org.rascalmpl.uri.vfs.IRemoteResolverRegistryClient; +import org.rascalmpl.uri.vfs.IRemoteResolverRegistryServer; import org.rascalmpl.util.NamedThreadPool; import io.usethesource.vallang.ISourceLocation; -public interface IRascalFileSystemServices extends IRemoteResolverRegistry { +public class IRascalFileSystemServices implements IRemoteResolverRegistryServer { static final URIResolverRegistry reg = URIResolverRegistry.getInstance(); static final ExecutorService executor = NamedThreadPool.cachedDaemon("rascal-vfs"); + private volatile @MonotonicNonNull IRemoteResolverRegistryClient client = null; + + @EnsuresNonNull("this.client") + protected void provideClient(IRemoteResolverRegistryClient client) { + this.client = client; + } + @Override - default public CompletableFuture resolveLocation(ISourceLocation loc) { + public CompletableFuture resolveLocation(ISourceLocation loc) { return CompletableFuture.supplyAsync(() -> { try { ISourceLocation resolved = reg.logicalToPhysical(loc); @@ -72,7 +82,7 @@ default public CompletableFuture resolveLocation(ISourceLocatio } @Override - default public CompletableFuture watch(WatchRequest params) { + public CompletableFuture watch(WatchRequest params) { return CompletableFuture.runAsync(() -> { try { ISourceLocation loc = params.getLocation(); @@ -104,7 +114,7 @@ static FileChangeType convertFileChangeType(ISourceLocationChangeType changeType } @Override - default public CompletableFuture stat(ISourceLocation loc) { + public CompletableFuture stat(ISourceLocation loc) { return CompletableFuture.supplyAsync(() -> { try { return reg.stat(loc); @@ -115,7 +125,7 @@ default public CompletableFuture stat(ISourceLocation loc) { } @Override - default public CompletableFuture list(ISourceLocation loc) { + public CompletableFuture list(ISourceLocation loc) { return CompletableFuture.supplyAsync(() -> { try { if (!reg.isDirectory(loc)) { @@ -130,7 +140,7 @@ default public CompletableFuture list(ISourceLocation loc) { } @Override - default public CompletableFuture mkDirectory(ISourceLocation loc) { + public CompletableFuture mkDirectory(ISourceLocation loc) { return CompletableFuture.runAsync(() -> { try { reg.mkDirectory(loc); @@ -141,7 +151,7 @@ default public CompletableFuture mkDirectory(ISourceLocation loc) { } @Override - default public CompletableFuture readFile(ISourceLocation loc) { + public CompletableFuture readFile(ISourceLocation loc) { return CompletableFuture.supplyAsync(() -> { try (InputStream source = new Base64InputStream(reg.getInputStream(loc), true)) { return new String(source.readAllBytes(), StandardCharsets.US_ASCII); @@ -152,7 +162,7 @@ default public CompletableFuture readFile(ISourceLocation loc) { } @Override - default public CompletableFuture writeFile(ISourceLocation loc, String content, boolean append) { + public CompletableFuture writeFile(ISourceLocation loc, String content, boolean append) { return CompletableFuture.runAsync(() -> { try { try (OutputStream target = reg.getOutputStream(loc, false)) { @@ -165,7 +175,7 @@ default public CompletableFuture writeFile(ISourceLocation loc, String con } @Override - default public CompletableFuture remove(ISourceLocation loc, boolean recursive) { + public CompletableFuture remove(ISourceLocation loc, boolean recursive) { return CompletableFuture.runAsync(() -> { try { reg.remove(loc, recursive); @@ -176,7 +186,7 @@ default public CompletableFuture remove(ISourceLocation loc, boolean recur } @Override - default public CompletableFuture rename(ISourceLocation from, ISourceLocation to, boolean overwrite) { + public CompletableFuture rename(ISourceLocation from, ISourceLocation to, boolean overwrite) { return CompletableFuture.runAsync(() -> { try { reg.rename(from, to, overwrite); diff --git a/src/org/rascalmpl/uri/remote/RemoteExternalResolverRegistry.java b/src/org/rascalmpl/uri/remote/RemoteExternalResolverRegistry.java index 00b2a6e5631..afcf0e584b2 100644 --- a/src/org/rascalmpl/uri/remote/RemoteExternalResolverRegistry.java +++ b/src/org/rascalmpl/uri/remote/RemoteExternalResolverRegistry.java @@ -59,16 +59,18 @@ import org.checkerframework.checker.nullness.qual.Nullable; import org.eclipse.lsp4j.jsonrpc.Launcher; import org.eclipse.lsp4j.jsonrpc.ResponseErrorException; -import org.eclipse.lsp4j.jsonrpc.services.JsonNotification; +import org.eclipse.lsp4j.jsonrpc.messages.ResponseError; +import org.eclipse.lsp4j.jsonrpc.messages.ResponseErrorCode; import org.rascalmpl.ideservices.GsonUtils; import org.rascalmpl.uri.FileAttributes; import org.rascalmpl.uri.IExternalResolverRegistry; import org.rascalmpl.uri.ISourceLocationWatcher; import org.rascalmpl.uri.URIUtil; -import org.rascalmpl.uri.vfs.IRemoteResolverRegistry; -import org.rascalmpl.uri.vfs.IRemoteResolverRegistry.FileType; -import org.rascalmpl.uri.vfs.IRemoteResolverRegistry.FileWithType; -import org.rascalmpl.uri.vfs.IRemoteResolverRegistry.WatchRequest; +import org.rascalmpl.uri.vfs.IRemoteResolverRegistryClient; +import org.rascalmpl.uri.vfs.IRemoteResolverRegistryServer; +import org.rascalmpl.uri.vfs.IRemoteResolverRegistryServer.FileType; +import org.rascalmpl.uri.vfs.IRemoteResolverRegistryServer.FileWithType; +import org.rascalmpl.uri.vfs.IRemoteResolverRegistryServer.WatchRequest; import org.rascalmpl.util.Lazy; import org.rascalmpl.util.NamedThreadPool; import org.rascalmpl.util.base64.StreamingBase64; @@ -79,8 +81,8 @@ import io.usethesource.vallang.ISourceLocation; -public class RemoteExternalResolverRegistry implements IExternalResolverRegistry { - private volatile IRemoteResolverRegistry remote = null; +public class RemoteExternalResolverRegistry implements IExternalResolverRegistry, IRemoteResolverRegistryClient { + private volatile IRemoteResolverRegistryServer remote = null; private final Map watchers = new ConcurrentHashMap<>(); private final Map watchersById = new ConcurrentHashMap<>(); @@ -222,13 +224,13 @@ public void close() throws IOException { }; } - private IRemoteResolverRegistry startClient() { + private IRemoteResolverRegistryServer startClient() { try { @SuppressWarnings("resource") var socket = new Socket(InetAddress.getLoopbackAddress(), remoteResolverRegistryPort); socket.setTcpNoDelay(true); - Launcher clientLauncher = new Launcher.Builder() - .setRemoteInterface(IRemoteResolverRegistry.class) + Launcher clientLauncher = new Launcher.Builder() + .setRemoteInterface(IRemoteResolverRegistryServer.class) .setLocalService(this) .setInput(errorDetectingInputStream(socket.getInputStream())) .setOutput(errorDetectingOutputStream(socket.getOutputStream())) @@ -440,7 +442,8 @@ public boolean isWritable(ISourceLocation loc) throws IOException { @Override public ISourceLocation resolve(ISourceLocation input) throws IOException { - return call(remote::resolveLocation, input); + var resolved = call(remote::resolveLocation, input); + return resolved; } @Override From e9aeeac64dc36499a57c9bdd7aadfa65ca5e2e32 Mon Sep 17 00:00:00 2001 From: Rodin Aarssen Date: Tue, 24 Mar 2026 09:45:48 +0100 Subject: [PATCH 46/98] Removed FileChangeType enum in favor of ISourceLocationChanged interface and finished remote watch/unwatch callback implementations --- .../rascalmpl/uri/ISourceLocationWatcher.java | 6 +- .../uri/remote/IRascalFileSystemServices.java | 43 ++----------- .../RemoteExternalResolverRegistry.java | 61 +++++++++---------- 3 files changed, 38 insertions(+), 72 deletions(-) diff --git a/src/org/rascalmpl/uri/ISourceLocationWatcher.java b/src/org/rascalmpl/uri/ISourceLocationWatcher.java index 1139d97f462..36123b4b7d5 100644 --- a/src/org/rascalmpl/uri/ISourceLocationWatcher.java +++ b/src/org/rascalmpl/uri/ISourceLocationWatcher.java @@ -62,9 +62,9 @@ default boolean isChanged() { } public enum ISourceLocationChangeType { - CREATED(1), - DELETED(2), - MODIFIED(3); + CREATED(2), + DELETED(3), + MODIFIED(1); int value; diff --git a/src/org/rascalmpl/uri/remote/IRascalFileSystemServices.java b/src/org/rascalmpl/uri/remote/IRascalFileSystemServices.java index 5c7574fbff3..a1a87ae354e 100644 --- a/src/org/rascalmpl/uri/remote/IRascalFileSystemServices.java +++ b/src/org/rascalmpl/uri/remote/IRascalFileSystemServices.java @@ -88,7 +88,7 @@ public CompletableFuture watch(WatchRequest params) { ISourceLocation loc = params.getLocation(); URIResolverRegistry.getInstance().watch(loc, params.isRecursive(), changed -> { - onDidChangeFile(changed.getLocation(), changed.getChangeType().getValue(), params.getWatcher()); + client.sourceLocationChanged(changed.getLocation(), changed.getChangeType(), ""); }); } catch (IOException | RuntimeException e) { throw new CompletionException(e); @@ -97,20 +97,7 @@ public CompletableFuture watch(WatchRequest params) { } static FileChangeEvent convertChangeEvent(ISourceLocationChanged changed) throws IOException { - return new FileChangeEvent(convertFileChangeType(changed.getChangeType()), changed.getLocation().getURI().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); - } + return new FileChangeEvent(changed.getChangeType(), changed.getLocation().getURI().toASCIIString()); } @Override @@ -196,21 +183,16 @@ public CompletableFuture rename(ISourceLocation from, ISourceLocation to, }, executor); } - @Override - default public void onDidChangeFile(ISourceLocation loc, int type, String watchId) { - //TODO - } - public static class FileChangeEvent { - @NonNull private final FileChangeType type; + @NonNull private final ISourceLocationChangeType type; @NonNull private final String uri; - public FileChangeEvent(FileChangeType type, @NonNull String uri) { + public FileChangeEvent(ISourceLocationChangeType type, @NonNull String uri) { this.type = type; this.uri = uri; } - public FileChangeType getType() { + public ISourceLocationChangeType getType() { return type; } @@ -218,19 +200,4 @@ public ISourceLocation getLocation() throws URISyntaxException { return null; } } - - 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; - } - } } diff --git a/src/org/rascalmpl/uri/remote/RemoteExternalResolverRegistry.java b/src/org/rascalmpl/uri/remote/RemoteExternalResolverRegistry.java index afcf0e584b2..c0371f7c7e1 100644 --- a/src/org/rascalmpl/uri/remote/RemoteExternalResolverRegistry.java +++ b/src/org/rascalmpl/uri/remote/RemoteExternalResolverRegistry.java @@ -469,18 +469,20 @@ public void watch(ISourceLocation root, Consumer watcher @Override public void unwatch(ISourceLocation root, Consumer watcher, boolean recursive) throws IOException { var watchKey = new WatchSubscriptionKey(root, recursive); - var watch = watchers.get(watchKey); - if (watch != null && watch.removeWatcher(watcher)) { - System.err.println("No other watchers registered, so unregistering at server"); - watchers.remove(watchKey); - if (!watch.getCallbacks().isEmpty()) { - System.err.println("Raced by another thread, canceling unregister"); - watchers.put(watchKey, watch); - return; + synchronized (watchers) { + var watch = watchers.get(watchKey); + if (watch != null && watch.removeWatcher(watcher)) { + System.err.println("No other watchers registered, so unregistering at server"); + watchers.remove(watchKey); + if (!watch.getCallbacks().isEmpty()) { + System.err.println("Raced by another thread, canceling unregister"); + watchers.put(watchKey, watch); + return; + } + watchersById.remove(watch.getId()); + call(remote::unwatch, new WatchRequest(root, recursive, watch.getId())); } - watchersById.remove(watch.getId()); } - call(remote::unwatch, new WatchRequest(root, recursive, watch.getId())); } @Override @@ -492,27 +494,24 @@ public boolean supportsRecursiveWatch() { } } - private static final int FileChangeType_Changed = 1; - private static final int FileChangeType_Created = 2; - private static final int FileChangeType_Deleted = 3; - - @JsonNotification("rascal/vfs/watcher/fileChanged") - public void emitWatch(ISourceLocation root, int type, String watchId) throws IOException { - synchronized (watchers) { - var watcher = watchersById.get(watchId); - switch (type) { - case FileChangeType_Changed: - watcher.publish(ISourceLocationWatcher.modified(root)); - break; - case FileChangeType_Created: - watcher.publish(ISourceLocationWatcher.created(root)); - break; - case FileChangeType_Deleted: - watcher.publish(ISourceLocationWatcher.deleted(root)); - break; - default: - throw new IOException("Unexpected FileChangeType " + type); - } + @Override + public void sourceLocationChanged(ISourceLocation root, ISourceLocationChangeType type, String watchId) { + var watcher = watchersById.get(watchId); + if (watcher == null) { + throw new ResponseErrorException(new ResponseError(ResponseErrorCode.RequestFailed, "Received notification for unregistered watch", root)); + } + switch (type) { + case MODIFIED: + watcher.publish(ISourceLocationWatcher.modified(root)); + break; + case CREATED: + watcher.publish(ISourceLocationWatcher.created(root)); + break; + case DELETED: + watcher.publish(ISourceLocationWatcher.deleted(root)); + break; + default: + throw new ResponseErrorException(new ResponseError(ResponseErrorCode.InvalidParams, "Unexpected FileChangeType " + type, root)); } } From 0c44a28f7c46c7089b8046232ac803b41088e73b Mon Sep 17 00:00:00 2001 From: Rodin Aarssen Date: Tue, 24 Mar 2026 09:48:43 +0100 Subject: [PATCH 47/98] Renamed IRascalFileSystemServices to RascalFileSystemServices as this now a class and no longer an interface --- ...calFileSystemServices.java => RascalFileSystemServices.java} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename src/org/rascalmpl/uri/remote/{IRascalFileSystemServices.java => RascalFileSystemServices.java} (98%) diff --git a/src/org/rascalmpl/uri/remote/IRascalFileSystemServices.java b/src/org/rascalmpl/uri/remote/RascalFileSystemServices.java similarity index 98% rename from src/org/rascalmpl/uri/remote/IRascalFileSystemServices.java rename to src/org/rascalmpl/uri/remote/RascalFileSystemServices.java index a1a87ae354e..a4b3f262201 100644 --- a/src/org/rascalmpl/uri/remote/IRascalFileSystemServices.java +++ b/src/org/rascalmpl/uri/remote/RascalFileSystemServices.java @@ -53,7 +53,7 @@ import io.usethesource.vallang.ISourceLocation; -public class IRascalFileSystemServices implements IRemoteResolverRegistryServer { +public class RascalFileSystemServices implements IRemoteResolverRegistryServer { static final URIResolverRegistry reg = URIResolverRegistry.getInstance(); static final ExecutorService executor = NamedThreadPool.cachedDaemon("rascal-vfs"); From 3303ff5a674370668f0dbdfaea10260dc9637736 Mon Sep 17 00:00:00 2001 From: Rodin Aarssen Date: Tue, 24 Mar 2026 14:59:52 +0100 Subject: [PATCH 48/98] Registration of remote resolver is now publicly available such that RemoteExternalResolverRegistry can be subclassed by GUI implementations --- .../rascalmpl/uri/URIResolverRegistry.java | 35 ++++++++++++++----- 1 file changed, 26 insertions(+), 9 deletions(-) diff --git a/src/org/rascalmpl/uri/URIResolverRegistry.java b/src/org/rascalmpl/uri/URIResolverRegistry.java index e5f9a24e1f3..5119ab91175 100644 --- a/src/org/rascalmpl/uri/URIResolverRegistry.java +++ b/src/org/rascalmpl/uri/URIResolverRegistry.java @@ -117,18 +117,35 @@ private void loadServices() { throw new Error("WARNING: Could not load URIResolverRegistry extensions from " + RESOLVERS_CONFIG, e); } - var remoteResolverRegistryPortProperty = System.getProperty("rascal.remoteResolverRegistryPort"); - if (remoteResolverRegistryPortProperty != null) { - try { - var remoteResolverRegistryPort = Integer.parseInt(remoteResolverRegistryPortProperty); - this.externalRegistry = new RemoteExternalResolverRegistry(remoteResolverRegistryPort); - watchers.setExternalRegistry(this.externalRegistry); - } catch (NumberFormatException e) { - System.err.println("WARNING: Invalid remoteResolverRegistryPort environment variable: " + remoteResolverRegistryPortProperty + " is not parseable as integer"); - } + var remoteResolverRegistryPort = getRemoteResolverRegistryPort(); + if (remoteResolverRegistryPort != null) { + synchronized (this.externalRegistry) { + if (this.externalRegistry == null) { + registerRemoteResolverRegistry(new RemoteExternalResolverRegistry(remoteResolverRegistryPort)); + } + } } } + public static Integer getRemoteResolverRegistryPort() { + var remoteResolverRegistryPortProperty = System.getProperty("rascal.remoteResolverRegistryPort"); + if (remoteResolverRegistryPortProperty != null) { + try { + return Integer.parseInt(remoteResolverRegistryPortProperty); + } catch (NumberFormatException e) { + System.err.println("WARNING: Invalid remoteResolverRegistryPort environment variable: " + remoteResolverRegistryPortProperty + " is not parseable as integer"); + } + } + return null; + } + + public synchronized void registerRemoteResolverRegistry(RemoteExternalResolverRegistry registry) { + synchronized (this.externalRegistry) { + this.externalRegistry = registry; + watchers.setExternalRegistry(registry); + } + } + public Set getRegisteredInputSchemes() { return Collections.unmodifiableSet(inputResolvers.keySet()); } From 6530f8ac2d244ec6686c604ca7e48a5599c30cef Mon Sep 17 00:00:00 2001 From: Rodin Aarssen Date: Wed, 25 Mar 2026 15:26:49 +0100 Subject: [PATCH 49/98] Fixed NPE; no synchronizing on nullable objects --- src/org/rascalmpl/uri/URIResolverRegistry.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/org/rascalmpl/uri/URIResolverRegistry.java b/src/org/rascalmpl/uri/URIResolverRegistry.java index 5119ab91175..8b2f6bbadd6 100644 --- a/src/org/rascalmpl/uri/URIResolverRegistry.java +++ b/src/org/rascalmpl/uri/URIResolverRegistry.java @@ -119,7 +119,7 @@ private void loadServices() { var remoteResolverRegistryPort = getRemoteResolverRegistryPort(); if (remoteResolverRegistryPort != null) { - synchronized (this.externalRegistry) { + synchronized (this) { if (this.externalRegistry == null) { registerRemoteResolverRegistry(new RemoteExternalResolverRegistry(remoteResolverRegistryPort)); } @@ -140,7 +140,7 @@ public static Integer getRemoteResolverRegistryPort() { } public synchronized void registerRemoteResolverRegistry(RemoteExternalResolverRegistry registry) { - synchronized (this.externalRegistry) { + synchronized (this) { this.externalRegistry = registry; watchers.setExternalRegistry(registry); } From 1ea9070d322c284d9e8fd6934e97348d2ad467b2 Mon Sep 17 00:00:00 2001 From: Rodin Aarssen Date: Wed, 25 Mar 2026 15:51:58 +0100 Subject: [PATCH 50/98] Changed interface to have a plain integer instead of an enum --- src/org/rascalmpl/uri/remote/RascalFileSystemServices.java | 2 +- src/org/rascalmpl/uri/vfs/IRemoteResolverRegistryClient.java | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/org/rascalmpl/uri/remote/RascalFileSystemServices.java b/src/org/rascalmpl/uri/remote/RascalFileSystemServices.java index a4b3f262201..0bca947d917 100644 --- a/src/org/rascalmpl/uri/remote/RascalFileSystemServices.java +++ b/src/org/rascalmpl/uri/remote/RascalFileSystemServices.java @@ -88,7 +88,7 @@ public CompletableFuture watch(WatchRequest params) { ISourceLocation loc = params.getLocation(); URIResolverRegistry.getInstance().watch(loc, params.isRecursive(), changed -> { - client.sourceLocationChanged(changed.getLocation(), changed.getChangeType(), ""); + client.sourceLocationChanged(changed.getLocation(), changed.getChangeType().getValue(), ""); }); } catch (IOException | RuntimeException e) { throw new CompletionException(e); diff --git a/src/org/rascalmpl/uri/vfs/IRemoteResolverRegistryClient.java b/src/org/rascalmpl/uri/vfs/IRemoteResolverRegistryClient.java index ea5e1181400..6b479a43da7 100644 --- a/src/org/rascalmpl/uri/vfs/IRemoteResolverRegistryClient.java +++ b/src/org/rascalmpl/uri/vfs/IRemoteResolverRegistryClient.java @@ -1,11 +1,10 @@ package org.rascalmpl.uri.vfs; import org.eclipse.lsp4j.jsonrpc.services.JsonNotification; -import org.rascalmpl.uri.ISourceLocationWatcher.ISourceLocationChangeType; import io.usethesource.vallang.ISourceLocation; public interface IRemoteResolverRegistryClient { @JsonNotification("rascal/vfs/watcher/sourceLocationChanged") - void sourceLocationChanged(ISourceLocation root, ISourceLocationChangeType type, String watchId); + void sourceLocationChanged(ISourceLocation root, int type, String watchId); } From e1d4dfc3c6827e91a3b406e8978662f981e54de2 Mon Sep 17 00:00:00 2001 From: Rodin Aarssen Date: Wed, 25 Mar 2026 15:52:27 +0100 Subject: [PATCH 51/98] Fixed watch callback argument --- src/org/rascalmpl/uri/remote/RascalFileSystemServices.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/org/rascalmpl/uri/remote/RascalFileSystemServices.java b/src/org/rascalmpl/uri/remote/RascalFileSystemServices.java index 0bca947d917..dc9d7f6fe85 100644 --- a/src/org/rascalmpl/uri/remote/RascalFileSystemServices.java +++ b/src/org/rascalmpl/uri/remote/RascalFileSystemServices.java @@ -88,7 +88,7 @@ public CompletableFuture watch(WatchRequest params) { ISourceLocation loc = params.getLocation(); URIResolverRegistry.getInstance().watch(loc, params.isRecursive(), changed -> { - client.sourceLocationChanged(changed.getLocation(), changed.getChangeType().getValue(), ""); + client.sourceLocationChanged(changed.getLocation(), changed.getChangeType().getValue(), params.getWatcher()); }); } catch (IOException | RuntimeException e) { throw new CompletionException(e); From abd938593b0347e55665030c3aeb6ee67b11d677 Mon Sep 17 00:00:00 2001 From: Rodin Aarssen Date: Wed, 25 Mar 2026 15:53:04 +0100 Subject: [PATCH 52/98] Removed FileChangeEvent class in favor of ISourceLocationChanged --- .../rascalmpl/uri/ISourceLocationWatcher.java | 11 +++++++- .../uri/remote/RascalFileSystemServices.java | 26 ----------------- .../RemoteExternalResolverRegistry.java | 28 ++++++++++--------- 3 files changed, 25 insertions(+), 40 deletions(-) diff --git a/src/org/rascalmpl/uri/ISourceLocationWatcher.java b/src/org/rascalmpl/uri/ISourceLocationWatcher.java index 36123b4b7d5..a7db15f66c7 100644 --- a/src/org/rascalmpl/uri/ISourceLocationWatcher.java +++ b/src/org/rascalmpl/uri/ISourceLocationWatcher.java @@ -66,7 +66,7 @@ public enum ISourceLocationChangeType { DELETED(3), MODIFIED(1); - int value; + private final int value; public int getValue() { return value; @@ -75,6 +75,15 @@ public int getValue() { ISourceLocationChangeType(int value) { this.value = value; } + + public static ISourceLocationChangeType fromValue(int value) { + switch (value) { + case 2: return CREATED; + case 3: return DELETED; + case 1: return MODIFIED; + default: throw new IllegalArgumentException("Unknown ISourceLocationChangeType value " + value); + } + } } static ISourceLocationChanged created(ISourceLocation loc) { diff --git a/src/org/rascalmpl/uri/remote/RascalFileSystemServices.java b/src/org/rascalmpl/uri/remote/RascalFileSystemServices.java index dc9d7f6fe85..d26c9381d73 100644 --- a/src/org/rascalmpl/uri/remote/RascalFileSystemServices.java +++ b/src/org/rascalmpl/uri/remote/RascalFileSystemServices.java @@ -29,7 +29,6 @@ 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.NotDirectoryException; import java.util.Arrays; @@ -41,10 +40,7 @@ import org.apache.commons.codec.binary.Base64InputStream; import org.checkerframework.checker.nullness.qual.EnsuresNonNull; import org.checkerframework.checker.nullness.qual.MonotonicNonNull; -import org.checkerframework.checker.nullness.qual.NonNull; import org.rascalmpl.uri.FileAttributes; -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.vfs.IRemoteResolverRegistryClient; @@ -96,10 +92,6 @@ public CompletableFuture watch(WatchRequest params) { }, executor); } - static FileChangeEvent convertChangeEvent(ISourceLocationChanged changed) throws IOException { - return new FileChangeEvent(changed.getChangeType(), changed.getLocation().getURI().toASCIIString()); - } - @Override public CompletableFuture stat(ISourceLocation loc) { return CompletableFuture.supplyAsync(() -> { @@ -182,22 +174,4 @@ public CompletableFuture rename(ISourceLocation from, ISourceLocation to, } }, executor); } - - public static class FileChangeEvent { - @NonNull private final ISourceLocationChangeType type; - @NonNull private final String uri; - - public FileChangeEvent(ISourceLocationChangeType type, @NonNull String uri) { - this.type = type; - this.uri = uri; - } - - public ISourceLocationChangeType getType() { - return type; - } - - public ISourceLocation getLocation() throws URISyntaxException { - return null; - } - } } diff --git a/src/org/rascalmpl/uri/remote/RemoteExternalResolverRegistry.java b/src/org/rascalmpl/uri/remote/RemoteExternalResolverRegistry.java index c0371f7c7e1..306cbc19f23 100644 --- a/src/org/rascalmpl/uri/remote/RemoteExternalResolverRegistry.java +++ b/src/org/rascalmpl/uri/remote/RemoteExternalResolverRegistry.java @@ -495,23 +495,25 @@ public boolean supportsRecursiveWatch() { } @Override - public void sourceLocationChanged(ISourceLocation root, ISourceLocationChangeType type, String watchId) { + public void sourceLocationChanged(ISourceLocation root, int type, String watchId) { var watcher = watchersById.get(watchId); if (watcher == null) { throw new ResponseErrorException(new ResponseError(ResponseErrorCode.RequestFailed, "Received notification for unregistered watch", root)); } - switch (type) { - case MODIFIED: - watcher.publish(ISourceLocationWatcher.modified(root)); - break; - case CREATED: - watcher.publish(ISourceLocationWatcher.created(root)); - break; - case DELETED: - watcher.publish(ISourceLocationWatcher.deleted(root)); - break; - default: - throw new ResponseErrorException(new ResponseError(ResponseErrorCode.InvalidParams, "Unexpected FileChangeType " + type, root)); + try { + switch (ISourceLocationChangeType.fromValue(type)) { + case CREATED: + watcher.publish(ISourceLocationWatcher.created(root)); + break; + case DELETED: + watcher.publish(ISourceLocationWatcher.deleted(root)); + break; + case MODIFIED: + watcher.publish(ISourceLocationWatcher.modified(root)); + break; + } + } catch (IllegalArgumentException e) { + throw new ResponseErrorException(new ResponseError(ResponseErrorCode.InvalidParams, "Unexpected FileChangeType " + type, root)); } } From 466cf1d25637ffd0add0d2d9593e32b313b2d319 Mon Sep 17 00:00:00 2001 From: Rodin Aarssen Date: Thu, 26 Mar 2026 12:36:03 +0100 Subject: [PATCH 53/98] Removed comment --- src/org/rascalmpl/uri/vfs/IRemoteResolverRegistryServer.java | 1 - 1 file changed, 1 deletion(-) diff --git a/src/org/rascalmpl/uri/vfs/IRemoteResolverRegistryServer.java b/src/org/rascalmpl/uri/vfs/IRemoteResolverRegistryServer.java index c27a7c41d8b..61d216abea7 100644 --- a/src/org/rascalmpl/uri/vfs/IRemoteResolverRegistryServer.java +++ b/src/org/rascalmpl/uri/vfs/IRemoteResolverRegistryServer.java @@ -110,7 +110,6 @@ default CompletableFuture mkDirectory(ISourceLocation loc) { } @JsonRequest("rascal/vfs/output/remove") - //TODO (Rodin): ISourceLocationOutput heeft geen `recursive`, URIResolverRegistry wel default CompletableFuture remove(ISourceLocation loc, boolean recursive) { throw new UnsupportedOperationException(); } From 388ac3fbffab5380ee2359bbb267298d0b1c4f06 Mon Sep 17 00:00:00 2001 From: Rodin Aarssen Date: Fri, 27 Mar 2026 14:58:21 +0100 Subject: [PATCH 54/98] Added jsonrpc request and notification classes for modularity and changed the signature and implementation to use them --- .../uri/remote/RascalFileSystemServices.java | 43 ++++--- .../RemoteExternalResolverRegistry.java | 48 ++++---- .../jsonrpc/ISourceLocationChangeType.java | 69 +++++++++++ .../jsonrpc/ISourceLocationChanged.java | 87 ++++++++++++++ .../jsonrpc/ISourceLocationRequest.java | 59 ++++++++++ .../uri/remote/jsonrpc/RemoveRequest.java | 61 ++++++++++ .../uri/remote/jsonrpc/RenameRequest.java | 78 +++++++++++++ .../jsonrpc/SetLastModifiedRequest.java | 60 ++++++++++ .../uri/remote/jsonrpc/WatchRequest.java | 71 ++++++++++++ .../uri/remote/jsonrpc/WriteFileRequest.java | 71 ++++++++++++ .../vfs/IRemoteResolverRegistryClient.java | 5 +- .../vfs/IRemoteResolverRegistryServer.java | 107 ++++-------------- 12 files changed, 634 insertions(+), 125 deletions(-) create mode 100644 src/org/rascalmpl/uri/remote/jsonrpc/ISourceLocationChangeType.java create mode 100644 src/org/rascalmpl/uri/remote/jsonrpc/ISourceLocationChanged.java create mode 100644 src/org/rascalmpl/uri/remote/jsonrpc/ISourceLocationRequest.java create mode 100644 src/org/rascalmpl/uri/remote/jsonrpc/RemoveRequest.java create mode 100644 src/org/rascalmpl/uri/remote/jsonrpc/RenameRequest.java create mode 100644 src/org/rascalmpl/uri/remote/jsonrpc/SetLastModifiedRequest.java create mode 100644 src/org/rascalmpl/uri/remote/jsonrpc/WatchRequest.java create mode 100644 src/org/rascalmpl/uri/remote/jsonrpc/WriteFileRequest.java diff --git a/src/org/rascalmpl/uri/remote/RascalFileSystemServices.java b/src/org/rascalmpl/uri/remote/RascalFileSystemServices.java index d26c9381d73..b742c04e50d 100644 --- a/src/org/rascalmpl/uri/remote/RascalFileSystemServices.java +++ b/src/org/rascalmpl/uri/remote/RascalFileSystemServices.java @@ -43,6 +43,13 @@ import org.rascalmpl.uri.FileAttributes; import org.rascalmpl.uri.URIResolverRegistry; import org.rascalmpl.uri.URIUtil; +import org.rascalmpl.uri.remote.jsonrpc.ISourceLocationChangeType; +import org.rascalmpl.uri.remote.jsonrpc.ISourceLocationChanged; +import org.rascalmpl.uri.remote.jsonrpc.ISourceLocationRequest; +import org.rascalmpl.uri.remote.jsonrpc.RemoveRequest; +import org.rascalmpl.uri.remote.jsonrpc.RenameRequest; +import org.rascalmpl.uri.remote.jsonrpc.WatchRequest; +import org.rascalmpl.uri.remote.jsonrpc.WriteFileRequest; import org.rascalmpl.uri.vfs.IRemoteResolverRegistryClient; import org.rascalmpl.uri.vfs.IRemoteResolverRegistryServer; import org.rascalmpl.util.NamedThreadPool; @@ -61,8 +68,9 @@ protected void provideClient(IRemoteResolverRegistryClient client) { } @Override - public CompletableFuture resolveLocation(ISourceLocation loc) { + public CompletableFuture resolveLocation(ISourceLocationRequest req) { return CompletableFuture.supplyAsync(() -> { + ISourceLocation loc = req.getLocation(); try { ISourceLocation resolved = reg.logicalToPhysical(loc); @@ -84,7 +92,9 @@ public CompletableFuture watch(WatchRequest params) { ISourceLocation loc = params.getLocation(); URIResolverRegistry.getInstance().watch(loc, params.isRecursive(), changed -> { - client.sourceLocationChanged(changed.getLocation(), changed.getChangeType().getValue(), params.getWatcher()); + client.sourceLocationChanged(new ISourceLocationChanged( + changed.getLocation(), ISourceLocationChangeType.forValue(changed.getChangeType().getValue()), params.getWatcher() + )); }); } catch (IOException | RuntimeException e) { throw new CompletionException(e); @@ -93,10 +103,10 @@ public CompletableFuture watch(WatchRequest params) { } @Override - public CompletableFuture stat(ISourceLocation loc) { + public CompletableFuture stat(ISourceLocationRequest req) { return CompletableFuture.supplyAsync(() -> { try { - return reg.stat(loc); + return reg.stat(req.getLocation()); } catch (IOException | RuntimeException e) { throw new CompletionException(e); } @@ -104,9 +114,10 @@ public CompletableFuture stat(ISourceLocation loc) { } @Override - public CompletableFuture list(ISourceLocation loc) { + public CompletableFuture list(ISourceLocationRequest req) { return CompletableFuture.supplyAsync(() -> { try { + ISourceLocation loc = req.getLocation(); if (!reg.isDirectory(loc)) { throw new NotDirectoryException(loc.toString()); } @@ -119,10 +130,10 @@ public CompletableFuture list(ISourceLocation loc) { } @Override - public CompletableFuture mkDirectory(ISourceLocation loc) { + public CompletableFuture mkDirectory(ISourceLocationRequest req) { return CompletableFuture.runAsync(() -> { try { - reg.mkDirectory(loc); + reg.mkDirectory(req.getLocation()); } catch (IOException | RuntimeException e) { throw new CompletionException(e); } @@ -130,9 +141,9 @@ public CompletableFuture mkDirectory(ISourceLocation loc) { } @Override - public CompletableFuture readFile(ISourceLocation loc) { + public CompletableFuture readFile(ISourceLocationRequest req) { return CompletableFuture.supplyAsync(() -> { - try (InputStream source = new Base64InputStream(reg.getInputStream(loc), true)) { + try (InputStream source = new Base64InputStream(reg.getInputStream(req.getLocation()), true)) { return new String(source.readAllBytes(), StandardCharsets.US_ASCII); } catch (IOException | RuntimeException e) { throw new CompletionException(e); @@ -141,11 +152,11 @@ public CompletableFuture readFile(ISourceLocation loc) { } @Override - public CompletableFuture writeFile(ISourceLocation loc, String content, boolean append) { + public CompletableFuture writeFile(WriteFileRequest req) { return CompletableFuture.runAsync(() -> { try { - try (OutputStream target = reg.getOutputStream(loc, false)) { - target.write(Base64.getDecoder().decode(content)); + try (OutputStream target = reg.getOutputStream(req.getLocation(), req.isAppend())) { + target.write(Base64.getDecoder().decode(req.getContent())); } } catch (IOException | RuntimeException e) { throw new CompletionException(e); @@ -154,10 +165,10 @@ public CompletableFuture writeFile(ISourceLocation loc, String content, bo } @Override - public CompletableFuture remove(ISourceLocation loc, boolean recursive) { + public CompletableFuture remove(RemoveRequest req) { return CompletableFuture.runAsync(() -> { try { - reg.remove(loc, recursive); + reg.remove(req.getLocation(), req.isRecursive()); } catch (IOException e) { throw new CompletionException(e); } @@ -165,10 +176,10 @@ public CompletableFuture remove(ISourceLocation loc, boolean recursive) { } @Override - public CompletableFuture rename(ISourceLocation from, ISourceLocation to, boolean overwrite) { + public CompletableFuture rename(RenameRequest req) { return CompletableFuture.runAsync(() -> { try { - reg.rename(from, to, overwrite); + reg.rename(req.getFrom(), req.getTo(), req.isOverwrite()); } catch (IOException e) { throw new CompletionException(e); } diff --git a/src/org/rascalmpl/uri/remote/RemoteExternalResolverRegistry.java b/src/org/rascalmpl/uri/remote/RemoteExternalResolverRegistry.java index 306cbc19f23..e389d3f5d00 100644 --- a/src/org/rascalmpl/uri/remote/RemoteExternalResolverRegistry.java +++ b/src/org/rascalmpl/uri/remote/RemoteExternalResolverRegistry.java @@ -66,11 +66,15 @@ import org.rascalmpl.uri.IExternalResolverRegistry; import org.rascalmpl.uri.ISourceLocationWatcher; import org.rascalmpl.uri.URIUtil; +import org.rascalmpl.uri.remote.jsonrpc.ISourceLocationRequest; +import org.rascalmpl.uri.remote.jsonrpc.RemoveRequest; +import org.rascalmpl.uri.remote.jsonrpc.SetLastModifiedRequest; +import org.rascalmpl.uri.remote.jsonrpc.WatchRequest; +import org.rascalmpl.uri.remote.jsonrpc.WriteFileRequest; import org.rascalmpl.uri.vfs.IRemoteResolverRegistryClient; import org.rascalmpl.uri.vfs.IRemoteResolverRegistryServer; import org.rascalmpl.uri.vfs.IRemoteResolverRegistryServer.FileType; import org.rascalmpl.uri.vfs.IRemoteResolverRegistryServer.FileWithType; -import org.rascalmpl.uri.vfs.IRemoteResolverRegistryServer.WatchRequest; import org.rascalmpl.util.Lazy; import org.rascalmpl.util.NamedThreadPool; import org.rascalmpl.util.base64.StreamingBase64; @@ -306,14 +310,14 @@ private static IOException translateException(ResponseErrorException cause) { } @Override - public InputStream getInputStream(ISourceLocation loc) throws IOException { - return StreamingBase64.decode(call(remote::readFile, loc)); + public InputStream getInputStream(ISourceLocation loc) throws IOException { + return StreamingBase64.decode(call(remote::readFile, new ISourceLocationRequest(loc))); } @Override public boolean exists(ISourceLocation loc) { try { - return call(remote::exists, loc); + return call(remote::exists, new ISourceLocationRequest(loc)); } catch (IOException e) { return false; } @@ -321,12 +325,12 @@ public boolean exists(ISourceLocation loc) { @Override public long lastModified(ISourceLocation loc) throws IOException { - return call(remote::lastModified, loc); + return call(remote::lastModified, new ISourceLocationRequest(loc)); } @Override public long size(ISourceLocation loc) throws IOException { - return call(remote::size, loc); + return call(remote::size, new ISourceLocationRequest(loc)); } @Override @@ -339,7 +343,7 @@ public boolean isDirectory(ISourceLocation loc) { return result; } } - return call(remote::isDirectory, loc); + return call(remote::isDirectory, new ISourceLocationRequest(loc)); } catch (IOException e) { return false; } @@ -355,7 +359,7 @@ public boolean isFile(ISourceLocation loc) { return !result; } } - return call(remote::isFile, loc); + return call(remote::isFile, new ISourceLocationRequest(loc)); } catch (IOException e) { return false; } @@ -363,7 +367,7 @@ public boolean isFile(ISourceLocation loc) { @Override public boolean isReadable(ISourceLocation loc) throws IOException { - return call(remote::isReadable, loc); + return call(remote::isReadable, new ISourceLocationRequest(loc)); } /** @@ -379,7 +383,7 @@ public boolean isReadable(ISourceLocation loc) throws IOException { @Override public String[] list(ISourceLocation loc) throws IOException { - var result = call(remote::list, loc); + var result = call(remote::list, new ISourceLocationRequest(loc)); cachedDirectoryListing.put(loc, Lazy.defer(() -> { return Stream.of(result).collect(Collectors.toMap(FileWithType::getName, e -> e.getType() == FileType.Directory)); })); @@ -393,7 +397,7 @@ public boolean supportsHost() { @Override public FileAttributes stat(ISourceLocation loc) throws IOException { - return call(remote::stat, loc); + return call(remote::stat, new ISourceLocationRequest(loc)); } @Override @@ -411,7 +415,7 @@ public void close() throws IOException { try (var input = new ByteArrayInputStream(this.toByteArray())) { StreamingBase64.encode(input, content, true); } - call(l -> remote.writeFile(l, content.toString(), append), loc); + call(remote::writeFile, new WriteFileRequest(loc, content.toString(), append)); cachedDirectoryListing.invalidate(URIUtil.getParentLocation(loc)); } }; @@ -419,31 +423,30 @@ public void close() throws IOException { @Override public void mkDirectory(ISourceLocation loc) throws IOException { - call(remote::mkDirectory, loc); + call(remote::mkDirectory, new ISourceLocationRequest(loc)); cachedDirectoryListing.invalidate(URIUtil.getParentLocation(loc)); } @Override public void remove(ISourceLocation loc) throws IOException { - call(l -> remote.remove(l, true), loc); + call(remote::remove, new RemoveRequest(loc, true)); cachedDirectoryListing.invalidate(loc); cachedDirectoryListing.invalidate(URIUtil.getParentLocation(loc)); } @Override public void setLastModified(ISourceLocation loc, long timestamp) throws IOException { - call(l -> remote.setLastModified(l, timestamp), loc); + call(remote::setLastModified, new SetLastModifiedRequest(loc, timestamp)); } @Override public boolean isWritable(ISourceLocation loc) throws IOException { - return call(remote::isWritable, loc); + return call(remote::isWritable, new ISourceLocationRequest(loc)); } @Override public ISourceLocation resolve(ISourceLocation input) throws IOException { - var resolved = call(remote::resolveLocation, input); - return resolved; + return call(remote::resolveLocation, new ISourceLocationRequest(input)); } @Override @@ -495,13 +498,14 @@ public boolean supportsRecursiveWatch() { } @Override - public void sourceLocationChanged(ISourceLocation root, int type, String watchId) { - var watcher = watchersById.get(watchId); + public void sourceLocationChanged(org.rascalmpl.uri.remote.jsonrpc.ISourceLocationChanged changed) { + var watcher = watchersById.get(changed.getWatchId()); + var root = changed.getRoot(); if (watcher == null) { throw new ResponseErrorException(new ResponseError(ResponseErrorCode.RequestFailed, "Received notification for unregistered watch", root)); } try { - switch (ISourceLocationChangeType.fromValue(type)) { + switch (ISourceLocationChangeType.fromValue(changed.getChangeType().getValue())) { case CREATED: watcher.publish(ISourceLocationWatcher.created(root)); break; @@ -513,7 +517,7 @@ public void sourceLocationChanged(ISourceLocation root, int type, String watchId break; } } catch (IllegalArgumentException e) { - throw new ResponseErrorException(new ResponseError(ResponseErrorCode.InvalidParams, "Unexpected FileChangeType " + type, root)); + throw new ResponseErrorException(new ResponseError(ResponseErrorCode.InvalidParams, "Unexpected FileChangeType " + changed.getChangeType().getValue(), root)); } } diff --git a/src/org/rascalmpl/uri/remote/jsonrpc/ISourceLocationChangeType.java b/src/org/rascalmpl/uri/remote/jsonrpc/ISourceLocationChangeType.java new file mode 100644 index 00000000000..070dde2db87 --- /dev/null +++ b/src/org/rascalmpl/uri/remote/jsonrpc/ISourceLocationChangeType.java @@ -0,0 +1,69 @@ +/* + * 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.uri.remote.jsonrpc; + +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/src/org/rascalmpl/uri/remote/jsonrpc/ISourceLocationChanged.java b/src/org/rascalmpl/uri/remote/jsonrpc/ISourceLocationChanged.java new file mode 100644 index 00000000000..b711ed6067b --- /dev/null +++ b/src/org/rascalmpl/uri/remote/jsonrpc/ISourceLocationChanged.java @@ -0,0 +1,87 @@ +/* + * 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.uri.remote.jsonrpc; + +import java.util.Objects; + +import org.checkerframework.checker.nullness.qual.Nullable; +import org.eclipse.lsp4j.jsonrpc.validation.NonNull; +import org.rascalmpl.uri.ISourceLocationWatcher; + +import io.usethesource.vallang.ISourceLocation; + +public class ISourceLocationChanged { + @NonNull + private ISourceLocation root; + @NonNull + private ISourceLocationChangeType type; + @NonNull + private String watchId; + + public ISourceLocationChanged(@NonNull ISourceLocation root, @NonNull ISourceLocationChangeType type, @NonNull String watchId) { + this.root = root; + this.type = type; + this.watchId = watchId; + } + + public ISourceLocation getRoot() { + return root; + } + + public ISourceLocationChangeType getChangeType() { + return type; + } + + public String getWatchId() { + return watchId; + } + + @Override + public boolean equals(@Nullable Object obj) { + if (obj instanceof ISourceLocationChanged) { + var other = (ISourceLocationChanged)obj; + return Objects.equals(root, other.root) + && Objects.equals(type, other.type) + && Objects.equals(watchId, other.watchId); + } + return false; + } + + @Override + public int hashCode() { + return Objects.hash(root, type, watchId); + } + + public ISourceLocationWatcher.ISourceLocationChanged translate() { + return ISourceLocationWatcher.makeChange(getRoot(), ISourceLocationChangeType.translate(type)); + } + + @Override + public String toString() { + return "ISourceLocationChanged [changeType=" + type + ", root=" + root + ", watchId=" + watchId + "]"; + } +} diff --git a/src/org/rascalmpl/uri/remote/jsonrpc/ISourceLocationRequest.java b/src/org/rascalmpl/uri/remote/jsonrpc/ISourceLocationRequest.java new file mode 100644 index 00000000000..91ade0b861e --- /dev/null +++ b/src/org/rascalmpl/uri/remote/jsonrpc/ISourceLocationRequest.java @@ -0,0 +1,59 @@ +/* + * 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.uri.remote.jsonrpc; + +import org.checkerframework.checker.nullness.qual.Nullable; +import org.eclipse.lsp4j.jsonrpc.validation.NonNull; + +import io.usethesource.vallang.ISourceLocation; + +public class ISourceLocationRequest { + @NonNull + private ISourceLocation loc; + + + public ISourceLocationRequest(ISourceLocation loc) { + this.loc = loc; + } + + @Override + public boolean equals(@Nullable Object obj) { + if (obj instanceof ISourceLocationRequest) { + return loc.equals(((ISourceLocationRequest)obj).loc); + } + return false; + } + + @Override + public int hashCode() { + return 7 * loc.hashCode(); + } + + public ISourceLocation getLocation() { + return loc; + } +} diff --git a/src/org/rascalmpl/uri/remote/jsonrpc/RemoveRequest.java b/src/org/rascalmpl/uri/remote/jsonrpc/RemoveRequest.java new file mode 100644 index 00000000000..f0dcc9ca235 --- /dev/null +++ b/src/org/rascalmpl/uri/remote/jsonrpc/RemoveRequest.java @@ -0,0 +1,61 @@ +/* + * 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.uri.remote.jsonrpc; + +import java.util.Objects; + +import org.checkerframework.checker.nullness.qual.Nullable; + +import io.usethesource.vallang.ISourceLocation; + +public class RemoveRequest extends ISourceLocationRequest { + private boolean recursive; + + public RemoveRequest(ISourceLocation loc, boolean recursive) { + super(loc); + this.recursive = recursive; + } + + public boolean isRecursive() { + return recursive; + } + + @Override + public boolean equals(@Nullable Object obj) { + if (obj instanceof RemoveRequest) { + var other = (RemoveRequest)obj; + return super.equals(obj) + && recursive == other.recursive; + } + return false; + } + + @Override + public int hashCode() { + return Objects.hash(super.hashCode(), recursive); + } +} diff --git a/src/org/rascalmpl/uri/remote/jsonrpc/RenameRequest.java b/src/org/rascalmpl/uri/remote/jsonrpc/RenameRequest.java new file mode 100644 index 00000000000..9355516418a --- /dev/null +++ b/src/org/rascalmpl/uri/remote/jsonrpc/RenameRequest.java @@ -0,0 +1,78 @@ +/* + * 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.uri.remote.jsonrpc; + +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 RenameRequest { + @NonNull + private ISourceLocation from; + @NonNull + private ISourceLocation to; + + private boolean overwrite; + + public RenameRequest(ISourceLocation from, ISourceLocation to, boolean overwrite) { + this.from = from; + this.to = to; + this.overwrite = overwrite; + } + + public ISourceLocation getFrom() { + return from; + } + + public ISourceLocation getTo() { + return to; + } + + public boolean isOverwrite() { + 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/src/org/rascalmpl/uri/remote/jsonrpc/SetLastModifiedRequest.java b/src/org/rascalmpl/uri/remote/jsonrpc/SetLastModifiedRequest.java new file mode 100644 index 00000000000..e6c6399fcf7 --- /dev/null +++ b/src/org/rascalmpl/uri/remote/jsonrpc/SetLastModifiedRequest.java @@ -0,0 +1,60 @@ +/* + * 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.uri.remote.jsonrpc; + +import java.util.Objects; + +import org.checkerframework.checker.nullness.qual.Nullable; + +import io.usethesource.vallang.ISourceLocation; + +public class SetLastModifiedRequest extends ISourceLocationRequest{ + private long timestamp; + + public SetLastModifiedRequest(ISourceLocation loc, long timestamp) { + super(loc); + this.timestamp = timestamp; + } + + public long getTimestamp() { + return timestamp; + } + + @Override + public boolean equals(@Nullable Object obj) { + if (obj instanceof SetLastModifiedRequest) { + var other = (SetLastModifiedRequest)obj; + return super.equals(other) && timestamp == other.timestamp; + } + return false; + } + + @Override + public int hashCode() { + return Objects.hash(super.hashCode(), timestamp); + } +} diff --git a/src/org/rascalmpl/uri/remote/jsonrpc/WatchRequest.java b/src/org/rascalmpl/uri/remote/jsonrpc/WatchRequest.java new file mode 100644 index 00000000000..77358f59d73 --- /dev/null +++ b/src/org/rascalmpl/uri/remote/jsonrpc/WatchRequest.java @@ -0,0 +1,71 @@ +/* + * 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.uri.remote.jsonrpc; + +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 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/src/org/rascalmpl/uri/remote/jsonrpc/WriteFileRequest.java b/src/org/rascalmpl/uri/remote/jsonrpc/WriteFileRequest.java new file mode 100644 index 00000000000..5c21bd2d277 --- /dev/null +++ b/src/org/rascalmpl/uri/remote/jsonrpc/WriteFileRequest.java @@ -0,0 +1,71 @@ +/* + * 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.uri.remote.jsonrpc; + +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 final String content; + + private final boolean append; + + public WriteFileRequest(ISourceLocation loc, String content, boolean append) { + super(loc); + this.content = content; + this.append = append; + } + + public String getContent() { + return content; + } + + public boolean isAppend() { + 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/src/org/rascalmpl/uri/vfs/IRemoteResolverRegistryClient.java b/src/org/rascalmpl/uri/vfs/IRemoteResolverRegistryClient.java index 6b479a43da7..f0ea0d91b55 100644 --- a/src/org/rascalmpl/uri/vfs/IRemoteResolverRegistryClient.java +++ b/src/org/rascalmpl/uri/vfs/IRemoteResolverRegistryClient.java @@ -1,10 +1,9 @@ package org.rascalmpl.uri.vfs; import org.eclipse.lsp4j.jsonrpc.services.JsonNotification; - -import io.usethesource.vallang.ISourceLocation; +import org.rascalmpl.uri.remote.jsonrpc.ISourceLocationChanged; public interface IRemoteResolverRegistryClient { @JsonNotification("rascal/vfs/watcher/sourceLocationChanged") - void sourceLocationChanged(ISourceLocation root, int type, String watchId); + void sourceLocationChanged(ISourceLocationChanged changed); } diff --git a/src/org/rascalmpl/uri/vfs/IRemoteResolverRegistryServer.java b/src/org/rascalmpl/uri/vfs/IRemoteResolverRegistryServer.java index 61d216abea7..a2e1045db5f 100644 --- a/src/org/rascalmpl/uri/vfs/IRemoteResolverRegistryServer.java +++ b/src/org/rascalmpl/uri/vfs/IRemoteResolverRegistryServer.java @@ -26,96 +26,98 @@ */ package org.rascalmpl.uri.vfs; -import java.util.Arrays; -import java.util.Objects; import java.util.concurrent.CompletableFuture; -import org.checkerframework.checker.nullness.qual.Nullable; import org.eclipse.lsp4j.jsonrpc.services.JsonRequest; import org.eclipse.lsp4j.jsonrpc.validation.NonNull; import org.rascalmpl.uri.FileAttributes; -import org.rascalmpl.values.ValueFactoryFactory; +import org.rascalmpl.uri.remote.jsonrpc.ISourceLocationRequest; +import org.rascalmpl.uri.remote.jsonrpc.RemoveRequest; +import org.rascalmpl.uri.remote.jsonrpc.RenameRequest; +import org.rascalmpl.uri.remote.jsonrpc.SetLastModifiedRequest; +import org.rascalmpl.uri.remote.jsonrpc.WatchRequest; +import org.rascalmpl.uri.remote.jsonrpc.WriteFileRequest; import io.usethesource.vallang.ISourceLocation; public interface IRemoteResolverRegistryServer { @JsonRequest("rascal/vfs/input/readFile") - default CompletableFuture readFile(ISourceLocation loc) { + default CompletableFuture readFile(ISourceLocationRequest req) { throw new UnsupportedOperationException(); } @JsonRequest("rascal/vfs/input/exists") - default CompletableFuture exists(ISourceLocation loc) { + default CompletableFuture exists(ISourceLocationRequest req) { throw new UnsupportedOperationException(); } @JsonRequest("rascal/vfs/input/lastModified") - default CompletableFuture lastModified(ISourceLocation loc) { + default CompletableFuture lastModified(ISourceLocationRequest req) { throw new UnsupportedOperationException(); } @JsonRequest("rascal/vfs/input/created") - default CompletableFuture created(ISourceLocation loc) { + default CompletableFuture created(ISourceLocationRequest req) { throw new UnsupportedOperationException(); } @JsonRequest("rascal/vfs/input/isDirectory") - default CompletableFuture isDirectory(ISourceLocation loc) { + default CompletableFuture isDirectory(ISourceLocationRequest req) { throw new UnsupportedOperationException(); } @JsonRequest("rascal/vfs/input/isFile") - default CompletableFuture isFile(ISourceLocation loc) { + default CompletableFuture isFile(ISourceLocationRequest req) { throw new UnsupportedOperationException(); } @JsonRequest("rascal/vfs/input/list") - default CompletableFuture list(ISourceLocation loc) { + default CompletableFuture list(ISourceLocationRequest req) { throw new UnsupportedOperationException(); } @JsonRequest("rascal/vfs/input/size") - default CompletableFuture size(ISourceLocation loc) { + default CompletableFuture size(ISourceLocationRequest req) { throw new UnsupportedOperationException(); } @JsonRequest("rascal/vfs/input/stat") - default CompletableFuture stat(ISourceLocation loc) { + default CompletableFuture stat(ISourceLocationRequest req) { throw new UnsupportedOperationException(); } @JsonRequest("rascal/vfs/input/isReadable") - default CompletableFuture isReadable(ISourceLocation loc) { + default CompletableFuture isReadable(ISourceLocationRequest req) { throw new UnsupportedOperationException(); } @JsonRequest("rascal/vfs/output/setLastModified") - default CompletableFuture setLastModified(ISourceLocation loc, long timestamp) { + default CompletableFuture setLastModified(SetLastModifiedRequest req) { throw new UnsupportedOperationException(); } @JsonRequest("rascal/vfs/input/isWritable") - default CompletableFuture isWritable(ISourceLocation loc) { + default CompletableFuture isWritable(ISourceLocationRequest req) { throw new UnsupportedOperationException(); } @JsonRequest("rascal/vfs/output/writeFile") - default CompletableFuture writeFile(ISourceLocation loc, String content, boolean append) { + default CompletableFuture writeFile(WriteFileRequest req) { throw new UnsupportedOperationException(); } @JsonRequest("rascal/vfs/output/mkDirectory") - default CompletableFuture mkDirectory(ISourceLocation loc) { + default CompletableFuture mkDirectory(ISourceLocationRequest req) { throw new UnsupportedOperationException(); } @JsonRequest("rascal/vfs/output/remove") - default CompletableFuture remove(ISourceLocation loc, boolean recursive) { + default CompletableFuture remove(RemoveRequest req) { throw new UnsupportedOperationException(); } @JsonRequest("rascal/vfs/output/rename") - default CompletableFuture rename(ISourceLocation from, ISourceLocation to, boolean overwrite) { + default CompletableFuture rename(RenameRequest req) { throw new UnsupportedOperationException(); } @@ -135,73 +137,10 @@ default CompletableFuture supportsRecursiveWatch() { } @JsonRequest("rascal/vfs/logical/resolveLocation") - default CompletableFuture resolveLocation(ISourceLocation loc) { + default CompletableFuture resolveLocation(ISourceLocationRequest loc) { throw new UnsupportedOperationException(); } - public static class WatchRequest { - @NonNull private ISourceLocation loc; - @NonNull private String watcher; - - private boolean recursive; - - private final String[] excludes; - - public WatchRequest(ISourceLocation loc, boolean recursive, String watcher) { - this.loc = loc; - this.recursive = recursive; - this.watcher = watcher; - this.excludes = new String[0]; - } - - public WatchRequest(@NonNull String uri, boolean recursive, @NonNull String watcher) { - this.loc = ValueFactoryFactory.getValueFactory().sourceLocation(uri); - this.recursive = recursive; - this.watcher = watcher; - this.excludes = new String[0]; - } - - public WatchRequest(String uri, boolean recursive, String[] excludes) { - this.loc = ValueFactoryFactory.getValueFactory().sourceLocation(uri); - this.recursive = recursive; - this.watcher = ""; - this.excludes = excludes; - } - - public ISourceLocation getLocation() { - return loc; - } - - public String getWatcher() { - return watcher; - } - - public boolean isRecursive() { - return recursive; - } - - public String[] getExcludes() { - return excludes; - } - - @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) - && Arrays.equals(excludes, other.excludes); - } - return false; - } - - @Override - public int hashCode() { - return Objects.hash(super.hashCode(), watcher, recursive, excludes); - } - } - public static class FileWithType { @NonNull private final String name; @NonNull private final FileType type; From 4654a6ac2afdeaddd033ebce9759b1cb22821302 Mon Sep 17 00:00:00 2001 From: Rodin Aarssen Date: Tue, 31 Mar 2026 09:41:10 +0200 Subject: [PATCH 55/98] Added jsonrpc response classes for modularity and changed the signature and implementation to use them --- .../uri/remote/RascalFileSystemServices.java | 14 +++--- .../RemoteExternalResolverRegistry.java | 20 ++++----- .../uri/remote/jsonrpc/BooleanResponse.java | 39 ++++++++++++++++ .../jsonrpc/LocationContentResponse.java | 42 ++++++++++++++++++ .../uri/remote/jsonrpc/NumberResponse.java | 39 ++++++++++++++++ .../jsonrpc/SourceLocationResponse.java | 44 +++++++++++++++++++ .../uri/remote/jsonrpc/TimestampResponse.java | 39 ++++++++++++++++ .../vfs/IRemoteResolverRegistryServer.java | 29 ++++++------ 8 files changed, 237 insertions(+), 29 deletions(-) create mode 100644 src/org/rascalmpl/uri/remote/jsonrpc/BooleanResponse.java create mode 100644 src/org/rascalmpl/uri/remote/jsonrpc/LocationContentResponse.java create mode 100644 src/org/rascalmpl/uri/remote/jsonrpc/NumberResponse.java create mode 100644 src/org/rascalmpl/uri/remote/jsonrpc/SourceLocationResponse.java create mode 100644 src/org/rascalmpl/uri/remote/jsonrpc/TimestampResponse.java diff --git a/src/org/rascalmpl/uri/remote/RascalFileSystemServices.java b/src/org/rascalmpl/uri/remote/RascalFileSystemServices.java index b742c04e50d..d219d00f3f5 100644 --- a/src/org/rascalmpl/uri/remote/RascalFileSystemServices.java +++ b/src/org/rascalmpl/uri/remote/RascalFileSystemServices.java @@ -46,8 +46,10 @@ import org.rascalmpl.uri.remote.jsonrpc.ISourceLocationChangeType; import org.rascalmpl.uri.remote.jsonrpc.ISourceLocationChanged; import org.rascalmpl.uri.remote.jsonrpc.ISourceLocationRequest; +import org.rascalmpl.uri.remote.jsonrpc.LocationContentResponse; 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.uri.vfs.IRemoteResolverRegistryClient; @@ -68,19 +70,19 @@ protected void provideClient(IRemoteResolverRegistryClient client) { } @Override - public CompletableFuture resolveLocation(ISourceLocationRequest req) { + public CompletableFuture resolveLocation(ISourceLocationRequest req) { return CompletableFuture.supplyAsync(() -> { ISourceLocation loc = req.getLocation(); try { ISourceLocation resolved = reg.logicalToPhysical(loc); if (resolved == null) { - return loc; + return new SourceLocationResponse(loc); } - return resolved; + return new SourceLocationResponse(resolved); } catch (Exception e) { - return loc; + return new SourceLocationResponse(loc); } }, executor); } @@ -141,10 +143,10 @@ public CompletableFuture mkDirectory(ISourceLocationRequest req) { } @Override - public CompletableFuture readFile(ISourceLocationRequest req) { + public CompletableFuture readFile(ISourceLocationRequest req) { return CompletableFuture.supplyAsync(() -> { try (InputStream source = new Base64InputStream(reg.getInputStream(req.getLocation()), true)) { - return new String(source.readAllBytes(), StandardCharsets.US_ASCII); + return new LocationContentResponse(new String(source.readAllBytes(), StandardCharsets.US_ASCII)); } catch (IOException | RuntimeException e) { throw new CompletionException(e); } diff --git a/src/org/rascalmpl/uri/remote/RemoteExternalResolverRegistry.java b/src/org/rascalmpl/uri/remote/RemoteExternalResolverRegistry.java index e389d3f5d00..187802444a1 100644 --- a/src/org/rascalmpl/uri/remote/RemoteExternalResolverRegistry.java +++ b/src/org/rascalmpl/uri/remote/RemoteExternalResolverRegistry.java @@ -311,13 +311,13 @@ private static IOException translateException(ResponseErrorException cause) { @Override public InputStream getInputStream(ISourceLocation loc) throws IOException { - return StreamingBase64.decode(call(remote::readFile, new ISourceLocationRequest(loc))); + return StreamingBase64.decode(call(remote::readFile, new ISourceLocationRequest(loc)).getContent()); } @Override public boolean exists(ISourceLocation loc) { try { - return call(remote::exists, new ISourceLocationRequest(loc)); + return call(remote::exists, new ISourceLocationRequest(loc)).getValue(); } catch (IOException e) { return false; } @@ -325,12 +325,12 @@ public boolean exists(ISourceLocation loc) { @Override public long lastModified(ISourceLocation loc) throws IOException { - return call(remote::lastModified, new ISourceLocationRequest(loc)); + return call(remote::lastModified, new ISourceLocationRequest(loc)).getTimestamp(); } @Override public long size(ISourceLocation loc) throws IOException { - return call(remote::size, new ISourceLocationRequest(loc)); + return call(remote::size, new ISourceLocationRequest(loc)).getNumber(); } @Override @@ -343,7 +343,7 @@ public boolean isDirectory(ISourceLocation loc) { return result; } } - return call(remote::isDirectory, new ISourceLocationRequest(loc)); + return call(remote::isDirectory, new ISourceLocationRequest(loc)).getValue(); } catch (IOException e) { return false; } @@ -359,7 +359,7 @@ public boolean isFile(ISourceLocation loc) { return !result; } } - return call(remote::isFile, new ISourceLocationRequest(loc)); + return call(remote::isFile, new ISourceLocationRequest(loc)).getValue(); } catch (IOException e) { return false; } @@ -367,7 +367,7 @@ public boolean isFile(ISourceLocation loc) { @Override public boolean isReadable(ISourceLocation loc) throws IOException { - return call(remote::isReadable, new ISourceLocationRequest(loc)); + return call(remote::isReadable, new ISourceLocationRequest(loc)).getValue(); } /** @@ -441,12 +441,12 @@ public void setLastModified(ISourceLocation loc, long timestamp) throws IOExcept @Override public boolean isWritable(ISourceLocation loc) throws IOException { - return call(remote::isWritable, new ISourceLocationRequest(loc)); + return call(remote::isWritable, new ISourceLocationRequest(loc)).getValue(); } @Override public ISourceLocation resolve(ISourceLocation input) throws IOException { - return call(remote::resolveLocation, new ISourceLocationRequest(input)); + return call(remote::resolveLocation, new ISourceLocationRequest(input)).getLocation(); } @Override @@ -491,7 +491,7 @@ public void unwatch(ISourceLocation root, Consumer watch @Override public boolean supportsRecursiveWatch() { try { - return call(n -> remote.supportsRecursiveWatch(), null); + return call(n -> remote.supportsRecursiveWatch(), null).getValue(); } catch (IOException e) { return false; } diff --git a/src/org/rascalmpl/uri/remote/jsonrpc/BooleanResponse.java b/src/org/rascalmpl/uri/remote/jsonrpc/BooleanResponse.java new file mode 100644 index 00000000000..9861074f086 --- /dev/null +++ b/src/org/rascalmpl/uri/remote/jsonrpc/BooleanResponse.java @@ -0,0 +1,39 @@ +/* + * 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.uri.remote.jsonrpc; + +public class BooleanResponse { + private final boolean value; + + public BooleanResponse(boolean value) { + this.value = value; + } + + public boolean getValue() { + return value; + } +} diff --git a/src/org/rascalmpl/uri/remote/jsonrpc/LocationContentResponse.java b/src/org/rascalmpl/uri/remote/jsonrpc/LocationContentResponse.java new file mode 100644 index 00000000000..57d32837d0c --- /dev/null +++ b/src/org/rascalmpl/uri/remote/jsonrpc/LocationContentResponse.java @@ -0,0 +1,42 @@ +/* + * 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.uri.remote.jsonrpc; + +import org.checkerframework.checker.nullness.qual.NonNull; + +public class LocationContentResponse { + @NonNull + private final String content; + + public LocationContentResponse(@NonNull String content) { + this.content = content; + } + + public String getContent() { + return content; + } +} diff --git a/src/org/rascalmpl/uri/remote/jsonrpc/NumberResponse.java b/src/org/rascalmpl/uri/remote/jsonrpc/NumberResponse.java new file mode 100644 index 00000000000..da695f6515d --- /dev/null +++ b/src/org/rascalmpl/uri/remote/jsonrpc/NumberResponse.java @@ -0,0 +1,39 @@ +/* + * 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.uri.remote.jsonrpc; + +public class NumberResponse { + private final long number; + + public NumberResponse(long number) { + this.number = number; + } + + public long getNumber() { + return number; + } +} diff --git a/src/org/rascalmpl/uri/remote/jsonrpc/SourceLocationResponse.java b/src/org/rascalmpl/uri/remote/jsonrpc/SourceLocationResponse.java new file mode 100644 index 00000000000..d97c88db499 --- /dev/null +++ b/src/org/rascalmpl/uri/remote/jsonrpc/SourceLocationResponse.java @@ -0,0 +1,44 @@ +/* + * Copyright (c) 2018-2025, NWO-I CWI and Swat.engineering + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ +package org.rascalmpl.uri.remote.jsonrpc; + +import org.checkerframework.checker.nullness.qual.NonNull; + +import io.usethesource.vallang.ISourceLocation; + +public class SourceLocationResponse { + @NonNull + private final ISourceLocation loc; + + public SourceLocationResponse(@NonNull ISourceLocation loc) { + this.loc = loc; + } + + public ISourceLocation getLocation() { + return loc; + } +} diff --git a/src/org/rascalmpl/uri/remote/jsonrpc/TimestampResponse.java b/src/org/rascalmpl/uri/remote/jsonrpc/TimestampResponse.java new file mode 100644 index 00000000000..894b2c269d4 --- /dev/null +++ b/src/org/rascalmpl/uri/remote/jsonrpc/TimestampResponse.java @@ -0,0 +1,39 @@ +/* + * 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.uri.remote.jsonrpc; + +public class TimestampResponse { + private final long timestamp; + + public TimestampResponse(long timestamp) { + this.timestamp = timestamp; + } + + public long getTimestamp() { + return timestamp; + } +} diff --git a/src/org/rascalmpl/uri/vfs/IRemoteResolverRegistryServer.java b/src/org/rascalmpl/uri/vfs/IRemoteResolverRegistryServer.java index a2e1045db5f..8c4cf0bb5ec 100644 --- a/src/org/rascalmpl/uri/vfs/IRemoteResolverRegistryServer.java +++ b/src/org/rascalmpl/uri/vfs/IRemoteResolverRegistryServer.java @@ -31,43 +31,46 @@ import org.eclipse.lsp4j.jsonrpc.services.JsonRequest; import org.eclipse.lsp4j.jsonrpc.validation.NonNull; import org.rascalmpl.uri.FileAttributes; +import org.rascalmpl.uri.remote.jsonrpc.BooleanResponse; import org.rascalmpl.uri.remote.jsonrpc.ISourceLocationRequest; +import org.rascalmpl.uri.remote.jsonrpc.LocationContentResponse; +import org.rascalmpl.uri.remote.jsonrpc.NumberResponse; import org.rascalmpl.uri.remote.jsonrpc.RemoveRequest; import org.rascalmpl.uri.remote.jsonrpc.RenameRequest; import org.rascalmpl.uri.remote.jsonrpc.SetLastModifiedRequest; +import org.rascalmpl.uri.remote.jsonrpc.SourceLocationResponse; +import org.rascalmpl.uri.remote.jsonrpc.TimestampResponse; import org.rascalmpl.uri.remote.jsonrpc.WatchRequest; import org.rascalmpl.uri.remote.jsonrpc.WriteFileRequest; -import io.usethesource.vallang.ISourceLocation; - public interface IRemoteResolverRegistryServer { @JsonRequest("rascal/vfs/input/readFile") - default CompletableFuture readFile(ISourceLocationRequest req) { + default CompletableFuture readFile(ISourceLocationRequest req) { throw new UnsupportedOperationException(); } @JsonRequest("rascal/vfs/input/exists") - default CompletableFuture exists(ISourceLocationRequest req) { + default CompletableFuture exists(ISourceLocationRequest req) { throw new UnsupportedOperationException(); } @JsonRequest("rascal/vfs/input/lastModified") - default CompletableFuture lastModified(ISourceLocationRequest req) { + default CompletableFuture lastModified(ISourceLocationRequest req) { throw new UnsupportedOperationException(); } @JsonRequest("rascal/vfs/input/created") - default CompletableFuture created(ISourceLocationRequest req) { + default CompletableFuture created(ISourceLocationRequest req) { throw new UnsupportedOperationException(); } @JsonRequest("rascal/vfs/input/isDirectory") - default CompletableFuture isDirectory(ISourceLocationRequest req) { + default CompletableFuture isDirectory(ISourceLocationRequest req) { throw new UnsupportedOperationException(); } @JsonRequest("rascal/vfs/input/isFile") - default CompletableFuture isFile(ISourceLocationRequest req) { + default CompletableFuture isFile(ISourceLocationRequest req) { throw new UnsupportedOperationException(); } @@ -77,7 +80,7 @@ default CompletableFuture list(ISourceLocationRequest req) { } @JsonRequest("rascal/vfs/input/size") - default CompletableFuture size(ISourceLocationRequest req) { + default CompletableFuture size(ISourceLocationRequest req) { throw new UnsupportedOperationException(); } @@ -87,7 +90,7 @@ default CompletableFuture stat(ISourceLocationRequest req) { } @JsonRequest("rascal/vfs/input/isReadable") - default CompletableFuture isReadable(ISourceLocationRequest req) { + default CompletableFuture isReadable(ISourceLocationRequest req) { throw new UnsupportedOperationException(); } @@ -97,7 +100,7 @@ default CompletableFuture setLastModified(SetLastModifiedRequest req) { } @JsonRequest("rascal/vfs/input/isWritable") - default CompletableFuture isWritable(ISourceLocationRequest req) { + default CompletableFuture isWritable(ISourceLocationRequest req) { throw new UnsupportedOperationException(); } @@ -132,12 +135,12 @@ default CompletableFuture unwatch(WatchRequest req) { } @JsonRequest("rascal/vfs/watcher/supportsRecursiveWatch") - default CompletableFuture supportsRecursiveWatch() { + default CompletableFuture supportsRecursiveWatch() { throw new UnsupportedOperationException(); } @JsonRequest("rascal/vfs/logical/resolveLocation") - default CompletableFuture resolveLocation(ISourceLocationRequest loc) { + default CompletableFuture resolveLocation(ISourceLocationRequest loc) { throw new UnsupportedOperationException(); } From 1a086e78fc5ae7647036ead902395482a30d7aa1 Mon Sep 17 00:00:00 2001 From: Rodin Aarssen Date: Wed, 1 Apr 2026 12:48:58 +0200 Subject: [PATCH 56/98] Apply suggestions from code review Co-authored-by: Toine Hartman --- src/org/rascalmpl/uri/remote/RascalFileSystemServices.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/org/rascalmpl/uri/remote/RascalFileSystemServices.java b/src/org/rascalmpl/uri/remote/RascalFileSystemServices.java index d219d00f3f5..bf9762b4979 100644 --- a/src/org/rascalmpl/uri/remote/RascalFileSystemServices.java +++ b/src/org/rascalmpl/uri/remote/RascalFileSystemServices.java @@ -59,8 +59,8 @@ import io.usethesource.vallang.ISourceLocation; public class RascalFileSystemServices implements IRemoteResolverRegistryServer { - static final URIResolverRegistry reg = URIResolverRegistry.getInstance(); - static final ExecutorService executor = NamedThreadPool.cachedDaemon("rascal-vfs"); + private static final URIResolverRegistry reg = URIResolverRegistry.getInstance(); + private static final ExecutorService executor = NamedThreadPool.cachedDaemon("rascal-vfs"); private volatile @MonotonicNonNull IRemoteResolverRegistryClient client = null; From 3ca1ae555b1cd13f22aba4d3b839f1f2462f6857 Mon Sep 17 00:00:00 2001 From: Rodin Aarssen Date: Wed, 1 Apr 2026 12:40:48 +0200 Subject: [PATCH 57/98] Factored out catch-reconnect-throw pattern from reconnecting input/output streams --- .../RemoteExternalResolverRegistry.java | 112 ++++++++++++------ 1 file changed, 78 insertions(+), 34 deletions(-) diff --git a/src/org/rascalmpl/uri/remote/RemoteExternalResolverRegistry.java b/src/org/rascalmpl/uri/remote/RemoteExternalResolverRegistry.java index 187802444a1..afc062c91e7 100644 --- a/src/org/rascalmpl/uri/remote/RemoteExternalResolverRegistry.java +++ b/src/org/rascalmpl/uri/remote/RemoteExternalResolverRegistry.java @@ -124,28 +124,75 @@ private void connect() { private void scheduleReconnect() { CompletableFuture.runAsync(() -> connect()); } + + @FunctionalInterface + private interface ThrowingSupplier { + T get() throws E; + } + @FunctionalInterface + private interface ThrowingFunction { + R apply(T t) throws E; + } + + @FunctionalInterface + private interface ThrowingTriFunction { + R apply(T t, U u, V v) throws E; + } + + @FunctionalInterface + private interface ThrowingConsumer { + void accept(T t) throws E; + } + + @FunctionalInterface + private interface ThrowingTriConsumer { + void accept(T t, U u, V v) throws E; + } + + @FunctionalInterface + private interface ThrowingRunnable { + void run() throws E; + } + private InputStream errorDetectingInputStream(InputStream original) { return new InputStream() { - @Override - public int read() throws IOException { + private T socketExceptionCatcher(ThrowingSupplier function) throws IOException { try { - return original.read(); + return function.get(); + } catch (SocketException e) { + scheduleReconnect(); + throw e; + } + } + + private R socketExceptionCatcher(ThrowingFunction function, T arg) throws IOException { + try { + return function.apply(arg); } catch (SocketException e) { scheduleReconnect(); throw e; } } - @Override - public int read(byte[] b, int off, int len) throws IOException { + private R socketExceptionCatcher(ThrowingTriFunction function, T t, U u, V v) throws IOException { try { - return original.read(b, off, len); + return function.apply(t, u, v); } catch (SocketException e) { scheduleReconnect(); throw e; } } + + @Override + public int read() throws IOException { + return socketExceptionCatcher(original::read); + } + + @Override + public int read(byte[] b, int off, int len) throws IOException { + return socketExceptionCatcher(original::read, b, off, len); + } @Override public int available() throws IOException { @@ -154,12 +201,7 @@ public int available() throws IOException { @Override public long skip(long n) throws IOException { - try { - return original.skip(n); - } catch (SocketException e) { - scheduleReconnect(); - throw e; - } + return socketExceptionCatcher(original::skip, n); } @Override @@ -169,58 +211,60 @@ public void close() throws IOException { @Override public byte[] readNBytes(int len) throws IOException { - try { - return original.readNBytes(len); - } catch (SocketException e) { - scheduleReconnect(); - throw e; - } + return socketExceptionCatcher(original::readNBytes, len); } @Override public int readNBytes(byte[] b, int off, int len) throws IOException { - try { - return original.readNBytes(b, off, len); - } catch (SocketException e) { - scheduleReconnect(); - throw e; - } + return socketExceptionCatcher(original::readNBytes, b, off, len); } }; } private OutputStream errorDetectingOutputStream(OutputStream original) { return new OutputStream() { - @Override - public void write(int b) throws IOException { + private void socketExceptionCatcher(ThrowingConsumer consumer, T arg) throws IOException { try { - original.write(b); + consumer.accept(arg); } catch (SocketException e) { scheduleReconnect(); throw e; } } - - @Override - public void write(byte[] b, int off, int len) throws IOException { + + private void socketExceptionCatcher(ThrowingTriConsumer consumer, T t, U u, V v) throws IOException { try { - original.write(b, off, len); + consumer.accept(t, u, v); } catch (SocketException e) { scheduleReconnect(); throw e; } } - @Override - public void flush() throws IOException { + private void socketExceptionCatcher(ThrowingRunnable runnable) throws IOException { try { - original.flush(); + runnable.run(); } catch (SocketException e) { scheduleReconnect(); throw e; } } + @Override + public void write(int b) throws IOException { + socketExceptionCatcher(original::write, b); + } + + @Override + public void write(byte[] b, int off, int len) throws IOException { + socketExceptionCatcher(original::write, b, off, len); + } + + @Override + public void flush() throws IOException { + socketExceptionCatcher(original::flush); + } + @Override public void close() throws IOException { original.close(); From cd94881d6a679df7fc84c39ab2ac8073c814c9d3 Mon Sep 17 00:00:00 2001 From: Rodin Aarssen Date: Wed, 1 Apr 2026 12:41:02 +0200 Subject: [PATCH 58/98] Removed unnecessary block scope --- .../uri/remote/RemoteExternalResolverRegistry.java | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/org/rascalmpl/uri/remote/RemoteExternalResolverRegistry.java b/src/org/rascalmpl/uri/remote/RemoteExternalResolverRegistry.java index afc062c91e7..f2c5102b83a 100644 --- a/src/org/rascalmpl/uri/remote/RemoteExternalResolverRegistry.java +++ b/src/org/rascalmpl/uri/remote/RemoteExternalResolverRegistry.java @@ -112,11 +112,10 @@ private void connect() { if (remote != null) { this.remote = remote; return; - } else { - timeout = timeout.plusMillis(10); - if (timeout.compareTo(LONGEST_TIMEOUT) >= 0) { - timeout = LONGEST_TIMEOUT; - } + } + timeout = timeout.plusMillis(10); + if (timeout.compareTo(LONGEST_TIMEOUT) >= 0) { + timeout = LONGEST_TIMEOUT; } } } From 9c29f7737b85dfd447270731984bc8229ba2a3b2 Mon Sep 17 00:00:00 2001 From: Rodin Aarssen Date: Wed, 1 Apr 2026 12:47:17 +0200 Subject: [PATCH 59/98] Using @JsonSegment for cleaner jsonrpc interface defintions --- .../vfs/IRemoteResolverRegistryClient.java | 4 +- .../vfs/IRemoteResolverRegistryServer.java | 42 ++++++++++--------- 2 files changed, 25 insertions(+), 21 deletions(-) diff --git a/src/org/rascalmpl/uri/vfs/IRemoteResolverRegistryClient.java b/src/org/rascalmpl/uri/vfs/IRemoteResolverRegistryClient.java index f0ea0d91b55..f03296d22a5 100644 --- a/src/org/rascalmpl/uri/vfs/IRemoteResolverRegistryClient.java +++ b/src/org/rascalmpl/uri/vfs/IRemoteResolverRegistryClient.java @@ -1,9 +1,11 @@ package org.rascalmpl.uri.vfs; import org.eclipse.lsp4j.jsonrpc.services.JsonNotification; +import org.eclipse.lsp4j.jsonrpc.services.JsonSegment; import org.rascalmpl.uri.remote.jsonrpc.ISourceLocationChanged; +@JsonSegment("rascal/vfs/watcher") public interface IRemoteResolverRegistryClient { - @JsonNotification("rascal/vfs/watcher/sourceLocationChanged") + @JsonNotification void sourceLocationChanged(ISourceLocationChanged changed); } diff --git a/src/org/rascalmpl/uri/vfs/IRemoteResolverRegistryServer.java b/src/org/rascalmpl/uri/vfs/IRemoteResolverRegistryServer.java index 8c4cf0bb5ec..9a4a324a8a2 100644 --- a/src/org/rascalmpl/uri/vfs/IRemoteResolverRegistryServer.java +++ b/src/org/rascalmpl/uri/vfs/IRemoteResolverRegistryServer.java @@ -29,6 +29,7 @@ import java.util.concurrent.CompletableFuture; import org.eclipse.lsp4j.jsonrpc.services.JsonRequest; +import org.eclipse.lsp4j.jsonrpc.services.JsonSegment; import org.eclipse.lsp4j.jsonrpc.validation.NonNull; import org.rascalmpl.uri.FileAttributes; import org.rascalmpl.uri.remote.jsonrpc.BooleanResponse; @@ -43,103 +44,104 @@ import org.rascalmpl.uri.remote.jsonrpc.WatchRequest; import org.rascalmpl.uri.remote.jsonrpc.WriteFileRequest; +@JsonSegment("rascal/vfs/input") public interface IRemoteResolverRegistryServer { - @JsonRequest("rascal/vfs/input/readFile") + @JsonRequest default CompletableFuture readFile(ISourceLocationRequest req) { throw new UnsupportedOperationException(); } - @JsonRequest("rascal/vfs/input/exists") + @JsonRequest default CompletableFuture exists(ISourceLocationRequest req) { throw new UnsupportedOperationException(); } - @JsonRequest("rascal/vfs/input/lastModified") + @JsonRequest default CompletableFuture lastModified(ISourceLocationRequest req) { throw new UnsupportedOperationException(); } - @JsonRequest("rascal/vfs/input/created") + @JsonRequest default CompletableFuture created(ISourceLocationRequest req) { throw new UnsupportedOperationException(); } - @JsonRequest("rascal/vfs/input/isDirectory") + @JsonRequest default CompletableFuture isDirectory(ISourceLocationRequest req) { throw new UnsupportedOperationException(); } - @JsonRequest("rascal/vfs/input/isFile") + @JsonRequest default CompletableFuture isFile(ISourceLocationRequest req) { throw new UnsupportedOperationException(); } - @JsonRequest("rascal/vfs/input/list") + @JsonRequest default CompletableFuture list(ISourceLocationRequest req) { throw new UnsupportedOperationException(); } - @JsonRequest("rascal/vfs/input/size") + @JsonRequest default CompletableFuture size(ISourceLocationRequest req) { throw new UnsupportedOperationException(); } - @JsonRequest("rascal/vfs/input/stat") + @JsonRequest default CompletableFuture stat(ISourceLocationRequest req) { throw new UnsupportedOperationException(); } - @JsonRequest("rascal/vfs/input/isReadable") + @JsonRequest default CompletableFuture isReadable(ISourceLocationRequest req) { throw new UnsupportedOperationException(); } - @JsonRequest("rascal/vfs/output/setLastModified") + @JsonRequest default CompletableFuture setLastModified(SetLastModifiedRequest req) { throw new UnsupportedOperationException(); } - @JsonRequest("rascal/vfs/input/isWritable") + @JsonRequest default CompletableFuture isWritable(ISourceLocationRequest req) { throw new UnsupportedOperationException(); } - @JsonRequest("rascal/vfs/output/writeFile") + @JsonRequest default CompletableFuture writeFile(WriteFileRequest req) { throw new UnsupportedOperationException(); } - @JsonRequest("rascal/vfs/output/mkDirectory") + @JsonRequest default CompletableFuture mkDirectory(ISourceLocationRequest req) { throw new UnsupportedOperationException(); } - @JsonRequest("rascal/vfs/output/remove") + @JsonRequest default CompletableFuture remove(RemoveRequest req) { throw new UnsupportedOperationException(); } - @JsonRequest("rascal/vfs/output/rename") + @JsonRequest default CompletableFuture rename(RenameRequest req) { throw new UnsupportedOperationException(); } - @JsonRequest("rascal/vfs/watcher/watch") + @JsonRequest default CompletableFuture watch(WatchRequest req) { throw new UnsupportedOperationException(); } - @JsonRequest("rascal/vfs/watcher/unwatch") + @JsonRequest default CompletableFuture unwatch(WatchRequest req) { throw new UnsupportedOperationException(); } - @JsonRequest("rascal/vfs/watcher/supportsRecursiveWatch") + @JsonRequest default CompletableFuture supportsRecursiveWatch() { throw new UnsupportedOperationException(); } - @JsonRequest("rascal/vfs/logical/resolveLocation") + @JsonRequest default CompletableFuture resolveLocation(ISourceLocationRequest loc) { throw new UnsupportedOperationException(); } From 3e452a848a2bccd8fea2db71bcec0f359e4d52a1 Mon Sep 17 00:00:00 2001 From: Rodin Aarssen Date: Wed, 1 Apr 2026 12:56:26 +0200 Subject: [PATCH 60/98] Removed unnecessary null check --- .../uri/remote/RemoteExternalResolverRegistry.java | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/org/rascalmpl/uri/remote/RemoteExternalResolverRegistry.java b/src/org/rascalmpl/uri/remote/RemoteExternalResolverRegistry.java index f2c5102b83a..35a832238b2 100644 --- a/src/org/rascalmpl/uri/remote/RemoteExternalResolverRegistry.java +++ b/src/org/rascalmpl/uri/remote/RemoteExternalResolverRegistry.java @@ -303,10 +303,8 @@ private static U call(Function> function, T argum throw new UnsupportedOperationException("Thread should have been interrupted"); } catch (CompletionException | ExecutionException e) { var cause = e.getCause(); - if (cause != null) { - if (cause instanceof ResponseErrorException) { - throw translateException((ResponseErrorException) cause); - } + if (cause instanceof ResponseErrorException) { + throw translateException((ResponseErrorException) cause); } throw new IOException(e); } From dd5c50f2feabeb62a980a13f9e1987563d310f7b Mon Sep 17 00:00:00 2001 From: Rodin Aarssen Date: Wed, 1 Apr 2026 12:57:02 +0200 Subject: [PATCH 61/98] Removed REPL prints during remote watch/unwatch calls --- .../rascalmpl/uri/remote/RemoteExternalResolverRegistry.java | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/org/rascalmpl/uri/remote/RemoteExternalResolverRegistry.java b/src/org/rascalmpl/uri/remote/RemoteExternalResolverRegistry.java index 35a832238b2..d07c2fdf706 100644 --- a/src/org/rascalmpl/uri/remote/RemoteExternalResolverRegistry.java +++ b/src/org/rascalmpl/uri/remote/RemoteExternalResolverRegistry.java @@ -496,7 +496,6 @@ public void watch(ISourceLocation root, Consumer watcher synchronized (watchers) { var key = new WatchSubscriptionKey(root, recursive); if (!watchers.containsKey(key)) { - System.err.println("Fresh watch, setting up request to server"); var freshWatchers = new Watchers(); freshWatchers.addNewWatcher(watcher); watchersById.put(freshWatchers.getId(), freshWatchers); @@ -516,10 +515,8 @@ public void unwatch(ISourceLocation root, Consumer watch synchronized (watchers) { var watch = watchers.get(watchKey); if (watch != null && watch.removeWatcher(watcher)) { - System.err.println("No other watchers registered, so unregistering at server"); watchers.remove(watchKey); if (!watch.getCallbacks().isEmpty()) { - System.err.println("Raced by another thread, canceling unregister"); watchers.put(watchKey, watch); return; } From ddae704ced67de5b12eeee6f22aaab913458cc5c Mon Sep 17 00:00:00 2001 From: Rodin Aarssen Date: Wed, 1 Apr 2026 13:12:56 +0200 Subject: [PATCH 62/98] RemoteExternalResolverRegistry now synchronizes on the directory listing cache --- .../RemoteExternalResolverRegistry.java | 56 +++++++++++-------- 1 file changed, 34 insertions(+), 22 deletions(-) diff --git a/src/org/rascalmpl/uri/remote/RemoteExternalResolverRegistry.java b/src/org/rascalmpl/uri/remote/RemoteExternalResolverRegistry.java index d07c2fdf706..e1b33e69efb 100644 --- a/src/org/rascalmpl/uri/remote/RemoteExternalResolverRegistry.java +++ b/src/org/rascalmpl/uri/remote/RemoteExternalResolverRegistry.java @@ -377,11 +377,13 @@ public long size(ISourceLocation loc) throws IOException { @Override public boolean isDirectory(ISourceLocation loc) { try { - var cached = cachedDirectoryListing.getIfPresent(URIUtil.getParentLocation(loc)); - if (cached != null) { - var result = cached.get().get(URIUtil.getLocationName(loc)); - if (result != null) { - return result; + synchronized (cachedDirectoryListing) { + var cached = cachedDirectoryListing.getIfPresent(URIUtil.getParentLocation(loc)); + if (cached != null) { + var result = cached.get().get(URIUtil.getLocationName(loc)); + if (result != null) { + return result; + } } } return call(remote::isDirectory, new ISourceLocationRequest(loc)).getValue(); @@ -393,11 +395,13 @@ public boolean isDirectory(ISourceLocation loc) { @Override public boolean isFile(ISourceLocation loc) { try { - var cached = cachedDirectoryListing.getIfPresent(URIUtil.getParentLocation(loc)); - if (cached != null) { - var result = cached.get().get(URIUtil.getLocationName(loc)); - if (result != null) { - return !result; + synchronized (cachedDirectoryListing) { + var cached = cachedDirectoryListing.getIfPresent(URIUtil.getParentLocation(loc)); + if (cached != null) { + var result = cached.get().get(URIUtil.getLocationName(loc)); + if (result != null) { + return !result; + } } } return call(remote::isFile, new ISourceLocationRequest(loc)).getValue(); @@ -424,11 +428,13 @@ public boolean isReadable(ISourceLocation loc) throws IOException { @Override public String[] list(ISourceLocation loc) throws IOException { - var result = call(remote::list, new ISourceLocationRequest(loc)); - cachedDirectoryListing.put(loc, Lazy.defer(() -> { - return Stream.of(result).collect(Collectors.toMap(FileWithType::getName, e -> e.getType() == FileType.Directory)); - })); - return Stream.of(result).map(FileWithType::getName).toArray(String[]::new); + synchronized (cachedDirectoryListing) { + var result = call(remote::list, new ISourceLocationRequest(loc)); + cachedDirectoryListing.put(loc, Lazy.defer(() -> { + return Stream.of(result).collect(Collectors.toMap(FileWithType::getName, e -> e.getType() == FileType.Directory)); + })); + return Stream.of(result).map(FileWithType::getName).toArray(String[]::new); + } } @Override @@ -456,23 +462,29 @@ public void close() throws IOException { try (var input = new ByteArrayInputStream(this.toByteArray())) { StreamingBase64.encode(input, content, true); } - call(remote::writeFile, new WriteFileRequest(loc, content.toString(), append)); - cachedDirectoryListing.invalidate(URIUtil.getParentLocation(loc)); + synchronized (cachedDirectoryListing) { + call(remote::writeFile, new WriteFileRequest(loc, content.toString(), append)); + cachedDirectoryListing.invalidate(URIUtil.getParentLocation(loc)); + } } }; } @Override public void mkDirectory(ISourceLocation loc) throws IOException { - call(remote::mkDirectory, new ISourceLocationRequest(loc)); - cachedDirectoryListing.invalidate(URIUtil.getParentLocation(loc)); + synchronized (cachedDirectoryListing) { + call(remote::mkDirectory, new ISourceLocationRequest(loc)); + cachedDirectoryListing.invalidate(URIUtil.getParentLocation(loc)); + } } @Override public void remove(ISourceLocation loc) throws IOException { - call(remote::remove, new RemoveRequest(loc, true)); - cachedDirectoryListing.invalidate(loc); - cachedDirectoryListing.invalidate(URIUtil.getParentLocation(loc)); + synchronized (cachedDirectoryListing) { + call(remote::remove, new RemoveRequest(loc, true)); + cachedDirectoryListing.invalidate(loc); + cachedDirectoryListing.invalidate(URIUtil.getParentLocation(loc)); + } } @Override From 87240ddf08b7dfade3cacb466653783fdd039fdb Mon Sep 17 00:00:00 2001 From: Rodin Aarssen Date: Wed, 1 Apr 2026 16:37:20 +0200 Subject: [PATCH 63/98] Fixed jsonrpc method names --- .../vfs/IRemoteResolverRegistryServer.java | 42 +++++++++---------- 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/src/org/rascalmpl/uri/vfs/IRemoteResolverRegistryServer.java b/src/org/rascalmpl/uri/vfs/IRemoteResolverRegistryServer.java index 9a4a324a8a2..14f79188933 100644 --- a/src/org/rascalmpl/uri/vfs/IRemoteResolverRegistryServer.java +++ b/src/org/rascalmpl/uri/vfs/IRemoteResolverRegistryServer.java @@ -44,104 +44,104 @@ import org.rascalmpl.uri.remote.jsonrpc.WatchRequest; import org.rascalmpl.uri.remote.jsonrpc.WriteFileRequest; -@JsonSegment("rascal/vfs/input") +@JsonSegment("rascal/vfs") public interface IRemoteResolverRegistryServer { - @JsonRequest + @JsonRequest("input/readFile") default CompletableFuture readFile(ISourceLocationRequest req) { throw new UnsupportedOperationException(); } - @JsonRequest + @JsonRequest("input/exists") default CompletableFuture exists(ISourceLocationRequest req) { throw new UnsupportedOperationException(); } - @JsonRequest + @JsonRequest("input/lastModified") default CompletableFuture lastModified(ISourceLocationRequest req) { throw new UnsupportedOperationException(); } - @JsonRequest + @JsonRequest("input/created") default CompletableFuture created(ISourceLocationRequest req) { throw new UnsupportedOperationException(); } - @JsonRequest + @JsonRequest("input/isDirectory") default CompletableFuture isDirectory(ISourceLocationRequest req) { throw new UnsupportedOperationException(); } - @JsonRequest + @JsonRequest("input/isFile") default CompletableFuture isFile(ISourceLocationRequest req) { throw new UnsupportedOperationException(); } - @JsonRequest + @JsonRequest("input/list") default CompletableFuture list(ISourceLocationRequest req) { throw new UnsupportedOperationException(); } - @JsonRequest + @JsonRequest("input/size") default CompletableFuture size(ISourceLocationRequest req) { throw new UnsupportedOperationException(); } - @JsonRequest + @JsonRequest("input/stat") default CompletableFuture stat(ISourceLocationRequest req) { throw new UnsupportedOperationException(); } - @JsonRequest + @JsonRequest("input/isReadable") default CompletableFuture isReadable(ISourceLocationRequest req) { throw new UnsupportedOperationException(); } - @JsonRequest + @JsonRequest("output/setLastModified") default CompletableFuture setLastModified(SetLastModifiedRequest req) { throw new UnsupportedOperationException(); } - @JsonRequest + @JsonRequest("output/isWritable") default CompletableFuture isWritable(ISourceLocationRequest req) { throw new UnsupportedOperationException(); } - @JsonRequest + @JsonRequest("output/writeFile") default CompletableFuture writeFile(WriteFileRequest req) { throw new UnsupportedOperationException(); } - @JsonRequest + @JsonRequest("output/mkDirectory") default CompletableFuture mkDirectory(ISourceLocationRequest req) { throw new UnsupportedOperationException(); } - @JsonRequest + @JsonRequest("output/remove") default CompletableFuture remove(RemoveRequest req) { throw new UnsupportedOperationException(); } - @JsonRequest + @JsonRequest("output/rename") default CompletableFuture rename(RenameRequest req) { throw new UnsupportedOperationException(); } - @JsonRequest + @JsonRequest("watcher/watch") default CompletableFuture watch(WatchRequest req) { throw new UnsupportedOperationException(); } - @JsonRequest + @JsonRequest("watcher/unwatch") default CompletableFuture unwatch(WatchRequest req) { throw new UnsupportedOperationException(); } - @JsonRequest + @JsonRequest("watcher/supportsRecursiveWatch") default CompletableFuture supportsRecursiveWatch() { throw new UnsupportedOperationException(); } - @JsonRequest + @JsonRequest("logical/resolveLocation") default CompletableFuture resolveLocation(ISourceLocationRequest loc) { throw new UnsupportedOperationException(); } From 15b59ab7ef6a751edea098355c873aec6917fedd Mon Sep 17 00:00:00 2001 From: Rodin Aarssen Date: Thu, 2 Apr 2026 11:33:01 +0200 Subject: [PATCH 64/98] Javadoc --- src/org/rascalmpl/uri/remote/RascalFileSystemServices.java | 3 +++ .../rascalmpl/uri/remote/RemoteExternalResolverRegistry.java | 5 ++++- src/org/rascalmpl/uri/vfs/IRemoteResolverRegistryClient.java | 3 +++ src/org/rascalmpl/uri/vfs/IRemoteResolverRegistryServer.java | 3 +++ 4 files changed, 13 insertions(+), 1 deletion(-) diff --git a/src/org/rascalmpl/uri/remote/RascalFileSystemServices.java b/src/org/rascalmpl/uri/remote/RascalFileSystemServices.java index bf9762b4979..b259210db7a 100644 --- a/src/org/rascalmpl/uri/remote/RascalFileSystemServices.java +++ b/src/org/rascalmpl/uri/remote/RascalFileSystemServices.java @@ -58,6 +58,9 @@ import io.usethesource.vallang.ISourceLocation; +/** + * RascalFileSystemServices offers remote access to the Rascal file system. + */ public class RascalFileSystemServices implements IRemoteResolverRegistryServer { private static final URIResolverRegistry reg = URIResolverRegistry.getInstance(); private static final ExecutorService executor = NamedThreadPool.cachedDaemon("rascal-vfs"); diff --git a/src/org/rascalmpl/uri/remote/RemoteExternalResolverRegistry.java b/src/org/rascalmpl/uri/remote/RemoteExternalResolverRegistry.java index e1b33e69efb..8267f0d7597 100644 --- a/src/org/rascalmpl/uri/remote/RemoteExternalResolverRegistry.java +++ b/src/org/rascalmpl/uri/remote/RemoteExternalResolverRegistry.java @@ -85,6 +85,9 @@ import io.usethesource.vallang.ISourceLocation; +/** + * Default implementation for access to a remote file system. + */ public class RemoteExternalResolverRegistry implements IExternalResolverRegistry, IRemoteResolverRegistryClient { private volatile IRemoteResolverRegistryServer remote = null; @@ -417,7 +420,7 @@ public boolean isReadable(ISourceLocation loc) throws IOException { /** * Rascal's current implementions sometimes ask for a directory listing and then iterate over all entries - * checking whether they are a directory. This is very slow for jsonrcp, so we store the last directory listing + * checking whether they are a directory. This is very slow for JSON-RPC, so we store the last directory listing * and check the cache first */ private final Cache>> cachedDirectoryListing diff --git a/src/org/rascalmpl/uri/vfs/IRemoteResolverRegistryClient.java b/src/org/rascalmpl/uri/vfs/IRemoteResolverRegistryClient.java index f03296d22a5..a3b53fa4601 100644 --- a/src/org/rascalmpl/uri/vfs/IRemoteResolverRegistryClient.java +++ b/src/org/rascalmpl/uri/vfs/IRemoteResolverRegistryClient.java @@ -4,6 +4,9 @@ import org.eclipse.lsp4j.jsonrpc.services.JsonSegment; import org.rascalmpl.uri.remote.jsonrpc.ISourceLocationChanged; +/** + * This interface defines the JSON-RPC interface for callbacks related to remote file systems. + */ @JsonSegment("rascal/vfs/watcher") public interface IRemoteResolverRegistryClient { @JsonNotification diff --git a/src/org/rascalmpl/uri/vfs/IRemoteResolverRegistryServer.java b/src/org/rascalmpl/uri/vfs/IRemoteResolverRegistryServer.java index 14f79188933..cffc79c23c0 100644 --- a/src/org/rascalmpl/uri/vfs/IRemoteResolverRegistryServer.java +++ b/src/org/rascalmpl/uri/vfs/IRemoteResolverRegistryServer.java @@ -44,6 +44,9 @@ import org.rascalmpl.uri.remote.jsonrpc.WatchRequest; import org.rascalmpl.uri.remote.jsonrpc.WriteFileRequest; +/** + * This interface defines the JSON-RPC interface for remote access to the Rascal file system. + */ @JsonSegment("rascal/vfs") public interface IRemoteResolverRegistryServer { @JsonRequest("input/readFile") From e186aa0e876e2fd001add12e473f588978522168 Mon Sep 17 00:00:00 2001 From: Rodin Aarssen Date: Fri, 3 Apr 2026 13:27:29 +0200 Subject: [PATCH 65/98] The JsonValueWriter option fileLocationsAsPathOnly is now exposed as a setting on the Rascal side --- src/org/rascalmpl/library/Content.rsc | 2 +- src/org/rascalmpl/library/lang/json/IO.java | 6 ++++-- src/org/rascalmpl/library/lang/json/IO.rsc | 5 +++-- .../lang/rascal/tests/library/lang/json/JSONIOTests.rsc | 5 +++-- src/org/rascalmpl/library/util/TermREPL.java | 2 ++ src/org/rascalmpl/library/util/Webserver.java | 2 ++ src/org/rascalmpl/repl/http/REPLContentServer.java | 2 ++ 7 files changed, 17 insertions(+), 7 deletions(-) diff --git a/src/org/rascalmpl/library/Content.rsc b/src/org/rascalmpl/library/Content.rsc index f7615a63368..9a8299f380e 100644 --- a/src/org/rascalmpl/library/Content.rsc +++ b/src/org/rascalmpl/library/Content.rsc @@ -82,7 +82,7 @@ data Response = response(Status status, str mimeType, map[str,str] header, str content) | fileResponse(loc file, str mimeType, map[str,str] header) | jsonResponse(Status status, map[str,str] header, value val, str dateTimeFormat = "yyyy-MM-dd\'T\'HH:mm:ss\'Z\'", JSONFormatter[value] formatter = str (value _) { fail; }, - bool explicitConstructorNames=false, bool explicitDataTypes=false, bool dateTimeAsInt=true, bool rationalsAsString=false) + bool explicitConstructorNames=false, bool explicitDataTypes=false, bool dateTimeAsInt=true, bool rationalsAsString=false, bool fileLocationsAsPathOnly=true) ; @synopsis{Utility to quickly render a string as HTML content} diff --git a/src/org/rascalmpl/library/lang/json/IO.java b/src/org/rascalmpl/library/lang/json/IO.java index e5b91d89fda..7075077564d 100644 --- a/src/org/rascalmpl/library/lang/json/IO.java +++ b/src/org/rascalmpl/library/lang/json/IO.java @@ -106,7 +106,7 @@ public IValue parseJSON(IValue type, IString src, IString dateTimeFormat, IBool public void writeJSON(ISourceLocation loc, IValue value, IBool unpackedLocations, IString dateTimeFormat, IBool dateTimeAsInt, IBool rationalsAsString, IInteger indent, IBool dropOrigins, IFunction formatter, IBool explicitConstructorNames, - IBool explicitDataTypes) { + IBool explicitDataTypes, IBool fileLocationsAsPathOnly) { try (JsonWriter out = new JsonWriter(new OutputStreamWriter(URIResolverRegistry.getInstance().getOutputStream(loc, false), Charset.forName("UTF8")))) { @@ -123,6 +123,7 @@ public void writeJSON(ISourceLocation loc, IValue value, IBool unpackedLocations .setFormatters(formatter) .setExplicitConstructorNames(explicitConstructorNames.getValue()) .setExplicitDataTypes(explicitDataTypes.getValue()) + .setFileLocationsAsPathOnly(fileLocationsAsPathOnly.getValue()) .write(out, value); } catch (IOException e) { @@ -132,7 +133,7 @@ public void writeJSON(ISourceLocation loc, IValue value, IBool unpackedLocations public IString asJSON(IValue value, IBool unpackedLocations, IString dateTimeFormat, IBool dateTimeAsInt, IBool rationalsAsString, IInteger indent, IBool dropOrigins, IFunction formatter, IBool explicitConstructorNames, - IBool explicitDataTypes) { + IBool explicitDataTypes, IBool fileLocationsAsPathOnly) { StringWriter string = new StringWriter(); try (JsonWriter out = new JsonWriter(string)) { @@ -148,6 +149,7 @@ public IString asJSON(IValue value, IBool unpackedLocations, IString dateTimeFor .setFormatters(formatter) .setExplicitConstructorNames(explicitConstructorNames.getValue()) .setExplicitDataTypes(explicitDataTypes.getValue()) + .setFileLocationsAsPathOnly(fileLocationsAsPathOnly.getValue()) .write(out, value); return values.string(string.toString()); diff --git a/src/org/rascalmpl/library/lang/json/IO.rsc b/src/org/rascalmpl/library/lang/json/IO.rsc index 7b177097029..b5c5066508e 100644 --- a/src/org/rascalmpl/library/lang/json/IO.rsc +++ b/src/org/rascalmpl/library/lang/json/IO.rsc @@ -175,7 +175,8 @@ java void writeJSON(loc target, value val, bool dropOrigins=true, JSONFormatter[value] formatter = str (value _) { fail; }, bool explicitConstructorNames=false, - bool explicitDataTypes=false + bool explicitDataTypes=false, + bool fileLocationsAsPathOnly=true ); @javaClass{org.rascalmpl.library.lang.json.IO} @@ -184,7 +185,7 @@ java void writeJSON(loc target, value val, @description{ This function uses `writeJSON` and stores the result in a string. } -java str asJSON(value val, bool unpackedLocations=false, str dateTimeFormat=DEFAULT_DATETIME_FORMAT, bool dateTimeAsInt=false, bool rationalsAsString=false, int indent = 0, bool dropOrigins=true, JSONFormatter[value] formatter = str (value _) { fail; }, bool explicitConstructorNames=false, bool explicitDataTypes=false); +java str asJSON(value val, bool unpackedLocations=false, str dateTimeFormat=DEFAULT_DATETIME_FORMAT, bool dateTimeAsInt=false, bool rationalsAsString=false, int indent = 0, bool dropOrigins=true, JSONFormatter[value] formatter = str (value _) { fail; }, bool explicitConstructorNames=false, bool explicitDataTypes=false, bool fileLocationsAsPathOnly=true); @synopsis{((writeJSON)) and ((asJSON)) uses `Formatter` functions to flatten structured data to strings, on-demand} @description{ diff --git a/src/org/rascalmpl/library/lang/rascal/tests/library/lang/json/JSONIOTests.rsc b/src/org/rascalmpl/library/lang/rascal/tests/library/lang/json/JSONIOTests.rsc index 65602a9e73b..350a355e7fc 100644 --- a/src/org/rascalmpl/library/lang/rascal/tests/library/lang/json/JSONIOTests.rsc +++ b/src/org/rascalmpl/library/lang/rascal/tests/library/lang/json/JSONIOTests.rsc @@ -14,8 +14,8 @@ import Node; loc targetFile = |memory://test-tmp/test-<"">.json|; public int maxLong = floor(pow(2,63)); -bool writeRead(type[&T] returnType, &T dt, value (value x) normalizer = value(value x) { return x; }, bool dateTimeAsInt=false, bool rationalsAsString=false, bool unpackedLocations=false, bool explicitConstructorNames=false, bool explicitDataTypes=false) { - json = asJSON(dt, dateTimeAsInt=dateTimeAsInt, rationalsAsString=rationalsAsString, unpackedLocations=unpackedLocations, explicitConstructorNames=explicitConstructorNames, explicitDataTypes=explicitDataTypes); +bool writeRead(type[&T] returnType, &T dt, value (value x) normalizer = value(value x) { return x; }, bool dateTimeAsInt=false, bool rationalsAsString=false, bool unpackedLocations=false, bool explicitConstructorNames=false, bool explicitDataTypes=false, bool fileLocationsAsPathOnly=true) { + json = asJSON(dt, dateTimeAsInt=dateTimeAsInt, rationalsAsString=rationalsAsString, unpackedLocations=unpackedLocations, explicitConstructorNames=explicitConstructorNames, explicitDataTypes=explicitDataTypes, fileLocationsAsPathOnly=fileLocationsAsPathOnly); readBack = normalizer(parseJSON(returnType, json, explicitConstructorNames=explicitConstructorNames, explicitDataTypes=explicitDataTypes)); if (readBack !:= normalizer(dt) /* ignores additional src fields */) { println("What is read back, a :"); @@ -59,6 +59,7 @@ test bool jsonWithNum1(num dt) = writeRead(#num, dt, normalizer=numNormalizer); test bool jsonWithLoc1(loc dt) = writeRead(#loc, dt); test bool jsonWithLoc2(loc dt) = writeRead(#loc, dt, unpackedLocations=true); +test bool jsonWithLoc3(loc dt) = writeRead(#loc, dt, fileLocationsAsPathOnly=false); test bool jsonWithStr1(str dt) = writeRead(#str, dt); test bool jsonWithDatetime1(datetime dt) = writeRead(#datetime, dt); test bool jsonWithDatetime2(datetime dt) = writeRead(#datetime, dt, dateTimeAsInt=true); diff --git a/src/org/rascalmpl/library/util/TermREPL.java b/src/org/rascalmpl/library/util/TermREPL.java index a98798b79ea..54d0854126a 100644 --- a/src/org/rascalmpl/library/util/TermREPL.java +++ b/src/org/rascalmpl/library/util/TermREPL.java @@ -286,6 +286,7 @@ private ICommandOutput handleJSONResponse(IConstructor response) { IValue formatters = kws.getParameter("formatter"); IValue ecn = kws.getParameter("explicitConstructorNames"); IValue edt = kws.getParameter("explicitDataTypes"); + IValue fpo = kws.getParameter("fileLocationsAsPathOnly"); JsonValueWriter writer = new JsonValueWriter() .setCalendarFormat(dtf != null ? ((IString) dtf).getValue() : "yyyy-MM-dd\'T\'HH:mm:ss\'Z\'") @@ -294,6 +295,7 @@ private ICommandOutput handleJSONResponse(IConstructor response) { .setRationalsAsString(ras != null ? ((IBool) ras).getValue() : false) .setExplicitConstructorNames(ecn != null ? ((IBool) ecn).getValue() : false) .setExplicitDataTypes(edt != null ? ((IBool) edt).getValue() : false) + .setFileLocationsAsPathOnly(edt != null ? ((IBool) fpo).getValue() : true) ; return new ICommandOutput() { diff --git a/src/org/rascalmpl/library/util/Webserver.java b/src/org/rascalmpl/library/util/Webserver.java index 5dd103a0b5e..e39689c5e4c 100644 --- a/src/org/rascalmpl/library/util/Webserver.java +++ b/src/org/rascalmpl/library/util/Webserver.java @@ -245,6 +245,7 @@ private Response translateJsonResponse(Method method, IConstructor cons) { IValue formatters = kws.getParameter("formatter"); IValue ecn = kws.getParameter("explicitConstructorNames"); IValue edt = kws.getParameter("explicitDataTypes"); + IValue fpo = kws.getParameter("fileLocationsAsPathOnly"); JsonValueWriter writer = new JsonValueWriter() .setCalendarFormat(dtf != null ? ((IString) dtf).getValue() : "yyyy-MM-dd\'T\'HH:mm:ss\'Z\'") @@ -253,6 +254,7 @@ private Response translateJsonResponse(Method method, IConstructor cons) { .setRationalsAsString(ras != null ? ((IBool) ras).getValue() : false) .setExplicitConstructorNames(ecn != null ? ((IBool) ecn).getValue() : false) .setExplicitDataTypes(edt != null ? ((IBool) edt).getValue() : false) + .setFileLocationsAsPathOnly(fpo != null ? ((IBool) fpo).getValue() : true) ; try { diff --git a/src/org/rascalmpl/repl/http/REPLContentServer.java b/src/org/rascalmpl/repl/http/REPLContentServer.java index 498517f36ba..f9add746e92 100644 --- a/src/org/rascalmpl/repl/http/REPLContentServer.java +++ b/src/org/rascalmpl/repl/http/REPLContentServer.java @@ -167,6 +167,7 @@ private static Response translateJsonResponse(Method method, IConstructor cons) IValue formatters = kws.getParameter("formatter"); IValue ecn = kws.getParameter("explicitConstructorNames"); IValue edt = kws.getParameter("explicitDataTypes"); + IValue fpo = kws.getParameter("fileLocationsAsPathOnly"); JsonValueWriter writer = new JsonValueWriter() .setCalendarFormat(dtf != null ? ((IString) dtf).getValue() : "yyyy-MM-dd\'T\'HH:mm:ss\'Z\'") @@ -175,6 +176,7 @@ private static Response translateJsonResponse(Method method, IConstructor cons) .setRationalsAsString(ras != null ? ((IBool) ras).getValue() : false) .setExplicitConstructorNames(ecn != null ? ((IBool) ecn).getValue() : false) .setExplicitDataTypes(edt != null ? ((IBool) edt).getValue() : false) + .setFileLocationsAsPathOnly(fpo != null ? ((IBool) fpo).getValue() : true); ; try { From b44fce18cba917e72c173441ea90ef6dce7640e9 Mon Sep 17 00:00:00 2001 From: Rodin Aarssen Date: Fri, 3 Apr 2026 13:39:04 +0200 Subject: [PATCH 66/98] Added missing equals/hashcode implementations for several request/response classes --- .../uri/remote/jsonrpc/BooleanResponse.java | 15 +++++++++++++++ .../remote/jsonrpc/ISourceLocationRequest.java | 13 +++++++------ .../remote/jsonrpc/LocationContentResponse.java | 15 +++++++++++++++ .../uri/remote/jsonrpc/NumberResponse.java | 15 +++++++++++++++ .../remote/jsonrpc/SourceLocationResponse.java | 15 +++++++++++++++ .../uri/remote/jsonrpc/TimestampResponse.java | 15 +++++++++++++++ 6 files changed, 82 insertions(+), 6 deletions(-) diff --git a/src/org/rascalmpl/uri/remote/jsonrpc/BooleanResponse.java b/src/org/rascalmpl/uri/remote/jsonrpc/BooleanResponse.java index 9861074f086..2f46ebb8743 100644 --- a/src/org/rascalmpl/uri/remote/jsonrpc/BooleanResponse.java +++ b/src/org/rascalmpl/uri/remote/jsonrpc/BooleanResponse.java @@ -26,6 +26,8 @@ */ package org.rascalmpl.uri.remote.jsonrpc; +import java.util.Objects; + public class BooleanResponse { private final boolean value; @@ -36,4 +38,17 @@ public BooleanResponse(boolean value) { public boolean getValue() { return value; } + + @Override + public boolean equals(Object obj) { + if (obj instanceof BooleanResponse) { + return value == ((BooleanResponse)obj).value; + } + return false; + } + + @Override + public int hashCode() { + return Objects.hash(value); + } } diff --git a/src/org/rascalmpl/uri/remote/jsonrpc/ISourceLocationRequest.java b/src/org/rascalmpl/uri/remote/jsonrpc/ISourceLocationRequest.java index 91ade0b861e..e1370538e1f 100644 --- a/src/org/rascalmpl/uri/remote/jsonrpc/ISourceLocationRequest.java +++ b/src/org/rascalmpl/uri/remote/jsonrpc/ISourceLocationRequest.java @@ -26,6 +26,8 @@ */ package org.rascalmpl.uri.remote.jsonrpc; +import java.util.Objects; + import org.checkerframework.checker.nullness.qual.Nullable; import org.eclipse.lsp4j.jsonrpc.validation.NonNull; @@ -35,11 +37,14 @@ public class ISourceLocationRequest { @NonNull private ISourceLocation loc; - public ISourceLocationRequest(ISourceLocation loc) { this.loc = loc; } + public ISourceLocation getLocation() { + return loc; + } + @Override public boolean equals(@Nullable Object obj) { if (obj instanceof ISourceLocationRequest) { @@ -50,10 +55,6 @@ public boolean equals(@Nullable Object obj) { @Override public int hashCode() { - return 7 * loc.hashCode(); - } - - public ISourceLocation getLocation() { - return loc; + return Objects.hash(loc.hashCode()); } } diff --git a/src/org/rascalmpl/uri/remote/jsonrpc/LocationContentResponse.java b/src/org/rascalmpl/uri/remote/jsonrpc/LocationContentResponse.java index 57d32837d0c..00670a948fc 100644 --- a/src/org/rascalmpl/uri/remote/jsonrpc/LocationContentResponse.java +++ b/src/org/rascalmpl/uri/remote/jsonrpc/LocationContentResponse.java @@ -26,6 +26,8 @@ */ package org.rascalmpl.uri.remote.jsonrpc; +import java.util.Objects; + import org.checkerframework.checker.nullness.qual.NonNull; public class LocationContentResponse { @@ -39,4 +41,17 @@ public LocationContentResponse(@NonNull String content) { public String getContent() { return content; } + + @Override + public boolean equals(Object obj) { + if (obj instanceof LocationContentResponse) { + return content.equals(((LocationContentResponse)obj).content); + } + return false; + } + + @Override + public int hashCode() { + return Objects.hash(content); + } } diff --git a/src/org/rascalmpl/uri/remote/jsonrpc/NumberResponse.java b/src/org/rascalmpl/uri/remote/jsonrpc/NumberResponse.java index da695f6515d..5569b3f0245 100644 --- a/src/org/rascalmpl/uri/remote/jsonrpc/NumberResponse.java +++ b/src/org/rascalmpl/uri/remote/jsonrpc/NumberResponse.java @@ -26,6 +26,8 @@ */ package org.rascalmpl.uri.remote.jsonrpc; +import java.util.Objects; + public class NumberResponse { private final long number; @@ -36,4 +38,17 @@ public NumberResponse(long number) { public long getNumber() { return number; } + + @Override + public boolean equals(Object obj) { + if (obj instanceof NumberResponse) { + return number == ((NumberResponse)obj).number; + } + return false; + } + + @Override + public int hashCode() { + return Objects.hash(number); + } } diff --git a/src/org/rascalmpl/uri/remote/jsonrpc/SourceLocationResponse.java b/src/org/rascalmpl/uri/remote/jsonrpc/SourceLocationResponse.java index d97c88db499..b24c0fd44b1 100644 --- a/src/org/rascalmpl/uri/remote/jsonrpc/SourceLocationResponse.java +++ b/src/org/rascalmpl/uri/remote/jsonrpc/SourceLocationResponse.java @@ -26,6 +26,8 @@ */ package org.rascalmpl.uri.remote.jsonrpc; +import java.util.Objects; + import org.checkerframework.checker.nullness.qual.NonNull; import io.usethesource.vallang.ISourceLocation; @@ -41,4 +43,17 @@ public SourceLocationResponse(@NonNull ISourceLocation loc) { public ISourceLocation getLocation() { return loc; } + + @Override + public boolean equals(Object obj) { + if (obj instanceof SourceLocationResponse) { + return loc.equals(((SourceLocationResponse)obj).loc); + } + return false; + } + + @Override + public int hashCode() { + return Objects.hash(loc); + } } diff --git a/src/org/rascalmpl/uri/remote/jsonrpc/TimestampResponse.java b/src/org/rascalmpl/uri/remote/jsonrpc/TimestampResponse.java index 894b2c269d4..b3f88e7bdff 100644 --- a/src/org/rascalmpl/uri/remote/jsonrpc/TimestampResponse.java +++ b/src/org/rascalmpl/uri/remote/jsonrpc/TimestampResponse.java @@ -26,6 +26,8 @@ */ package org.rascalmpl.uri.remote.jsonrpc; +import java.util.Objects; + public class TimestampResponse { private final long timestamp; @@ -36,4 +38,17 @@ public TimestampResponse(long timestamp) { public long getTimestamp() { return timestamp; } + + @Override + public boolean equals(Object obj) { + if (obj instanceof TimestampResponse) { + return timestamp == ((TimestampResponse)obj).timestamp; + } + return false; + } + + @Override + public int hashCode() { + return Objects.hash(timestamp); + } } From 290fbf9052cb706de8acaafa3f191169a1c9f18a Mon Sep 17 00:00:00 2001 From: Rodin Aarssen Date: Fri, 3 Apr 2026 13:40:11 +0200 Subject: [PATCH 67/98] Renamed watcher member/parameter to watchId --- .../uri/remote/RascalFileSystemServices.java | 2 +- .../rascalmpl/uri/remote/jsonrpc/WatchRequest.java | 14 +++++++------- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/org/rascalmpl/uri/remote/RascalFileSystemServices.java b/src/org/rascalmpl/uri/remote/RascalFileSystemServices.java index b259210db7a..e33096d7354 100644 --- a/src/org/rascalmpl/uri/remote/RascalFileSystemServices.java +++ b/src/org/rascalmpl/uri/remote/RascalFileSystemServices.java @@ -98,7 +98,7 @@ public CompletableFuture watch(WatchRequest params) { URIResolverRegistry.getInstance().watch(loc, params.isRecursive(), changed -> { client.sourceLocationChanged(new ISourceLocationChanged( - changed.getLocation(), ISourceLocationChangeType.forValue(changed.getChangeType().getValue()), params.getWatcher() + changed.getLocation(), ISourceLocationChangeType.forValue(changed.getChangeType().getValue()), params.getWatchId() )); }); } catch (IOException | RuntimeException e) { diff --git a/src/org/rascalmpl/uri/remote/jsonrpc/WatchRequest.java b/src/org/rascalmpl/uri/remote/jsonrpc/WatchRequest.java index 77358f59d73..a0a48ecee5f 100644 --- a/src/org/rascalmpl/uri/remote/jsonrpc/WatchRequest.java +++ b/src/org/rascalmpl/uri/remote/jsonrpc/WatchRequest.java @@ -35,18 +35,18 @@ public class WatchRequest extends ISourceLocationRequest { @NonNull - private String watcher; + private String watchId; private boolean recursive; - public WatchRequest(ISourceLocation loc, boolean recursive, String watcher) { + public WatchRequest(ISourceLocation loc, boolean recursive, String watchId) { super(loc); this.recursive = recursive; - this.watcher = watcher; + this.watchId = watchId; } - public String getWatcher() { - return watcher; + public String getWatchId() { + return watchId; } public boolean isRecursive() { @@ -59,13 +59,13 @@ public boolean equals(@Nullable Object obj) { var other = (WatchRequest)obj; return super.equals(other) && other.recursive == recursive - && Objects.equals(watcher, other.watcher); + && Objects.equals(watchId, other.watchId); } return false; } @Override public int hashCode() { - return Objects.hash(super.hashCode(), watcher, recursive); + return Objects.hash(super.hashCode(), watchId, recursive); } } From 6fb394b436258463bc475aa7ee7ee9148e8f12f5 Mon Sep 17 00:00:00 2001 From: Rodin Aarssen Date: Fri, 3 Apr 2026 13:40:56 +0200 Subject: [PATCH 68/98] Added comment --- src/org/rascalmpl/uri/remote/RascalFileSystemServices.java | 1 + 1 file changed, 1 insertion(+) diff --git a/src/org/rascalmpl/uri/remote/RascalFileSystemServices.java b/src/org/rascalmpl/uri/remote/RascalFileSystemServices.java index e33096d7354..c586b0467fa 100644 --- a/src/org/rascalmpl/uri/remote/RascalFileSystemServices.java +++ b/src/org/rascalmpl/uri/remote/RascalFileSystemServices.java @@ -60,6 +60,7 @@ /** * RascalFileSystemServices offers remote access to the Rascal file system. + * Currently, this is limited to a single client-server connection. */ public class RascalFileSystemServices implements IRemoteResolverRegistryServer { private static final URIResolverRegistry reg = URIResolverRegistry.getInstance(); From 60d4af2e0520b37daa293d25f6e33d42d6dec36a Mon Sep 17 00:00:00 2001 From: Rodin Aarssen Date: Fri, 3 Apr 2026 14:06:03 +0200 Subject: [PATCH 69/98] Introduces interfaces to clean up a lot of boilerplate code in RascalFileSystemServices --- .../uri/remote/RascalFileSystemServices.java | 131 ++++++++---------- 1 file changed, 59 insertions(+), 72 deletions(-) diff --git a/src/org/rascalmpl/uri/remote/RascalFileSystemServices.java b/src/org/rascalmpl/uri/remote/RascalFileSystemServices.java index c586b0467fa..e689d6f0f60 100644 --- a/src/org/rascalmpl/uri/remote/RascalFileSystemServices.java +++ b/src/org/rascalmpl/uri/remote/RascalFileSystemServices.java @@ -73,35 +73,30 @@ protected void provideClient(IRemoteResolverRegistryClient client) { this.client = client; } - @Override - public CompletableFuture resolveLocation(ISourceLocationRequest req) { - return CompletableFuture.supplyAsync(() -> { - ISourceLocation loc = req.getLocation(); - try { - ISourceLocation resolved = reg.logicalToPhysical(loc); + @FunctionalInterface + private interface IOSupplier { + T supply() throws IOException; + } - if (resolved == null) { - return new SourceLocationResponse(loc); - } + @FunctionalInterface + private interface IORunner { + void run() throws IOException; + } - return new SourceLocationResponse(resolved); - } catch (Exception e) { - return new SourceLocationResponse(loc); + private CompletableFuture async(IOSupplier job) { + return CompletableFuture.supplyAsync(() -> { + try { + return job.supply(); + } catch (IOException | RuntimeException e) { + throw new CompletionException(e); } }, executor); } - @Override - public CompletableFuture watch(WatchRequest params) { + private CompletableFuture async(IORunner job) { return CompletableFuture.runAsync(() -> { try { - ISourceLocation loc = params.getLocation(); - - URIResolverRegistry.getInstance().watch(loc, params.isRecursive(), changed -> { - client.sourceLocationChanged(new ISourceLocationChanged( - changed.getLocation(), ISourceLocationChangeType.forValue(changed.getChangeType().getValue()), params.getWatchId() - )); - }); + job.run(); } catch (IOException | RuntimeException e) { throw new CompletionException(e); } @@ -109,86 +104,78 @@ public CompletableFuture watch(WatchRequest params) { } @Override - public CompletableFuture stat(ISourceLocationRequest req) { - return CompletableFuture.supplyAsync(() -> { - try { - return reg.stat(req.getLocation()); - } catch (IOException | RuntimeException e) { - throw new CompletionException(e); + public CompletableFuture resolveLocation(ISourceLocationRequest req) { + return async(() -> { + ISourceLocation loc = req.getLocation(); + ISourceLocation resolved = reg.logicalToPhysical(loc); + + if (resolved == null) { + resolved = loc; } - }, executor); + + return new SourceLocationResponse(resolved); + }); + } + + @Override + public CompletableFuture watch(WatchRequest params) { + return async(() -> { + URIResolverRegistry.getInstance().watch(params.getLocation(), params.isRecursive(), changed -> + client.sourceLocationChanged(new ISourceLocationChanged( + changed.getLocation(), ISourceLocationChangeType.forValue(changed.getChangeType().getValue()), params.getWatchId() + )) + ); + }); + } + + @Override + public CompletableFuture stat(ISourceLocationRequest req) { + return async(() -> reg.stat(req.getLocation())); } @Override public CompletableFuture list(ISourceLocationRequest req) { - return CompletableFuture.supplyAsync(() -> { - try { - ISourceLocation loc = req.getLocation(); - if (!reg.isDirectory(loc)) { - throw new NotDirectoryException(loc.toString()); - } - 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 | RuntimeException e) { - throw new CompletionException(e); + return async(() -> { + ISourceLocation loc = req.getLocation(); + if (!reg.isDirectory(loc)) { + throw new NotDirectoryException(loc.toString()); } - }, executor); + return Arrays.stream(reg.list(loc)) + .map(l -> new FileWithType(URIUtil.getLocationName(l), reg.isDirectory(l) ? FileType.Directory : FileType.File)) + .toArray(FileWithType[]::new); + }); } @Override public CompletableFuture mkDirectory(ISourceLocationRequest req) { - return CompletableFuture.runAsync(() -> { - try { - reg.mkDirectory(req.getLocation()); - } catch (IOException | RuntimeException e) { - throw new CompletionException(e); - } - }, executor); + return async(() -> reg.mkDirectory(req.getLocation())); } @Override public CompletableFuture readFile(ISourceLocationRequest req) { - return CompletableFuture.supplyAsync(() -> { + return async(() -> { try (InputStream source = new Base64InputStream(reg.getInputStream(req.getLocation()), true)) { return new LocationContentResponse(new String(source.readAllBytes(), StandardCharsets.US_ASCII)); - } catch (IOException | RuntimeException e) { - throw new CompletionException(e); } - }, executor); + }); } @Override public CompletableFuture writeFile(WriteFileRequest req) { - return CompletableFuture.runAsync(() -> { - try { - try (OutputStream target = reg.getOutputStream(req.getLocation(), req.isAppend())) { - target.write(Base64.getDecoder().decode(req.getContent())); - } - } catch (IOException | RuntimeException e) { - throw new CompletionException(e); + return async(() -> { + try (OutputStream target = reg.getOutputStream(req.getLocation(), req.isAppend())) { + target.write(Base64.getDecoder().decode(req.getContent())); } - }, executor); + }); } @Override public CompletableFuture remove(RemoveRequest req) { - return CompletableFuture.runAsync(() -> { - try { - reg.remove(req.getLocation(), req.isRecursive()); - } catch (IOException e) { - throw new CompletionException(e); - } - }, executor); + return async(() -> reg.remove(req.getLocation(), req.isRecursive())); } @Override public CompletableFuture rename(RenameRequest req) { - return CompletableFuture.runAsync(() -> { - try { - reg.rename(req.getFrom(), req.getTo(), req.isOverwrite()); - } catch (IOException e) { - throw new CompletionException(e); - } - }, executor); + return async(() -> reg.rename(req.getFrom(), req.getTo(), req.isOverwrite())); } } From 84dbe7f025ed50e8ab72c9f7417d6f7223cddc30 Mon Sep 17 00:00:00 2001 From: Rodin Aarssen Date: Fri, 3 Apr 2026 14:20:11 +0200 Subject: [PATCH 70/98] RascalFileSystemServices now uses streaming Base64 en/decoding --- .../uri/remote/RascalFileSystemServices.java | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/org/rascalmpl/uri/remote/RascalFileSystemServices.java b/src/org/rascalmpl/uri/remote/RascalFileSystemServices.java index e689d6f0f60..84b95938a35 100644 --- a/src/org/rascalmpl/uri/remote/RascalFileSystemServices.java +++ b/src/org/rascalmpl/uri/remote/RascalFileSystemServices.java @@ -55,6 +55,7 @@ import org.rascalmpl.uri.vfs.IRemoteResolverRegistryClient; import org.rascalmpl.uri.vfs.IRemoteResolverRegistryServer; import org.rascalmpl.util.NamedThreadPool; +import org.rascalmpl.util.base64.StreamingBase64; import io.usethesource.vallang.ISourceLocation; @@ -154,17 +155,18 @@ public CompletableFuture mkDirectory(ISourceLocationRequest req) { @Override public CompletableFuture readFile(ISourceLocationRequest req) { return async(() -> { - try (InputStream source = new Base64InputStream(reg.getInputStream(req.getLocation()), true)) { - return new LocationContentResponse(new String(source.readAllBytes(), StandardCharsets.US_ASCII)); - } + StringBuilder builder = new StringBuilder(); + StreamingBase64.encode(reg.getInputStream(req.getLocation()), builder, true); + return new LocationContentResponse(builder.toString()); }); } @Override public CompletableFuture writeFile(WriteFileRequest req) { return async(() -> { - try (OutputStream target = reg.getOutputStream(req.getLocation(), req.isAppend())) { - target.write(Base64.getDecoder().decode(req.getContent())); + try (var decoder = StreamingBase64.decode(req.getContent()); + var target = reg.getOutputStream(req.getLocation(), req.isAppend())) { + decoder.transferTo(target); } }); } From 0afa8c2af644b7cda37d2cec5c00f5bc247a6de7 Mon Sep 17 00:00:00 2001 From: Rodin Aarssen Date: Fri, 3 Apr 2026 14:23:20 +0200 Subject: [PATCH 71/98] Removed unused imports --- src/org/rascalmpl/uri/remote/RascalFileSystemServices.java | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/org/rascalmpl/uri/remote/RascalFileSystemServices.java b/src/org/rascalmpl/uri/remote/RascalFileSystemServices.java index 84b95938a35..e3366912e75 100644 --- a/src/org/rascalmpl/uri/remote/RascalFileSystemServices.java +++ b/src/org/rascalmpl/uri/remote/RascalFileSystemServices.java @@ -27,17 +27,12 @@ package org.rascalmpl.uri.remote; import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.nio.charset.StandardCharsets; import java.nio.file.NotDirectoryException; import java.util.Arrays; -import java.util.Base64; import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletionException; import java.util.concurrent.ExecutorService; -import org.apache.commons.codec.binary.Base64InputStream; import org.checkerframework.checker.nullness.qual.EnsuresNonNull; import org.checkerframework.checker.nullness.qual.MonotonicNonNull; import org.rascalmpl.uri.FileAttributes; From 8c8d046a47a73b6d68a13dbaa12e9ec846b0018f Mon Sep 17 00:00:00 2001 From: Rodin Aarssen Date: Fri, 3 Apr 2026 14:23:37 +0200 Subject: [PATCH 72/98] Scheduling reconnect instead of a direct call --- .../rascalmpl/uri/remote/RemoteExternalResolverRegistry.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/org/rascalmpl/uri/remote/RemoteExternalResolverRegistry.java b/src/org/rascalmpl/uri/remote/RemoteExternalResolverRegistry.java index 8267f0d7597..6c22ae14f17 100644 --- a/src/org/rascalmpl/uri/remote/RemoteExternalResolverRegistry.java +++ b/src/org/rascalmpl/uri/remote/RemoteExternalResolverRegistry.java @@ -98,7 +98,7 @@ public class RemoteExternalResolverRegistry implements IExternalResolverRegistry public RemoteExternalResolverRegistry(int remoteResolverRegistryPort) { this.remoteResolverRegistryPort = remoteResolverRegistryPort; - connect(); + scheduleReconnect(); } private static final Duration LONGEST_TIMEOUT = Duration.ofMinutes(1); From c5c966820767126b08d40e7277b547d9dd7ab2f1 Mon Sep 17 00:00:00 2001 From: Rodin Aarssen Date: Fri, 3 Apr 2026 14:37:50 +0200 Subject: [PATCH 73/98] Removed throwing default implementations of IRemoteResolverRegistryServer methods --- .../vfs/IRemoteResolverRegistryServer.java | 80 +++++-------------- 1 file changed, 20 insertions(+), 60 deletions(-) diff --git a/src/org/rascalmpl/uri/vfs/IRemoteResolverRegistryServer.java b/src/org/rascalmpl/uri/vfs/IRemoteResolverRegistryServer.java index cffc79c23c0..61739e468a4 100644 --- a/src/org/rascalmpl/uri/vfs/IRemoteResolverRegistryServer.java +++ b/src/org/rascalmpl/uri/vfs/IRemoteResolverRegistryServer.java @@ -50,104 +50,64 @@ @JsonSegment("rascal/vfs") public interface IRemoteResolverRegistryServer { @JsonRequest("input/readFile") - default CompletableFuture readFile(ISourceLocationRequest req) { - throw new UnsupportedOperationException(); - } + CompletableFuture readFile(ISourceLocationRequest req); @JsonRequest("input/exists") - default CompletableFuture exists(ISourceLocationRequest req) { - throw new UnsupportedOperationException(); - } + CompletableFuture exists(ISourceLocationRequest req); @JsonRequest("input/lastModified") - default CompletableFuture lastModified(ISourceLocationRequest req) { - throw new UnsupportedOperationException(); - } + CompletableFuture lastModified(ISourceLocationRequest req); @JsonRequest("input/created") - default CompletableFuture created(ISourceLocationRequest req) { - throw new UnsupportedOperationException(); - } + CompletableFuture created(ISourceLocationRequest req); @JsonRequest("input/isDirectory") - default CompletableFuture isDirectory(ISourceLocationRequest req) { - throw new UnsupportedOperationException(); - } + CompletableFuture isDirectory(ISourceLocationRequest req); @JsonRequest("input/isFile") - default CompletableFuture isFile(ISourceLocationRequest req) { - throw new UnsupportedOperationException(); - } + CompletableFuture isFile(ISourceLocationRequest req); @JsonRequest("input/list") - default CompletableFuture list(ISourceLocationRequest req) { - throw new UnsupportedOperationException(); - } + CompletableFuture list(ISourceLocationRequest req); @JsonRequest("input/size") - default CompletableFuture size(ISourceLocationRequest req) { - throw new UnsupportedOperationException(); - } + CompletableFuture size(ISourceLocationRequest req); @JsonRequest("input/stat") - default CompletableFuture stat(ISourceLocationRequest req) { - throw new UnsupportedOperationException(); - } + CompletableFuture stat(ISourceLocationRequest req); @JsonRequest("input/isReadable") - default CompletableFuture isReadable(ISourceLocationRequest req) { - throw new UnsupportedOperationException(); - } + CompletableFuture isReadable(ISourceLocationRequest req); @JsonRequest("output/setLastModified") - default CompletableFuture setLastModified(SetLastModifiedRequest req) { - throw new UnsupportedOperationException(); - } + CompletableFuture setLastModified(SetLastModifiedRequest req); @JsonRequest("output/isWritable") - default CompletableFuture isWritable(ISourceLocationRequest req) { - throw new UnsupportedOperationException(); - } + CompletableFuture isWritable(ISourceLocationRequest req); @JsonRequest("output/writeFile") - default CompletableFuture writeFile(WriteFileRequest req) { - throw new UnsupportedOperationException(); - } + CompletableFuture writeFile(WriteFileRequest req); @JsonRequest("output/mkDirectory") - default CompletableFuture mkDirectory(ISourceLocationRequest req) { - throw new UnsupportedOperationException(); - } + CompletableFuture mkDirectory(ISourceLocationRequest req); @JsonRequest("output/remove") - default CompletableFuture remove(RemoveRequest req) { - throw new UnsupportedOperationException(); - } + CompletableFuture remove(RemoveRequest req); @JsonRequest("output/rename") - default CompletableFuture rename(RenameRequest req) { - throw new UnsupportedOperationException(); - } + CompletableFuture rename(RenameRequest req); @JsonRequest("watcher/watch") - default CompletableFuture watch(WatchRequest req) { - throw new UnsupportedOperationException(); - } + CompletableFuture watch(WatchRequest req); @JsonRequest("watcher/unwatch") - default CompletableFuture unwatch(WatchRequest req) { - throw new UnsupportedOperationException(); - } + CompletableFuture unwatch(WatchRequest req); @JsonRequest("watcher/supportsRecursiveWatch") - default CompletableFuture supportsRecursiveWatch() { - throw new UnsupportedOperationException(); - } + CompletableFuture supportsRecursiveWatch(); @JsonRequest("logical/resolveLocation") - default CompletableFuture resolveLocation(ISourceLocationRequest loc) { - throw new UnsupportedOperationException(); - } + CompletableFuture resolveLocation(ISourceLocationRequest loc); public static class FileWithType { @NonNull private final String name; From 8aaede23a55571cf904cb7d50780b85c919d143b Mon Sep 17 00:00:00 2001 From: Rodin Aarssen Date: Fri, 3 Apr 2026 15:40:28 +0200 Subject: [PATCH 74/98] Added copy to the IRemoteResolverRegistryServer interface and implemented it in RascalFileSystemServices --- .../uri/remote/RascalFileSystemServices.java | 6 ++ .../uri/remote/jsonrpc/CopyRequest.java | 85 +++++++++++++++++++ .../vfs/IRemoteResolverRegistryServer.java | 4 + 3 files changed, 95 insertions(+) create mode 100644 src/org/rascalmpl/uri/remote/jsonrpc/CopyRequest.java diff --git a/src/org/rascalmpl/uri/remote/RascalFileSystemServices.java b/src/org/rascalmpl/uri/remote/RascalFileSystemServices.java index e3366912e75..ca6158c314a 100644 --- a/src/org/rascalmpl/uri/remote/RascalFileSystemServices.java +++ b/src/org/rascalmpl/uri/remote/RascalFileSystemServices.java @@ -38,6 +38,7 @@ import org.rascalmpl.uri.FileAttributes; import org.rascalmpl.uri.URIResolverRegistry; import org.rascalmpl.uri.URIUtil; +import org.rascalmpl.uri.remote.jsonrpc.CopyRequest; import org.rascalmpl.uri.remote.jsonrpc.ISourceLocationChangeType; import org.rascalmpl.uri.remote.jsonrpc.ISourceLocationChanged; import org.rascalmpl.uri.remote.jsonrpc.ISourceLocationRequest; @@ -175,4 +176,9 @@ public CompletableFuture remove(RemoveRequest req) { public CompletableFuture rename(RenameRequest req) { return async(() -> reg.rename(req.getFrom(), req.getTo(), req.isOverwrite())); } + + @Override + public CompletableFuture copy(CopyRequest req) { + return async(() -> reg.copy(req.getFrom(), req.getTo(), req.isRecursive(), req.isOverwrite())); + } } diff --git a/src/org/rascalmpl/uri/remote/jsonrpc/CopyRequest.java b/src/org/rascalmpl/uri/remote/jsonrpc/CopyRequest.java new file mode 100644 index 00000000000..ec511c66939 --- /dev/null +++ b/src/org/rascalmpl/uri/remote/jsonrpc/CopyRequest.java @@ -0,0 +1,85 @@ +/* + * 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.uri.remote.jsonrpc; + +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 CopyRequest { + @NonNull + private ISourceLocation from; + @NonNull + private ISourceLocation to; + + private boolean recursive; + private boolean overwrite; + + public CopyRequest(ISourceLocation from, ISourceLocation to, boolean recursive, boolean overwrite) { + this.from = from; + this.to = to; + this.recursive = recursive; + this.overwrite = overwrite; + } + + public ISourceLocation getFrom() { + return from; + } + + public ISourceLocation getTo() { + return to; + } + + public boolean isRecursive() { + return recursive; + } + + public boolean isOverwrite() { + return overwrite; + } + + @Override + public boolean equals(@Nullable Object obj) { + if (obj instanceof CopyRequest) { + var other = (CopyRequest)obj; + return Objects.equals(from, other.from) + && Objects.equals(to, other.to) + && recursive == other.recursive + && overwrite == other.overwrite + ; + } + return false; + } + + @Override + public int hashCode() { + return Objects.hash(from, to, recursive, overwrite); + } +} diff --git a/src/org/rascalmpl/uri/vfs/IRemoteResolverRegistryServer.java b/src/org/rascalmpl/uri/vfs/IRemoteResolverRegistryServer.java index 61739e468a4..1604a00cc22 100644 --- a/src/org/rascalmpl/uri/vfs/IRemoteResolverRegistryServer.java +++ b/src/org/rascalmpl/uri/vfs/IRemoteResolverRegistryServer.java @@ -33,6 +33,7 @@ import org.eclipse.lsp4j.jsonrpc.validation.NonNull; import org.rascalmpl.uri.FileAttributes; import org.rascalmpl.uri.remote.jsonrpc.BooleanResponse; +import org.rascalmpl.uri.remote.jsonrpc.CopyRequest; import org.rascalmpl.uri.remote.jsonrpc.ISourceLocationRequest; import org.rascalmpl.uri.remote.jsonrpc.LocationContentResponse; import org.rascalmpl.uri.remote.jsonrpc.NumberResponse; @@ -97,6 +98,9 @@ public interface IRemoteResolverRegistryServer { @JsonRequest("output/rename") CompletableFuture rename(RenameRequest req); + @JsonRequest("output/copy") + CompletableFuture copy(CopyRequest req); + @JsonRequest("watcher/watch") CompletableFuture watch(WatchRequest req); From eb77334aa6bcec063d0be3c84248bf150b25c378 Mon Sep 17 00:00:00 2001 From: Rodin Aarssen Date: Tue, 7 Apr 2026 09:14:11 +0200 Subject: [PATCH 75/98] Implemented missing methods in RascalFileSystemServices --- .../uri/remote/RascalFileSystemServices.java | 127 +++++++++++++----- 1 file changed, 93 insertions(+), 34 deletions(-) diff --git a/src/org/rascalmpl/uri/remote/RascalFileSystemServices.java b/src/org/rascalmpl/uri/remote/RascalFileSystemServices.java index ca6158c314a..1de97903017 100644 --- a/src/org/rascalmpl/uri/remote/RascalFileSystemServices.java +++ b/src/org/rascalmpl/uri/remote/RascalFileSystemServices.java @@ -38,14 +38,18 @@ import org.rascalmpl.uri.FileAttributes; import org.rascalmpl.uri.URIResolverRegistry; import org.rascalmpl.uri.URIUtil; +import org.rascalmpl.uri.remote.jsonrpc.BooleanResponse; import org.rascalmpl.uri.remote.jsonrpc.CopyRequest; import org.rascalmpl.uri.remote.jsonrpc.ISourceLocationChangeType; import org.rascalmpl.uri.remote.jsonrpc.ISourceLocationChanged; import org.rascalmpl.uri.remote.jsonrpc.ISourceLocationRequest; import org.rascalmpl.uri.remote.jsonrpc.LocationContentResponse; +import org.rascalmpl.uri.remote.jsonrpc.NumberResponse; import org.rascalmpl.uri.remote.jsonrpc.RemoveRequest; import org.rascalmpl.uri.remote.jsonrpc.RenameRequest; +import org.rascalmpl.uri.remote.jsonrpc.SetLastModifiedRequest; import org.rascalmpl.uri.remote.jsonrpc.SourceLocationResponse; +import org.rascalmpl.uri.remote.jsonrpc.TimestampResponse; import org.rascalmpl.uri.remote.jsonrpc.WatchRequest; import org.rascalmpl.uri.remote.jsonrpc.WriteFileRequest; import org.rascalmpl.uri.vfs.IRemoteResolverRegistryClient; @@ -80,20 +84,20 @@ private interface IORunner { void run() throws IOException; } - private CompletableFuture async(IOSupplier job) { - return CompletableFuture.supplyAsync(() -> { + private CompletableFuture async(IORunner job) { + return CompletableFuture.runAsync(() -> { try { - return job.supply(); + job.run(); } catch (IOException | RuntimeException e) { throw new CompletionException(e); } }, executor); } - private CompletableFuture async(IORunner job) { - return CompletableFuture.runAsync(() -> { + private CompletableFuture async(IOSupplier job) { + return CompletableFuture.supplyAsync(() -> { try { - job.run(); + return job.supply(); } catch (IOException | RuntimeException e) { throw new CompletionException(e); } @@ -101,33 +105,37 @@ private CompletableFuture async(IORunner job) { } @Override - public CompletableFuture resolveLocation(ISourceLocationRequest req) { + public CompletableFuture readFile(ISourceLocationRequest req) { return async(() -> { - ISourceLocation loc = req.getLocation(); - ISourceLocation resolved = reg.logicalToPhysical(loc); + StringBuilder builder = new StringBuilder(); + StreamingBase64.encode(reg.getInputStream(req.getLocation()), builder, true); + return new LocationContentResponse(builder.toString()); + }); + } - if (resolved == null) { - resolved = loc; - } + @Override + public CompletableFuture exists(ISourceLocationRequest req) { + return async(() -> new BooleanResponse(reg.exists(req.getLocation()))); + } - return new SourceLocationResponse(resolved); - }); + @Override + public CompletableFuture lastModified(ISourceLocationRequest req) { + return async(() -> new TimestampResponse(reg.lastModified(req.getLocation()))); } @Override - public CompletableFuture watch(WatchRequest params) { - return async(() -> { - URIResolverRegistry.getInstance().watch(params.getLocation(), params.isRecursive(), changed -> - client.sourceLocationChanged(new ISourceLocationChanged( - changed.getLocation(), ISourceLocationChangeType.forValue(changed.getChangeType().getValue()), params.getWatchId() - )) - ); - }); + public CompletableFuture created(ISourceLocationRequest req) { + return async(() -> new TimestampResponse(reg.created(req.getLocation()))); } @Override - public CompletableFuture stat(ISourceLocationRequest req) { - return async(() -> reg.stat(req.getLocation())); + public CompletableFuture isDirectory(ISourceLocationRequest req) { + return async(() -> new BooleanResponse(reg.isDirectory(req.getLocation()))); + } + + @Override + public CompletableFuture isFile(ISourceLocationRequest req) { + return async(() -> new BooleanResponse(reg.isFile(req.getLocation()))); } @Override @@ -138,23 +146,34 @@ public CompletableFuture list(ISourceLocationRequest req) { throw new NotDirectoryException(loc.toString()); } return Arrays.stream(reg.list(loc)) - .map(l -> new FileWithType(URIUtil.getLocationName(l), reg.isDirectory(l) ? FileType.Directory : FileType.File)) - .toArray(FileWithType[]::new); + .map(l -> new FileWithType(URIUtil.getLocationName(l), reg.isDirectory(l) ? FileType.Directory : FileType.File)) + .toArray(FileWithType[]::new); }); } @Override - public CompletableFuture mkDirectory(ISourceLocationRequest req) { - return async(() -> reg.mkDirectory(req.getLocation())); + public CompletableFuture size(ISourceLocationRequest req) { + return async(() -> new NumberResponse(reg.size(req.getLocation()))); } @Override - public CompletableFuture readFile(ISourceLocationRequest req) { - return async(() -> { - StringBuilder builder = new StringBuilder(); - StreamingBase64.encode(reg.getInputStream(req.getLocation()), builder, true); - return new LocationContentResponse(builder.toString()); - }); + public CompletableFuture stat(ISourceLocationRequest req) { + return async(() -> reg.stat(req.getLocation())); + } + + @Override + public CompletableFuture isReadable(ISourceLocationRequest req) { + return async(() -> new BooleanResponse(reg.isReadable(req.getLocation()))); + } + + @Override + public CompletableFuture setLastModified(SetLastModifiedRequest req) { + return async(() -> reg.setLastModified(req.getLocation(), req.getTimestamp())); + } + + @Override + public CompletableFuture isWritable(ISourceLocationRequest req) { + return async(() -> new BooleanResponse(reg.isWritable(req.getLocation()))); } @Override @@ -167,6 +186,11 @@ public CompletableFuture writeFile(WriteFileRequest req) { }); } + @Override + public CompletableFuture mkDirectory(ISourceLocationRequest req) { + return async(() -> reg.mkDirectory(req.getLocation())); + } + @Override public CompletableFuture remove(RemoveRequest req) { return async(() -> reg.remove(req.getLocation(), req.isRecursive())); @@ -181,4 +205,39 @@ public CompletableFuture rename(RenameRequest req) { public CompletableFuture copy(CopyRequest req) { return async(() -> reg.copy(req.getFrom(), req.getTo(), req.isRecursive(), req.isOverwrite())); } + + @Override + public CompletableFuture watch(WatchRequest params) { + return async(() -> { + URIResolverRegistry.getInstance().watch(params.getLocation(), params.isRecursive(), changed -> + client.sourceLocationChanged(new ISourceLocationChanged( + changed.getLocation(), ISourceLocationChangeType.forValue(changed.getChangeType().getValue()), params.getWatchId() + )) + ); + }); + } + + @Override + public CompletableFuture unwatch(WatchRequest req) { + return async(() -> URIResolverRegistry.getInstance().unwatch(req.getLocation(), req.isRecursive(), e -> {})); + } + + @Override + public CompletableFuture supportsRecursiveWatch() { + return async(() -> new BooleanResponse(true)); + } + + @Override + public CompletableFuture resolveLocation(ISourceLocationRequest req) { + return async(() -> { + ISourceLocation loc = req.getLocation(); + ISourceLocation resolved = reg.logicalToPhysical(loc); + + if (resolved == null) { + resolved = loc; + } + + return new SourceLocationResponse(resolved); + }); + } } From ad5db563738f4e19c5b881ca32a010b98b088e91 Mon Sep 17 00:00:00 2001 From: Rodin Aarssen Date: Tue, 7 Apr 2026 09:17:54 +0200 Subject: [PATCH 76/98] Removed locks in favor of double invalidation to avoid potential deadlocks --- .../RemoteExternalResolverRegistry.java | 60 ++++++++----------- 1 file changed, 26 insertions(+), 34 deletions(-) diff --git a/src/org/rascalmpl/uri/remote/RemoteExternalResolverRegistry.java b/src/org/rascalmpl/uri/remote/RemoteExternalResolverRegistry.java index 6c22ae14f17..44dc8d665a1 100644 --- a/src/org/rascalmpl/uri/remote/RemoteExternalResolverRegistry.java +++ b/src/org/rascalmpl/uri/remote/RemoteExternalResolverRegistry.java @@ -380,13 +380,11 @@ public long size(ISourceLocation loc) throws IOException { @Override public boolean isDirectory(ISourceLocation loc) { try { - synchronized (cachedDirectoryListing) { - var cached = cachedDirectoryListing.getIfPresent(URIUtil.getParentLocation(loc)); - if (cached != null) { - var result = cached.get().get(URIUtil.getLocationName(loc)); - if (result != null) { - return result; - } + var cached = cachedDirectoryListing.getIfPresent(URIUtil.getParentLocation(loc)); + if (cached != null) { + var result = cached.get().get(URIUtil.getLocationName(loc)); + if (result != null) { + return result; } } return call(remote::isDirectory, new ISourceLocationRequest(loc)).getValue(); @@ -398,13 +396,11 @@ public boolean isDirectory(ISourceLocation loc) { @Override public boolean isFile(ISourceLocation loc) { try { - synchronized (cachedDirectoryListing) { - var cached = cachedDirectoryListing.getIfPresent(URIUtil.getParentLocation(loc)); - if (cached != null) { - var result = cached.get().get(URIUtil.getLocationName(loc)); - if (result != null) { - return !result; - } + var cached = cachedDirectoryListing.getIfPresent(URIUtil.getParentLocation(loc)); + if (cached != null) { + var result = cached.get().get(URIUtil.getLocationName(loc)); + if (result != null) { + return !result; } } return call(remote::isFile, new ISourceLocationRequest(loc)).getValue(); @@ -431,13 +427,11 @@ public boolean isReadable(ISourceLocation loc) throws IOException { @Override public String[] list(ISourceLocation loc) throws IOException { - synchronized (cachedDirectoryListing) { - var result = call(remote::list, new ISourceLocationRequest(loc)); - cachedDirectoryListing.put(loc, Lazy.defer(() -> { - return Stream.of(result).collect(Collectors.toMap(FileWithType::getName, e -> e.getType() == FileType.Directory)); - })); - return Stream.of(result).map(FileWithType::getName).toArray(String[]::new); - } + var result = call(remote::list, new ISourceLocationRequest(loc)); + cachedDirectoryListing.put(loc, Lazy.defer(() -> { + return Stream.of(result).collect(Collectors.toMap(FileWithType::getName, e -> e.getType() == FileType.Directory)); + })); + return Stream.of(result).map(FileWithType::getName).toArray(String[]::new); } @Override @@ -465,29 +459,27 @@ public void close() throws IOException { try (var input = new ByteArrayInputStream(this.toByteArray())) { StreamingBase64.encode(input, content, true); } - synchronized (cachedDirectoryListing) { - call(remote::writeFile, new WriteFileRequest(loc, content.toString(), append)); - cachedDirectoryListing.invalidate(URIUtil.getParentLocation(loc)); - } + cachedDirectoryListing.invalidate(URIUtil.getParentLocation(loc)); + call(remote::writeFile, new WriteFileRequest(loc, content.toString(), append)); + cachedDirectoryListing.invalidate(URIUtil.getParentLocation(loc)); } }; } @Override public void mkDirectory(ISourceLocation loc) throws IOException { - synchronized (cachedDirectoryListing) { - call(remote::mkDirectory, new ISourceLocationRequest(loc)); - cachedDirectoryListing.invalidate(URIUtil.getParentLocation(loc)); - } + cachedDirectoryListing.invalidate(URIUtil.getParentLocation(loc)); + call(remote::mkDirectory, new ISourceLocationRequest(loc)); + cachedDirectoryListing.invalidate(URIUtil.getParentLocation(loc)); } @Override public void remove(ISourceLocation loc) throws IOException { - synchronized (cachedDirectoryListing) { - call(remote::remove, new RemoveRequest(loc, true)); - cachedDirectoryListing.invalidate(loc); - cachedDirectoryListing.invalidate(URIUtil.getParentLocation(loc)); - } + cachedDirectoryListing.invalidate(loc); + cachedDirectoryListing.invalidate(URIUtil.getParentLocation(loc)); + call(remote::remove, new RemoveRequest(loc, true)); + cachedDirectoryListing.invalidate(loc); + cachedDirectoryListing.invalidate(URIUtil.getParentLocation(loc)); } @Override From aede04888e1a12cf6f46fa90436e012d521c16fa Mon Sep 17 00:00:00 2001 From: Rodin Aarssen Date: Tue, 7 Apr 2026 09:23:52 +0200 Subject: [PATCH 77/98] Fixed indentation --- .../rascalmpl/uri/remote/RemoteExternalResolverRegistry.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/org/rascalmpl/uri/remote/RemoteExternalResolverRegistry.java b/src/org/rascalmpl/uri/remote/RemoteExternalResolverRegistry.java index 44dc8d665a1..4092a0131a2 100644 --- a/src/org/rascalmpl/uri/remote/RemoteExternalResolverRegistry.java +++ b/src/org/rascalmpl/uri/remote/RemoteExternalResolverRegistry.java @@ -288,8 +288,8 @@ private IRemoteResolverRegistryServer startClient() { .setExecutorService(NamedThreadPool.cachedDaemon("rascal-remote-resolver-registry")) .create(); - clientLauncher.startListening(); - return clientLauncher.getRemoteProxy(); + clientLauncher.startListening(); + return clientLauncher.getRemoteProxy(); } catch (RuntimeException | IOException e) { System.err.println("Error setting up remote resolver registry connection, will reconnect: " + e.getMessage()); return null; From 1b7c9db62e833f6c5d12250d3233902983e92622 Mon Sep 17 00:00:00 2001 From: Rodin Aarssen Date: Tue, 7 Apr 2026 09:27:11 +0200 Subject: [PATCH 78/98] Improved logic in isDirectory and isFile --- .../uri/remote/RemoteExternalResolverRegistry.java | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/src/org/rascalmpl/uri/remote/RemoteExternalResolverRegistry.java b/src/org/rascalmpl/uri/remote/RemoteExternalResolverRegistry.java index 4092a0131a2..3cb655ede65 100644 --- a/src/org/rascalmpl/uri/remote/RemoteExternalResolverRegistry.java +++ b/src/org/rascalmpl/uri/remote/RemoteExternalResolverRegistry.java @@ -383,9 +383,7 @@ public boolean isDirectory(ISourceLocation loc) { var cached = cachedDirectoryListing.getIfPresent(URIUtil.getParentLocation(loc)); if (cached != null) { var result = cached.get().get(URIUtil.getLocationName(loc)); - if (result != null) { - return result; - } + return result != null && result; } return call(remote::isDirectory, new ISourceLocationRequest(loc)).getValue(); } catch (IOException e) { @@ -399,9 +397,7 @@ public boolean isFile(ISourceLocation loc) { var cached = cachedDirectoryListing.getIfPresent(URIUtil.getParentLocation(loc)); if (cached != null) { var result = cached.get().get(URIUtil.getLocationName(loc)); - if (result != null) { - return !result; - } + return result != null && !result; } return call(remote::isFile, new ISourceLocationRequest(loc)).getValue(); } catch (IOException e) { From b2e358e5147ae004832c506ac2166ca196588e1b Mon Sep 17 00:00:00 2001 From: Rodin Aarssen Date: Tue, 7 Apr 2026 09:30:17 +0200 Subject: [PATCH 79/98] Watch now uses the remote call wrapper function --- .../RemoteExternalResolverRegistry.java | 24 ++++++++----------- 1 file changed, 10 insertions(+), 14 deletions(-) diff --git a/src/org/rascalmpl/uri/remote/RemoteExternalResolverRegistry.java b/src/org/rascalmpl/uri/remote/RemoteExternalResolverRegistry.java index 3cb655ede65..db66ebff57f 100644 --- a/src/org/rascalmpl/uri/remote/RemoteExternalResolverRegistry.java +++ b/src/org/rascalmpl/uri/remote/RemoteExternalResolverRegistry.java @@ -495,27 +495,23 @@ public ISourceLocation resolve(ISourceLocation input) throws IOException { @Override public void watch(ISourceLocation root, Consumer watcher, boolean recursive) throws IOException { - try { - synchronized (watchers) { - var key = new WatchSubscriptionKey(root, recursive); - if (!watchers.containsKey(key)) { - var freshWatchers = new Watchers(); - freshWatchers.addNewWatcher(watcher); - watchersById.put(freshWatchers.getId(), freshWatchers); - remote.watch(new WatchRequest(root, recursive, freshWatchers.getId())).get(1, TimeUnit.MINUTES); - watchers.put(key, freshWatchers); - } - watchers.get(key).addNewWatcher(watcher); + synchronized (watchers) { + var key = new WatchSubscriptionKey(root, recursive); + if (!watchers.containsKey(key)) { + var freshWatchers = new Watchers(); + freshWatchers.addNewWatcher(watcher); + watchersById.put(freshWatchers.getId(), freshWatchers); + call(remote::watch, new WatchRequest(root, recursive, freshWatchers.getId())); + watchers.put(key, freshWatchers); } - } catch (CompletionException | InterruptedException | ExecutionException | TimeoutException ce) { - throw new IOException("Could not watch `" + root + "` remotely: " + ce.getCause().getMessage()); + watchers.get(key).addNewWatcher(watcher); } } @Override public void unwatch(ISourceLocation root, Consumer watcher, boolean recursive) throws IOException { - var watchKey = new WatchSubscriptionKey(root, recursive); synchronized (watchers) { + var watchKey = new WatchSubscriptionKey(root, recursive); var watch = watchers.get(watchKey); if (watch != null && watch.removeWatcher(watcher)) { watchers.remove(watchKey); From 714ed81bb6965623f85b3562f7ff8a9c1b82ec68 Mon Sep 17 00:00:00 2001 From: Rodin Aarssen Date: Tue, 7 Apr 2026 09:31:38 +0200 Subject: [PATCH 80/98] Watchers class is now private --- .../rascalmpl/uri/remote/RemoteExternalResolverRegistry.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/org/rascalmpl/uri/remote/RemoteExternalResolverRegistry.java b/src/org/rascalmpl/uri/remote/RemoteExternalResolverRegistry.java index db66ebff57f..2ec740f76fb 100644 --- a/src/org/rascalmpl/uri/remote/RemoteExternalResolverRegistry.java +++ b/src/org/rascalmpl/uri/remote/RemoteExternalResolverRegistry.java @@ -565,7 +565,7 @@ public void sourceLocationChanged(org.rascalmpl.uri.remote.jsonrpc.ISourceLocati * 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 remote side. */ - public static class Watchers { + private static class Watchers { private final String id; private final List> callbacks = new CopyOnWriteArrayList<>(); From fd577773c3234a60b98d422925005b770af37906 Mon Sep 17 00:00:00 2001 From: Rodin Aarssen Date: Tue, 7 Apr 2026 09:35:51 +0200 Subject: [PATCH 81/98] Improved locking logic around registration of external resolver registry --- src/org/rascalmpl/uri/URIResolverRegistry.java | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/src/org/rascalmpl/uri/URIResolverRegistry.java b/src/org/rascalmpl/uri/URIResolverRegistry.java index 8b2f6bbadd6..534ac7eb5f3 100644 --- a/src/org/rascalmpl/uri/URIResolverRegistry.java +++ b/src/org/rascalmpl/uri/URIResolverRegistry.java @@ -119,11 +119,7 @@ private void loadServices() { var remoteResolverRegistryPort = getRemoteResolverRegistryPort(); if (remoteResolverRegistryPort != null) { - synchronized (this) { - if (this.externalRegistry == null) { - registerRemoteResolverRegistry(new RemoteExternalResolverRegistry(remoteResolverRegistryPort)); - } - } + registerRemoteResolverRegistry(new RemoteExternalResolverRegistry(remoteResolverRegistryPort)); } } @@ -140,7 +136,7 @@ public static Integer getRemoteResolverRegistryPort() { } public synchronized void registerRemoteResolverRegistry(RemoteExternalResolverRegistry registry) { - synchronized (this) { + if (this.externalRegistry != null) { this.externalRegistry = registry; watchers.setExternalRegistry(registry); } From aec8c404c63b50d39452d2aeb79cf0b587a570f1 Mon Sep 17 00:00:00 2001 From: Rodin Aarssen Date: Wed, 8 Apr 2026 08:49:34 +0200 Subject: [PATCH 82/98] Rewrote remote watch --- .../remote/RemoteExternalResolverRegistry.java | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/src/org/rascalmpl/uri/remote/RemoteExternalResolverRegistry.java b/src/org/rascalmpl/uri/remote/RemoteExternalResolverRegistry.java index 2ec740f76fb..3fea91f04e0 100644 --- a/src/org/rascalmpl/uri/remote/RemoteExternalResolverRegistry.java +++ b/src/org/rascalmpl/uri/remote/RemoteExternalResolverRegistry.java @@ -497,14 +497,15 @@ public ISourceLocation resolve(ISourceLocation input) throws IOException { public void watch(ISourceLocation root, Consumer watcher, boolean recursive) throws IOException { synchronized (watchers) { var key = new WatchSubscriptionKey(root, recursive); - if (!watchers.containsKey(key)) { - var freshWatchers = new Watchers(); - freshWatchers.addNewWatcher(watcher); - watchersById.put(freshWatchers.getId(), freshWatchers); - call(remote::watch, new WatchRequest(root, recursive, freshWatchers.getId())); - watchers.put(key, freshWatchers); + var watch = watchers.get(key); + if (watch == null) { + watch = new Watchers(); + watch.addNewWatcher(watcher); + watchersById.put(watch.getId(), watch); + call(remote::watch, new WatchRequest(root, recursive, watch.getId())); + watchers.put(key, watch); } - watchers.get(key).addNewWatcher(watcher); + watch.addNewWatcher(watcher); } } From f73c3604d9c108783f4dc76513e6e57eccd68d7b Mon Sep 17 00:00:00 2001 From: Rodin Aarssen Date: Wed, 8 Apr 2026 08:49:42 +0200 Subject: [PATCH 83/98] Added comment --- src/org/rascalmpl/uri/ISourceLocationWatcher.java | 1 + 1 file changed, 1 insertion(+) diff --git a/src/org/rascalmpl/uri/ISourceLocationWatcher.java b/src/org/rascalmpl/uri/ISourceLocationWatcher.java index a7db15f66c7..6fd2b7852e5 100644 --- a/src/org/rascalmpl/uri/ISourceLocationWatcher.java +++ b/src/org/rascalmpl/uri/ISourceLocationWatcher.java @@ -62,6 +62,7 @@ default boolean isChanged() { } public enum ISourceLocationChangeType { + // The numeric values are based on the values in the FileChangeType enum of VS Code CREATED(2), DELETED(3), MODIFIED(1); From eaf9cb00a191a2aba7c20a33dfff225962d1f77c Mon Sep 17 00:00:00 2001 From: Rodin Aarssen Date: Wed, 8 Apr 2026 08:57:25 +0200 Subject: [PATCH 84/98] RemoteExternalResolverRegistry now uses a single tread pool --- .../uri/remote/RemoteExternalResolverRegistry.java | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/org/rascalmpl/uri/remote/RemoteExternalResolverRegistry.java b/src/org/rascalmpl/uri/remote/RemoteExternalResolverRegistry.java index 3fea91f04e0..364b3b3c4ec 100644 --- a/src/org/rascalmpl/uri/remote/RemoteExternalResolverRegistry.java +++ b/src/org/rascalmpl/uri/remote/RemoteExternalResolverRegistry.java @@ -90,6 +90,7 @@ */ public class RemoteExternalResolverRegistry implements IExternalResolverRegistry, IRemoteResolverRegistryClient { private volatile IRemoteResolverRegistryServer remote = null; + private static final ExecutorService exec = NamedThreadPool.cachedDaemon("rascal-remote-resolver-registry"); private final Map watchers = new ConcurrentHashMap<>(); private final Map watchersById = new ConcurrentHashMap<>(); @@ -273,7 +274,7 @@ public void close() throws IOException { } }; } - + private IRemoteResolverRegistryServer startClient() { try { @SuppressWarnings("resource") @@ -285,7 +286,7 @@ private IRemoteResolverRegistryServer startClient() { .setInput(errorDetectingInputStream(socket.getInputStream())) .setOutput(errorDetectingOutputStream(socket.getOutputStream())) .configureGson(GsonUtils.complexAsJsonObject()) - .setExecutorService(NamedThreadPool.cachedDaemon("rascal-remote-resolver-registry")) + .setExecutorService(exec) .create(); clientLauncher.startListening(); @@ -559,8 +560,6 @@ public void sourceLocationChanged(org.rascalmpl.uri.remote.jsonrpc.ISourceLocati } } - private static final ExecutorService exec = NamedThreadPool.cachedDaemon("RemoteExternalResolverRegistry-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. From 58d46731601ae2d75c7d412180ac68c86da6b9e0 Mon Sep 17 00:00:00 2001 From: Rodin Aarssen Date: Wed, 8 Apr 2026 09:20:51 +0200 Subject: [PATCH 85/98] Removed checker framework annotations from request/response classes --- .../rascalmpl/uri/remote/jsonrpc/BooleanResponse.java | 2 +- src/org/rascalmpl/uri/remote/jsonrpc/CopyRequest.java | 7 +------ .../uri/remote/jsonrpc/ISourceLocationChanged.java | 9 ++------- .../uri/remote/jsonrpc/ISourceLocationRequest.java | 6 +----- .../uri/remote/jsonrpc/LocationContentResponse.java | 5 +---- src/org/rascalmpl/uri/remote/jsonrpc/RenameRequest.java | 7 +------ .../uri/remote/jsonrpc/SetLastModifiedRequest.java | 4 +--- .../uri/remote/jsonrpc/SourceLocationResponse.java | 5 +---- src/org/rascalmpl/uri/remote/jsonrpc/WatchRequest.java | 6 +----- .../rascalmpl/uri/remote/jsonrpc/WriteFileRequest.java | 6 +----- 10 files changed, 11 insertions(+), 46 deletions(-) diff --git a/src/org/rascalmpl/uri/remote/jsonrpc/BooleanResponse.java b/src/org/rascalmpl/uri/remote/jsonrpc/BooleanResponse.java index 2f46ebb8743..1cc74712556 100644 --- a/src/org/rascalmpl/uri/remote/jsonrpc/BooleanResponse.java +++ b/src/org/rascalmpl/uri/remote/jsonrpc/BooleanResponse.java @@ -50,5 +50,5 @@ public boolean equals(Object obj) { @Override public int hashCode() { return Objects.hash(value); - } + } } diff --git a/src/org/rascalmpl/uri/remote/jsonrpc/CopyRequest.java b/src/org/rascalmpl/uri/remote/jsonrpc/CopyRequest.java index ec511c66939..7f16e6259a4 100644 --- a/src/org/rascalmpl/uri/remote/jsonrpc/CopyRequest.java +++ b/src/org/rascalmpl/uri/remote/jsonrpc/CopyRequest.java @@ -28,15 +28,10 @@ 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 CopyRequest { - @NonNull private ISourceLocation from; - @NonNull private ISourceLocation to; private boolean recursive; @@ -66,7 +61,7 @@ public boolean isOverwrite() { } @Override - public boolean equals(@Nullable Object obj) { + public boolean equals(Object obj) { if (obj instanceof CopyRequest) { var other = (CopyRequest)obj; return Objects.equals(from, other.from) diff --git a/src/org/rascalmpl/uri/remote/jsonrpc/ISourceLocationChanged.java b/src/org/rascalmpl/uri/remote/jsonrpc/ISourceLocationChanged.java index b711ed6067b..5a8274f1cc2 100644 --- a/src/org/rascalmpl/uri/remote/jsonrpc/ISourceLocationChanged.java +++ b/src/org/rascalmpl/uri/remote/jsonrpc/ISourceLocationChanged.java @@ -28,21 +28,16 @@ import java.util.Objects; -import org.checkerframework.checker.nullness.qual.Nullable; -import org.eclipse.lsp4j.jsonrpc.validation.NonNull; import org.rascalmpl.uri.ISourceLocationWatcher; import io.usethesource.vallang.ISourceLocation; public class ISourceLocationChanged { - @NonNull private ISourceLocation root; - @NonNull private ISourceLocationChangeType type; - @NonNull private String watchId; - public ISourceLocationChanged(@NonNull ISourceLocation root, @NonNull ISourceLocationChangeType type, @NonNull String watchId) { + public ISourceLocationChanged(ISourceLocation root, ISourceLocationChangeType type, String watchId) { this.root = root; this.type = type; this.watchId = watchId; @@ -61,7 +56,7 @@ public String getWatchId() { } @Override - public boolean equals(@Nullable Object obj) { + public boolean equals(Object obj) { if (obj instanceof ISourceLocationChanged) { var other = (ISourceLocationChanged)obj; return Objects.equals(root, other.root) diff --git a/src/org/rascalmpl/uri/remote/jsonrpc/ISourceLocationRequest.java b/src/org/rascalmpl/uri/remote/jsonrpc/ISourceLocationRequest.java index e1370538e1f..85e4390666d 100644 --- a/src/org/rascalmpl/uri/remote/jsonrpc/ISourceLocationRequest.java +++ b/src/org/rascalmpl/uri/remote/jsonrpc/ISourceLocationRequest.java @@ -28,13 +28,9 @@ 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 ISourceLocationRequest { - @NonNull private ISourceLocation loc; public ISourceLocationRequest(ISourceLocation loc) { @@ -46,7 +42,7 @@ public ISourceLocation getLocation() { } @Override - public boolean equals(@Nullable Object obj) { + public boolean equals(Object obj) { if (obj instanceof ISourceLocationRequest) { return loc.equals(((ISourceLocationRequest)obj).loc); } diff --git a/src/org/rascalmpl/uri/remote/jsonrpc/LocationContentResponse.java b/src/org/rascalmpl/uri/remote/jsonrpc/LocationContentResponse.java index 00670a948fc..3aa98bfdac9 100644 --- a/src/org/rascalmpl/uri/remote/jsonrpc/LocationContentResponse.java +++ b/src/org/rascalmpl/uri/remote/jsonrpc/LocationContentResponse.java @@ -28,13 +28,10 @@ import java.util.Objects; -import org.checkerframework.checker.nullness.qual.NonNull; - public class LocationContentResponse { - @NonNull private final String content; - public LocationContentResponse(@NonNull String content) { + public LocationContentResponse(String content) { this.content = content; } diff --git a/src/org/rascalmpl/uri/remote/jsonrpc/RenameRequest.java b/src/org/rascalmpl/uri/remote/jsonrpc/RenameRequest.java index 9355516418a..f678ef888ae 100644 --- a/src/org/rascalmpl/uri/remote/jsonrpc/RenameRequest.java +++ b/src/org/rascalmpl/uri/remote/jsonrpc/RenameRequest.java @@ -28,15 +28,10 @@ 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 RenameRequest { - @NonNull private ISourceLocation from; - @NonNull private ISourceLocation to; private boolean overwrite; @@ -60,7 +55,7 @@ public boolean isOverwrite() { } @Override - public boolean equals(@Nullable Object obj) { + public boolean equals(Object obj) { if (obj instanceof RenameRequest) { var other = (RenameRequest)obj; return Objects.equals(from, other.from) diff --git a/src/org/rascalmpl/uri/remote/jsonrpc/SetLastModifiedRequest.java b/src/org/rascalmpl/uri/remote/jsonrpc/SetLastModifiedRequest.java index e6c6399fcf7..1627179dad1 100644 --- a/src/org/rascalmpl/uri/remote/jsonrpc/SetLastModifiedRequest.java +++ b/src/org/rascalmpl/uri/remote/jsonrpc/SetLastModifiedRequest.java @@ -28,8 +28,6 @@ import java.util.Objects; -import org.checkerframework.checker.nullness.qual.Nullable; - import io.usethesource.vallang.ISourceLocation; public class SetLastModifiedRequest extends ISourceLocationRequest{ @@ -45,7 +43,7 @@ public long getTimestamp() { } @Override - public boolean equals(@Nullable Object obj) { + public boolean equals(Object obj) { if (obj instanceof SetLastModifiedRequest) { var other = (SetLastModifiedRequest)obj; return super.equals(other) && timestamp == other.timestamp; diff --git a/src/org/rascalmpl/uri/remote/jsonrpc/SourceLocationResponse.java b/src/org/rascalmpl/uri/remote/jsonrpc/SourceLocationResponse.java index b24c0fd44b1..a4a509965d1 100644 --- a/src/org/rascalmpl/uri/remote/jsonrpc/SourceLocationResponse.java +++ b/src/org/rascalmpl/uri/remote/jsonrpc/SourceLocationResponse.java @@ -28,15 +28,12 @@ import java.util.Objects; -import org.checkerframework.checker.nullness.qual.NonNull; - import io.usethesource.vallang.ISourceLocation; public class SourceLocationResponse { - @NonNull private final ISourceLocation loc; - public SourceLocationResponse(@NonNull ISourceLocation loc) { + public SourceLocationResponse(ISourceLocation loc) { this.loc = loc; } diff --git a/src/org/rascalmpl/uri/remote/jsonrpc/WatchRequest.java b/src/org/rascalmpl/uri/remote/jsonrpc/WatchRequest.java index a0a48ecee5f..296ba446780 100644 --- a/src/org/rascalmpl/uri/remote/jsonrpc/WatchRequest.java +++ b/src/org/rascalmpl/uri/remote/jsonrpc/WatchRequest.java @@ -28,13 +28,9 @@ 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 watchId; private boolean recursive; @@ -54,7 +50,7 @@ public boolean isRecursive() { } @Override - public boolean equals(@Nullable Object obj) { + public boolean equals(Object obj) { if (obj instanceof WatchRequest) { var other = (WatchRequest)obj; return super.equals(other) diff --git a/src/org/rascalmpl/uri/remote/jsonrpc/WriteFileRequest.java b/src/org/rascalmpl/uri/remote/jsonrpc/WriteFileRequest.java index 5c21bd2d277..cae2e525760 100644 --- a/src/org/rascalmpl/uri/remote/jsonrpc/WriteFileRequest.java +++ b/src/org/rascalmpl/uri/remote/jsonrpc/WriteFileRequest.java @@ -28,13 +28,9 @@ 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 final String content; private final boolean append; @@ -54,7 +50,7 @@ public boolean isAppend() { } @Override - public boolean equals(@Nullable Object obj) { + public boolean equals(Object obj) { if (obj instanceof WriteFileRequest) { var other = (WriteFileRequest)obj; return super.equals(obj) From 5d2bef9c1bcdb506d965c6c2987b7e9662df307e Mon Sep 17 00:00:00 2001 From: Rodin Aarssen Date: Wed, 8 Apr 2026 09:23:32 +0200 Subject: [PATCH 86/98] Made all members of request/response classes final --- src/org/rascalmpl/uri/remote/jsonrpc/CopyRequest.java | 9 ++++----- .../uri/remote/jsonrpc/ISourceLocationChanged.java | 6 +++--- .../uri/remote/jsonrpc/ISourceLocationRequest.java | 2 +- src/org/rascalmpl/uri/remote/jsonrpc/RemoveRequest.java | 2 +- src/org/rascalmpl/uri/remote/jsonrpc/RenameRequest.java | 7 +++---- .../uri/remote/jsonrpc/SetLastModifiedRequest.java | 2 +- src/org/rascalmpl/uri/remote/jsonrpc/WatchRequest.java | 5 ++--- .../rascalmpl/uri/remote/jsonrpc/WriteFileRequest.java | 1 - 8 files changed, 15 insertions(+), 19 deletions(-) diff --git a/src/org/rascalmpl/uri/remote/jsonrpc/CopyRequest.java b/src/org/rascalmpl/uri/remote/jsonrpc/CopyRequest.java index 7f16e6259a4..6a0fb695c37 100644 --- a/src/org/rascalmpl/uri/remote/jsonrpc/CopyRequest.java +++ b/src/org/rascalmpl/uri/remote/jsonrpc/CopyRequest.java @@ -31,11 +31,10 @@ import io.usethesource.vallang.ISourceLocation; public class CopyRequest { - private ISourceLocation from; - private ISourceLocation to; - - private boolean recursive; - private boolean overwrite; + private final ISourceLocation from; + private final ISourceLocation to; + private final boolean recursive; + private final boolean overwrite; public CopyRequest(ISourceLocation from, ISourceLocation to, boolean recursive, boolean overwrite) { this.from = from; diff --git a/src/org/rascalmpl/uri/remote/jsonrpc/ISourceLocationChanged.java b/src/org/rascalmpl/uri/remote/jsonrpc/ISourceLocationChanged.java index 5a8274f1cc2..fe2723cd934 100644 --- a/src/org/rascalmpl/uri/remote/jsonrpc/ISourceLocationChanged.java +++ b/src/org/rascalmpl/uri/remote/jsonrpc/ISourceLocationChanged.java @@ -33,9 +33,9 @@ import io.usethesource.vallang.ISourceLocation; public class ISourceLocationChanged { - private ISourceLocation root; - private ISourceLocationChangeType type; - private String watchId; + private final ISourceLocation root; + private final ISourceLocationChangeType type; + private final String watchId; public ISourceLocationChanged(ISourceLocation root, ISourceLocationChangeType type, String watchId) { this.root = root; diff --git a/src/org/rascalmpl/uri/remote/jsonrpc/ISourceLocationRequest.java b/src/org/rascalmpl/uri/remote/jsonrpc/ISourceLocationRequest.java index 85e4390666d..ef5712c3654 100644 --- a/src/org/rascalmpl/uri/remote/jsonrpc/ISourceLocationRequest.java +++ b/src/org/rascalmpl/uri/remote/jsonrpc/ISourceLocationRequest.java @@ -31,7 +31,7 @@ import io.usethesource.vallang.ISourceLocation; public class ISourceLocationRequest { - private ISourceLocation loc; + private final ISourceLocation loc; public ISourceLocationRequest(ISourceLocation loc) { this.loc = loc; diff --git a/src/org/rascalmpl/uri/remote/jsonrpc/RemoveRequest.java b/src/org/rascalmpl/uri/remote/jsonrpc/RemoveRequest.java index f0dcc9ca235..4ae03176ee2 100644 --- a/src/org/rascalmpl/uri/remote/jsonrpc/RemoveRequest.java +++ b/src/org/rascalmpl/uri/remote/jsonrpc/RemoveRequest.java @@ -33,7 +33,7 @@ import io.usethesource.vallang.ISourceLocation; public class RemoveRequest extends ISourceLocationRequest { - private boolean recursive; + private final boolean recursive; public RemoveRequest(ISourceLocation loc, boolean recursive) { super(loc); diff --git a/src/org/rascalmpl/uri/remote/jsonrpc/RenameRequest.java b/src/org/rascalmpl/uri/remote/jsonrpc/RenameRequest.java index f678ef888ae..c37c81b16f6 100644 --- a/src/org/rascalmpl/uri/remote/jsonrpc/RenameRequest.java +++ b/src/org/rascalmpl/uri/remote/jsonrpc/RenameRequest.java @@ -31,10 +31,9 @@ import io.usethesource.vallang.ISourceLocation; public class RenameRequest { - private ISourceLocation from; - private ISourceLocation to; - - private boolean overwrite; + private final ISourceLocation from; + private final ISourceLocation to; + private final boolean overwrite; public RenameRequest(ISourceLocation from, ISourceLocation to, boolean overwrite) { this.from = from; diff --git a/src/org/rascalmpl/uri/remote/jsonrpc/SetLastModifiedRequest.java b/src/org/rascalmpl/uri/remote/jsonrpc/SetLastModifiedRequest.java index 1627179dad1..142b2798fe5 100644 --- a/src/org/rascalmpl/uri/remote/jsonrpc/SetLastModifiedRequest.java +++ b/src/org/rascalmpl/uri/remote/jsonrpc/SetLastModifiedRequest.java @@ -31,7 +31,7 @@ import io.usethesource.vallang.ISourceLocation; public class SetLastModifiedRequest extends ISourceLocationRequest{ - private long timestamp; + private final long timestamp; public SetLastModifiedRequest(ISourceLocation loc, long timestamp) { super(loc); diff --git a/src/org/rascalmpl/uri/remote/jsonrpc/WatchRequest.java b/src/org/rascalmpl/uri/remote/jsonrpc/WatchRequest.java index 296ba446780..eba75913ab1 100644 --- a/src/org/rascalmpl/uri/remote/jsonrpc/WatchRequest.java +++ b/src/org/rascalmpl/uri/remote/jsonrpc/WatchRequest.java @@ -31,9 +31,8 @@ import io.usethesource.vallang.ISourceLocation; public class WatchRequest extends ISourceLocationRequest { - private String watchId; - - private boolean recursive; + private final String watchId; + private final boolean recursive; public WatchRequest(ISourceLocation loc, boolean recursive, String watchId) { super(loc); diff --git a/src/org/rascalmpl/uri/remote/jsonrpc/WriteFileRequest.java b/src/org/rascalmpl/uri/remote/jsonrpc/WriteFileRequest.java index cae2e525760..e87d46cc8a7 100644 --- a/src/org/rascalmpl/uri/remote/jsonrpc/WriteFileRequest.java +++ b/src/org/rascalmpl/uri/remote/jsonrpc/WriteFileRequest.java @@ -32,7 +32,6 @@ public class WriteFileRequest extends ISourceLocationRequest { private final String content; - private final boolean append; public WriteFileRequest(ISourceLocation loc, String content, boolean append) { From 8d6ad8bebb8c91d5b092124b64d92e0bbf6a893c Mon Sep 17 00:00:00 2001 From: Rodin Aarssen Date: Wed, 8 Apr 2026 10:13:24 +0200 Subject: [PATCH 87/98] Removed enum values from ISourceLocationWatcher.ISourceLocationChangeType and aligned uses of the interfaces --- .../rascalmpl/uri/ISourceLocationWatcher.java | 24 +------------------ .../RemoteExternalResolverRegistry.java | 2 +- .../jsonrpc/ISourceLocationChangeType.java | 15 +++++++++++- 3 files changed, 16 insertions(+), 25 deletions(-) diff --git a/src/org/rascalmpl/uri/ISourceLocationWatcher.java b/src/org/rascalmpl/uri/ISourceLocationWatcher.java index 6fd2b7852e5..b18124c3436 100644 --- a/src/org/rascalmpl/uri/ISourceLocationWatcher.java +++ b/src/org/rascalmpl/uri/ISourceLocationWatcher.java @@ -62,29 +62,7 @@ default boolean isChanged() { } public enum ISourceLocationChangeType { - // The numeric values are based on the values in the FileChangeType enum of VS Code - CREATED(2), - DELETED(3), - MODIFIED(1); - - private final int value; - - public int getValue() { - return value; - } - - ISourceLocationChangeType(int value) { - this.value = value; - } - - public static ISourceLocationChangeType fromValue(int value) { - switch (value) { - case 2: return CREATED; - case 3: return DELETED; - case 1: return MODIFIED; - default: throw new IllegalArgumentException("Unknown ISourceLocationChangeType value " + value); - } - } + CREATED, DELETED, MODIFIED; } static ISourceLocationChanged created(ISourceLocation loc) { diff --git a/src/org/rascalmpl/uri/remote/RemoteExternalResolverRegistry.java b/src/org/rascalmpl/uri/remote/RemoteExternalResolverRegistry.java index 364b3b3c4ec..2b72dc194ac 100644 --- a/src/org/rascalmpl/uri/remote/RemoteExternalResolverRegistry.java +++ b/src/org/rascalmpl/uri/remote/RemoteExternalResolverRegistry.java @@ -544,7 +544,7 @@ public void sourceLocationChanged(org.rascalmpl.uri.remote.jsonrpc.ISourceLocati throw new ResponseErrorException(new ResponseError(ResponseErrorCode.RequestFailed, "Received notification for unregistered watch", root)); } try { - switch (ISourceLocationChangeType.fromValue(changed.getChangeType().getValue())) { + switch (changed.getChangeType()) { case CREATED: watcher.publish(ISourceLocationWatcher.created(root)); break; diff --git a/src/org/rascalmpl/uri/remote/jsonrpc/ISourceLocationChangeType.java b/src/org/rascalmpl/uri/remote/jsonrpc/ISourceLocationChangeType.java index 070dde2db87..83bad0aa032 100644 --- a/src/org/rascalmpl/uri/remote/jsonrpc/ISourceLocationChangeType.java +++ b/src/org/rascalmpl/uri/remote/jsonrpc/ISourceLocationChangeType.java @@ -63,7 +63,20 @@ public static ISourceLocationWatcher.ISourceLocationChangeType translate(ISource case MODIFIED: return ISourceLocationWatcher.ISourceLocationChangeType.MODIFIED; default: - throw new RuntimeException("Forgotten type: " + lsp); + throw new IllegalArgumentException("Unknown ISourceLocationChangeType " + lsp); + } + } + + public static ISourceLocationChangeType translate(ISourceLocationWatcher.ISourceLocationChangeType rascal) { + switch (rascal) { + case CREATED: + return ISourceLocationChangeType.CREATED; + case DELETED: + return ISourceLocationChangeType.DELETED; + case MODIFIED: + return ISourceLocationChangeType.MODIFIED; + default: + throw new IllegalArgumentException("Unknown ISourceLocationChangeType " + rascal); } } } From 1124c9324386c8338dc8f30ab876919988ed42d2 Mon Sep 17 00:00:00 2001 From: Rodin Aarssen Date: Wed, 8 Apr 2026 10:16:06 +0200 Subject: [PATCH 88/98] Catching socketException around available --- .../rascalmpl/uri/remote/RemoteExternalResolverRegistry.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/org/rascalmpl/uri/remote/RemoteExternalResolverRegistry.java b/src/org/rascalmpl/uri/remote/RemoteExternalResolverRegistry.java index 2b72dc194ac..6f3efce8a7e 100644 --- a/src/org/rascalmpl/uri/remote/RemoteExternalResolverRegistry.java +++ b/src/org/rascalmpl/uri/remote/RemoteExternalResolverRegistry.java @@ -199,7 +199,7 @@ public int read(byte[] b, int off, int len) throws IOException { @Override public int available() throws IOException { - return original.available(); + return socketExceptionCatcher(original::available); } @Override From a03b929a3e98ba2ac1a9d74a7e51cbad674bfb66 Mon Sep 17 00:00:00 2001 From: Rodin Aarssen Date: Wed, 8 Apr 2026 12:48:46 +0200 Subject: [PATCH 89/98] Using correct translation for ISourceLocationChanged --- src/org/rascalmpl/uri/remote/RascalFileSystemServices.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/org/rascalmpl/uri/remote/RascalFileSystemServices.java b/src/org/rascalmpl/uri/remote/RascalFileSystemServices.java index 1de97903017..04b7d95ff52 100644 --- a/src/org/rascalmpl/uri/remote/RascalFileSystemServices.java +++ b/src/org/rascalmpl/uri/remote/RascalFileSystemServices.java @@ -211,7 +211,7 @@ public CompletableFuture watch(WatchRequest params) { return async(() -> { URIResolverRegistry.getInstance().watch(params.getLocation(), params.isRecursive(), changed -> client.sourceLocationChanged(new ISourceLocationChanged( - changed.getLocation(), ISourceLocationChangeType.forValue(changed.getChangeType().getValue()), params.getWatchId() + changed.getLocation(), ISourceLocationChangeType.translate(changed.getChangeType()), params.getWatchId() )) ); }); From f1ad341cbea6b384da930884e9fe0f7e5a6026dd Mon Sep 17 00:00:00 2001 From: Rodin Aarssen Date: Wed, 8 Apr 2026 13:06:53 +0200 Subject: [PATCH 90/98] Added callback to Base64EncodingOutputStream that runs after close --- .../base64/Base64EncodingOutputStream.java | 5 +- .../util/base64/StreamingBase64.java | 56 +++++++++++++++++-- 2 files changed, 54 insertions(+), 7 deletions(-) diff --git a/src/org/rascalmpl/util/base64/Base64EncodingOutputStream.java b/src/org/rascalmpl/util/base64/Base64EncodingOutputStream.java index b07c0594d98..7d1494176fe 100644 --- a/src/org/rascalmpl/util/base64/Base64EncodingOutputStream.java +++ b/src/org/rascalmpl/util/base64/Base64EncodingOutputStream.java @@ -42,6 +42,7 @@ public class Base64EncodingOutputStream extends OutputStream { private final Encoder encoder; private final Base64CharWriter target; + private final Runnable onClose; private static final int ENC_INPUT_SIZE = 3 * 1024; private static final int ENC_OUTPUT_SIZE = 4 * 1024; @@ -50,9 +51,10 @@ public class Base64EncodingOutputStream extends OutputStream { private byte[] buffer = new byte[ENC_INPUT_SIZE]; private int written = 0; - public Base64EncodingOutputStream(Base64CharWriter writer, boolean padding) { + public Base64EncodingOutputStream(Base64CharWriter writer, boolean padding, Runnable onClose) { encoder = padding ? Base64.getEncoder() : Base64.getEncoder().withoutPadding(); this.target = writer; + this.onClose = onClose; } private void actualFlush() throws IOException { @@ -108,6 +110,7 @@ public void close() throws IOException { closed = true; actualFlush(); this.target.close(); + onClose.run(); } diff --git a/src/org/rascalmpl/util/base64/StreamingBase64.java b/src/org/rascalmpl/util/base64/StreamingBase64.java index d2a052059f0..fba3261316a 100644 --- a/src/org/rascalmpl/util/base64/StreamingBase64.java +++ b/src/org/rascalmpl/util/base64/StreamingBase64.java @@ -45,7 +45,18 @@ public class StreamingBase64 { * The OutputStream needs to be closed before the target can be consumed */ public static OutputStream encode(Writer target) { - return encode(target, true); + return encode(target, () -> {}); + } + + /** + * Create an OutputStream that on writing bytes, encodes them to the target writer + * + * The OutputStream needs to be closed before the target can be consumed + * + * The callback Runnable is run after the OutputStream closes + */ + public static OutputStream encode(Writer target, Runnable onClose) { + return encode(target, true, onClose); } /** @@ -54,7 +65,18 @@ public static OutputStream encode(Writer target) { * The OutputStream needs to be closed before the target can be consumed */ public static OutputStream encode(Writer target, boolean padding) { - return encode(Base64CharWriter.latinBytesTo(target), padding); + return encode(target, padding, () -> {}); + } + + /** + * Create an OutputStream that on writing bytes, encodes them to the target writer, and you can disable the optional `=` padding characters + * + * The OutputStream needs to be closed before the target can be consumed + * + * The callback Runnable is run after the OutputStream closes + */ + public static OutputStream encode(Writer target, boolean padding, Runnable onClose) { + return encode(Base64CharWriter.latinBytesTo(target), padding, onClose); } /** @@ -63,7 +85,18 @@ public static OutputStream encode(Writer target, boolean padding) { * The OutputStream needs to be closed before the target can be consumed */ public static OutputStream encode(StringBuilder target) { - return encode(target, true); + return encode(target, () -> {}); + } + + /** + * Create an OutputStream that on writing bytes, encodes them to the StringBuilder + * + * The OutputStream needs to be closed before the target can be consumed + * + * The callback Runnable is run after the OutputStream closes + */ + public static OutputStream encode(StringBuilder target, Runnable onClose) { + return encode(target, true, onClose); } /** @@ -72,11 +105,22 @@ public static OutputStream encode(StringBuilder target) { * The OutputStream needs to be closed before the target can be consumed */ public static OutputStream encode(StringBuilder target, boolean padding) { - return encode(Base64CharWriter.latinBytesTo(target), padding); + return encode(target, padding, () -> {}); + } + + /** + * Create an OutputStream that on writing bytes, encodes them to the StringBuilder, and you can disable the optional `=` padding characters + * + * The OutputStream needs to be closed before the target can be consumed + * + * The callback Runnable is run after the OutputStream closes + */ + public static OutputStream encode(StringBuilder target, boolean padding, Runnable onClose) { + return encode(Base64CharWriter.latinBytesTo(target), padding, onClose); } - private static OutputStream encode(Base64CharWriter target, boolean padding) { - return new Base64EncodingOutputStream(target, padding); + private static OutputStream encode(Base64CharWriter target, boolean padding, Runnable onClose) { + return new Base64EncodingOutputStream(target, padding, onClose); } /** From e549d4075b71201999621721b301822dad26d59b Mon Sep 17 00:00:00 2001 From: Rodin Aarssen Date: Wed, 8 Apr 2026 13:10:31 +0200 Subject: [PATCH 91/98] Avoiding extra allocation of byte array by providing a callback --- .../RemoteExternalResolverRegistry.java | 24 +++++++------------ 1 file changed, 8 insertions(+), 16 deletions(-) diff --git a/src/org/rascalmpl/uri/remote/RemoteExternalResolverRegistry.java b/src/org/rascalmpl/uri/remote/RemoteExternalResolverRegistry.java index 6f3efce8a7e..440ab3b88bd 100644 --- a/src/org/rascalmpl/uri/remote/RemoteExternalResolverRegistry.java +++ b/src/org/rascalmpl/uri/remote/RemoteExternalResolverRegistry.java @@ -443,24 +443,16 @@ public FileAttributes stat(ISourceLocation loc) throws IOException { @Override public OutputStream getOutputStream(ISourceLocation loc, boolean append) throws IOException { - return new ByteArrayOutputStream() { - private boolean closed = false; - - @Override - public void close() throws IOException { - if (closed) { - return; - } - closed = true; - var content = new StringBuilder(); - try (var input = new ByteArrayInputStream(this.toByteArray())) { - StreamingBase64.encode(input, content, true); - } - cachedDirectoryListing.invalidate(URIUtil.getParentLocation(loc)); + var content = new StringBuilder(); + return StreamingBase64.encode(content, () -> { + cachedDirectoryListing.invalidate(URIUtil.getParentLocation(loc)); + try { call(remote::writeFile, new WriteFileRequest(loc, content.toString(), append)); - cachedDirectoryListing.invalidate(URIUtil.getParentLocation(loc)); + } catch (IOException e) { + // Ignore } - }; + cachedDirectoryListing.invalidate(URIUtil.getParentLocation(loc)); + }); } @Override From 25a1f5355b9e8777811c186d7b5660c7bbaba27a Mon Sep 17 00:00:00 2001 From: Rodin Aarssen Date: Wed, 8 Apr 2026 15:26:24 +0200 Subject: [PATCH 92/98] Layout --- src/org/rascalmpl/uri/ISourceLocationWatcher.java | 4 +++- src/org/rascalmpl/util/NamedThreadPool.java | 1 - 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/org/rascalmpl/uri/ISourceLocationWatcher.java b/src/org/rascalmpl/uri/ISourceLocationWatcher.java index b18124c3436..c1c5aa9bdcb 100644 --- a/src/org/rascalmpl/uri/ISourceLocationWatcher.java +++ b/src/org/rascalmpl/uri/ISourceLocationWatcher.java @@ -62,7 +62,9 @@ default boolean isChanged() { } public enum ISourceLocationChangeType { - CREATED, DELETED, MODIFIED; + CREATED, + DELETED, + MODIFIED; } static ISourceLocationChanged created(ISourceLocation loc) { diff --git a/src/org/rascalmpl/util/NamedThreadPool.java b/src/org/rascalmpl/util/NamedThreadPool.java index efd38a3bd6e..f0036691304 100644 --- a/src/org/rascalmpl/util/NamedThreadPool.java +++ b/src/org/rascalmpl/util/NamedThreadPool.java @@ -59,5 +59,4 @@ private static ThreadFactory factory(String name, boolean daemon) { return t; }; } - } From 77bb3d09eed7256f8f1fcae7dbe7ae6c57a01348 Mon Sep 17 00:00:00 2001 From: Rodin Aarssen Date: Wed, 8 Apr 2026 15:26:53 +0200 Subject: [PATCH 93/98] Reordered some interfaces and renames some parameters for uniformity --- .../RemoteExternalResolverRegistry.java | 26 +++++++++---------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/src/org/rascalmpl/uri/remote/RemoteExternalResolverRegistry.java b/src/org/rascalmpl/uri/remote/RemoteExternalResolverRegistry.java index 440ab3b88bd..1bb526084c4 100644 --- a/src/org/rascalmpl/uri/remote/RemoteExternalResolverRegistry.java +++ b/src/org/rascalmpl/uri/remote/RemoteExternalResolverRegistry.java @@ -143,6 +143,11 @@ private interface ThrowingTriFunction { R apply(T t, U u, V v) throws E; } + @FunctionalInterface + private interface ThrowingRunnable { + void run() throws E; + } + @FunctionalInterface private interface ThrowingConsumer { void accept(T t) throws E; @@ -153,11 +158,6 @@ private interface ThrowingTriConsumer { void accept(T t, U u, V v) throws E; } - @FunctionalInterface - private interface ThrowingRunnable { - void run() throws E; - } - private InputStream errorDetectingInputStream(InputStream original) { return new InputStream() { private T socketExceptionCatcher(ThrowingSupplier function) throws IOException { @@ -169,9 +169,9 @@ private T socketExceptionCatcher(ThrowingSupplier function) } } - private R socketExceptionCatcher(ThrowingFunction function, T arg) throws IOException { + private R socketExceptionCatcher(ThrowingFunction function, T t) throws IOException { try { - return function.apply(arg); + return function.apply(t); } catch (SocketException e) { scheduleReconnect(); throw e; @@ -226,27 +226,27 @@ public int readNBytes(byte[] b, int off, int len) throws IOException { private OutputStream errorDetectingOutputStream(OutputStream original) { return new OutputStream() { - private void socketExceptionCatcher(ThrowingConsumer consumer, T arg) throws IOException { + private void socketExceptionCatcher(ThrowingRunnable runnable) throws IOException { try { - consumer.accept(arg); + runnable.run(); } catch (SocketException e) { scheduleReconnect(); throw e; } } - private void socketExceptionCatcher(ThrowingTriConsumer consumer, T t, U u, V v) throws IOException { + private void socketExceptionCatcher(ThrowingConsumer consumer, T t) throws IOException { try { - consumer.accept(t, u, v); + consumer.accept(t); } catch (SocketException e) { scheduleReconnect(); throw e; } } - private void socketExceptionCatcher(ThrowingRunnable runnable) throws IOException { + private void socketExceptionCatcher(ThrowingTriConsumer consumer, T t, U u, V v) throws IOException { try { - runnable.run(); + consumer.accept(t, u, v); } catch (SocketException e) { scheduleReconnect(); throw e; From 5a67222cd8a9561445129932ae0940811eb19a0a Mon Sep 17 00:00:00 2001 From: Rodin Aarssen Date: Wed, 8 Apr 2026 15:44:59 +0200 Subject: [PATCH 94/98] Added license header --- .../vfs/IRemoteResolverRegistryClient.java | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/src/org/rascalmpl/uri/vfs/IRemoteResolverRegistryClient.java b/src/org/rascalmpl/uri/vfs/IRemoteResolverRegistryClient.java index a3b53fa4601..ea75d4b6ee2 100644 --- a/src/org/rascalmpl/uri/vfs/IRemoteResolverRegistryClient.java +++ b/src/org/rascalmpl/uri/vfs/IRemoteResolverRegistryClient.java @@ -1,3 +1,29 @@ +/* + * Copyright (c) 2018-2026, 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.uri.vfs; import org.eclipse.lsp4j.jsonrpc.services.JsonNotification; From 4db63e40571285baebb3d32443bc4e24a80047c3 Mon Sep 17 00:00:00 2001 From: Rodin Aarssen Date: Wed, 8 Apr 2026 15:46:16 +0200 Subject: [PATCH 95/98] Layout --- src/org/rascalmpl/util/Lazy.java | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/org/rascalmpl/util/Lazy.java b/src/org/rascalmpl/util/Lazy.java index 8253d6e8328..21633b893f8 100644 --- a/src/org/rascalmpl/util/Lazy.java +++ b/src/org/rascalmpl/util/Lazy.java @@ -42,9 +42,6 @@ public T get() { } return result; } - }; - } - } From 0261916a8c102bbdd7773df6aa42bb92ecb312a6 Mon Sep 17 00:00:00 2001 From: Rodin Aarssen Date: Wed, 8 Apr 2026 16:00:03 +0200 Subject: [PATCH 96/98] Unused imports --- .../rascalmpl/uri/remote/RemoteExternalResolverRegistry.java | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/org/rascalmpl/uri/remote/RemoteExternalResolverRegistry.java b/src/org/rascalmpl/uri/remote/RemoteExternalResolverRegistry.java index 1bb526084c4..da10ec72ae2 100644 --- a/src/org/rascalmpl/uri/remote/RemoteExternalResolverRegistry.java +++ b/src/org/rascalmpl/uri/remote/RemoteExternalResolverRegistry.java @@ -26,8 +26,6 @@ */ package org.rascalmpl.uri.remote; -import java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; From a4514cfc0193becb61b775cea07c7bfd49f1d73b Mon Sep 17 00:00:00 2001 From: Rodin Aarssen Date: Wed, 8 Apr 2026 16:02:59 +0200 Subject: [PATCH 97/98] Removed another Nullable annotation from request class --- src/org/rascalmpl/uri/remote/jsonrpc/RemoveRequest.java | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/org/rascalmpl/uri/remote/jsonrpc/RemoveRequest.java b/src/org/rascalmpl/uri/remote/jsonrpc/RemoveRequest.java index 4ae03176ee2..d32a65ad7b3 100644 --- a/src/org/rascalmpl/uri/remote/jsonrpc/RemoveRequest.java +++ b/src/org/rascalmpl/uri/remote/jsonrpc/RemoveRequest.java @@ -28,8 +28,6 @@ import java.util.Objects; -import org.checkerframework.checker.nullness.qual.Nullable; - import io.usethesource.vallang.ISourceLocation; public class RemoveRequest extends ISourceLocationRequest { @@ -45,7 +43,7 @@ public boolean isRecursive() { } @Override - public boolean equals(@Nullable Object obj) { + public boolean equals(Object obj) { if (obj instanceof RemoveRequest) { var other = (RemoveRequest)obj; return super.equals(obj) From 7dc808030bafe1a9891726a260ec79f600850ec8 Mon Sep 17 00:00:00 2001 From: Rodin Aarssen Date: Wed, 8 Apr 2026 16:03:25 +0200 Subject: [PATCH 98/98] Removed nullable annotation from RemoteExternalResolverRegistry --- .../rascalmpl/uri/remote/RemoteExternalResolverRegistry.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/org/rascalmpl/uri/remote/RemoteExternalResolverRegistry.java b/src/org/rascalmpl/uri/remote/RemoteExternalResolverRegistry.java index da10ec72ae2..a979db563b5 100644 --- a/src/org/rascalmpl/uri/remote/RemoteExternalResolverRegistry.java +++ b/src/org/rascalmpl/uri/remote/RemoteExternalResolverRegistry.java @@ -54,7 +54,6 @@ import java.util.stream.Collectors; import java.util.stream.Stream; -import org.checkerframework.checker.nullness.qual.Nullable; import org.eclipse.lsp4j.jsonrpc.Launcher; import org.eclipse.lsp4j.jsonrpc.ResponseErrorException; import org.eclipse.lsp4j.jsonrpc.messages.ResponseError; @@ -602,7 +601,7 @@ public int hashCode() { } @Override - public boolean equals(@Nullable Object obj) { + public boolean equals(Object obj) { if (this == obj) { return true; }