diff --git a/.coderabbit.yaml b/.coderabbit.yaml new file mode 100644 index 00000000000..28cc24b1eeb --- /dev/null +++ b/.coderabbit.yaml @@ -0,0 +1,5 @@ +reviews: + auto_review: + base_branches: + - main + - main-v2 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 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/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..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(()) @@ -68,6 +72,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..80cf07c5010 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,12 @@ final class FileDecoderProvider( case "file" => decodeMetalsFile(uri) case "metalsDecode" => decodedFileContents(uri.getSchemeSpecificPart()) + case "metalsfs" => + 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 74535d64f08..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,6 +29,9 @@ trait IndexProviders { def referencesProvider: ReferenceProvider def workspaceSymbols: WorkspaceSymbolProvider def buildTargets: BuildTargets + def uriMapper: WorkspaceURIMapper + def folderUriMapper: FolderURIMapper + 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..00532ee7514 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,10 @@ case class Indexer(indexProviders: IndexProviders)(implicit rc: ReportContext) { buildTool.importedBuild.dependencySources, ) } + if (clientConfig.isLibraryFileSystemSupported()) { + folderUriMapper.rebuildIndexes() + 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..d65debe2d89 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], @@ -128,6 +129,7 @@ object InitializationOptions { None, None, None, + None, ) def from( @@ -171,6 +173,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/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 new file mode 100644 index 00000000000..0f2314fe068 --- /dev/null +++ b/metals/src/main/scala/scala/meta/internal/metals/LSPFileSystemProvider.scala @@ -0,0 +1,171 @@ +package scala.meta.internal.metals + +import java.nio.charset.StandardCharsets +import java.nio.file.Files +import java.util.stream.Collectors + +import scala.concurrent.ExecutionContext +import scala.concurrent.Future +import scala.util.Using + +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, +)(implicit ec: ExecutionContext) { + + /** Callers must gate on [[ClientConfiguration.isLibraryFileSystemSupported]]. */ + def sendLibraryFileSystemReady(): Unit = { + 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("/")) + Using.resource(Files.list(path)) { stream => + stream + .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(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 + * 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]) = + URIEncoderDecoder.decode(uri) 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) + 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 99fb844b43e..3ad0bd47ec3 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( @@ -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,6 +169,13 @@ abstract class MetalsLspService( val buildTargets: BuildTargets = BuildTargets.from(folder, mainBuildTargetsData, tables) + val folderUriMapper: FolderURIMapper = + new FolderURIMapper( + buildTargets, + () => userConfig.javaHome, + jarFileSystemCache, + ) + implicit val reports: StdReportContext = new StdReportContext( folder.toNIO, _.flatMap { uri => @@ -603,6 +612,13 @@ abstract class MetalsLspService( protected def fileDecoderProvider: FileDecoderProvider + override lazy val lspFileSystemProvider: LSPFileSystemProvider = + new LSPFileSystemProvider( + languageClient, + uriMapper, + fileDecoderProvider, + ) + 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..fa4278413bb 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()) @@ -129,6 +133,7 @@ class ProjectMetalsLspService( folder, compilers, buildTargets, + uriMapper, () => userConfig, shellRunner, optFileSystemSemanticdbs, @@ -260,6 +265,7 @@ class ProjectMetalsLspService( new McpTestRunner( debugProvider, buildTargets, + compilations, folder, () => userConfig, mcpSearch, @@ -686,6 +692,7 @@ class ProjectMetalsLspService( new FolderTreeViewProvider( new Folder(folder, folderVisibleName, true), buildTargets, + uriMapper, definitionIndex, () => userConfig, scalaVersionSelector, @@ -936,5 +943,4 @@ class ProjectMetalsLspService( super.resetService() treeView.reset() } - } 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..ace7ed66408 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", @@ -827,6 +851,7 @@ object ServerCommands { GotoPosition, GotoSuperMethod, GotoSymbol, + GotoLog, ImportBuild, InsertInferredType, InsertInferredMethod, @@ -842,6 +867,9 @@ object ServerCommands { RunScalafix, ScalafixRunOnly, DecodeFile, + FileSystemStat, + FileSystemReadDirectory, + FileSystemReadFile, DisconnectBuildServer, DisconnectBuildServerAndShutdown, ListBuildTargets, 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 new file mode 100644 index 00000000000..4cdb997e3d9 --- /dev/null +++ b/metals/src/main/scala/scala/meta/internal/metals/URIMapper.scala @@ -0,0 +1,350 @@ +package scala.meta.internal.metals + +import java.net.URI +import java.nio.file.Files +import java.nio.file.Path +import java.util.Properties +import java.util.concurrent.atomic.AtomicReference + +import scala.util.Using +import scala.util.control.NonFatal + +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.SymbolInformation +import org.eclipse.lsp4j.TextDocumentIdentifier + +/** + * Bidirectional mapper between virtual `metalsfs://` URIs exposed to + * the LSP client and local `jar:file://` URIs used internally by Metals. + * + * `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. + */ +trait URIMapper { + + def convertToLocal(uri: String): String + def convertToMetalsFS(uri: String): String + + def tryConvertToLocal(uri: String): Option[String] + def tryConvertToMetalsFS(uri: String): Option[String] + + 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") + ) + + final def getWorkspaceJarFileSystem(name: String): FileSystemInfo = + tryGetWorkspaceJarFileSystem(name).getOrElse( + throw new NoSuchElementException(s"Workspace jar not found: $name") + ) + + final def getSourceJarFileSystem(name: String): FileSystemInfo = + tryGetSourceJarFileSystem(name).getOrElse( + throw new NoSuchElementException(s"Source jar not found: $name") + ) + + final def convertToMetalsFS( + symbolInformation: SymbolInformation + ): SymbolInformation = { + val symbolInfo = new SymbolInformation( + symbolInformation.getName, + symbolInformation.getKind, + convertToMetalsFS(symbolInformation.getLocation), + symbolInformation.getContainerName, + ) + symbolInfo.setTags(symbolInformation.getTags) + symbolInfo + } + + 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) + + /** + * 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 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 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 unquote(s: String): String = + s.stripPrefix("\"").stripSuffix("\"") + + private def jdkSources: Option[AbsolutePath] = + JdkSources(userJavaHome()).toOption + + /** 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 + + val newKnownPaths = + newJdk.values.toSet ++ newWs.values.toSet ++ newSrc.values.toSet + jarFileSystemCache.closeObsolete(newKnownPaths) + + jdkIndex.set(newJdk) + workspaceJarIndex.set(newWs) + sourceJarIndex.set(newSrc) + } + + 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 NonFatal(e) => + scribe.warn( + s"Failed to parse $decodedJarPath while classifying local URI", + e, + ) + 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 + } + } + + /** + * `metalsfs:` URIs are returned as-is — `new URI("metalsfs", "", path, null)` + * would otherwise produce triple slashes. + */ + 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") + } + + 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) + tryGetJDKFileSystem(name).map(fs => (fs, remaining)) + case ws if ws.startsWith(URIMapper.workspaceJarURI) => + val (name, remaining) = + URIMapper.getURIParts(ws, URIMapper.workspaceJarURI) + tryGetWorkspaceJarFileSystem(name).map(fs => (fs, remaining)) + case src if src.startsWith(URIMapper.sourceJarURI) => + val (name, remaining) = + URIMapper.getURIParts(src, URIMapper.sourceJarURI) + tryGetSourceJarFileSystem(name).map(fs => (fs, remaining)) + case _ => None + } + resolved.map { case (fs, fsPath) => + fsPath.fold(fs.fileUri)(p => fs.fs.getPath(p).toUri.toString) + } + } + } + + 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) + } +} + +/** + * 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 { + + private def tryAll[A](f: URIMapper => Option[A]): Option[A] = + folders().iterator.flatMap(m => f(m).iterator).nextOption() + + def convertToLocal(uri: String): String = + tryConvertToLocal(uri).getOrElse(uri) + def convertToMetalsFS(uri: String): String = + tryConvertToMetalsFS(uri).getOrElse(uri) + + def tryConvertToLocal(uri: String): Option[String] = + tryAll(_.tryConvertToLocal(uri)) + def tryConvertToMetalsFS(uri: String): Option[String] = + tryAll(_.tryConvertToMetalsFS(uri)) + + 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)) +} + +/** Single-slash form because VS Code normalises `metalsfs:///` to `metalsfs:/`. */ +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..ae421661ac4 100644 --- a/metals/src/main/scala/scala/meta/internal/metals/WorkspaceLspService.scala +++ b/metals/src/main/scala/scala/meta/internal/metals/WorkspaceLspService.scala @@ -128,6 +128,12 @@ class WorkspaceLspService( initializeParams, ) + private val jarFileSystemCache: JarFileSystemCache = new JarFileSystemCache + + private val uriMapper: WorkspaceURIMapper = new WorkspaceURIMapper(() => + folderServices.map(_.folderUriMapper) :+ fallbackService.folderUriMapper + ) + private val languageClient = { val languageClient = new ConfiguredLanguageClient(client, clientConfig, Some(this)) @@ -239,6 +245,8 @@ class WorkspaceLspService( workDoneProgress, bspStatus, moduleStatus, + jarFileSystemCache, + uriMapper, ) } @@ -263,6 +271,8 @@ class WorkspaceLspService( workDoneProgress, maxScalaCliServers = 3, moduleStatus, + jarFileSystemCache, + uriMapper, ) } @@ -353,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 = uri.stripPrefix("metalsDecode:") + val strippedUri = uriMapper.convertToLocal(uri.stripPrefix("metalsDecode:")) for { path <- strippedUri.toAbsolutePathSafe() service <- @@ -366,6 +376,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 +640,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 +654,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 +669,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 +736,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 +918,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 +939,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 +1072,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 +1084,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 +1115,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 +1207,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/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#", ) } 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())) 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" 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), ) 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 a2efa239e82..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" @@ -3468,6 +3460,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 +3570,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 +4229,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,19 +4329,12 @@ 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" - 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" @@ -4310,6 +4430,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 +4453,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 +4643,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 +4851,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 +4966,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 +4979,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 +5068,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 +5423,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 +6279,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 +6305,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 +8499,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 +8527,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 +8611,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 +8871,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 +9526,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" @@ -9417,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" @@ -9499,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" @@ -9533,6 +9708,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 +10016,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 +10113,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 +10734,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" @@ -10584,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" @@ -10865,13 +11057,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 +11073,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 +11083,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"