diff --git a/metals/src/main/scala/scala/meta/internal/metals/Messages.scala b/metals/src/main/scala/scala/meta/internal/metals/Messages.scala index c5caab57513..c3e6ddb7fec 100644 --- a/metals/src/main/scala/scala/meta/internal/metals/Messages.scala +++ b/metals/src/main/scala/scala/meta/internal/metals/Messages.scala @@ -1082,6 +1082,9 @@ object Messages { } } + final val PickSymbolForReferenceSearch = + "Choose symbol to search references for." + } object FileOutOfScalaCliBspScope { 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 dc3cbb0f0ac..9291c5b78a2 100644 --- a/metals/src/main/scala/scala/meta/internal/metals/MetalsLspService.scala +++ b/metals/src/main/scala/scala/meta/internal/metals/MetalsLspService.scala @@ -465,6 +465,8 @@ abstract class MetalsLspService( buildTargets, compilers, scalaVersionSelector, + languageClient, + clientConfig.isQuickPickProvider(), ) protected val packageProvider: PackageProvider = @@ -1126,7 +1128,7 @@ abstract class MetalsLspService( params: ReferenceParams ): Future[List[ReferencesResult]] = { val timer = new Timer(time) - referencesProvider.references(params).map { results => + referencesProvider.references(params, isForRename = false).map { results => if (clientConfig.initialConfig.statistics.isReferences) { if (results.forall(_.symbol.isEmpty)) { scribe.info(s"time: found 0 references in $timer") 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 6ca106c9466..8d862987ec1 100644 --- a/metals/src/main/scala/scala/meta/internal/metals/ReferenceProvider.scala +++ b/metals/src/main/scala/scala/meta/internal/metals/ReferenceProvider.scala @@ -15,6 +15,9 @@ import scala.util.matching.Regex import scala.meta.Importee import scala.meta.internal.metals.MetalsEnrichments._ import scala.meta.internal.metals.ResolvedSymbolOccurrence +import scala.meta.internal.metals.clients.language.MetalsLanguageClient +import scala.meta.internal.metals.clients.language.MetalsQuickPickItem +import scala.meta.internal.metals.clients.language.MetalsQuickPickParams import scala.meta.internal.mtags.DefinitionAlternatives.GlobalSymbol import scala.meta.internal.mtags.Semanticdbs import scala.meta.internal.mtags.Symbol @@ -46,6 +49,8 @@ final class ReferenceProvider( buildTargets: BuildTargets, compilers: Compilers, scalaVersionSelector: ScalaVersionSelector, + languageClient: MetalsLanguageClient, + isMetalsQuickPickProvider: Boolean, )(implicit ec: ExecutionContext) extends SemanticdbFeatureProvider with CompletionItemPriority { @@ -186,6 +191,7 @@ final class ReferenceProvider( */ def references( params: ReferenceParams, + isForRename: Boolean, findRealRange: AdjustRange = noAdjustRange, includeSynthetics: Synthetic => Boolean = _ => true, )(implicit report: ReportContext): Future[List[ReferencesResult]] = { @@ -197,7 +203,7 @@ final class ReferenceProvider( if (supportsPcRefs) textDoc.toOption else textDoc.documentIncludingStale textDocOpt match { case Some(doc) => - val results: List[ResolvedSymbolOccurrence] = { + val occurences: List[ResolvedSymbolOccurrence] = { val posOccurrences = definition.positionOccurrences(source, params.getPosition, doc) if (posOccurrences.isEmpty) @@ -205,78 +211,99 @@ final class ReferenceProvider( occurrencesForRenamedImport(source, params, doc) else posOccurrences } - if (results.isEmpty) { + if (occurences.isEmpty) { scribe.debug( s"No symbol found at ${params.getPosition()} for $source" ) } - val semanticdbResult = Future.sequence { - results.map { result => + val resultWithAlt = + occurences.zipWithIndex.flatMap { case (result, idx) => val occurrence = result.occurrence.get - val distance = result.distance - val alternatives = - referenceAlternatives(occurrence.symbol, source, doc) - references( + (referenceAlternatives( + occurrence.symbol, source, - params, doc, - distance, - occurrence, - alternatives, - params.getContext.isIncludeDeclaration, - findRealRange, - includeSynthetics, - supportsPcRefs, - ).map { locations => - // It's possible to return nothing is we exclude declaration - if ( - locations.isEmpty && params.getContext().isIncludeDeclaration() - ) { - val fileInIndex = - if (index.contains(source.toNIO)) - s"Current file ${source} is present" - else s"Missing current file ${source}" - scribe.debug( - s"No references found, index size ${index.size}\n" + fileInIndex - ) - report.unsanitized.create( - Report( - "empty-references", - index - .map { case (path, entry) => - s"$path -> ${entry.bloom.approximateElementCount()}" - } - .mkString("\n"), - s"Could not find any locations for ${result.occurrence}, printing index state", - Some(source.toString()), - Some( - source.toString() + ":" + result.occurrence.getOrElse("") - ), - ) - ) - } - ReferencesResult(occurrence.symbol, locations) - } + ) + occurrence.symbol).map((_, idx)) } - } - val pcResult = - pcReferences( - source, - results.flatMap(_.occurrence).map(_.symbol), - params.getContext().isIncludeDeclaration(), - findRealRange, - ) - Future - .sequence(List(semanticdbResult, pcResult)) - .map( - _.flatten - .groupBy(_.symbol) - .collect { case (symbol, refs) => - ReferencesResult(symbol, refs.flatMap(_.locations)) + quickPickSymbolGroup(resultWithAlt.map(_._1).toSet, isForRename) + .map { chosen => + resultWithAlt.filter(x => chosen(x._1)).groupMap(_._2)(_._1).map { + case (idx, syms) => occurences(idx) -> syms.toSet + } + } + .flatMap { results => + val semanticdbResult = Future.sequence { + results.map { case (result, alternatives) => + val occurrence = result.occurrence.get + val distance = result.distance + references( + source, + params, + doc, + distance, + occurrence, + alternatives, + params.getContext.isIncludeDeclaration, + findRealRange, + includeSynthetics, + supportsPcRefs, + ).map { locations => + // It's possible to return nothing is we exclude declaration + if ( + locations.isEmpty && params + .getContext() + .isIncludeDeclaration() + ) { + val fileInIndex = + if (index.contains(source.toNIO)) + s"Current file ${source} is present" + else s"Missing current file ${source}" + scribe.debug( + s"No references found, index size ${index.size}\n" + fileInIndex + ) + report.unsanitized.create( + Report( + "empty-references", + index + .map { case (path, entry) => + s"$path -> ${entry.bloom.approximateElementCount()}" + } + .mkString("\n"), + s"Could not find any locations for ${result.occurrence}, printing index state", + Some(source.toString()), + Some( + source.toString() + ":" + result.occurrence + .getOrElse("") + ), + ) + ) + } + ReferencesResult(occurrence.symbol, locations) + } } - .toList - ) + } + val chosen = results.flatMap(_._2).toSet + val pcResult = + pcReferences( + source, + chosen.toList, + params.getContext().isIncludeDeclaration(), + findRealRange, + isForRename, + ).map(_.filter(ref => chosen(ref.symbol) || ref.symbol.isLocal)) + + Future + .sequence(List(semanticdbResult, pcResult)) + .map( + _.flatten + .groupBy(_.symbol) + .collect { case (symbol, refs) => + ReferencesResult(symbol, refs.flatMap(_.locations)) + } + .toList + ) + } case None => if (textDoc.documentIncludingStale.isEmpty) scribe.debug(s"No semanticDB for $source") @@ -289,30 +316,35 @@ final class ReferenceProvider( findRealRange, ) symbols = foundRefs.map(_.symbol).filterNot(_.isLocal) + chosen <- quickPickSymbolGroup(symbols.toSet, isForRename) + fileteredFoundRefs = foundRefs.filter(ref => + chosen(ref.symbol) || ref.symbol.isLocal + ) fromPc <- - if (symbols.isEmpty) Future.successful(Nil) + if (chosen.isEmpty) Future.successful(Nil) else { pcReferences( source, - symbols, + chosen.toList, includeDeclaration, findRealRange, + isForRename, ) } } yield { - if (symbols.isEmpty) foundRefs + if (chosen.isEmpty) fileteredFoundRefs else { val fromWorkspace = workspaceReferences( source, - symbols.toSet, + chosen.toSet, includeDeclaration, findRealRange, includeSynthetics, ) val results = ReferencesResult( - symbols.head, + chosen.head, fromWorkspace, - ) :: (fromPc ++ foundRefs) + ) :: (fromPc ++ fileteredFoundRefs) results .groupBy(_.symbol) @@ -325,6 +357,57 @@ final class ReferenceProvider( } } + private def quickPickSymbolGroup( + symbols: Set[String], + forRename: Boolean, + ): Future[Set[String]] = { + lazy val symsContainType = symbols.exists(sym => Symbol(sym).isType) + if (forRename) { + Future.successful { + if (symsContainType) { + symbols + .filter { sym => + Symbol(sym) match { + case sym if sym.isApply => false + case _ => true + } + } + } else symbols + } + } else { + lazy val symsMap = symbols + .map { sym => + val tpe = Symbol(sym) match { + case sym if sym.isType => "Type" + case sym if sym.isConstructor || (sym.isApply && symsContainType) => + "Constructor / Synthetic apply" + case sym if sym.isApply => "Apply" + case _ => "Term" + } + tpe -> sym + } + .groupMap(_._1)(_._2) + if (symsMap.size > 1 && isMetalsQuickPickProvider) { + val allElem = MetalsQuickPickItem("All", "All") + languageClient + .metalsQuickPick( + MetalsQuickPickParams( + (symsMap.keySet + .map(name => MetalsQuickPickItem(name, name)) + .toList :+ allElem).asJava, + placeHolder = Messages.PickSymbolForReferenceSearch, + ) + ) + .asScala + .map { + case None => symbols + case Some(res) if res.itemId == null => symbols + case Some(id) => symsMap.get(id.itemId).getOrElse(symbols) + } + } else Future.successful(symbols) + } + } + // for `import package.{AA as B@@B}` we look for occurrences at `import package.{@@AA as BB}`, // since rename is not a position occurrence in semanticDB private def occurrencesForRenamedImport( @@ -375,6 +458,14 @@ final class ReferenceProvider( if check(info) } yield info.symbol + val nonSyntheticSymbols = for { + occ <- definitionDoc.occurrences + if occ.role.isDefinition + } yield occ.symbol + + def isSyntheticSymbol(symbol: String) = + !nonSyntheticSymbols.contains(symbol) + val isCandidate = if (defPath.isJava) candidates { info => @@ -385,17 +476,11 @@ final class ReferenceProvider( alternatives.isVarSetter(info) || alternatives.isCompanionObject(info) || alternatives.isCopyOrApplyParam(info) || - alternatives.isContructorParam(info) + alternatives.isContructorParam(info) || + alternatives.isJavaConstructor(info) || + alternatives.isApplyMethod(info, isSyntheticSymbol) }.toSet - val nonSyntheticSymbols = for { - occ <- definitionDoc.occurrences - if isCandidate(occ.symbol) || occ.symbol == symbol - if occ.role.isDefinition - } yield occ.symbol - - def isSyntheticSymbol = !nonSyntheticSymbols.contains(symbol) - def additionalAlternativesForSynthetic = for { info <- definitionDoc.symbols if info.symbol != name @@ -407,10 +492,10 @@ final class ReferenceProvider( if (defPath.isJava) isCandidate - else if (isSyntheticSymbol) - isCandidate -- nonSyntheticSymbols ++ additionalAlternativesForSynthetic + else if (isSyntheticSymbol(symbol)) + isCandidate ++ additionalAlternativesForSynthetic else - isCandidate -- nonSyntheticSymbols + isCandidate case None => Set.empty } } @@ -420,6 +505,7 @@ final class ReferenceProvider( symbols: List[String], includeDeclaration: Boolean, adjustLocation: AdjustRange, + isForRename: Boolean, ): Future[List[ReferencesResult]] = { val visited = mutable.Set[AbsolutePath]() val names = symbols.map(nameFromSymbol(_)).toSet @@ -458,12 +544,14 @@ final class ReferenceProvider( .toList val lock = new Lock val result = - pcReferencesLock.getAndSet(lock).cancelAndWaitUntilCompleted().flatMap { - _ => + pcReferencesLock + .getAndSet(lock) + .cancelAndWaitUntilCompleted(isForRename) + .flatMap { _ => val maxPcsNumber = Runtime.getRuntime().availableProcessors() / 2 executeBatched(lazyResults, maxPcsNumber, () => lock.isCancelled) .map(_.flatten) - } + } result.onComplete(_ => lock.complete()) result } @@ -633,13 +721,12 @@ final class ReferenceProvider( snapshot: TextDocument, distance: TokenEditDistance, occ: SymbolOccurrence, - alternatives: Set[String], + isSymbol: Set[String], isIncludeDeclaration: Boolean, findRealRange: AdjustRange, includeSynthetics: Synthetic => Boolean, supportsPcRefs: Boolean, ): Future[Seq[Location]] = { - val isSymbol = alternatives + occ.symbol val isLocal = occ.symbol.isLocal if (isLocal && supportsPcRefs) compilers @@ -728,7 +815,30 @@ final class ReferenceProvider( * are ok and speed is more important, we just use the default noAdjustRange. */ else { - findRealRange(range, snapshot.text, reference.symbol).foreach(add) + val adjustedForConstructorRange = { + val symbol = Symbol(reference.symbol) + if (symbol.isConstructor && range.isPoint) { + lazy val forConstructorOccurence = range.withStartCharacter( + range.startCharacter - symbol.owner.displayName.length() + ) + if (reference.role.isDefinition) { + // if primary constructor definition find parent definition + snapshot.occurrences + .collectFirst { + case occ + if occ.symbol == symbol.owner.value && occ.role.isDefinition => + occ.range + } + .flatten + .getOrElse(forConstructorOccurence) + } else forConstructorOccurence + } else range + } + findRealRange( + adjustedForConstructorRange, + snapshot.text, + reference.symbol, + ).foreach(add) } } @@ -810,6 +920,21 @@ class SymbolAlternatives(symbol: String, name: String) { }) } + def isApplyMethod( + info: SymbolInformation, + isSyntheticSymbol: String => Boolean, + ): Boolean = + symbol == (Symbol(info.symbol) match { + case GlobalSymbol( + GlobalSymbol(owner, Descriptor.Term(obj)), + Descriptor.Method("apply", _), + ) => + if (isSyntheticSymbol(info.symbol)) + Symbols.Global(owner.value, Descriptor.Type(obj)) + else Symbols.Global(owner.value, Descriptor.Term(obj)) + case _ => "" + }) + // Returns true if `info` is a parameter of a synthetic `copy` or `apply` matching the occurrence field symbol. def isCopyOrApplyParam(info: SymbolInformation): Boolean = info.isParameter && @@ -902,8 +1027,8 @@ class Lock { def isCancelled = cancelPromise.isCompleted def complete(): Unit = completedPromise.trySuccess(()) - def cancelAndWaitUntilCompleted(): Future[Unit] = { - cancelPromise.trySuccess(()) + def cancelAndWaitUntilCompleted(isForRename: Boolean): Future[Unit] = { + if (!isForRename) cancelPromise.trySuccess(()) completedPromise.future } } 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 8c8043eb118..034f61d0efd 100644 --- a/metals/src/main/scala/scala/meta/internal/rename/RenameProvider.scala +++ b/metals/src/main/scala/scala/meta/internal/rename/RenameProvider.scala @@ -220,6 +220,7 @@ final class RenameProvider( txtParams, includeDeclaration = isJava, ), + isForRename = true, findRealRange = AdjustRange(findRealRange(newName)), includeSynthetic, ) @@ -425,6 +426,7 @@ final class RenameProvider( referenceProvider .references( toReferenceParams(loc, includeDeclaration = false), + isForRename = true, findRealRange = AdjustRange(findRealRange(newName)), ) .map(_.flatMap(_.locations :+ loc)) @@ -480,6 +482,7 @@ final class RenameProvider( referenceProvider .references( locParams, + isForRename = true, findRealRange = AdjustRange(findRealRange(newName)), includeSynthetic, ) diff --git a/mtags/src/main/scala-2/scala/meta/internal/pc/MetalsGlobal.scala b/mtags/src/main/scala-2/scala/meta/internal/pc/MetalsGlobal.scala index ed925e4064a..dd67cf6a24b 100644 --- a/mtags/src/main/scala-2/scala/meta/internal/pc/MetalsGlobal.scala +++ b/mtags/src/main/scala-2/scala/meta/internal/pc/MetalsGlobal.scala @@ -780,8 +780,9 @@ class MetalsGlobal( val name = if (defn.symbol.isPackageObject) defn.symbol.enclosingPackageClass.name else defn.name + val nameLength = name.dropLocal.decoded.length() val start = defn.pos.point - val end = start + name.dropLocal.decoded.length() + val end = start + nameLength Position.range(defn.pos.source, start, start, end) } } @@ -794,6 +795,11 @@ class MetalsGlobal( def namePosition: Position = { sel match { case _ if !sel.pos.isRange => sel.pos + case Select(New(qualifier), name) if name == nme.CONSTRUCTOR => + qualifier match { + case qualifier: Select => qualifier.namePosition + case _ => qualifier.pos.withStart(qualifier.pos.point) + } case Select(qualifier: Select, name) if (name == nme.apply || name == nme.unapply) && sel.pos.point == qualifier.pos.point => qualifier.namePosition diff --git a/mtags/src/main/scala-2/scala/meta/internal/pc/PcCollector.scala b/mtags/src/main/scala-2/scala/meta/internal/pc/PcCollector.scala index ca19ba1eb85..e422dff4cb5 100644 --- a/mtags/src/main/scala-2/scala/meta/internal/pc/PcCollector.scala +++ b/mtags/src/main/scala-2/scala/meta/internal/pc/PcCollector.scala @@ -45,6 +45,11 @@ trait PcCollector[T] { self: WithCompilationUnit => def soughtOrOverride(sym: Symbol) = sought(sym) || sym.allOverriddenSymbols.exists(sought(_)) + def primaryContructorParent(sym: Symbol) = + soughtSet + .flatMap(sym => if (sym.isPrimaryConstructor) Some(sym.owner) else None) + .contains(sym) + def soughtTreeFilter(tree: Tree): Boolean = tree match { case ident: Ident @@ -57,7 +62,8 @@ trait PcCollector[T] { self: WithCompilationUnit => soughtOrOverride(sel.symbol) case df: MemberDef => (soughtOrOverride(df.symbol) || - isForComprehensionOwner(df)) + isForComprehensionOwner(df) || + primaryContructorParent(df.symbol)) case appl: Apply => appl.symbol != null && (owners(appl.symbol) || @@ -172,13 +178,22 @@ trait PcCollector[T] { self: WithCompilationUnit => * class <> = ??? * etc. */ - case df: MemberDef if isCorrectPos(df) && filter(df) => + case df: MemberDef + if isCorrectPos(df) && filter( + df + ) && !df.symbol.isPrimaryConstructor => (annotationChildren(df) ++ df.children).foldLeft({ - val t = collect( - df, - df.namePosition + val maybeConstructor = + Some(df.symbol.primaryConstructor).filter(_ != NoSymbol) + val collected = collect(df, df.namePosition) + // we also collect primary constructor on class, + // since primary constructor is synthetic and doesn't have needed span anymore + // later we cannot get the correct + val collectedConstructor = maybeConstructor.map(sym => + collect(df, df.namePosition, Some(sym)) ) - if (acc(t)) acc else acc + t + + acc + collected ++ collectedConstructor })(traverse(_, _)) /* Named parameters, since they don't show up in typed tree: * foo(<> = "abc") diff --git a/mtags/src/main/scala-2/scala/meta/internal/pc/PcReferencesProvider.scala b/mtags/src/main/scala-2/scala/meta/internal/pc/PcReferencesProvider.scala index 06d0df536c1..3550749b18e 100644 --- a/mtags/src/main/scala-2/scala/meta/internal/pc/PcReferencesProvider.scala +++ b/mtags/src/main/scala-2/scala/meta/internal/pc/PcReferencesProvider.scala @@ -67,7 +67,6 @@ class BySymbolPCReferencesProvider( def result(): List[(String, Option[l.Range])] = { val sought = semanticDbSymbols .flatMap(compiler.compilerSymbol(_)) - .flatMap(symbolAlternatives(_)) if (sought.isEmpty) Nil else resultWithSought(sought) } diff --git a/mtags/src/main/scala-2/scala/meta/internal/pc/WithCompilationUnit.scala b/mtags/src/main/scala-2/scala/meta/internal/pc/WithCompilationUnit.scala index a3b49d0a924..c511f566153 100644 --- a/mtags/src/main/scala-2/scala/meta/internal/pc/WithCompilationUnit.scala +++ b/mtags/src/main/scala-2/scala/meta/internal/pc/WithCompilationUnit.scala @@ -36,13 +36,39 @@ class WithCompilationUnit( * @return set of possible symbols */ def symbolAlternatives(sym: Symbol): Set[Symbol] = { + def allMembers(name: Name, symbol: Symbol): List[Symbol] = { + val member = symbol.info.member(name) + if (member.isOverloaded) + member.info.asInstanceOf[OverloadedType].alternatives + else List(member) + } + val all = if (sym.isClass) { - if (sym.owner.isMethod) Set(sym) ++ sym.localCompanion(pos) - else Set(sym, sym.companionModule, sym.companion.moduleClass) + def contructorLike(moduleClass: Symbol) = + sym.primaryConstructor :: allMembers(nme.apply, moduleClass).filter( + _.isSynthetic + ) + if (sym.owner.isMethod) + Set(sym) ++ sym + .localCompanion(pos) + .toList + .flatMap(comp => comp :: contructorLike(comp)) + else + Set( + sym, + sym.companionModule, + sym.companion.moduleClass + ) ++ contructorLike(sym.companion.moduleClass) } else if (sym.isModuleOrModuleClass) { - if (sym.owner.isMethod) Set(sym) ++ sym.localCompanion(pos) - else Set(sym, sym.companionClass, sym.moduleClass) + def contructorLike(moduleClass: Symbol) = + allMembers(nme.apply, moduleClass) + if (sym.owner.isMethod) + Set(sym) ++ sym.localCompanion(pos) ++ contructorLike(sym) + else + Set(sym, sym.companionClass, sym.moduleClass) ++ contructorLike( + sym.moduleClass + ) } else if (sym.isTerm && (sym.owner.isClass || sym.owner.isConstructor)) { val info = if (sym.owner.isClass) sym.owner.info diff --git a/mtags/src/main/scala-3/scala/meta/internal/pc/PcCollector.scala b/mtags/src/main/scala-3/scala/meta/internal/pc/PcCollector.scala index a86e7bff308..6153956ce18 100644 --- a/mtags/src/main/scala-3/scala/meta/internal/pc/PcCollector.scala +++ b/mtags/src/main/scala-3/scala/meta/internal/pc/PcCollector.scala @@ -59,6 +59,9 @@ trait PcCollector[T]: def soughtOrOverride(sym: Symbol) = sought(sym) || sym.allOverriddenSymbols.exists(sought(_)) + lazy val constructorOwners = sought.collect { + case sym if sym.isPrimaryConstructor => sym.owner + } def soughtTreeFilter(tree: Tree): Boolean = tree match @@ -68,7 +71,7 @@ trait PcCollector[T]: true case sel: Select if soughtOrOverride(sel.symbol) => true case df: NamedDefTree - if soughtOrOverride(df.symbol) && !df.symbol.isSetter => + if (soughtOrOverride(df.symbol) || constructorOwners(df.symbol)) && !df.symbol.isSetter => true case imp: Import if owners(imp.expr.symbol) => true case _ => false @@ -167,12 +170,16 @@ trait PcCollector[T]: */ case df: NamedDefTree if df.span.isCorrect && df.nameSpan.isCorrect && - filter(df) && !isGeneratedGiven(df, sourceText) => + filter(df) && !isGeneratedGiven(df, sourceText) && !df.symbol.isPrimaryConstructor => def collectEndMarker = EndMarker.getPosition(df, pos, sourceText).map { collect(EndMarker(df.symbol), _) } end collectEndMarker + def collectPrimaryConstructor = + Some(df.symbol.primaryConstructor).filter(_.exists).map( sym => + collect(df, pos.withSpan(df.nameSpan), Some(sym)) + ) val annots = collectTrees(df.mods.annotations) val traverser = @@ -183,7 +190,7 @@ trait PcCollector[T]: occurences + collect( df, pos.withSpan(df.nameSpan), - ) ++ collectEndMarker + ) ++ collectEndMarker ++ collectPrimaryConstructor ) { case (set, tree) => traverser(set, tree) } diff --git a/mtags/src/main/scala-3/scala/meta/internal/pc/PcReferencesProvider.scala b/mtags/src/main/scala-3/scala/meta/internal/pc/PcReferencesProvider.scala index 35fffbb6295..d5dc0a18740 100644 --- a/mtags/src/main/scala-3/scala/meta/internal/pc/PcReferencesProvider.scala +++ b/mtags/src/main/scala-3/scala/meta/internal/pc/PcReferencesProvider.scala @@ -32,7 +32,7 @@ class PcReferencesProvider( symbolSearch.soughtSymbols.map(_._1) } else { val semanticDBSymbols = request.alternativeSymbols().asScala.toSet + request.offsetOrSymbol().getRight() - Some(semanticDBSymbols.flatMap(SymbolProvider.compilerSymbol).flatMap(symbolAlternatives(_))).filter(_.nonEmpty) + Some(semanticDBSymbols.flatMap(SymbolProvider.compilerSymbol)).filter(_.nonEmpty) } def collect(parent: Option[Tree])( diff --git a/mtags/src/main/scala-3/scala/meta/internal/pc/PcSymbolSearch.scala b/mtags/src/main/scala-3/scala/meta/internal/pc/PcSymbolSearch.scala index 0ab8bdce569..60d9e462d91 100644 --- a/mtags/src/main/scala-3/scala/meta/internal/pc/PcSymbolSearch.scala +++ b/mtags/src/main/scala-3/scala/meta/internal/pc/PcSymbolSearch.scala @@ -1,5 +1,7 @@ package scala.meta.internal.pc +import scala.annotation.tailrec + import scala.meta.internal.mtags.MtagsEnrichments.* import scala.meta.internal.pc.MetalsInteractive.ExtensionMethodCall import scala.meta.internal.pc.PcSymbolSearch.* @@ -255,11 +257,22 @@ object PcSymbolSearch: // NOTE: Connected to https://github.com/lampepfl/dotty/issues/16771 // `sel.nameSpan` is calculated incorrectly in (1 + 2).toString // See test DocumentHighlightSuite.select-parentheses + @tailrec def selectNameSpan(sel: Select): Span = val span = sel.span if span.exists then val point = span.point - if sel.name.toTermName == nme.ERROR then Span(point) + val termName = sel.name.toTermName + if termName == nme.ERROR then Span(point) + else if (termName == nme.apply || termName == nme.unapply || termName == nme.CONSTRUCTOR) && span.point == sel.qualifier.span.start then + @tailrec + def fromQualfier(qualifier: Tree): Span = + qualifier match + case sel: Select => selectNameSpan(sel) + case New(qual) => fromQualfier(qual) + case AppliedTypeTree(tycon, _) => fromQualfier(tycon) + case qual => qual.span + fromQualfier(sel.qualifier) else if sel.qualifier.span.start > span.point then // right associative val realName = sel.name.stripModuleClassSuffix.lastPart Span(span.start, span.start + realName.length, point) diff --git a/mtags/src/main/scala-3/scala/meta/internal/pc/WithCompilationUnit.scala b/mtags/src/main/scala-3/scala/meta/internal/pc/WithCompilationUnit.scala index 8ce0fd390be..4abcef218e9 100644 --- a/mtags/src/main/scala-3/scala/meta/internal/pc/WithCompilationUnit.scala +++ b/mtags/src/main/scala-3/scala/meta/internal/pc/WithCompilationUnit.scala @@ -11,6 +11,7 @@ import scala.meta.pc.VirtualFileParams import dotty.tools.dotc.core.Contexts.* import dotty.tools.dotc.core.Flags import dotty.tools.dotc.core.NameOps.* +import dotty.tools.dotc.core.StdNames import dotty.tools.dotc.core.Symbols.* import dotty.tools.dotc.interactive.InteractiveDriver import dotty.tools.dotc.util.SourceFile @@ -73,11 +74,12 @@ class WithCompilationUnit( else Set.empty val all = if sym.is(Flags.ModuleClass) then - Set(sym, sym.companionModule, sym.companionModule.companion) + Set(sym, sym.companionModule, sym.companionModule.companion) ++ sym.info.member(StdNames.nme.apply).allSymbols else if sym.isClass then - Set(sym, sym.companionModule, sym.companion.moduleClass) + Set(sym, sym.companionModule, sym.companion.moduleClass, sym.primaryConstructor) + ++ sym.companion.moduleClass.info.member(StdNames.nme.apply).allSymbols.filter(_.is(Flags.Synthetic)) else if sym.is(Flags.Module) then - Set(sym, sym.companionClass, sym.moduleClass) + Set(sym, sym.companionClass, sym.moduleClass) ++ sym.moduleClass.info.member(StdNames.nme.apply).allSymbols else if sym.isTerm && (sym.owner.isClass || sym.owner.isConstructor) then val info = diff --git a/mtags/src/main/scala/scala/meta/internal/mtags/Symbol.scala b/mtags/src/main/scala/scala/meta/internal/mtags/Symbol.scala index 85aa46b26b4..d1f4f7ff3d1 100644 --- a/mtags/src/main/scala/scala/meta/internal/mtags/Symbol.scala +++ b/mtags/src/main/scala/scala/meta/internal/mtags/Symbol.scala @@ -3,6 +3,7 @@ package scala.meta.internal.mtags import scala.annotation.tailrec import scala.util.control.NonFatal +import scala.meta.internal.mtags.DefinitionAlternatives.GlobalSymbol import scala.meta.internal.mtags.MtagsEnrichments._ import scala.meta.internal.semanticdb.Scala._ @@ -31,6 +32,20 @@ final class Symbol private (val value: String) { def isPackage: Boolean = desc.isPackage def isParameter: Boolean = desc.isParameter def isTypeParameter: Boolean = desc.isTypeParameter + def isConstructor: Boolean = + this match { + case GlobalSymbol(_, Descriptor.Method("", _)) => true + case _ => false + } + def isApply: Boolean = + this match { + case GlobalSymbol( + GlobalSymbol(_, Descriptor.Term(_)), + Descriptor.Method("apply", _) + ) => + true + case _ => false + } private lazy val desc: Descriptor = value.desc def owner: Symbol = Symbol(value.owner) diff --git a/tests/cross/src/test/scala/tests/highlight/DocumentHighlightSuite.scala b/tests/cross/src/test/scala/tests/highlight/DocumentHighlightSuite.scala index 9ce2fb726fd..a21b05fd018 100644 --- a/tests/cross/src/test/scala/tests/highlight/DocumentHighlightSuite.scala +++ b/tests/cross/src/test/scala/tests/highlight/DocumentHighlightSuite.scala @@ -1098,7 +1098,7 @@ class DocumentHighlightSuite extends BaseDocumentHighlightSuite { |object Main { | class <>[T](abc: T) | object <> { - | def apply(abc: Int, bde: Int) = new <>(abc + bde) + | def <>(abc: Int, bde: Int) = new <>(abc + bde) | } | val x = <>(123, 456) |}""".stripMargin diff --git a/tests/unit/src/main/scala/tests/BaseRangesSuite.scala b/tests/unit/src/main/scala/tests/BaseRangesSuite.scala index 06204d63945..cf86892550d 100644 --- a/tests/unit/src/main/scala/tests/BaseRangesSuite.scala +++ b/tests/unit/src/main/scala/tests/BaseRangesSuite.scala @@ -2,9 +2,13 @@ package tests import scala.concurrent.Future +import scala.meta.internal.metals.Messages +import scala.meta.internal.metals.MetalsEnrichments._ +import scala.meta.internal.metals.clients.language.MetalsQuickPickItem +import scala.meta.internal.metals.clients.language.RawMetalsQuickPickResult + import munit.Location import munit.TestOptions - abstract class BaseRangesSuite(name: String) extends BaseLspSuite(name) { protected def libraryDependencies: List[String] = Nil @@ -23,6 +27,9 @@ abstract class BaseRangesSuite(name: String) extends BaseLspSuite(name) { additionalLibraryDependencies: List[String] = Nil, scalacOptions: List[String] = Nil, customMetalsJson: Option[String] = None, + quickPickReferencesSymbol: List[MetalsQuickPickItem] => Option[ + MetalsQuickPickItem + ] = _.lastOption, )(implicit loc: Location ): Unit = { @@ -66,6 +73,15 @@ abstract class BaseRangesSuite(name: String) extends BaseLspSuite(name) { |${input .replaceAll("(<<|>>|@@)", "")}""".stripMargin ) + _ = client.quickPickHandler = { + case params + if params.placeHolder == Messages.PickSymbolForReferenceSearch => + quickPickReferencesSymbol(params.items.asScala.toList) + .map(item => RawMetalsQuickPickResult(item.id)) + .getOrElse(RawMetalsQuickPickResult(cancelled = true)) + case _ => + RawMetalsQuickPickResult(cancelled = true) + } _ <- Future.sequence( files.map(file => server.didOpen(s"${file._1}")) ) diff --git a/tests/unit/src/test/scala/tests/FingerprintsLspSuite.scala b/tests/unit/src/test/scala/tests/FingerprintsLspSuite.scala index c9fc508b272..7f771be974b 100644 --- a/tests/unit/src/test/scala/tests/FingerprintsLspSuite.scala +++ b/tests/unit/src/test/scala/tests/FingerprintsLspSuite.scala @@ -76,6 +76,12 @@ class FingerprintsLspSuite extends BaseLspSuite("fingerprints") { |a/src/main/scala/a/Adresses.scala:2:7: a/Adresses# |class Adresses { | ^^^^^^^^ + |======================== + |= a/Adresses#``(). + |======================== + |a/src/main/scala/a/Adresses.scala:2:7: a/Adresses#``(). + |class Adresses { + | ^^^^^^^^ |==================== |= a/Adresses#number. |==================== @@ -103,6 +109,12 @@ class FingerprintsLspSuite extends BaseLspSuite("fingerprints") { |a/src/main/scala/a/Names.scala:2:7: a/Names# |class Names { | ^^^^^ + |===================== + |= a/Names#``(). + |===================== + |a/src/main/scala/a/Names.scala:2:7: a/Names#``(). + |class Names { + | ^^^^^ |=============== |= a/Names#name. |=============== diff --git a/tests/unit/src/test/scala/tests/ReferenceLspSuite.scala b/tests/unit/src/test/scala/tests/ReferenceLspSuite.scala index 3400016e66e..155e9d94b14 100644 --- a/tests/unit/src/test/scala/tests/ReferenceLspSuite.scala +++ b/tests/unit/src/test/scala/tests/ReferenceLspSuite.scala @@ -8,7 +8,7 @@ import scala.meta.internal.metals.ServerCommands class ReferenceLspSuite extends BaseRangesSuite("reference") { override protected def initializationOptions: Option[InitializationOptions] = - Some(TestingServer.TestDefault) + Some(TestingServer.TestDefault.copy(quickPickProvider = Some(true))) test("case-class") { cleanWorkspace() @@ -84,6 +84,65 @@ class ReferenceLspSuite extends BaseRangesSuite("reference") { |""".stripMargin, ) + check( + "contructor-only", + """|/a/src/main/scala/a/Main.scala + |package a + |case class <>(i: Int) + |object AA { + | def apply(): AA = <>(1) + | val empty: AA = new <>(0) + |} + |/a/src/main/scala/b/Other.scala + |package b + |import a.AA + |object O { + | val empty: AA = new <>(0) + |} + |""".stripMargin, + quickPickReferencesSymbol = { items => + items.find(_.label.contains("Constructor")) + }, + ) + + check( + "object-only", + """|/a/src/main/scala/a/Main.scala + |package a + |case class A@@A(i: Int) + |object <> { + | def apply(): AA = <>(1) + | val empty: AA = new AA(0) + |} + |/a/src/main/scala/b/Other.scala + |package b + |import a.<> + |object O { + | val empty: AA = new AA(0) + |} + |""".stripMargin, + quickPickReferencesSymbol = _.find(_.label.contains("Term")), + ) + + check( + "type-only", + """|/a/src/main/scala/a/Main.scala + |package a + |case class <>(i: Int) + |object AA { + | def apply(): <> = AA(1) + | val empty: <> = new <>(0) + |} + |/a/src/main/scala/b/Other.scala + |package b + |import a.<> + |object O { + | val empty: <> = new <>(0) + |} + |""".stripMargin, + quickPickReferencesSymbol = _.find(_.label.contains("Type")), + ) + test("synthetic") { cleanWorkspace() for { @@ -164,7 +223,7 @@ class ReferenceLspSuite extends BaseRangesSuite("reference") { |} |""".stripMargin, """|object Other { - | val mb = <
>.apply("b") + | val mb = <
>.<>("b") |} |""".stripMargin, ) @@ -196,10 +255,10 @@ class ReferenceLspSuite extends BaseRangesSuite("reference") { |""".stripMargin, ) - checkInSamePackage( // FIXME: should, but doesn't find the class declaration: https://github.com/scalameta/metals/issues/1553#issuecomment-617884934 + checkInSamePackage( "case-class-unapply-starting-elsewhere", """|sealed trait Stuff - |case class <>(n: Int) extends Stuff // doesn't find this + |case class <>(n: Int) extends Stuff |""".stripMargin, """| |object ByTheWay {