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: Improve heuritics used for fallback symbol search #6642

Merged
merged 6 commits into from
Aug 6, 2024
Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,6 @@ import java.{util => ju}
import scala.concurrent.ExecutionContext
import scala.concurrent.Future

import scala.meta.Term
import scala.meta.Type
import scala.meta.XtensionClassifiable
import scala.meta.inputs.Input
import scala.meta.inputs.Position.Range
import scala.meta.internal.metals.MetalsEnrichments._
Expand All @@ -28,13 +25,10 @@ import scala.meta.internal.semanticdb.Synthetic
import scala.meta.internal.semanticdb.TextDocument
import scala.meta.io.AbsolutePath
import scala.meta.pc.CancelToken
import scala.meta.tokens.Token

import ch.epfl.scala.bsp4j.BuildTargetIdentifier
import org.eclipse.lsp4j.Location
import org.eclipse.lsp4j.Position
import org.eclipse.lsp4j.SymbolInformation
import org.eclipse.lsp4j.SymbolKind
import org.eclipse.lsp4j.TextDocumentIdentifier
import org.eclipse.lsp4j.TextDocumentPositionParams

Expand Down Expand Up @@ -65,10 +59,10 @@ final class DefinitionProvider(
scalaVersionSelector: ScalaVersionSelector,
saveDefFileToDisk: Boolean,
sourceMapper: SourceMapper,
workspaceSearch: WorkspaceSymbolProvider,
warnings: () => Warnings,
)(implicit ec: ExecutionContext, rc: ReportContext) {

private val fallback = new FallbackDefinitionProvider(trees, index)
val destinationProvider = new DestinationProvider(
index,
buffers,
Expand Down Expand Up @@ -123,7 +117,7 @@ final class DefinitionProvider(
)
scaladocDefinitionProvider
.definition(path, params, isScala3)
.orElse(fromSearch(path, params.getPosition(), token))
.orElse(fallback.search(path, params.getPosition(), isScala3))
.getOrElse(definition)
} else {
definition
Expand All @@ -148,78 +142,6 @@ final class DefinitionProvider(
)
}

/**
* Tries to find an identifier token at the current position
* to use it for symbol search. This is the last possibility for
* finding the definition.
*
* @param path path of the current file
* @param pos position we are searching for
* @return possible definition locations based on exact symbol search
*/
def fromSearch(
path: AbsolutePath,
pos: Position,
token: CancelToken,
): Option[DefinitionResult] = {

val defResult = for {
sourceText <- buffers.get(path)
virtualFile = Input.VirtualFile(path.toURI.toString(), sourceText)
metaPos <- pos.toMeta(virtualFile)
tokens <- trees.tokenized(path)
ident <- tokens.collectFirst {
case id: Token.Ident if id.pos.encloses(metaPos) => id
}
} yield {
lazy val nameTree = trees.findLastEnclosingAt(path, pos)

// for sure is not a class/trait/enum if we access it via select
lazy val isInSelectPosition = nameTree.exists { name =>
name.parent.exists {
case Type.Select(qual, _) if nameTree.contains(qual) => true
case Term.Select(qual, _) if nameTree.contains(qual) => true
case _ => false
}
}

lazy val isInTypePosition = nameTree.exists(_.is[Type.Name])

def filterViaHeuristics(symbolInfo: SymbolInformation) = {
val kind = symbolInfo.getKind()
val isClassLike =
kind == SymbolKind.Class || kind == SymbolKind.Enum || kind == SymbolKind.Interface
if (isClassLike && isInSelectPosition) false
else if (kind == SymbolKind.Object && isInTypePosition) false
else true
}

val locs = workspaceSearch
.searchExactFrom(ident.value, path, token, Some(path))

val reducedGuesses =
if (locs.size > 1)
locs.filter(filterViaHeuristics)
else
locs

if (reducedGuesses.nonEmpty) {
scribe.warn(s"Using indexes to guess the definition of ${ident.value}")
Some(
DefinitionResult(
reducedGuesses.map(_.getLocation()).asJava,
ident.value,
None,
None,
ident.value,
)
)
} else None

}
defResult.flatten
}

def fromSymbol(
sym: String,
source: Option[AbsolutePath],
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
package scala.meta.internal.metals

import scala.meta.Term
import scala.meta.Type
import scala.meta._
import scala.meta.internal.metals.MetalsEnrichments._
import scala.meta.internal.mtags
import scala.meta.internal.mtags.GlobalSymbolIndex
import scala.meta.internal.parsing.Trees
import scala.meta.io.AbsolutePath
import scala.meta.tokens.Token

import org.eclipse.lsp4j.Location
import org.eclipse.lsp4j.Position
import org.eclipse.lsp4j.{Range => LspRange}

class FallbackDefinitionProvider(
trees: Trees,
index: GlobalSymbolIndex,
) {

/**
* Tries to find an identifier token at the current position to
* guess the symbol to find and then searches for it in the symbol index.
* This is the last possibility for finding the definition.
*
* @param path path of the current file
* @param pos position we are searching for
* @return possible definition locations based on exact symbol search
*/
def search(
path: AbsolutePath,
pos: Position,
isScala3: Boolean,
): Option[DefinitionResult] = {
val range = new LspRange(pos, pos)

val defResult = for {
tokens <- trees.tokenized(path)
ident <- tokens.collectFirst {
case id: Token.Ident if id.pos.encloses(range) => id
}
tree <- trees.get(path)
} yield {
lazy val nameTree = trees.findLastEnclosingAt(path, pos)

// for sure is not a class/trait/enum if we access it via select
lazy val isInSelectPosition =
nameTree.flatMap(_.parent).exists(isInSelect(_, range))

lazy val isInTypePosition = nameTree.exists(_.is[Type.Name])

def guessObjectOrClass(parts: List[String]) = {
val symbolPrefix = mtags.Symbol
.guessSymbolFromParts(parts, isScala3)
.value
if (isInSelectPosition) List(symbolPrefix + ".")
else if (isInTypePosition) List(symbolPrefix + "#")
else List(".", "#").map(ending => symbolPrefix + ending)
}

// Get all select parts to build symbol from it later
val proposedNameParts =
nameTree
.flatMap(_.parent)
.map {
case tree: Term.Select if nameTree.contains(tree.name) =>
nameFromSelect(tree, Nil)
case _ => List(ident.value)
}
.getOrElse(List(ident.value))

val currentPackageStatements = trees
.packageStatementsAtPosition(path, pos) match {
case None => List("_empty_")
case Some(value) =>
// generate packages from all the package statements
value.foldLeft(Seq.empty[String]) { case (pre, suffix) =>
if (pre.isEmpty) List(suffix) else pre :+ (pre.last + "." + suffix)
}
}

val proposedCurrentPackageSymbols =
currentPackageStatements.flatMap(pkg =>
guessObjectOrClass(
(pkg.split("\\.").toList ++ proposedNameParts)
)
)

// First name in select is the one that must be imported or in scope
val probablyImported = proposedNameParts.headOption.getOrElse(ident.value)

// Search for imports that match the current symbol
val proposedImportedSymbols =
tree.collect {
case imp @ Import(importers)
// imports should be in the same scope as the current position
if imp.parent.exists(_.pos.encloses(range)) =>
importers.collect { case Importer(ref: Term, p) =>
val packageSyntax = ref.toString.split("\\.").toList
p.collect {
case Importee.Name(name) if name.value == probablyImported =>
guessObjectOrClass(packageSyntax ++ proposedNameParts)

case Importee.Rename(name, renamed)
if renamed.value == probablyImported =>
guessObjectOrClass(
packageSyntax ++ (name.value +: proposedNameParts.drop(1))
)
case _: Importee.Wildcard =>
guessObjectOrClass(packageSyntax ++ proposedNameParts)

}.flatten
}.flatten
}.flatten

val fullyScopedName =
guessObjectOrClass(proposedNameParts)

val nonLocalGuesses =
(proposedImportedSymbols ++ fullyScopedName).distinct
.flatMap { proposedSymbol =>
index.definition(mtags.Symbol(proposedSymbol))
}

def toDefinition(guesses: List[mtags.SymbolDefinition]) = {
DefinitionResult(
guesses
.flatMap(guess =>
guess.range.map(range =>
new Location(guess.path.toURI.toString(), range.toLsp)
)
)
.asJava,
ident.value,
None,
None,
ident.value,
)
}
val result = if (nonLocalGuesses.nonEmpty) {
Some(toDefinition(nonLocalGuesses))
} else {
// otherwise might be symbol in a local package, starting from enclosing
proposedCurrentPackageSymbols.reverse
.map(proposedSymbol => index.definition(mtags.Symbol(proposedSymbol)))
.collectFirst { case Some(dfn) => toDefinition(List(dfn)) }
}

result.foreach { _ =>
scribe.warn(
s"Could not find '${ident.value}' using presentation compiler nor semanticdb. " +
s"Trying to guess the definition using available information from local class context. "
)
}
result
}

defResult.flatten
}

private def isInSelect(tree: Tree, range: LspRange): Boolean = tree match {
case Type.Select(qual, _) if qual.pos.encloses(range) => true
case Term.Select(qual, _) if qual.pos.encloses(range) => true
case Term.Select(_, _) => tree.parent.exists(isInSelect(_, range))
case _: Importer => true
case _ => false
}

private def nameFromSelect(tree: Tree, acc: List[String]): List[String] = {
tree match {
case Term.Select(qualifier, name) =>
nameFromSelect(qualifier, name.value +: acc)
case Term.Name(value) => value +: acc
}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ final class FileSystemSemanticdbs(
}
} yield {
if (!targetroot.exists)
scribe.warn(s"Target root $targetroot does not exist")
scribe.debug(s"Target root $targetroot does not exist")
val optScalaVersion =
if (file.toLanguage.isJava) None
else buildTargets.scalaTarget(buildTarget).map(_.scalaVersion)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -296,7 +296,6 @@ abstract class MetalsLspService(
scalaVersionSelector,
saveDefFileToDisk = !clientConfig.isVirtualDocumentSupported(),
sourceMapper,
workspaceSymbols,
() => warnings,
)

Expand Down
Loading
Loading