Skip to content

Commit d4b80cf

Browse files
committed
feat: Don't run InteractiveDriver.run if the content didn't change
If the content didn't change since the last `driver.run`, do not compile because the compilation unit should have the up to date typed tree. Skip compilation save up much of CPU usage when users are reading source code (and hover on symbols) without modifying the source code. What happens if we change in a file affect another file's type information? For example, when we have the following source files, if we hover on `B.foo` in `A.scala`, Metals shows the symbol's type is `Int`. ```scala // A.scala def main = println(B.foo) // B.scala object B: def foo: Int = ??? ``` When we modify `B.scala` to `def foo: String = ???`, what happens to `A.scala`? You may think that, Metals keep using the old compilation unit that shows `B.foo` as `Int` instead of `String` because Metals invalidates the compilation cache based on its content hash. That's not true, `alreadyCompiled("A.scala", <content hash>)` will be `false`, and Metals will run `driver.run` on `A.scala` and show `String` for `B.foo` in `A.scala`. Why? Because Metals will recreate a new `MetalsDriver` and `lastCompiled` cache will be cleared up. - When a file has changed, Metals call [Compilers.restart](https://github.com/scalameta/metals/blob/c6e0f30febeff67e970e463d79862e27e45ada3f/metals/src/main/scala/scala/meta/internal/metals/Compilers.scala#L247). - The restart method calls `shutdownCurrentCompiler` that [set `_compiler` to `null`.](https://github.com/scalameta/metals/blob/c6e0f30febeff67e970e463d79862e27e45ada3f/mtags/src/main/scala/scala/meta/internal/pc/CompilerAccess.scala#L57-L74). This is basically the same way to cache compilation as Scala2 See my explanation on how Scala2 + Metals compilation cache works https://contributors.scala-lang.org/t/how-does-scala2-compiler-cache-invalidate-the-typechecking-result-for-the-given-tree/5858/4?u=tanishiking
1 parent c6e0f30 commit d4b80cf

File tree

4 files changed

+49
-7
lines changed

4 files changed

+49
-7
lines changed
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
package scala.meta.internal.pc
2+
3+
import java.net.URI
4+
5+
import scala.collection.concurrent.TrieMap
6+
7+
import scala.meta.internal.mtags.MD5
8+
9+
import dotty.tools.dotc.core.Contexts.Context
10+
import dotty.tools.dotc.interactive.InteractiveDriver
11+
import dotty.tools.dotc.interactive.SourceTree
12+
import dotty.tools.dotc.reporting.Diagnostic
13+
import dotty.tools.dotc.util.SourceFile
14+
15+
class MetalsDriver(
16+
override val settings: List[String]
17+
) extends InteractiveDriver(settings):
18+
19+
private val lastCompiled: TrieMap[URI, String] = TrieMap.empty
20+
21+
private def alreadyCompiled(uri: URI, hash: String): Boolean =
22+
lastCompiled
23+
.get(uri)
24+
.map(cache => cache == hash)
25+
.getOrElse(false)
26+
27+
override def run(uri: URI, source: SourceFile): List[Diagnostic] =
28+
val content = source.content.mkString
29+
val contentHash = MD5.compute(content)
30+
if alreadyCompiled(uri, contentHash) then Nil
31+
else
32+
lastCompiled.put(uri, contentHash)
33+
super.run(uri, source)
34+
35+
override def run(uri: URI, sourceCode: String): List[Diagnostic] =
36+
val contentHash = MD5.compute(sourceCode)
37+
if alreadyCompiled(uri, contentHash) then Nil
38+
else
39+
lastCompiled.put(uri, contentHash)
40+
super.run(uri, sourceCode)
41+
42+
end MetalsDriver

mtags/src/main/scala-3/scala/meta/internal/pc/Scala3CompilerAccess.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ class Scala3CompilerAccess(
2424
sh: Option[ScheduledExecutorService],
2525
newCompiler: () => Scala3CompilerWrapper,
2626
)(using ec: ExecutionContextExecutor)
27-
extends CompilerAccess[StoreReporter, InteractiveDriver](
27+
extends CompilerAccess[StoreReporter, MetalsDriver](
2828
config,
2929
sh,
3030
newCompiler,

mtags/src/main/scala-3/scala/meta/internal/pc/Scala3CompilerWrapper.scala

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,10 @@ import dotty.tools.dotc.core.Contexts.Context
77
import dotty.tools.dotc.interactive.InteractiveDriver
88
import dotty.tools.dotc.reporting.StoreReporter
99

10-
class Scala3CompilerWrapper(driver: InteractiveDriver)
11-
extends CompilerWrapper[StoreReporter, InteractiveDriver]:
10+
class Scala3CompilerWrapper(driver: MetalsDriver)
11+
extends CompilerWrapper[StoreReporter, MetalsDriver]:
1212

13-
override def compiler(): InteractiveDriver = driver
13+
override def compiler(): MetalsDriver = driver
1414

1515
override def resetReporter(): Unit =
1616
val ctx = driver.currentCtx

mtags/src/main/scala-3/scala/meta/internal/pc/ScalaPresentationCompiler.scala

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ case class ScalaPresentationCompiler(
5454
private val forbiddenOptions = Set("-print-lines", "-print-tasty")
5555
private val forbiddenDoubleOptions = Set("-release")
5656

57-
val compilerAccess: CompilerAccess[StoreReporter, InteractiveDriver] =
57+
val compilerAccess: CompilerAccess[StoreReporter, MetalsDriver] =
5858
Scala3CompilerAccess(
5959
config,
6060
sh,
@@ -70,7 +70,7 @@ case class ScalaPresentationCompiler(
7070
case head :: tail => head :: removeDoubleOptions(tail)
7171
case Nil => options
7272

73-
def newDriver: InteractiveDriver =
73+
def newDriver: MetalsDriver =
7474
val implicitSuggestionTimeout = List("-Ximport-suggestion-timeout", "0")
7575
val defaultFlags = List("-color:never")
7676
val filteredOptions = removeDoubleOptions(
@@ -81,7 +81,7 @@ case class ScalaPresentationCompiler(
8181
.mkString(
8282
File.pathSeparator
8383
) :: Nil
84-
new InteractiveDriver(settings)
84+
new MetalsDriver(settings)
8585

8686
override def getTasty(
8787
targetUri: URI,

0 commit comments

Comments
 (0)