From af2ba5a0ec6378f9b88ca1362f4bd651877198d6 Mon Sep 17 00:00:00 2001 From: Katarzyna Marek Date: Wed, 20 Dec 2023 12:45:21 +0100 Subject: [PATCH] use pc as fallback when missing semanticdb [skip ci] --- .../scala/meta/internal/metals/Indexer.scala | 1 + .../internal/metals/MetalsLspService.scala | 69 +--------- .../internal/metals/ReferenceProvider.scala | 121 ++++++++++++++++-- 3 files changed, 115 insertions(+), 76 deletions(-) diff --git a/metals/src/main/scala/scala/meta/internal/metals/Indexer.scala b/metals/src/main/scala/scala/meta/internal/metals/Indexer.scala index 57ddb7b2006..c271e461056 100644 --- a/metals/src/main/scala/scala/meta/internal/metals/Indexer.scala +++ b/metals/src/main/scala/scala/meta/internal/metals/Indexer.scala @@ -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) { diff --git a/metals/src/main/scala/scala/meta/internal/metals/MetalsLspService.scala b/metals/src/main/scala/scala/meta/internal/metals/MetalsLspService.scala index 436057ab69d..7c14afdb80c 100644 --- a/metals/src/main/scala/scala/meta/internal/metals/MetalsLspService.scala +++ b/metals/src/main/scala/scala/meta/internal/metals/MetalsLspService.scala @@ -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 @@ -607,10 +606,10 @@ class MetalsLspService( semanticdbs, buffers, definitionProvider, - remote, trees, buildTargets, compilers, + scalaVersionSelector, ) private val syntheticHoverProvider: SyntheticHoverProvider = @@ -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)), @@ -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]] = { @@ -1600,7 +1542,8 @@ class MetalsLspService( } } if (results.nonEmpty) { - compileAndLookForNewReferences(params, results) + val path = params.getTextDocument.getUri.toAbsolutePath + compilations.cascadeCompileFiles(Seq(path)) } results } diff --git a/metals/src/main/scala/scala/meta/internal/metals/ReferenceProvider.scala b/metals/src/main/scala/scala/meta/internal/metals/ReferenceProvider.scala index b71566b5b78..9a3a67940ba 100644 --- a/metals/src/main/scala/scala/meta/internal/metals/ReferenceProvider.scala +++ b/metals/src/main/scala/scala/meta/internal/metals/ReferenceProvider.scala @@ -9,7 +9,9 @@ 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 @@ -17,20 +19,23 @@ 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( @@ -38,10 +43,10 @@ final class ReferenceProvider( semanticdbs: Semanticdbs, buffers: Buffers, definition: DefinitionProvider, - remote: RemoteLanguageServer, trees: Trees, buildTargets: BuildTargets, compilers: Compilers, + scalaVersionSelector: ScalaVersionSelector, )(implicit ec: ExecutionContext) extends SemanticdbFeatureProvider { @@ -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() @@ -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) @@ -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 @@ -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) } } @@ -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. */