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 ce5d461643f..1c713e79a0f 100644 --- a/metals/src/main/scala/scala/meta/internal/metals/MetalsEnrichments.scala +++ b/metals/src/main/scala/scala/meta/internal/metals/MetalsEnrichments.scala @@ -303,14 +303,28 @@ object MetalsEnrichments implicit class XtensionAbsolutePathBuffers(path: AbsolutePath) { - def hasScalaFiles: Boolean = { - def isScalaDir(file: File): Boolean = { + def isScalaProject(): Boolean = + containsProjectFilesSatisfying(_.isScala) + def isMetalsProject(): Boolean = + containsProjectFilesSatisfying(_.isScalaOrJavaFilename) + + private def containsProjectFilesSatisfying( + fileNamePredicate: String => Boolean + ): Boolean = { + val directoriesToCheck = Set("test", "src", "it") + def dirFilter(f: File) = directoriesToCheck(f.getName()) || f + .listFiles() + .exists(dir => dir.isDirectory && directoriesToCheck(dir.getName())) + def isScalaDir( + file: File, + dirFilter: File => Boolean = _ => true, + ): Boolean = { file.listFiles().exists { file => - if (file.isDirectory()) isScalaDir(file) - else file.getName().endsWith(".scala") + if (file.isDirectory()) dirFilter(file) && isScalaDir(file) + else fileNamePredicate(file.getName()) } } - path.isDirectory && isScalaDir(path.toFile) + path.isDirectory && isScalaDir(path.toFile, dirFilter) } def scalaSourcerootOption: String = s""""-P:semanticdb:sourceroot:$path"""" diff --git a/metals/src/main/scala/scala/meta/internal/metals/MetalsLspService.scala b/metals/src/main/scala/scala/meta/internal/metals/MetalsLspService.scala index dc28139e724..2b74ecb511f 100644 --- a/metals/src/main/scala/scala/meta/internal/metals/MetalsLspService.scala +++ b/metals/src/main/scala/scala/meta/internal/metals/MetalsLspService.scala @@ -2,7 +2,6 @@ package scala.meta.internal.metals import java.net.URI import java.nio.file._ -import java.sql.Connection import java.util import java.util.concurrent.CompletableFuture import java.util.concurrent.ScheduledExecutorService @@ -128,10 +127,11 @@ class MetalsLspService( shellRunner: ShellRunner, timerProvider: TimerProvider, initTreeView: () => Unit, - val folder: AbsolutePath, + folder: AbsolutePath, folderVisibleName: Option[String], headDoctor: HeadDoctor, -) extends Cancelable +) extends Folder(folder, folderVisibleName, isKnownMetalsProject = true) + with Cancelable with TextDocumentService { import serverInputs._ @@ -781,7 +781,7 @@ class MetalsLspService( val treeView = new FolderTreeViewProvider( - Folder(folder, folderVisibleName), + new Folder(folder, folderVisibleName, true), buildTargets, () => buildClient.ongoingCompilations(), definitionIndex, @@ -847,7 +847,7 @@ class MetalsLspService( cancelable } - def loadFingerPrints(): Unit = { + private def loadFingerPrints(): Unit = { // load fingerprints from last execution fingerprints.addAll(tables.fingerprints.load()) } @@ -859,7 +859,7 @@ class MetalsLspService( token: CancelToken, ): Future[Unit] = codeActionProvider.executeCommands(params, token) - def registerNiceToHaveFilePatterns(): Unit = { + private def registerNiceToHaveFilePatterns(): Unit = { for { params <- Option(initializeParams) capabilities <- Option(params.getCapabilities) @@ -885,9 +885,11 @@ class MetalsLspService( val isInitialized = new AtomicBoolean(false) - def connectTables(): Connection = tables.connect() + def initialized(): Future[Unit] = { + loadFingerPrints() + registerNiceToHaveFilePatterns() + tables.connect() - def initialized(): Future[Unit] = for { _ <- maybeSetupScalaCli() _ <- @@ -902,6 +904,7 @@ class MetalsLspService( ) ) } yield () + } def onShutdown(): Unit = { tables.fingerprints.save(fingerprints.getAllFingerprints()) @@ -2035,7 +2038,7 @@ class MetalsLspService( if ( !buildTools.isAutoConnectable() && buildTools.loadSupported.isEmpty - && folder.hasScalaFiles + && folder.isScalaProject() ) scalaCli.setupIDE(folder) else Future.successful(()) } diff --git a/metals/src/main/scala/scala/meta/internal/metals/WorkspaceFolders.scala b/metals/src/main/scala/scala/meta/internal/metals/WorkspaceFolders.scala index d1f07dc2c87..c0104e36a17 100644 --- a/metals/src/main/scala/scala/meta/internal/metals/WorkspaceFolders.scala +++ b/metals/src/main/scala/scala/meta/internal/metals/WorkspaceFolders.scala @@ -10,16 +10,23 @@ import scala.meta.internal.metals.logging.MetalsLogger class WorkspaceFolders( initialFolders: List[Folder], createService: Folder => MetalsLspService, - onInitialize: MetalsLspService => Future[Unit], - shoutdownMetals: () => Future[Unit], + shutdownMetals: () => Future[Unit], redirectSystemOut: Boolean, initialServerConfig: MetalsServerConfig, )(implicit ec: ExecutionContext) { - private val folderServices: AtomicReference[List[MetalsLspService]] = - new AtomicReference(initialFolders.map(createService)) + private val folderServices: AtomicReference[WorkspaceFoldersServices] = { + val (scalaProjects, nonScalaProjects) = + initialFolders.partition(_.isMetalsProject) match { + case (Nil, nonScala) => (List(nonScala.head), nonScala.tail) + case t => t + } + val services = scalaProjects.map(createService(_)) + new AtomicReference(WorkspaceFoldersServices(services, nonScalaProjects)) + } - def getFolderServices: List[MetalsLspService] = folderServices.get() + def getFolderServices: List[MetalsLspService] = folderServices.get().services + def nonScalaProjects: List[Folder] = folderServices.get().nonScalaFolders def changeFolderServices( toRemove: List[Folder], @@ -27,39 +34,74 @@ class WorkspaceFolders( ): Future[Unit] = { val actualToRemove = toRemove.filterNot(folder => toAdd.exists(_.path == folder.path)) - def shouldBeRemoved(service: MetalsLspService) = - actualToRemove.exists(_.path == service.folder) - def isIn(services: List[MetalsLspService], service: MetalsLspService) = - services.exists(_.folder == service.folder) - - val newServices = toAdd.map { folder => - val newService = createService(folder) - newService.loadFingerPrints() - newService.registerNiceToHaveFilePatterns() - newService.connectTables() - newService - } + def shouldBeRemoved(folder: Folder) = + actualToRemove.exists(_.path == folder.path) + + val (newScala, newNonScala) = toAdd.partition(_.isMetalsProject) + val newServices = newScala.map(createService(_)) if (newServices.isEmpty && getFolderServices.forall(shouldBeRemoved)) { - shoutdownMetals() + shutdownMetals() } else { - val prev = - folderServices.getAndUpdate { current => - val afterRemove = current.filterNot(shouldBeRemoved) - val newToAdd = newServices.filterNot(isIn(current, _)) - afterRemove ++ newToAdd + val WorkspaceFoldersServices(prev, _) = + folderServices.getAndUpdate { + case WorkspaceFoldersServices(services, nonScalaProjects) => + val updatedServices = + services.filterNot(shouldBeRemoved) ++ + newServices.filterNot(isIn(services, _)) + val updatedNonScala = + nonScalaProjects.filterNot(shouldBeRemoved) ++ + newNonScala.filterNot(isIn(nonScalaProjects ++ services, _)) + WorkspaceFoldersServices( + updatedServices, + updatedNonScala, + ) } - MetalsLogger.setupLspLogger( - folderServices.get().map(_.folder), - redirectSystemOut, - initialServerConfig, - ) + + setupLogger() + for { _ <- Future.sequence( - newServices.filterNot(isIn(prev, _)).map(onInitialize) + newServices.filterNot(isIn(prev, _)).map(_.initialized()) ) _ <- Future(prev.filter(shouldBeRemoved).foreach(_.onShutdown())) } yield () } } + def convertToScalaProject(folder: Folder): MetalsLspService = { + val newService = createService(folder) + val WorkspaceFoldersServices(prev, _) = folderServices.getAndUpdate { + case wfs @ WorkspaceFoldersServices(services, nonScalaProjects) => + if (!isIn(services, folder)) { + WorkspaceFoldersServices( + services :+ newService, + nonScalaProjects.filterNot(_ == folder), + ) + } else wfs + } + + prev.find(_.path == folder.path) match { + case Some(service) => service + case None => + setupLogger() + newService.initialized() + newService + } + } + + private def setupLogger() = + MetalsLogger.setupLspLogger( + getFolderServices.map(_.path), + redirectSystemOut, + initialServerConfig, + ) + + private def isIn(services: List[Folder], service: Folder) = + services.exists(_.path == service.path) + } + +case class WorkspaceFoldersServices( + services: List[MetalsLspService], + nonScalaFolders: List[Folder], +) 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 dc6ad6c7d76..46224dad05a 100644 --- a/metals/src/main/scala/scala/meta/internal/metals/WorkspaceLspService.scala +++ b/metals/src/main/scala/scala/meta/internal/metals/WorkspaceLspService.scala @@ -179,13 +179,14 @@ class WorkspaceLspService( new WorkspaceFolders( folders, createService, - _.initialized(), () => shutdown().asScala, redirectSystemOut, serverInputs.initialServerConfig, ) def folderServices = workspaceFolders.getFolderServices + def nonScalaProjects = workspaceFolders.nonScalaProjects + def fallbackService: MetalsLspService = folderServices.head val treeView: TreeViewProvider = if (clientConfig.isTreeViewProvider) { @@ -223,30 +224,32 @@ class WorkspaceLspService( cancelable } - def getServiceForOpt(path: AbsolutePath): Option[MetalsLspService] = + def getFolderForOpt[T <: Folder]( + path: AbsolutePath, + folders: List[T], + ): Option[T] = try { for { - workSpaceFolder <- folderServices - .filter(service => path.toNIO.startsWith(service.folder.toNIO)) - .sortBy(_.folder.toNIO.getNameCount()) + workSpaceFolder <- folders + .filter(service => path.toNIO.startsWith(service.path.toNIO)) + .sortBy(_.path.toNIO.getNameCount()) .lastOption } yield workSpaceFolder } catch { case _: ProviderMismatchException => None } + def getServiceForOpt(path: AbsolutePath): Option[MetalsLspService] = + getFolderForOpt(path, folderServices) + def getServiceFor(path: AbsolutePath): MetalsLspService = - if (folderServices.length == 1) folderServices.head - else getServiceForOpt(path).getOrElse(folderServices.head) + getServiceForOpt(path).getOrElse(fallbackService) - def getServiceFor(uri: String): MetalsLspService = { - val folder = - for { - // "metalsDecode" prefix is used for showing special files and is not an actual file system - path <- uri.stripPrefix("metalsDecode:").toAbsolutePathSafe() - } yield getServiceFor(path) - folder.getOrElse(folderServices.head) // fallback to the first one - } + def getServiceFor(uri: String): MetalsLspService = + (for { + // "metalsDecode" prefix is used for showing special files and is not an actual file system + path <- uri.stripPrefix("metalsDecode:").toAbsolutePathSafe() + } yield getServiceFor(path)).getOrElse(fallbackService) def currentFolder: Option[MetalsLspService] = focusedDocument.flatMap(getServiceForOpt) @@ -295,7 +298,7 @@ class WorkspaceLspService( ): Option[MetalsLspService] = for { workSpaceFolder <- folderServices - .find(service => service.folder.toString == folderUri) + .find(service => service.path.toString == folderUri) } yield workSpaceFolder def foreachSeq[A]( @@ -316,8 +319,18 @@ class WorkspaceLspService( params: DidOpenTextDocumentParams ): CompletableFuture[Unit] = { focusedDocument.foreach(recentlyFocusedFiles.add) - focusedDocument = Some(params.getTextDocument.getUri.toAbsolutePath) - getServiceFor(params.getTextDocument().getUri()).didOpen(params) + val uri = params.getTextDocument.getUri + val path = uri.toAbsolutePath + focusedDocument = Some(path) + val service = getServiceForOpt(path) + .orElse { + if (path.filename.isScalaOrJavaFilename) { + getFolderForOpt(path, nonScalaProjects) + .map(workspaceFolders.convertToScalaProject) + } else None + } + .getOrElse(fallbackService) + service.didOpen(params) } override def didChange( @@ -469,7 +482,7 @@ class WorkspaceLspService( /* Changing package for files moved out of the workspace or between folders will cause unexpected issues * This showed up with emacs automated backups. */ - if (newPath.startWith(service.folder)) + if (newPath.startWith(service.path)) service.willRenameFile(oldPath, newPath) else Future.successful( @@ -526,8 +539,18 @@ class WorkspaceLspService( ): CompletableFuture[Unit] = workspaceFolders .changeFolderServices( - params.getEvent().getRemoved().map(Folder.apply).asScala.toList, - params.getEvent().getAdded().map(Folder.apply).asScala.toList, + params + .getEvent() + .getRemoved() + .map(Folder(_, isKnownMetalsProject = false)) + .asScala + .toList, + params + .getEvent() + .getAdded() + .map(Folder(_, isKnownMetalsProject = false)) + .asScala + .toList, ) .asJava @@ -792,7 +815,7 @@ class WorkspaceLspService( onCurrentFolder( service => Future { - val log = service.folder.resolve(Directories.log) + val log = service.path.resolve(Directories.log) val linesCount = log.readText.linesIterator.size val pos = new lsp4j.Position(linesCount, 0) val location = new Location( @@ -878,7 +901,7 @@ class WorkspaceLspService( Future { // Getting the service for focused document and first one otherwise val service = - focusedDocument.map(getServiceFor).getOrElse(folderServices.head) + focusedDocument.map(getServiceFor).getOrElse(fallbackService) val command = service.analyzeStackTrace(content) command.foreach(languageClient.metalsExecuteClientCommand) scribe.debug(s"Executing AnalyzeStacktrace ${command}") @@ -924,7 +947,7 @@ class WorkspaceLspService( val fileType = args.lift(2).flatten directoryURI .map(getServiceFor) - .getOrElse(folderServices.head) + .getOrElse(fallbackService) .createFile(directoryURI, name, fileType, isScala = true) case ServerCommands.NewJavaFile(args) => val directoryURI = args.lift(0).flatten @@ -932,7 +955,7 @@ class WorkspaceLspService( val fileType = args.lift(2).flatten directoryURI .map(getServiceFor) - .getOrElse(folderServices.head) + .getOrElse(fallbackService) .createFile(directoryURI, name, fileType, isScala = false) case ServerCommands.StartAmmoniteBuildServer() => val res = focusedDocument match { @@ -996,8 +1019,6 @@ class WorkspaceLspService( def initialize(): CompletableFuture[lsp4j.InitializeResult] = { timerProvider .timed("initialize")(Future { - // load fingerprints from last execution - folderServices.foreach(_.loadFingerPrints()) val capabilities = new lsp4j.ServerCapabilities() capabilities.setExecuteCommandProvider( new lsp4j.ExecuteCommandOptions( @@ -1107,10 +1128,6 @@ class WorkspaceLspService( def initialized(): Future[Unit] = { statusBar.start(sh, 0, 1, ju.concurrent.TimeUnit.SECONDS) - folderServices.foreach { service => - service.registerNiceToHaveFilePatterns() - service.connectTables() - } syncUserconfiguration().flatMap { _ => Future .sequence(folderServices.map(_.initialized())) @@ -1258,16 +1275,29 @@ class WorkspaceLspService( } -case class Folder(path: AbsolutePath, visibleName: Option[String]) { +class Folder( + val path: AbsolutePath, + val visibleName: Option[String], + isKnownMetalsProject: Boolean, +) { + lazy val isMetalsProject: Boolean = + isKnownMetalsProject || path.resolve(".metals").exists || path + .isMetalsProject() def nameOrUri: String = visibleName.getOrElse(path.toString()) } object Folder { - def apply(folder: lsp4j.WorkspaceFolder): Folder = { + def unapply(f: Folder): Option[(AbsolutePath, Option[String])] = Some( + (f.path, f.visibleName) + ) + def apply( + folder: lsp4j.WorkspaceFolder, + isKnownMetalsProject: Boolean, + ): Folder = { val name = Option(folder.getName()) match { case Some("") => None case maybeValue => maybeValue } - Folder(folder.getUri().toAbsolutePath, name) + new Folder(folder.getUri().toAbsolutePath, name, isKnownMetalsProject) } } diff --git a/metals/src/main/scala/scala/meta/metals/MetalsLanguageServer.scala b/metals/src/main/scala/scala/meta/metals/MetalsLanguageServer.scala index d8e26277dc2..a74ce31ce5d 100644 --- a/metals/src/main/scala/scala/meta/metals/MetalsLanguageServer.scala +++ b/metals/src/main/scala/scala/meta/metals/MetalsLanguageServer.scala @@ -126,19 +126,27 @@ class MetalsLanguageServer( ) .asJava } else { - val folders = { - val workspaceFolders = + val folders: List[Folder] = { + val allFolders = Option(params.getWorkspaceFolders()) .map(_.asScala) .toList .flatten - .map(Folder.apply) - if (workspaceFolders.nonEmpty) workspaceFolders - else - Option(params.getRootUri()) - .orElse(Option(params.getRootPath())) - .map(root => Folder(root.toAbsolutePath, Some("root"))) - .toList + allFolders match { + case Nil => + Option(params.getRootUri()) + .orElse(Option(params.getRootPath())) + .map(root => + new Folder( + root.toAbsolutePath, + Some("root"), + isKnownMetalsProject = true, + ) + ) + .toList + case head :: Nil => List(Folder(head, isKnownMetalsProject = true)) + case many => many.map(Folder(_, isKnownMetalsProject = false)) + } } folders match { @@ -163,8 +171,21 @@ class MetalsLanguageServer( val folderPaths = folders.map(_.path) setupJna() + + val folderPathsWithScala = + folders.collect { + case folder if folder.isMetalsProject => folder.path + } match { + case Nil => + scribe.warn( + s"No scala project detected. The logs will be in the first workspace folder: ${folderPaths.head}" + ) + List(folderPaths.head) + case paths => paths + } + MetalsLogger.setupLspLogger( - folderPaths, + folderPathsWithScala, redirectSystemOut, serverInputs.initialServerConfig, ) @@ -180,7 +201,7 @@ class MetalsLanguageServer( serverState.set(ServerState.Initialized(service)) metalsService.underlying = service - folderPaths.foreach(folder => + folderPathsWithScala.foreach(folder => new StdReportContext(folder.toNIO).cleanUpOldReports() ) diff --git a/mtags-shared/src/main/scala/scala/meta/internal/metals/ReportContext.scala b/mtags-shared/src/main/scala/scala/meta/internal/metals/ReportContext.scala index c64d8c3ef5d..fb1a3c0cef3 100644 --- a/mtags-shared/src/main/scala/scala/meta/internal/metals/ReportContext.scala +++ b/mtags-shared/src/main/scala/scala/meta/internal/metals/ReportContext.scala @@ -31,8 +31,7 @@ trait Reporter { class StdReportContext(workspace: Path, level: ReportLevel = ReportLevel.Info) extends ReportContext { - lazy val reportsDir: Path = - workspace.resolve(StdReportContext.reportsDir).createDirectories() + val reportsDir: Path = workspace.resolve(StdReportContext.reportsDir) val unsanitized = new StdReporter( @@ -61,17 +60,18 @@ class StdReportContext(workspace: Path, level: ReportLevel = ReportLevel.Info) override def deleteAll(): Unit = { all.foreach(_.deleteAll()) - Files.delete(reportsDir.resolve(StdReportContext.ZIP_FILE_NAME)) + val zipFile = reportsDir.resolve(StdReportContext.ZIP_FILE_NAME) + if (Files.exists(zipFile)) Files.delete(zipFile) } } class StdReporter(workspace: Path, pathToReports: Path, level: ReportLevel) extends Reporter { - private lazy val reportsDir = - workspace.resolve(pathToReports).createDirectories() + private lazy val maybeReportsDir: Path = workspace.resolve(pathToReports) + private lazy val reportsDir = maybeReportsDir.createDirectories() private val limitedFilesManager = new LimitedFilesManager( - reportsDir, + maybeReportsDir, StdReportContext.MAX_NUMBER_OF_REPORTS, "r_.*_" ) diff --git a/mtags-shared/src/main/scala/scala/meta/internal/metals/utils/LimitedFilesManager.scala b/mtags-shared/src/main/scala/scala/meta/internal/metals/utils/LimitedFilesManager.scala index d37083f5987..365b4f94db9 100644 --- a/mtags-shared/src/main/scala/scala/meta/internal/metals/utils/LimitedFilesManager.scala +++ b/mtags-shared/src/main/scala/scala/meta/internal/metals/utils/LimitedFilesManager.scala @@ -22,11 +22,14 @@ class LimitedFilesManager( } else List() } - def directoriesWithDate: List[File] = directory - .toFile() - .listFiles() - .toList - .filter(d => d.isDirectory() && TimeFormatter.hasDateName(d.getName())) + def directoriesWithDate: List[File] = + if (Files.exists(directory) && Files.isDirectory(directory)) + directory + .toFile() + .listFiles() + .toList + .filter(d => d.isDirectory() && TimeFormatter.hasDateName(d.getName())) + else List() def deleteOld(limit: Int = fileLimit): List[TimestampedFile] = { val files = getAllFiles() diff --git a/tests/slow/src/main/scala/tests/scalacli/BaseScalaCliSuite.scala b/tests/slow/src/main/scala/tests/scalacli/BaseScalaCliSuite.scala index e81a0988bac..37f19ae4ff4 100644 --- a/tests/slow/src/main/scala/tests/scalacli/BaseScalaCliSuite.scala +++ b/tests/slow/src/main/scala/tests/scalacli/BaseScalaCliSuite.scala @@ -66,7 +66,7 @@ abstract class BaseScalaCliSuite(protected val scalaVersion: String) server.client.showMessageRequestHandler server.client.showMessageRequestHandler = { params => def useBsp = Files.exists( - server.server.folder + server.server.path .resolve(".bsp/scala-cli.json") .toNIO ) diff --git a/tests/unit/src/main/scala/tests/TestingServer.scala b/tests/unit/src/main/scala/tests/TestingServer.scala index a4ae0d0ade5..71549fb440c 100644 --- a/tests/unit/src/main/scala/tests/TestingServer.scala +++ b/tests/unit/src/main/scala/tests/TestingServer.scala @@ -594,7 +594,7 @@ final case class TestingServer( fullServer.folderServices.foreach { service => require( service.bspSession.isDefined, - s"Build server ${service.folder} did not initialize", + s"Build server ${service.path} did not initialize", ) } } diff --git a/tests/unit/src/test/scala/tests/TreeViewLspSuite.scala b/tests/unit/src/test/scala/tests/TreeViewLspSuite.scala index 2552a7b7645..806d0c94b50 100644 --- a/tests/unit/src/test/scala/tests/TreeViewLspSuite.scala +++ b/tests/unit/src/test/scala/tests/TreeViewLspSuite.scala @@ -95,7 +95,7 @@ class TreeViewLspSuite extends BaseLspSuite("tree-view") { |${TreeViewProvider.Compile} |""".stripMargin, ) - folder = server.server.folder + folder = server.server.path _ = server.assertTreeViewChildren( s"projects-$folder:${server.buildTarget("a")}", "", @@ -158,7 +158,7 @@ class TreeViewLspSuite extends BaseLspSuite("tree-view") { |} |""".stripMargin ) - folder = server.server.folder + folder = server.server.path _ = { server.assertTreeViewChildren( s"libraries-$folder:${server.jar("sourcecode")}", @@ -500,7 +500,7 @@ class TreeViewLspSuite extends BaseLspSuite("tree-view") { |} |""".stripMargin ) - folder = server.server.folder + folder = server.server.path // Trigger a compilation of Second.scala _ <- server.didOpen("b/src/main/scala/b/Second.scala") _ = server.assertTreeViewChildren( diff --git a/tests/unit/src/test/scala/tests/WorkspaceFoldersSuite.scala b/tests/unit/src/test/scala/tests/WorkspaceFoldersSuite.scala index e0fa1a5aa09..de29a5fe7c4 100644 --- a/tests/unit/src/test/scala/tests/WorkspaceFoldersSuite.scala +++ b/tests/unit/src/test/scala/tests/WorkspaceFoldersSuite.scala @@ -56,4 +56,48 @@ class WorkspaceFoldersSuite ) } yield () } + + test("non-scala-project") { + cleanWorkspace() + val newScalaFile = "/a/src/main/scala/A.scala" + for { + _ <- initialize( + Map( + "testFolder" -> + s"""|/metals.json + |{"a":{"scalaVersion" : ${V.scala213}}} + |/a/src/main/scala/a/A.scala + |package a + |case class MyObjectA() + |""".stripMargin, + "notYetScalaProject" -> + s"""|/README.md + |Will be a Scala project. + |""".stripMargin, + ), + expectError = false, + ) + _ = assert(server.fullServer.folderServices.length == 1) + _ = assert(server.fullServer.nonScalaProjects.length == 1) + _ = writeLayout( + s"""|$newScalaFile + |package a + |object O { + | val i: Int = "aaa" + |} + |""".stripMargin, + "notYetScalaProject", + ) + _ <- server.didOpen(s"notYetScalaProject$newScalaFile") + _ = assert(server.fullServer.folderServices.length == 2) + _ = assertNoDiff( + server.client.pathDiagnostics(s"notYetScalaProject$newScalaFile"), + s"""|notYetScalaProject$newScalaFile:3:15: error: Found: ("aaa" : String) + |Required: Int + | val i: Int = "aaa" + | ^^^^^ + |""".stripMargin, + ) + } yield () + } } diff --git a/tests/unit/src/test/scala/tests/testProvider/TestSuitesProviderSuite.scala b/tests/unit/src/test/scala/tests/testProvider/TestSuitesProviderSuite.scala index 47007e2cc5b..df1eb099e75 100644 --- a/tests/unit/src/test/scala/tests/testProvider/TestSuitesProviderSuite.scala +++ b/tests/unit/src/test/scala/tests/testProvider/TestSuitesProviderSuite.scala @@ -1064,7 +1064,7 @@ class TestSuitesProviderSuite extends BaseLspSuite("testSuitesFinderSuite") { targetName, targetUri, "root", - server.server.folder.toNIO.toString, + server.server.path.toNIO.toString, events, ) }