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

Outline compiler #6114

Open
wants to merge 19 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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
294 changes: 252 additions & 42 deletions metals/src/main/scala/scala/meta/internal/metals/Compilers.scala

Large diffs are not rendered by default.

Expand Up @@ -3,12 +3,9 @@ package scala.meta.internal.metals
import java.nio.charset.Charset
import java.util.Collections

import scala.concurrent.Await
import scala.concurrent.duration.Duration
import scala.util.Success
import scala.util.Try

import scala.meta.internal.builds.SbtBuildTool
import scala.meta.internal.metals.MetalsEnrichments._
import scala.meta.internal.metals.scalacli.ScalaCliServers
import scala.meta.internal.mtags.MD5
Expand Down Expand Up @@ -145,111 +142,7 @@ final class InteractiveSemanticdbs(
javaInteractiveSemanticdb.fold(s.TextDocument())(
_.textDocument(source, text)
)
else scalaCompile(source, text)
else compilers().semanticdbTextDocument(source, text)
}

private def scalaCompile(
source: AbsolutePath,
text: String,
): s.TextDocument = {

val pc = compilers()
.loadCompiler(source)
.orElse {
// load presentation compiler for sources that were create by a worksheet definition request
tables.worksheetSources
.getWorksheet(source)
.flatMap(compilers().loadWorksheetCompiler)
}
.getOrElse {
// this is highly unlikely since None is only returned for non scala/java files
throw new RuntimeException(
s"No presentation compiler found for $source"
)
}

val (prependedLinesSize, modifiedText) =
Option
.when(source.isSbt)(
buildTargets
.sbtAutoImports(source)
)
.flatten
.fold((0, text))(imports =>
(imports.size, SbtBuildTool.prependAutoImports(text, imports))
)

// NOTE(olafur): it's unfortunate that we block on `semanticdbTextDocument`
// here but to avoid it we would need to refactor the `Semanticdbs` trait,
// which requires more effort than it's worth.
val bytes = Await
.result(
pc.semanticdbTextDocument(source.toURI, modifiedText).asScala,
Duration(
clientConfig.initialConfig.compilers.timeoutDelay,
clientConfig.initialConfig.compilers.timeoutUnit,
),
)
val textDocument = {
val doc = s.TextDocument.parseFrom(bytes)
if (doc.text.isEmpty()) doc.withText(text)
else doc
}
if (prependedLinesSize > 0)
cleanupAutoImports(textDocument, text, prependedLinesSize)
else textDocument
}

