diff --git a/metals/src/main/scala/scala/meta/internal/implementation/ImplementationProvider.scala b/metals/src/main/scala/scala/meta/internal/implementation/ImplementationProvider.scala index 50daed381bc..ce26732fa18 100644 --- a/metals/src/main/scala/scala/meta/internal/implementation/ImplementationProvider.scala +++ b/metals/src/main/scala/scala/meta/internal/implementation/ImplementationProvider.scala @@ -1,7 +1,5 @@ package scala.meta.internal.implementation -import org.eclipse.lsp4j.Location -import org.eclipse.lsp4j.TextDocumentPositionParams import scala.meta.internal.mtags.Semanticdbs import scala.meta.internal.mtags.{Symbol => MSymbol} import scala.meta.internal.metals.MetalsEnrichments._ @@ -12,14 +10,13 @@ import scala.meta.internal.semanticdb.ClassSignature import scala.meta.internal.semanticdb.TypeRef import scala.meta.internal.semanticdb.Signature import scala.meta.internal.semanticdb.TextDocument -import java.util.concurrent.ConcurrentHashMap -import java.nio.file.Path import scala.meta.internal.semanticdb.SymbolInformation import scala.meta.internal.semanticdb.MethodSignature import scala.meta.internal.mtags.GlobalSymbolIndex -import scala.meta.internal.metals.BuildTargets import scala.meta.internal.metals.Buffers +import scala.meta.internal.metals.BuildTargets import scala.meta.internal.metals.DefinitionProvider +import scala.meta.internal.metals.FilePosition import scala.meta.internal.metals.TokenEditDistance import scala.meta.internal.semanticdb.Scala._ import scala.meta.internal.semanticdb.TypeSignature @@ -28,6 +25,10 @@ import scala.meta.internal.symtab.GlobalSymbolTable import scala.util.control.NonFatal import scala.meta.internal.mtags.Mtags import java.util.concurrent.ConcurrentLinkedQueue +import java.util.concurrent.ConcurrentHashMap +import java.nio.file.Path +import org.eclipse.lsp4j.Location +import org.eclipse.lsp4j.TextDocumentPositionParams final class ImplementationProvider( semanticdbs: Semanticdbs, @@ -82,93 +83,122 @@ final class ImplementationProvider( } } + def implementations(position: TextDocumentPositionParams): List[Location] = { + implementations( + Left( + FilePosition( + position.getTextDocument.getUri.toAbsolutePath, + position.getPosition + ) + ), + position.getTextDocument.getUri.toAbsolutePath + ) + } + def implementations( - params: TextDocumentPositionParams + filePositionOrSymbol: Either[FilePosition, SymbolInformation], + source: AbsolutePath + ): List[Location] = { + filePositionOrSymbol match { + case Left(filePosition) => + definitionProvider.symbolOccurrence(filePosition) match { + case Some((so, doc)) => + implementations(Some(doc), so.symbol, source) + case None => + List.empty + } + case Right(symbolInformation) => + implementations(None, symbolInformation.symbol, source) + } + } + + private def implementations( + currentDocument: Option[TextDocument], + symbol: String, + source: AbsolutePath ): List[Location] = { - val source = params.getTextDocument.getUri.toAbsolutePath lazy val global = globalTable.globalSymbolTableFor(source) - val locations = for { - (symbolOccurrence, currentDocument) <- definitionProvider - .symbolOccurence( + + // 1. Search locally for symbol + // 2. Search inside workspace + // 3. Search classpath via GlobalSymbolTable + def symbolSearch(symbol: String): Option[SymbolInformation] = { + currentDocument + .flatMap(doc => findSymbol(doc, symbol)) + .orElse(findSymbolDef(symbol)) + .orElse(global.flatMap(_.safeInfo(symbol))) + } + + val dealiased = + if (symbol.desc.isType) dealiasClass(symbol, symbolSearch _) else symbol + + val definitionDocument = + currentDocument.flatMap(doc => + if (doc.definesSymbol(dealiased)) currentDocument + else findSemanticDbForSymbol(dealiased) + ) + + val inheritanceContext = definitionDocument match { + // symbol is not in workspace, we only search classpath for it + case None => + globalTable.globalContextFor( source, - params + implementationsInPath.asScala.toMap ) - .toIterable - } yield { - // 1. Search locally for symbol - // 2. Search inside workspace - // 3. Search classpath via GlobalSymbolTable - def symbolSearch(symbol: String): Option[SymbolInformation] = { - findSymbol(currentDocument, symbol) - .orElse(findSymbolDef(symbol)) - .orElse(global.flatMap(_.safeInfo(symbol))) - } - val sym = symbolOccurrence.symbol - val dealiased = - if (sym.desc.isType) dealiasClass(sym, symbolSearch _) else sym - - val definitionDocument = - if (currentDocument.definesSymbol(dealiased)) { - Some(currentDocument) - } else { - findSemanticDbForSymbol(dealiased) - } - - val inheritanceContext = definitionDocument match { - // symbol is not in workspace, we only search classpath for it - case None => - globalTable.globalContextFor( - source, + // symbol is in workspace, + // we might need to search different places for related symbols + case Some(textDocument) => + Some( + InheritanceContext.fromDefinitions( + symbolSearch, implementationsInPath.asScala.toMap ) - // symbol is in workspace, - // we might need to search different places for related symbols - case Some(textDocument) => - Some( - InheritanceContext.fromDefinitions( - symbolSearch, - implementationsInPath.asScala.toMap - ) - ) - } - symbolLocationsFromContext( - dealiased, - source, - inheritanceContext - ) + ) } - locations.flatten.toList + symbolLocationsFromContext( + dealiased, + source, + inheritanceContext + ).toList } def topMethodParents( - symbol: String, - textDocument: TextDocument - ): Seq[Location] = { + doc: TextDocument, + symbol: String + ): Seq[Either[Location, SymbolInformation]] = { + // location in semanticDB for symbol might not be present when symbol is local then it must be in current document + val textDocument = findSemanticDbForSymbol(symbol).getOrElse(doc) def findClassInfo(owner: String) = { if (owner.nonEmpty) { findSymbol(textDocument, owner) } else { - textDocument.symbols.find { - case sym => - sym.signature match { - case sig: ClassSignature => - sig.declarations.exists(_.symlinks.contains(symbol)) - case _ => false - } + textDocument.symbols.find { sym => + sym.signature match { + case sig: ClassSignature => + sig.declarations.exists(_.symlinks.contains(symbol)) + case _ => false + } } } } val results = for { currentInfo <- findSymbol(textDocument, symbol) - if (!isClassLike(currentInfo)) + if !isClassLike(currentInfo) classInfo <- findClassInfo(symbol.owner) } yield { classInfo.signature match { - case sig: ClassSignature => - methodInParentSignature(sig, currentInfo) - case _ => Nil + case classSignature: ClassSignature => + val globalSymbolTable = globalTable.globalSymbolTableFor( + workspace.resolve(textDocument.uri) + ) + methodInParentSignature( + classSignature, + currentInfo, + globalSymbolTable + ) + case _ => Seq.empty } } results.getOrElse(Seq.empty) @@ -177,13 +207,18 @@ final class ImplementationProvider( private def methodInParentSignature( sig: ClassSignature, childInfo: SymbolInformation, + globalSymbolTable: Option[GlobalSymbolTable], childASF: Map[String, String] = Map.empty - ): Seq[Location] = { + ): Seq[Either[Location, SymbolInformation]] = { sig.parents.flatMap { - case parentSym: TypeRef => + case parentSym: TypeRef if parentSym.symbol != "scala/AnyRef#" => val parentTextDocument = findSemanticDbForSymbol(parentSym.symbol) - def search(symbol: String) = - parentTextDocument.flatMap(findSymbol(_, symbol)) + + def search(symbol: String): Option[SymbolInformation] = + parentTextDocument + .flatMap(findSymbol(_, symbol)) + .orElse(globalSymbolTable.flatMap(_.safeInfo(symbol))) + val parentASF = AsSeenFrom.calculateAsSeenFrom(parentSym, sig.typeParameters) val asSeenFrom = AsSeenFrom.translateAsSeenFrom(childASF, parentASF) @@ -192,6 +227,7 @@ final class ImplementationProvider( val fromParent = methodInParentSignature( parenClassSig, childInfo, + globalSymbolTable, asSeenFrom ) if (fromParent.isEmpty) { @@ -206,10 +242,10 @@ final class ImplementationProvider( } else { fromParent } - case _ => Nil + case _ => Seq.empty } - case _ => Nil + case _ => Seq.empty } } @@ -220,31 +256,34 @@ final class ImplementationProvider( asSeenFrom: Map[String, String], search: String => Option[SymbolInformation], parentTextDocument: Option[TextDocument] - ): Option[Location] = { + ): Option[Either[Location, SymbolInformation]] = { val matchingSymbol = MethodImplementation.findParentSymbol( childInfo, - sig, parenClassSig, asSeenFrom, search ) - for { - symbol <- matchingSymbol - parentDoc <- parentTextDocument - source = workspace.resolve(parentDoc.uri) - implOccurrence <- findDefOccurrence( - parentDoc, - symbol, - source - ) - range <- implOccurrence.range - distance = TokenEditDistance.fromBuffer( - source, - parentDoc.text, - buffer - ) - revised <- distance.toRevised(range.toLSP) - } yield new Location(source.toNIO.toUri().toString(), revised) + if (matchingSymbol.isDefined && parentTextDocument.isEmpty) { + Some(Right(matchingSymbol.get)) + } else { + for { + symbol <- matchingSymbol + parentDoc <- parentTextDocument + source = workspace.resolve(parentDoc.uri) + implOccurrence <- findDefOccurrence( + parentDoc, + symbol.symbol, + source + ) + range <- implOccurrence.range + distance = TokenEditDistance.fromBuffer( + source, + parentDoc.text, + buffer + ) + revised <- distance.toRevised(range.toLSP) + } yield Left(new Location(source.toNIO.toUri.toString, revised)) + } } private def symbolLocationsFromContext( @@ -328,10 +367,10 @@ final class ImplementationProvider( loc => // we are not interested in local symbols from outside the workspace (loc.symbol.isLocal && loc.file.isEmpty) || - // local symbols ineheritance should only be picked up in the same file + // local symbols inheritance should only be picked up in the same file (loc.symbol.isLocal && loc.file != currentPath) } - directImplementations.toSet ++ directImplementations + directImplementations ++ directImplementations .flatMap { loc => val allPossible = loop(loc.symbol, loc.file) allPossible.map(_.translateAsSeenFrom(loc)) diff --git a/metals/src/main/scala/scala/meta/internal/implementation/MethodImplementation.scala b/metals/src/main/scala/scala/meta/internal/implementation/MethodImplementation.scala index 4898a9101c9..042d6190509 100644 --- a/metals/src/main/scala/scala/meta/internal/implementation/MethodImplementation.scala +++ b/metals/src/main/scala/scala/meta/internal/implementation/MethodImplementation.scala @@ -30,11 +30,10 @@ object MethodImplementation { def findParentSymbol( childSymbol: SymbolInformation, - childClassSig: ClassSignature, parentClassSig: ClassSignature, asSeenFromMap: Map[String, String], findSymbol: String => Option[SymbolInformation] - ): Option[String] = { + ): Option[SymbolInformation] = { val validMethods = for { declarations <- parentClassSig.declarations.toIterable methodSymbol <- declarations.symlinks @@ -52,7 +51,7 @@ object MethodImplementation { if isOverridenMethod(methodSymbolInfo, childSymbol, findParent = true)( context ) - } yield methodSymbol + } yield methodSymbolInfo validMethods.headOption } 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 66b20e54542..6fe336e10cc 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 java.util.Collections import org.eclipse.lsp4j.TextDocumentPositionParams import org.eclipse.lsp4j.Location +import org.eclipse.lsp4j.RenameParams import scala.meta.pc.CancelToken import scala.meta.inputs.Input import scala.meta.internal.metals.MetalsEnrichments._ @@ -18,7 +19,6 @@ import scala.meta.io.AbsolutePath import scala.concurrent.Future import scala.concurrent.ExecutionContext import scala.meta.internal.semanticdb.SymbolOccurrence -import org.eclipse.lsp4j.Position /** * Implements goto definition that works even in code that doesn't parse. @@ -60,7 +60,7 @@ final class DefinitionProvider( warnings.noSemanticdb(path) DefinitionResult.empty } - if (fromSemanticdb.locations.isEmpty()) { + if (fromSemanticdb.locations.isEmpty) { compilers().definition(params, token) } else { Future.successful(fromSemanticdb) @@ -84,47 +84,68 @@ final class DefinitionProvider( .definition(Symbol(sym)) .map(symDef => symDef.path.toInputFromBuffers(buffers)) - def symbolOccurence( - source: AbsolutePath, - dirtyPosition: TextDocumentPositionParams + def symbolOccurrence( + params: RenameParams + ): Option[(SymbolOccurrence, TextDocument)] = { + symbolOccurrence( + FilePosition( + params.getTextDocument.getUri.toAbsolutePath, + params.getPosition + ) + ) + } + + def symbolOccurrence( + params: TextDocumentPositionParams + ): Option[(SymbolOccurrence, TextDocument)] = { + symbolOccurrence( + FilePosition( + params.getTextDocument.getUri.toAbsolutePath, + params.getPosition + ) + ) + } + + def symbolOccurrence( + dirtyFilePosition: FilePosition ): Option[(SymbolOccurrence, TextDocument)] = { for { currentDocument <- semanticdbs - .textDocument(source) + .textDocument(dirtyFilePosition.filePath) .documentIncludingStale posOcc = positionOccurrence( - source, - dirtyPosition, + dirtyFilePosition, currentDocument ) - symbolOccurrence <- { - def mtagsOccurrence = - fromMtags(source, dirtyPosition.getPosition()) - posOcc.occurrence.orElse(mtagsOccurrence) - } + symbolOccurrence <- posOcc.occurrence.orElse( + fromMtags(dirtyFilePosition) + ) } yield (symbolOccurrence, currentDocument) } def positionOccurrence( - source: AbsolutePath, - dirtyPosition: TextDocumentPositionParams, + dirtyFilePosition: FilePosition, snapshot: TextDocument ): ResolvedSymbolOccurrence = { // Convert dirty buffer position to snapshot position in "source" val sourceDistance = - TokenEditDistance.fromBuffer(source, snapshot.text, buffers) + TokenEditDistance.fromBuffer( + dirtyFilePosition.filePath, + snapshot.text, + buffers + ) val snapshotPosition = sourceDistance.toOriginal( - dirtyPosition.getPosition.getLine, - dirtyPosition.getPosition.getCharacter + dirtyFilePosition.position.getLine, + dirtyFilePosition.position.getCharacter ) // Find matching symbol occurrence in SemanticDB snapshot val occurrence = for { - queryPosition <- snapshotPosition.toPosition(dirtyPosition.getPosition) + queryPosition <- snapshotPosition.toPosition(dirtyFilePosition.position) occurrence <- snapshot.occurrences - .find(_.encloses(queryPosition, true)) + .find(_.encloses(queryPosition, includeLastCharacter = true)) // In case of macros we might need to get the postion from the presentation compiler - .orElse(fromMtags(source, queryPosition)) + .orElse(fromMtags(dirtyFilePosition.copy(position = queryPosition))) } yield occurrence ResolvedSymbolOccurrence(sourceDistance, occurrence) @@ -136,7 +157,10 @@ final class DefinitionProvider( snapshot: TextDocument ): DefinitionResult = { val ResolvedSymbolOccurrence(sourceDistance, occurrence) = - positionOccurrence(source, dirtyPosition, snapshot) + positionOccurrence( + FilePosition(source, dirtyPosition.getPosition), + snapshot + ) // Find symbol definition location. val result: Option[DefinitionResult] = occurrence.flatMap { occ => val isLocal = occ.symbol.isLocal || snapshot.definesSymbol(occ.symbol) @@ -158,11 +182,13 @@ final class DefinitionProvider( result.getOrElse(DefinitionResult.empty(occurrence.fold("")(_.symbol))) } - private def fromMtags(source: AbsolutePath, dirtyPos: Position) = { + private def fromMtags( + dirtyFilePosition: FilePosition + ): Option[SymbolOccurrence] = { Mtags - .allToplevels(source.toInput) + .allToplevels(dirtyFilePosition.filePath.toInput) .occurrences - .find(_.encloses(dirtyPos)) + .find(_.encloses(dirtyFilePosition.position)) } private case class DefinitionDestination( diff --git a/metals/src/main/scala/scala/meta/internal/metals/DocumentHighlightProvider.scala b/metals/src/main/scala/scala/meta/internal/metals/DocumentHighlightProvider.scala index 72b21bf63a5..576f77fec2f 100644 --- a/metals/src/main/scala/scala/meta/internal/metals/DocumentHighlightProvider.scala +++ b/metals/src/main/scala/scala/meta/internal/metals/DocumentHighlightProvider.scala @@ -20,15 +20,24 @@ final class DocumentHighlightProvider( def documentHighlight( params: TextDocumentPositionParams - ): java.util.List[DocumentHighlight] = { - val source = params.getTextDocument.getUri.toAbsolutePath - val result = semanticdbs.textDocument(source) + ): List[DocumentHighlight] = { + documentHighlight( + FilePosition( + params.getTextDocument.getUri.toAbsolutePath, + params.getPosition + ) + ) + } + + def documentHighlight( + filePosition: FilePosition + ): List[DocumentHighlight] = { + val result = semanticdbs.textDocument(filePosition.filePath) val highlights = for { doc <- result.documentIncludingStale.toList positionOccurrence = definitionProvider.positionOccurrence( - source, - params, + filePosition, doc ) occ <- positionOccurrence.occurrence.toList @@ -43,7 +52,7 @@ final class DocumentHighlightProvider( DocumentHighlightKind.Read } } yield new DocumentHighlight(revised, kind) - highlights.asJava + highlights } private def findAllAlternatives( diff --git a/metals/src/main/scala/scala/meta/internal/metals/FilePosition.scala b/metals/src/main/scala/scala/meta/internal/metals/FilePosition.scala new file mode 100644 index 00000000000..3bfb9f84db3 --- /dev/null +++ b/metals/src/main/scala/scala/meta/internal/metals/FilePosition.scala @@ -0,0 +1,13 @@ +package scala.meta.internal.metals + +import org.eclipse.lsp4j.Position +import org.eclipse.lsp4j.Location +import scala.meta.internal.metals.MetalsEnrichments._ +import scala.meta.io.AbsolutePath + +case class FilePosition(filePath: AbsolutePath, position: Position) + +object FilePosition { + def locationToFilePosition(location: Location): FilePosition = + FilePosition(location.getUri.toAbsolutePath, location.getRange.getStart) +} 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 109044e3cf0..88f1c5fead8 100644 --- a/metals/src/main/scala/scala/meta/internal/metals/MetalsLanguageServer.scala +++ b/metals/src/main/scala/scala/meta/internal/metals/MetalsLanguageServer.scala @@ -362,12 +362,6 @@ class MetalsLanguageServer( semanticdbs, buffers ) - referencesProvider = new ReferenceProvider( - workspace, - semanticdbs, - buffers, - definitionProvider - ) implementationProvider = new ImplementationProvider( semanticdbs, workspace, @@ -376,11 +370,16 @@ class MetalsLanguageServer( buffers, definitionProvider ) + referencesProvider = new ReferenceProvider( + workspace, + semanticdbs, + buffers, + definitionProvider, + implementationProvider + ) renameProvider = new RenameProvider( referencesProvider, - implementationProvider, definitionProvider, - semanticdbs, definitionIndex, workspace, languageClient, @@ -952,7 +951,7 @@ class MetalsLanguageServer( _.orElse { val path = params.getTextDocument.getUri.toAbsolutePath if (path.isWorksheet) - worksheetProvider.hover(path, params.getPosition()) + worksheetProvider.hover(path, params.getPosition) else None }.orNull @@ -964,7 +963,7 @@ class MetalsLanguageServer( params: TextDocumentPositionParams ): CompletableFuture[util.List[DocumentHighlight]] = CancelTokens { _ => - documentHighlightProvider.documentHighlight(params) + documentHighlightProvider.documentHighlight(params).asJava } @JsonRequest("textDocument/documentSymbol") @@ -1021,7 +1020,7 @@ class MetalsLanguageServer( params: TextDocumentPositionParams ): CompletableFuture[l.Range] = CancelTokens { _ => - renameProvider.prepareRename(params).getOrElse(null) + renameProvider.prepareRename(params).orNull } @JsonRequest("textDocument/rename") @@ -1875,8 +1874,7 @@ class MetalsLanguageServer( (for { doc <- semanticDBDoc positionOccurrence = definitionProvider.positionOccurrence( - source, - position, + FilePosition(source, position.getPosition), doc ) occ <- positionOccurrence.occurrence 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 c33149145a7..80add4718c3 100644 --- a/metals/src/main/scala/scala/meta/internal/metals/ReferenceProvider.scala +++ b/metals/src/main/scala/scala/meta/internal/metals/ReferenceProvider.scala @@ -1,12 +1,13 @@ package scala.meta.internal.metals -import com.google.common.hash.BloomFilter -import com.google.common.hash.Funnels import java.nio.charset.StandardCharsets import java.nio.file.Path -import org.eclipse.lsp4j.Location +import com.google.common.hash.BloomFilter +import com.google.common.hash.Funnels import org.eclipse.lsp4j.ReferenceParams +import org.eclipse.lsp4j.Location import scala.collection.concurrent.TrieMap +import scala.meta.internal.implementation.ImplementationProvider import scala.meta.internal.metals.MetalsEnrichments._ import scala.meta.internal.mtags.DefinitionAlternatives.GlobalSymbol import scala.meta.internal.mtags.SemanticdbClasspath @@ -15,18 +16,20 @@ import scala.meta.internal.mtags.Symbol import scala.meta.internal.semanticdb.Scala._ import scala.meta.internal.semanticdb.SymbolInformation import scala.meta.internal.semanticdb.SymbolOccurrence +import scala.meta.internal.semanticdb.Synthetic import scala.meta.internal.semanticdb.TextDocument import scala.meta.internal.semanticdb.TextDocuments import scala.meta.internal.{semanticdb => s} +import scala.meta.internal.metals.FilePosition.locationToFilePosition import scala.meta.io.AbsolutePath import scala.util.control.NonFatal -import scala.meta.internal.semanticdb.Synthetic final class ReferenceProvider( workspace: AbsolutePath, semanticdbs: Semanticdbs, buffers: Buffers, - definition: DefinitionProvider + definition: DefinitionProvider, + implementation: ImplementationProvider ) { var referencedPackages: BloomFilter[CharSequence] = BloomFilters.create(10000) val index: TrieMap[Path, BloomFilter[CharSequence]] = @@ -65,31 +68,54 @@ final class ReferenceProvider( resizeReferencedPackages() } + def references(params: ReferenceParams): ReferencesResult = { + references( + FilePosition( + params.getTextDocument.getUri.toAbsolutePath, + params.getPosition + ), + params.getContext.isIncludeDeclaration + ) + } + def references( - params: ReferenceParams, - canSkipExactMatchCheck: Boolean = true, - includeSynthetics: Synthetic => Boolean = _ => true + filePosition: FilePosition, + includeDeclaration: Boolean ): ReferencesResult = { - val source = params.getTextDocument.getUri.toAbsolutePath - semanticdbs.textDocument(source).documentIncludingStale match { + semanticdbs + .textDocument(filePosition.filePath) + .documentIncludingStale match { case Some(doc) => val ResolvedSymbolOccurrence(distance, maybeOccurrence) = - definition.positionOccurrence(source, params, doc) + definition.positionOccurrence( + filePosition, + doc + ) maybeOccurrence match { case Some(occurrence) => - val alternatives = referenceAlternatives(doc, occurrence) - val locations = references( - source, - params, - doc, - distance, - occurrence, - alternatives, - params.getContext.isIncludeDeclaration, - canSkipExactMatchCheck, - includeSynthetics - ) - ReferencesResult(occurrence.symbol, locations) + val symbolName = occurrence.symbol.desc.name.value + val shouldNotIncludeInheritance = + ReferenceProvider.methodsSearchedWithoutInheritance.contains( + symbolName + ) + + if (shouldNotIncludeInheritance) { + currentSymbolReferences( + filePosition, + includeDeclaration + ) + } else { + ReferencesResult( + occurrence.symbol, + allInheritanceReferences( + occurrence, + doc, + filePosition, + failWhenReachingDependencySymbol = false, + fnIncludeSynthetics = _ => true + ) + ) + } case None => ReferencesResult.empty } @@ -98,13 +124,118 @@ final class ReferenceProvider( } } + def allInheritanceReferences( + symbolOccurrence: SymbolOccurrence, + doc: TextDocument, + filePosition: FilePosition, + fnIncludeSynthetics: Synthetic => Boolean, + failWhenReachingDependencySymbol: Boolean, + canSkipExactMatchCheck: Boolean = true + ): Seq[Location] = { + val parentSymbols = implementation + .topMethodParents(doc, symbolOccurrence.symbol) + + if (failWhenReachingDependencySymbol && parentSymbols.exists(_.isRight)) { + Seq.empty + } else { + + val mainDefinitions: Seq[Either[FilePosition, SymbolInformation]] = { + if (parentSymbols.isEmpty) List(Left(filePosition)) + else parentSymbols.map(ps => ps.left.map(locationToFilePosition)) + } + + val topParentWorkspaceLocations = + mainDefinitions.flatMap(_.swap.toOption) + + val isLocal = symbolOccurrence.symbol.isLocal + val currentReferences = topParentWorkspaceLocations + .flatMap( + currentSymbolReferences( + _, + includeDeclaration = isLocal, + canSkipExactMatchCheck = canSkipExactMatchCheck, + includeSynthetics = fnIncludeSynthetics + ).locations + ) + val definitionLocation = { + val parentSymbolLocs = parentSymbols.flatMap(_.left.toOption) + if (parentSymbolLocs.isEmpty) + definition + .fromSymbol(symbolOccurrence.symbol) + .asScala + .filter(_.getUri.isScalaFilename) + else parentSymbolLocs + } + val implReferences = mainDefinitions.flatMap( + implementations( + _, + filePosition.filePath, + !symbolOccurrence.symbol.desc.isType, + canSkipExactMatchCheck + ) + ) + + currentReferences ++ implReferences ++ definitionLocation + } + } + + private def implementations( + filePosition: Either[FilePosition, SymbolInformation], + source: AbsolutePath, + shouldCheckImplementation: Boolean, + canSkipExactMatchCheck: Boolean + ): Seq[Location] = { + if (shouldCheckImplementation) { + for { + implLoc <- implementation.implementations(filePosition, source) + loc <- currentSymbolReferences( + locationToFilePosition(implLoc), + includeDeclaration = true, + canSkipExactMatchCheck = canSkipExactMatchCheck + ).locations + } yield loc + } else { + Nil + } + } + + def currentSymbolReferences( + filePosition: FilePosition, + includeDeclaration: Boolean, + canSkipExactMatchCheck: Boolean = true, + includeSynthetics: Synthetic => Boolean = _ => true + ): ReferencesResult = { + val referencesResult = for { + doc <- semanticdbs + .textDocument(filePosition.filePath) + .documentIncludingStale + ResolvedSymbolOccurrence(distance, maybeOccurrence) = definition + .positionOccurrence( + filePosition, + doc + ) + occurrence <- maybeOccurrence + alternatives = referenceAlternatives(doc, occurrence) + locations = currentSymbolReferences( + doc, + distance, + occurrence, + filePosition.filePath.toURI.toString, + alternatives, + includeDeclaration, + canSkipExactMatchCheck, + includeSynthetics + ) + } yield ReferencesResult(occurrence.symbol, locations) + referencesResult.getOrElse(ReferencesResult.empty) + } + // Returns alternatives symbols for which "goto definition" resolves to the occurrence symbol. private def referenceAlternatives( doc: TextDocument, occ: SymbolOccurrence ): Set[String] = { val name = occ.symbol.desc.name.value - val isCopyOrApply = Set("apply", "copy") // Returns true if `info` is the companion object matching the occurrence class symbol. def isCompanionObject(info: SymbolInformation): Boolean = info.isObject && @@ -173,24 +304,23 @@ final class ReferenceProvider( } yield doc.symbol isCandidate -- nonSyntheticSymbols } - private def references( - source: AbsolutePath, - params: ReferenceParams, + private def currentSymbolReferences( snapshot: TextDocument, distance: TokenEditDistance, - occ: SymbolOccurrence, + symbolOccurrence: SymbolOccurrence, + symbolOccurrenceUri: String, alternatives: Set[String], isIncludeDeclaration: Boolean, canSkipExactMatchCheck: Boolean, includeSynthetics: Synthetic => Boolean ): Seq[Location] = { - val isSymbol = alternatives + occ.symbol - if (occ.symbol.isLocal) { + val isSymbol = alternatives + symbolOccurrence.symbol + if (symbolOccurrence.symbol.isLocal) { referenceLocations( snapshot, isSymbol, distance, - params.getTextDocument.getUri, + symbolOccurrenceUri, isIncludeDeclaration, canSkipExactMatchCheck, includeSynthetics @@ -199,7 +329,7 @@ final class ReferenceProvider( val visited = scala.collection.mutable.Set.empty[AbsolutePath] val results: Iterator[Location] = for { (path, bloom) <- index.iterator - if bloom.mightContain(occ.symbol) + if bloom.mightContain(symbolOccurrence.symbol) scalaPath <- SemanticdbClasspath .toScala(workspace, AbsolutePath(path)) .iterator @@ -333,3 +463,8 @@ final class ReferenceProvider( } } + +object ReferenceProvider { + val methodsSearchedWithoutInheritance: Set[String] = Set("eq", "equals", + "hashCode", "toString", "clone", "notify", "wait", "getClass") +} 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 a50fbdc0a95..181e4b0c232 100644 --- a/metals/src/main/scala/scala/meta/internal/rename/RenameProvider.scala +++ b/metals/src/main/scala/scala/meta/internal/rename/RenameProvider.scala @@ -1,24 +1,19 @@ package scala.meta.internal.rename +import scala.meta.internal.metals.DefinitionProvider +import scala.meta.internal.metals.MetalsLanguageClient import scala.meta.internal.metals.ReferenceProvider import org.eclipse.lsp4j.RenameParams import org.eclipse.lsp4j.WorkspaceEdit -import scala.meta.internal.implementation.ImplementationProvider import org.eclipse.lsp4j.TextDocumentPositionParams -import org.eclipse.lsp4j.ReferenceParams -import org.eclipse.lsp4j.TextDocumentIdentifier import org.eclipse.lsp4j.TextEdit + import scala.meta.internal.metals.MetalsEnrichments._ -import org.eclipse.lsp4j.Position -import org.eclipse.lsp4j.ReferenceContext -import scala.meta.internal.metals.DefinitionProvider import scala.meta.internal.mtags.{Symbol => MSymbol} -import scala.meta.internal.mtags.Semanticdbs import scala.meta.internal.semanticdb.Scala._ import scala.meta.internal.mtags.GlobalSymbolIndex import scala.meta.io.AbsolutePath import org.eclipse.lsp4j.Location -import scala.meta.internal.metals.MetalsLanguageClient import org.eclipse.lsp4j.MessageParams import org.eclipse.lsp4j.MessageType import org.eclipse.lsp4j.{Range => LSPRange} @@ -31,16 +26,17 @@ import org.eclipse.lsp4j.VersionedTextDocumentIdentifier import org.eclipse.lsp4j.ResourceOperation import org.eclipse.lsp4j.RenameFile import java.util.concurrent.ConcurrentLinkedQueue + import scala.meta.internal.async.ConcurrentQueue -import scala.meta.internal.semanticdb.Synthetic +import scala.meta.internal.metals.FilePosition +import scala.meta.internal.metals.FilePosition.locationToFilePosition import scala.meta.internal.semanticdb.SelectTree +import scala.meta.internal.semanticdb.Synthetic import scala.meta.internal.metals.MetalsServerConfig final class RenameProvider( referenceProvider: ReferenceProvider, - implementationProvider: ImplementationProvider, definitionProvider: DefinitionProvider, - semanticdbs: Semanticdbs, index: GlobalSymbolIndex, workspace: AbsolutePath, client: MetalsLanguageClient, @@ -49,16 +45,14 @@ final class RenameProvider( metalsConfig: MetalsServerConfig ) { - private var awaitingSave = new ConcurrentLinkedQueue[() => Unit] + private val awaitingSave = new ConcurrentLinkedQueue[() => Unit] def prepareRename(params: TextDocumentPositionParams): Option[LSPRange] = { - if (!compilations.currentlyCompiling.isEmpty) { + if (compilations.currentlyCompiling.nonEmpty) { client.showMessage(isCompiling) None } else { - val source = params.getTextDocument.getUri.toAbsolutePath - val symbolOccurence = - definitionProvider.symbolOccurence(source, params) + val symbolOccurence = definitionProvider.symbolOccurrence(params) for { (occurence, semanticDb) <- symbolOccurence if canRenameSymbol(occurence.symbol, None) @@ -68,18 +62,18 @@ final class RenameProvider( } def rename(params: RenameParams): WorkspaceEdit = { - if (!compilations.currentlyCompiling.isEmpty) { + def includeSynthetic(syn: Synthetic) = { + syn.tree match { + case SelectTree(_, id) => + id.exists(_.symbol.desc.name.toString == "apply") + case _ => false + } + } + + if (compilations.currentlyCompiling.nonEmpty) { client.showMessage(isCompiling) new WorkspaceEdit() } else { - val source = params.getTextDocument.getUri.toAbsolutePath - val textParams = new TextDocumentPositionParams( - params.getTextDocument(), - params.getPosition() - ) - - val symbolOccurence = - definitionProvider.symbolOccurence(source, textParams) val suggestedName = params.getNewName() val newName = @@ -87,50 +81,32 @@ final class RenameProvider( suggestedName.substring(1, suggestedName.length() - 1) else suggestedName - def includeSynthetic(syn: Synthetic) = { - syn.tree match { - case SelectTree(_, id) => - id.exists(_.symbol.desc.name.toString == "apply") - case _ => false - } - } + val filePosition = FilePosition( + params.getTextDocument.getUri.toAbsolutePath, + params.getPosition + ) - val allReferences = for { - (occurence, semanticDb) <- symbolOccurence.toIterable - if canRenameSymbol(occurence.symbol, Option(newName)) - parentSymbols = implementationProvider - .topMethodParents(occurence.symbol, semanticDb) - txtParams <- { - if (parentSymbols.isEmpty) List(textParams) - else parentSymbols.map(toTextParams) - } - isLocal = occurence.symbol.isLocal - currentReferences = referenceProvider - .references( - // we can't get definition by name for local symbols - toReferenceParams(txtParams, includeDeclaration = isLocal), + val symbolOccurrence = + definitionProvider.symbolOccurrence(params) + val allReferences = symbolOccurrence match { + case Some((occurrence, doc)) + if canRenameSymbol(occurrence.symbol, Some(newName)) => + referenceProvider.allInheritanceReferences( + occurrence, + doc, + filePosition, + includeSynthetic, canSkipExactMatchCheck = false, - includeSynthetics = includeSynthetic + failWhenReachingDependencySymbol = true + ) ++ companionReferences( + occurrence.symbol ) - .locations - definitionLocation = { - if (parentSymbols.isEmpty) - definitionProvider - .fromSymbol(occurence.symbol) - .asScala - .filter(_.getUri().isScalaFilename) - else parentSymbols - } - companionRefs = companionReferences(occurence.symbol) - implReferences = implementations( - txtParams, - !occurence.symbol.desc.isType - ) - loc <- currentReferences ++ implReferences ++ companionRefs ++ definitionLocation - } yield loc + case _ => + List() + } - def isOccurence(fn: String => Boolean): Boolean = { - symbolOccurence.exists { + def isOccurrence(fn: String => Boolean): Boolean = { + symbolOccurrence.exists { case (occ, _) => fn(occ.symbol) } } @@ -139,10 +115,11 @@ final class RenameProvider( (uri, locs) <- allReferences.toList.distinct.groupBy(_.getUri()) } yield { val textEdits = for (loc <- locs) yield { - textEdit(isOccurence, loc, newName) + textEdit(isOccurrence, loc, newName) } - Seq(uri.toAbsolutePath -> textEdits.toList) + Seq(uri.toAbsolutePath -> textEdits) } + val fileChanges = allChanges.flatten.toMap val shouldRenameInBackground = !metalsConfig.openFilesOnRenames || @@ -164,7 +141,7 @@ final class RenameProvider( val edits = documentEdits(openedEdits) val renames = - fileRenames(isOccurence, fileChanges.keySet, newName) + fileRenames(isOccurrence, fileChanges.keySet, newName) new WorkspaceEdit((edits ++ renames).asJava) } } @@ -179,20 +156,20 @@ final class RenameProvider( openedEdits.map { case (file, edits) => val textId = new VersionedTextDocumentIdentifier() - textId.setUri(file.toURI.toString()) + textId.setUri(file.toURI.toString) val ed = new TextDocumentEdit(textId, edits.asJava) LSPEither.forLeft[TextDocumentEdit, ResourceOperation](ed) }.toList } private def fileRenames( - isOccurence: (String => Boolean) => Boolean, + isOccurrence: (String => Boolean) => Boolean, fileChanges: Set[AbsolutePath], newName: String ): Option[LSPEither[TextDocumentEdit, ResourceOperation]] = { fileChanges .find { file => - isOccurence { str => + isOccurrence { str => str.owner.isPackage && (str.desc.isType || str.desc.isTerm) && file.toURI.toString.endsWith(s"/${str.desc.name.value}.scala") @@ -208,15 +185,30 @@ final class RenameProvider( } } + private def changeClosedFiles( + fileEdits: Map[AbsolutePath, List[TextEdit]] + ): Unit = { + fileEdits.toArray.par.foreach { + case (file, changes) => + val text = file.readText + val newText = TextEdits.applyEdits(text, changes) + file.writeText(newText) + } + } + private def companionReferences(sym: String): Seq[Location] = { val results = for { companionSymbol <- companion(sym).toIterable loc <- definitionProvider .fromSymbol(companionSymbol) .asScala - if loc.getUri().isScalaFilename + if loc.getUri.isScalaFilename companionLocs <- referenceProvider - .references(toReferenceParams(loc, includeDeclaration = false)) + .currentSymbolReferences( + locationToFilePosition(loc), + includeDeclaration = false, + canSkipExactMatchCheck = true + ) .locations :+ loc } yield companionLocs results.toList @@ -240,37 +232,10 @@ final class RenameProvider( ) } - private def changeClosedFiles( - fileEdits: Map[AbsolutePath, List[TextEdit]] - ) = { - fileEdits.toArray.par.foreach { - case (file, changes) => - val text = file.readText - val newText = TextEdits.applyEdits(text, changes) - file.writeText(newText) - } - } - - private def implementations( - textParams: TextDocumentPositionParams, - shouldCheckImplementation: Boolean - ): Seq[Location] = { - if (shouldCheckImplementation) { - for { - implLoc <- implementationProvider.implementations(textParams) - locParams = toReferenceParams(implLoc, includeDeclaration = true) - loc <- referenceProvider.references(locParams).locations - } yield loc - } else { - Nil - } - } - private def canRenameSymbol(symbol: String, newName: Option[String]) = { - val forbiddenMethods = Set("equals", "hashCode", "unapply", "unary_!", "!") val desc = symbol.desc val name = desc.name.value - val isForbidden = forbiddenMethods(name) + val isForbidden = RenameProvider.forbiddenToModifyMethods.contains(name) if (isForbidden) { client.showMessage(forbiddenRename(name, newName)) } @@ -298,26 +263,23 @@ final class RenameProvider( } private def textEdit( - isOccurence: (String => Boolean) => Boolean, + isOccurrence: (String => Boolean) => Boolean, loc: Location, newName: String ): TextEdit = { - val isApply = isOccurence(str => str.desc.name.value == "apply") + val isApply = isOccurrence(str => str.desc.name.value == "apply") lazy val default = new TextEdit( - loc.getRange(), + loc.getRange, newName ) if (isApply) { - val locSource = loc.getUri.toAbsolutePath - val textParams = new TextDocumentPositionParams - textParams.setPosition(loc.getRange().getStart()) val isImplicitApply = definitionProvider - .symbolOccurence(locSource, textParams) + .symbolOccurrence(locationToFilePosition(loc)) .exists(_._1.symbol.desc.name.value != "apply") if (isImplicitApply) { - val range = loc.getRange() - range.setStart(range.getEnd()) + val range = loc.getRange + range.setStart(range.getEnd) new TextEdit( range, "." + newName @@ -330,51 +292,6 @@ final class RenameProvider( } } - private def toReferenceParams( - textDoc: TextDocumentIdentifier, - pos: Position, - includeDeclaration: Boolean - ): ReferenceParams = { - val referenceParams = new ReferenceParams() - referenceParams.setPosition(pos) - referenceParams.setTextDocument(textDoc) - val context = new ReferenceContext() - context.setIncludeDeclaration(includeDeclaration) - referenceParams.setContext(context) - referenceParams - } - - private def toReferenceParams( - location: Location, - includeDeclaration: Boolean - ): ReferenceParams = { - val textDoc = new TextDocumentIdentifier() - textDoc.setUri(location.getUri()) - toReferenceParams( - textDoc, - location.getRange().getStart(), - includeDeclaration - ) - } - - private def toReferenceParams( - params: TextDocumentPositionParams, - includeDeclaration: Boolean - ): ReferenceParams = { - toReferenceParams( - params.getTextDocument(), - params.getPosition(), - includeDeclaration - ) - } - - private def toTextParams(location: Location): TextDocumentPositionParams = { - new TextDocumentPositionParams( - new TextDocumentIdentifier(location.getUri()), - location.getRange().getStart() - ) - } - private def fileThreshold( files: Int ): MessageParams = { @@ -424,3 +341,9 @@ final class RenameProvider( new MessageParams(MessageType.Error, message) } } + +object RenameProvider { + val forbiddenToModifyMethods: Set[String] = + Set("equals", "hashCode", "toString", "unapply", "unary_!", "!") + +} diff --git a/tests/unit/src/main/scala/tests/TestingServer.scala b/tests/unit/src/main/scala/tests/TestingServer.scala index fc4340f5b7e..315121e90a9 100644 --- a/tests/unit/src/main/scala/tests/TestingServer.scala +++ b/tests/unit/src/main/scala/tests/TestingServer.scala @@ -3,17 +3,19 @@ package tests import java.io.IOException import java.net.URI import java.nio.charset.StandardCharsets -import java.nio.file.FileVisitResult -import java.nio.file.Files -import java.nio.file.Path import java.nio.file.Paths +import java.nio.file.Path +import java.nio.file.Files import java.nio.file.SimpleFileVisitor +import java.nio.file.FileVisitResult import java.nio.file.attribute.BasicFileAttributes import java.util import java.util.Collections import java.util.concurrent.ScheduledExecutorService import ch.epfl.scala.{bsp4j => b} import org.eclipse.lsp4j.ClientCapabilities +import org.eclipse.lsp4j.CodeActionContext +import org.eclipse.lsp4j.CodeActionParams import org.eclipse.lsp4j.CodeLensParams import org.eclipse.lsp4j.CompletionList import org.eclipse.lsp4j.CompletionParams @@ -24,6 +26,7 @@ import org.eclipse.lsp4j.DidOpenTextDocumentParams import org.eclipse.lsp4j.DidSaveTextDocumentParams import org.eclipse.lsp4j.DocumentFormattingParams import org.eclipse.lsp4j.DocumentOnTypeFormattingParams +import org.eclipse.lsp4j.DocumentRangeFormattingParams import org.eclipse.lsp4j.DocumentSymbolParams import org.eclipse.lsp4j.ExecuteCommandParams import org.eclipse.lsp4j.FoldingRangeCapabilities @@ -34,6 +37,8 @@ import org.eclipse.lsp4j.InitializedParams import org.eclipse.lsp4j.Location import org.eclipse.lsp4j.ReferenceContext import org.eclipse.lsp4j.ReferenceParams +import org.eclipse.lsp4j.RenameFile +import org.eclipse.lsp4j.RenameParams import org.eclipse.lsp4j.TextDocumentClientCapabilities import org.eclipse.lsp4j.TextDocumentContentChangeEvent import org.eclipse.lsp4j.TextDocumentIdentifier @@ -42,6 +47,7 @@ import org.eclipse.lsp4j.TextDocumentPositionParams import org.eclipse.lsp4j.TextEdit import org.eclipse.lsp4j.VersionedTextDocumentIdentifier import org.eclipse.lsp4j.WorkspaceClientCapabilities +import org.eclipse.lsp4j.WorkspaceEdit import org.eclipse.lsp4j.WorkspaceFolder import org.eclipse.{lsp4j => l} import tests.MetalsTestEnrichments._ @@ -50,48 +56,42 @@ import scala.collection.mutable import scala.collection.mutable.ListBuffer import scala.concurrent.ExecutionContextExecutorService import scala.concurrent.Future +import scala.concurrent.Promise import scala.meta.Input import scala.meta.internal.io.FileIO import scala.meta.internal.io.PathIO +import scala.meta.internal.metals.MetalsEnrichments._ +import scala.meta.internal.metals.PositionSyntax._ import scala.meta.internal.metals.Buffers import scala.meta.internal.metals.Debug import scala.meta.internal.metals.DidFocusResult import scala.meta.internal.metals.WindowStateDidChangeParams import scala.meta.internal.metals.Directories -import scala.meta.internal.metals.MetalsEnrichments._ import scala.meta.internal.metals.MetalsLanguageServer import scala.meta.internal.metals.MetalsServerConfig -import scala.meta.internal.metals.PositionSyntax._ import scala.meta.internal.metals.ProgressTicks import scala.meta.internal.metals.Time import scala.meta.internal.metals.UserConfiguration +import scala.meta.internal.metals.ClientExperimentalCapabilities +import scala.meta.internal.metals.debug.Stoppage +import scala.meta.internal.metals.debug.TestDebugger import scala.meta.internal.mtags.Semanticdbs import scala.meta.internal.semanticdb.Scala.Symbols import scala.meta.internal.semanticdb.Scala._ -import scala.meta.internal.{semanticdb => s} import scala.meta.internal.tvp.TreeViewChildrenParams +import scala.meta.internal.tvp.TreeViewProvider +import scala.meta.internal.{semanticdb => s} import scala.meta.io.AbsolutePath import scala.meta.io.RelativePath +import scala.util.Properties +import scala.util.matching.Regex import scala.{meta => m} -import scala.meta.internal.tvp.TreeViewProvider -import org.eclipse.lsp4j.DocumentRangeFormattingParams -import scala.concurrent.Promise -import scala.meta.internal.metals.ClientExperimentalCapabilities -import scala.meta.internal.metals.ServerCommands -import scala.meta.internal.metals.debug.TestDebugger import scala.meta.internal.metals.DebugSession -import scala.util.matching.Regex -import org.eclipse.lsp4j.RenameParams +import scala.meta.internal.metals.ServerCommands import scala.meta.internal.metals.TextEdits -import org.eclipse.lsp4j.WorkspaceEdit -import org.eclipse.lsp4j.RenameFile -import scala.meta.internal.metals.debug.Stoppage -import scala.util.Properties -import org.eclipse.lsp4j.CodeActionParams -import org.eclipse.lsp4j.CodeActionContext /** - * Wrapper around `MetalsLanguageServer` with helpers methods for testing purpopses. + * Wrapper around `MetalsLanguageServer` with helpers methods for testing purposes. * * - manages text synchronization, example didSave writes file contents to disk. * - pretty-prints results of textDocument/definition for readable multiline string diffing. @@ -214,6 +214,18 @@ final class TestingServer( expectedDiff ) } + + def assertReferenceDiff( + filename: String, + substring: String, + expectedDiff: String + )(implicit loc: munit.Location): Unit = { + Assertions.assertNoDiff( + references(filename, substring), + expectedDiff + ) + } + def workspaceReferences(): WorkspaceSymbolReferences = { val inverse = mutable.Map.empty[SymbolReference, mutable.ListBuffer[Location]] @@ -876,7 +888,7 @@ final class TestingServer( def references( filename: String, substring: String - ): Future[String] = { + ): String = { val path = toPath(filename) val input = path.toInputFromBuffers(buffers) val index = input.text.lastIndexOf(substring) @@ -890,18 +902,25 @@ final class TestingServer( val offset = index + substring.length - 1 val pos = m.Position.Range(input, offset, offset + 1) params.setPosition(new l.Position(pos.startLine, pos.startColumn)) - server.references(params).asScala.map { r => - r.asScala - .map { l => - val path = l.getUri.toAbsolutePath - val input = path - .toInputFromBuffers(buffers) - .copy(path = path.toRelative(workspace).toURI(false).toString) - val pos = l.getRange.toMeta(input) - pos.formatMessage("info", "reference") - } - .mkString("\n") - } + server + .referencesResult(params) + .locations + .sortBy(loc => + ( + loc.getUri, + loc.getRange.getStart.getLine, + loc.getRange.getStart.getCharacter + ) + ) + .map { l => + val path = l.getUri.toAbsolutePath + val input = path + .toInputFromBuffers(buffers) + .copy(path = path.toRelative(workspace).toURI(false).toString) + val pos = l.getRange.toMeta(input) + pos.formatMessage("info", "reference") + } + .mkString("\n") } def formatting(filename: String): Future[Unit] = { diff --git a/tests/unit/src/test/scala/tests/ReferenceLspSuite.scala b/tests/unit/src/test/scala/tests/ReferenceLspSuite.scala index 367f537d561..9244a0c76ed 100644 --- a/tests/unit/src/test/scala/tests/ReferenceLspSuite.scala +++ b/tests/unit/src/test/scala/tests/ReferenceLspSuite.scala @@ -159,6 +159,7 @@ class ReferenceLspSuite extends BaseLspSuite("reference") { } test("var") { + cleanWorkspace() for { _ <- server.initialize( """ @@ -182,6 +183,7 @@ class ReferenceLspSuite extends BaseLspSuite("reference") { } test("implicit") { + cleanWorkspace() for { _ <- server.initialize( """ @@ -212,4 +214,174 @@ class ReferenceLspSuite extends BaseLspSuite("reference") { _ = server.assertReferenceDefinitionBijection() } yield () } + + test("method-hierarchy") { + cleanWorkspace() + for { + _ <- server.initialize( + """ + |/metals.json + |{ + | "a": {} + |} + |/a/src/main/scala/a/A.scala + | + |package a + | + |object TestHierarchy { + | + | class A { def fx(): Unit = () } + | class B extends A { override def fx(): Unit = () } + | class C extends B { override def fx(): Unit = () } + | class D extends C + | class E extends D { override def fx(): Unit = () } + | + | class X { def fx(): Unit = () } + | + | val a = new A() + | a.fx() + | + | val b = new B(); + | b.fx() + | + | val c = new C(); + | c.fx() + | + | val d = new D(); + | d.fx() + | + | val e = new E(); + | e.fx() + |} + | + |""".stripMargin + ) + _ <- server.didOpen("a/src/main/scala/a/A.scala") + _ = assertNoDiagnostics() + expectedDiff = """ + |a/src/main/scala/a/A.scala:6:17: info: reference + | class A { def fx(): Unit = () } + | ^^ + |a/src/main/scala/a/A.scala:7:36: info: reference + | class B extends A { override def fx(): Unit = () } + | ^^ + |a/src/main/scala/a/A.scala:8:36: info: reference + | class C extends B { override def fx(): Unit = () } + | ^^ + |a/src/main/scala/a/A.scala:10:36: info: reference + | class E extends D { override def fx(): Unit = () } + | ^^ + |a/src/main/scala/a/A.scala:15:5: info: reference + | a.fx() + | ^^ + |a/src/main/scala/a/A.scala:18:5: info: reference + | b.fx() + | ^^ + |a/src/main/scala/a/A.scala:21:5: info: reference + | c.fx() + | ^^ + |a/src/main/scala/a/A.scala:24:5: info: reference + | d.fx() + | ^^ + |a/src/main/scala/a/A.scala:27:5: info: reference + | e.fx() + | ^^ + |""".stripMargin + _ = server.assertReferenceDiff( + "a/src/main/scala/a/A.scala", + "b.fx", + expectedDiff + ) + _ = server.assertReferenceDiff( + "a/src/main/scala/a/A.scala", + "e.fx", + expectedDiff + ) + _ = server.assertReferenceDiff( + "a/src/main/scala/a/A.scala", + "a.fx", + expectedDiff + ) + } yield () + } + + test("method-hierarchy-multiple-files") { + cleanWorkspace() + for { + _ <- server.initialize( + """ + |/metals.json + |{ + | "a": {}, + | "b": {"dependsOn": ["a"]} + |} + |/a/src/main/scala/a/A.scala + |package a + |class A { def fx(): Unit = () } + |class B extends A { override def fx(): Unit = () } + |class C extends B { override def fx(): Unit = () } + |class D extends C + |class E extends D { override def fx(): Unit = () } + | + |object A { + | val a = new A() + | a.fx() + | val b = new B(); + | b.fx() + | val c = new C(); + | c.fx() + | val d = new D(); + | d.fx() + | val e = new E(); + | e.fx() + |} + |/b/src/main/scala/b/B.scala + |package b + |object B { + | val bc = new a.C() + | bc.fx() + |} + |""".stripMargin + ) + _ <- server.didOpen("b/src/main/scala/b/B.scala") + _ = assertNoDiagnostics() + expectedDiff = """ + |a/src/main/scala/a/A.scala:2:15: info: reference + |class A { def fx(): Unit = () } + | ^^ + |a/src/main/scala/a/A.scala:3:34: info: reference + |class B extends A { override def fx(): Unit = () } + | ^^ + |a/src/main/scala/a/A.scala:4:34: info: reference + |class C extends B { override def fx(): Unit = () } + | ^^ + |a/src/main/scala/a/A.scala:6:34: info: reference + |class E extends D { override def fx(): Unit = () } + | ^^ + |a/src/main/scala/a/A.scala:10:5: info: reference + | a.fx() + | ^^ + |a/src/main/scala/a/A.scala:12:5: info: reference + | b.fx() + | ^^ + |a/src/main/scala/a/A.scala:14:5: info: reference + | c.fx() + | ^^ + |a/src/main/scala/a/A.scala:16:5: info: reference + | d.fx() + | ^^ + |a/src/main/scala/a/A.scala:18:5: info: reference + | e.fx() + | ^^ + |b/src/main/scala/b/B.scala:4:6: info: reference + | bc.fx() + | ^^ + |""".stripMargin + _ = server.assertReferenceDiff( + "b/src/main/scala/b/B.scala", + "bc.fx", + expectedDiff + ) + } yield () + } } diff --git a/tests/unit/src/test/scala/tests/RenameLspSuite.scala b/tests/unit/src/test/scala/tests/RenameLspSuite.scala index 961d7d96e07..d3c50703b21 100644 --- a/tests/unit/src/test/scala/tests/RenameLspSuite.scala +++ b/tests/unit/src/test/scala/tests/RenameLspSuite.scala @@ -1,6 +1,7 @@ package tests import scala.concurrent.Future import munit.Location +import munit.TestOptions class RenameLspSuite extends BaseLspSuite("rename") { @@ -576,7 +577,7 @@ class RenameLspSuite extends BaseLspSuite("rename") { ) def renamed( - name: String, + name: TestOptions, input: String, newName: String, nonOpened: Set[String] = Set.empty, @@ -605,7 +606,7 @@ class RenameLspSuite extends BaseLspSuite("rename") { ) def check( - name: String, + name: TestOptions, input: String, newName: String, notRenamed: Boolean = false, @@ -639,8 +640,7 @@ class RenameLspSuite extends BaseLspSuite("rename") { ) } - val openedFiles = files.keySet - .filterNot(file => nonOpened.contains(file)) + val openedFiles = files.keySet.diff(nonOpened) val fullInput = input.replaceAll(allMarkersRegex, "") for { _ <- server.initialize( diff --git a/tests/unit/src/test/scala/tests/WorkspaceSymbolLspSuite.scala b/tests/unit/src/test/scala/tests/WorkspaceSymbolLspSuite.scala index 33fedf1c226..cd52d9822d6 100644 --- a/tests/unit/src/test/scala/tests/WorkspaceSymbolLspSuite.scala +++ b/tests/unit/src/test/scala/tests/WorkspaceSymbolLspSuite.scala @@ -142,10 +142,13 @@ class WorkspaceSymbolLspSuite extends BaseLspSuite("workspace-symbol") { _ = server.workspaceSymbol("scala.None") option = ".metals/readonly/scala/Option.scala" _ <- server.didOpen(option) - references <- server.references(option, "object None") + references = server.references(option, "object None") _ = assertNoDiff( references, - """|a/src/main/scala/a/A.scala:4:24: info: reference + """|.metals/readonly/scala/Option.scala:527:13: info: reference + |case object None extends Option[Nothing] { + | ^^^^ + |a/src/main/scala/a/A.scala:4:24: info: reference | val x: Option[Int] = None | ^^^^ |""".stripMargin