Skip to content

Commit

Permalink
Infer base package in PackageProvider
Browse files Browse the repository at this point in the history
This tweaks the package inference logic in `PackageProvider` to
support the case where the directory structure is missing a common
prefix of the package structure.

Implements scalameta/metals-feature-requests#414
  • Loading branch information
harpocrates committed Jan 12, 2025
1 parent 70eb623 commit 3ff030b
Show file tree
Hide file tree
Showing 3 changed files with 106 additions and 1 deletion.
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import scala.meta.internal.semanticdb.XtensionSemanticdbSymbolInformation
import scala.meta.internal.{semanticdb => s}
import scala.meta.io.AbsolutePath

import ch.epfl.scala.bsp4j.BuildTargetIdentifier
import org.eclipse.lsp4j.Position
import org.eclipse.lsp4j.Range
import org.eclipse.lsp4j.ResourceOperation
Expand Down Expand Up @@ -826,9 +827,76 @@ class PackageProvider(
}
}

private def deducePackageParts(path: AbsolutePath): Option[List[String]] =
private def deducePackageParts(path: AbsolutePath): Option[List[String]] = {
def basePackage = buildTargets
.inverseSources(path)
.flatMap(deduceBuildTargetBasePackage(_, _ != path))
.getOrElse(Nil)
deducePackagePartsFromPath(path).map(basePackage ++ _)
}

private def deducePackagePartsFromPath(
path: AbsolutePath
): Option[List[String]] =
calcPathToSourceRoot(path).map(_.dropRight(1))

/**
* Infer any implicit package prefix for a build target.
*
* This is to help with the case where packages in a build target all start
* with some common package prefix that is not reflected in the directory
* structure.
*/
private def deduceBuildTargetBasePackage(
buildTargetId: BuildTargetIdentifier,
pathShouldBeSampled: AbsolutePath => Boolean,
): Option[List[String]] = {

/**
* If a sequence ends in a given suffix, return the sequence without that
* suffix
*
* @param original original sequence
* @param maybeSuffix suffix to remove from that sequence
*/
def stripSuffix[A](
original: List[A],
maybeSuffix: List[A],
): Option[List[A]] = {
@tailrec
def loop(
originalRev: List[A],
maybeSuffixRev: List[A],
): Option[List[A]] = {
maybeSuffixRev match {
case Nil => Some(originalRev.reverse)
case lastSuffix :: maybeRestSuffixRev =>
originalRev match {
case `lastSuffix` :: maybeRestOriginalRev =>
loop(maybeRestOriginalRev, maybeRestSuffixRev)
case _ => None
}
}
}

loop(original.reverse, maybeSuffix.reverse)
}

// Pull out an arbitrary source file from the build target to infering the base package
val sampleSourcePathAndTree = buildTargets
.buildTargetSources(buildTargetId)
.filter(pathShouldBeSampled)
.flatMap(path => trees.get(path).map(path -> _))
.headOption

for {
(sampleSourcePath, tree) <- sampleSourcePathAndTree
packagePartsFromTree = findPackages(tree).allParts
packagePartsFromPath <- deducePackagePartsFromPath(sampleSourcePath)
packagePrefix <- stripSuffix(packagePartsFromTree, packagePartsFromPath)
} yield packagePrefix
}

private def calcPathToSourceRoot(
path: AbsolutePath
): Option[List[String]] =
Expand Down
18 changes: 18 additions & 0 deletions tests/unit/src/test/scala/tests/NewFileLspSuite.scala
Original file line number Diff line number Diff line change
Expand Up @@ -356,6 +356,24 @@ class NewFileLspSuite extends BaseLspSuite("new-file") {
|""".stripMargin,
)

checkScala("new-class-infer-base-package")(
directory = Some("a/src/main/scala/foo/bar"),
fileType = Right(Class),
fileName = Right("Baz"),
expectedFilePath = "a/src/main/scala/foo/bar/Baz.scala",
expectedContent = s"""|package org.someorg.foo.bar
|
|class Baz {
|$indent
|}
|""".stripMargin,
existingFiles = """|/a/src/main/scala/foo/Qux.scala
|package org.someorg.foo
|
|class Qux
|""".stripMargin,
)

checkJava("new-java-class")(
directory = Some("a/src/main/java/foo/"),
fileType = Right(JavaClass),
Expand Down
19 changes: 19 additions & 0 deletions tests/unit/src/test/scala/tests/RenameFilesLspSuite.scala
Original file line number Diff line number Diff line change
Expand Up @@ -896,6 +896,25 @@ class RenameFilesLspSuite extends BaseRenameFilesLspSuite("rename_files") {
expectedRenames = Map.empty,
)

renamed(
"infer-base-package",
s"""|/$prefix/C/Sun.scala
|package org.someorg.<<C>>
|
|class Sun
|/$prefix/C/Moon.scala
|package org.someorg.C
|
|import org.someorg.<<C>>.Sun
|object Moon {
| val o = new Sun()
|}
|""".stripMargin,
fileRenames = Map(s"$prefix/C/Sun.scala" -> s"$prefix/C/D/Sun.scala"),
expectedRenames = Map("C" -> "C.D"),
sourcesAreCompiled = true,
)

/* Cases that are not yet supported */

renamed(
Expand Down

0 comments on commit 3ff030b

Please sign in to comment.