diff --git a/metals-bench/src/main/scala/bench/ClasspathOnlySymbolSearch.scala b/metals-bench/src/main/scala/bench/ClasspathOnlySymbolSearch.scala index 332f3ff11ea..ceed77f0dae 100644 --- a/metals-bench/src/main/scala/bench/ClasspathOnlySymbolSearch.scala +++ b/metals-bench/src/main/scala/bench/ClasspathOnlySymbolSearch.scala @@ -1,6 +1,8 @@ package bench +import java.{util => ju} import java.util.Optional +import org.eclipse.lsp4j.Location import scala.meta.internal.metals.ClasspathSearch import scala.meta.internal.metals.WorkspaceSymbolQuery import scala.meta.pc.SymbolDocumentation @@ -17,6 +19,8 @@ class ClasspathOnlySymbolSearch(classpath: ClasspathSearch) override def documentation(symbol: String): Optional[SymbolDocumentation] = Optional.empty() + def definition(symbol: String): ju.List[Location] = ju.Collections.emptyList() + override def search( query: String, buildTargetIdentifier: String, diff --git a/metals/src/main/scala/scala/meta/internal/metals/Compilers.scala b/metals/src/main/scala/scala/meta/internal/metals/Compilers.scala index f0569af2e0f..9ef1bc810c3 100644 --- a/metals/src/main/scala/scala/meta/internal/metals/Compilers.scala +++ b/metals/src/main/scala/scala/meta/internal/metals/Compilers.scala @@ -4,9 +4,11 @@ import ch.epfl.scala.bsp4j.BuildTargetIdentifier import ch.epfl.scala.bsp4j.CompileReport import ch.epfl.scala.bsp4j.ScalaBuildTarget import ch.epfl.scala.bsp4j.ScalacOptionsItem +import java.{util => ju} import java.util.Collections import java.util.Optional import java.util.concurrent.ScheduledExecutorService +import org.eclipse.lsp4j.Location import org.eclipse.lsp4j.CompletionItem import org.eclipse.lsp4j.CompletionList import org.eclipse.lsp4j.CompletionParams @@ -153,6 +155,15 @@ class Compilers( CompilerOffsetParams(pos.input.syntax, pos.input.text, pos.start, token) ) } + def definition( + params: TextDocumentPositionParams, + token: CancelToken + ): Option[ju.List[Location]] = + withPC(params, None) { (pc, pos) => + pc.definition( + CompilerOffsetParams(pos.input.syntax, pos.input.text, pos.start, token) + ) + } def signatureHelp( params: TextDocumentPositionParams, token: CancelToken, @@ -214,7 +225,9 @@ class Compilers( )(fn: (PresentationCompiler, Position) => T): Option[T] = { val path = params.getTextDocument.getUri.toAbsolutePath loadCompiler(path, interactiveSemanticdbs).map { compiler => - val input = path.toInputFromBuffers(buffers) + val input = path + .toInputFromBuffers(buffers) + .copy(path = params.getTextDocument.getUri()) val pos = params.getPosition.toMeta(input) val result = fn(compiler, pos) result 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 8d1280a030d..c5e0dda786f 100644 --- a/metals/src/main/scala/scala/meta/internal/metals/DefinitionProvider.scala +++ b/metals/src/main/scala/scala/meta/internal/metals/DefinitionProvider.scala @@ -1,7 +1,10 @@ package scala.meta.internal.metals +import java.{util => ju} import java.util.Collections import org.eclipse.lsp4j.TextDocumentPositionParams +import org.eclipse.lsp4j.Location +import scala.meta.pc.CancelToken import scala.meta.inputs.Input import scala.meta.internal.metals.MetalsEnrichments._ import scala.meta.internal.mtags.GlobalSymbolIndex @@ -36,23 +39,43 @@ final class DefinitionProvider( semanticdbs: Semanticdbs, icons: Icons, statusBar: StatusBar, - warnings: Warnings + warnings: Warnings, + compilers: () => Compilers ) { def definition( path: AbsolutePath, - params: TextDocumentPositionParams + params: TextDocumentPositionParams, + token: CancelToken ): DefinitionResult = { - val result = semanticdbs.textDocument(path) - result.documentIncludingStale match { + semanticdbs.textDocument(path).documentIncludingStale match { case Some(doc) => definitionFromSnapshot(path, params, doc) case _ => - warnings.noSemanticdb(path) - DefinitionResult.empty + val fromCompilers = + compilers() + .definition(params, token) + .getOrElse(Collections.emptyList()) + if (!fromCompilers.isEmpty()) { + DefinitionResult( + fromCompilers, + "", + None, + None + ) + } else { + warnings.noSemanticdb(path) + DefinitionResult.empty + } } } + def fromSymbol(sym: String): ju.List[Location] = + DefinitionDestination.fromSymbol(sym).flatMap(_.toResult) match { + case None => ju.Collections.emptyList() + case Some(destination) => destination.locations + } + def positionOccurrence( source: AbsolutePath, dirtyPosition: TextDocumentPositionParams, diff --git a/metals/src/main/scala/scala/meta/internal/metals/MetalsLanguageServer.scala b/metals/src/main/scala/scala/meta/internal/metals/MetalsLanguageServer.scala index 7b9aa86b333..cd8f35e7fc9 100644 --- a/metals/src/main/scala/scala/meta/internal/metals/MetalsLanguageServer.scala +++ b/metals/src/main/scala/scala/meta/internal/metals/MetalsLanguageServer.scala @@ -15,6 +15,7 @@ import java.net.URLClassLoader import java.nio.charset.Charset import scala.meta.internal.semanticdb.Scala._ +import scala.meta.pc.CancelToken import java.nio.charset.StandardCharsets import java.nio.file._ import java.util @@ -258,7 +259,8 @@ class MetalsLanguageServer( semanticdbs, config.icons, statusBar, - warnings + warnings, + () => compilers ) formattingProvider = new FormattingProvider( workspace, @@ -306,7 +308,11 @@ class MetalsLanguageServer( () => userConfig, buildTargets, buffers, - new MetalsSymbolSearch(symbolDocs, workspaceSymbols), + new MetalsSymbolSearch( + symbolDocs, + workspaceSymbols, + definitionProvider + ), embedded, statusBar, sh @@ -676,8 +682,8 @@ class MetalsLanguageServer( def definition( position: TextDocumentPositionParams ): CompletableFuture[util.List[Location]] = - CancelTokens { _ => - definitionResult(position).locations + CancelTokens { token => + definitionResult(position, token).locations } @JsonRequest("textDocument/typeDefinition") @@ -1410,12 +1416,13 @@ class MetalsLanguageServer( * The resolved symbol is used for testing purposes only. */ def definitionResult( - position: TextDocumentPositionParams + position: TextDocumentPositionParams, + token: CancelToken = EmptyCancelToken ): DefinitionResult = { val source = position.getTextDocument.getUri.toAbsolutePath if (source.toLanguage.isScala) { val result = timedThunk("definition", config.statistics.isDefinition)( - definitionProvider.definition(source, position) + definitionProvider.definition(source, position, token) ) // Record what build target this dependency source (if any) was jumped from, // needed to know what classpath to compile the dependency source with. diff --git a/metals/src/main/scala/scala/meta/internal/metals/MetalsSymbolSearch.scala b/metals/src/main/scala/scala/meta/internal/metals/MetalsSymbolSearch.scala index cbc0b866059..e010046d114 100644 --- a/metals/src/main/scala/scala/meta/internal/metals/MetalsSymbolSearch.scala +++ b/metals/src/main/scala/scala/meta/internal/metals/MetalsSymbolSearch.scala @@ -1,7 +1,9 @@ package scala.meta.internal.metals -import ch.epfl.scala.bsp4j.BuildTargetIdentifier +import java.{util => ju} +import org.eclipse.lsp4j.Location import java.util.Optional +import ch.epfl.scala.bsp4j.BuildTargetIdentifier import scala.meta.pc.SymbolDocumentation import scala.meta.pc.SymbolSearch import scala.meta.pc.SymbolSearchVisitor @@ -11,11 +13,16 @@ import scala.meta.pc.SymbolSearchVisitor */ class MetalsSymbolSearch( docs: Docstrings, - wsp: WorkspaceSymbolProvider + wsp: WorkspaceSymbolProvider, + defn: DefinitionProvider ) extends SymbolSearch { override def documentation(symbol: String): Optional[SymbolDocumentation] = docs.documentation(symbol) + def definition(symbol: String): ju.List[Location] = { + defn.fromSymbol(symbol) + } + override def search( query: String, buildTargetIdentifier: String, diff --git a/mtags-interfaces/src/main/java/scala/meta/pc/PresentationCompiler.java b/mtags-interfaces/src/main/java/scala/meta/pc/PresentationCompiler.java index 9c7dec6e0a7..9e7ff4fcd7d 100644 --- a/mtags-interfaces/src/main/java/scala/meta/pc/PresentationCompiler.java +++ b/mtags-interfaces/src/main/java/scala/meta/pc/PresentationCompiler.java @@ -1,5 +1,6 @@ package scala.meta.pc; +import org.eclipse.lsp4j.Location; import org.eclipse.lsp4j.CompletionItem; import org.eclipse.lsp4j.CompletionList; import org.eclipse.lsp4j.Hover; @@ -55,6 +56,11 @@ public abstract class PresentationCompiler { */ public abstract Optional hover(OffsetParams params); + /** + * Returns the definition of the symbol at the given position. + */ + public abstract List definition(OffsetParams params); + /** * Returns the Protobuf byte array representation of a SemanticDB TextDocument for the given source. */ diff --git a/mtags-interfaces/src/main/java/scala/meta/pc/SymbolSearch.java b/mtags-interfaces/src/main/java/scala/meta/pc/SymbolSearch.java index d17db44d53c..a1078bb27e9 100644 --- a/mtags-interfaces/src/main/java/scala/meta/pc/SymbolSearch.java +++ b/mtags-interfaces/src/main/java/scala/meta/pc/SymbolSearch.java @@ -1,5 +1,7 @@ package scala.meta.pc; +import org.eclipse.lsp4j.Location; +import java.util.List; import java.util.Optional; /** @@ -12,6 +14,11 @@ public interface SymbolSearch { */ Optional documentation(String symbol); + /** + * Returns the definition of this symbol, if any. + */ + List definition(String symbol); + /** * Runs fuzzy symbol search for the given query. * diff --git a/mtags/src/main/scala/scala/meta/internal/mtags/MtagsEnrichments.scala b/mtags/src/main/scala/scala/meta/internal/mtags/MtagsEnrichments.scala index c60ccc20aac..8a671e2cb03 100644 --- a/mtags/src/main/scala/scala/meta/internal/mtags/MtagsEnrichments.scala +++ b/mtags/src/main/scala/scala/meta/internal/mtags/MtagsEnrichments.scala @@ -14,6 +14,7 @@ import org.eclipse.lsp4j.MarkupContent import org.eclipse.{lsp4j => l} import scala.annotation.tailrec import scala.collection.AbstractIterator +import scala.meta.pc.OffsetParams import scala.meta.inputs.Input import scala.meta.inputs.Position import scala.meta.internal.io.FileIO @@ -292,4 +293,19 @@ trait MtagsEnrichments { ) } } + implicit class XtensionOffsetParams(params: OffsetParams) { + def isDelimiter: Boolean = { + params.offset() < 0 || + params.offset() >= params.text().length || + (params.text().charAt(params.offset()) match { + case '(' | ')' | '{' | '}' | '[' | ']' | ',' | '=' | '.' => true + case _ => false + }) + } + def isWhitespace: Boolean = { + params.offset() < 0 || + params.offset() >= params.text().length || + params.text().charAt(params.offset()).isWhitespace + } + } } diff --git a/mtags/src/main/scala/scala/meta/internal/pc/Completions.scala b/mtags/src/main/scala/scala/meta/internal/pc/Completions.scala index a40a4adcd27..ba6a5f98c8e 100644 --- a/mtags/src/main/scala/scala/meta/internal/pc/Completions.scala +++ b/mtags/src/main/scala/scala/meta/internal/pc/Completions.scala @@ -1,5 +1,6 @@ package scala.meta.internal.pc +import java.net.URI import java.lang.StringBuilder import org.eclipse.{lsp4j => l} import scala.meta.internal.semanticdb.Scala._ @@ -874,9 +875,10 @@ trait Completions { this: MetalsGlobal => override def contribute: List[Member] = { try { val name = Paths - .get(pos.source.file.name.stripSuffix(".scala")) + .get(URI.create(pos.source.file.name)) .getFileName() .toString() + .stripSuffix(".scala") val isTermName = toplevel.name.isTermName val siblings = pkg.stats.count { case d: DefTree => diff --git a/mtags/src/main/scala/scala/meta/internal/pc/EmptySymbolSearch.scala b/mtags/src/main/scala/scala/meta/internal/pc/EmptySymbolSearch.scala index 34c41d3b1fa..e65a1a42a42 100644 --- a/mtags/src/main/scala/scala/meta/internal/pc/EmptySymbolSearch.scala +++ b/mtags/src/main/scala/scala/meta/internal/pc/EmptySymbolSearch.scala @@ -1,6 +1,8 @@ package scala.meta.internal.pc import java.util.Optional +import java.{util => ju} +import org.eclipse.lsp4j.Location import scala.meta.pc.SymbolDocumentation import scala.meta.pc.SymbolSearch import scala.meta.pc.SymbolSearchVisitor @@ -14,6 +16,10 @@ object EmptySymbolSearch extends SymbolSearch { SymbolSearch.Result.COMPLETE } + def definition(symbol: String): ju.List[Location] = { + ju.Collections.emptyList() + } + override def documentation(symbol: String): Optional[SymbolDocumentation] = Optional.empty() } diff --git a/mtags/src/main/scala/scala/meta/internal/pc/HoverProvider.scala b/mtags/src/main/scala/scala/meta/internal/pc/HoverProvider.scala index d2755c15953..ac80b209858 100644 --- a/mtags/src/main/scala/scala/meta/internal/pc/HoverProvider.scala +++ b/mtags/src/main/scala/scala/meta/internal/pc/HoverProvider.scala @@ -12,9 +12,7 @@ class HoverProvider(val compiler: MetalsGlobal, params: OffsetParams) { import compiler._ def hover(): Option[Hover] = { - if (params.offset() < 0 || - params.offset() >= params.text().length || - params.text().charAt(params.offset()).isWhitespace) { + if (params.isWhitespace) { None } else { val unit = addCompilationUnit( @@ -236,7 +234,7 @@ class HoverProvider(val compiler: MetalsGlobal, params: OffsetParams) { } val typedTree = typedTreeAt(pos) typedTree match { - case Import(qual, _) if qual.pos.includes(pos) => + case Import(qual, se) if qual.pos.includes(pos) => loop(qual) case Apply(fun, args) if !fun.pos.includes(pos) && diff --git a/mtags/src/main/scala/scala/meta/internal/pc/MetalsGlobal.scala b/mtags/src/main/scala/scala/meta/internal/pc/MetalsGlobal.scala index ae7f43cab9d..250ae23b658 100644 --- a/mtags/src/main/scala/scala/meta/internal/pc/MetalsGlobal.scala +++ b/mtags/src/main/scala/scala/meta/internal/pc/MetalsGlobal.scala @@ -89,17 +89,21 @@ class MetalsGlobal( } def printPretty(pos: sourcecode.Text[Position]): Unit = { - import scala.meta.internal.metals.PositionSyntax._ - val input = scala.meta.Input.String(new String(pos.value.source.content)) - val (start, end) = - if (pos.value.isRange) { - (pos.value.start, pos.value.end) - } else { - (pos.value.point, pos.value.point) - } - val range = - scala.meta.Position.Range(input, start, end) - println(range.formatMessage("info", pos.source)) + if (pos.value == null || pos.value == NoPosition) { + println(pos.value.toString()) + } else { + import scala.meta.internal.metals.PositionSyntax._ + val input = scala.meta.Input.String(new String(pos.value.source.content)) + val (start, end) = + if (pos.value.isRange) { + (pos.value.start, pos.value.end) + } else { + (pos.value.point, pos.value.point) + } + val range = + scala.meta.Position.Range(input, start, end) + println(range.formatMessage("info", pos.source)) + } } def pretty(pos: Position): String = { diff --git a/mtags/src/main/scala/scala/meta/internal/pc/PcDefinitionProvider.scala b/mtags/src/main/scala/scala/meta/internal/pc/PcDefinitionProvider.scala new file mode 100644 index 00000000000..9a7c682fa72 --- /dev/null +++ b/mtags/src/main/scala/scala/meta/internal/pc/PcDefinitionProvider.scala @@ -0,0 +1,112 @@ +package scala.meta.internal.pc + +import java.{util => ju} +import org.eclipse.lsp4j.Location +import scala.meta.pc.OffsetParams +import scala.meta.internal.semanticdb.Scala._ +import scala.meta.internal.mtags.MtagsEnrichments._ + +class PcDefinitionProvider(val compiler: MetalsGlobal, params: OffsetParams) { + import compiler._ + def definition(): ju.List[Location] = { + if (params.isWhitespace || params.isDelimiter) { + ju.Collections.emptyList() + } else { + val unit = addCompilationUnit( + params.text(), + params.filename(), + None + ) + val pos = unit.position(params.offset()) + val tree = definitionTypedTreeAt(pos) + if (tree.symbol == null || + tree.symbol == NoSymbol || + tree.symbol.hasPackageFlag || + tree.symbol.isErroneous) { + ju.Collections.emptyList() + } else if (tree.symbol.pos != null && + tree.symbol.pos.isDefined && + tree.symbol.pos.source.eq(unit.source)) { + ju.Collections.singletonList( + new Location(params.filename(), tree.symbol.pos.toLSP) + ) + } else { + val res = new ju.ArrayList[Location]() + tree.symbol.alternatives.foreach { alternative => + val sym = semanticdbSymbol(alternative) + if (sym.isGlobal) { + res.addAll(search.definition(sym)) + } + } + res + } + } + } + + def definitionTypedTreeAt(pos: Position): Tree = { + def loop(tree: Tree): Tree = { + tree match { + case Select(qualifier, name) => + if (name == termNames.apply && + qualifier.pos.includes(pos)) { + if (definitions.isFunctionSymbol(tree.symbol.owner)) loop(qualifier) + else if (definitions.isTupleSymbol(tree.symbol.owner)) { + EmptyTree + } else { + tree + } + } else { + tree + } + case Apply(sel @ Select(New(_), termNames.CONSTRUCTOR), args) + if sel.pos != null && + sel.pos.isRange && + !sel.pos.includes(pos) => + // Position is on `new` keyword + EmptyTree + case TreeApply(fun, _) + if fun.pos != null && + fun.pos.isDefined && + fun.pos.point == tree.pos.point && + definitions.isTupleSymbol(tree.symbol.owner.companionClass) => + EmptyTree + case _ => tree + } + } + val typedTree = typedTreeAt(pos) + val tree0 = typedTree match { + case sel @ Select(qual, _) if sel.tpe == ErrorType => qual + case Import(expr, _) => expr + case t => t + } + val context = doLocateContext(pos) + val shouldTypeQualifier = tree0.tpe match { + case null => true + case mt: MethodType => mt.isImplicit + case pt: PolyType => isImplicitMethodType(pt.resultType) + case _ => false + } + def selectQual(tree: Tree): Tree = tree match { + case Select(qual, _) if qual.pos.includes(pos) => loop(qual) + case t => t + } + // TODO: guard with try/catch to deal with ill-typed qualifiers. + val tree = + if (shouldTypeQualifier) analyzer.newTyper(context).typedQualifier(tree0) + else tree0 + typedTree match { + case Import(expr, selectors) => + EmptyTree + case sel @ Select(qualifier, name) if sel.tpe == ErrorType => + val symbols = tree.tpe.member(name) + typedTree.setSymbol(symbols) + case defn: DefTree if !defn.namePos.includes(pos) => + EmptyTree + case _: Template => + EmptyTree + case _ => + loop(tree) + + } + } +} diff --git a/mtags/src/main/scala/scala/meta/internal/pc/ScalaPresentationCompiler.scala b/mtags/src/main/scala/scala/meta/internal/pc/ScalaPresentationCompiler.scala index 52ed9d02a00..1c06be231ce 100644 --- a/mtags/src/main/scala/scala/meta/internal/pc/ScalaPresentationCompiler.scala +++ b/mtags/src/main/scala/scala/meta/internal/pc/ScalaPresentationCompiler.scala @@ -1,5 +1,6 @@ package scala.meta.internal.pc +import java.{util => ju} import java.io.File import java.nio.file.Path import java.util @@ -7,6 +8,7 @@ import java.util.Optional import java.util.concurrent.ExecutorService import java.util.concurrent.ScheduledExecutorService import java.util.logging.Logger +import org.eclipse.lsp4j.Location import org.eclipse.lsp4j.CompletionItem import org.eclipse.lsp4j.CompletionList import org.eclipse.lsp4j.Hover @@ -114,6 +116,15 @@ case class ScalaPresentationCompiler( Optional.ofNullable(new HoverProvider(global, params).hover().orNull) } + def definition(params: OffsetParams): ju.List[Location] = { + access.withNonCancelableCompiler( + ju.Collections.emptyList[Location](), + params.token + ) { global => + new PcDefinitionProvider(global, params).definition() + } + } + override def semanticdbTextDocument( filename: String, code: String diff --git a/project/project/plugins.sbt b/project/project/plugins.sbt index 689483f3961..76801d043dd 100644 --- a/project/project/plugins.sbt +++ b/project/project/plugins.sbt @@ -1 +1 @@ -addSbtPlugin("io.get-coursier" % "sbt-coursier" % "1.1.0-M13-2") +addSbtPlugin("io.get-coursier" % "sbt-coursier" % "1.1.0-M13-4") diff --git a/tests/cross/src/main/scala/tests/BaseCompletionSuite.scala b/tests/cross/src/main/scala/tests/BaseCompletionSuite.scala index a2e06c3927f..0e0fb933539 100644 --- a/tests/cross/src/main/scala/tests/BaseCompletionSuite.scala +++ b/tests/cross/src/main/scala/tests/BaseCompletionSuite.scala @@ -31,7 +31,7 @@ abstract class BaseCompletionSuite extends BasePCSuite { ): Seq[CompletionItem] = { val (code, offset) = params(original) val result = resolvedCompletions( - CompilerOffsetParams(filename, code, offset, cancelToken) + CompilerOffsetParams("file:/" + filename, code, offset, cancelToken) ) result.getItems.asScala.sortBy(_.getSortText) } diff --git a/tests/cross/src/main/scala/tests/BasePCSuite.scala b/tests/cross/src/main/scala/tests/BasePCSuite.scala index f143bf314d5..38e76ab5a8a 100644 --- a/tests/cross/src/main/scala/tests/BasePCSuite.scala +++ b/tests/cross/src/main/scala/tests/BasePCSuite.scala @@ -41,7 +41,8 @@ abstract class BasePCSuite extends BaseSuite { val search = new TestingSymbolSearch( ClasspathSearch.fromClasspath(myclasspath, _ => 0), new Docstrings(index), - workspace + workspace, + index ) val pc = new ScalaPresentationCompiler() .withSearch(search) diff --git a/tests/cross/src/main/scala/tests/pc/BasePcDefinitionSuite.scala b/tests/cross/src/main/scala/tests/pc/BasePcDefinitionSuite.scala new file mode 100644 index 00000000000..2569bd8ca85 --- /dev/null +++ b/tests/cross/src/main/scala/tests/pc/BasePcDefinitionSuite.scala @@ -0,0 +1,61 @@ +package tests.pc + +import tests.BasePCSuite +import scala.meta.internal.metals.CompilerOffsetParams +import org.eclipse.lsp4j.TextEdit +import org.eclipse.{lsp4j => l} +import tests.TextEdits +import scala.collection.JavaConverters._ +import scala.meta.internal.mtags.MtagsEnrichments._ + +abstract class BasePcDefinitionSuite extends BasePCSuite { + def check( + name: String, + original: String + ): Unit = { + test(name) { + val noRange = original + .replaceAllLiterally("<<", "") + .replaceAllLiterally(">>", "") + val uri = "A.scala" + val (code, offset) = params(noRange, uri) + import scala.meta.inputs.Position + import scala.meta.inputs.Input + val offsetRange = Position.Range(Input.String(code), offset, offset).toLSP + val locations = pc.definition( + CompilerOffsetParams(uri, code, offset) + ) + val edits = locations.asScala.toList.flatMap { location => + if (location.getUri() == uri) { + List( + new TextEdit( + new l.Range( + location.getRange().getStart(), + location.getRange().getStart() + ), + "<<" + ), + new TextEdit( + new l.Range( + location.getRange().getEnd(), + location.getRange().getEnd() + ), + ">>" + ) + ) + } else { + val filename = location.getUri() + val comment = s"/*$filename*/" + if (code.contains(comment)) { + Nil + } else { + List(new TextEdit(offsetRange, comment)) + } + } + } + val obtained = TextEdits.applyEdits(code, edits) + val expected = original.replaceAllLiterally("@@", "") + assertNoDiff(obtained, expected) + } + } +} diff --git a/tests/cross/src/test/scala/tests/pc/PcDefinitionSuite.scala b/tests/cross/src/test/scala/tests/pc/PcDefinitionSuite.scala new file mode 100644 index 00000000000..4b42cce96a8 --- /dev/null +++ b/tests/cross/src/test/scala/tests/pc/PcDefinitionSuite.scala @@ -0,0 +1,174 @@ +package tests.pc + +object PcDefinitionSuite extends BasePcDefinitionSuite { + + override def beforeAll(): Unit = { + indexJDK() + indexScalaLibrary() + } + + check( + "basic", + """| + |object Main { + | val <<>>abc = 42 + | println(a@@bc) + |} + |""".stripMargin + ) + + check( + "for", + """| + |object Main { + | for { + | <> <- List(1) + | y <- 1.to(x) + | z = y + x + | if y < @@x + | } yield y + |} + |""".stripMargin + ) + + check( + "for-flatMap", + """| + |object Main { + | for { + | x /*scala/Option#flatMap(). Option.scala*/@@<- Option(1) + | y <- Option(x) + | } yield y + |} + |""".stripMargin + ) + + check( + "for-map", + """| + |object Main { + | for { + | x <- Option(1) + | y /*scala/Option#map(). Option.scala*/@@<- Option(x) + | } yield y + |} + |""".stripMargin + ) + + check( + "for-withFilter", + """| + |object Main { + | for { + | x <- Option(1) + | y <- Option(x) + | /*scala/Option#withFilter(). Option.scala*/@@if y > 2 + | } yield y + |} + |""".stripMargin + ) + + check( + "function", + """| + |object Main { + | val <<>>increment: Int => Int = _ + 2 + | incre@@ment(1) + |} + |""".stripMargin + ) + + check( + "tuple", + // assert we don't go to `Tuple2.scala` + """| + |object Main { + | @@(1, 2) + |} + |""".stripMargin + ) + + check( + "apply", + """| + |object Main { + | /*scala/collection/immutable/List.apply(). List.scala*/@@List(1) + |} + |""".stripMargin + ) + + check( + "error", + """| + |object Main { + | /*scala/Predef.assert(+1). Predef.scala*//*scala/Predef.assert(). Predef.scala*/@@assert + |} + |""".stripMargin + ) + + check( + "new", + """| + |object Main { + | ne@@w java.io.File("") + |} + |""".stripMargin + ) + + check( + "extends", + """| + |object Main ex@@tends java.io.Serializable { + |} + |""".stripMargin + ) + + // NOTE(olafur): we don't provide navigation in imports for now. + check( + "import1", + """| + |import scala.concurrent.@@Future + |object Main { + |} + |""".stripMargin + ) + check( + "import2", + """| + |imp@@ort scala.concurrent.Future + |object Main { + |} + |""".stripMargin + ) + + check( + "named-arg-local", + """| + |object Main { + | <> + | + | foo(a@@rg = 42) + |} + |""".stripMargin + ) + + check( + "named-arg-global", + // NOTE(olafur) ideally we should navigate to the parameter symbol instead of the + // enclosing method symbol, but I can live with this behavior. + """| + |object Main { + | assert(/*scala/Predef.assert(). Predef.scala*/@@assertion = true) + |} + |""".stripMargin + ) + + check( + "symbolic-infix", + """| + |object Main { + | val lst = 1 /*scala/collection/immutable/List#`::`(). List.scala*/@@:: Nil + |} + |""".stripMargin + ) +} diff --git a/tests/mtest/src/main/scala/tests/TestingSymbolSearch.scala b/tests/mtest/src/main/scala/tests/TestingSymbolSearch.scala index b59634bc5d5..c43e68c2ce1 100644 --- a/tests/mtest/src/main/scala/tests/TestingSymbolSearch.scala +++ b/tests/mtest/src/main/scala/tests/TestingSymbolSearch.scala @@ -1,5 +1,7 @@ package tests +import java.{util => ju} +import org.eclipse.lsp4j.Location import java.util.Optional import scala.meta.internal.metals.ClasspathSearch import scala.meta.internal.metals.Docstrings @@ -7,6 +9,7 @@ import scala.meta.internal.metals.WorkspaceSymbolQuery import scala.meta.pc.SymbolDocumentation import scala.meta.pc.SymbolSearch import scala.meta.pc.SymbolSearchVisitor +import scala.meta.internal.mtags.OnDemandSymbolIndex /** * Implementation of `SymbolSearch` for testing purposes. @@ -16,12 +19,32 @@ import scala.meta.pc.SymbolSearchVisitor class TestingSymbolSearch( classpath: ClasspathSearch = ClasspathSearch.empty, docs: Docstrings = Docstrings.empty, - workspace: TestingWorkspaceSearch = TestingWorkspaceSearch.empty + workspace: TestingWorkspaceSearch = TestingWorkspaceSearch.empty, + index: OnDemandSymbolIndex = OnDemandSymbolIndex() ) extends SymbolSearch { override def documentation(symbol: String): Optional[SymbolDocumentation] = { docs.documentation(symbol) } + import scala.meta.internal.mtags.Symbol + override def definition(symbol: String): ju.List[Location] = { + index.definition(Symbol(symbol)) match { + case None => + ju.Collections.emptyList() + case Some(value) => + import org.eclipse.lsp4j.Range + import org.eclipse.lsp4j.Position + val filename = value.path.toNIO.getFileName().toString() + val uri = s"$symbol $filename" + ju.Collections.singletonList( + new Location( + uri, + new Range(new Position(0, 0), new Position(0, 0)) + ) + ) + } + } + override def search( textQuery: String, buildTargetIdentifier: String,