Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ case class MavenBuildTool(
override def mavenBaseCommand(): List[String] =
mbtMavenBaseCommand(projectRoot)

override def isBuildRelated(path: AbsolutePath): Boolean =
override def isWatchedFile(path: AbsolutePath): Boolean =
MavenBuildTool.isMavenRelatedPath(projectRoot, path)

override def digest(workspace: AbsolutePath): Option[String] =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,8 +45,15 @@ import scala.meta.internal.metals.scalacli.ScalaCliServers
import scala.meta.io.AbsolutePath

import org.eclipse.lsp4j
import org.eclipse.lsp4j.DidChangeWatchedFilesRegistrationOptions
import org.eclipse.lsp4j.FileSystemWatcher
import org.eclipse.lsp4j.MessageParams
import org.eclipse.lsp4j.MessageType
import org.eclipse.lsp4j.Registration
import org.eclipse.lsp4j.RegistrationParams
import org.eclipse.lsp4j.Unregistration
import org.eclipse.lsp4j.UnregistrationParams
import org.eclipse.lsp4j.jsonrpc.messages.{Either => JEither}

class ConnectionProvider(
buildToolProvider: BuildToolProvider,
Expand Down Expand Up @@ -141,6 +148,9 @@ class ConnectionProvider(
)

private val isMbtImportInProcess: AtomicBoolean = new AtomicBoolean(false)
private val mbtWatchedFilesRegistered: AtomicBoolean = new AtomicBoolean(
false
)
private val buildServerPromptShown: AtomicBoolean = new AtomicBoolean(false)

val cancelables = new MutableCancelable
Expand Down Expand Up @@ -223,6 +233,7 @@ class ConnectionProvider(
.runUnconditionally(mbtImporters, isMbtImportInProcess)
.flatMap { importStatus =>
if (importStatus.isInstalled) {
updateMbtWatchedFiles(mbtImporters)
tables.buildServers.chooseServer(MbtBuildServer.name)
connect(CreateSession(), progress).ignoreValue
} else Future.unit
Expand All @@ -242,11 +253,96 @@ class ConnectionProvider(
userConfig.preferredBuildServer.contains(MbtBuildServer.name) ||
tables.buildServers.selectedServer().contains(MbtBuildServer.name)

private def unregisterMbtWatchedFiles(): Unit =
if (mbtWatchedFilesRegistered.getAndSet(false)) {
languageClient.unregisterCapability(
new UnregistrationParams(
List(
new Unregistration(
"mbt-watched-files",
"workspace/didChangeWatchedFiles",
)
).asJava
)
)
}

private def registerMbtWatchedFiles(
watchers: List[FileSystemWatcher]
): Unit = {
if (watchers.nonEmpty) {
languageClient.registerCapability(
new RegistrationParams(
List(
new Registration(
"mbt-watched-files",
"workspace/didChangeWatchedFiles",
new DidChangeWatchedFilesRegistrationOptions(watchers.asJava),
)
).asJava
)
)
mbtWatchedFilesRegistered.set(true)
}
}

private def refreshMbtWatchedFiles(build: MbtBuild): Unit = {
unregisterMbtWatchedFiles()
val explicitPaths = build.getWatchedFiles.asScala.toList
.filterNot(mbt.MbtGlobMatcher.isPatternGlob)
if (explicitPaths.nonEmpty) {
val root = folder.toString().replace('\\', '/')
val watchers = explicitPaths.map { p =>
val normalized = mbt.MbtGlobMatcher.normalizeSlashes(p)
val withoutLeading =
if (normalized.startsWith("./")) normalized.substring(2)
else normalized
new FileSystemWatcher(JEither.forLeft(s"$root/$withoutLeading"))
}
registerMbtWatchedFiles(watchers)
}
}

private def updateMbtWatchedFiles(
importers: List[MbtImportProvider]
): Unit = {
val build = mbtBuild()
importers.foreach {
case script: mbt.importer.ScriptMbtImporter =>
mbt.importer.ScriptMbtImporter
.updateWatchedFiles(script.scriptPath, build)
case _ =>
}
refreshMbtWatchedFiles(build)
}

private def withWatchedFilesUpdate(
run: Future[WorkspaceLoadedStatus],
importers: List[MbtImportProvider],
): Future[Unit] =
run.map { status =>
if (status.isInstalled) updateMbtWatchedFiles(importers)
}.ignoreValue

def runMbtReimport(importers: List[MbtImportProvider]): Future[Unit] =
mbtImport.runIfApproved(importers, isMbtImportInProcess).ignoreValue
withWatchedFilesUpdate(
mbtImport.runIfApproved(importers, isMbtImportInProcess),
importers,
)

def runMbtReimportIgnoringDigest(
importers: List[MbtImportProvider]
): Future[Unit] =
withWatchedFilesUpdate(
mbtImport.runIgnoringDigest(importers, isMbtImportInProcess),
importers,
)

def forceMbtReimport(importers: List[MbtImportProvider]): Future[Unit] =
mbtImport.runUnconditionally(importers, isMbtImportInProcess).ignoreValue
withWatchedFilesUpdate(
mbtImport.runUnconditionally(importers, isMbtImportInProcess),
importers,
)

def reloadCurrentSession(): Future[Unit] =
bspSession match {
Expand Down Expand Up @@ -1049,17 +1145,22 @@ class ConnectionProvider(
for {
importStatus <-
if (isMbtPreferred) {
mbtImport
.runUnconditionally(
buildTools
.mbtImporters(
shellRunner,
() => userConfig,
Some(languageClient),
Some(tables),
),
isMbtImportInProcess,
val importers = buildTools
.mbtImporters(
shellRunner,
() => userConfig,
Some(languageClient),
Some(tables),
)
mbtImport
.runUnconditionally(importers, isMbtImportInProcess)
.map { status =>
if (status.isInstalled)
updateMbtWatchedFiles(
importers
)
status
}
} else Future.successful(WorkspaceLoadedStatus.Installed)
change <-
if (importStatus.isInstalled) connect(request, progress)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -463,8 +463,8 @@ class ProjectMetalsLspService(
Some(languageClient),
Some(tables),
)
if (paths.exists(path => mbtImporters.exists(_.isBuildRelated(path))))
connectionProvider.runMbtReimport(mbtImporters)
if (paths.exists(path => mbtImporters.exists(_.isWatchedFile(path))))
connectionProvider.runMbtReimportIgnoringDigest(mbtImporters)
else
Future.unit
} else if (userConfig.buildChangedAction.isNone) {
Expand Down
37 changes: 23 additions & 14 deletions metals/src/main/scala/scala/meta/internal/metals/mbt/MbtBuild.scala
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ case class MbtBuild(
@Nullable dependencyModules: ju.List[MbtDependencyModule],
@Nullable namespaces: ju.Map[String, MbtNamespace],
@Nullable uncheckedSources: ju.List[String],
@Nullable watchedFiles: ju.List[String] = null,
) {

def getDependencyModules(): ju.List[MbtDependencyModule] =
Expand All @@ -34,10 +35,14 @@ case class MbtBuild(
(topLevel ++ fromNamespaces).distinct.asJava
}

def getWatchedFiles: ju.List[String] =
Option(this.watchedFiles).getOrElse(ju.Collections.emptyList())

def isEmpty: Boolean =
Option(this.dependencyModules).forall(_.isEmpty) &&
Option(this.namespaces).forall(_.isEmpty) &&
Option(this.uncheckedSources).forall(_.isEmpty)
Option(this.uncheckedSources).forall(_.isEmpty) &&
Option(this.watchedFiles).forall(_.isEmpty)

def asBspModules: bsp4j.DependencyModulesResult =
new bsp4j.DependencyModulesResult(
Expand Down Expand Up @@ -86,7 +91,8 @@ case class MbtBuild(
None
}
}
val globPatterns = namespace.getSources.asScala.toSeq.filter(isGlob)
val globPatterns = namespace.getSources.asScala.toSeq
.filter(MbtGlobMatcher.isPatternGlob)
val nsModules = for {
moduleId <- namespace.getDependencyModuleIds.asScala.toSeq
module <- modulesById.get(moduleId).orElse {
Expand All @@ -101,7 +107,7 @@ case class MbtBuild(
id =
new bsp4j.BuildTargetIdentifier(MbtBuild.namespaceTargetId(name)),
sources = namespace.getSources.asScala.toSeq
.filterNot(isGlob),
.filterNot(MbtGlobMatcher.isPatternGlob),
globMatchers = globPatterns.map(pattern =>
MbtGlobMatcher(
pattern = pattern,
Expand All @@ -127,17 +133,9 @@ case class MbtBuild(
}
}

private def isGlob(pattern: String): Boolean = {
val n = normalizeSlashes(pattern)
n.exists(c => c == '*' || c == '?' || c == '[' || c == '{')
}

private def normalizeSlashes(s: String): String =
s.trim.replace('\\', '/')

/** Leading `./` is stripped so matchers align with workspace-relative paths. */
private def globPatternForMatcher(pattern: String): String = {
val n = normalizeSlashes(pattern)
val n = MbtGlobMatcher.normalizeSlashes(pattern)
if (n.startsWith("./")) n.substring(2) else n
}

Expand All @@ -146,7 +144,7 @@ case class MbtBuild(
.split('/')
.toSeq
.filter(_.nonEmpty)
.takeWhile(segment => !isGlob(segment))
.takeWhile(segment => !MbtGlobMatcher.isPatternGlob(segment))
literalSegments match {
case head +: tail => Some(Paths.get(head, tail: _*))
case _ => None
Expand Down Expand Up @@ -230,7 +228,18 @@ object MbtBuild {
.getOrElse(ju.Collections.emptyList())
.asScala).distinct.asJava

MbtBuild(mergedModules, mergedNamespaces, mergedUncheckedSources)
val mergedWatchedFiles = {
val combined =
(a.getWatchedFiles.asScala ++ b.getWatchedFiles.asScala).distinct
if (combined.isEmpty) null else combined.asJava
}

MbtBuild(
mergedModules,
mergedNamespaces,
mergedUncheckedSources,
mergedWatchedFiles,
)
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,12 @@ case class MbtGlobMatcher(
value.startsWith(relativeDirectory)
}
}

object MbtGlobMatcher {
def normalizeSlashes(s: String): String = s.trim.replace('\\', '/')

def isPatternGlob(pattern: String): Boolean =
normalizeSlashes(pattern).exists(c =>
c == '*' || c == '?' || c == '[' || c == '{'
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -224,7 +224,7 @@ abstract class BazelMbtImporter(
tables.bazelMbtNamespaceModes.chooseMode(projectRoot, mode.name)
}

override def isBuildRelated(path: AbsolutePath): Boolean =
override def isWatchedFile(path: AbsolutePath): Boolean =
BazelBuildTool.isBazelRelatedPath(projectRoot, path)

override def digest(workspace: AbsolutePath): Option[String] =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ class GradleMbtImporter(
scribe.info(s"time: gradle-extractor extract in $timer")
}

override def isBuildRelated(path: AbsolutePath): Boolean =
override def isWatchedFile(path: AbsolutePath): Boolean =
GradleBuildTool.isGradleRelatedPath(projectRoot, path)

override def digest(workspace: AbsolutePath): Option[String] =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,40 @@ final class MbtImport(
scribe.info("mbt-import: wrote .metals/mbt.json")
}

/**
* Like [[runUnconditionally]] but ignores the digest check and prompts the
* user for confirmation when auto-import is disabled. Used for watched files.
*/
def runIgnoringDigest(
providers: List[MbtImportProvider],
isImportInProcess: AtomicBoolean,
): Future[WorkspaceLoadedStatus] = {
val digest = computeDigest(providers).getOrElse(
s"watched-file-changed:${providers.map(_.name).mkString(",")}"
)
val run =
if (userConfig().shouldAutoImportNewProject) {
runUnconditionally(providers, isImportInProcess)
} else {
scribe.debug("mbt-import: awaiting user response for watched file…")
for {
response <- requestImport(providers, digest)
result <-
if (response.isYes)
runUnconditionally(providers, isImportInProcess)
else {
notification.dismiss(2, TimeUnit.MINUTES)
Future.successful(WorkspaceLoadedStatus.Rejected)
}
} yield result
}
run.andThen { case Success(status) =>
status.toChecksumStatus.foreach(
tables.digests.setStatus(digest, _)
)
}
}

Comment thread
zielinsky marked this conversation as resolved.
/**
* Like [[runUnconditionally]] but first checks whether the build digest has
* changed and prompts the user for confirmation when auto-import is disabled.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,10 +35,9 @@ trait MbtImportProvider {
workspace.resolve(s".metals/mbt-$name.json")

/**
* Returns `true` when the given path is a build file that, if modified,
* should trigger a re-import (e.g. `pom.xml` for Maven).
* Returns `true` when the given path, if modified, should trigger a re-import.
*/
def isBuildRelated(path: AbsolutePath): Boolean
def isWatchedFile(path: AbsolutePath): Boolean

/**
* Stable digest of all build files owned by this importer.
Expand Down
Loading
Loading