From e1f7db233bb51d6b0a629cfe4accefea1e9910c1 Mon Sep 17 00:00:00 2001 From: manykeys Date: Fri, 20 Feb 2026 09:31:25 +0500 Subject: [PATCH 01/12] Add LSP jar file system support --- .../meta/internal/metals/ClientCommands.scala | 6 + .../internal/metals/ClientConfiguration.scala | 3 + .../metals/FallbackMetalsLspService.scala | 1 + .../internal/metals/FileDecoderProvider.scala | 3 + .../meta/internal/metals/IndexProviders.scala | 2 + .../scala/meta/internal/metals/Indexer.scala | 3 + .../metals/InitializationOptions.scala | 3 + .../metals/LSPFileSystemProvider.scala | 170 ++++++++ .../internal/metals/MetalsLspService.scala | 11 + .../metals/ProjectMetalsLspService.scala | 2 + .../meta/internal/metals/ServerCommands.scala | 27 ++ .../meta/internal/metals/URIMapper.scala | 318 ++++++++++++++ .../internal/metals/WorkspaceLspService.scala | 399 ++++++++++++++++-- .../internal/tvp/MetalsTreeViewProvider.scala | 13 +- .../meta/metals/MetalsLanguageServer.scala | 5 +- 15 files changed, 922 insertions(+), 44 deletions(-) create mode 100644 metals/src/main/scala/scala/meta/internal/metals/LSPFileSystemProvider.scala create mode 100644 metals/src/main/scala/scala/meta/internal/metals/URIMapper.scala diff --git a/metals/src/main/scala/scala/meta/internal/metals/ClientCommands.scala b/metals/src/main/scala/scala/meta/internal/metals/ClientCommands.scala index 69ea78db52a..56e0ca1b6af 100644 --- a/metals/src/main/scala/scala/meta/internal/metals/ClientCommands.scala +++ b/metals/src/main/scala/scala/meta/internal/metals/ClientCommands.scala @@ -38,6 +38,12 @@ object ClientCommands { |""".stripMargin, ) + val LibraryFileSystemReady = new Command( + "metals-library-filesystem-ready", + "Library File System Ready", + "Notifies client that library file system is ready", + ) + val RunDoctor = new ParametrizedCommand[String]( "metals-doctor-run", "Run doctor", diff --git a/metals/src/main/scala/scala/meta/internal/metals/ClientConfiguration.scala b/metals/src/main/scala/scala/meta/internal/metals/ClientConfiguration.scala index 18dad4ba1f5..d33085f83e4 100644 --- a/metals/src/main/scala/scala/meta/internal/metals/ClientConfiguration.scala +++ b/metals/src/main/scala/scala/meta/internal/metals/ClientConfiguration.scala @@ -78,6 +78,9 @@ final class ClientConfiguration( def isVirtualDocumentSupported(): Boolean = initializationOptions.isVirtualDocumentSupported.getOrElse(false) + def isLibraryFileSystemSupported(): Boolean = + initializationOptions.isLibraryFileSystemSupported.getOrElse(false) + def icons(): Icons = initializationOptions.icons .map(Icons.fromString) diff --git a/metals/src/main/scala/scala/meta/internal/metals/FallbackMetalsLspService.scala b/metals/src/main/scala/scala/meta/internal/metals/FallbackMetalsLspService.scala index b9150466236..8e327613fe9 100644 --- a/metals/src/main/scala/scala/meta/internal/metals/FallbackMetalsLspService.scala +++ b/metals/src/main/scala/scala/meta/internal/metals/FallbackMetalsLspService.scala @@ -68,6 +68,7 @@ class FallbackMetalsLspService( folder, compilers, buildTargets, + uriMapper, () => userConfig, shellRunner, optFileSystemSemanticdbs, diff --git a/metals/src/main/scala/scala/meta/internal/metals/FileDecoderProvider.scala b/metals/src/main/scala/scala/meta/internal/metals/FileDecoderProvider.scala index a4fa5a3762d..5c41bca1aa5 100644 --- a/metals/src/main/scala/scala/meta/internal/metals/FileDecoderProvider.scala +++ b/metals/src/main/scala/scala/meta/internal/metals/FileDecoderProvider.scala @@ -86,6 +86,7 @@ final class FileDecoderProvider( workspace: AbsolutePath, compilers: Compilers, buildTargets: BuildTargets, + uriMapper: URIMapper, userConfig: () => UserConfiguration, shellRunner: ShellRunner, optFileSystemSemanticdbs: () => Option[FileSystemSemanticdbs], @@ -169,6 +170,8 @@ final class FileDecoderProvider( case "file" => decodeMetalsFile(uri) case "metalsDecode" => decodedFileContents(uri.getSchemeSpecificPart()) + case "metalsfs" => + decodedFileContents(uriMapper.convertToLocal(uriAsStr)) case _ => Future.successful( DecoderResponse.failed( diff --git a/metals/src/main/scala/scala/meta/internal/metals/IndexProviders.scala b/metals/src/main/scala/scala/meta/internal/metals/IndexProviders.scala index 74535d64f08..2e3f949e18a 100644 --- a/metals/src/main/scala/scala/meta/internal/metals/IndexProviders.scala +++ b/metals/src/main/scala/scala/meta/internal/metals/IndexProviders.scala @@ -29,6 +29,8 @@ trait IndexProviders { def referencesProvider: ReferenceProvider def workspaceSymbols: WorkspaceSymbolProvider def buildTargets: BuildTargets + def uriMapper: URIMapper + def lspFileSystemProvider: LSPFileSystemProvider def semanticDBIndexer: SemanticdbIndexer def fileWatcher: FileWatcher def focusedDocument: Option[AbsolutePath] diff --git a/metals/src/main/scala/scala/meta/internal/metals/Indexer.scala b/metals/src/main/scala/scala/meta/internal/metals/Indexer.scala index 28b7ad6659a..c4436432445 100644 --- a/metals/src/main/scala/scala/meta/internal/metals/Indexer.scala +++ b/metals/src/main/scala/scala/meta/internal/metals/Indexer.scala @@ -203,6 +203,9 @@ case class Indexer(indexProviders: IndexProviders)(implicit rc: ReportContext) { buildTool.importedBuild.dependencySources, ) } + if (clientConfig.isLibraryFileSystemSupported()) { + lspFileSystemProvider.sendLibraryFileSystemReady() + } // Schedule removal of unused toplevel symbols from cache if (usedJars.nonEmpty) sh.schedule( diff --git a/metals/src/main/scala/scala/meta/internal/metals/InitializationOptions.scala b/metals/src/main/scala/scala/meta/internal/metals/InitializationOptions.scala index f7372457a4a..c22509f44f6 100644 --- a/metals/src/main/scala/scala/meta/internal/metals/InitializationOptions.scala +++ b/metals/src/main/scala/scala/meta/internal/metals/InitializationOptions.scala @@ -69,6 +69,7 @@ final case class InitializationOptions( isHttpEnabled: Option[Boolean], commandInHtmlFormat: Option[CommandHTMLFormat], isVirtualDocumentSupported: Option[Boolean], + isLibraryFileSystemSupported: Option[Boolean], openFilesOnRenameProvider: Option[Boolean], quickPickProvider: Option[Boolean], readClipboardProvider: Option[Boolean], @@ -171,6 +172,8 @@ object InitializationOptions { .flatMap(CommandHTMLFormat.fromString), isVirtualDocumentSupported = jsonObj.getBooleanOption("isVirtualDocumentSupported"), + isLibraryFileSystemSupported = + jsonObj.getBooleanOption("isLibraryFileSystemSupported"), openFilesOnRenameProvider = jsonObj.getBooleanOption("openFilesOnRenameProvider"), quickPickProvider = jsonObj.getBooleanOption("quickPickProvider"), diff --git a/metals/src/main/scala/scala/meta/internal/metals/LSPFileSystemProvider.scala b/metals/src/main/scala/scala/meta/internal/metals/LSPFileSystemProvider.scala new file mode 100644 index 00000000000..2c635cd42d0 --- /dev/null +++ b/metals/src/main/scala/scala/meta/internal/metals/LSPFileSystemProvider.scala @@ -0,0 +1,170 @@ +package scala.meta.internal.metals + +import java.nio.charset.StandardCharsets +import java.nio.file.Files +import java.util.stream.Collectors + +import scala.concurrent.Await +import scala.concurrent.ExecutionContext +import scala.concurrent.Future +import scala.concurrent.duration.DurationInt + +import scala.meta.internal.metals.MetalsEnrichments._ +import scala.meta.internal.metals.clients.language.MetalsLanguageClient +import scala.meta.internal.mtags.URIEncoderDecoder + +/** Response indicating whether an entry is a file or directory. */ +case class FSReadDirectoryResponse(name: String, isFile: Boolean) + +/** Response containing the contents of a directory listing. */ +case class FSReadDirectoriesResponse( + name: String, + directories: Array[FSReadDirectoryResponse], + error: String, +) + +/** Response containing the textual contents of a file. */ +case class FSReadFileResponse(name: String, value: String, error: String) + +/** Response containing file metadata (existence and type). */ +case class FSStatResponse(name: String, isFile: Boolean, error: String) + +/** + * Handles virtual file system requests from the LSP client. + * + * Translates `metalsfs://` URIs into JAR file system reads using Java NIO, + * delegating URI resolution to [[URIMapper]] and `.class` decompilation + * to [[FileDecoderProvider]]. + */ +class LSPFileSystemProvider( + languageClient: MetalsLanguageClient, + uriMapper: URIMapper, + fileDecoderProvider: FileDecoderProvider, + clientConfig: ClientConfiguration, +)(implicit ec: ExecutionContext) { + + /** Notifies the client that the library file system is ready for use. */ + def sendLibraryFileSystemReady(): Unit = { + if (clientConfig.isLibraryFileSystemSupported()) { + val params = + ClientCommands.LibraryFileSystemReady.toExecuteCommandParams() + languageClient.metalsExecuteClientCommand(params) + } + } + + /** + * Lists the contents of a virtual directory. + * + * For top-level URIs returns the three root categories (jdk, jar, source). + * For category URIs returns the available archives from [[BuildTargets]]. + * For paths within an archive opens the corresponding NIO file system + * and lists the directory entries. + */ + def readDirectory(uri: String): Future[FSReadDirectoriesResponse] = Future { + val entries = uri match { + case URIMapper.parentURI => + Array( + FSReadDirectoryResponse(URIMapper.jdkDir, isFile = false), + FSReadDirectoryResponse(URIMapper.workspaceJarDir, isFile = false), + FSReadDirectoryResponse(URIMapper.sourceJarDir, isFile = false), + ) + case URIMapper.jdkURI => + uriMapper.getJDKs + .map(name => FSReadDirectoryResponse(name, isFile = false)) + .toArray + case URIMapper.workspaceJarURI => + uriMapper.getWorkspaceJars + .map(name => FSReadDirectoryResponse(name, isFile = false)) + .toArray + case URIMapper.sourceJarURI => + uriMapper.getSourceJars + .map(name => FSReadDirectoryResponse(name, isFile = false)) + .toArray + case _ => + val (fs, innerPath) = resolveInnerPath(uri) + val path = fs.fs.getPath(innerPath.getOrElse("/")) + Files + .list(path) + .collect(Collectors.toList()) + .asScala + .map(p => + FSReadDirectoryResponse( + p.getFileName.toString, + isFile = Files.isRegularFile(p), + ) + ) + .toArray + } + FSReadDirectoriesResponse(uri, entries, "") + } + + /** + * Reads the textual contents of a file inside a JAR archive. + * + * For `.class` files the bytecode is decompiled via CFR through + * [[FileDecoderProvider]]. All other files are read as UTF-8 text. + */ + def readFile(uri: String): Future[FSReadFileResponse] = Future { + val (fs, innerPath) = resolveInnerPath(uri) + val path = fs.fs.getPath(innerPath.getOrElse("/")) + val contents = + if (path.getFileName.toString.endsWith(".class")) { + val fut = fileDecoderProvider.decodedFileContents(path.toUri.toString) + val res = Await.result(fut, 10.minutes) + Option(res.value).filter(_.nonEmpty).getOrElse(res.error) + } else { + new String(Files.readAllBytes(path), StandardCharsets.UTF_8) + } + FSReadFileResponse(uri, contents, "") + } + + /** + * Returns metadata for the given URI, indicating whether it + * represents a file or a directory. + * + * Known root-level URIs are resolved statically; paths within + * an archive are checked via the NIO file system. + */ + def getSystemStat(uri: String): Future[FSStatResponse] = Future { + uri match { + case URIMapper.parentURI => + FSStatResponse(URIMapper.rootDir, isFile = false, "") + case URIMapper.jdkURI => + FSStatResponse(URIMapper.jdkDir, isFile = false, "") + case URIMapper.workspaceJarURI => + FSStatResponse(URIMapper.workspaceJarDir, isFile = false, "") + case URIMapper.sourceJarURI => + FSStatResponse(URIMapper.sourceJarDir, isFile = false, "") + case _ => + val (fs, innerPath) = resolveInnerPath(uri) + val path = fs.fs.getPath(innerPath.getOrElse("/")) + FSStatResponse(uri, isFile = Files.isRegularFile(path), "") + } + } + + /** + * Resolves a `metalsfs://` URI into a NIO [[FileSystemInfo]] and + * an optional inner path within the archive. + * + * Dispatches to the appropriate [[URIMapper]] method based on whether + * the URI falls under the jdk, workspace jar, or source jar category. + */ + private def resolveInnerPath( + uri: String + ): (FileSystemInfo, Option[String]) = { + val decoded = URIEncoderDecoder.decode(uri) + decoded match { + case jdk if jdk.startsWith(URIMapper.jdkURI) => + val (name, path) = URIMapper.getURIParts(jdk, URIMapper.jdkURI) + (uriMapper.getJDKFileSystem(name), path) + case jar if jar.startsWith(URIMapper.workspaceJarURI) => + val (name, path) = + URIMapper.getURIParts(jar, URIMapper.workspaceJarURI) + (uriMapper.getWorkspaceJarFileSystem(name), path) + case src if src.startsWith(URIMapper.sourceJarURI) => + val (name, path) = + URIMapper.getURIParts(src, URIMapper.sourceJarURI) + (uriMapper.getSourceJarFileSystem(name), path) + } + } +} diff --git a/metals/src/main/scala/scala/meta/internal/metals/MetalsLspService.scala b/metals/src/main/scala/scala/meta/internal/metals/MetalsLspService.scala index 99fb844b43e..fad05c6a4d0 100644 --- a/metals/src/main/scala/scala/meta/internal/metals/MetalsLspService.scala +++ b/metals/src/main/scala/scala/meta/internal/metals/MetalsLspService.scala @@ -167,6 +167,9 @@ abstract class MetalsLspService( val buildTargets: BuildTargets = BuildTargets.from(folder, mainBuildTargetsData, tables) + val uriMapper: URIMapper = + URIMapper(buildTargets, () => userConfig.javaHome) + implicit val reports: StdReportContext = new StdReportContext( folder.toNIO, _.flatMap { uri => @@ -603,6 +606,14 @@ abstract class MetalsLspService( protected def fileDecoderProvider: FileDecoderProvider + override lazy val lspFileSystemProvider: LSPFileSystemProvider = + new LSPFileSystemProvider( + languageClient, + uriMapper, + fileDecoderProvider, + clientConfig, + ) + def loadedPresentationCompilerCount(): Int = compilers.loadedPresentationCompilerCount() diff --git a/metals/src/main/scala/scala/meta/internal/metals/ProjectMetalsLspService.scala b/metals/src/main/scala/scala/meta/internal/metals/ProjectMetalsLspService.scala index b95548b03b2..a82ba68568d 100644 --- a/metals/src/main/scala/scala/meta/internal/metals/ProjectMetalsLspService.scala +++ b/metals/src/main/scala/scala/meta/internal/metals/ProjectMetalsLspService.scala @@ -129,6 +129,7 @@ class ProjectMetalsLspService( folder, compilers, buildTargets, + uriMapper, () => userConfig, shellRunner, optFileSystemSemanticdbs, @@ -686,6 +687,7 @@ class ProjectMetalsLspService( new FolderTreeViewProvider( new Folder(folder, folderVisibleName, true), buildTargets, + uriMapper, definitionIndex, () => userConfig, scalaVersionSelector, diff --git a/metals/src/main/scala/scala/meta/internal/metals/ServerCommands.scala b/metals/src/main/scala/scala/meta/internal/metals/ServerCommands.scala index 13c4ade5906..c25c56d5609 100644 --- a/metals/src/main/scala/scala/meta/internal/metals/ServerCommands.scala +++ b/metals/src/main/scala/scala/meta/internal/metals/ServerCommands.scala @@ -137,6 +137,30 @@ object ServerCommands { |""".stripMargin, ) + val FileSystemStat = new ParametrizedCommand[String]( + "filesystem-stat", + "Get file system stat", + """|Get file system stat for the specified uri. + |""".stripMargin, + "[uri]", + ) + + val FileSystemReadDirectory = new ParametrizedCommand[String]( + "filesystem-read-directory", + "Read directory", + """|Read directory for the specified uri. + |""".stripMargin, + "[uri]", + ) + + val FileSystemReadFile = new ParametrizedCommand[String]( + "filesystem-read-file", + "Read file", + """|Read file for the specified uri. + |""".stripMargin, + "[uri]", + ) + val DiscoverMainClasses = new ParametrizedCommand[DebugDiscoveryParams]( "discover-jvm-run-command", "Discover main classes to run and return the object", @@ -842,6 +866,9 @@ object ServerCommands { RunScalafix, ScalafixRunOnly, DecodeFile, + FileSystemStat, + FileSystemReadDirectory, + FileSystemReadFile, DisconnectBuildServer, DisconnectBuildServerAndShutdown, ListBuildTargets, diff --git a/metals/src/main/scala/scala/meta/internal/metals/URIMapper.scala b/metals/src/main/scala/scala/meta/internal/metals/URIMapper.scala new file mode 100644 index 00000000000..6c826bc066f --- /dev/null +++ b/metals/src/main/scala/scala/meta/internal/metals/URIMapper.scala @@ -0,0 +1,318 @@ +package scala.meta.internal.metals + +import java.net.URI +import java.nio.file.FileSystem +import java.nio.file.FileSystemNotFoundException +import java.nio.file.FileSystems +import java.nio.file.Path + +import scala.annotation.tailrec +import scala.util.Properties + +import scala.meta.internal.metals.MetalsEnrichments._ +import scala.meta.internal.mtags.URIEncoderDecoder +import scala.meta.io.AbsolutePath + +import org.eclipse.lsp4j.CodeActionParams +import org.eclipse.lsp4j.Location +import org.eclipse.lsp4j.ReferenceParams +import org.eclipse.lsp4j.SymbolInformation +import org.eclipse.lsp4j.TextDocumentIdentifier +import org.eclipse.lsp4j.TextDocumentPositionParams + +/** + * Bidirectional mapper between virtual `metalsfs://` URIs exposed to + * the LSP client and local `jar:file://` URIs used internally by Metals. + * + * Maintains lazy NIO file system handles for JAR archives and provides + * overloaded conversions for common LSP4J parameter types. + */ +final case class URIMapper( + buildTargets: BuildTargets, + userJavaHome: () => Option[String], +) { + + private lazy val isWindows: Boolean = Properties.isWin + + private def changeCase(uri: String): String = + if (isWindows) uri.toLowerCase() else uri + + /** + * Walks up the path to find a meaningful JDK directory name, + * skipping intermediate segments like `lib` or `src.zip`. + */ + @tailrec + private def getDecentJDKName(path: Path): String = + if ( + path.getParent == null || !List("lib", "src.zip").contains(path.filename) + ) + path.filename + else + getDecentJDKName(path.getParent) + + private def jdkSources: Option[AbsolutePath] = + JdkSources(userJavaHome()).toOption + + /** Returns the display names of all available JDK source archives. */ + def getJDKs: Iterator[String] = + jdkSources.iterator.map(jdk => getDecentJDKName(jdk.toNIO)) + + /** Returns the filenames of all workspace dependency JARs. */ + def getWorkspaceJars: Iterator[String] = + buildTargets.allWorkspaceJars.map(_.filename) + + /** Returns the filenames of all source JARs. */ + def getSourceJars: Iterator[String] = + buildTargets.allSourceJars.map(_.filename) + + /** + * Returns an existing NIO [[FileSystem]] for the given archive, + * creating one if it does not yet exist. + */ + private def getOrCreateFileSystem(localPath: AbsolutePath): FileSystemInfo = { + val fileUri = localPath.toNIO.toUri.toString.stripSuffix("/") + val localUri = s"jar:$fileUri" + val zipURI = URI.create(localUri) + val fs = + try { + FileSystems.getFileSystem(zipURI) + } catch { + case _: FileSystemNotFoundException => + FileSystems.newFileSystem(zipURI, new java.util.HashMap[String, Any]) + } + FileSystemInfo(fs, fileUri) + } + + private def findJdkByName(name: String): Option[AbsolutePath] = + jdkSources.find(jdk => getDecentJDKName(jdk.toNIO) == name) + + private def findWorkspaceJarByName(name: String): Option[AbsolutePath] = + buildTargets.allWorkspaceJars.find(_.filename == name) + + private def findSourceJarByName(name: String): Option[AbsolutePath] = + buildTargets.allSourceJars.find(_.filename == name) + + /** Opens the NIO file system for a JDK source archive identified by name. */ + def getJDKFileSystem(name: String): FileSystemInfo = + findJdkByName(name) + .map(getOrCreateFileSystem) + .getOrElse(throw new NoSuchElementException(s"JDK not found: $name")) + + /** Opens the NIO file system for a workspace JAR identified by filename. */ + def getWorkspaceJarFileSystem(name: String): FileSystemInfo = + findWorkspaceJarByName(name) + .map(getOrCreateFileSystem) + .getOrElse( + throw new NoSuchElementException(s"Workspace jar not found: $name") + ) + + /** Opens the NIO file system for a source JAR identified by filename. */ + def getSourceJarFileSystem(name: String): FileSystemInfo = + findSourceJarByName(name) + .map(getOrCreateFileSystem) + .getOrElse( + throw new NoSuchElementException(s"Source jar not found: $name") + ) + + /** + * Determines the virtual category (jdk, jar, or source) for a local + * `file://` JAR path by matching it against known build dependencies. + * + * @param decodedJarPath a decoded `file:///path/to/some.jar` string + * @return the corresponding `metalsfs:/metalsLibraries//` + * prefix, or `None` if the path is not a known dependency + */ + private def classifyLocalUri( + decodedJarPath: String + ): Option[String] = { + val path = + try Some(AbsolutePath(java.nio.file.Paths.get(new URI(decodedJarPath)))) + catch { case _: Exception => None } + + path.flatMap { absPath => + val filename = absPath.filename + jdkSources match { + case Some(jdk) + if changeCase( + absPath.toURI.toString.stripSuffix("/") + ) == changeCase(jdk.toURI.toString.stripSuffix("/")) => + val name = getDecentJDKName(jdk.toNIO) + Some(s"${URIMapper.jdkURI}/$name") + case _ => + if ( + buildTargets.allWorkspaceJars.exists(j => + changeCase(j.toURI.toString.stripSuffix("/")) == changeCase( + absPath.toURI.toString.stripSuffix("/") + ) + ) + ) + Some(s"${URIMapper.workspaceJarURI}/$filename") + else if ( + buildTargets.allSourceJars.exists(j => + changeCase(j.toURI.toString.stripSuffix("/")) == changeCase( + absPath.toURI.toString.stripSuffix("/") + ) + ) + ) + Some(s"${URIMapper.sourceJarURI}/$filename") + else + None + } + } + } + + /** + * Percent-encodes the given URI according to its scheme. + * + * `metalsfs:` URIs are returned as-is because constructing them via + * `new URI("metalsfs", "", path, null)` would produce triple slashes. + */ + def encodeUri(uri: String): String = { + if (uri.startsWith("file://")) { + val path = uri.stripPrefix("file://") + new URI("file", "", path, null).toString + } else if (uri.startsWith("jar:")) { + val ssp = uri.stripPrefix("jar:") + new URI("jar", ssp, null).toString + } else if (uri.startsWith("metalsfs:")) { + uri + } else + throw new IllegalStateException(s"Why here? $uri") + } + + /** + * Converts a virtual `metalsfs://` URI into a local `jar:file://` URI. + * + * If the URI points to an archive root (no inner path), the plain + * `file://` URI of the archive is returned. Otherwise the full + * `jar:file:///archive.jar!/inner/path` form is produced. + * The returned URI is already percent-encoded by `Path#toUri`. + */ + def convertToLocal(uri: String): String = { + if (uri.startsWith(URIMapper.parentURI)) { + val (fs, fsPath) = URIEncoderDecoder.decode(uri) match { + case jdk if jdk.startsWith(URIMapper.jdkURI) => + val (name, remaining) = URIMapper.getURIParts(jdk, URIMapper.jdkURI) + val fs = getJDKFileSystem(name) + (fs, remaining) + case workspaceJar + if workspaceJar.startsWith(URIMapper.workspaceJarURI) => + val (name, remaining) = + URIMapper.getURIParts(workspaceJar, URIMapper.workspaceJarURI) + val fs = getWorkspaceJarFileSystem(name) + (fs, remaining) + case sourceJar if sourceJar.startsWith(URIMapper.sourceJarURI) => + val (name, remaining) = + URIMapper.getURIParts(sourceJar, URIMapper.sourceJarURI) + val fs = getSourceJarFileSystem(name) + (fs, remaining) + } + if (fsPath.isEmpty) fs.fileUri + else fs.fs.getPath(fsPath.get).toUri.toString + } else uri + } + + /** + * Converts a local `jar:file://` URI into a virtual `metalsfs://` URI + * by classifying the archive and rewriting the path. + */ + def convertToMetalsFS(uri: String): String = { + val decodedURI = URIEncoderDecoder.decode(uri) + val metalsfsUri = if (uri.startsWith("jar:")) { + val path = decodedURI.stripPrefix("jar:") + val splitter = path.indexOf('!') + if (splitter == -1) { + classifyLocalUri(path).getOrElse(path) + } else { + val remainder = path.substring(splitter + 1) + val localJarPath = path.substring(0, splitter) + val metalsJarPath = + classifyLocalUri(localJarPath).getOrElse(localJarPath) + s"${metalsJarPath}${remainder}" + } + } else { + classifyLocalUri(decodedURI).getOrElse(decodedURI) + } + encodeUri(metalsfsUri) + } + + /** Rewrites the location URI inside a [[SymbolInformation]] to `metalsfs://`. */ + def convertToMetalsFS( + symbolInformation: SymbolInformation + ): SymbolInformation = { + val symbolInfo = new SymbolInformation( + symbolInformation.getName, + symbolInformation.getKind, + convertToMetalsFS(symbolInformation.getLocation), + symbolInformation.getContainerName, + ) + symbolInfo.setTags(symbolInformation.getTags) + symbolInfo + } + + def convertToMetalsFS(location: Location): Location = + new Location(convertToMetalsFS(location.getUri), location.getRange) + + def convertToLocal( + params: TextDocumentIdentifier + ): TextDocumentIdentifier = + new TextDocumentIdentifier(convertToLocal(params.getUri)) + + def convertToLocal( + params: HoverExtParams + ): HoverExtParams = + params.copy(textDocument = convertToLocal(params.textDocument)) + + def convertToLocal( + params: CodeActionParams + ): CodeActionParams = + new CodeActionParams( + convertToLocal(params.getTextDocument()), + params.getRange(), + params.getContext(), + ) +} + +/** Handle to an open NIO [[FileSystem]] together with its source `file://` URI. */ +final case class FileSystemInfo(fs: FileSystem, fileUri: String) + +/** + * Constants and utilities for the `metalsfs://` virtual file system URI scheme. + * + * VS Code normalises `metalsfs:///` to `metalsfs:/`, so all paths + * use the single-slash form. + */ +object URIMapper { + val rootDir: String = "metalsLibraries" + val parentURI: String = s"metalsfs:/$rootDir" + val jdkDir: String = "jdk" + val workspaceJarDir: String = "jar" + val sourceJarDir: String = "source" + val jdkURI: String = s"${parentURI}/$jdkDir" + val workspaceJarURI: String = s"${parentURI}/$workspaceJarDir" + val sourceJarURI: String = s"${parentURI}/$sourceJarDir" + + /** + * Splits a `metalsfs://` URI into the archive name and an optional + * inner path relative to that archive. + * + * {{{ + * getURIParts("metalsfs:/X/Y", "metalsfs:/X") // ("Y", None) + * getURIParts("metalsfs:/X/Y/a/B", "metalsfs:/X") // ("Y", Some("a/B")) + * }}} + */ + def getURIParts( + uri: String, + prefix: String, + ): (String, Option[String]) = { + val basePart = uri.stripPrefix(s"${prefix}/") + val separatorIdx = basePart.indexOf('/') + if (separatorIdx == -1) + (basePart, None) + else { + val name = basePart.substring(0, separatorIdx) + val remaining = basePart.substring(separatorIdx + 1) + (name, Some(remaining)) + } + } +} diff --git a/metals/src/main/scala/scala/meta/internal/metals/WorkspaceLspService.scala b/metals/src/main/scala/scala/meta/internal/metals/WorkspaceLspService.scala index 820df69c579..d084e7e2449 100644 --- a/metals/src/main/scala/scala/meta/internal/metals/WorkspaceLspService.scala +++ b/metals/src/main/scala/scala/meta/internal/metals/WorkspaceLspService.scala @@ -127,6 +127,10 @@ class WorkspaceLspService( serverInputs.initialServerConfig, initializeParams, ) + private def uriMapper: URIMapper = + folderServices.headOption + .map(_.uriMapper) + .getOrElse(fallbackService.uriMapper) private val languageClient = { val languageClient = @@ -353,7 +357,7 @@ class WorkspaceLspService( def getServiceForOpt(uri: String): Option[ProjectMetalsLspService] = { // "metalsDecode" prefix is used for showing special files and is not an actual file system - val strippedUri = uri.stripPrefix("metalsDecode:") + val strippedUri = uriMapper.convertToLocal(uri).stripPrefix("metalsDecode:") for { path <- strippedUri.toAbsolutePathSafe() service <- @@ -366,6 +370,176 @@ class WorkspaceLspService( def getServiceFor(uri: String): MetalsLspService = getServiceForOpt(uri).getOrElse(fallbackService) + private def toLocalUri(uri: String): String = uriMapper.convertToLocal(uri) + + private def toMetalsFSUri(uri: String): String = + if (clientConfig.isLibraryFileSystemSupported()) + uriMapper.convertToMetalsFS(uri) + else uri + + private def toLocal( + params: TextDocumentPositionParams + ): TextDocumentPositionParams = { + params.getTextDocument.setUri(toLocalUri(params.getTextDocument.getUri)) + params + } + + private def toLocal(params: CompletionParams): CompletionParams = { + params.getTextDocument.setUri(toLocalUri(params.getTextDocument.getUri)) + params + } + + private def toLocal(params: lsp4j.InlayHintParams): lsp4j.InlayHintParams = { + params.getTextDocument.setUri(toLocalUri(params.getTextDocument.getUri)) + params + } + + private def toLocal(params: ReferenceParams): ReferenceParams = { + params.getTextDocument.setUri(toLocalUri(params.getTextDocument.getUri)) + params + } + + private def toLocal(params: HoverExtParams): HoverExtParams = + uriMapper.convertToLocal(params) + + private def toLocal(params: CodeActionParams): CodeActionParams = + uriMapper.convertToLocal(params) + + private def toLocal( + params: DidOpenTextDocumentParams + ): DidOpenTextDocumentParams = { + params.getTextDocument.setUri(toLocalUri(params.getTextDocument.getUri)) + params + } + + private def toLocal( + params: DidChangeTextDocumentParams + ): DidChangeTextDocumentParams = { + params.getTextDocument.setUri(toLocalUri(params.getTextDocument.getUri)) + params + } + + private def toLocal( + params: DidCloseTextDocumentParams + ): DidCloseTextDocumentParams = { + params.getTextDocument.setUri(toLocalUri(params.getTextDocument.getUri)) + params + } + + private def toLocal( + params: DidSaveTextDocumentParams + ): DidSaveTextDocumentParams = { + params.getTextDocument.setUri(toLocalUri(params.getTextDocument.getUri)) + params + } + + private def toLocal(params: DocumentSymbolParams): DocumentSymbolParams = { + params.getTextDocument.setUri(toLocalUri(params.getTextDocument.getUri)) + params + } + + private def toLocal( + params: DocumentFormattingParams + ): DocumentFormattingParams = { + params.getTextDocument.setUri(toLocalUri(params.getTextDocument.getUri)) + params + } + + private def toLocal( + params: DocumentOnTypeFormattingParams + ): DocumentOnTypeFormattingParams = { + params.getTextDocument.setUri(toLocalUri(params.getTextDocument.getUri)) + params + } + + private def toLocal( + params: DocumentRangeFormattingParams + ): DocumentRangeFormattingParams = { + params.getTextDocument.setUri(toLocalUri(params.getTextDocument.getUri)) + params + } + + private def toLocal(params: RenameParams): RenameParams = { + params.getTextDocument.setUri(toLocalUri(params.getTextDocument.getUri)) + params + } + + private def toLocal(params: CodeLensParams): CodeLensParams = { + params.getTextDocument.setUri(toLocalUri(params.getTextDocument.getUri)) + params + } + + private def toLocal( + params: FoldingRangeRequestParams + ): FoldingRangeRequestParams = { + params.getTextDocument.setUri(toLocalUri(params.getTextDocument.getUri)) + params + } + + private def toLocal(params: SelectionRangeParams): SelectionRangeParams = { + params.getTextDocument.setUri(toLocalUri(params.getTextDocument.getUri)) + params + } + + private def toLocal(params: SemanticTokensParams): SemanticTokensParams = { + params.getTextDocument.setUri(toLocalUri(params.getTextDocument.getUri)) + params + } + + private def toLocal( + params: CallHierarchyPrepareParams + ): CallHierarchyPrepareParams = { + params.getTextDocument.setUri(toLocalUri(params.getTextDocument.getUri)) + params + } + + private def toLocal( + params: CallHierarchyIncomingCallsParams + ): CallHierarchyIncomingCallsParams = { + params.getItem.setUri(toLocalUri(params.getItem.getUri)) + params + } + + private def toLocal( + params: CallHierarchyOutgoingCallsParams + ): CallHierarchyOutgoingCallsParams = { + params.getItem.setUri(toLocalUri(params.getItem.getUri)) + params + } + + private def toLocal( + params: TypeHierarchyPrepareParams + ): TypeHierarchyPrepareParams = { + params.getTextDocument.setUri(toLocalUri(params.getTextDocument.getUri)) + params + } + + private def toLocal( + params: TypeHierarchySupertypesParams + ): TypeHierarchySupertypesParams = { + params.getItem.setUri(toLocalUri(params.getItem.getUri)) + params + } + + private def toLocal( + params: TypeHierarchySubtypesParams + ): TypeHierarchySubtypesParams = { + params.getItem.setUri(toLocalUri(params.getItem.getUri)) + params + } + + private def toMetalsFS(location: Location): Location = + if (clientConfig.isLibraryFileSystemSupported()) + uriMapper.convertToMetalsFS(location) + else location + + private def toMetalsFS( + symbolInformation: SymbolInformation + ): SymbolInformation = + if (clientConfig.isLibraryFileSystemSupported()) + uriMapper.convertToMetalsFS(symbolInformation) + else symbolInformation + def currentFolder: Option[MetalsLspService] = focusedDocument.get().flatMap(getServiceForOpt) @@ -460,8 +634,9 @@ class WorkspaceLspService( override def didOpen( params: DidOpenTextDocumentParams ): CompletableFuture[Unit] = { + val localParams = toLocal(params) focusedDocument.get().foreach(recentlyFocusedFiles.add) - val uri = params.getTextDocument.getUri + val uri = localParams.getTextDocument.getUri val path = uri.toAbsolutePath if (!clientConfig.isDidFocusProvider() || focusedDocument.get().isEmpty) setFocusedDocument(Some(path)) @@ -473,13 +648,14 @@ class WorkspaceLspService( } else None } .getOrElse(fallbackService) - service.didOpen(params) + service.didOpen(localParams) } override def didChange( params: DidChangeTextDocumentParams ): CompletableFuture[Unit] = { - val uri = params.getTextDocument().getUri() + val localParams = toLocal(params) + val uri = localParams.getTextDocument().getUri() /* If a file changed that was most likely caused by the user, * we should consider it as the focused document. * @@ -487,46 +663,62 @@ class WorkspaceLspService( */ if (!clientConfig.isDidFocusProvider()) setFocusedDocument(Some(uri.toAbsolutePath)) - getServiceFor(uri).didChange(params) + getServiceFor(uri).didChange(localParams) } override def didClose(params: DidCloseTextDocumentParams): Unit = { - val path = params.getTextDocument.getUri.toAbsolutePath + val localParams = toLocal(params) + val path = localParams.getTextDocument.getUri.toAbsolutePath if ( !clientConfig.isDidFocusProvider() && focusedDocument.get().contains(path) ) { setFocusedDocument(recentlyFocusedFiles.pollRecent()) } - getServiceFor(params.getTextDocument().getUri()).didClose(params) + getServiceFor(localParams.getTextDocument().getUri()).didClose(localParams) } override def didSave( params: DidSaveTextDocumentParams - ): CompletableFuture[Unit] = - getServiceFor(params.getTextDocument().getUri()).didSave(params) + ): CompletableFuture[Unit] = { + val localParams = toLocal(params) + getServiceFor(localParams.getTextDocument().getUri()).didSave(localParams) + } override def definition( position: TextDocumentPositionParams ): CompletableFuture[ju.List[Location]] = - getServiceFor(position.getTextDocument().getUri()).definition(position) + getServiceFor(toLocal(position).getTextDocument.getUri) + .definition(toLocal(position)) + .asScala + .map(_.asScala.map(toMetalsFS).asJava) + .asJava override def typeDefinition( position: TextDocumentPositionParams ): CompletableFuture[ju.List[Location]] = - getServiceFor(position.getTextDocument().getUri()).typeDefinition(position) + getServiceFor(toLocal(position).getTextDocument.getUri) + .typeDefinition(toLocal(position)) + .asScala + .map(_.asScala.map(toMetalsFS).asJava) + .asJava override def implementation( position: TextDocumentPositionParams ): CompletableFuture[ju.List[Location]] = - getServiceFor(position.getTextDocument().getUri()).implementation(position) + getServiceFor(toLocal(position).getTextDocument.getUri) + .implementation(toLocal(position)) + .asScala + .map(_.asScala.map(toMetalsFS).asJava) + .asJava override def hover(params: HoverExtParams): CompletableFuture[Hover] = - getServiceFor(params.textDocument.getUri()).hover(params) + getServiceFor(toLocal(params).textDocument.getUri()).hover(toLocal(params)) override def inlayHints( params: lsp4j.InlayHintParams ): CompletableFuture[java.util.List[lsp4j.InlayHint]] = - getServiceFor(params.getTextDocument.getUri()).inlayHints(params) + getServiceFor(toLocal(params).getTextDocument.getUri) + .inlayHints(toLocal(params)) override def inlayHintResolve( inlayHint: lsp4j.InlayHint @@ -538,75 +730,177 @@ class WorkspaceLspService( override def documentHighlights( params: TextDocumentPositionParams ): CompletableFuture[ju.List[DocumentHighlight]] = - getServiceFor(params.getTextDocument.getUri()).documentHighlights(params) + getServiceFor(toLocal(params).getTextDocument.getUri) + .documentHighlights(toLocal(params)) override def documentSymbol(params: DocumentSymbolParams): CompletableFuture[ messages.Either[ju.List[DocumentSymbol], ju.List[SymbolInformation]] ] = - getServiceFor(params.getTextDocument.getUri()).documentSymbol(params) + getServiceFor(toLocal(params).getTextDocument.getUri) + .documentSymbol(toLocal(params)) + .asScala + .map { either => + if (either.isLeft) + messages.Either.forLeft[ + ju.List[DocumentSymbol], + ju.List[SymbolInformation], + ](either.getLeft) + else { + val updated = either.getRight.asScala.map(toMetalsFS).asJava + messages.Either.forRight[ + ju.List[DocumentSymbol], + ju.List[SymbolInformation], + ](updated) + } + } + .asJava override def formatting( params: DocumentFormattingParams ): CompletableFuture[ju.List[TextEdit]] = - getServiceFor(params.getTextDocument.getUri()).formatting(params) + getServiceFor(toLocal(params).getTextDocument.getUri) + .formatting(toLocal(params)) override def onTypeFormatting( params: DocumentOnTypeFormattingParams ): CompletableFuture[ju.List[TextEdit]] = - getServiceFor(params.getTextDocument.getUri()).onTypeFormatting(params) + getServiceFor(toLocal(params).getTextDocument.getUri) + .onTypeFormatting(toLocal(params)) override def rangeFormatting( params: DocumentRangeFormattingParams ): CompletableFuture[ju.List[TextEdit]] = - getServiceFor(params.getTextDocument.getUri()).rangeFormatting(params) + getServiceFor(toLocal(params).getTextDocument.getUri) + .rangeFormatting(toLocal(params)) override def prepareRename( params: TextDocumentPositionParams ): CompletableFuture[lsp4j.Range] = - getServiceFor(params.getTextDocument.getUri()).prepareRename(params) + getServiceFor(toLocal(params).getTextDocument.getUri) + .prepareRename(toLocal(params)) override def rename(params: RenameParams): CompletableFuture[WorkspaceEdit] = - getServiceFor(params.getTextDocument.getUri()).rename(params) + getServiceFor(toLocal(params).getTextDocument.getUri) + .rename(toLocal(params)) override def references( params: ReferenceParams ): CompletableFuture[ju.List[Location]] = - getServiceFor(params.getTextDocument.getUri()).references(params) + getServiceFor(toLocal(params).getTextDocument.getUri) + .references(toLocal(params)) + .asScala + .map(_.asScala.map(toMetalsFS).asJava) + .asJava override def prepareCallHierarchy( params: CallHierarchyPrepareParams ): CompletableFuture[ju.List[CallHierarchyItem]] = - getServiceFor(params.getTextDocument.getUri()).prepareCallHierarchy(params) + getServiceFor(toLocal(params).getTextDocument.getUri) + .prepareCallHierarchy(toLocal(params)) + .asScala + .map( + _.asScala + .map { item => + if (clientConfig.isLibraryFileSystemSupported()) + item.setUri(toMetalsFSUri(item.getUri)) + item + } + .asJava + ) + .asJava override def callHierarchyIncomingCalls( params: CallHierarchyIncomingCallsParams ): CompletableFuture[ju.List[CallHierarchyIncomingCall]] = - getServiceFor(params.getItem.getUri).callHierarchyIncomingCalls(params) + getServiceFor(toLocal(params).getItem.getUri) + .callHierarchyIncomingCalls(toLocal(params)) + .asScala + .map( + _.asScala + .map { call => + if (clientConfig.isLibraryFileSystemSupported()) { + call.getFrom.setUri(toMetalsFSUri(call.getFrom.getUri)) + } + call + } + .asJava + ) + .asJava override def callHierarchyOutgoingCalls( params: CallHierarchyOutgoingCallsParams ): CompletableFuture[ju.List[CallHierarchyOutgoingCall]] = - getServiceFor(params.getItem.getUri).callHierarchyOutgoingCalls(params) + getServiceFor(toLocal(params).getItem.getUri) + .callHierarchyOutgoingCalls(toLocal(params)) + .asScala + .map( + _.asScala + .map { call => + if (clientConfig.isLibraryFileSystemSupported()) { + call.getTo.setUri(toMetalsFSUri(call.getTo.getUri)) + } + call + } + .asJava + ) + .asJava override def prepareTypeHierarchy( params: TypeHierarchyPrepareParams ): CompletableFuture[ju.List[TypeHierarchyItem]] = - getServiceFor(params.getTextDocument.getUri).prepareTypeHierarchy(params) + getServiceFor(toLocal(params).getTextDocument.getUri) + .prepareTypeHierarchy(toLocal(params)) + .asScala + .map( + _.asScala + .map { item => + if (clientConfig.isLibraryFileSystemSupported()) + item.setUri(toMetalsFSUri(item.getUri)) + item + } + .asJava + ) + .asJava override def typeHierarchySupertypes( params: TypeHierarchySupertypesParams ): CompletableFuture[ju.List[TypeHierarchyItem]] = - getServiceFor(params.getItem.getUri).typeHierarchySupertypes(params) + getServiceFor(toLocal(params).getItem.getUri) + .typeHierarchySupertypes(toLocal(params)) + .asScala + .map( + _.asScala + .map { item => + if (clientConfig.isLibraryFileSystemSupported()) + item.setUri(toMetalsFSUri(item.getUri)) + item + } + .asJava + ) + .asJava override def typeHierarchySubtypes( params: TypeHierarchySubtypesParams ): CompletableFuture[ju.List[TypeHierarchyItem]] = - getServiceFor(params.getItem.getUri).typeHierarchySubtypes(params) + getServiceFor(toLocal(params).getItem.getUri) + .typeHierarchySubtypes(toLocal(params)) + .asScala + .map( + _.asScala + .map { item => + if (clientConfig.isLibraryFileSystemSupported()) + item.setUri(toMetalsFSUri(item.getUri)) + item + } + .asJava + ) + .asJava override def completion( params: CompletionParams ): CompletableFuture[CompletionList] = - getServiceFor(params.getTextDocument.getUri).completion(params) + getServiceFor(toLocal(params).getTextDocument.getUri) + .completion(toLocal(params)) override def completionItemResolve( item: CompletionItem @@ -618,12 +912,14 @@ class WorkspaceLspService( override def signatureHelp( params: TextDocumentPositionParams ): CompletableFuture[SignatureHelp] = - getServiceFor(params.getTextDocument.getUri).signatureHelp(params) + getServiceFor(toLocal(params).getTextDocument.getUri) + .signatureHelp(toLocal(params)) override def codeAction( params: CodeActionParams ): CompletableFuture[ju.List[CodeAction]] = - getServiceFor(params.getTextDocument.getUri).codeAction(params) + getServiceFor(toLocal(params).getTextDocument.getUri) + .codeAction(toLocal(params)) override def codeActionResolve( codeAction: CodeAction @@ -637,28 +933,33 @@ class WorkspaceLspService( override def codeLens( params: CodeLensParams ): CompletableFuture[ju.List[CodeLens]] = - getServiceFor(params.getTextDocument.getUri).codeLens(params) + getServiceFor(toLocal(params).getTextDocument.getUri) + .codeLens(toLocal(params)) override def foldingRange( params: FoldingRangeRequestParams ): CompletableFuture[ju.List[FoldingRange]] = - getServiceFor(params.getTextDocument.getUri).foldingRange(params) + getServiceFor(toLocal(params).getTextDocument.getUri) + .foldingRange(toLocal(params)) override def selectionRange( params: SelectionRangeParams ): CompletableFuture[ju.List[SelectionRange]] = - getServiceFor(params.getTextDocument.getUri).selectionRange(params) + getServiceFor(toLocal(params).getTextDocument.getUri) + .selectionRange(toLocal(params)) override def semanticTokensFull( params: SemanticTokensParams ): CompletableFuture[SemanticTokens] = - getServiceFor(params.getTextDocument.getUri).semanticTokensFull(params) + getServiceFor(toLocal(params).getTextDocument.getUri) + .semanticTokensFull(toLocal(params)) override def workspaceSymbol( params: WorkspaceSymbolParams ): CompletableFuture[ju.List[lsp4j.SymbolInformation]] = CancelTokens.future { token => - collectSeq(_.workspaceSymbol(params, token))(_.flatten.asJava) + collectSeq(_.workspaceSymbol(params, token))(_.flatten) + .map(_.map(toMetalsFS).asJava) } override def willRenameFiles( @@ -765,10 +1066,11 @@ class WorkspaceLspService( params: TextDocumentPositionParams ): CompletableFuture[TreeViewNodeRevealResult] = Future { + val localParams = toLocal(params) treeView .reveal( - params.getTextDocument().getUri().toAbsolutePath, - params.getPosition(), + localParams.getTextDocument().getUri().toAbsolutePath, + localParams.getPosition(), ) .orNull }.asJava @@ -776,7 +1078,9 @@ class WorkspaceLspService( override def findTextInDependencyJars( params: FindTextInDependencyJarsRequest ): CompletableFuture[ju.List[Location]] = - collectSeq(_.findTextInDependencyJars(params))(_.flatten.asJava).asJava + collectSeq(_.findTextInDependencyJars(params))(_.flatten) + .map(_.map(toMetalsFS).asJava) + .asJava override def didCancelWorkDoneProgress( params: lsp4j.WorkDoneProgressCancelParams @@ -805,8 +1109,9 @@ class WorkspaceLspService( } uriOpt match { case Some(uri) => - setFocusedDocument(Some(uri.toAbsolutePath)) - getServiceFor(uri).didFocus(uri) + val localUri = toLocalUri(uri) + setFocusedDocument(Some(localUri.toAbsolutePath)) + getServiceFor(localUri).didFocus(localUri) case None => CompletableFuture.completedFuture(DidFocusResult.NoBuildTarget) } @@ -896,6 +1201,18 @@ class WorkspaceLspService( .getOrElse(fallbackService) .decodeFile(uri) .asJavaObject + case ServerCommands.FileSystemStat(uri) => + currentOrHeadOrFallback.lspFileSystemProvider + .getSystemStat(uri) + .asJavaObject + case ServerCommands.FileSystemReadDirectory(uri) => + currentOrHeadOrFallback.lspFileSystemProvider + .readDirectory(uri) + .asJavaObject + case ServerCommands.FileSystemReadFile(uri) => + currentOrHeadOrFallback.lspFileSystemProvider + .readFile(uri) + .asJavaObject case ServerCommands.DiscoverTestSuites(params) => Option(params.uri) match { case None => diff --git a/metals/src/main/scala/scala/meta/internal/tvp/MetalsTreeViewProvider.scala b/metals/src/main/scala/scala/meta/internal/tvp/MetalsTreeViewProvider.scala index c76e2e8cf5b..c6242188eb3 100644 --- a/metals/src/main/scala/scala/meta/internal/tvp/MetalsTreeViewProvider.scala +++ b/metals/src/main/scala/scala/meta/internal/tvp/MetalsTreeViewProvider.scala @@ -232,6 +232,7 @@ class MetalsTreeViewProvider( class FolderTreeViewProvider( folder: Folder, buildTargets: BuildTargets, + uriMapper: URIMapper, definitionIndex: GlobalSymbolIndex, userConfig: () => UserConfiguration, scalaVersionSelector: ScalaVersionSelector, @@ -265,8 +266,16 @@ class FolderTreeViewProvider( title = s"Libraries", folder = folder, id = identity, - encode = _.toURI.toString(), - decode = _.toAbsolutePath(followSymlink = false), + encode = path => { + val uri = path.toURI.toString() + if (clientConfig.isLibraryFileSystemSupported()) + uriMapper.convertToMetalsFS(uri) + else uri + }, + decode = uri => + uriMapper + .convertToLocal(uri) + .toAbsolutePath(followSymlink = false), valueTitle = path => { if (path.filename == JdkSources.zipFileName) { maybeUsedJdkVersion diff --git a/metals/src/main/scala/scala/meta/metals/MetalsLanguageServer.scala b/metals/src/main/scala/scala/meta/metals/MetalsLanguageServer.scala index 973185f725c..9157162bf0e 100644 --- a/metals/src/main/scala/scala/meta/metals/MetalsLanguageServer.scala +++ b/metals/src/main/scala/scala/meta/metals/MetalsLanguageServer.scala @@ -137,7 +137,10 @@ class MetalsLanguageServer( .map(_.asScala) .toList .flatten - allFolders match { + val userFolders = allFolders.filterNot { folder => + Option(folder.getUri()).exists(_.startsWith("metalsfs:")) + } + userFolders match { case Nil => Option(params.getRootUri()) .orElse(Option(params.getRootPath())) From 6fa7670212fa4a5d33e3226469c6649393ae432f Mon Sep 17 00:00:00 2001 From: Tomasz Godzik Date: Wed, 27 May 2026 19:43:40 +0200 Subject: [PATCH 02/12] improvement: Add main-v2 to be review as well (#8415) ## Summary by CodeRabbit * **Chores** * Updated configuration to expand base branch support for code review processes. [![Review Change Stack](https://storage.googleapis.com/coderabbit_public_assets/review-stack-in-coderabbit-ui.svg)](https://app.coderabbit.ai/change-stack/scalameta/metals/pull/8415?utm_source=github_walkthrough&utm_medium=github&utm_campaign=change_stack) --- .coderabbit.yaml | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 .coderabbit.yaml diff --git a/.coderabbit.yaml b/.coderabbit.yaml new file mode 100644 index 00000000000..3f330d31f4e --- /dev/null +++ b/.coderabbit.yaml @@ -0,0 +1,3 @@ +base_branches: + - main + - main-v2 From 3f9903e6ccf28394835aa83a29948809d7d1241d Mon Sep 17 00:00:00 2001 From: manykeys Date: Mon, 25 May 2026 23:54:53 +0500 Subject: [PATCH 03/12] Extract JarFileSystemCache, add per-category indexes with rebuild on indexing, read JDK name from JAVA_HOME/release, make readFile non-blocking (flatMap, no Await), close Files.list via Using.resource, add non-exhaustive match fallbacks, move isUriEqual to MetalsEnrichments --- .../metals/mcp/StandaloneMcpService.scala | 7 + .../metals/FallbackMetalsLspService.scala | 4 + .../internal/metals/FileDecoderProvider.scala | 6 +- .../meta/internal/metals/IndexProviders.scala | 3 +- .../scala/meta/internal/metals/Indexer.scala | 1 + .../metals/InitializationOptions.scala | 1 + .../internal/metals/JarFileSystemCache.scala | 66 +++ .../metals/LSPFileSystemProvider.scala | 73 +-- .../internal/metals/MetalsEnrichments.scala | 4 + .../internal/metals/MetalsLspService.scala | 11 +- .../metals/ProjectMetalsLspService.scala | 5 +- .../meta/internal/metals/TargetData.scala | 11 +- .../meta/internal/metals/URIMapper.scala | 470 ++++++++++-------- .../internal/metals/WorkspaceLspService.scala | 16 +- 14 files changed, 402 insertions(+), 276 deletions(-) create mode 100644 metals/src/main/scala/scala/meta/internal/metals/JarFileSystemCache.scala diff --git a/metals-mcp/src/main/scala/scala/meta/internal/metals/mcp/StandaloneMcpService.scala b/metals-mcp/src/main/scala/scala/meta/internal/metals/mcp/StandaloneMcpService.scala index 90addea990e..e0483692c12 100644 --- a/metals-mcp/src/main/scala/scala/meta/internal/metals/mcp/StandaloneMcpService.scala +++ b/metals-mcp/src/main/scala/scala/meta/internal/metals/mcp/StandaloneMcpService.scala @@ -103,6 +103,11 @@ class StandaloneMcpService( initialUserConfig = initialUserConfig.getOrElse(UserConfiguration.default) ) + private val jarFileSystemCache: JarFileSystemCache = new JarFileSystemCache + + private val uriMapper: WorkspaceURIMapper = + new WorkspaceURIMapper(() => Seq(projectMetalsLspService.folderUriMapper)) + lazy val projectMetalsLspService = new ProjectMetalsLspService( ec, scheduledExecutor, @@ -121,6 +126,8 @@ class StandaloneMcpService( workDoneProgress, maxScalaCliServers = 3, moduleStatus, + jarFileSystemCache, + uriMapper, ) /** diff --git a/metals/src/main/scala/scala/meta/internal/metals/FallbackMetalsLspService.scala b/metals/src/main/scala/scala/meta/internal/metals/FallbackMetalsLspService.scala index 8e327613fe9..7ea89c90366 100644 --- a/metals/src/main/scala/scala/meta/internal/metals/FallbackMetalsLspService.scala +++ b/metals/src/main/scala/scala/meta/internal/metals/FallbackMetalsLspService.scala @@ -41,6 +41,8 @@ class FallbackMetalsLspService( override val workDoneProgress: WorkDoneProgress, bspStatus: BspStatus, moduleStatus: ModuleStatus, + jarFileSystemCache: JarFileSystemCache, + uriMapper: WorkspaceURIMapper, ) extends MetalsLspService( ec, sh, @@ -58,6 +60,8 @@ class FallbackMetalsLspService( workDoneProgress, maxScalaCliServers = 10, moduleStatus, + jarFileSystemCache, + uriMapper, ) { val buildServerPromise: Promise[Unit] = Promise.successful(()) diff --git a/metals/src/main/scala/scala/meta/internal/metals/FileDecoderProvider.scala b/metals/src/main/scala/scala/meta/internal/metals/FileDecoderProvider.scala index 5c41bca1aa5..80cf07c5010 100644 --- a/metals/src/main/scala/scala/meta/internal/metals/FileDecoderProvider.scala +++ b/metals/src/main/scala/scala/meta/internal/metals/FileDecoderProvider.scala @@ -171,7 +171,11 @@ final class FileDecoderProvider( case "metalsDecode" => decodedFileContents(uri.getSchemeSpecificPart()) case "metalsfs" => - decodedFileContents(uriMapper.convertToLocal(uriAsStr)) + Try(uriMapper.convertToLocal(uriAsStr)) match { + case Success(local) => decodedFileContents(local) + case Failure(e) => + Future.successful(DecoderResponse.failed(uri, e)) + } case _ => Future.successful( DecoderResponse.failed( diff --git a/metals/src/main/scala/scala/meta/internal/metals/IndexProviders.scala b/metals/src/main/scala/scala/meta/internal/metals/IndexProviders.scala index 2e3f949e18a..37144accec9 100644 --- a/metals/src/main/scala/scala/meta/internal/metals/IndexProviders.scala +++ b/metals/src/main/scala/scala/meta/internal/metals/IndexProviders.scala @@ -29,7 +29,8 @@ trait IndexProviders { def referencesProvider: ReferenceProvider def workspaceSymbols: WorkspaceSymbolProvider def buildTargets: BuildTargets - def uriMapper: URIMapper + def uriMapper: WorkspaceURIMapper + def folderUriMapper: FolderURIMapper def lspFileSystemProvider: LSPFileSystemProvider def semanticDBIndexer: SemanticdbIndexer def fileWatcher: FileWatcher diff --git a/metals/src/main/scala/scala/meta/internal/metals/Indexer.scala b/metals/src/main/scala/scala/meta/internal/metals/Indexer.scala index c4436432445..00532ee7514 100644 --- a/metals/src/main/scala/scala/meta/internal/metals/Indexer.scala +++ b/metals/src/main/scala/scala/meta/internal/metals/Indexer.scala @@ -204,6 +204,7 @@ case class Indexer(indexProviders: IndexProviders)(implicit rc: ReportContext) { ) } if (clientConfig.isLibraryFileSystemSupported()) { + folderUriMapper.rebuildIndexes() lspFileSystemProvider.sendLibraryFileSystemReady() } // Schedule removal of unused toplevel symbols from cache diff --git a/metals/src/main/scala/scala/meta/internal/metals/InitializationOptions.scala b/metals/src/main/scala/scala/meta/internal/metals/InitializationOptions.scala index c22509f44f6..d65debe2d89 100644 --- a/metals/src/main/scala/scala/meta/internal/metals/InitializationOptions.scala +++ b/metals/src/main/scala/scala/meta/internal/metals/InitializationOptions.scala @@ -129,6 +129,7 @@ object InitializationOptions { None, None, None, + None, ) def from( diff --git a/metals/src/main/scala/scala/meta/internal/metals/JarFileSystemCache.scala b/metals/src/main/scala/scala/meta/internal/metals/JarFileSystemCache.scala new file mode 100644 index 00000000000..116cec4c977 --- /dev/null +++ b/metals/src/main/scala/scala/meta/internal/metals/JarFileSystemCache.scala @@ -0,0 +1,66 @@ +package scala.meta.internal.metals + +import java.net.URI +import java.nio.file.FileSystem +import java.nio.file.FileSystemAlreadyExistsException +import java.nio.file.FileSystemNotFoundException +import java.nio.file.FileSystems + +import scala.collection.mutable +import scala.util.control.NonFatal + +import scala.meta.io.AbsolutePath + +final case class FileSystemInfo(fs: FileSystem, fileUri: String) + +final class JarFileSystemCache { + + private val openFileSystems = mutable.Map.empty[URI, FileSystem] + + /** + * Synchronized to avoid a race where two threads both call newFileSystem + * for the same archive. + */ + def open(localPath: AbsolutePath): FileSystemInfo = synchronized { + val fileUri = localPath.toNIO.toUri.toString.stripSuffix("/") + val zipURI = JarFileSystemCache.jarUriFor(localPath) + val fs = openFileSystems.getOrElseUpdate(zipURI, openFileSystem(zipURI)) + FileSystemInfo(fs, fileUri) + } + + private def openFileSystem(zipURI: URI): FileSystem = + try FileSystems.getFileSystem(zipURI) + catch { + case _: FileSystemNotFoundException => + try + FileSystems + .newFileSystem(zipURI, new java.util.HashMap[String, Any]) + catch { + case _: FileSystemAlreadyExistsException => + FileSystems.getFileSystem(zipURI) + } + } + + def closeObsolete(knownPaths: Set[AbsolutePath]): Unit = synchronized { + val knownUris = knownPaths.map(JarFileSystemCache.jarUriFor) + val obsoleteUris = openFileSystems.keysIterator.filterNot(knownUris).toList + obsoleteUris.foreach(close) + } + + def close(zipURI: URI): Unit = synchronized { + openFileSystems.remove(zipURI).foreach { fs => + try fs.close() + catch { + case NonFatal(e) => + scribe.warn(s"Failed to close jar file system $zipURI", e) + } + } + } +} + +object JarFileSystemCache { + def jarUriFor(localPath: AbsolutePath): URI = { + val fileUri = localPath.toNIO.toUri.toString.stripSuffix("/") + URI.create(s"jar:$fileUri") + } +} diff --git a/metals/src/main/scala/scala/meta/internal/metals/LSPFileSystemProvider.scala b/metals/src/main/scala/scala/meta/internal/metals/LSPFileSystemProvider.scala index 2c635cd42d0..0f2314fe068 100644 --- a/metals/src/main/scala/scala/meta/internal/metals/LSPFileSystemProvider.scala +++ b/metals/src/main/scala/scala/meta/internal/metals/LSPFileSystemProvider.scala @@ -4,10 +4,9 @@ import java.nio.charset.StandardCharsets import java.nio.file.Files import java.util.stream.Collectors -import scala.concurrent.Await import scala.concurrent.ExecutionContext import scala.concurrent.Future -import scala.concurrent.duration.DurationInt +import scala.util.Using import scala.meta.internal.metals.MetalsEnrichments._ import scala.meta.internal.metals.clients.language.MetalsLanguageClient @@ -40,16 +39,13 @@ class LSPFileSystemProvider( languageClient: MetalsLanguageClient, uriMapper: URIMapper, fileDecoderProvider: FileDecoderProvider, - clientConfig: ClientConfiguration, )(implicit ec: ExecutionContext) { - /** Notifies the client that the library file system is ready for use. */ + /** Callers must gate on [[ClientConfiguration.isLibraryFileSystemSupported]]. */ def sendLibraryFileSystemReady(): Unit = { - if (clientConfig.isLibraryFileSystemSupported()) { - val params = - ClientCommands.LibraryFileSystemReady.toExecuteCommandParams() - languageClient.metalsExecuteClientCommand(params) - } + val params = + ClientCommands.LibraryFileSystemReady.toExecuteCommandParams() + languageClient.metalsExecuteClientCommand(params) } /** @@ -83,17 +79,18 @@ class LSPFileSystemProvider( case _ => val (fs, innerPath) = resolveInnerPath(uri) val path = fs.fs.getPath(innerPath.getOrElse("/")) - Files - .list(path) - .collect(Collectors.toList()) - .asScala - .map(p => - FSReadDirectoryResponse( - p.getFileName.toString, - isFile = Files.isRegularFile(p), + Using.resource(Files.list(path)) { stream => + stream + .collect(Collectors.toList()) + .asScala + .map(p => + FSReadDirectoryResponse( + p.getFileName.toString, + isFile = Files.isRegularFile(p), + ) ) - ) - .toArray + .toArray + } } FSReadDirectoriesResponse(uri, entries, "") } @@ -104,19 +101,23 @@ class LSPFileSystemProvider( * For `.class` files the bytecode is decompiled via CFR through * [[FileDecoderProvider]]. All other files are read as UTF-8 text. */ - def readFile(uri: String): Future[FSReadFileResponse] = Future { - val (fs, innerPath) = resolveInnerPath(uri) - val path = fs.fs.getPath(innerPath.getOrElse("/")) - val contents = - if (path.getFileName.toString.endsWith(".class")) { - val fut = fileDecoderProvider.decodedFileContents(path.toUri.toString) - val res = Await.result(fut, 10.minutes) - Option(res.value).filter(_.nonEmpty).getOrElse(res.error) - } else { - new String(Files.readAllBytes(path), StandardCharsets.UTF_8) - } - FSReadFileResponse(uri, contents, "") - } + def readFile(uri: String): Future[FSReadFileResponse] = + Future(resolveInnerPath(uri)).flatMap { case (fs, innerPath) => + val path = fs.fs.getPath(innerPath.getOrElse("/")) + if (path.getFileName.toString.endsWith(".class")) + fileDecoderProvider.decodedFileContents(path.toUri.toString).map { + res => + val contents = Option(res.value).filter(_.nonEmpty).getOrElse("") + val error = Option(res.error).getOrElse("") + FSReadFileResponse(uri, contents, error) + } + else + Future { + val contents = + new String(Files.readAllBytes(path), StandardCharsets.UTF_8) + FSReadFileResponse(uri, contents, "") + } + } /** * Returns metadata for the given URI, indicating whether it @@ -151,9 +152,8 @@ class LSPFileSystemProvider( */ private def resolveInnerPath( uri: String - ): (FileSystemInfo, Option[String]) = { - val decoded = URIEncoderDecoder.decode(uri) - decoded match { + ): (FileSystemInfo, Option[String]) = + URIEncoderDecoder.decode(uri) match { case jdk if jdk.startsWith(URIMapper.jdkURI) => val (name, path) = URIMapper.getURIParts(jdk, URIMapper.jdkURI) (uriMapper.getJDKFileSystem(name), path) @@ -165,6 +165,7 @@ class LSPFileSystemProvider( val (name, path) = URIMapper.getURIParts(src, URIMapper.sourceJarURI) (uriMapper.getSourceJarFileSystem(name), path) + case other => + throw new IllegalArgumentException(s"Unknown metalsfs URI: $other") } - } } diff --git a/metals/src/main/scala/scala/meta/internal/metals/MetalsEnrichments.scala b/metals/src/main/scala/scala/meta/internal/metals/MetalsEnrichments.scala index acb8ea4fefe..2d2c0c0a773 100644 --- a/metals/src/main/scala/scala/meta/internal/metals/MetalsEnrichments.scala +++ b/metals/src/main/scala/scala/meta/internal/metals/MetalsEnrichments.scala @@ -711,6 +711,10 @@ object MetalsEnrichments implicit class XtensionString(value: String) { + /** Case-insensitive on Windows where e.g. `Coursier/Cache` and `Coursier/cache` are equivalent. */ + def isUriEqual(other: String): Boolean = + if (Properties.isWin) value.equalsIgnoreCase(other) else value == other + /** * Returns true if this is a Scala.js or Scala Native target * diff --git a/metals/src/main/scala/scala/meta/internal/metals/MetalsLspService.scala b/metals/src/main/scala/scala/meta/internal/metals/MetalsLspService.scala index fad05c6a4d0..23b90f52938 100644 --- a/metals/src/main/scala/scala/meta/internal/metals/MetalsLspService.scala +++ b/metals/src/main/scala/scala/meta/internal/metals/MetalsLspService.scala @@ -113,6 +113,8 @@ abstract class MetalsLspService( val workDoneProgress: WorkDoneProgress, maxScalaCliServers: Int, moduleStatus: ModuleStatus, + jarFileSystemCache: JarFileSystemCache, + val uriMapper: WorkspaceURIMapper, ) extends Folder(folder, folderVisibleName, isKnownMetalsProject = true) with Cancelable with TextDocumentService @@ -167,8 +169,12 @@ abstract class MetalsLspService( val buildTargets: BuildTargets = BuildTargets.from(folder, mainBuildTargetsData, tables) - val uriMapper: URIMapper = - URIMapper(buildTargets, () => userConfig.javaHome) + val folderUriMapper: FolderURIMapper = + new FolderURIMapper( + buildTargets, + () => userConfig.javaHome, + jarFileSystemCache, + ) implicit val reports: StdReportContext = new StdReportContext( folder.toNIO, @@ -611,7 +617,6 @@ abstract class MetalsLspService( languageClient, uriMapper, fileDecoderProvider, - clientConfig, ) def loadedPresentationCompilerCount(): Int = diff --git a/metals/src/main/scala/scala/meta/internal/metals/ProjectMetalsLspService.scala b/metals/src/main/scala/scala/meta/internal/metals/ProjectMetalsLspService.scala index a82ba68568d..88dd8e69ea9 100644 --- a/metals/src/main/scala/scala/meta/internal/metals/ProjectMetalsLspService.scala +++ b/metals/src/main/scala/scala/meta/internal/metals/ProjectMetalsLspService.scala @@ -63,6 +63,8 @@ class ProjectMetalsLspService( override val workDoneProgress: WorkDoneProgress, maxScalaCliServers: Int, moduleStatus: ModuleStatus, + jarFileSystemCache: JarFileSystemCache, + uriMapper: WorkspaceURIMapper, ) extends MetalsLspService( ec, sh, @@ -80,6 +82,8 @@ class ProjectMetalsLspService( workDoneProgress, maxScalaCliServers, moduleStatus, + jarFileSystemCache, + uriMapper, ) { scribe.debug(clientConfig.toString()) @@ -938,5 +942,4 @@ class ProjectMetalsLspService( super.resetService() treeView.reset() } - } diff --git a/metals/src/main/scala/scala/meta/internal/metals/TargetData.scala b/metals/src/main/scala/scala/meta/internal/metals/TargetData.scala index cdb9e8d0b77..e1826fd4b04 100644 --- a/metals/src/main/scala/scala/meta/internal/metals/TargetData.scala +++ b/metals/src/main/scala/scala/meta/internal/metals/TargetData.scala @@ -12,7 +12,6 @@ import scala.collection.mutable.{Map => MMap} import scala.concurrent.ExecutionContext import scala.concurrent.Future import scala.concurrent.Promise -import scala.util.Properties import scala.meta.inputs.Input import scala.meta.internal.metals.MetalsEnrichments._ @@ -220,18 +219,10 @@ final class TargetData() { case Some(id) => buildTargetDependencyModules.get(id).iterator.flatten } - /** - * For windows file:///C:/Users/runneradmin/AppData/Local/Coursier/Cache and - * file:///C:/Users/runneradmin/AppData/Local/Coursier/cache is equivalent - */ - def isUriEqual(uri: String, otherUri: String) = { - Properties.isWin && uri.toLowerCase() == otherUri - .toLowerCase() || uri == otherUri - } val allFound = for { module <- depModules artifacts = module.getArtifacts().asScala - if artifacts.exists(artifact => isUriEqual(artifact.getUri(), jarUri)) + if artifacts.exists(_.getUri().isUriEqual(jarUri)) foundJar <- artifacts.find(_.getClassifier() == classifier) foundJarPath = foundJar.getUri().toAbsolutePath if foundJarPath.exists diff --git a/metals/src/main/scala/scala/meta/internal/metals/URIMapper.scala b/metals/src/main/scala/scala/meta/internal/metals/URIMapper.scala index 6c826bc066f..4cdb997e3d9 100644 --- a/metals/src/main/scala/scala/meta/internal/metals/URIMapper.scala +++ b/metals/src/main/scala/scala/meta/internal/metals/URIMapper.scala @@ -1,13 +1,13 @@ package scala.meta.internal.metals import java.net.URI -import java.nio.file.FileSystem -import java.nio.file.FileSystemNotFoundException -import java.nio.file.FileSystems +import java.nio.file.Files import java.nio.file.Path +import java.util.Properties +import java.util.concurrent.atomic.AtomicReference -import scala.annotation.tailrec -import scala.util.Properties +import scala.util.Using +import scala.util.control.NonFatal import scala.meta.internal.metals.MetalsEnrichments._ import scala.meta.internal.mtags.URIEncoderDecoder @@ -15,273 +15,305 @@ import scala.meta.io.AbsolutePath import org.eclipse.lsp4j.CodeActionParams import org.eclipse.lsp4j.Location -import org.eclipse.lsp4j.ReferenceParams import org.eclipse.lsp4j.SymbolInformation import org.eclipse.lsp4j.TextDocumentIdentifier -import org.eclipse.lsp4j.TextDocumentPositionParams /** * Bidirectional mapper between virtual `metalsfs://` URIs exposed to * the LSP client and local `jar:file://` URIs used internally by Metals. * - * Maintains lazy NIO file system handles for JAR archives and provides - * overloaded conversions for common LSP4J parameter types. + * `try*` variants return `None` when the receiving mapper does not own + * the archive in question, so an aggregator can fall through to another + * folder's mapper. */ -final case class URIMapper( - buildTargets: BuildTargets, - userJavaHome: () => Option[String], -) { +trait URIMapper { - private lazy val isWindows: Boolean = Properties.isWin + def convertToLocal(uri: String): String + def convertToMetalsFS(uri: String): String - private def changeCase(uri: String): String = - if (isWindows) uri.toLowerCase() else uri + def tryConvertToLocal(uri: String): Option[String] + def tryConvertToMetalsFS(uri: String): Option[String] - /** - * Walks up the path to find a meaningful JDK directory name, - * skipping intermediate segments like `lib` or `src.zip`. - */ - @tailrec - private def getDecentJDKName(path: Path): String = - if ( - path.getParent == null || !List("lib", "src.zip").contains(path.filename) + def getJDKs: Iterator[String] + def getWorkspaceJars: Iterator[String] + def getSourceJars: Iterator[String] + + def tryGetJDKFileSystem(name: String): Option[FileSystemInfo] + def tryGetWorkspaceJarFileSystem(name: String): Option[FileSystemInfo] + def tryGetSourceJarFileSystem(name: String): Option[FileSystemInfo] + + final def getJDKFileSystem(name: String): FileSystemInfo = + tryGetJDKFileSystem(name).getOrElse( + throw new NoSuchElementException(s"JDK not found: $name") ) - path.filename - else - getDecentJDKName(path.getParent) - private def jdkSources: Option[AbsolutePath] = - JdkSources(userJavaHome()).toOption + final def getWorkspaceJarFileSystem(name: String): FileSystemInfo = + tryGetWorkspaceJarFileSystem(name).getOrElse( + throw new NoSuchElementException(s"Workspace jar not found: $name") + ) - /** Returns the display names of all available JDK source archives. */ - def getJDKs: Iterator[String] = - jdkSources.iterator.map(jdk => getDecentJDKName(jdk.toNIO)) + final def getSourceJarFileSystem(name: String): FileSystemInfo = + tryGetSourceJarFileSystem(name).getOrElse( + throw new NoSuchElementException(s"Source jar not found: $name") + ) - /** Returns the filenames of all workspace dependency JARs. */ - def getWorkspaceJars: Iterator[String] = - buildTargets.allWorkspaceJars.map(_.filename) + final def convertToMetalsFS( + symbolInformation: SymbolInformation + ): SymbolInformation = { + val symbolInfo = new SymbolInformation( + symbolInformation.getName, + symbolInformation.getKind, + convertToMetalsFS(symbolInformation.getLocation), + symbolInformation.getContainerName, + ) + symbolInfo.setTags(symbolInformation.getTags) + symbolInfo + } - /** Returns the filenames of all source JARs. */ - def getSourceJars: Iterator[String] = - buildTargets.allSourceJars.map(_.filename) + final def convertToMetalsFS(location: Location): Location = + new Location(convertToMetalsFS(location.getUri), location.getRange) + + final def convertToLocal( + params: TextDocumentIdentifier + ): TextDocumentIdentifier = + new TextDocumentIdentifier(convertToLocal(params.getUri)) + + final def convertToLocal(params: HoverExtParams): HoverExtParams = + params.copy(textDocument = convertToLocal(params.textDocument)) + + final def convertToLocal(params: CodeActionParams): CodeActionParams = + new CodeActionParams( + convertToLocal(params.getTextDocument()), + params.getRange(), + params.getContext(), + ) +} + +/** + * Per-folder [[URIMapper]] backed by that folder's [[BuildTargets]] and + * the shared [[JarFileSystemCache]]. + */ +final class FolderURIMapper( + buildTargets: BuildTargets, + userJavaHome: () => Option[String], + jarFileSystemCache: JarFileSystemCache, +) extends URIMapper { + + private val emptyIndex: Map[String, AbsolutePath] = Map.empty + private val jdkIndex = new AtomicReference(emptyIndex) + private val workspaceJarIndex = new AtomicReference(emptyIndex) + private val sourceJarIndex = new AtomicReference(emptyIndex) /** - * Returns an existing NIO [[FileSystem]] for the given archive, - * creating one if it does not yet exist. + * Derives a display name for a JDK source archive (`src.zip`) by reading + * `JAVA_VERSION`/`IMPLEMENTOR` from the `release` file in `JAVA_HOME`. + * Falls back to the JDK home directory name when `release` is missing + * (e.g. JDKs older than Java 9). */ - private def getOrCreateFileSystem(localPath: AbsolutePath): FileSystemInfo = { - val fileUri = localPath.toNIO.toUri.toString.stripSuffix("/") - val localUri = s"jar:$fileUri" - val zipURI = URI.create(localUri) - val fs = - try { - FileSystems.getFileSystem(zipURI) - } catch { - case _: FileSystemNotFoundException => - FileSystems.newFileSystem(zipURI, new java.util.HashMap[String, Any]) - } - FileSystemInfo(fs, fileUri) + private def getDecentJDKName(srcZipPath: Path): String = { + val jdkHome = Option(srcZipPath.getParent).map { parent => + if (parent.filename == "lib") parent.getParent else parent + } + jdkHome + .flatMap(readReleaseName) + .orElse(jdkHome.map(_.filename)) + .getOrElse("JDK") } - private def findJdkByName(name: String): Option[AbsolutePath] = - jdkSources.find(jdk => getDecentJDKName(jdk.toNIO) == name) + private def readReleaseName(jdkHome: Path): Option[String] = { + val release = jdkHome.resolve("release") + if (!Files.exists(release)) None + else { + val props = new Properties() + val loaded = + try { + Using.resource(Files.newBufferedReader(release))(props.load) + true + } catch { + case NonFatal(e) => + scribe.warn(s"Failed to read JDK release file at $release", e) + false + } + if (!loaded) None + else { + val version = Option(props.getProperty("JAVA_VERSION")).map(unquote) + val implementor = Option(props.getProperty("IMPLEMENTOR")).map(unquote) + (implementor, version) match { + case (Some(impl), Some(ver)) => Some(s"$impl $ver") + case (_, Some(ver)) => Some(s"JDK $ver") + case _ => None + } + } + } + } - private def findWorkspaceJarByName(name: String): Option[AbsolutePath] = - buildTargets.allWorkspaceJars.find(_.filename == name) + private def unquote(s: String): String = + s.stripPrefix("\"").stripSuffix("\"") - private def findSourceJarByName(name: String): Option[AbsolutePath] = - buildTargets.allSourceJars.find(_.filename == name) + private def jdkSources: Option[AbsolutePath] = + JdkSources(userJavaHome()).toOption - /** Opens the NIO file system for a JDK source archive identified by name. */ - def getJDKFileSystem(name: String): FileSystemInfo = - findJdkByName(name) - .map(getOrCreateFileSystem) - .getOrElse(throw new NoSuchElementException(s"JDK not found: $name")) + /** Call after indexing the build so listings reflect current state. */ + def rebuildIndexes(): Unit = synchronized { + val newJdk = + jdkSources.iterator.map(j => getDecentJDKName(j.toNIO) -> j).toMap + val newWs = buildTargets.allWorkspaceJars.map(j => j.filename -> j).toMap + val newSrc = buildTargets.allSourceJars.map(j => j.filename -> j).toMap - /** Opens the NIO file system for a workspace JAR identified by filename. */ - def getWorkspaceJarFileSystem(name: String): FileSystemInfo = - findWorkspaceJarByName(name) - .map(getOrCreateFileSystem) - .getOrElse( - throw new NoSuchElementException(s"Workspace jar not found: $name") - ) + val newKnownPaths = + newJdk.values.toSet ++ newWs.values.toSet ++ newSrc.values.toSet + jarFileSystemCache.closeObsolete(newKnownPaths) - /** Opens the NIO file system for a source JAR identified by filename. */ - def getSourceJarFileSystem(name: String): FileSystemInfo = - findSourceJarByName(name) - .map(getOrCreateFileSystem) - .getOrElse( - throw new NoSuchElementException(s"Source jar not found: $name") - ) + jdkIndex.set(newJdk) + workspaceJarIndex.set(newWs) + sourceJarIndex.set(newSrc) + } - /** - * Determines the virtual category (jdk, jar, or source) for a local - * `file://` JAR path by matching it against known build dependencies. - * - * @param decodedJarPath a decoded `file:///path/to/some.jar` string - * @return the corresponding `metalsfs:/metalsLibraries//` - * prefix, or `None` if the path is not a known dependency - */ - private def classifyLocalUri( - decodedJarPath: String - ): Option[String] = { - val path = + def getJDKs: Iterator[String] = jdkIndex.get().keys.iterator + def getWorkspaceJars: Iterator[String] = + workspaceJarIndex.get().keys.iterator + def getSourceJars: Iterator[String] = sourceJarIndex.get().keys.iterator + + def tryGetJDKFileSystem(name: String): Option[FileSystemInfo] = + jdkIndex.get().get(name).map(jarFileSystemCache.open) + def tryGetWorkspaceJarFileSystem(name: String): Option[FileSystemInfo] = + workspaceJarIndex.get().get(name).map(jarFileSystemCache.open) + def tryGetSourceJarFileSystem(name: String): Option[FileSystemInfo] = + sourceJarIndex.get().get(name).map(jarFileSystemCache.open) + + private def classifyLocalUri(decodedJarPath: String): Option[String] = { + val pathOpt = try Some(AbsolutePath(java.nio.file.Paths.get(new URI(decodedJarPath)))) - catch { case _: Exception => None } - - path.flatMap { absPath => - val filename = absPath.filename - jdkSources match { - case Some(jdk) - if changeCase( - absPath.toURI.toString.stripSuffix("/") - ) == changeCase(jdk.toURI.toString.stripSuffix("/")) => - val name = getDecentJDKName(jdk.toNIO) - Some(s"${URIMapper.jdkURI}/$name") - case _ => - if ( - buildTargets.allWorkspaceJars.exists(j => - changeCase(j.toURI.toString.stripSuffix("/")) == changeCase( - absPath.toURI.toString.stripSuffix("/") - ) - ) - ) - Some(s"${URIMapper.workspaceJarURI}/$filename") - else if ( - buildTargets.allSourceJars.exists(j => - changeCase(j.toURI.toString.stripSuffix("/")) == changeCase( - absPath.toURI.toString.stripSuffix("/") - ) - ) + catch { + case NonFatal(e) => + scribe.warn( + s"Failed to parse $decodedJarPath while classifying local URI", + e, ) - Some(s"${URIMapper.sourceJarURI}/$filename") - else - None + None + } + + pathOpt.flatMap { path => + val absPathUri = path.toURI.toString.stripSuffix("/") + val matchesJdk = jdkSources.exists { jdk => + absPathUri.isUriEqual(jdk.toURI.toString.stripSuffix("/")) } + if (matchesJdk) + jdkSources + .map(jdk => s"${URIMapper.jdkURI}/${getDecentJDKName(jdk.toNIO)}") + else if ( + buildTargets.allWorkspaceJars + .exists(j => absPathUri.isUriEqual(j.toURI.toString.stripSuffix("/"))) + ) + Some(s"${URIMapper.workspaceJarURI}/${path.filename}") + else if ( + buildTargets.allSourceJars + .exists(j => absPathUri.isUriEqual(j.toURI.toString.stripSuffix("/"))) + ) + Some(s"${URIMapper.sourceJarURI}/${path.filename}") + else + None } } /** - * Percent-encodes the given URI according to its scheme. - * - * `metalsfs:` URIs are returned as-is because constructing them via - * `new URI("metalsfs", "", path, null)` would produce triple slashes. + * `metalsfs:` URIs are returned as-is — `new URI("metalsfs", "", path, null)` + * would otherwise produce triple slashes. */ - def encodeUri(uri: String): String = { - if (uri.startsWith("file://")) { - val path = uri.stripPrefix("file://") - new URI("file", "", path, null).toString - } else if (uri.startsWith("jar:")) { - val ssp = uri.stripPrefix("jar:") - new URI("jar", ssp, null).toString - } else if (uri.startsWith("metalsfs:")) { - uri - } else - throw new IllegalStateException(s"Why here? $uri") + def encodeUri(uri: String): String = uri match { + case s if s.startsWith("file://") => + new URI("file", "", s.stripPrefix("file://"), null).toString + case s if s.startsWith("jar:") => + new URI("jar", s.stripPrefix("jar:"), null).toString + case s if s.startsWith("metalsfs:") => + s + case _ => + throw new IllegalArgumentException(s"Unsupported URI scheme: $uri") } - /** - * Converts a virtual `metalsfs://` URI into a local `jar:file://` URI. - * - * If the URI points to an archive root (no inner path), the plain - * `file://` URI of the archive is returned. Otherwise the full - * `jar:file:///archive.jar!/inner/path` form is produced. - * The returned URI is already percent-encoded by `Path#toUri`. - */ - def convertToLocal(uri: String): String = { - if (uri.startsWith(URIMapper.parentURI)) { - val (fs, fsPath) = URIEncoderDecoder.decode(uri) match { + def convertToLocal(uri: String): String = + tryConvertToLocal(uri).getOrElse(uri) + + def convertToMetalsFS(uri: String): String = + tryConvertToMetalsFS(uri).getOrElse(encodeUri(uri)) + + def tryConvertToLocal(uri: String): Option[String] = { + if (!uri.startsWith(URIMapper.parentURI)) None + else { + val decoded = URIEncoderDecoder.decode(uri) + val resolved = decoded match { case jdk if jdk.startsWith(URIMapper.jdkURI) => val (name, remaining) = URIMapper.getURIParts(jdk, URIMapper.jdkURI) - val fs = getJDKFileSystem(name) - (fs, remaining) - case workspaceJar - if workspaceJar.startsWith(URIMapper.workspaceJarURI) => + tryGetJDKFileSystem(name).map(fs => (fs, remaining)) + case ws if ws.startsWith(URIMapper.workspaceJarURI) => val (name, remaining) = - URIMapper.getURIParts(workspaceJar, URIMapper.workspaceJarURI) - val fs = getWorkspaceJarFileSystem(name) - (fs, remaining) - case sourceJar if sourceJar.startsWith(URIMapper.sourceJarURI) => + URIMapper.getURIParts(ws, URIMapper.workspaceJarURI) + tryGetWorkspaceJarFileSystem(name).map(fs => (fs, remaining)) + case src if src.startsWith(URIMapper.sourceJarURI) => val (name, remaining) = - URIMapper.getURIParts(sourceJar, URIMapper.sourceJarURI) - val fs = getSourceJarFileSystem(name) - (fs, remaining) + URIMapper.getURIParts(src, URIMapper.sourceJarURI) + tryGetSourceJarFileSystem(name).map(fs => (fs, remaining)) + case _ => None } - if (fsPath.isEmpty) fs.fileUri - else fs.fs.getPath(fsPath.get).toUri.toString - } else uri - } - - /** - * Converts a local `jar:file://` URI into a virtual `metalsfs://` URI - * by classifying the archive and rewriting the path. - */ - def convertToMetalsFS(uri: String): String = { - val decodedURI = URIEncoderDecoder.decode(uri) - val metalsfsUri = if (uri.startsWith("jar:")) { - val path = decodedURI.stripPrefix("jar:") - val splitter = path.indexOf('!') - if (splitter == -1) { - classifyLocalUri(path).getOrElse(path) - } else { - val remainder = path.substring(splitter + 1) - val localJarPath = path.substring(0, splitter) - val metalsJarPath = - classifyLocalUri(localJarPath).getOrElse(localJarPath) - s"${metalsJarPath}${remainder}" + resolved.map { case (fs, fsPath) => + fsPath.fold(fs.fileUri)(p => fs.fs.getPath(p).toUri.toString) } - } else { - classifyLocalUri(decodedURI).getOrElse(decodedURI) } - encodeUri(metalsfsUri) } - /** Rewrites the location URI inside a [[SymbolInformation]] to `metalsfs://`. */ - def convertToMetalsFS( - symbolInformation: SymbolInformation - ): SymbolInformation = { - val symbolInfo = new SymbolInformation( - symbolInformation.getName, - symbolInformation.getKind, - convertToMetalsFS(symbolInformation.getLocation), - symbolInformation.getContainerName, - ) - symbolInfo.setTags(symbolInformation.getTags) - symbolInfo + def tryConvertToMetalsFS(uri: String): Option[String] = { + val decodedURI = URIEncoderDecoder.decode(uri) + val metalsfsUri = + if (uri.startsWith("jar:")) { + val path = decodedURI.stripPrefix("jar:") + val splitter = path.indexOf('!') + if (splitter == -1) classifyLocalUri(path) + else { + val remainder = path.substring(splitter + 1) + val localJarPath = path.substring(0, splitter) + classifyLocalUri(localJarPath).map(prefix => s"$prefix$remainder") + } + } else classifyLocalUri(decodedURI) + metalsfsUri.map(encodeUri) } +} - def convertToMetalsFS(location: Location): Location = - new Location(convertToMetalsFS(location.getUri), location.getRange) +/** + * Aggregating [[URIMapper]] that delegates to per-folder mappers, + * returning the first non-empty match. Used by the workspace-level + * service so requests from any folder are handled correctly. + */ +final class WorkspaceURIMapper(folders: () => Iterable[URIMapper]) + extends URIMapper { - def convertToLocal( - params: TextDocumentIdentifier - ): TextDocumentIdentifier = - new TextDocumentIdentifier(convertToLocal(params.getUri)) + private def tryAll[A](f: URIMapper => Option[A]): Option[A] = + folders().iterator.flatMap(m => f(m).iterator).nextOption() - def convertToLocal( - params: HoverExtParams - ): HoverExtParams = - params.copy(textDocument = convertToLocal(params.textDocument)) + def convertToLocal(uri: String): String = + tryConvertToLocal(uri).getOrElse(uri) + def convertToMetalsFS(uri: String): String = + tryConvertToMetalsFS(uri).getOrElse(uri) - def convertToLocal( - params: CodeActionParams - ): CodeActionParams = - new CodeActionParams( - convertToLocal(params.getTextDocument()), - params.getRange(), - params.getContext(), - ) -} + def tryConvertToLocal(uri: String): Option[String] = + tryAll(_.tryConvertToLocal(uri)) + def tryConvertToMetalsFS(uri: String): Option[String] = + tryAll(_.tryConvertToMetalsFS(uri)) -/** Handle to an open NIO [[FileSystem]] together with its source `file://` URI. */ -final case class FileSystemInfo(fs: FileSystem, fileUri: String) + def getJDKs: Iterator[String] = folders().iterator.flatMap(_.getJDKs).distinct + def getWorkspaceJars: Iterator[String] = + folders().iterator.flatMap(_.getWorkspaceJars).distinct + def getSourceJars: Iterator[String] = + folders().iterator.flatMap(_.getSourceJars).distinct + + def tryGetJDKFileSystem(name: String): Option[FileSystemInfo] = + tryAll(_.tryGetJDKFileSystem(name)) + def tryGetWorkspaceJarFileSystem(name: String): Option[FileSystemInfo] = + tryAll(_.tryGetWorkspaceJarFileSystem(name)) + def tryGetSourceJarFileSystem(name: String): Option[FileSystemInfo] = + tryAll(_.tryGetSourceJarFileSystem(name)) +} -/** - * Constants and utilities for the `metalsfs://` virtual file system URI scheme. - * - * VS Code normalises `metalsfs:///` to `metalsfs:/`, so all paths - * use the single-slash form. - */ +/** Single-slash form because VS Code normalises `metalsfs:///` to `metalsfs:/`. */ object URIMapper { val rootDir: String = "metalsLibraries" val parentURI: String = s"metalsfs:/$rootDir" diff --git a/metals/src/main/scala/scala/meta/internal/metals/WorkspaceLspService.scala b/metals/src/main/scala/scala/meta/internal/metals/WorkspaceLspService.scala index d084e7e2449..ae421661ac4 100644 --- a/metals/src/main/scala/scala/meta/internal/metals/WorkspaceLspService.scala +++ b/metals/src/main/scala/scala/meta/internal/metals/WorkspaceLspService.scala @@ -127,10 +127,12 @@ class WorkspaceLspService( serverInputs.initialServerConfig, initializeParams, ) - private def uriMapper: URIMapper = - folderServices.headOption - .map(_.uriMapper) - .getOrElse(fallbackService.uriMapper) + + private val jarFileSystemCache: JarFileSystemCache = new JarFileSystemCache + + private val uriMapper: WorkspaceURIMapper = new WorkspaceURIMapper(() => + folderServices.map(_.folderUriMapper) :+ fallbackService.folderUriMapper + ) private val languageClient = { val languageClient = @@ -243,6 +245,8 @@ class WorkspaceLspService( workDoneProgress, bspStatus, moduleStatus, + jarFileSystemCache, + uriMapper, ) } @@ -267,6 +271,8 @@ class WorkspaceLspService( workDoneProgress, maxScalaCliServers = 3, moduleStatus, + jarFileSystemCache, + uriMapper, ) } @@ -357,7 +363,7 @@ class WorkspaceLspService( def getServiceForOpt(uri: String): Option[ProjectMetalsLspService] = { // "metalsDecode" prefix is used for showing special files and is not an actual file system - val strippedUri = uriMapper.convertToLocal(uri).stripPrefix("metalsDecode:") + val strippedUri = uriMapper.convertToLocal(uri.stripPrefix("metalsDecode:")) for { path <- strippedUri.toAbsolutePathSafe() service <- From 5ba0af659572d152dcc5107edd6bbc3f1d62af6f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 28 May 2026 20:13:22 +0200 Subject: [PATCH 04/12] build(deps): bump webpack-dev-server from 5.2.2 to 5.2.4 in /website (#8398) Bumps [webpack-dev-server](https://github.com/webpack/webpack-dev-server) from 5.2.2 to 5.2.4.
Release notes

Sourced from webpack-dev-server's releases.

v5.2.4

5.2.4 (2026-05-11)

Bug Fixes

  • set Cross-Origin-Resource-Policy header to prevent source code theft over HTTP

v5.2.3

5.2.3 (2026-01-12)

Bug Fixes

  • add cause for errorObject (#5518) (37b033d)
  • compatibility with event target and universal target and lazy compilation (574026c)
  • overlay: add ESC key to dismiss overlay (#5598) (f91baa8)
  • progress indicator styles (#5557) (41a53a1)
  • upgrade selfsigned to v5
Changelog

Sourced from webpack-dev-server's changelog.

5.2.4 (2026-05-11)

Bug Fixes

  • set Cross-Origin-Resource-Policy header to prevent source code theft over HTTP

5.2.3 (2026-01-12)

Bug Fixes

  • add cause for errorObject (#5518) (37b033d)
  • compatibility with event target and universal target and lazy compilation (574026c)
  • overlay: add ESC key to dismiss overlay (#5598) (f91baa8)
  • progress indicator styles (#5557) (41a53a1)
  • upgrade selfsigned to v5
Commits

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=webpack-dev-server&package-manager=npm_and_yarn&previous-version=5.2.2&new-version=5.2.4)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself) You can disable automated security fix PRs for this repo from the [Security Alerts page](https://github.com/scalameta/metals/network/alerts).
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- website/yarn.lock | 334 ++++++++++++++++++++++++++++++++++++---------- 1 file changed, 267 insertions(+), 67 deletions(-) diff --git a/website/yarn.lock b/website/yarn.lock index a2efa239e82..cb773ccf29a 100644 --- a/website/yarn.lock +++ b/website/yarn.lock @@ -3468,6 +3468,11 @@ "@emnapi/runtime" "^1.5.0" "@tybys/wasm-util" "^0.10.1" +"@noble/hashes@1.4.0": + version "1.4.0" + resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.4.0.tgz#45814aa329f30e4fe0ba49426f49dfccdd066426" + integrity sha512-V1JJ1WTRUqHHrOSh597hURcMqVKVGL/ea3kv0gSnEdsEZ0/+VyPghM1lMNGc00z7CIQorSvbKpuJkxvuHbvdbg== + "@node-rs/jieba-android-arm-eabi@1.7.0": version "1.7.0" resolved "https://registry.yarnpkg.com/@node-rs/jieba-android-arm-eabi/-/jieba-android-arm-eabi-1.7.0.tgz#50b9921c6feb44755584963e8e00e425e557965a" @@ -3573,6 +3578,136 @@ "@nodelib/fs.scandir" "2.1.5" fastq "^1.6.0" +"@peculiar/asn1-cms@^2.6.0", "@peculiar/asn1-cms@^2.7.0": + version "2.7.0" + resolved "https://registry.yarnpkg.com/@peculiar/asn1-cms/-/asn1-cms-2.7.0.tgz#8e0eb656f4fc85f7c621dd442fa2d298faa84984" + integrity sha512-hew63shtzzvBcSHbhm+cyAmKe6AIfinT9hzEqSPjDC6opTTMKmTkQ0gHuN2KsWlvqiKw1S/fS94fhag/FJkioQ== + dependencies: + "@peculiar/asn1-schema" "^2.7.0" + "@peculiar/asn1-x509" "^2.7.0" + "@peculiar/asn1-x509-attr" "^2.7.0" + asn1js "^3.0.6" + tslib "^2.8.1" + +"@peculiar/asn1-csr@^2.6.0": + version "2.7.0" + resolved "https://registry.yarnpkg.com/@peculiar/asn1-csr/-/asn1-csr-2.7.0.tgz#1a03ac03f7571ea981f5d8377c6f4510c5d43411" + integrity sha512-VVsAyGqErT9D1SY4aEqozThXMVI+ssVRiv2DDeYuvpBKLIgZ3hYs3Ay3u/VSoKq6ESFi9cf6rf3IOOzfwh7oMA== + dependencies: + "@peculiar/asn1-schema" "^2.7.0" + "@peculiar/asn1-x509" "^2.7.0" + asn1js "^3.0.6" + tslib "^2.8.1" + +"@peculiar/asn1-ecc@^2.6.0": + version "2.7.0" + resolved "https://registry.yarnpkg.com/@peculiar/asn1-ecc/-/asn1-ecc-2.7.0.tgz#c35b57859812ecd0c2ae7b2144855e8208c2cfee" + integrity sha512-n7KEs/Q/wrB415cxy4fHOBhegp4NdJ15fkJPwcB/3/8iNBQC2L/N7SChJPKDJPZGYH0jD4Tg4/0vnHmwghnbKw== + dependencies: + "@peculiar/asn1-schema" "^2.7.0" + "@peculiar/asn1-x509" "^2.7.0" + asn1js "^3.0.6" + tslib "^2.8.1" + +"@peculiar/asn1-pfx@^2.7.0": + version "2.7.0" + resolved "https://registry.yarnpkg.com/@peculiar/asn1-pfx/-/asn1-pfx-2.7.0.tgz#d00766b13ff49785684a604248e6aadd184b291f" + integrity sha512-V/nrlQVmhg7lYAsM7E13UDL5erAwFv6kCIVFqNaMIHSVi7dngcT839JkRTkQBqznMG98l2XjxYk74ZztAohZzA== + dependencies: + "@peculiar/asn1-cms" "^2.7.0" + "@peculiar/asn1-pkcs8" "^2.7.0" + "@peculiar/asn1-rsa" "^2.7.0" + "@peculiar/asn1-schema" "^2.7.0" + asn1js "^3.0.6" + tslib "^2.8.1" + +"@peculiar/asn1-pkcs8@^2.7.0": + version "2.7.0" + resolved "https://registry.yarnpkg.com/@peculiar/asn1-pkcs8/-/asn1-pkcs8-2.7.0.tgz#5ee602d8a9a3e0a3f09f7b008ff644a657378692" + integrity sha512-9GTl1nE8Mx1kTZ+7QyYatDyKsm34QcWRBFkY1iPvWC3X4Dona5s/tlLiQsx5WzVdZqiMBZNYT0buyw4/vbhnjw== + dependencies: + "@peculiar/asn1-schema" "^2.7.0" + "@peculiar/asn1-x509" "^2.7.0" + asn1js "^3.0.6" + tslib "^2.8.1" + +"@peculiar/asn1-pkcs9@^2.6.0": + version "2.7.0" + resolved "https://registry.yarnpkg.com/@peculiar/asn1-pkcs9/-/asn1-pkcs9-2.7.0.tgz#23b4eae41c2feb8df258aa69c502a70092026b0a" + integrity sha512-Bh7m+OuIaSEllPQcSd9OSp93F4ROWH7sbITWV8MI+8dwsjE5111/87VxiWVvYFKyww3vp39geLv9ENqhwWHcew== + dependencies: + "@peculiar/asn1-cms" "^2.7.0" + "@peculiar/asn1-pfx" "^2.7.0" + "@peculiar/asn1-pkcs8" "^2.7.0" + "@peculiar/asn1-schema" "^2.7.0" + "@peculiar/asn1-x509" "^2.7.0" + "@peculiar/asn1-x509-attr" "^2.7.0" + asn1js "^3.0.6" + tslib "^2.8.1" + +"@peculiar/asn1-rsa@^2.6.0", "@peculiar/asn1-rsa@^2.7.0": + version "2.7.0" + resolved "https://registry.yarnpkg.com/@peculiar/asn1-rsa/-/asn1-rsa-2.7.0.tgz#6dc5c78c643264dd5a251a66f1dd9a38fcbba385" + integrity sha512-/qvENQrXyTZURjMqSeofHul0JJt2sNSzSwk36pl2olkHbaioMQgrASDZAlHXl0xUlnVbHj0uGgOrBMTb5x2aJQ== + dependencies: + "@peculiar/asn1-schema" "^2.7.0" + "@peculiar/asn1-x509" "^2.7.0" + asn1js "^3.0.6" + tslib "^2.8.1" + +"@peculiar/asn1-schema@^2.6.0", "@peculiar/asn1-schema@^2.7.0": + version "2.7.0" + resolved "https://registry.yarnpkg.com/@peculiar/asn1-schema/-/asn1-schema-2.7.0.tgz#f2dcb25995ce7cac8687ba1039f043e5eff43820" + integrity sha512-W8ZfWzLmQnrcky+eh3tni4IozMdqBDiHWU0N+vve/UGjMaUs8c0L7A2oEdkBXS8rTpWDpK/aoI3DG/L/hxmxPg== + dependencies: + "@peculiar/utils" "^2.0.2" + asn1js "^3.0.6" + tslib "^2.8.1" + +"@peculiar/asn1-x509-attr@^2.7.0": + version "2.7.0" + resolved "https://registry.yarnpkg.com/@peculiar/asn1-x509-attr/-/asn1-x509-attr-2.7.0.tgz#5ef2a10d3a78d4763b848a2cb56db4bb6b1e082a" + integrity sha512-NS8e7SOgXipkzUPLF/sce7ukpMpWjhxYsH0n6Y+bHYo4TTxOb95Zv7hqwSuL212mj5YxovjdOKQOgH1As3E94w== + dependencies: + "@peculiar/asn1-schema" "^2.7.0" + "@peculiar/asn1-x509" "^2.7.0" + asn1js "^3.0.6" + tslib "^2.8.1" + +"@peculiar/asn1-x509@^2.6.0", "@peculiar/asn1-x509@^2.7.0": + version "2.7.0" + resolved "https://registry.yarnpkg.com/@peculiar/asn1-x509/-/asn1-x509-2.7.0.tgz#84793efb7819dbc9526fd6b0a4ccd86f90464b96" + integrity sha512-mUn9RRrkGDnG4ALfunDmzyRW5dg+sWCj/pfnCCqEHYbkGxEpvUt6iVJv8Yw1cyp6SWZ26ZE5oSmI5SqEaen15g== + dependencies: + "@peculiar/asn1-schema" "^2.7.0" + "@peculiar/utils" "^2.0.2" + asn1js "^3.0.6" + tslib "^2.8.1" + +"@peculiar/utils@^2.0.2": + version "2.0.3" + resolved "https://registry.yarnpkg.com/@peculiar/utils/-/utils-2.0.3.tgz#a27ca4c4b73652e110f19a7d16d664f458a5528e" + integrity sha512-+oL3HPFRIZ1St2K50lWCXiioIgSoxzz7R1J3uF6neO2yl1sgmpgY6XXJH4BdpoDkMWznQTeYF6oWNDZLCdQ4eQ== + dependencies: + tslib "^2.8.1" + +"@peculiar/x509@^1.14.2": + version "1.14.3" + resolved "https://registry.yarnpkg.com/@peculiar/x509/-/x509-1.14.3.tgz#2c44c2b89474346afec38a0c2803ec4fb8ce959e" + integrity sha512-C2Xj8FZ0uHWeCXXqX5B4/gVFQmtSkiuOolzAgutjTfseNOHT3pUjljDZsTSxXFGgio54bCzVFqmEOUrIVk8RDA== + dependencies: + "@peculiar/asn1-cms" "^2.6.0" + "@peculiar/asn1-csr" "^2.6.0" + "@peculiar/asn1-ecc" "^2.6.0" + "@peculiar/asn1-pkcs9" "^2.6.0" + "@peculiar/asn1-rsa" "^2.6.0" + "@peculiar/asn1-schema" "^2.6.0" + "@peculiar/asn1-x509" "^2.6.0" + pvtsutils "^1.3.6" + reflect-metadata "^0.2.2" + tslib "^2.8.1" + tsyringe "^4.10.0" + "@pnpm/config.env-replace@^1.1.0": version "1.1.0" resolved "https://registry.yarnpkg.com/@pnpm/config.env-replace/-/config.env-replace-1.1.0.tgz#ab29da53df41e8948a00f2433f085f54de8b3a4c" @@ -4102,15 +4237,15 @@ "@types/qs" "*" "@types/serve-static" "*" -"@types/express@^4.17.21": - version "4.17.23" - resolved "https://registry.yarnpkg.com/@types/express/-/express-4.17.23.tgz#35af3193c640bfd4d7fe77191cd0ed411a433bef" - integrity sha512-Crp6WY9aTYP3qPi2wGDo9iUe/rceX01UMhnF1jmwDcKCFM6cx7YhGP/Mpr3y9AASpfHixIG0E6azCcL5OcDHsQ== +"@types/express@^4.17.25": + version "4.17.25" + resolved "https://registry.yarnpkg.com/@types/express/-/express-4.17.25.tgz#070c8c73a6fee6936d65c195dbbfb7da5026649b" + integrity sha512-dVd04UKsfpINUnK0yBoYHDF3xu7xVH4BuDotC/xGuycx4CgbP48X/KF/586bcObxT0HENHXEU8Nqtu6NR+eKhw== dependencies: "@types/body-parser" "*" "@types/express-serve-static-core" "^4.17.33" "@types/qs" "*" - "@types/serve-static" "*" + "@types/serve-static" "^1" "@types/gtag.js@^0.0.20": version "0.0.20" @@ -4202,13 +4337,6 @@ resolved "https://registry.yarnpkg.com/@types/ms/-/ms-0.7.34.tgz#10964ba0dee6ac4cd462e2795b6bebd407303433" integrity sha512-nG96G3Wp6acyAgJqGasjODb+acrI7KltPiRxzHPXnP3NgI28bpQDRv53olbqGXbfcgF5aiiHmO3xpwEpS5Ld9g== -"@types/node-forge@^1.3.0": - version "1.3.14" - resolved "https://registry.yarnpkg.com/@types/node-forge/-/node-forge-1.3.14.tgz#006c2616ccd65550560c2757d8472eb6d3ecea0b" - integrity sha512-mhVF2BnD4BO+jtOp7z1CdzaK4mbuK0LLQYAvdOLqHTavxFNq4zA1EmYkpnFjP8HOUzedfQkRnp0E2ulSAYSzAw== - dependencies: - "@types/node" "*" - "@types/node@*", "@types/node@^25.6.0": version "25.6.0" resolved "https://registry.yarnpkg.com/@types/node/-/node-25.6.0.tgz#4e09bad9b469871f2d0f68140198cbd714f4edca" @@ -4310,6 +4438,14 @@ "@types/mime" "^1" "@types/node" "*" +"@types/send@<1": + version "0.17.6" + resolved "https://registry.yarnpkg.com/@types/send/-/send-0.17.6.tgz#aeb5385be62ff58a52cd5459daa509ae91651d25" + integrity sha512-Uqt8rPBE8SY0RK8JB1EzVOIZ32uqy8HwdxCnoCOsYrvnswqmFZ/k+9Ikidlk/ImhsdvBsloHbAlewb2IEBV/Og== + dependencies: + "@types/mime" "^1" + "@types/node" "*" + "@types/serve-index@^1.9.4": version "1.9.4" resolved "https://registry.yarnpkg.com/@types/serve-index/-/serve-index-1.9.4.tgz#e6ae13d5053cb06ed36392110b4f9a49ac4ec898" @@ -4325,6 +4461,15 @@ "@types/mime" "^1" "@types/node" "*" +"@types/serve-static@^1": + version "1.15.10" + resolved "https://registry.yarnpkg.com/@types/serve-static/-/serve-static-1.15.10.tgz#768169145a778f8f5dfcb6360aead414a3994fee" + integrity sha512-tRs1dB+g8Itk72rlSI2ZrW6vZg0YrLI81iQSTkMmOqnqCaNr/8Ek4VwWcN5vZgCYWbg/JJSGBlUaYGAOP73qBw== + dependencies: + "@types/http-errors" "*" + "@types/node" "*" + "@types/send" "<1" + "@types/serve-static@^1.15.5": version "1.15.8" resolved "https://registry.yarnpkg.com/@types/serve-static/-/serve-static-1.15.8.tgz#8180c3fbe4a70e8f00b9f70b9ba7f08f35987877" @@ -4506,7 +4651,7 @@ resolved "https://registry.yarnpkg.com/@xtuc/long/-/long-4.2.2.tgz#d291c6a4e97989b5c61d9acf396ae4fe133a718d" integrity sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ== -accepts@~1.3.4, accepts@~1.3.5, accepts@~1.3.8: +accepts@~1.3.4, accepts@~1.3.8: version "1.3.8" resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.8.tgz#0bf0be125b67014adcb0b0921e62db7bffe16b2e" integrity sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw== @@ -4714,6 +4859,15 @@ array-union@^2.1.0: resolved "https://registry.yarnpkg.com/array-union/-/array-union-2.1.0.tgz#b798420adbeb1de828d84acd8a2e23d3efe85e8d" integrity sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw== +asn1js@^3.0.6: + version "3.0.10" + resolved "https://registry.yarnpkg.com/asn1js/-/asn1js-3.0.10.tgz#df26c874c8a8b41ca605efea47b2ad07551013dd" + integrity sha512-S2s3aOytiKdFRdulw2qPE51MzjzVOisppcVv7jVFR+Kw0kxwvFrDcYA0h7Ndqbmj0HkMIXYWaoj7fli8kgx1eg== + dependencies: + pvtsutils "^1.3.6" + pvutils "^1.1.5" + tslib "^2.8.1" + astring@^1.8.0: version "1.8.6" resolved "https://registry.yarnpkg.com/astring/-/astring-1.8.6.tgz#2c9c157cf1739d67561c56ba896e6948f6b93731" @@ -4820,10 +4974,10 @@ binary-extensions@^2.0.0: resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.2.0.tgz#75f502eeaf9ffde42fc98829645be4ea76bd9e2d" integrity sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA== -body-parser@~1.20.3: - version "1.20.4" - resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.20.4.tgz#f8e20f4d06ca8a50a71ed329c15dccad1cdc547f" - integrity sha512-ZTgYYLMOXY9qKU/57FAo8F+HA2dGX7bqGc71txDRC1rS4frdFI5R7NhluHxH6M0YItAP0sHB4uqAOcYKxO6uGA== +body-parser@~1.20.5: + version "1.20.5" + resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.20.5.tgz#303c8c34423d1d6fa799bc764e93c1e4dc6ebf64" + integrity sha512-3grm+/2tUOvu2cjJkvsIxrv/wVpfXQW4PsQHYm7yk4vfpu7Ekl6nEsYBoJUL6qDwZUx8wUhQ8tR2qz+ad9c9OA== dependencies: bytes "~3.1.2" content-type "~1.0.5" @@ -4833,7 +4987,7 @@ body-parser@~1.20.3: http-errors "~2.0.1" iconv-lite "~0.4.24" on-finished "~2.4.1" - qs "~6.14.0" + qs "~6.15.1" raw-body "~2.5.3" type-is "~1.6.18" unpipe "~1.0.0" @@ -4922,11 +5076,16 @@ bytes@3.0.0: resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.0.0.tgz#d32815404d689699f85a4ea4fa8755dd13a96048" integrity sha512-pMhOfFDPiv9t5jjIXkHosWmkSyQbvsgEVNkz0ERHbuLh2T/7j4Mqqpz523Fe8MVY89KC6Sh/QfS2sM+SjgFDcw== -bytes@~3.1.2: +bytes@3.1.2, bytes@~3.1.2: version "3.1.2" resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.2.tgz#8b0beeb98605adf1b128fa4386403c009e0221a5" integrity sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg== +bytestreamjs@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/bytestreamjs/-/bytestreamjs-2.0.1.tgz#a32947c7ce389a6fa11a09a9a563d0a45889535e" + integrity sha512-U1Z/ob71V/bXfVABvNr/Kumf5VyeQRBEm6Txb0PQ6S7V5GpBM3w4Cbqz/xPDicR5tN0uvDifng8C+5qECeGwyQ== + cacheable-lookup@^7.0.0: version "7.0.0" resolved "https://registry.yarnpkg.com/cacheable-lookup/-/cacheable-lookup-7.0.0.tgz#3476a8215d046e5a3202a9209dd13fec1f933a27" @@ -5272,24 +5431,24 @@ common-path-prefix@^3.0.0: resolved "https://registry.yarnpkg.com/common-path-prefix/-/common-path-prefix-3.0.0.tgz#7d007a7e07c58c4b4d5f433131a19141b29f11e0" integrity sha512-QE33hToZseCH3jS0qN96O/bSh3kaw/h+Tq7ngyY9eWDUnTlTNUyqfqvCXioLe5Na5jFsL78ra/wuBU4iuEgd4w== -compressible@~2.0.16: +compressible@~2.0.18: version "2.0.18" resolved "https://registry.yarnpkg.com/compressible/-/compressible-2.0.18.tgz#af53cca6b070d4c3c0750fbd77286a6d7cc46fba" integrity sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg== dependencies: mime-db ">= 1.43.0 < 2" -compression@^1.7.4: - version "1.7.4" - resolved "https://registry.yarnpkg.com/compression/-/compression-1.7.4.tgz#95523eff170ca57c29a0ca41e6fe131f41e5bb8f" - integrity sha512-jaSIDzP9pZVS4ZfQ+TzvtiWhdpFhE2RDHz8QJkpX9SIpLq88VueF5jJw6t+6CUQcAoA6t+x89MLrWAqpfDE8iQ== +compression@^1.8.1: + version "1.8.1" + resolved "https://registry.yarnpkg.com/compression/-/compression-1.8.1.tgz#4a45d909ac16509195a9a28bd91094889c180d79" + integrity sha512-9mAqGPHLakhCLeNyxPkK4xVo746zQ/czLH1Ky+vkitMnWfWZps8r0qXuwhwizagCRttsL4lfG4pIOvaWLpAP0w== dependencies: - accepts "~1.3.5" - bytes "3.0.0" - compressible "~2.0.16" + bytes "3.1.2" + compressible "~2.0.18" debug "2.6.9" - on-headers "~1.0.2" - safe-buffer "5.1.2" + negotiator "~0.6.4" + on-headers "~1.1.0" + safe-buffer "5.2.1" vary "~1.1.2" concat-map@0.0.1: @@ -6128,14 +6287,14 @@ execa@^5.1.1: signal-exit "^3.0.3" strip-final-newline "^2.0.0" -express@^4.21.2: - version "4.22.1" - resolved "https://registry.yarnpkg.com/express/-/express-4.22.1.tgz#1de23a09745a4fffdb39247b344bb5eaff382069" - integrity sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g== +express@^4.22.1: + version "4.22.2" + resolved "https://registry.yarnpkg.com/express/-/express-4.22.2.tgz#c17ae0981e5efc24b22272f0e041c4662503b700" + integrity sha512-IuL+Elrou2ZvCFHs18/CIzy2Nzvo25nZ1/D2eIZlz7c+QUayAcYoiM2BthCjs+EBHVpjYjcuLDAiCWgeIX3X1Q== dependencies: accepts "~1.3.8" array-flatten "1.1.1" - body-parser "~1.20.3" + body-parser "~1.20.5" content-disposition "~0.5.4" content-type "~1.0.4" cookie "~0.7.1" @@ -6154,7 +6313,7 @@ express@^4.21.2: parseurl "~1.3.3" path-to-regexp "~0.1.12" proxy-addr "~2.0.7" - qs "~6.14.0" + qs "~6.15.1" range-parser "~1.2.1" safe-buffer "5.2.1" send "~0.19.0" @@ -8348,6 +8507,11 @@ negotiator@0.6.3: resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.3.tgz#58e323a72fedc0d6f9cd4d31fe49f51479590ccd" integrity sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg== +negotiator@~0.6.4: + version "0.6.4" + resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.4.tgz#777948e2452651c570b712dd01c23e262713fff7" + integrity sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w== + neo-async@^2.6.2: version "2.6.2" resolved "https://registry.yarnpkg.com/neo-async/-/neo-async-2.6.2.tgz#b4aafb93e3aeb2d8174ca53cf163ab7d7308305f" @@ -8371,11 +8535,6 @@ node-emoji@^2.1.0: emojilib "^2.4.0" skin-tone "^2.0.0" -node-forge@^1: - version "1.4.0" - resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-1.4.0.tgz#1c7b7d8bdc2d078739f58287d589d903a11b2fc2" - integrity sha512-LarFH0+6VfriEhqMMcLX2F7SwSXeWwnEAJEsYm5QKWchiVYVvJyV9v7UDvUv+w5HO23ZpQTXDv/GxdDdMyOuoQ== - node-releases@^2.0.27: version "2.0.27" resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.27.tgz#eedca519205cf20f650f61d56b070db111231e4e" @@ -8460,10 +8619,10 @@ on-finished@2.4.1, on-finished@^2.4.1, on-finished@~2.4.1: dependencies: ee-first "1.1.1" -on-headers@~1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/on-headers/-/on-headers-1.0.2.tgz#772b0ae6aaa525c399e489adfad90c403eb3c28f" - integrity sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA== +on-headers@~1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/on-headers/-/on-headers-1.1.0.tgz#59da4f91c45f5f989c6e4bcedc5a3b0aed70ff65" + integrity sha512-737ZY3yNnXy37FHkQxPzt4UZ2UWPWiCZWLvFZ4fu5cueciegX0zGPnrlY6bwRg4FdQOe9YU8MkmJwGhoMybl8A== onetime@^5.1.2: version "5.1.2" @@ -8720,6 +8879,18 @@ pkg-dir@^7.0.0: dependencies: find-up "^6.3.0" +pkijs@^3.3.3: + version "3.4.0" + resolved "https://registry.yarnpkg.com/pkijs/-/pkijs-3.4.0.tgz#d9164def30ff6d97be2d88966d5e36192499ca9c" + integrity sha512-emEcLuomt2j03vxD54giVB4SxTjnsqkU692xZOZXHDVoYyypEm+b3jpiTcc+Cf+myooc+/Ly0z01jqeNHVgJGw== + dependencies: + "@noble/hashes" "1.4.0" + asn1js "^3.0.6" + bytestreamjs "^2.0.1" + pvtsutils "^1.3.6" + pvutils "^1.1.3" + tslib "^2.8.1" + postcss-attribute-case-insensitive@^7.0.1: version "7.0.1" resolved "https://registry.yarnpkg.com/postcss-attribute-case-insensitive/-/postcss-attribute-case-insensitive-7.0.1.tgz#0c4500e3bcb2141848e89382c05b5a31c23033a3" @@ -9363,10 +9534,22 @@ pupa@^3.1.0: dependencies: escape-goat "^4.0.0" -qs@~6.14.0: - version "6.14.2" - resolved "https://registry.yarnpkg.com/qs/-/qs-6.14.2.tgz#b5634cf9d9ad9898e31fba3504e866e8efb6798c" - integrity sha512-V/yCWTTF7VJ9hIh18Ugr2zhJMP01MY7c5kh4J870L7imm6/DIzBsNLTXzMwUA3yZ5b/KBqLx8Kp3uRvd7xSe3Q== +pvtsutils@^1.3.6: + version "1.3.6" + resolved "https://registry.yarnpkg.com/pvtsutils/-/pvtsutils-1.3.6.tgz#ec46e34db7422b9e4fdc5490578c1883657d6001" + integrity sha512-PLgQXQ6H2FWCaeRak8vvk1GW462lMxB5s3Jm673N82zI4vqtVUPuZdffdZbPDFRoU8kAhItWFtPCWiPpp4/EDg== + dependencies: + tslib "^2.8.1" + +pvutils@^1.1.3, pvutils@^1.1.5: + version "1.1.5" + resolved "https://registry.yarnpkg.com/pvutils/-/pvutils-1.1.5.tgz#84b0dea4a5d670249aa9800511804ee0b7c2809c" + integrity sha512-KTqnxsgGiQ6ZAzZCVlJH5eOjSnvlyEgx1m8bkRJfOhmGRqfo5KLvmAlACQkrjEtOQ4B7wF9TdSLIs9O90MX9xA== + +qs@~6.15.1: + version "6.15.2" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.15.2.tgz#fd55426d710403ddccc45e0f9eab16db7727ece9" + integrity sha512-Rzq0KEyX/w/tEybncDgdkZrJgVUsUMk3xjh3t5bv3S1HTAtg+uOYt72+ZfwiQwKdysThkTBdL/rTi6HDmX9Ddw== dependencies: side-channel "^1.1.0" @@ -9533,6 +9716,11 @@ readdirp@~3.6.0: dependencies: picomatch "^2.2.1" +reflect-metadata@^0.2.2: + version "0.2.2" + resolved "https://registry.yarnpkg.com/reflect-metadata/-/reflect-metadata-0.2.2.tgz#400c845b6cba87a21f2c65c4aeb158f4fa4d9c5b" + integrity sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q== + regenerate-unicode-properties@^10.0.1: version "10.0.1" resolved "https://registry.yarnpkg.com/regenerate-unicode-properties/-/regenerate-unicode-properties-10.0.1.tgz#7f442732aa7934a3740c779bb9b3340dccc1fb56" @@ -9836,16 +10024,16 @@ run-parallel@^1.1.9: dependencies: queue-microtask "^1.2.2" -safe-buffer@5.1.2, safe-buffer@~5.1.0, safe-buffer@~5.1.1: - version "5.1.2" - resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" - integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== - safe-buffer@5.2.1, safe-buffer@>=5.1.0, safe-buffer@^5.1.0, safe-buffer@~5.2.0: version "5.2.1" resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== +safe-buffer@~5.1.0, safe-buffer@~5.1.1: + version "5.1.2" + resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" + integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== + "safer-buffer@>= 2.1.2 < 3", "safer-buffer@>= 2.1.2 < 3.0.0": version "2.1.2" resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" @@ -9933,13 +10121,13 @@ select-hose@^2.0.0: resolved "https://registry.yarnpkg.com/select-hose/-/select-hose-2.0.0.tgz#625d8658f865af43ec962bfc376a37359a4994ca" integrity sha512-mEugaLK+YfkijB4fx0e6kImuJdCIt2LxCRcbEYPqRGCs4F2ogyfZU5IAZRdjCP8JPq2AtdNoC/Dux63d9Kiryg== -selfsigned@^2.4.1: - version "2.4.1" - resolved "https://registry.yarnpkg.com/selfsigned/-/selfsigned-2.4.1.tgz#560d90565442a3ed35b674034cec4e95dceb4ae0" - integrity sha512-th5B4L2U+eGLq1TVh7zNRGBapioSORUeymIydxgFpwww9d2qyKvtuPU2jJuHvYAwwqi2Y596QBL3eEqcPEYL8Q== +selfsigned@^5.5.0: + version "5.5.0" + resolved "https://registry.yarnpkg.com/selfsigned/-/selfsigned-5.5.0.tgz#4c9ab7c7c9f35f18fb6a9882c253eb0e6bd6557b" + integrity sha512-ftnu3TW4+3eBfLRFnDEkzGxSF/10BJBkaLJuBHZX0kiPS7bRdlpZGu6YGt4KngMkdTwJE6MbjavFpqHvqVt+Ew== dependencies: - "@types/node-forge" "^1.3.0" - node-forge "^1" + "@peculiar/x509" "^1.14.2" + pkijs "^3.3.3" semver-diff@^4.0.0: version "4.0.0" @@ -10554,11 +10742,23 @@ trough@^2.0.0: resolved "https://registry.yarnpkg.com/trough/-/trough-2.2.0.tgz#94a60bd6bd375c152c1df911a4b11d5b0256f50f" integrity sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw== -tslib@^2.0.0, tslib@^2.0.3, tslib@^2.4.0, tslib@^2.6.0: +tslib@^1.9.3: + version "1.14.1" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00" + integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg== + +tslib@^2.0.0, tslib@^2.0.3, tslib@^2.4.0, tslib@^2.6.0, tslib@^2.8.1: version "2.8.1" resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.8.1.tgz#612efe4ed235d567e8aba5f2a5fab70280ade83f" integrity sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w== +tsyringe@^4.10.0: + version "4.10.0" + resolved "https://registry.yarnpkg.com/tsyringe/-/tsyringe-4.10.0.tgz#d0c95815d584464214060285eaaadd94aa03299c" + integrity sha512-axr3IdNuVIxnaK5XGEUFTu3YmAQ6lllgrvqfEoR16g/HGnYY/6We4oWENtAnzK6/LpJ2ur9PAb80RBt7/U4ugw== + dependencies: + tslib "^1.9.3" + type-fest@^1.0.1: version "1.4.0" resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-1.4.0.tgz#e9fb813fe3bf1744ec359d55d1affefa76f14be1" @@ -10865,13 +11065,13 @@ webpack-dev-middleware@^7.4.2: schema-utils "^4.0.0" webpack-dev-server@^5.2.2: - version "5.2.2" - resolved "https://registry.yarnpkg.com/webpack-dev-server/-/webpack-dev-server-5.2.2.tgz#96a143d50c58fef0c79107e61df911728d7ceb39" - integrity sha512-QcQ72gh8a+7JO63TAx/6XZf/CWhgMzu5m0QirvPfGvptOusAxG12w2+aua1Jkjr7hzaWDnJ2n6JFeexMHI+Zjg== + version "5.2.4" + resolved "https://registry.yarnpkg.com/webpack-dev-server/-/webpack-dev-server-5.2.4.tgz#6e6306ce59848ed322c235e48b326632b1eed6d6" + integrity sha512-GqDPGZN9bRqKBTkp4aWkobDDHMsrXKoGSdOH56smIri8qR0JG8gfL8/v/f/OZR3/OKXjG8uwJbFVhKm/FNU/UA== dependencies: "@types/bonjour" "^3.5.13" "@types/connect-history-api-fallback" "^1.5.4" - "@types/express" "^4.17.21" + "@types/express" "^4.17.25" "@types/express-serve-static-core" "^4.17.21" "@types/serve-index" "^1.9.4" "@types/serve-static" "^1.15.5" @@ -10881,9 +11081,9 @@ webpack-dev-server@^5.2.2: bonjour-service "^1.2.1" chokidar "^3.6.0" colorette "^2.0.10" - compression "^1.7.4" + compression "^1.8.1" connect-history-api-fallback "^2.0.0" - express "^4.21.2" + express "^4.22.1" graceful-fs "^4.2.6" http-proxy-middleware "^2.0.9" ipaddr.js "^2.1.0" @@ -10891,7 +11091,7 @@ webpack-dev-server@^5.2.2: open "^10.0.3" p-retry "^6.2.0" schema-utils "^4.2.0" - selfsigned "^2.4.1" + selfsigned "^5.5.0" serve-index "^1.9.1" sockjs "^0.3.24" spdy "^4.0.2" From 0755d845409215a4c294939ef4974f2e9e957a5e Mon Sep 17 00:00:00 2001 From: Tomasz Godzik Date: Thu, 28 May 2026 20:27:42 +0200 Subject: [PATCH 05/12] chore: Fix code rabbit config (#8419) --- .coderabbit.yaml | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/.coderabbit.yaml b/.coderabbit.yaml index 3f330d31f4e..28cc24b1eeb 100644 --- a/.coderabbit.yaml +++ b/.coderabbit.yaml @@ -1,3 +1,5 @@ -base_branches: - - main - - main-v2 +reviews: + auto_review: + base_branches: + - main + - main-v2 From be76003fdece78ac805a06936a99a9672ebec979 Mon Sep 17 00:00:00 2001 From: Simon Schenk Date: Thu, 28 May 2026 20:36:30 +0200 Subject: [PATCH 06/12] fix: test execution over MCP (#8418) Test execution over mcp would silently fail with no output, if the class to test could not be found by the presentation compiler. This change ensures the file with the test is compiled before attempting to find the test. In case a test lives in a class with a companion object, two `Discovered` instances were potentially created, one for the class and one for the object. Now the class is preferred over the object, such that for class based test frameworks the companion is ignored, but for module based frameworks such as ZIO test the module is uses, since no class exists. /fixes https://github.com/scalameta/metals/issues/8276 /fixes https://github.com/scalameta/metals/issues/8138 ## Summary by CodeRabbit * **New Features** * Broader ZIO test framework compatibility to detect additional suite types. * **Bug Fixes** * Fixed duplicate test discovery by improving deduplication logic. * Ensured source files are compiled before running tests to improve execution reliability. * Added error logging when compiler/type information is missing for test suites. [![Review Change Stack](https://storage.googleapis.com/coderabbit_public_assets/review-stack-in-coderabbit-ui.svg)](https://app.coderabbit.ai/change-stack/scalameta/metals/pull/8418?utm_source=github_walkthrough&utm_medium=github&utm_campaign=change_stack) --- .../metals/ProjectMetalsLspService.scala | 1 + .../internal/metals/debug/DebugProvider.scala | 36 ++++++++++++------- .../internal/metals/mcp/McpTestRunner.scala | 3 ++ .../frameworks/ZioTestFinder.scala | 1 + 4 files changed, 28 insertions(+), 13 deletions(-) diff --git a/metals/src/main/scala/scala/meta/internal/metals/ProjectMetalsLspService.scala b/metals/src/main/scala/scala/meta/internal/metals/ProjectMetalsLspService.scala index b95548b03b2..8e95be3ad1f 100644 --- a/metals/src/main/scala/scala/meta/internal/metals/ProjectMetalsLspService.scala +++ b/metals/src/main/scala/scala/meta/internal/metals/ProjectMetalsLspService.scala @@ -260,6 +260,7 @@ class ProjectMetalsLspService( new McpTestRunner( debugProvider, buildTargets, + compilations, folder, () => userConfig, mcpSearch, diff --git a/metals/src/main/scala/scala/meta/internal/metals/debug/DebugProvider.scala b/metals/src/main/scala/scala/meta/internal/metals/debug/DebugProvider.scala index 3cbcf1a6574..4d955769ed2 100644 --- a/metals/src/main/scala/scala/meta/internal/metals/debug/DebugProvider.scala +++ b/metals/src/main/scala/scala/meta/internal/metals/debug/DebugProvider.scala @@ -449,6 +449,7 @@ class DebugProvider( s"Found infos about: ${infos.flatten.map(_._2.symbol).mkString(", ")} test classes" ) val allInfo = infos.flatten + if (allInfo.isEmpty) { val suitesString = testClasses.getSuites().asScala.map(_.getClassName()).mkString(", ") @@ -456,21 +457,30 @@ class DebugProvider( s"Could not get information from the compiler about $suitesString" ) } - infos.flatten.groupBy(_._1.framework).map { - case (framework, testSuites) => + + // Prefer `#` over `.` to avoid synthetic companion objects + allInfo + .groupBy(_._1.fullyQualifiedName) + .values + .map { entries => + val (info, pcInfo) = entries + .find(e => !e._2.symbol.endsWith(".")) + .getOrElse(entries.head) ( - framework, - testSuites.map { case (testInfo, pcInfo) => - new Discovered( - pcInfo.symbol, - testInfo.fullyQualifiedName, - pcInfo.recursiveParents.map(_.symbolToFullyQualifiedName).toSet, - (pcInfo.annotations ++ pcInfo.memberDefsAnnotations).toSet, - isModule = pcInfo.symbol.endsWith("."), - ) - }, + info.framework, + new Discovered( + pcInfo.symbol, + info.fullyQualifiedName, + pcInfo.recursiveParents.map(_.symbolToFullyQualifiedName).toSet, + (pcInfo.annotations ++ pcInfo.memberDefsAnnotations).toSet, + isModule = pcInfo.symbol.endsWith("."), + ), ) - } + } + .groupBy(_._1) + .map { case (framework, entries) => + framework -> entries.map(_._2).toList + } } } diff --git a/metals/src/main/scala/scala/meta/internal/metals/mcp/McpTestRunner.scala b/metals/src/main/scala/scala/meta/internal/metals/mcp/McpTestRunner.scala index 0304902a1c3..7ddb8b5d6d2 100644 --- a/metals/src/main/scala/scala/meta/internal/metals/mcp/McpTestRunner.scala +++ b/metals/src/main/scala/scala/meta/internal/metals/mcp/McpTestRunner.scala @@ -8,6 +8,7 @@ import scala.concurrent.Promise import scala.meta.internal.ansi.AnsiFilter import scala.meta.internal.metals.BuildTargets +import scala.meta.internal.metals.Compilations import scala.meta.internal.metals.MetalsEnrichments._ import scala.meta.internal.metals.UserConfiguration import scala.meta.internal.metals.debug.DebugProvider @@ -22,6 +23,7 @@ import ch.epfl.scala.{bsp4j => b} class McpTestRunner( debugProvider: DebugProvider, buildTargets: BuildTargets, + compilations: Compilations, workspace: AbsolutePath, userConfig: () => UserConfiguration, mcpSearch: McpSymbolSearch, @@ -54,6 +56,7 @@ class McpTestRunner( ) } yield { for { + _ <- compilations.compileFile(path) env <- jvmTestEnv settings = DebugProvider.scalaTestLocalRunSettings(workspace, env) testSuites = new b.ScalaTestSuites( diff --git a/metals/src/main/scala/scala/meta/internal/metals/testProvider/frameworks/ZioTestFinder.scala b/metals/src/main/scala/scala/meta/internal/metals/testProvider/frameworks/ZioTestFinder.scala index 59236c27b86..8aa7555603c 100644 --- a/metals/src/main/scala/scala/meta/internal/metals/testProvider/frameworks/ZioTestFinder.scala +++ b/metals/src/main/scala/scala/meta/internal/metals/testProvider/frameworks/ZioTestFinder.scala @@ -117,5 +117,6 @@ object ZioTestFinder { "zio/test/DefaultRunnableSpec#", "zio/test/RunnableSpec#", "zio/test/ZIOSpecDefault#", + "zio/test/ZIOSpecAbstract#", ) } From 9845c5565ced651381b17fae9767167d01330888 Mon Sep 17 00:00:00 2001 From: Tomasz Godzik Date: Fri, 29 May 2026 08:12:17 +0200 Subject: [PATCH 07/12] bugfix: Fix failing JDK 25 tests (#8420) ## Summary by CodeRabbit * **Tests** * Updated test expectations for Java 21 compatibility. [![Review Change Stack](https://storage.googleapis.com/coderabbit_public_assets/review-stack-in-coderabbit-ui.svg)](https://app.coderabbit.ai/change-stack/scalameta/metals/pull/8420?utm_source=github_walkthrough&utm_medium=github&utm_campaign=change_stack) --- tests/unit/src/test/scala/tests/ImplementationLspSuite.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/unit/src/test/scala/tests/ImplementationLspSuite.scala b/tests/unit/src/test/scala/tests/ImplementationLspSuite.scala index b8ae4cf82fc..464be799034 100644 --- a/tests/unit/src/test/scala/tests/ImplementationLspSuite.scala +++ b/tests/unit/src/test/scala/tests/ImplementationLspSuite.scala @@ -682,6 +682,7 @@ class ImplementationLspSuite extends BaseImplementationSuite("implementation") { |com/sun/nio/sctp/IllegalUnbindException# |com/sun/nio/sctp/InvalidStreamException# |com/sun/org/apache/bcel/internal/classfile/ClassFormatException# + |com/sun/org/apache/bcel/internal/classfile/InvalidMethodSignatureException# |com/sun/org/apache/bcel/internal/generic/ClassGenException# |com/sun/org/apache/bcel/internal/generic/TargetLostException# |com/sun/org/apache/xalan/internal/xsltc/TransletException# @@ -695,7 +696,6 @@ class ImplementationLspSuite extends BaseImplementationSuite("implementation") { |com/sun/org/apache/xerces/internal/impl/dv/InvalidDatatypeFacetException# |com/sun/org/apache/xerces/internal/impl/dv/InvalidDatatypeValueException# |com/sun/org/apache/xerces/internal/impl/dv/xs/SchemaDateTimeException# - |com/sun/org/apache/xerces/internal/impl/io/MalformedByteSequenceException# |""".stripMargin, topLines = Some(50), ) From 14a67800c2743a180ea3ee06d7ed8ebb99e35b44 Mon Sep 17 00:00:00 2001 From: Myroslav Date: Fri, 29 May 2026 09:03:06 +0200 Subject: [PATCH 08/12] Fixed typos and grammar (#8421) ## Summary by CodeRabbit * **Documentation** * Improved contributor guide wording for clearer setup and development guidance across multiple sections. * Clarified a plugin description to indicate integration via the Build Server Protocol and its role in producing SemanticDB outputs. * Fixed a presentation typo in the cross-tests section. * Rephrased instructions for updating build tool launchers/wrappers and the sbt launcher to reduce ambiguity while preserving existing guidance about launching tools not on the PATH. [![Review Change Stack](https://storage.googleapis.com/coderabbit_public_assets/review-stack-in-coderabbit-ui.svg)](https://app.coderabbit.ai/change-stack/scalameta/metals/pull/8421?utm_source=github_walkthrough&utm_medium=github&utm_campaign=change_stack) --- docs/contributors/getting-started.md | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/docs/contributors/getting-started.md b/docs/contributors/getting-started.md index 28e818319a2..dbf133ae9e1 100644 --- a/docs/contributors/getting-started.md +++ b/docs/contributors/getting-started.md @@ -52,8 +52,8 @@ To avoid repetition, common utilities of presentation compilers are in `mtags-sh - `tests/input` - Example Scala code that is used as testing data for unit tests. ### Other modules -- `sbt-metals` - the sbt plugin used when users are using the BSP support from - sbt to ensure semanticDB is being produced by sbt. +- `sbt-metals` - an sbt plugin for integration with Metals through the Build Server Protocol. + It ensures that SemanticDB files are produced by sbt. - `docs` - documentation markdown for the Metals website. - `metals-docs` - methods used for generating documentation across multiple pages in `docs`. @@ -235,7 +235,7 @@ possible to investigate why test is failing manually. ## Cross tests -Tests for Scala 2 presenatation compiler, check common features such as hover, completions or signatures. +Tests for Scala 2 presentation compiler that check common features such as hover, completions, and signatures. ```sh sbt @@ -398,9 +398,9 @@ waiting for the debugger to connect: ## Updating build tool launcher/wrappers Metals uses various wrappers or launchers for each build tool that it supports. -This makes sure that when your in a workspace for you build tool that metals is -able to correctly launch that build tool, even if it doesn't exist on the users -`$PATH`. You can see their usages in `BuildTool.scala`. +This ensures that Metals is able to launch the necessary build tool in any workspace, +even if the tool isn't present on the users `$PATH`. +You can see their usages in `BuildTool.scala`. ### Updating sbt-launcher @@ -411,8 +411,8 @@ command: cp "$(cs fetch org.scala-sbt:sbt-launch:)" sbt-launch.jar ``` -This will allow you to not have to do some of the manual steps with the launcher -properties file listed [here](https://github.com/sbt/launcher). +This will allow you to skip some of the manual steps with the launcher +properties file that are listed [here](https://github.com/sbt/launcher). ### Updating maven wrappers From 278ac1a114dbc5e59cc94f517265f4317543ff93 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 2 Jun 2026 10:00:36 +0200 Subject: [PATCH 09/12] build(deps): bump the npm-dependencies group in /website with 4 updates (#8434) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps the npm-dependencies group in /website with 4 updates: [@easyops-cn/docusaurus-search-local](https://github.com/easyops-cn/docusaurus-search-local/tree/HEAD/packages/docusaurus-search-local), [react](https://github.com/facebook/react/tree/HEAD/packages/react), [react-dom](https://github.com/facebook/react/tree/HEAD/packages/react-dom) and [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node). Updates `@easyops-cn/docusaurus-search-local` from 0.55.1 to 0.55.2
Release notes

Sourced from @​easyops-cn/docusaurus-search-local's releases.

v0.55.2

0.55.2 (2026-05-31)

Bug Fixes

  • some languages need lunr wordcut (1e60641)
  • some languages need lunr wordcut (d871555), closes #438
Commits

Updates `react` from 19.2.5 to 19.2.7
Release notes

Sourced from react's releases.

19.2.7 (June 1st, 2026)

React Server Components

19.2.6 (May 6th, 2026)

React Server Components

Commits
Maintainer changes

This version was pushed to npm by GitHub Actions, a new releaser for react since your current version.


Updates `react-dom` from 19.2.5 to 19.2.7
Release notes

Sourced from react-dom's releases.

19.2.7 (June 1st, 2026)

React Server Components

19.2.6 (May 6th, 2026)

React Server Components

Commits
Maintainer changes

This version was pushed to npm by GitHub Actions, a new releaser for react-dom since your current version.


Updates `@types/node` from 25.6.0 to 25.9.1
Commits

Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore major version` will close this group update PR and stop Dependabot creating any more for the specific dependency's major version (unless you unignore this specific dependency's major version or upgrade to it yourself) - `@dependabot ignore minor version` will close this group update PR and stop Dependabot creating any more for the specific dependency's minor version (unless you unignore this specific dependency's minor version or upgrade to it yourself) - `@dependabot ignore ` will close this group update PR and stop Dependabot creating any more for the specific dependency (unless you unignore this specific dependency or upgrade to it yourself) - `@dependabot unignore ` will remove all of the ignore conditions of the specified dependency - `@dependabot unignore ` will remove the ignore condition of the specified dependency and ignore conditions
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- website/package.json | 8 +++---- website/yarn.lock | 52 +++++++++++++++++++------------------------- 2 files changed, 26 insertions(+), 34 deletions(-) diff --git a/website/package.json b/website/package.json index 8238008b0df..4ffd669171f 100644 --- a/website/package.json +++ b/website/package.json @@ -8,16 +8,16 @@ "serve": "docusaurus serve" }, "devDependencies": { - "@types/node": "^25.6.0" + "@types/node": "^25.9.1" }, "dependencies": { "@docusaurus/core": "3.10.1", "@docusaurus/faster": "3.10.1", "@docusaurus/plugin-client-redirects": "3.10.1", "@docusaurus/preset-classic": "3.10.1", - "@easyops-cn/docusaurus-search-local": "0.55.1", + "@easyops-cn/docusaurus-search-local": "0.55.2", "clsx": "^2.1.1", - "react": "^19.2.5", - "react-dom": "^19.2.5" + "react": "^19.2.7", + "react-dom": "^19.2.7" } } diff --git a/website/yarn.lock b/website/yarn.lock index cb773ccf29a..a6a731210f8 100644 --- a/website/yarn.lock +++ b/website/yarn.lock @@ -3057,7 +3057,7 @@ tslib "^2.6.0" utility-types "^3.10.0" -"@docusaurus/theme-translations@3.10.1": +"@docusaurus/theme-translations@3.10.1", "@docusaurus/theme-translations@^2 || ^3": version "3.10.1" resolved "https://registry.yarnpkg.com/@docusaurus/theme-translations/-/theme-translations-3.10.1.tgz#c3119a015652290eea560ca45ac775963d6eb75b" integrity sha512-cLMyaKivjBVWKMJuWqyFVVgtqe8DPJNPkog0bn8W1MDVAKcPdxRFycBfC1We1RaNp7Rdk513bmtW78RR6OBxBw== @@ -3065,14 +3065,6 @@ fs-extra "^11.1.1" tslib "^2.6.0" -"@docusaurus/theme-translations@^2 || ^3": - version "3.9.2" - resolved "https://registry.yarnpkg.com/@docusaurus/theme-translations/-/theme-translations-3.9.2.tgz#238cd69c2da92d612be3d3b4f95944c1d0f1e041" - integrity sha512-vIryvpP18ON9T9rjgMRFLr2xJVDpw1rtagEGf8Ccce4CkTrvM/fRB8N2nyWYOW5u3DdjkwKw5fBa+3tbn9P4PA== - dependencies: - fs-extra "^11.1.1" - tslib "^2.6.0" - "@docusaurus/types@3.10.1": version "3.10.1" resolved "https://registry.yarnpkg.com/@docusaurus/types/-/types-3.10.1.tgz#d42837938ae43ca2be0ca47e63e00476b5eb94be" @@ -3146,10 +3138,10 @@ cssesc "^3.0.0" immediate "^3.2.3" -"@easyops-cn/docusaurus-search-local@0.55.1": - version "0.55.1" - resolved "https://registry.yarnpkg.com/@easyops-cn/docusaurus-search-local/-/docusaurus-search-local-0.55.1.tgz#a0ee40e598c10138d328db04183584b51a8cb0d3" - integrity sha512-jmBKj1J+tajqNrCvECwKCQYTWwHVZDGApy8lLOYEPe+Dm0/f3Ccdw8BP5/OHNpltr7WDNY2roQXn+TWn2f1kig== +"@easyops-cn/docusaurus-search-local@0.55.2": + version "0.55.2" + resolved "https://registry.yarnpkg.com/@easyops-cn/docusaurus-search-local/-/docusaurus-search-local-0.55.2.tgz#563ab1f8fd7bc18541d889f4d5001a3ef85c5a1e" + integrity sha512-dI/riu+MbDxkAjAHAdc0uahjXRaWKvbIPe9IAmA6AGcUfnVb9xd8s2I/6wEPTOXsAd6eFqn4Yis3WBWh3KUd3g== dependencies: "@docusaurus/plugin-content-docs" "^2 || ^3" "@docusaurus/theme-translations" "^2 || ^3" @@ -4337,12 +4329,12 @@ resolved "https://registry.yarnpkg.com/@types/ms/-/ms-0.7.34.tgz#10964ba0dee6ac4cd462e2795b6bebd407303433" integrity sha512-nG96G3Wp6acyAgJqGasjODb+acrI7KltPiRxzHPXnP3NgI28bpQDRv53olbqGXbfcgF5aiiHmO3xpwEpS5Ld9g== -"@types/node@*", "@types/node@^25.6.0": - version "25.6.0" - resolved "https://registry.yarnpkg.com/@types/node/-/node-25.6.0.tgz#4e09bad9b469871f2d0f68140198cbd714f4edca" - integrity sha512-+qIYRKdNYJwY3vRCZMdJbPLJAtGjQBudzZzdzwQYkEPQd+PJGixUL5QfvCLDaULoLv+RhT3LDkwEfKaAkgSmNQ== +"@types/node@*", "@types/node@^25.9.1": + version "25.9.1" + resolved "https://registry.yarnpkg.com/@types/node/-/node-25.9.1.tgz#3bda556db500ae4319c08e7fc9ab94f19013ba0b" + integrity sha512-xfrlY7UD5rMJk3ZVJP8BNzS28J36YJg+xp+LPXV1TdWxr8uMH5A860QNxYDGQe/ylDSgjxE52Q9VnO7p75tJxg== dependencies: - undici-types "~7.19.0" + undici-types ">=7.24.0 <7.24.7" "@types/node@^17.0.5": version "17.0.45" @@ -9600,10 +9592,10 @@ rc@1.2.8: minimist "^1.2.0" strip-json-comments "~2.0.1" -react-dom@^19.2.5: - version "19.2.5" - resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-19.2.5.tgz#b8768b10837d0b8e9ca5b9e2d58dff3d880ea25e" - integrity sha512-J5bAZz+DXMMwW/wV3xzKke59Af6CHY7G4uYLN1OvBcKEsWOs4pQExj86BBKamxl/Ik5bx9whOrvBlSDfWzgSag== +react-dom@^19.2.7: + version "19.2.7" + resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-19.2.7.tgz#0450dc9ae9ddbff76ef196401cd8b8c7fb466ccc" + integrity sha512-t0BRVXvbiE/o20Hfw669rLbMCDWtYZLvmJigy2f0MxsXF+71pxhR3xOkspmsO8h3ZlNzyibAmtCa3l4lYKk6gQ== dependencies: scheduler "^0.27.0" @@ -9682,10 +9674,10 @@ react-router@5.3.4, react-router@^5.3.4: tiny-invariant "^1.0.2" tiny-warning "^1.0.0" -react@^19.2.5: - version "19.2.5" - resolved "https://registry.yarnpkg.com/react/-/react-19.2.5.tgz#c888ab8b8ef33e2597fae8bdb2d77edbdb42858b" - integrity sha512-llUJLzz1zTUBrskt2pwZgLq59AemifIftw4aB7JxOqf1HY2FDaGDxgwpAPVzHU1kdWabH7FauP4i1oEeer2WCA== +react@^19.2.7: + version "19.2.7" + resolved "https://registry.yarnpkg.com/react/-/react-19.2.7.tgz#1f47a1bfc06f8ec885752c6f4af14369a9f8260b" + integrity sha512-HNe9WslTbXmFK8o8cmwgAeJFSBvt1bPdHCVKtaaV+WlAN36mpT4hcRpwbf3fY56ar2oIXzsBpOAiIRHAdY0OlQ== readable-stream@^2.0.1: version "2.3.7" @@ -10784,10 +10776,10 @@ typedarray-to-buffer@^3.1.5: dependencies: is-typedarray "^1.0.0" -undici-types@~7.19.0: - version "7.19.2" - resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-7.19.2.tgz#1b67fc26d0f157a0cba3a58a5b5c1e2276b8ba2a" - integrity sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg== +"undici-types@>=7.24.0 <7.24.7": + version "7.24.6" + resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-7.24.6.tgz#61275b485d7fd4e9d269c7cf04ec2873c9cc0f91" + integrity sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg== undici@^6.19.5: version "6.24.1" From 3012458fa36e57f8e207dbba74b0b10c6fd53c3b Mon Sep 17 00:00:00 2001 From: Myroslav Date: Tue, 2 Jun 2026 16:45:20 +0200 Subject: [PATCH 10/12] Fixed grammar (#8433) ## Summary by CodeRabbit * **Documentation** * Refined internal documentation with improved wording and grammar clarifications. --- .../internal/metals/MetalsLspService.scala | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/metals/src/main/scala/scala/meta/internal/metals/MetalsLspService.scala b/metals/src/main/scala/scala/meta/internal/metals/MetalsLspService.scala index 99fb844b43e..5726dc2727e 100644 --- a/metals/src/main/scala/scala/meta/internal/metals/MetalsLspService.scala +++ b/metals/src/main/scala/scala/meta/internal/metals/MetalsLspService.scala @@ -78,22 +78,22 @@ import org.eclipse.{lsp4j => l} /** * Metals implementation of the Scala Language Service. * @param ec - * Execution context used for submitting tasks. This class DO NOT manage the - * lifecycle of this execution context. + * Execution context used for submitting tasks. + * This class DOES NOT manage the lifecycle of this execution context. * @param sh - * Scheduled executor service used for scheduling tasks. This class DO NOT - * manage the lifecycle of this executor. + * Scheduled executor service used for scheduling tasks. + * This class DOES NOT manage the lifecycle of this executor. * @param serverInputs - * Collection of different parameters used by Metals for running, - * which main purpose is allowing for custom behavior in tests. + * Collection of different parameters used by Metals for running. + * Their main purpose is allowing for custom behavior in tests. * @param workspace * An absolute path to the workspace. * @param client - * Metals client used for sending notifications to the client. This class DO - * NOT manage the lifecycle of this client. It is the responsibility of the - * caller to shut down the client. + * Metals client used for sending notifications to the client. + * This class DOES NOT manage the lifecycle of this client. + * It is the responsibility of the caller to shut down the client. * @param initializeParams - * Initialization parameters send by the client in the initialize request, + * Initialization parameters received from the client in the initialize request, * which is the first request sent to the server by the client. */ abstract class MetalsLspService( From 87b9faf18b9c20f8eb60c5254d40c2f61bd4049f Mon Sep 17 00:00:00 2001 From: Tomasz Godzik Date: Wed, 3 Jun 2026 13:16:47 +0200 Subject: [PATCH 11/12] chore: Test with Scala 3.8.4 (#8288) ## Summary by CodeRabbit * **Chores** * Updated Scala version to 3.8.4. --- project/V.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/project/V.scala b/project/V.scala index 68218b77dd0..0c14888b5f4 100644 --- a/project/V.scala +++ b/project/V.scala @@ -13,7 +13,7 @@ object V { val scala3ForSBT2 = "3.7.4" - val latestScala3Next = "3.8.3" + val latestScala3Next = "3.8.4" // When you can add to removedScalaVersions in MtagsResolver.scala with the last released version val sbtScala = "2.12.20" From 7cf0e6b61b2459f2584164579aea16514b7414e8 Mon Sep 17 00:00:00 2001 From: Tomasz Godzik Date: Thu, 4 Jun 2026 13:42:44 +0200 Subject: [PATCH 12/12] improvement: Expose go to log to be used in command palette (#8448) ## Summary by CodeRabbit ## Release Notes * **New Features** * Enabled the GotoLog command, allowing users to quickly navigate to and view application logs. --- .../main/scala/scala/meta/internal/metals/ServerCommands.scala | 1 + 1 file changed, 1 insertion(+) diff --git a/metals/src/main/scala/scala/meta/internal/metals/ServerCommands.scala b/metals/src/main/scala/scala/meta/internal/metals/ServerCommands.scala index 13c4ade5906..e8aa15d71ec 100644 --- a/metals/src/main/scala/scala/meta/internal/metals/ServerCommands.scala +++ b/metals/src/main/scala/scala/meta/internal/metals/ServerCommands.scala @@ -827,6 +827,7 @@ object ServerCommands { GotoPosition, GotoSuperMethod, GotoSymbol, + GotoLog, ImportBuild, InsertInferredType, InsertInferredMethod,