diff --git a/metals/src/main/scala/scala/meta/internal/metals/DefinitionProvider.scala b/metals/src/main/scala/scala/meta/internal/metals/DefinitionProvider.scala index fc34c7975a4..599851d4887 100644 --- a/metals/src/main/scala/scala/meta/internal/metals/DefinitionProvider.scala +++ b/metals/src/main/scala/scala/meta/internal/metals/DefinitionProvider.scala @@ -4,6 +4,7 @@ import java.{util => ju} import scala.concurrent.ExecutionContext import scala.concurrent.Future +import scala.util.Try import scala.meta.inputs.Input import scala.meta.inputs.Position.Range @@ -59,7 +60,6 @@ final class DefinitionProvider( scalaVersionSelector: ScalaVersionSelector, saveDefFileToDisk: Boolean, sourceMapper: SourceMapper, - warnings: () => Warnings, )(implicit ec: ExecutionContext, rc: ReportContext) { private val fallback = new FallbackDefinitionProvider(trees, index) @@ -78,48 +78,77 @@ final class DefinitionProvider( val scaladocDefinitionProvider = new ScaladocDefinitionProvider(buffers, trees, destinationProvider) + private def isAmmonnite(path: AbsolutePath): Boolean = + path.isAmmoniteScript && buildTargets + .inverseSources(path) + .flatMap(buildTargets.targetData) + .exists(_.isAmmonite) + def definition( path: AbsolutePath, params: TextDocumentPositionParams, token: CancelToken, ): Future[DefinitionResult] = { - val fromSemanticdb = - semanticdbs().textDocument(path).documentIncludingStale - val fromSnapshot = fromSemanticdb match { - case Some(doc) => - definitionFromSnapshot(path, params, doc) - case _ => - DefinitionResult.empty - } - val fromCompilerOrSemanticdb = - fromSnapshot match { - case defn if defn.isEmpty && path.isScalaFilename => - compilers().definition(params, token) - case defn @ DefinitionResult(_, symbol, _, _, querySymbol) - if symbol != querySymbol && path.isScalaFilename => - compilers().definition(params, token).map { compilerDefn => - if (compilerDefn.isEmpty || compilerDefn.querySymbol == querySymbol) - defn - else compilerDefn.copy(semanticdb = defn.semanticdb) + val reportBuilder = new DefinitionProviderReportBuilder(path, params) + lazy val isScala3 = ScalaVersions.isScala3Version( + scalaVersionSelector.scalaVersionForPath(path) + ) + + def fromCompiler() = + if (path.isScalaFilename) { + compilers() + .definition(params, token) + .map(reportBuilder.setCompilerResult) + .map { + case res if res.isEmpty => Some(res) + case res => + val pathToDef = res.locations.asScala.head.getUri.toAbsolutePath + Some( + res.copy(semanticdb = + semanticdbs().textDocument(pathToDef).documentIncludingStale + ) + ) } - case defn => - if (fromSemanticdb.isEmpty) { - warnings().noSemanticdb(path) + } else Future.successful(None) + + def fromSemanticDb() = Future.successful { + semanticdbs() + .textDocument(path) + .documentIncludingStale + .map(definitionFromSnapshot(path, params, _)) + .map(reportBuilder.setSemanticDBResult) + } + + def fromScalaDoc() = Future.successful { + scaladocDefinitionProvider + .definition(path, params, isScala3) + .map(reportBuilder.setFoundScaladocDef) + } + + def fromFallback() = + Future.successful( + fallback + .search(path, params.getPosition(), isScala3, reportBuilder) + .map(reportBuilder.setFallbackResult) + ) + + val strategies: List[() => Future[Option[DefinitionResult]]] = + if (isAmmonnite(path)) + List(fromSemanticDb, fromCompiler, fromScalaDoc, fromFallback) + else List(fromCompiler, fromSemanticDb, fromScalaDoc, fromFallback) + + for { + result <- strategies.foldLeft(Future.successful(DefinitionResult.empty)) { + case (acc, next) => + acc.flatMap { + case res if res.isEmpty && !res.symbol.endsWith("/") => + next().map(_.getOrElse(res)) + case res => Future.successful(res) } - Future.successful(defn) } - - fromCompilerOrSemanticdb.map { definition => - if (definition.isEmpty && !definition.symbol.endsWith("/")) { - val isScala3 = - ScalaVersions.isScala3Version( - scalaVersionSelector.scalaVersionForPath(path) - ) - scaladocDefinitionProvider - .definition(path, params, isScala3) - .orElse(fallback.search(path, params.getPosition(), isScala3)) - .getOrElse(definition) - } else definition + } yield { + reportBuilder.build().foreach(rc.unsanitized.create(_)) + result } } @@ -508,3 +537,96 @@ class DestinationProvider( } } } + +class DefinitionProviderReportBuilder( + path: AbsolutePath, + params: TextDocumentPositionParams, +) { + private var compilerDefn: Option[DefinitionResult] = None + private var semanticDBDefn: Option[DefinitionResult] = None + + private var fallbackDefn: Option[DefinitionResult] = None + private var nonLocalGuesses: List[String] = List.empty + + private var foundScalaDocDef = false + + private var error: Option[Throwable] = None + + def setCompilerResult(result: DefinitionResult): DefinitionResult = { + compilerDefn = Some(result) + result + } + + def setSemanticDBResult(result: DefinitionResult): DefinitionResult = { + semanticDBDefn = Some(result) + result + } + + def setFallbackResult(result: DefinitionResult): DefinitionResult = { + fallbackDefn = Some(result) + result + } + + def setError(e: Throwable): Unit = { + error = Some(e) + } + + def setNonLocalGuesses(guesses: List[String]): Unit = { + nonLocalGuesses = guesses + } + + def setFoundScaladocDef(result: DefinitionResult): DefinitionResult = { + foundScalaDocDef = true + result + } + + def build(): Option[Report] = + compilerDefn match { + case Some(compilerDefn) if !foundScalaDocDef => + Some( + Report( + "empty-definition", + s"""|empty definition using pc, found symbol in pc: ${compilerDefn.querySymbol} + |${semanticDBDefn match { + case None => + s"semanticdb not found" + case Some(defn) if defn.isEmpty => + s"empty definition using semanticdb" + case Some(defn) => + s"found definition using semanticdb; symbol ${defn.symbol}" + }} + |${fallbackDefn.filterNot(_.isEmpty) match { + case None => + s"""|empty definition using fallback + |non-local guesses: + |${nonLocalGuesses.mkString("\t -", "\n\t -", "")} + |""" + case Some(defn) => + s"found definition using fallback; symbol ${defn.symbol}" + }} + |Document text: + | + |```scala + |${Try(path.readText).toOption.getOrElse("Failed to read text")} + |``` + |""".stripMargin, + s"empty definition using pc, found symbol in pc: ${compilerDefn.querySymbol}", + path = Some(path.toURI), + id = querySymbol.orElse( + Some(s"${path.toURI}:${params.getPosition().getLine()}") + ), + error = error, + ) + ) + case _ => None + } + + private def querySymbol: Option[String] = + compilerDefn.map(_.querySymbol) match { + case Some("") | None => + semanticDBDefn + .map(_.querySymbol) + .orElse(fallbackDefn.map(_.querySymbol)) + case res => res + } +} diff --git a/metals/src/main/scala/scala/meta/internal/metals/FallbackDefinitionProvider.scala b/metals/src/main/scala/scala/meta/internal/metals/FallbackDefinitionProvider.scala index c4d660bcc59..365ddbb5272 100644 --- a/metals/src/main/scala/scala/meta/internal/metals/FallbackDefinitionProvider.scala +++ b/metals/src/main/scala/scala/meta/internal/metals/FallbackDefinitionProvider.scala @@ -1,5 +1,7 @@ package scala.meta.internal.metals +import scala.util.control.NonFatal + import scala.meta.Term import scala.meta.Type import scala.meta._ @@ -32,144 +34,153 @@ class FallbackDefinitionProvider( 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) + reportBuilder: DefinitionProviderReportBuilder, + ): Option[DefinitionResult] = + try { + 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) + // 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 - }.flatten - - val standardScalaImports = guessObjectOrClass( - List("scala", "Predef") ++ proposedNameParts - ) - val fullyScopedName = - guessObjectOrClass(proposedNameParts) - - def findInIndex(proposedSymbol: String) = { - index - .definition(mtags.Symbol(proposedSymbol)) - // Make sure we don't return unrelated definitions - .filter { _.definitionSymbol.value == proposedSymbol } - } - val nonLocalGuesses = - (proposedImportedSymbols ++ fullyScopedName ++ standardScalaImports).distinct - .flatMap { proposedSymbol => - findInIndex(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 => findInIndex(proposedSymbol)) - .collectFirst { case Some(dfn) => - toDefinition(List(dfn)) - } - } + }.flatten - 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. " + val standardScalaImports = guessObjectOrClass( + List("scala", "Predef") ++ proposedNameParts ) + val fullyScopedName = + guessObjectOrClass(proposedNameParts) + + def findInIndex(proposedSymbol: String) = { + index + .definition(mtags.Symbol(proposedSymbol)) + // Make sure we don't return unrelated definitions + .filter { _.definitionSymbol.value == proposedSymbol } + } + val nonLocalCandidates = + (proposedImportedSymbols ++ fullyScopedName ++ standardScalaImports).distinct + + reportBuilder.setNonLocalGuesses(nonLocalCandidates) + + val nonLocalGuesses = nonLocalCandidates.flatMap(findInIndex) + + 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 => findInIndex(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 } - result - } - defResult.flatten - } + defResult.flatten + } catch { + case NonFatal(e) => + reportBuilder.setError(e) + None + } private def isInSelect(tree: Tree, range: LspRange): Boolean = tree match { case Type.Select(qual, _) if qual.pos.encloses(range) => true 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 2c2502fdabe..62c0bf137ba 100644 --- a/metals/src/main/scala/scala/meta/internal/metals/MetalsLspService.scala +++ b/metals/src/main/scala/scala/meta/internal/metals/MetalsLspService.scala @@ -304,7 +304,6 @@ abstract class MetalsLspService( scalaVersionSelector, saveDefFileToDisk = !clientConfig.isVirtualDocumentSupported(), sourceMapper, - () => warnings, ) val stacktraceAnalyzer: StacktraceAnalyzer = new StacktraceAnalyzer( diff --git a/metals/src/main/scala/scala/meta/internal/metals/TargetData.scala b/metals/src/main/scala/scala/meta/internal/metals/TargetData.scala index bb868f60409..eaa4aace70b 100644 --- a/metals/src/main/scala/scala/meta/internal/metals/TargetData.scala +++ b/metals/src/main/scala/scala/meta/internal/metals/TargetData.scala @@ -32,7 +32,7 @@ import ch.epfl.scala.bsp4j.SourceItemKind.FILE import ch.epfl.scala.bsp4j.WorkspaceBuildTargetsResult import org.eclipse.{lsp4j => l} -final class TargetData { +final class TargetData(val isAmmonite: Boolean = false) { val sourceItemsToBuildTarget : MMap[AbsolutePath, ConcurrentLinkedQueue[BuildTargetIdentifier]] = diff --git a/metals/src/main/scala/scala/meta/internal/metals/ammonite/Ammonite.scala b/metals/src/main/scala/scala/meta/internal/metals/ammonite/Ammonite.scala index fe53e2d40d4..a3506a26080 100644 --- a/metals/src/main/scala/scala/meta/internal/metals/ammonite/Ammonite.scala +++ b/metals/src/main/scala/scala/meta/internal/metals/ammonite/Ammonite.scala @@ -55,7 +55,7 @@ final class Ammonite( )(implicit ec: ExecutionContextExecutorService) extends Cancelable { - val buildTargetsData = new TargetData + val buildTargetsData = new TargetData(isAmmonite = true) def buildServer: Option[BuildServerConnection] = buildServer0 diff --git a/metals/src/main/scala/scala/meta/internal/rename/RenameProvider.scala b/metals/src/main/scala/scala/meta/internal/rename/RenameProvider.scala index da898edd00a..ac85c165d4c 100644 --- a/metals/src/main/scala/scala/meta/internal/rename/RenameProvider.scala +++ b/metals/src/main/scala/scala/meta/internal/rename/RenameProvider.scala @@ -7,6 +7,7 @@ import scala.concurrent.Future import scala.meta.Importee import scala.meta.Tree +import scala.meta.inputs.Input import scala.meta.internal.async.ConcurrentQueue import scala.meta.internal.implementation.ImplementationProvider import scala.meta.internal.metals.AdjustRange @@ -16,6 +17,7 @@ import scala.meta.internal.metals.Compilations import scala.meta.internal.metals.Compilers import scala.meta.internal.metals.DefinitionProvider import scala.meta.internal.metals.MetalsEnrichments._ +import scala.meta.internal.metals.PositionSyntax.XtensionPositionsScalafix import scala.meta.internal.metals.ReferenceProvider import scala.meta.internal.metals.ReportContext import scala.meta.internal.metals.TextEdits @@ -32,6 +34,7 @@ import scala.meta.internal.semanticdb.XtensionSemanticdbSymbolInformation import scala.meta.internal.{semanticdb => s} import scala.meta.io.AbsolutePath import scala.meta.pc.CancelToken +import scala.meta.tokens.Token.Ident import org.eclipse.lsp4j.Location import org.eclipse.lsp4j.MessageParams @@ -234,6 +237,7 @@ final class RenameProvider( if (parentSymbols.isEmpty) definition.locations.asScala .filter(_.getUri().isScalaOrJavaFilename) + .map(findDefinitionRage) else parentSymbols } companionRefs = companionReferences( @@ -331,6 +335,21 @@ final class RenameProvider( } } + private def findDefinitionRage(location: Location): Location = { + val adjustedPosition = for { + source <- location.getUri().toAbsolutePathSafe + tree <- trees.get(source) + pos <- location + .getRange() + .getStart() + .toMeta(Input.VirtualFile(source.toString(), tree.text)) + token <- tree.tokens.collectFirst { + case token: Ident if token.pos.contains(pos) => token + } + } yield token.pos.toLsp + adjustedPosition.map(new Location(location.getUri(), _)).getOrElse(location) + } + def runSave(): Future[Unit] = { val all = synchronized { ConcurrentQueue.pollAll(awaitingSave) diff --git a/mtags/src/main/scala-2/scala/meta/internal/pc/PcDefinitionProvider.scala b/mtags/src/main/scala-2/scala/meta/internal/pc/PcDefinitionProvider.scala index 0b49c51a36e..6b92bbc6212 100644 --- a/mtags/src/main/scala-2/scala/meta/internal/pc/PcDefinitionProvider.scala +++ b/mtags/src/main/scala-2/scala/meta/internal/pc/PcDefinitionProvider.scala @@ -87,13 +87,16 @@ class PcDefinitionProvider(val compiler: MetalsGlobal, params: OffsetParams) { symbol.pos.source.eq(unit.source) ) { val focused = symbol.pos.focus + val actualName = symbol.decodedName.stripSuffix("_=").trim val namePos = - if (symbol.name.startsWith("x$") && symbol.isSynthetic) focused.toLsp - else focused.withEnd(focused.start + symbol.name.length()).toLsp + if (symbol.name.startsWith("x$") && symbol.isSynthetic) focused + else focused.withEnd(focused.start + actualName.length()) + val adjusted = namePos.adjust(unit.source.content)._1 + DefinitionResultImpl( semanticdbSymbol(symbol), ju.Collections.singletonList( - new Location(params.uri().toString(), namePos) + new Location(params.uri().toString(), adjusted.toLsp) ) ) } else { diff --git a/tests/unit/src/test/scala/tests/DefinitionLspSuite.scala b/tests/unit/src/test/scala/tests/DefinitionLspSuite.scala index 074ae5f2f9a..b4e30c1f7f3 100644 --- a/tests/unit/src/test/scala/tests/DefinitionLspSuite.scala +++ b/tests/unit/src/test/scala/tests/DefinitionLspSuite.scala @@ -233,7 +233,7 @@ class DefinitionLspSuite |package a |object B/*L1*/ { | def main/*L2*/() = { - | println/*Predef.scala*/(A/*;A.scala:2;A.scala:3*/("John")) + | println/*Predef.scala*/(A/*A.scala:2*/("John")) | A/*A.scala:3*/.fun/*A.scala:5*/() | } |}