Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

improvement: ignore create or modify file watcher event when content did not change #7006

Open
wants to merge 6 commits into
base: main
Choose a base branch
from
Open
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
38 changes: 20 additions & 18 deletions metals/src/main/scala/scala/meta/internal/metals/Compilations.scala
Original file line number Diff line number Diff line change
Expand Up @@ -27,11 +27,12 @@ final class Compilations(
languageClient: MetalsLanguageClient,
refreshTestSuites: () => Unit,
afterSuccessfulCompilation: () => Unit,
isCurrentlyFocused: b.BuildTargetIdentifier => Boolean,
buildtargetInFocus: () => Option[b.BuildTargetIdentifier],
compileWorksheets: Seq[AbsolutePath] => Future[Unit],
onStartCompilation: () => Unit,
userConfiguration: () => UserConfiguration,
downstreamTargets: PreviouslyCompiledDownsteamTargets,
fileChanges: FileChanges,
bestEffortEnabled: Boolean,
)(implicit ec: ExecutionContext) {
private val compileTimeout: Timeout =
Expand Down Expand Up @@ -96,6 +97,7 @@ final class Compilations(
def compileTarget(
target: b.BuildTargetIdentifier
): Future[b.CompileResult] = {
fileChanges.willCompile(List(target))
compileBatch(target).map { results =>
results.getOrElse(target, new b.CompileResult(b.StatusCode.CANCELLED))
}
Expand All @@ -104,44 +106,44 @@ final class Compilations(
def compileTargets(
targets: Seq[b.BuildTargetIdentifier]
): Future[Unit] = {
fileChanges.willCompile(targets.toList)
compileBatch(targets).ignoreValue
}

def compileFile(path: AbsolutePath): Future[b.CompileResult] = {
def compileFile(
path: AbsolutePath,
fingerprint: Option[Fingerprint] = None,
assumeDidNotChange: Boolean = false,
): Future[Option[b.CompileResult]] = {
def empty = new b.CompileResult(b.StatusCode.CANCELLED)
for {
targetOpt <- expand(path)
targetOpt <- fileChanges.buildTargetToCompile(
path,
fingerprint,
assumeDidNotChange,
)
result <- targetOpt match {
case None => Future.successful(empty)
case Some(target) =>
compileBatch(target)
.map(res => res.getOrElse(target, empty))
}
_ <- compileWorksheets(Seq(path))
} yield result
} yield Some(result)
}

def compileFiles(
paths: Seq[AbsolutePath],
focusedDocumentBuildTarget: Option[BuildTargetIdentifier],
): Future[Unit] = {
def compileFiles(paths: Seq[(AbsolutePath, Fingerprint)]): Future[Unit] = {
for {
targets <- expand(paths)
targets <- fileChanges.buildTargetsToCompile(paths, buildtargetInFocus())
_ <- compileBatch(targets)
_ <- focusedDocumentBuildTarget match {
case Some(bt)
if !targets.contains(bt) &&
buildTargets.isInverseDependency(bt, targets.toList) =>
compileBatch(bt)
case _ => Future.successful(())
}
_ <- compileWorksheets(paths)
_ <- compileWorksheets(paths.map(_._1))
} yield ()
}

def cascadeCompile(targets: Seq[BuildTargetIdentifier]): Future[Unit] = {
val inverseDependencyLeaves =
targets.flatMap(buildTargets.inverseDependencyLeaves).distinct
fileChanges.willCompile(inverseDependencyLeaves.toList)
cascadeBatch(inverseDependencyLeaves).map(_ => ())
}

Expand Down Expand Up @@ -292,7 +294,7 @@ final class Compilations(
targets,
() => {
refreshTestSuites()
if (targets.exists(isCurrentlyFocused)) {
if (targets.exists(buildtargetInFocus().contains)) {
languageClient.refreshModel()
}
},
Expand Down
136 changes: 136 additions & 0 deletions metals/src/main/scala/scala/meta/internal/metals/FileChanges.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
package scala.meta.internal.metals

import scala.collection.concurrent.TrieMap
import scala.collection.mutable
import scala.concurrent.ExecutionContext
import scala.concurrent.Future

import scala.meta.internal.metals.MetalsEnrichments._
import scala.meta.io.AbsolutePath

import ch.epfl.scala.bsp4j.BuildTargetIdentifier

class FileChanges(buildTargets: BuildTargets, workspace: () => AbsolutePath)(
implicit ec: ExecutionContext
) {
private val previousSignatures = TrieMap[AbsolutePath, String]()
private val dirtyBuildTargets = mutable.Set[BuildTargetIdentifier]()

def addAllDirty(
targets: IterableOnce[BuildTargetIdentifier]
): mutable.Set[BuildTargetIdentifier] =
dirtyBuildTargets.addAll(targets)

def buildTargetsToCompile(
paths: Seq[(AbsolutePath, Fingerprint)],
focusedDocumentBuildTarget: Option[BuildTargetIdentifier],
): Future[Seq[BuildTargetIdentifier]] =
for {
toCompile <- paths.foldLeft(
Future.successful(Seq.empty[BuildTargetIdentifier])
) { case (toCompile, (path, fingerprint)) =>
toCompile.flatMap { acc =>
findBuildTargetIfShouldCompile(path, Some(fingerprint)).map(acc ++ _)
}
}
} yield {
val allToCompile =
if (focusedDocumentBuildTarget.exists(dirtyBuildTargets(_)))
toCompile ++ focusedDocumentBuildTarget
else toCompile
willCompile(allToCompile)
allToCompile
}

def buildTargetToCompile(
path: AbsolutePath,
fingerprint: Option[Fingerprint],
assumeDidNotChange: Boolean,
): Future[Option[BuildTargetIdentifier]] = {
for {
toCompile <- findBuildTargetIfShouldCompile(
path,
fingerprint,
assumeDidNotChange,
)
} yield {
willCompile(toCompile.toSeq)
toCompile
}
}

def willCompile(ids: Seq[BuildTargetIdentifier]): Unit =
buildTargets
.buildTargetTransitiveDependencies(ids.toList)
.foreach(dirtyBuildTargets.remove)

private def findBuildTargetIfShouldCompile(
path: AbsolutePath,
fingerprint: Option[Fingerprint],
assumeDidNotChange: Boolean = false,
): Future[Option[BuildTargetIdentifier]] = {
expand(path).map(
_.filter(bt =>
dirtyBuildTargets.contains(
bt
) || (!assumeDidNotChange && didContentChange(path, fingerprint, bt))
)
)
}

private def expand(
path: AbsolutePath
): Future[Option[BuildTargetIdentifier]] = {
val isCompilable =
(path.isScalaOrJava || path.isSbt) &&
!path.isDependencySource(workspace()) &&
!path.isInTmpDirectory(workspace())

if (isCompilable) {
val targetOpt = buildTargets.inverseSourcesBsp(path)
targetOpt.foreach {
case tgts if tgts.isEmpty => scribe.warn(s"no build target for: $path")
case _ =>
}

targetOpt
} else
Future.successful(None)
}

private def didContentChange(
path: AbsolutePath,
fingerprint: Option[Fingerprint],
buildTarget: BuildTargetIdentifier,
): Boolean = {
val didChange = didContentChange(path, fingerprint)
if (didChange) {
buildTargets
.allInverseDependencies(buildTarget)
.foreach { bt =>
if (bt != buildTarget) dirtyBuildTargets.add(bt)
}
}
didChange
}

private def didContentChange(
path: AbsolutePath,
fingerprint: Option[Fingerprint],
): Boolean = {
fingerprint
.map { fingerprint =>
synchronized {
if (previousSignatures.getOrElse(path, null) == fingerprint.md5)
false
else {
previousSignatures.put(path, fingerprint.md5)
true
}
}
}
.getOrElse(true)
}

def cancel(): Unit = previousSignatures.clear()
}
Original file line number Diff line number Diff line change
Expand Up @@ -42,4 +42,5 @@ trait IndexProviders {
def folder: AbsolutePath
def implementationProvider: ImplementationProvider
def resetService(): Unit
def fileChanges: FileChanges
}
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,12 @@ case class Indexer(indexProviders: IndexProviders)(implicit rc: ReportContext) {
data.reset()
buildTargetClasses.clear()
data.addWorkspaceBuildTargets(importedBuild.workspaceBuildTargets)
fileChanges.addAllDirty(
importedBuild.workspaceBuildTargets
.getTargets()
.map(_.getId())
.asScala
)
data.addScalacOptions(
importedBuild.scalacOptions,
bspSession.map(_.mainConnection),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,8 @@ abstract class MetalsLspService(
val buildTargets: BuildTargets =
BuildTargets.from(folder, mainBuildTargetsData, tables)

val fileChanges: FileChanges = new FileChanges(buildTargets, () => folder)

val buildTargetClasses =
new BuildTargetClasses(buildTargets)

Expand All @@ -217,11 +219,12 @@ abstract class MetalsLspService(
headDoctor.executeRefreshDoctor()
else ()
},
buildTarget => focusedDocumentBuildTarget.get() == buildTarget,
() => Option(focusedDocumentBuildTarget.get()),
worksheets => onWorksheetChanged(worksheets),
onStartCompilation,
() => userConfig,
downstreamTargets,
fileChanges,
clientConfig.initialConfig.enableBestEffort,
)
var indexingPromise: Promise[Unit] = Promise[Unit]()
Expand Down Expand Up @@ -744,11 +747,9 @@ abstract class MetalsLspService(
// In some cases like peeking definition didOpen might be followed up by close
// and we would lose the notion of the focused document
recentlyOpenedFiles.add(path)
val prevBuildTarget = focusedDocumentBuildTarget.getAndUpdate { current =>
buildTargets
.inverseSources(path)
.getOrElse(current)
}
focusedDocumentBuildTarget.set(
buildTargets.inverseSources(path).getOrElse(null)
)

// Update md5 fingerprint from file contents on disk
fingerprints.add(path, FileIO.slurp(path, charset))
Expand Down Expand Up @@ -784,7 +785,7 @@ abstract class MetalsLspService(
Future
.sequence(
List(
maybeCompileOnDidFocus(path, prevBuildTarget),
compilations.compileFile(path, assumeDidNotChange = true),
compilers.load(List(path)),
parser,
interactive,
Expand All @@ -807,12 +808,10 @@ abstract class MetalsLspService(
uri: String
): CompletableFuture[DidFocusResult.Value] = {
val path = uri.toAbsolutePath
val prevBuildTarget = focusedDocumentBuildTarget.getAndUpdate { current =>
buildTargets
.inverseSources(path)
.getOrElse(current)
}
scalaCli.didFocus(path)
focusedDocumentBuildTarget.set(
buildTargets.inverseSources(path).getOrElse(null)
)
// Don't trigger compilation on didFocus events under cascade compilation
// because save events already trigger compile in inverse dependencies.
if (path.isDependencySource(folder)) {
Expand All @@ -821,29 +820,16 @@ abstract class MetalsLspService(
CompletableFuture.completedFuture(DidFocusResult.RecentlyActive)
} else {
worksheetProvider.onDidFocus(path)
maybeCompileOnDidFocus(path, prevBuildTarget).asJava
compilations
.compileFile(path, assumeDidNotChange = true)
.map(
_.map(_ => DidFocusResult.Compiled)
.getOrElse(DidFocusResult.AlreadyCompiled)
)
.asJava
}
}

protected def maybeCompileOnDidFocus(
path: AbsolutePath,
prevBuildTarget: b.BuildTargetIdentifier,
): Future[DidFocusResult.Value] =
buildTargets.inverseSources(path) match {
case Some(target) if prevBuildTarget != target =>
compilations
.compileFile(path)
.map(_ => DidFocusResult.Compiled)
case _ if path.isWorksheet =>
compilations
.compileFile(path)
.map(_ => DidFocusResult.Compiled)
case Some(_) =>
Future.successful(DidFocusResult.AlreadyCompiled)
case None =>
Future.successful(DidFocusResult.NoBuildTarget)
}

def pause(): Unit = pauseables.pause()

def unpause(): Unit = pauseables.unpause()
Expand Down Expand Up @@ -938,16 +924,17 @@ abstract class MetalsLspService(
}

protected def onChange(paths: Seq[AbsolutePath]): Future[Unit] = {
paths.foreach { path =>
fingerprints.add(path, FileIO.slurp(path, charset))
}
val pathsWithFingerPrints =
paths.map { path =>
val fingerprint = fingerprints.add(path, FileIO.slurp(path, charset))
(path, fingerprint)
}

Future
.sequence(
List(
Future(indexer.reindexWorkspaceSources(paths)),
compilations
.compileFiles(paths, Option(focusedDocumentBuildTarget.get())),
compilations.compileFiles(pathsWithFingerPrints),
) ++ paths.map(f => Future(interactiveSemanticdbs.textDocument(f)))
)
.ignoreValue
Expand All @@ -957,8 +944,7 @@ abstract class MetalsLspService(
Future
.sequence(
List(
compilations
.compileFiles(List(path), Option(focusedDocumentBuildTarget.get())),
compilations.compileFiles(List((path, null))),
Future {
diagnostics.didDelete(path)
testProvider.onFileDelete(path)
Expand Down Expand Up @@ -1235,7 +1221,9 @@ abstract class MetalsLspService(
thresholdMillis = 1.second.toMillis,
) {
val path = params.getTextDocument.getUri.toAbsolutePath
codeLensProvider.findLenses(path).map(_.toList.asJava)
codeLensProvider.findLenses(path).map(_.toList.asJava).map { found =>
found
}
}
}
}
Expand Down
Loading
Loading