Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ class ConnectionProvider(
indexProviders: IndexProviders,
syncStatusReporter: SyncStatusReporter,
mbtBuild: () => MbtBuild,
refreshMbtStateAfterIndex: () => Future[Unit] = () => Future.unit,
mbtDebugStarter: () => Option[MbtDebugSessionStarter] = () => None,
)(implicit ec: ExecutionContextExecutorService, rc: ReportContext)
extends Indexer(indexProviders, mbtBuild)
Expand Down Expand Up @@ -284,6 +285,13 @@ class ConnectionProvider(
else runMbtReimport(mbtImporters)
runImport.flatMap { _ =>
bspSession match {
case Some(session)
if MbtBuildServer.isMbtServer(session.main.name) &&
session.canReloadWorkspace =>
for {
_ <- session.workspaceReload()
buildChange <- connect(new ImportBuildAndIndex(session), progress)
} yield buildChange
case Some(session) =>
connect(new ImportBuildAndIndex(session), progress)
case None =>
Expand Down Expand Up @@ -679,32 +687,27 @@ class ConnectionProvider(
}
_ = compilers.cancel()
buildChange <- index(check, progress)
// When testing we need to make sure the classpath is refreshed after mbt.json is generated
_ <- {
if (MetalsServerConfig.isTesting)
refreshMbtTurbineClasspath(session).withInterrupt
else
Future {
refreshMbtTurbineClasspath(session)
}.withInterrupt
}
_ <- refreshMbtStateAfterIndexIfNeeded(session)
} yield {
syncStatusReporter.importFinished(focusedDocument.map(_.toURI.toString))
buildChange
}
}

private def refreshMbtTurbineClasspath(
private def refreshMbtStateAfterIndexIfNeeded(
session: BspSession
): Future[Unit] =
if (
MbtBuildServer.isMbtServer(session.main.name) &&
userConfig.javaSymbolLoader.isTurbineClasspath
) {
mbtSymbolSearch.scheduleRecompileTurbineClasspath()
} else {
Future.unit
}
): Interruptable[Unit] = {
val refresh =
if (MbtBuildServer.isMbtServer(session.main.name))
refreshMbtStateAfterIndex()
else Future.unit

if (MetalsServerConfig.isTesting) refresh.withInterrupt
else
refresh.recover { case error =>
scribe.warn("failed to refresh MBT diagnostics", error)
}.withInterrupt
}

private def saveProjectReferencesInfo(
bspBuilds: List[BspSession.BspBuild]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1155,6 +1155,20 @@ abstract class MetalsLspService(
private def refreshAllDiagnostics(): Future[Unit] = {
refreshDiagnostics(_ => true)
}

protected def refreshMbtStateAfterIndex(): Future[Unit] = {
val refresh =
if (userConfig.javaSymbolLoader.isTurbineClasspath)
mbt2.recompileTurbineClasspath()
Comment thread
zielinsky marked this conversation as resolved.
Outdated
else Future.unit
for {
_ <- refresh
_ = compilers.cancel()
_ = diagnostics.reset(buffers.open.toSeq)
_ <- refreshAllDiagnostics()
} yield ()
}

protected def refreshDiagnostics(
isIncludedPath: AbsolutePath => Boolean
): Future[Unit] = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -236,6 +236,7 @@ class ProjectMetalsLspService(
this,
syncStatusReporter,
() => mbtBuild,
() => refreshMbtStateAfterIndex(),
mbtDebugStarter = () => mbtDebugStarter,
)
provider.buildServerPromise.future.onComplete(_ => moduleStatus.refresh())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import java.nio.file.Path
import java.util.Optional
import java.util.concurrent.ConcurrentLinkedQueue
import java.util.concurrent.atomic.AtomicBoolean
import java.util.concurrent.atomic.AtomicLong
import java.{util => ju}
import javax.tools.JavaFileManager
import javax.tools.JavaFileObject
Expand Down Expand Up @@ -158,6 +159,8 @@ class TurbineCompiler[T](
// In this mode, we rely entirely on SOURCE_PATH fallback for updated sources.
private def isRecompilationDisabled: Boolean =
debounceDelay.toMillis >= 3600000
private val compileGeneration = new AtomicLong(0L)
private val compileLock = new Object

private val doCompile =
BatchedFunction.fromFuture[Unit, TurbineCompileResult](
Expand All @@ -168,12 +171,14 @@ class TurbineCompiler[T](
Future.successful(result)
} else {
val toCompile = sourcepathSources()
val generation = compileGeneration.get()
for {
_ <- sleeper.sleep(debounceDelay)
} yield {
val result = doCompileNow()
toCompile.foreach(_.isCompiled.set(true))
result
doCompileNow(
expectedGeneration = generation,
markCompiled = toCompile,
)
}
}
},
Expand All @@ -185,20 +190,31 @@ class TurbineCompiler[T](
)
}

var result = TurbineCompiler.emptyResult
def doCompileNow(): TurbineCompileResult = {
result = TurbineCompiler.compileClassfiles(
allCompilationUnits(),
parseUnit,
classpath(),
progressBars,
)
cleanup()
// Clear deleted binary names after recompile - they are no longer in the compiled output
deletedBinaryNames.clear()
onIndexingDone()
result
}
@volatile var result = TurbineCompiler.emptyResult
private def doCompileNow(
expectedGeneration: Long,
markCompiled: Seq[SourcepathJavaFileObject] = Nil,
): TurbineCompileResult =
compileLock.synchronized {
if (expectedGeneration != compileGeneration.get()) result
else {
val compiled = TurbineCompiler.compileClassfiles(
allCompilationUnits(),
parseUnit,
classpath(),
progressBars,
)
if (expectedGeneration == compileGeneration.get()) {
result = compiled
markCompiled.foreach(_.isCompiled.set(true))
cleanup()
// Clear deleted binary names after recompile - they are no longer in the compiled output
deletedBinaryNames.clear()
onIndexingDone()
}
result
}
}

/**
* Called when a file is deleted. Tracks the binary names of the deleted classes
Expand Down Expand Up @@ -227,8 +243,12 @@ class TurbineCompiler[T](
deletedBinaryNames.contains(binaryName)
}

def compileNow(): Future[TurbineCompileResult] = Future {
doCompileNow()
def compileNow(): Future[TurbineCompileResult] = {
val generation = compileGeneration.incrementAndGet()
doCompile.cancelAll()
Future {
doCompileNow(expectedGeneration = generation)
}
}

def scheduleCompile(): Future[TurbineCompileResult] = {
Expand Down
81 changes: 81 additions & 0 deletions tests/unit/src/test/scala/tests/mbt/MbtBuildServerLspSuite.scala
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import scala.meta.internal.metals.Configs.FallbackSourcepathConfig
import scala.meta.internal.metals.Configs.ReferenceProviderConfig
import scala.meta.internal.metals.Configs.WorkspaceSymbolProviderConfig
import scala.meta.internal.metals.InitializationOptions
import scala.meta.internal.metals.ServerCommands
import scala.meta.internal.metals.TestUserInterfaceKind
import scala.meta.internal.metals.UserConfiguration
import scala.meta.internal.metals.mbt.MbtBuildServer
Expand Down Expand Up @@ -979,3 +980,83 @@ class MbtBuildServerLspSuite
} yield ()
}
}

class MbtBuildServerManualImportLspSuite
extends BaseCompletionLspSuite("mbt-build-server-manual-import") {

override def userConfig: UserConfiguration =
super.userConfig.copy(
fallbackScalaVersion = Some(BuildInfo.scalaVersion),
presentationCompilerDiagnostics = true,
buildOnChange = false,
buildOnFocus = false,
workspaceSymbolProvider = WorkspaceSymbolProviderConfig.mbt,
referenceProvider = ReferenceProviderConfig.mbt,
fallbackSourcepath = FallbackSourcepathConfig("all-sources"),
preferredBuildServer = Some(MbtBuildServer.name),
automaticImportBuild = AutoImportBuildKind.Off,
)

if (!Properties.isWin)
for (iteration <- 1 to 10)
test(s"script-import-clears-java-diagnostics-$iteration") {
runScriptImportClearsJavaDiagnostics()
}

private def runScriptImportClearsJavaDiagnostics(): Future[Unit] = {
cleanWorkspace()
val mainFile = "src/main/java/a/SampleProfileApplication.java"
val firstExtraFile = "src/main/java/a/FirstExtra.java"
val secondExtraFile = "src/main/java/a/SecondExtra.java"
val mbtJson = new MbtJsonBuilder(BuildInfo.scalaVersion)
.addJavaDependency("com.google.guava", "guava", "33.5.0-jre")
.addNamespace("core", List("src/**"))
.build()
val script =
s"""|#!/bin/sh
|sleep 1
|printf '%s' '$mbtJson' > "$$MBT_OUTPUT_FILE"
|""".stripMargin
def fileInput(className: String): String =
s"""|package a;
|
|import com.google.common.collect.ImmutableList;
|
|public class $className {
| public static ImmutableList<String> names = ImmutableList.of("Alice", "Bob");
|}
|""".stripMargin

client.showMessageRequestHandler = params =>
if (params.getMessage.startsWith("New MBT"))
params.getActions.asScala.find(_.getTitle == "Not now")
else None

for {
_ <- initialize(
s"""|/build.mbt.sh
|$script
|/$mainFile
|${fileInput("SampleProfileApplication")}
|/$firstExtraFile
|${fileInput("FirstExtra")}
|/$secondExtraFile
|${fileInput("SecondExtra")}
|""".stripMargin,
expectError = true,
)
_ <- server.didOpen(mainFile)
_ <- server.didFocus(mainFile)
_ = assert(
client.workspaceDiagnostics.nonEmpty,
"Expected diagnostics before MBT import",
)
importBuild = server.executeCommand(ServerCommands.ImportBuild)
_ <- server.didOpen(firstExtraFile)
_ <- server.didOpen(secondExtraFile)
_ <- importBuild
_ = assertConnectedToBuildServer("MBT")
_ = assertNoDiagnostics()
} yield ()
}
}
Loading