Skip to content

Commit

Permalink
use pc as fallback when missing semanticdb [skip ci]
Browse files Browse the repository at this point in the history
  • Loading branch information
kasiaMarek committed Dec 20, 2023
1 parent 61a8dbd commit af2ba5a
Show file tree
Hide file tree
Showing 3 changed files with 115 additions and 76 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -541,6 +541,7 @@ final case class Indexer(
val input = sourceToIndex0.toInput
val symbols = ArrayBuffer.empty[WorkspaceSymbolInformation]
val methodSymbols = ArrayBuffer.empty[WorkspaceSymbolInformation]
referencesProvider().indexTokens(source, input, dialect)
SemanticdbDefinition.foreach(input, dialect, includeMembers = true) {
case SemanticdbDefinition(info, occ, owner) =>
if (info.isExtension) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,6 @@ import scala.meta.internal.parsing.ClassFinder
import scala.meta.internal.parsing.ClassFinderGranularity
import scala.meta.internal.parsing.DocumentSymbolProvider
import scala.meta.internal.parsing.FoldingRangeProvider
import scala.meta.internal.parsing.TokenEditDistance
import scala.meta.internal.parsing.Trees
import scala.meta.internal.remotels.RemoteLanguageServer
import scala.meta.internal.rename.RenameProvider
Expand Down Expand Up @@ -607,10 +606,10 @@ class MetalsLspService(
semanticdbs,
buffers,
definitionProvider,
remote,
trees,
buildTargets,
compilers,
scalaVersionSelector,
)

private val syntheticHoverProvider: SyntheticHoverProvider =
Expand Down Expand Up @@ -1209,10 +1208,12 @@ class MetalsLspService(
val path = params.getTextDocument.getUri.toAbsolutePath
savedFiles.add(path)
// read file from disk, we only remove files from buffers on didClose.
buffers.put(path, path.toInput.text)
val text = path.toInput.text
buffers.put(path, text)
Future
.sequence(
List(
referencesProvider.indexTokens(path, text),
renameProvider.runSave(),
parseTrees(path),
onChange(List(path)),
Expand Down Expand Up @@ -1524,65 +1525,6 @@ class MetalsLspService(
referencesResult(params).map(_.flatMap(_.locations).asJava)
}

// Triggers a cascade compilation and tries to find new references to a given symbol.
// It's not possible to stream reference results so if we find new symbols we notify the
// user to run references again to see updated results.
private def compileAndLookForNewReferences(
params: ReferenceParams,
result: List[ReferencesResult],
): Future[Unit] = {
val path = params.getTextDocument.getUri.toAbsolutePath
val old = path.toInputFromBuffers(buffers)
compilations.cascadeCompileFiles(Seq(path)).flatMap { _ =>
val newBuffer = path.toInputFromBuffers(buffers)
val newParams: Option[ReferenceParams] =
if (newBuffer.text == old.text) Some(params)
else {
val edit = TokenEditDistance(old, newBuffer, trees)
edit
.getOrElse(TokenEditDistance.NoMatch)
.toRevised(
params.getPosition.getLine,
params.getPosition.getCharacter,
)
.foldResult(
pos => {
params.getPosition.setLine(pos.startLine)
params.getPosition.setCharacter(pos.startColumn)
Some(params)
},
() => Some(params),
() => None,
)
}
newParams match {
case None => Future.unit
case Some(p) =>
for {
newResult <- referencesProvider.references(p)
} yield {
val diff = newResult
.flatMap(_.locations)
.length - result.flatMap(_.locations).length
val diffSyms: Set[String] =
newResult.map(_.symbol).toSet -- result.map(_.symbol).toSet
if (diffSyms.nonEmpty && diff > 0) {
import scala.meta.internal.semanticdb.Scala._
val names =
diffSyms
.map(sym => s"'${sym.desc.name.value}'")
.mkString(" and ")
val message =
s"Found new symbol references for $names, try running again."
scribe.info(message)
statusBar
.addMessage(clientConfig.icons.info + message)
}
}
}
}
}

def referencesResult(
params: ReferenceParams
): Future[List[ReferencesResult]] = {
Expand All @@ -1600,7 +1542,8 @@ class MetalsLspService(
}
}
if (results.nonEmpty) {
compileAndLookForNewReferences(params, results)
val path = params.getTextDocument.getUri.toAbsolutePath
compilations.cascadeCompileFiles(Seq(path))
}
results
}
Expand Down
121 changes: 108 additions & 13 deletions metals/src/main/scala/scala/meta/internal/metals/ReferenceProvider.scala
Original file line number Diff line number Diff line change
Expand Up @@ -9,39 +9,44 @@ import scala.concurrent.Future
import scala.util.control.NonFatal
import scala.util.matching.Regex

import scala.meta.Dialect
import scala.meta.Importee
import scala.meta.inputs.Input
import scala.meta.internal.metals.MetalsEnrichments._
import scala.meta.internal.metals.ResolvedSymbolOccurrence
import scala.meta.internal.mtags.DefinitionAlternatives.GlobalSymbol
import scala.meta.internal.mtags.Semanticdbs
import scala.meta.internal.mtags.Symbol
import scala.meta.internal.parsing.TokenEditDistance
import scala.meta.internal.parsing.Trees
import scala.meta.internal.remotels.RemoteLanguageServer
import scala.meta.internal.semanticdb.Scala._
import scala.meta.internal.semanticdb.SymbolInformation
import scala.meta.internal.semanticdb.SymbolOccurrence
import scala.meta.internal.semanticdb.Synthetic
import scala.meta.internal.semanticdb.TextDocument
import scala.meta.internal.semanticdb.TextDocuments
import scala.meta.internal.tokenizers.LegacyScanner
import scala.meta.internal.tokenizers.LegacyToken._
import scala.meta.internal.{semanticdb => s}
import scala.meta.io.AbsolutePath
import scala.meta.tokens.Token.Ident

import ch.epfl.scala.bsp4j.BuildTargetIdentifier
import com.google.common.hash.BloomFilter
import com.google.common.hash.Funnels
import org.eclipse.lsp4j.Location
import org.eclipse.lsp4j.Position
import org.eclipse.lsp4j.ReferenceParams

final class ReferenceProvider(
workspace: AbsolutePath,
semanticdbs: Semanticdbs,
buffers: Buffers,
definition: DefinitionProvider,
remote: RemoteLanguageServer,
trees: Trees,
buildTargets: BuildTargets,
compilers: Compilers,
scalaVersionSelector: ScalaVersionSelector,
)(implicit ec: ExecutionContext)
extends SemanticdbFeatureProvider {

Expand All @@ -50,6 +55,7 @@ final class ReferenceProvider(
bloom: BloomFilter[CharSequence],
)
val index: TrieMap[Path, IndexEntry] = TrieMap.empty
val tokenIndex: TrieMap[Path, IndexEntry] = TrieMap.empty

override def reset(): Unit = {
index.clear()
Expand All @@ -59,6 +65,38 @@ final class ReferenceProvider(
index.remove(file.toNIO)
}

def indexTokens(
path: AbsolutePath,
text: String,
): Future[Unit] = Future {
val dialect = scalaVersionSelector.getDialect(path)
indexTokens(path, Input.String(text), dialect)
}

def indexTokens(
file: AbsolutePath,
input: Input,
dialect: Dialect,
): Unit =
buildTargets.inverseSources(file).map { id =>
var count = 0
new LegacyScanner(input, dialect).foreach {
case ident if ident.token == IDENTIFIER => count += 1
}

val bloom = BloomFilter.create(
Funnels.stringFunnel(StandardCharsets.UTF_8),
Integer.valueOf(count * 2),
0.01,
)

val entry = IndexEntry(id, bloom)
tokenIndex(file.toNIO) = entry
new LegacyScanner(input, dialect).foreach {
case ident if ident.token == IDENTIFIER => bloom.put(ident.name)
}
}

override def onChange(docs: TextDocuments, file: AbsolutePath): Unit = {
buildTargets.inverseSources(file).map { id =>
val count = docs.documents.foldLeft(0)(_ + _.occurrences.length)
Expand Down Expand Up @@ -146,7 +184,7 @@ final class ReferenceProvider(
s"No symbol found at ${params.getPosition()} for $source"
)
}
Future.sequence {
val semanticdbResult = Future.sequence {
results.map { result =>
val occurrence = result.occurrence.get
val distance = result.distance
Expand All @@ -166,18 +204,22 @@ final class ReferenceProvider(
locations.map(ReferencesResult(occurrence.symbol, _))
}
}
val pcResult =
pcReferences(source, params, path => !index.contains(path.toNIO))

Future
.sequence(List(semanticdbResult, pcResult))
.map(
_.flatten
.groupBy(_.symbol)
.map { case (symbol, refs) =>
ReferencesResult(symbol, refs.flatMap(_.locations))
}
.toList
)
case None =>
scribe.debug(s"No semanticdb for $source")
// NOTE(olafur): we block here instead of returning a Future because it
// requires a significant refactoring to make the reference provider and
// its dependencies (including rename provider) asynchronous. The remote
// language server returns `Future.successful(None)` when it's disabled
// so this isn't even blocking for normal usage of Metals.
Future.successful(
List(
remote.referencesBlocking(params).getOrElse(ReferencesResult.empty)
)
)
pcReferences(source, params)
}
}

Expand Down Expand Up @@ -269,6 +311,59 @@ final class ReferenceProvider(
}
}

private def pcReferences(
path: AbsolutePath,
params: ReferenceParams,
filterTargetFiles: AbsolutePath => Boolean = _ => true,
): Future[List[ReferencesResult]] = {
val result = for {
name <- nameAtPosition(path, params.getPosition())
buildTarget <- buildTargets.inverseSources(path)
targetFiles = pathsForName(buildTarget, name)
.filter(filterTargetFiles)
.toList
if targetFiles.nonEmpty
} yield compilers
.references(params, targetFiles, EmptyCancelToken)
.map(loc => List(ReferencesResult(Symbol.None.toString(), loc)))
result.getOrElse(Future.successful(Nil))
}

private def nameAtPosition(
path: AbsolutePath,
position: Position,
) =
for {
text <- buffers.get(path)
input = Input.String(text)
pos <- position.toMeta(input)
tree <- trees.get(path)
token <- tree.tokens.find { t => t.pos.encloses(pos) }
ident <- token match {
case _: Ident => Some(token)
case _ => None
}
} yield ident.name

private def pathsForName(
buildTarget: BuildTargetIdentifier,
name: String,
): Iterator[AbsolutePath] = {
val allowedBuildTargets = buildTargets.allInverseDependencies(buildTarget)
val visited = scala.collection.mutable.Set.empty[AbsolutePath]
val result = for {
(path, entry) <- tokenIndex.iterator
if allowedBuildTargets.contains(entry.id) &&
entry.bloom.mightContain(name)
sourcePath = AbsolutePath(path)
if !visited(sourcePath)
_ = visited.add(sourcePath)
if sourcePath.exists
} yield sourcePath

result
}

/**
* Return all paths to files which contain at least one symbol from isSymbol set.
*/
Expand Down

0 comments on commit af2ba5a

Please sign in to comment.