private def cleanupAutoImports(
document: s.TextDocument,
originalText: String,
linesSize: Int,
): s.TextDocument = {

def adjustRange(range: s.Range): Option[s.Range] = {
val nextStartLine = range.startLine - linesSize
val nextEndLine = range.endLine - linesSize
if (nextEndLine >= 0) {
val nextRange = range.copy(
startLine = nextStartLine,
endLine = nextEndLine,
)
Some(nextRange)
} else None
}

val adjustedOccurences =
document.occurrences.flatMap { occurence =>
occurence.range
.flatMap(adjustRange)
.map(r => occurence.copy(range = Some(r)))
}

val adjustedDiagnostic =
document.diagnostics.flatMap { diagnostic =>
diagnostic.range
.flatMap(adjustRange)
.map(r => diagnostic.copy(range = Some(r)))
}

val adjustedSynthetic =
document.synthetics.flatMap { synthetic =>
synthetic.range
.flatMap(adjustRange)
.map(r => synthetic.copy(range = Some(r)))
}

s.TextDocument(
schema = document.schema,
uri = document.uri,
text = originalText,
md5 = MD5.compute(originalText),
language = document.language,
symbols = document.symbols,
occurrences = adjustedOccurences,
diagnostics = adjustedDiagnostic,
synthetics = adjustedSynthetic,
)
}

}
Expand Up @@ -578,7 +578,7 @@ class MetalsLspService(
scalaVersionSelector,
clientConfig.icons,
onCreate = path => {
buildTargets.onCreate(path)
onCreate(path)
onChange(List(path))
},
)
Expand Down Expand Up @@ -1228,7 +1228,7 @@ class MetalsLspService(
val path = params.getTextDocument.getUri.toAbsolutePath
buffers.put(path, change.getText)
diagnostics.didChange(path)

compilers.didChange(path)
parseTrees(path)
.map { _ =>
treeView.onWorkspaceFileDidChange(path)
Expand Down Expand Up @@ -1357,6 +1357,11 @@ class MetalsLspService(
abs.isScalaOrJava || abs.isSemanticdb || abs.isInBspDirectory(folder)
}

private def onCreate(path: AbsolutePath) {
buildTargets.onCreate(path)
compilers.didChange(path)
}

/**
* Callback that is executed on a file change event by the file watcher.
*
Expand All @@ -1381,7 +1386,7 @@ class MetalsLspService(
) {
event.eventType match {
case EventType.CreateOrModify =>
buildTargets.onCreate(path)
onCreate(path)
case _ =>
}
onChange(List(path)).asJava
Expand Down
@@ -0,0 +1,198 @@
package scala.meta.internal.metals

import java.util.Optional
import java.util.concurrent.atomic.AtomicBoolean
import java.{util => ju}

import scala.collection.concurrent.TrieMap

import scala.meta.internal.metals.MetalsEnrichments._
import scala.meta.io.AbsolutePath
import scala.meta.pc.VirtualFileParams
import scala.meta.pc.{OutlineFiles => JOutlineFiles}

import ch.epfl.scala.bsp4j.BuildTargetIdentifier

class OutlineFilesProvider(
buildTargets: BuildTargets,
buffers: Buffers,
) {
private val outlineFiles =
new TrieMap[BuildTargetIdentifier, BuildTargetOutlineFilesProvider]()

def shouldRestartPc(
id: BuildTargetIdentifier,
reason: PcRestartReason,
): Boolean = {
reason match {
case DidCompile(true) => true
case _ =>
outlineFiles.get(id) match {
case Some(provider) =>
// if it was never compiled successfully by the build server
// we don't restart pc not to lose information from outline compile
provider.wasSuccessfullyCompiledByBuildServer
tgodzik marked this conversation as resolved.
Show resolved Hide resolved
case None => true
}
}
}

def onDidCompile(id: BuildTargetIdentifier, wasSuccessful: Boolean): Unit = {
outlineFiles.get(id) match {
case Some(provider) =>
if (wasSuccessful) provider.successfulCompilation()
case None =>
for {
scalaTarget <- buildTargets.scalaTarget(id)
// we don't perform outline compilation for Scala 3
if (!ScalaVersions.isScala3Version(scalaTarget.scalaVersion))
} outlineFiles.putIfAbsent(
id,
new BuildTargetOutlineFilesProvider(
buildTargets,
buffers,
id,
wasSuccessful,
),
)
}
}

def didChange(id: String, path: AbsolutePath): Unit =
buildTargetId(id).foreach(didChange(_, path))

def didChange(id: BuildTargetIdentifier, path: AbsolutePath): Unit = {
for {
provider <- outlineFiles.get(id)
} provider.didChange(path)
}

def getOutlineFiles(id: String): Optional[JOutlineFiles] =
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is this id? Should we use BuildTargetIdentifier here? Or at least add an alias.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is BuildTargetIdentifier, I'm constructing it here.

  def getOutlineFiles(id: String): Optional[JOutlineFiles] =
    getOutlineFiles(buildTargetId(id))

  def getOutlineFiles(
      buildTargetId: Option[BuildTargetIdentifier]
  )

It's just that it comes as a string from pc (can't be BuildTargetIdentifier, because pc interface doesn't depend on bsp4j) and it's less boilerplate to map it here instead of each place in Compilers.

getOutlineFiles(buildTargetId(id))

def getOutlineFiles(
buildTargetId: Option[BuildTargetIdentifier]
): Optional[JOutlineFiles] = {
val res: Option[JOutlineFiles] =
for {
id <- buildTargetId
provider <- outlineFiles.get(id)
outlineFiles <- provider.outlineFiles()
} yield outlineFiles
res.asJava
}

def enrichWithOutlineFiles(
buildTargetId: Option[BuildTargetIdentifier]
)(vFile: CompilerVirtualFileParams): CompilerVirtualFileParams = {
val optOutlineFiles =
for {
id <- buildTargetId
provider <- outlineFiles.get(id)
outlineFiles <- provider.outlineFiles()
} yield outlineFiles

optOutlineFiles
.map(outlineFiles => vFile.copy(outlineFiles = Optional.of(outlineFiles)))
.getOrElse(vFile)
}

def enrichWithOutlineFiles(
path: AbsolutePath
)(vFile: CompilerVirtualFileParams): CompilerVirtualFileParams =
enrichWithOutlineFiles(buildTargets.inferBuildTarget(path))(vFile)

def clear(): Unit = {
outlineFiles.clear()
}

private def buildTargetId(id: String): Option[BuildTargetIdentifier] =
Option(id).filter(_.nonEmpty).map(new BuildTargetIdentifier(_))
}

class BuildTargetOutlineFilesProvider(
buildTargets: BuildTargets,
buffers: Buffers,
id: BuildTargetIdentifier,
wasCompilationSuccessful: Boolean,
) {
private val changedDocuments =
ConcurrentHashSet.empty[AbsolutePath]

private val wasAllOutlined: AtomicBoolean =
new AtomicBoolean(false)

private val wasSuccessfullyCompiled: AtomicBoolean =
new AtomicBoolean(wasCompilationSuccessful)

def wasSuccessfullyCompiledByBuildServer: Boolean =
wasSuccessfullyCompiled.get()

def successfulCompilation(): Unit = {
wasSuccessfullyCompiled.set(true)
changedDocuments.clear()
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note: that this doesn't account for possible changes before compilation end is reported, it may not 100% reflect the state of files that was during compilation.

}

def didChange(path: AbsolutePath): Boolean =
changedDocuments.add(path)

def outlineFiles(): Option[OutlineFiles] = {
if (!wasSuccessfullyCompiled.get() && !wasAllOutlined.getAndSet(true)) {
// initial outline compilation that is a substitute for build server compilation
val allFiles =
buildTargets
.buildTargetSources(id)
.flatMap(_.listRecursive.toList)

if (allFiles.size > OutlineFilesProvider.maxOutlineFiles) {
// too many files to outline using pc
None
} else {
val inputs = allFiles.flatMap(toVirtualFileParams(_)).toList.asJava
Some(
OutlineFiles(
inputs,
isFirstCompileSubstitute = true,
)
)
}
} else {
changedDocuments.asScala.toList.flatMap(
toVirtualFileParams
) match {
case Nil => None
case files =>
Some(
OutlineFiles(
files.asJava,
isFirstCompileSubstitute = false,
)
)
}
}
}

private def toVirtualFileParams(
path: AbsolutePath
): Option[VirtualFileParams] =
buffers.get(path).orElse(path.readTextOpt).map { text =>
CompilerVirtualFileParams(
path.toURI,
text,
)
}

}

sealed trait PcRestartReason
case object InverseDependency extends PcRestartReason
case class DidCompile(wasSuccess: Boolean) extends PcRestartReason

case class OutlineFiles(
files: ju.List[VirtualFileParams],
isFirstCompileSubstitute: Boolean = false,
) extends JOutlineFiles

object OutlineFilesProvider {
val maxOutlineFiles = 300
}
Expand Up @@ -6,4 +6,4 @@
public interface DefinitionResult {
String symbol();
List<Location> locations();
}
}
16 changes: 16 additions & 0 deletions mtags-interfaces/src/main/java/scala/meta/pc/OutlineFiles.java
@@ -0,0 +1,16 @@
package scala.meta.pc;
import java.util.List;

public interface OutlineFiles {

/**
* Will this outline compilation be substitute for build server's compilation.
* Used if the first compilation using build server is unsuccessful.
*/
boolean isFirstCompileSubstitute();

/**
* Files that should be outline compiled before calculating result.
*/
List<VirtualFileParams> files();
tgodzik marked this conversation as resolved.
Show resolved Hide resolved
}