diff --git a/.jvmopts b/.jvmopts index 5674e043bc5..38f83f93dfb 100644 --- a/.jvmopts +++ b/.jvmopts @@ -1,6 +1,6 @@ -Xss4m -Xms1G --Xmx4G +-Xmx5G -XX:ReservedCodeCacheSize=1024m -XX:+TieredCompilation -XX:+CMSClassUnloadingEnabled diff --git a/.scalafmt.conf b/.scalafmt.conf index 1f58b6fa40d..dda8f814f25 100644 --- a/.scalafmt.conf +++ b/.scalafmt.conf @@ -13,6 +13,7 @@ To fix this problem: project.excludeFilters = [ "test-workspace" + "metals-bench/src/main/resources" "tests/unit/src/test/resources" "sbt-metals/src/sbt-test" ] diff --git a/NOTICE.md b/NOTICE.md index 7636230d61a..be2238b75e2 100644 --- a/NOTICE.md +++ b/NOTICE.md @@ -242,3 +242,221 @@ LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ``` + +# License notice for Akka +Metals contains parts which are derived from +[the akka project](https://github.com/akka/akka). We +include the text of the original license below: + +``` + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + +--------------- + +Licenses for dependency projects can be found here: +[http://akka.io/docs/akka/snapshot/project/licenses.html] + +--------------- + +akka-protobuf contains the sources of Google protobuf 2.5.0 runtime support, +moved into the source package `akka.protobuf` so as to avoid version conflicts. +For license information see COPYING.protobuf +``` diff --git a/build.sbt b/build.sbt index 2895b79d685..07643394be9 100644 --- a/build.sbt +++ b/build.sbt @@ -1,8 +1,9 @@ def localSnapshotVersion = "0.5.0-SNAPSHOT" +def isCI = System.getProperty("CI") != null inThisBuild( List( version ~= { dynVer => - if (sys.env.contains("CI")) dynVer + if (isCI) dynVer else localSnapshotVersion // only for local publishng }, scalaVersion := V.scala212, @@ -82,6 +83,7 @@ inThisBuild( ) cancelable.in(Global) := true +crossScalaVersions := Nil addCommandAlias("scalafixAll", "all compile:scalafix test:scalafix") addCommandAlias("scalafixCheck", "; scalafix --check ; test:scalafix --check") @@ -111,8 +113,36 @@ lazy val V = new { skip.in(publish) := true +lazy val interfaces = project + .in(file("pc/interfaces")) + .settings( + moduleName := "pc-interfaces", + autoScalaLibrary := false, + libraryDependencies ++= List( + "org.eclipse.lsp4j" % "org.eclipse.lsp4j" % "0.5.0" + ), + crossVersion := CrossVersion.disabled + ) +lazy val pc = project + .in(file("pc/core")) + .settings( + moduleName := "pc", + crossVersion := CrossVersion.full, + crossScalaVersions := List(V.scala212, V.scala211), + libraryDependencies ++= { + if (isCI) Nil + else List("com.lihaoyi" %% "pprint" % "0.5.3") + }, + libraryDependencies ++= List( + "org.scala-lang" % "scala-compiler" % scalaVersion.value, + "org.scalameta" % "semanticdb-scalac-core" % V.scalameta cross CrossVersion.full + ) + ) + .dependsOn(interfaces, mtags) + lazy val mtags = project .settings( + moduleName := "mtags", crossScalaVersions := List(V.scala212, V.scala211), libraryDependencies ++= List( "com.thoughtworks.qdox" % "qdox" % "2.0-M9", // for java mtags @@ -191,7 +221,7 @@ lazy val metals = project "scala212" -> V.scala212 ) ) - .dependsOn(mtags) + .dependsOn(pc, mtags) .enablePlugins(BuildInfoPlugin) lazy val `sbt-metals` = project @@ -229,6 +259,7 @@ lazy val input = project "org.scalameta" %% "scalameta" % V.scalameta, "io.circe" %% "circe-derivation-annotations" % "0.9.0-M5" ), + scalacOptions += "-P:semanticdb:synthetics:on", addCompilerPlugin( "org.scalamacros" % "paradise" % "2.1.0" cross CrossVersion.full ) @@ -240,6 +271,32 @@ lazy val testSettings: Seq[Def.Setting[_]] = List( testFrameworks := List(new TestFramework("utest.runner.Framework")) ) +lazy val mtest = project + .in(file("tests/mtest")) + .settings( + skip.in(publish) := true, + crossScalaVersions := List(V.scala212, V.scala211), + libraryDependencies ++= List( + "com.geirsson" %% "coursier-small" % "1.3.3", + "org.scalameta" %% "testkit" % V.scalameta, + "com.lihaoyi" %% "utest" % "0.6.0" + ), + buildInfoPackage := "tests", + buildInfoObject := "BuildInfoVersions", + buildInfoKeys := Seq[BuildInfoKey]( + "scala211" -> V.scala211, + "scala212" -> V.scala212 + ) + ) + .enablePlugins(BuildInfoPlugin) + +lazy val cross = project + .in(file("tests/cross")) + .settings( + testSettings, + crossScalaVersions := V.supportedScalaVersions + ) + .dependsOn(mtest, pc) lazy val unit = project .in(file("tests/unit")) .settings( @@ -262,7 +319,7 @@ lazy val unit = project "testResourceDirectory" -> resourceDirectory.in(Test).value ) ) - .dependsOn(metals) + .dependsOn(mtest, metals, pc) .enablePlugins(BuildInfoPlugin) lazy val slow = project .in(file("tests/slow")) diff --git a/metals-bench/readme.md b/metals-bench/readme.md index a57ac631b6c..d019b4d5f9a 100644 --- a/metals-bench/readme.md +++ b/metals-bench/readme.md @@ -45,6 +45,21 @@ Date: 2018 October 8th, commit 59bda2ac81a497fa168677499bd1a9df60fec5ab [info] WorkspaceFuzzBench.upper abcdefghijklmnopqrstabcdefghijkl ss 30 202.464 ± 1.423 ms/op ``` +## textDocument/completions + +First results show that loading a global instance of every request is too expensive. + +Commit: 65a2baea48637e44dc619dce46a164dc0aa055f9 +``` +[info] Benchmark (completion) Mode Cnt Score Error Units +[info] CachedSearchAndCompilerCompletionBench.complete scopeOpen ss 30 20.760 ± 1.387 ms/op +[info] CachedSearchAndCompilerCompletionBench.complete scopeDeep ss 30 61.732 ± 13.112 ms/op +[info] CachedSearchAndCompilerCompletionBench.complete memberDeep ss 30 62.284 ± 64.132 ms/op +[info] CachedSearchCompletionBench.complete scopeOpen ss 30 156.566 ± 178.618 ms/op +[info] CachedSearchCompletionBench.complete scopeDeep ss 30 233.977 ± 222.032 ms/op +[info] CachedSearchCompletionBench.complete memberDeep ss 30 154.681 ± 198.653 ms/op +``` + ## Flamegraphs Required steps before running. diff --git a/metals-bench/src/main/scala/bench/CompletionBench.scala b/metals-bench/src/main/scala/bench/CompletionBench.scala new file mode 100644 index 00000000000..75fff4ba1e7 --- /dev/null +++ b/metals-bench/src/main/scala/bench/CompletionBench.scala @@ -0,0 +1,127 @@ +package bench + +import scala.collection.JavaConverters._ +import java.nio.file.Path +import java.util.concurrent.TimeUnit +import org.openjdk.jmh.annotations.Benchmark +import org.openjdk.jmh.annotations.BenchmarkMode +import org.openjdk.jmh.annotations.Mode +import org.openjdk.jmh.annotations.OutputTimeUnit +import org.openjdk.jmh.annotations.Param +import org.openjdk.jmh.annotations.Scope +import org.openjdk.jmh.annotations.Setup +import org.openjdk.jmh.annotations.State +import scala.meta.internal.metals.ClasspathSearch +import scala.meta.internal.metals.CompilerOffsetParams +import scala.meta.internal.pc.ScalaPresentationCompiler +import scala.meta.io.AbsolutePath +import scala.meta.pc.CompletionItems +import scala.meta.pc.PresentationCompiler +import scala.meta.pc.SymbolSearch +import tests.Library +import tests.SimpleJavaSymbolIndexer + +@State(Scope.Benchmark) +abstract class CompletionBench { + var libraries: List[Library] = Nil + var completions: Map[String, SourceCompletion] = Map.empty + + def runSetup(): Unit + + def presentationCompiler(): PresentationCompiler + + @Setup + def setup(): Unit = { + runSetup() + } + + def downloadLibraries(): Unit = { + libraries = Library.jdk :: Library.all + completions = Map( + "scopeOpen" -> SourceCompletion( + "A.scala", + "import Java\n", + "import Java".length + ), + "scopeDeep" -> SourceCompletion.fromPath( + "UnzipWithApply.scala", + "if (pendin@@g12) pendingCount -= 1" + ), + "memberDeep" -> SourceCompletion.fromPath( + "UnzipWithApply.scala", + "shape.@@out21" + ) + ) + } + @Param(Array("scopeOpen", "scopeDeep", "memberDeep")) + var completion: String = _ + + @Benchmark + @BenchmarkMode(Array(Mode.SingleShotTime)) + @OutputTimeUnit(TimeUnit.MILLISECONDS) + def complete(): CompletionItems = { + val pc = presentationCompiler() + val result = currentCompletion.complete(pc) + val diagnostics = pc.diagnostics() + require(diagnostics.isEmpty, diagnostics.asScala.mkString("\n", "\n", "\n")) + result + } + + def currentCompletion: SourceCompletion = completions(completion) + + def classpath: List[Path] = + libraries.flatMap(_.classpath.entries.map(_.toNIO)) + def sources: List[AbsolutePath] = libraries.flatMap(_.sources.entries) + + def newSearch(): SymbolSearch = { + require(libraries.nonEmpty) + ClasspathSearch.fromClasspath(classpath, _ => 0) + } + def newIndexer() = new SimpleJavaSymbolIndexer(sources) + + def newPC( + search: SymbolSearch = newSearch(), + indexer: SimpleJavaSymbolIndexer = newIndexer() + ): PresentationCompiler = { + new ScalaPresentationCompiler() + .withIndexer(indexer) + .withSearch(search) + .newInstance("", classpath.asJava, Nil.asJava) + } + + def scopeComplete(pc: PresentationCompiler): CompletionItems = { + val code = "import Java\n" + pc.complete(CompilerOffsetParams("A.scala", code, code.length - 2)) + } +} + +class OnDemandCompletionBench extends CompletionBench { + override def runSetup(): Unit = downloadLibraries() + override def presentationCompiler(): PresentationCompiler = + newPC(newSearch(), newIndexer()) +} + +class CachedSearchAndCompilerCompletionBench extends CompletionBench { + var pc: PresentationCompiler = _ + + override def runSetup(): Unit = { + downloadLibraries() + pc = newPC() + } + + override def presentationCompiler(): PresentationCompiler = pc +} + +class CachedSearchCompletionBench extends CompletionBench { + var pc: PresentationCompiler = _ + var cachedSearch: SymbolSearch = _ + + override def runSetup(): Unit = { + downloadLibraries() + cachedSearch = newSearch() + } + + override def presentationCompiler(): PresentationCompiler = + newPC(cachedSearch) + +} diff --git a/metals-bench/src/main/scala/bench/MainBench.scala b/metals-bench/src/main/scala/bench/MainBench.scala index d028235d689..22d30aa879d 100644 --- a/metals-bench/src/main/scala/bench/MainBench.scala +++ b/metals-bench/src/main/scala/bench/MainBench.scala @@ -1,21 +1,13 @@ package bench -import scala.meta.internal.metals.Time -import scala.meta.internal.metals.Timer - object MainBench { def main(args: Array[String]): Unit = { - val bench = new ClasspathFuzzBench + val bench = new OnDemandCompletionBench bench.setup() - val symbols = bench.symbols - 1.to(10).foreach { i => - val timer = new Timer(Time.system) - val result = symbols.search("File") - if (i == 1) { - pprint.log(result.map(_.getName)) - } - pprint.log(result.length) - scribe.info(s"time: $timer") - } + bench.completion = "memberDeep" + val result = bench.complete() +// result.getItems.asScala.foreach { item => +// pprint.log(item.getLabel) +// } } } diff --git a/metals-bench/src/main/scala/bench/SourceCompletion.scala b/metals-bench/src/main/scala/bench/SourceCompletion.scala new file mode 100644 index 00000000000..550a4bff1fa --- /dev/null +++ b/metals-bench/src/main/scala/bench/SourceCompletion.scala @@ -0,0 +1,30 @@ +package bench + +import java.nio.charset.StandardCharsets +import scala.meta.internal.io.InputStreamIO +import scala.meta.internal.metals.CompilerOffsetParams +import scala.meta.pc.CompletionItems +import scala.meta.pc.PresentationCompiler + +case class SourceCompletion(filename: String, code: String, offset: Int) { + def complete(pc: PresentationCompiler): CompletionItems = + pc.complete(CompilerOffsetParams(filename, code, offset)) +} + +object SourceCompletion { + def fromPath(path: String, query: String): SourceCompletion = { + val text = readResource(path) + val queryIndex = text.indexOf(query.replaceAllLiterally("@@", "")) + if (queryIndex < 0) throw new IllegalArgumentException(query) + val offset = query.indexOf("@@") + if (offset < 0) throw new IllegalArgumentException(query) + SourceCompletion(path, text, queryIndex + offset) + } + private def readResource(path: String): String = + new String( + InputStreamIO.readBytes( + this.getClass.getResourceAsStream(s"/$path") + ), + StandardCharsets.UTF_8 + ) +} diff --git a/metals/src/main/scala/scala/meta/internal/metals/BuildTargetCompiler.scala b/metals/src/main/scala/scala/meta/internal/metals/BuildTargetCompiler.scala new file mode 100644 index 00000000000..5aa5313823e --- /dev/null +++ b/metals/src/main/scala/scala/meta/internal/metals/BuildTargetCompiler.scala @@ -0,0 +1,43 @@ +package scala.meta.internal.metals + +import ch.epfl.scala.bsp4j.ScalaBuildTarget +import ch.epfl.scala.bsp4j.ScalacOptionsItem +import scala.meta.internal.metals.MetalsEnrichments._ +import scala.meta.internal.pc.ScalaPresentationCompiler +import scala.meta.pc.PresentationCompiler +import scala.meta.pc.SymbolIndexer +import scala.meta.pc.SymbolSearch +import scala.util.Properties + +case class BuildTargetCompiler(pc: PresentationCompiler, search: SymbolSearch) + extends Cancelable { + override def cancel(): Unit = pc.shutdown() +} + +object BuildTargetCompiler { + def fromClasspath( + scalac: ScalacOptionsItem, + info: ScalaBuildTarget, + indexer: SymbolIndexer, + search: SymbolSearch, + embedded: Embedded + ): BuildTargetCompiler = { + val classpath = scalac.classpath.map(_.toNIO).toSeq + val pc: PresentationCompiler = + if (info.getScalaVersion == Properties.versionNumberString) { + new ScalaPresentationCompiler() + } else { + embedded.presentationCompiler(info, scalac) + } + BuildTargetCompiler( + pc.withIndexer(indexer) + .withSearch(search) + .newInstance( + scalac.getTarget.getUri, + classpath.asJava, + scalac.getOptions + ), + search + ) + } +} diff --git a/metals/src/main/scala/scala/meta/internal/metals/BuildTargets.scala b/metals/src/main/scala/scala/meta/internal/metals/BuildTargets.scala index ff1837e6be3..ff624a9f124 100644 --- a/metals/src/main/scala/scala/meta/internal/metals/BuildTargets.scala +++ b/metals/src/main/scala/scala/meta/internal/metals/BuildTargets.scala @@ -1,5 +1,7 @@ package scala.meta.internal.metals +import java.util +import java.lang.{Iterable => JIterable} import ch.epfl.scala.bsp4j.BuildTarget import ch.epfl.scala.bsp4j.BuildTargetIdentifier import ch.epfl.scala.bsp4j.ScalacOptionsItem @@ -25,6 +27,8 @@ final class BuildTargets() { TrieMap.empty[BuildTargetIdentifier, ScalacOptionsItem] private val inverseDependencies = TrieMap.empty[BuildTargetIdentifier, ListBuffer[BuildTargetIdentifier]] + private val buildTargetSources = + TrieMap.empty[BuildTargetIdentifier, util.Set[AbsolutePath]] private val inverseDependencySources = TrieMap.empty[AbsolutePath, BuildTargetIdentifier] @@ -34,10 +38,14 @@ final class BuildTargets() { buildTargetInfo.clear() scalacTargetInfo.clear() inverseDependencies.clear() + buildTargetSources.clear() inverseDependencySources.clear() } def sourceDirectories: Iterable[AbsolutePath] = sourceDirectoriesToBuildTarget.keys + def sourceDirectoriesToBuildTargets + : Iterator[(AbsolutePath, JIterable[BuildTargetIdentifier])] = + sourceDirectoriesToBuildTarget.iterator def scalacOptions: Iterable[ScalacOptionsItem] = scalacTargetInfo.values @@ -58,6 +66,50 @@ final class BuildTargets() { queue.add(buildTarget) } + def onCreate(source: AbsolutePath): Unit = { + for { + buildTarget <- sourceBuildTargets(source) + } { + linkSourceFile(buildTarget, source) + } + } + + def buildTargetTransitiveSources( + id: BuildTargetIdentifier + ): Iterator[AbsolutePath] = { + for { + dependency <- buildTargetTransitiveDependencies(id).iterator + sources <- buildTargetSources.get(dependency).iterator + source <- sources.asScala.iterator + } yield source + } + + def buildTargetTransitiveDependencies( + id: BuildTargetIdentifier + ): Iterable[BuildTargetIdentifier] = { + val isVisited = mutable.Set.empty[BuildTargetIdentifier] + val toVisit = new java.util.ArrayDeque[BuildTargetIdentifier] + toVisit.add(id) + while (!toVisit.isEmpty) { + val next = toVisit.pop() + if (!isVisited(next)) { + isVisited.add(next) + for { + info <- info(next).iterator + dependency <- info.getDependencies.asScala.iterator + } { + toVisit.add(dependency) + } + } + } + isVisited + } + + def linkSourceFile(id: BuildTargetIdentifier, source: AbsolutePath): Unit = { + val set = buildTargetSources.getOrElseUpdate(id, ConcurrentHashSet.empty) + set.add(source) + } + def addWorkspaceBuildTargets(result: WorkspaceBuildTargetsResult): Unit = { result.getTargets.asScala.foreach { target => buildTargetInfo(target.getId) = target @@ -89,18 +141,24 @@ final class BuildTargets() { * Returns the first build target containing this source file. */ def inverseSources( - textDocument: AbsolutePath + source: AbsolutePath ): Option[BuildTargetIdentifier] = { - for { - buildTargets <- sourceDirectoriesToBuildTarget.collectFirst { + val buildTargets = sourceBuildTargets(source) + buildTargets // prioritize JVM targets over JS/Native + .find(x => scalacOptions(x).exists(_.isJVM)) + .orElse(buildTargets.headOption) + } + + def sourceBuildTargets( + source: AbsolutePath + ): Iterable[BuildTargetIdentifier] = { + sourceDirectoriesToBuildTarget + .collectFirst { case (sourceDirectory, buildTargets) - if textDocument.toNIO.startsWith(sourceDirectory.toNIO) => + if source.toNIO.startsWith(sourceDirectory.toNIO) => buildTargets.asScala } - target <- buildTargets // prioritize JVM targets over JS/Native - .find(x => scalacOptions(x).exists(_.isJVM)) - .orElse(buildTargets.headOption) - } yield target + .getOrElse(Iterable.empty) } def inverseSourceDirectory(source: AbsolutePath): Option[AbsolutePath] = diff --git a/metals/src/main/scala/scala/meta/internal/metals/CachedSymbolInformation.scala b/metals/src/main/scala/scala/meta/internal/metals/CachedSymbolInformation.scala new file mode 100644 index 00000000000..bd9de70c0b3 --- /dev/null +++ b/metals/src/main/scala/scala/meta/internal/metals/CachedSymbolInformation.scala @@ -0,0 +1,29 @@ +package scala.meta.internal.metals + +import org.eclipse.{lsp4j => l} +import scala.meta.internal.{semanticdb => s} +import MetalsEnrichments._ + +case class CachedSymbolInformation( + symbol: String, + kind: l.SymbolKind, + range: l.Range +) { + def toLSP(uri: String): l.SymbolInformation = { + import scala.meta.internal.semanticdb.Scala._ + val (desc, owner) = DescriptorParser(symbol) + new l.SymbolInformation( + desc.name.value, + kind, + new l.Location(uri, range), + owner.replace('/', '.') + ) + } +} + +object CachedSymbolInformation { + def fromDefn(defn: SemanticdbDefinition): CachedSymbolInformation = { + val range = defn.occ.range.getOrElse(s.Range()) + CachedSymbolInformation(defn.info.symbol, defn.info.kind.toLSP, range.toLSP) + } +} diff --git a/metals/src/main/scala/scala/meta/internal/metals/Classfile.scala b/metals/src/main/scala/scala/meta/internal/metals/Classfile.scala deleted file mode 100644 index 25d4ed7480c..00000000000 --- a/metals/src/main/scala/scala/meta/internal/metals/Classfile.scala +++ /dev/null @@ -1,25 +0,0 @@ -package scala.meta.internal.metals - -import scala.meta.internal.mtags.OnDemandSymbolIndex -import scala.meta.internal.semanticdb.Scala.Descriptor -import scala.meta.internal.semanticdb.Scala.Symbols -import scala.meta.internal.mtags.Symbol -import scala.meta.internal.mtags.SymbolDefinition - -case class Classfile(pkg: String, filename: String) { - def isExact(query: WorkspaceSymbolQuery): Boolean = - name == query.query - def name: String = { - val dollar = filename.indexOf('$') - if (dollar < 0) filename.stripSuffix(".class") - else filename.substring(0, dollar) - } - def definition(index: OnDemandSymbolIndex): Option[SymbolDefinition] = { - val nme = name - val tpe = Symbol(Symbols.Global(pkg, Descriptor.Type(nme))) - index.definition(tpe).orElse { - val term = Symbol(Symbols.Global(pkg, Descriptor.Term(nme))) - index.definition(term) - } - } -} diff --git a/metals/src/main/scala/scala/meta/internal/metals/Compilers.scala b/metals/src/main/scala/scala/meta/internal/metals/Compilers.scala new file mode 100644 index 00000000000..f6c4b5698f2 --- /dev/null +++ b/metals/src/main/scala/scala/meta/internal/metals/Compilers.scala @@ -0,0 +1,117 @@ +package scala.meta.internal.metals + +import ch.epfl.scala.bsp4j.BuildTargetIdentifier +import org.eclipse.lsp4j.CompletionItem +import org.eclipse.lsp4j.CompletionList +import org.eclipse.lsp4j.CompletionParams +import org.eclipse.lsp4j.Hover +import org.eclipse.lsp4j.SignatureHelp +import org.eclipse.lsp4j.TextDocumentPositionParams +import org.eclipse.lsp4j.jsonrpc.CancelChecker +import scala.collection.concurrent.TrieMap +import scala.concurrent.Promise +import scala.meta.inputs.Position +import scala.meta.internal.metals.MetalsEnrichments._ +import scala.meta.pc.PresentationCompiler +import scala.meta.pc.SymbolIndexer +import scala.meta.pc.SymbolSearch + +class Compilers( + buildTargets: BuildTargets, + buffers: Buffers, + indexer: SymbolIndexer, + search: SymbolSearch, + embedded: Embedded, + statusBar: StatusBar +) extends Cancelable { + + private val cache = TrieMap.empty[BuildTargetIdentifier, BuildTargetCompiler] + override def cancel(): Unit = { + Cancelable.cancelAll(cache.values) + cache.clear() + } + def didCompileSuccessfully(id: BuildTargetIdentifier): Unit = { + cache.remove(id).foreach(_.cancel()) + } + + def completionItemResolve( + item: CompletionItem, + token: CancelChecker + ): Option[CompletionItem] = { + for { + data <- item.data + compiler <- cache.get(new BuildTargetIdentifier(data.target)) + } yield compiler.pc.completionItemResolve(item, data.symbol) + } + def completions( + params: CompletionParams, + token: CancelChecker + ): Option[CompletionList] = + withPC(params) { (pc, pos) => + pc.complete( + CompilerOffsetParams(pos.input.syntax, pos.input.text, pos.start, token) + ) + } + def hover( + params: TextDocumentPositionParams, + token: CancelChecker + ): Option[Hover] = + withPC(params) { (pc, pos) => + pc.hover( + CompilerOffsetParams(pos.input.syntax, pos.input.text, pos.start, token) + ) + } + def signatureHelp( + params: TextDocumentPositionParams, + token: CancelChecker + ): Option[SignatureHelp] = + withPC(params) { (pc, pos) => + pc.signatureHelp( + CompilerOffsetParams(pos.input.syntax, pos.input.text, pos.start, token) + ) + } + + private def withPC[T]( + params: TextDocumentPositionParams + )(fn: (PresentationCompiler, Position) => T): Option[T] = { + val path = params.getTextDocument.getUri.toAbsolutePath + for { + target <- buildTargets.inverseSources(path) + info <- buildTargets.info(target) + scala <- info.asScalaBuildTarget + isSupported = ScalaVersions.isSupportedScalaVersion(scala.getScalaVersion) + _ = { + if (!isSupported) { + scribe.warn(s"unsupported Scala ${scala.getScalaVersion}") + } + } + if isSupported + scalac <- buildTargets.scalacOptions(target) + } yield { + val promise = Promise[Unit]() + try { + val compiler = cache.getOrElseUpdate( + target, { + statusBar.trackFuture( + s"${statusBar.icons.sync}Loading presentation compiler", + promise.future + ) + BuildTargetCompiler.fromClasspath( + scalac, + scala, + indexer, + search, + embedded + ) + } + ) + val input = path.toInputFromBuffers(buffers) + val pos = params.getPosition.toMeta(input) + val result = fn(compiler.pc, pos) + result + } finally { + promise.trySuccess(()) + } + } + } +} diff --git a/metals/src/main/scala/scala/meta/internal/metals/CompressedPackageIndex.scala b/metals/src/main/scala/scala/meta/internal/metals/CompressedPackageIndex.scala deleted file mode 100644 index 6ad4b217307..00000000000 --- a/metals/src/main/scala/scala/meta/internal/metals/CompressedPackageIndex.scala +++ /dev/null @@ -1,20 +0,0 @@ -package scala.meta.internal.metals - -import com.google.common.hash.BloomFilter - -/** - * The memory-compressed version of PackageIndex. - * - * @param bloom the fuzzy search bloom filter for all members of this package. - * @param memberBytes the GZIP compressed bytes representing Array[String] for - * all members of this package. The members are compressed because the strings - * consume a lot of memory and most queries only require looking at a few packages. - * We decompress the members only when a search query matches the bloom filter - * for this package. - */ -case class CompressedPackageIndex( - bloom: BloomFilter[CharSequence], - memberBytes: Array[Byte] -) { - def members: Array[String] = Compression.decompress(memberBytes) -} diff --git a/metals/src/main/scala/scala/meta/internal/metals/CompressedSourceIndex.scala b/metals/src/main/scala/scala/meta/internal/metals/CompressedSourceIndex.scala new file mode 100644 index 00000000000..fbc2bc95b75 --- /dev/null +++ b/metals/src/main/scala/scala/meta/internal/metals/CompressedSourceIndex.scala @@ -0,0 +1,9 @@ +package scala.meta.internal.metals + +import com.google.common.hash.BloomFilter + +case class CompressedSourceIndex( + bloom: BloomFilter[CharSequence], + // TODO(olafur): actually compress these + symbols: Seq[CachedSymbolInformation] +) diff --git a/metals/src/main/scala/scala/meta/internal/metals/ConcurrentHashSet.scala b/metals/src/main/scala/scala/meta/internal/metals/ConcurrentHashSet.scala new file mode 100644 index 00000000000..a273f40e6e2 --- /dev/null +++ b/metals/src/main/scala/scala/meta/internal/metals/ConcurrentHashSet.scala @@ -0,0 +1,12 @@ +package scala.meta.internal.metals + +import java.util +import java.util.Collections +import java.util.concurrent.ConcurrentHashMap + +object ConcurrentHashSet { + def empty[T]: util.Set[T] = + Collections.newSetFromMap( + new ConcurrentHashMap[T, java.lang.Boolean]() + ) +} diff --git a/metals/src/main/scala/scala/meta/internal/metals/Embedded.scala b/metals/src/main/scala/scala/meta/internal/metals/Embedded.scala index b705d567479..777bb60abb1 100644 --- a/metals/src/main/scala/scala/meta/internal/metals/Embedded.scala +++ b/metals/src/main/scala/scala/meta/internal/metals/Embedded.scala @@ -1,12 +1,20 @@ package scala.meta.internal.metals +import ch.epfl.scala.bsp4j.ScalaBuildTarget +import ch.epfl.scala.bsp4j.ScalacOptionsItem import com.geirsson.coursiersmall +import com.geirsson.coursiersmall.Dependency +import com.geirsson.coursiersmall.Settings import java.net.URLClassLoader import java.nio.file.Files +import java.nio.file.Paths +import java.util.ServiceLoader import java.util.concurrent.atomic.AtomicBoolean -import scala.concurrent.Promise +import scala.collection.concurrent.TrieMap import scala.concurrent.duration.Duration +import scala.meta.internal.metals.MetalsEnrichments._ import scala.meta.io.AbsolutePath +import scala.meta.pc.PresentationCompiler import scala.util.control.NonFatal /** @@ -26,6 +34,12 @@ final class Embedded( if (isBloopJars.get()) { bloopJars.foreach(_.close()) } + for { + key <- presentationCompilers.keysIterator + compiler <- presentationCompilers.remove(key) + } { +// compiler.close() + } } /** @@ -49,16 +63,17 @@ final class Embedded( val isBloopJars = new AtomicBoolean(false) lazy val bloopJars: Option[URLClassLoader] = { isBloopJars.set(true) - val promise = Promise[Unit]() - statusBar.trackFuture(s"${icons.sync}Downloading Bloop", promise.future) - try { - Some(Embedded.newBloopClassloader()) - } catch { - case NonFatal(e) => - scribe.error("Failed to classload bloop, compilation will not work", e) - None - } finally { - promise.trySuccess(()) + statusBar.trackBlockingTask(s"${icons.sync}Downloading Bloop") { + try { + Some(Embedded.newBloopClassloader()) + } catch { + case NonFatal(e) => + scribe.error( + "Failed to classload bloop, compilation will not work", + e + ) + None + } } } @@ -77,29 +92,80 @@ final class Embedded( AbsolutePath(out) } + private val presentationCompilers: TrieMap[String, URLClassLoader] = + TrieMap.empty + def presentationCompiler( + info: ScalaBuildTarget, + scalac: ScalacOptionsItem + ): PresentationCompiler = { + val classloader = presentationCompilers.getOrElseUpdate( + info.getScalaVersion, + statusBar.trackBlockingTask( + s"${icons.sync}Downloading presentation compiler" + ) { + Embedded.newPresentationCompilerClassLoader(info, scalac) + } + ) + val services = + ServiceLoader.load(classOf[PresentationCompiler], classloader).iterator() + if (services.hasNext) services.next() + else throw new NoSuchElementException(classOf[PresentationCompiler].getName) + } } object Embedded { - private def newBloopClassloader(): URLClassLoader = { - val settings = new coursiersmall.Settings() + def downloadSettings(dependency: Dependency): Settings = + new coursiersmall.Settings() .withTtl(Some(Duration.Inf)) - .withDependencies( - List( - new coursiersmall.Dependency( - "ch.epfl.scala", - "bloop-frontend_2.12", - BuildInfo.bloopVersion - ) - ) + .withDependencies(List(dependency)) + private def newPresentationCompilerClassLoader( + info: ScalaBuildTarget, + scalac: ScalacOptionsItem + ): URLClassLoader = { + val pc = new Dependency( + "org.scalameta", + s"pc_${info.getScalaVersion}", + BuildInfo.metalsVersion + ) + val needsFullClasspath = !scalac.isSemanticdbEnabled + val dependency = + if (needsFullClasspath) pc + else pc.withTransitive(false) + val settings = downloadSettings(dependency) + val jars = coursiersmall.CoursierSmall.fetch(settings) + val scalaJars = info.getJars.asScala.map(_.toAbsolutePath.toNIO) + val semanticdbJars = + if (needsFullClasspath) Nil + else { + scalac.getOptions.asScala.collect { + case opt + if opt.startsWith("-Xplugin:") && + opt.contains("semanticdb-scalac") => + Paths.get(opt.stripPrefix("-Xplugin:")) + } + } + val allJars = Iterator(jars, scalaJars, semanticdbJars).flatten + val allURLs = allJars.map(_.toUri.toURL).toArray + // Share classloader for a subset of types. + val parent = + new PresentationCompilerClassLoader(this.getClass.getClassLoader) + new URLClassLoader(allURLs, parent) + } + private def newBloopClassloader(): URLClassLoader = { + val settings = downloadSettings( + new Dependency( + "ch.epfl.scala", + "bloop-frontend_2.12", + BuildInfo.bloopVersion ) - .addRepositories( - List( - coursiersmall.Repository.SonatypeReleases, - new coursiersmall.Repository.Maven( - "https://dl.bintray.com/scalacenter/releases" - ) + ).addRepositories( + List( + coursiersmall.Repository.SonatypeReleases, + new coursiersmall.Repository.Maven( + "https://dl.bintray.com/scalacenter/releases" ) ) + ) val jars = coursiersmall.CoursierSmall.fetch(settings) // Don't make Bloop classloader a child or our classloader. val parent: ClassLoader = null diff --git a/metals/src/main/scala/scala/meta/internal/metals/ForwardingMetalsBuildClient.scala b/metals/src/main/scala/scala/meta/internal/metals/ForwardingMetalsBuildClient.scala index f53f3c724ae..d9c643e7aff 100644 --- a/metals/src/main/scala/scala/meta/internal/metals/ForwardingMetalsBuildClient.scala +++ b/metals/src/main/scala/scala/meta/internal/metals/ForwardingMetalsBuildClient.scala @@ -26,7 +26,8 @@ final class ForwardingMetalsBuildClient( buildTargets: BuildTargets, config: MetalsServerConfig, statusBar: StatusBar, - time: Time + time: Time, + didCompileSuccessfully: BuildTargetIdentifier => Unit ) extends MetalsBuildClient with Cancelable { @@ -117,6 +118,9 @@ final class ForwardingMetalsBuildClient( compilation <- compilations.get(report.getTarget) } { diagnostics.onFinishCompileBuildTarget(report.getTarget) + if (!compilation.isNoOp && report.getErrors == 0) { + didCompileSuccessfully(report.getTarget) + } val target = report.getTarget compilation.promise.trySuccess(report) val name = buildTargets.info(report.getTarget) match { diff --git a/metals/src/main/scala/scala/meta/internal/metals/MetalsEnrichments.scala b/metals/src/main/scala/scala/meta/internal/metals/MetalsEnrichments.scala index 6ca2c424f43..573f9c0d92b 100644 --- a/metals/src/main/scala/scala/meta/internal/metals/MetalsEnrichments.scala +++ b/metals/src/main/scala/scala/meta/internal/metals/MetalsEnrichments.scala @@ -1,12 +1,9 @@ package scala.meta.internal.metals import ch.epfl.scala.{bsp4j => b} -import com.google.gson.Gson -import com.google.gson.JsonElement import io.undertow.server.HttpServerExchange import java.net.URI import java.nio.charset.StandardCharsets -import scala.meta.internal.semanticdb.SymbolInformation.{Kind => k} import java.nio.file.FileAlreadyExistsException import java.nio.file.Files import java.nio.file.Path @@ -19,7 +16,6 @@ import java.util.concurrent.CompletionStage import org.eclipse.lsp4j.TextDocumentIdentifier import org.eclipse.lsp4j.jsonrpc.CancelChecker import org.eclipse.{lsp4j => l} -import scala.collection.AbstractIterator import scala.collection.convert.DecorateAsJava import scala.collection.convert.DecorateAsScala import scala.compat.java8.FutureConverters @@ -34,7 +30,6 @@ import scala.meta.internal.semanticdb.Scala.Symbols import scala.meta.internal.{semanticdb => s} import scala.meta.io.AbsolutePath import scala.util.Properties -import scala.util.control.NonFatal import scala.{meta => m} /** @@ -56,24 +51,10 @@ import scala.{meta => m} * then we can split this up, but for now it's really convenient to have to * remember only one import. */ -object MetalsEnrichments extends DecorateAsJava with DecorateAsScala { - - private def decodeJson[T](obj: AnyRef, cls: Class[T]): Option[T] = - for { - data <- Option(obj) - value <- try { - Some( - new Gson().fromJson[T]( - data.asInstanceOf[JsonElement], - cls - ) - ) - } catch { - case NonFatal(e) => - scribe.error(s"decode error: $cls", e) - None - } - } yield value +object MetalsEnrichments + extends DecorateAsJava + with DecorateAsScala + with PCEnrichments { implicit class XtensionBuildTarget(buildTarget: b.BuildTarget) { def asScalaBuildTarget: Option[b.ScalaBuildTarget] = { @@ -172,6 +153,17 @@ object MetalsEnrichments extends DecorateAsJava with DecorateAsScala { } } + implicit class XtensionPositionLspInverse(pos: l.Position) { + def toMeta(input: m.Input): m.Position = { + m.Position.Range( + input, + pos.getLine, + pos.getCharacter, + pos.getLine, + pos.getCharacter + ) + } + } implicit class XtensionPositionLsp(pos: m.Position) { def toSemanticdb: s.Range = { new s.Range( @@ -233,6 +225,8 @@ object MetalsEnrichments extends DecorateAsJava with DecorateAsScala { } implicit class XtensionAbsolutePathBuffers(path: AbsolutePath) { + def filename: String = path.toNIO.getFileName.toString + def sourcerootOption: String = s""""-P:semanticdb:sourceroot:$path"""" /** @@ -413,26 +407,6 @@ object MetalsEnrichments extends DecorateAsJava with DecorateAsScala { range.getEnd.getCharacter ) } - implicit class XtensionRangeBuildProtocol(range: s.Range) { - def toLocation(uri: String): l.Location = { - new l.Location(uri, range.toLSP) - } - def toLSP: l.Range = { - val start = new l.Position(range.startLine, range.startCharacter) - val end = new l.Position(range.endLine, range.endCharacter) - new l.Range(start, end) - } - def encloses(other: l.Position): Boolean = { - range.startLine <= other.getLine && - range.endLine >= other.getLine && - range.startCharacter <= other.getCharacter && - range.endCharacter > other.getCharacter - } - def encloses(other: l.Range): Boolean = { - encloses(other.getStart) && - encloses(other.getEnd) - } - } implicit class XtensionSymbolOccurrenceProtocol(occ: s.SymbolOccurrence) { def toLocation(uri: String): l.Location = { @@ -525,37 +499,4 @@ object MetalsEnrichments extends DecorateAsJava with DecorateAsScala { } } - implicit class XtensionSymbolInformation(kind: s.SymbolInformation.Kind) { - def toLSP: l.SymbolKind = kind match { - case k.LOCAL => l.SymbolKind.Variable - case k.FIELD => l.SymbolKind.Field - case k.METHOD => l.SymbolKind.Method - case k.CONSTRUCTOR => l.SymbolKind.Constructor - case k.MACRO => l.SymbolKind.Method - case k.TYPE => l.SymbolKind.Class - case k.PARAMETER => l.SymbolKind.Variable - case k.SELF_PARAMETER => l.SymbolKind.Variable - case k.TYPE_PARAMETER => l.SymbolKind.TypeParameter - case k.OBJECT => l.SymbolKind.Object - case k.PACKAGE => l.SymbolKind.Module - case k.PACKAGE_OBJECT => l.SymbolKind.Module - case k.CLASS => l.SymbolKind.Class - case k.TRAIT => l.SymbolKind.Interface - case k.INTERFACE => l.SymbolKind.Interface - case _ => l.SymbolKind.Class - } - } - - implicit class XtensionJavaPriorityQueue[A](q: util.PriorityQueue[A]) { - - /** - * Returns iterator that consumes the priority queue in-order using `poll()`. - */ - def pollingIterator: Iterator[A] = new AbstractIterator[A] { - override def hasNext: Boolean = !q.isEmpty - override def next(): A = q.poll() - } - - } - } 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 3bb06a62e5b..4cf48d68313 100644 --- a/metals/src/main/scala/scala/meta/internal/metals/MetalsLanguageServer.scala +++ b/metals/src/main/scala/scala/meta/internal/metals/MetalsLanguageServer.scala @@ -96,6 +96,7 @@ class MetalsLanguageServer( private val mtags = new Mtags var workspace: AbsolutePath = _ private val definitionIndex = newSymbolIndex() + private val symbolIndexer = new MetalsSymbolIndexer(definitionIndex) var buildServer = Option.empty[BuildServerConnection] private val openTextDocument = new AtomicReference[AbsolutePath]() private val savedFiles = new ActiveFiles(time) @@ -131,6 +132,7 @@ class MetalsLanguageServer( private var initializeParams: Option[InitializeParams] = None private var referencesProvider: ReferenceProvider = _ private var workspaceSymbols: WorkspaceSymbolProvider = _ + private var compilers: Compilers = _ var tables: Tables = _ var statusBar: StatusBar = _ private var embedded: Embedded = _ @@ -139,7 +141,8 @@ class MetalsLanguageServer( def connectToLanguageClient(client: MetalsLanguageClient): Unit = { languageClient.underlying = client - statusBar = new StatusBar(() => languageClient, time, progressTicks) + statusBar = + new StatusBar(() => languageClient, time, progressTicks, config.icons) embedded = register(new Embedded(config.icons, statusBar, () => userConfig)) LanguageClientLogger.languageClient = Some(languageClient) cancelables.add(() => languageClient.shutdown()) @@ -189,7 +192,8 @@ class MetalsLanguageServer( buildTargets, config, statusBar, - time + time, + id => compilers.didCompileSuccessfully(id) ) trees = new Trees(buffers, diagnostics) documentSymbolProvider = new DocumentSymbolProvider(trees) @@ -268,9 +272,23 @@ class MetalsLanguageServer( config.statistics, buildTargets, definitionIndex, - pkg => referencesProvider.referencedPackages.mightContain(pkg), + pkg => { + val mightContain = + referencesProvider.referencedPackages.mightContain(pkg) + if (mightContain) 0 else 1 + }, interactiveSemanticdbs.toFileOnDisk ) + compilers = register( + new Compilers( + buildTargets, + buffers, + symbolIndexer, + workspaceSymbols, + embedded, + statusBar + ) + ) doctor = new Doctor( workspace, buildTargets, @@ -307,6 +325,13 @@ class MetalsLanguageServer( ) capabilities.setDefinitionProvider(true) capabilities.setReferencesProvider(true) + capabilities.setHoverProvider(true) + capabilities.setSignatureHelpProvider( + new SignatureHelpOptions(List("(", "[").asJava) + ) + capabilities.setCompletionProvider( + new CompletionOptions(true, List(".").asJava) + ) capabilities.setWorkspaceSymbolProvider(true) capabilities.setDocumentSymbolProvider(true) capabilities.setDocumentFormattingProvider(true) @@ -574,6 +599,11 @@ class MetalsLanguageServer( ): CompletableFuture[Unit] = { val path = AbsolutePath(event.path()) if (!savedFiles.isRecentlyActive(path) && path.isScalaOrJava) { + event.eventType() match { + case EventType.CREATE => + buildTargets.onCreate(path) + case _ => + } onChange(List(path)) } else if (path.isSemanticdb) { CompletableFuture.completedFuture { @@ -635,9 +665,8 @@ class MetalsLanguageServer( @JsonRequest("textDocument/hover") def hover(params: TextDocumentPositionParams): CompletableFuture[Hover] = - CompletableFutures.computeAsync { _ => - scribe.warn("textDocument/hover is not supported.") - null + CompletableFutures.computeAsync { token => + compilers.hover(params, token).orNull } @JsonRequest("textDocument/documentHighlight") @@ -767,21 +796,28 @@ class MetalsLanguageServer( } def referencesResult(params: ReferenceParams): ReferencesResult = referencesProvider.references(params) - @JsonRequest("textDocument/completion") def completion(params: CompletionParams): CompletableFuture[CompletionList] = - CompletableFutures.computeAsync { _ => - scribe.warn("textDocument/completion is not supported.") - null + CompletableFutures.computeAsync { token => + compilers.completions(params, token).orNull + } + + @JsonRequest("completionItem/resolve") + def completionItemResolve( + item: CompletionItem + ): CompletableFuture[CompletionItem] = + CompletableFutures.computeAsync { token => + compilers.completionItemResolve(item, token).getOrElse(item) } + def completionItemResolveSync(item: CompletionItem): CompletionItem = + compilers.completionItemResolve(item, EmptyCancelChecker).getOrElse(item) @JsonRequest("textDocument/signatureHelp") def signatureHelp( params: TextDocumentPositionParams ): CompletableFuture[SignatureHelp] = - CompletableFutures.computeAsync { _ => - scribe.warn("textDocument/signatureHelp is not supported.") - null + CompletableFutures.computeAsync { token => + compilers.signatureHelp(params, token).orNull } @JsonRequest("textDocument/codeAction") @@ -1015,11 +1051,14 @@ class MetalsLanguageServer( private def indexWorkspaceSources(): Unit = { for { - sourceDirectory <- buildTargets.sourceDirectories + (sourceDirectory, targets) <- buildTargets.sourceDirectoriesToBuildTargets if sourceDirectory.isDirectory source <- ListFiles(sourceDirectory) if source.isScalaOrJava } { + targets.asScala.foreach { target => + buildTargets.linkSourceFile(target, source) + } indexSourceFile(source, Some(sourceDirectory)) } } @@ -1028,7 +1067,7 @@ class MetalsLanguageServer( paths: Seq[AbsolutePath] ): Unit = { for { - path <- paths + path <- paths.iterator if path.isScalaOrJava } { indexSourceFile(path, buildTargets.inverseSourceDirectory(path)) @@ -1042,11 +1081,17 @@ class MetalsLanguageServer( try { val reluri = source.toIdeallyRelativeURI(sourceDirectory) val input = source.toInput - val symbols = ArrayBuffer.empty[String] + val symbols = ArrayBuffer.empty[CachedSymbolInformation] SemanticdbDefinition.foreach(input) { - case SemanticdbDefinition(info, _, owner) => + case SemanticdbDefinition(info, occ, owner) => if (WorkspaceSymbolProvider.isRelevantKind(info.kind)) { - symbols += info.symbol + occ.range.foreach { range => + symbols += CachedSymbolInformation( + info.symbol, + info.kind.toLSP, + range.toLSP + ) + } } if (sourceDirectory.isDefined && !info.symbol.isPackage && diff --git a/metals/src/main/scala/scala/meta/internal/metals/PresentationCompilerClassLoader.scala b/metals/src/main/scala/scala/meta/internal/metals/PresentationCompilerClassLoader.scala new file mode 100644 index 00000000000..55b3491c53e --- /dev/null +++ b/metals/src/main/scala/scala/meta/internal/metals/PresentationCompilerClassLoader.scala @@ -0,0 +1,22 @@ +package scala.meta.internal.metals + +/** + * ClassLoader that is used to reflectively invoke presentation compiler APIs. + * + * The presentation compiler APIs are compiled against exact Scala versions of the compiler + * while Metals only runs in a single Scala version. In order to communicate with + */ +class PresentationCompilerClassLoader(parent: ClassLoader) + extends ClassLoader(null) { + override def findClass(name: String): Class[_] = { + val isShared = + name.startsWith("org.eclipse.lsp4j") || + name.startsWith("com.google.gson") || + name.startsWith("scala.meta.pc") + if (isShared) { + parent.loadClass(name) + } else { + throw new ClassNotFoundException(name) + } + } +} diff --git a/metals/src/main/scala/scala/meta/internal/metals/StatusBar.scala b/metals/src/main/scala/scala/meta/internal/metals/StatusBar.scala index e66c1d44488..95ec3207f62 100644 --- a/metals/src/main/scala/scala/meta/internal/metals/StatusBar.scala +++ b/metals/src/main/scala/scala/meta/internal/metals/StatusBar.scala @@ -6,6 +6,7 @@ import java.util.concurrent.ScheduledFuture import java.util.concurrent.TimeUnit import java.util.concurrent.atomic.AtomicInteger import scala.concurrent.Future +import scala.concurrent.Promise import scala.meta.internal.metals.MetalsEnrichments._ import scala.util.control.NonFatal @@ -25,9 +26,23 @@ import scala.util.control.NonFatal final class StatusBar( client: () => MetalsLanguageClient, time: Time, - progressTicks: ProgressTicks = ProgressTicks.braille + progressTicks: ProgressTicks = ProgressTicks.braille, + val icons: Icons ) extends Cancelable { + def trackBlockingTask[T]( + message: String, + showTimer: Boolean = false + )(thunk: => T): T = { + val promise = Promise[Unit]() + trackFuture(message, promise.future) + try { + thunk + } finally { + promise.trySuccess(()) + } + } + def trackFuture[T]( message: String, value: Future[T], diff --git a/metals/src/main/scala/scala/meta/internal/metals/WorkspaceSymbolProvider.scala b/metals/src/main/scala/scala/meta/internal/metals/WorkspaceSymbolProvider.scala index d286e62822a..edc82e98ea5 100644 --- a/metals/src/main/scala/scala/meta/internal/metals/WorkspaceSymbolProvider.scala +++ b/metals/src/main/scala/scala/meta/internal/metals/WorkspaceSymbolProvider.scala @@ -1,25 +1,22 @@ package scala.meta.internal.metals +import ch.epfl.scala.bsp4j.BuildTargetIdentifier import com.google.common.hash.BloomFilter import com.google.common.hash.Funnels import java.nio.charset.StandardCharsets import java.nio.file.Path -import java.util -import java.util.Comparator -import java.util.PriorityQueue import java.util.concurrent.CancellationException import org.eclipse.lsp4j.jsonrpc.CancelChecker import org.eclipse.{lsp4j => l} import scala.collection.concurrent.TrieMap -import scala.collection.mutable -import scala.collection.mutable.ArrayBuffer import scala.concurrent.ExecutionContext import scala.meta.internal.metals.MetalsEnrichments._ import scala.meta.internal.mtags.MtagsEnrichments._ import scala.meta.internal.mtags.OnDemandSymbolIndex import scala.meta.internal.semanticdb.SymbolInformation.Kind -import scala.meta.internal.{semanticdb => s} import scala.meta.io.AbsolutePath +import scala.meta.pc.SymbolSearch +import scala.meta.pc.SymbolSearchVisitor import scala.util.control.NonFatal /** @@ -30,15 +27,12 @@ final class WorkspaceSymbolProvider( statistics: StatisticsConfig, val buildTargets: BuildTargets, val index: OnDemandSymbolIndex, - isReferencedPackage: String => Boolean, + isReferencedPackage: String => Int, fileOnDisk: AbsolutePath => AbsolutePath -)(implicit ec: ExecutionContext) { - val inWorkspace = TrieMap.empty[Path, BloomFilter[CharSequence]] - val inDependencies = TrieMap.empty[String, CompressedPackageIndex] - // The maximum number of non-exact matches that we return for classpath queries. - // Generic queries like "Str" can returns several thousand results, so we need - // to limit it at some arbitrary point. Exact matches are always included. - private val maxNonExactMatches = 10 +)(implicit ec: ExecutionContext) + extends SymbolSearch { + val inWorkspace = TrieMap.empty[Path, CompressedSourceIndex] + var inDependencies = ClasspathSearch.fromClasspath(Nil, isReferencedPackage) def search(query: String): Seq[l.SymbolInformation] = { search(query, () => ()) @@ -54,6 +48,18 @@ final class WorkspaceSymbolProvider( } } + override def search( + query: String, + buildTargetIdentifier: String, + visitor: SymbolSearchVisitor + ): SymbolSearch.Result = { + search( + WorkspaceSymbolQuery.exact(query), + visitor, + Some(new BuildTargetIdentifier(buildTargetIdentifier)) + ) + } + def indexClasspath(): Unit = { try { indexClasspathUnsafe() @@ -69,29 +75,24 @@ final class WorkspaceSymbolProvider( def didChange( source: AbsolutePath, - symbols: Seq[String] + symbols: Seq[CachedSymbolInformation] ): Unit = { - val bloomFilterStrings = Fuzzy.bloomFilterSymbolStrings(symbols) + val bloomFilterStrings = + Fuzzy.bloomFilterSymbolStrings(symbols.map(_.symbol)) val bloom = BloomFilter.create[CharSequence]( Funnels.stringFunnel(StandardCharsets.UTF_8), Integer.valueOf(bloomFilterStrings.size), 0.01 ) - inWorkspace(source.toNIO) = bloom bloomFilterStrings.foreach { c => bloom.put(c) } + inWorkspace(source.toNIO) = CompressedSourceIndex(bloom, symbols) } - private def isExcludedPackage(pkg: String): Boolean = { - // NOTE(olafur) I can't count how many times I've gotten unwanted results from these packages. - pkg.startsWith("com/sun/") || - pkg.startsWith("com/apple/") - } private def indexClasspathUnsafe(): Unit = { - inDependencies.clear() val packages = new PackageIndex() - packages.expandJdkClasspath() + packages.visitBootClasspath() for { target <- buildTargets.all classpathEntry <- target.scalac.classpath @@ -99,137 +100,62 @@ final class WorkspaceSymbolProvider( } { packages.visit(classpathEntry) } - for { - (pkg, members) <- packages.packages.asScala - if !isExcludedPackage(pkg) - } { - val buf = Fuzzy.bloomFilterSymbolStrings(members.asScala) - buf ++= Fuzzy.bloomFilterSymbolStrings(List(pkg), buf) - val bloom = BloomFilters.create(buf.size) - buf.foreach { key => - bloom.put(key) - } - // Sort members for deterministic order for deterministic results. - members.sort(String.CASE_INSENSITIVE_ORDER) - // Compress members because they make up the bulk of memory usage in the classpath index. - // For a 140mb classpath with spark/linkerd/akka/.. the members take up 12mb uncompressed - // and ~900kb compressed. We are accummulating a lot of different custom indexes in Metals - // so we should try to keep each of them as small as possible. - val compressedMembers = Compression.compress(members.asScala) - inDependencies(pkg) = CompressedPackageIndex(bloom, compressedMembers) - } + inDependencies = ClasspathSearch.fromPackages( + packages, + isReferencedPackage + ) } - private val byReferenceThenAlphabeticalComparator = new Comparator[String] { - override def compare(a: String, b: String): Int = { - val isReferencedA = isReferencedPackage(a) - val isReferencedB = isReferencedPackage(b) - val byReference = - -java.lang.Boolean.compare(isReferencedA, isReferencedB) - if (byReference != 0) byReference - else a.compare(b) - } + private def search( + query: WorkspaceSymbolQuery, + visitor: SymbolSearchVisitor, + target: Option[BuildTargetIdentifier] + ): SymbolSearch.Result = { + workspaceSearch(query, visitor, target) + inDependencies.search(query, visitor) } - private def packagesSortedByReferences(): Array[String] = { - val packages = inDependencies.keys.toArray - util.Arrays.sort(packages, byReferenceThenAlphabeticalComparator) - packages + private def workspaceSearch( + query: WorkspaceSymbolQuery, + visitor: SymbolSearchVisitor, + id: Option[BuildTargetIdentifier] + ): Unit = { + for { + (path, index) <- id match { + case None => + inWorkspace.iterator + case Some(target) => + for { + source <- buildTargets.buildTargetTransitiveSources(target) + index <- inWorkspace.get(source.toNIO) + } yield (source.toNIO, index) + } + if visitor.shouldVisitPath(path) + if query.matches(index.bloom) + symbol <- index.symbols + if query.matches(symbol.symbol) + } { + visitor.visitWorkspaceSymbol( + path, + symbol.symbol, + symbol.kind, + symbol.range + ) + } } private def searchUnsafe( textQuery: String, token: CancelChecker ): Seq[l.SymbolInformation] = { - val result = new PriorityQueue[l.SymbolInformation]( - (o1, o2) => -Integer.compare(o1.getName.length, o2.getName.length) - ) val query = WorkspaceSymbolQuery.fromTextQuery(textQuery) - def matches(info: s.SymbolInformation): Boolean = { - WorkspaceSymbolProvider.isRelevantKind(info.kind) && - query.matches(info.symbol) - } - def searchWorkspaceSymbols(): Unit = { - var visitsCount = 0 - var falsePositives = 0 - for { - (path, bloom) <- inWorkspace - _ = token.checkCanceled() - if query.matches(bloom) - } { - visitsCount += 1 - var isFalsePositive = true - val input = path.toUriInput - SemanticdbDefinition.foreach(input) { defn => - if (matches(defn.info)) { - isFalsePositive = false - result.add(defn.toLSP(input.path)) - } - } - if (isFalsePositive) { - falsePositives += 1 - } - } - } - def searchDependencySymbols(): Unit = { - val classfiles = new PriorityQueue[Classfile]( - (a, b) => Integer.compare(a.filename.length, b.filename.length) - ) - val packages = packagesSortedByReferences() - for { - pkg <- packages.iterator - compressed = inDependencies(pkg) - _ = token.checkCanceled() - if query.matches(compressed.bloom) - member <- compressed.members - if member.endsWith(".class") - name = member.subSequence(0, member.length - ".class".length) - symbol = new ConcatSequence(pkg, name) - isMatch = query.matches(symbol) - if isMatch - } { - classfiles.add(Classfile(pkg, member)) - } - val classpathEntries = ArrayBuffer.empty[l.SymbolInformation] - val isVisited = mutable.Set.empty[AbsolutePath] - var nonExactMatches = 0 - for { - hit <- classfiles.pollingIterator - _ = token.checkCanceled() - if nonExactMatches < maxNonExactMatches || hit.isExact(query) - defn <- hit.definition(index) - if !isVisited(defn.path) - } { - isVisited += defn.path - if (!hit.isExact(query)) { - nonExactMatches += 1 - } - val input = defn.path.toInput - lazy val uri = fileOnDisk(defn.path).toURI.toString - SemanticdbDefinition.foreach(input) { defn => - if (matches(defn.info)) { - classpathEntries += defn.toLSP(uri) - } - } - } - classpathEntries.foreach { s => - result.add(s) - } - } - searchWorkspaceSymbols() - searchDependencySymbols() - result.asScala.toSeq.sortBy(_.getName.length) + val visitor = new WorkspaceSymbolVisitor(query, token, index, fileOnDisk) + search(query, visitor, None) + visitor.results.sortBy(_.getName.length) } } object WorkspaceSymbolProvider { - def isRelevantKind(kind: Kind): Boolean = { - kind match { - case Kind.OBJECT | Kind.PACKAGE_OBJECT | Kind.CLASS | Kind.TRAIT | - Kind.INTERFACE => - true - case _ => - false - } - } + def isRelevantKind(kind: Kind): Boolean = + WorkspaceSymbolQuery.isRelevantKind(kind) } diff --git a/metals/src/main/scala/scala/meta/internal/metals/WorkspaceSymbolVisitor.scala b/metals/src/main/scala/scala/meta/internal/metals/WorkspaceSymbolVisitor.scala new file mode 100644 index 00000000000..35c95f7f416 --- /dev/null +++ b/metals/src/main/scala/scala/meta/internal/metals/WorkspaceSymbolVisitor.scala @@ -0,0 +1,77 @@ +package scala.meta.internal.metals + +import java.nio.file.Path +import org.eclipse.lsp4j.SymbolKind +import org.eclipse.lsp4j.jsonrpc.CancelChecker +import org.eclipse.{lsp4j => l} +import scala.collection.mutable +import scala.collection.mutable.ArrayBuffer +import scala.meta.internal.metals.MetalsEnrichments._ +import scala.meta.internal.mtags.MtagsEnrichments._ +import scala.meta.internal.mtags.OnDemandSymbolIndex +import scala.meta.internal.mtags.Symbol +import scala.meta.internal.mtags.SymbolDefinition +import scala.meta.internal.semanticdb.Scala.Descriptor +import scala.meta.internal.semanticdb.Scala.DescriptorParser +import scala.meta.internal.semanticdb.Scala.Symbols +import scala.meta.io.AbsolutePath +import scala.meta.pc.SymbolSearchVisitor + +class WorkspaceSymbolVisitor( + query: WorkspaceSymbolQuery, + token: CancelChecker, + index: OnDemandSymbolIndex, + fileOnDisk: AbsolutePath => AbsolutePath +) extends SymbolSearchVisitor { + val results = ArrayBuffer.empty[l.SymbolInformation] + val isVisited = mutable.Set.empty[AbsolutePath] + def definition( + pkg: String, + filename: String, + index: OnDemandSymbolIndex + ): Option[SymbolDefinition] = { + val nme = Classfile.name(filename) + val tpe = Symbol(Symbols.Global(pkg, Descriptor.Type(nme))) + index.definition(tpe).orElse { + val term = Symbol(Symbols.Global(pkg, Descriptor.Term(nme))) + index.definition(term) + } + } + override def shouldVisitPackage(pkg: String): Boolean = true + override def shouldVisitPath(path: Path): Boolean = true + + override def visitWorkspaceSymbol( + path: Path, + symbol: String, + kind: SymbolKind, + range: l.Range + ): Int = { + val (desc, owner) = DescriptorParser(symbol) + results += new l.SymbolInformation( + desc.name.value, + kind, + new l.Location(path.toUri.toString, range), + owner.replace('/', '.') + ) + 1 + } + override def visitClassfile(pkg: String, filename: String): Int = { + var isHit = false + for { + defn <- definition(pkg, filename, index) + if !isVisited(defn.path) + } { + isVisited += defn.path + val input = defn.path.toInput + lazy val uri = fileOnDisk(defn.path).toURI.toString + SemanticdbDefinition.foreach(input) { defn => + if (query.matches(defn.info)) { + results += defn.toLSP(uri) + isHit = true + } + } + } + if (isHit) 1 else 0 + } + override def isCancelled: Boolean = token.isCancelled +} diff --git a/mtags/src/main/scala/scala/meta/internal/mtags/JavaMtags.scala b/mtags/src/main/scala/scala/meta/internal/mtags/JavaMtags.scala index e146c8baa38..5c9d69ae8bb 100644 --- a/mtags/src/main/scala/scala/meta/internal/mtags/JavaMtags.scala +++ b/mtags/src/main/scala/scala/meta/internal/mtags/JavaMtags.scala @@ -2,6 +2,7 @@ package scala.meta.internal.mtags import com.thoughtworks.qdox._ import com.thoughtworks.qdox.model.JavaClass +import com.thoughtworks.qdox.model.JavaConstructor import com.thoughtworks.qdox.model.JavaField import com.thoughtworks.qdox.model.JavaMember import com.thoughtworks.qdox.model.JavaMethod @@ -81,11 +82,27 @@ class JavaMtags(virtualFile: Input.VirtualFile) extends MtagsIndexer { self => if (classes == null) () else classes.asScala.foreach(visitClass) + def visitClass( + cls: JavaClass, + name: String, + pos: Position, + kind: Kind, + properties: Int + ): Unit = { + tpe( + cls.getName, + pos, + kind, + if (cls.isEnum) Property.ENUM.value else 0 + ) + } + def visitClass(cls: JavaClass): Unit = withOwner(owner) { val kind = if (cls.isInterface) Kind.INTERFACE else Kind.CLASS val pos = toRangePosition(cls.lineNumber, cls.getName) - tpe( + visitClass( + cls, cls.getName, pos, kind, @@ -97,16 +114,39 @@ class JavaMtags(virtualFile: Input.VirtualFile) extends MtagsIndexer { self => visitMembers(cls.getFields) } + def visitConstructor( + ctor: JavaConstructor, + disambiguator: String, + pos: Position, + properties: Int + ): Unit = { + super.ctor(disambiguator, pos, 0) + } + def visitConstructors(cls: JavaClass): Unit = { val overloads = new OverloadDisambiguator() - cls.getConstructors.asScala.foreach { ctor => - val name = cls.getName - val disambiguator = overloads.disambiguator(name) - val pos = toRangePosition(ctor.lineNumber, name) - withOwner() { - super.ctor(disambiguator, pos, 0) + cls.getConstructors + .iterator() + .asScala + .filterNot(_.isPrivate) + .foreach { ctor => + val name = cls.getName + val disambiguator = overloads.disambiguator(name) + val pos = toRangePosition(ctor.lineNumber, name) + withOwner() { + visitConstructor(ctor, disambiguator, pos, 0) + } } - } + } + + def visitMethod( + method: JavaMethod, + name: String, + disambiguator: String, + pos: Position, + properties: Int + ): Unit = { + super.method(name, disambiguator, pos, properties) } def visitMethods(cls: JavaClass): Unit = { @@ -122,7 +162,7 @@ class JavaMtags(virtualFile: Input.VirtualFile) extends MtagsIndexer { self => val disambiguator = overloads.disambiguator(name) val pos = toRangePosition(method.lineNumber, name) withOwner() { - super.method(name, disambiguator, pos, 0) + visitMethod(method, name, disambiguator, pos, 0) } } } 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 b24e66c6602..b577abe1ece 100644 --- a/mtags/src/main/scala/scala/meta/internal/mtags/MtagsEnrichments.scala +++ b/mtags/src/main/scala/scala/meta/internal/mtags/MtagsEnrichments.scala @@ -4,7 +4,9 @@ import com.thoughtworks.qdox.model.JavaModel import java.nio.charset.StandardCharsets import java.nio.file.Path import java.nio.file.Paths +import java.util import scala.annotation.tailrec +import scala.collection.AbstractIterator import scala.meta.inputs.Input import scala.meta.inputs.Position import scala.meta.internal.io.FileIO @@ -126,4 +128,15 @@ object MtagsEnrichments { implicit class XtensionJavaModel(val m: JavaModel) extends AnyVal { def lineNumber: Int = m.getLineNumber - 1 } + implicit class XtensionJavaPriorityQueue[A](q: util.PriorityQueue[A]) { + + /** + * Returns iterator that consumes the priority queue in-order using `poll()`. + */ + def pollingIterator: Iterator[A] = new AbstractIterator[A] { + override def hasNext: Boolean = !q.isEmpty + override def next(): A = q.poll() + } + + } } diff --git a/mtags/src/main/scala/scala/meta/internal/mtags/ScalaMtags.scala b/mtags/src/main/scala/scala/meta/internal/mtags/ScalaMtags.scala index b2cf12661b7..3644c0d5097 100644 --- a/mtags/src/main/scala/scala/meta/internal/mtags/ScalaMtags.scala +++ b/mtags/src/main/scala/scala/meta/internal/mtags/ScalaMtags.scala @@ -17,16 +17,19 @@ object ScalaMtags { class ScalaMtags(val input: Input.VirtualFile) extends SimpleTraverser with MtagsIndexer { - private val root: Option[Source] = input.parse[Source].toOption + private val root: Parsed[Source] = input.parse[Source] + def source: Source = root.get override def language: Language = Language.SCALA override def indexRoot(): Unit = { root match { - case Some(tree) => apply(tree) + case Parsed.Success(tree) => apply(tree) case _ => // do nothing in case of parse error } // :facepalm: https://github.com/scalameta/scalameta/issues/1068 PlatformTokenizerCache.megaCache.clear() } + def currentTree: Tree = myCurrentTree + private var myCurrentTree: Tree = q"a" override def apply(tree: Tree): Unit = withOwner() { def continue(): Unit = super.apply(tree) def stop(): Unit = () @@ -76,31 +79,36 @@ class ScalaMtags(val input: Input.VirtualFile) } } } + myCurrentTree = tree tree match { case _: Source => continue() case t: Template => val overloads = new OverloadDisambiguator() overloads.disambiguator("") // primary constructor def disambiguatedMethod( + member: Member, name: Name, tparams: List[Type.Param], paramss: List[List[Term.Param]], kind: Kind ): Unit = { + val old = myCurrentTree + myCurrentTree = member val disambiguator = overloads.disambiguator(name.value) withOwner() { method(name, disambiguator, kind, 0) enterTypeParameters(tparams) enterTermParameters(paramss, isPrimaryCtor = false) } + myCurrentTree = old } t.stats.foreach { case t: Ctor.Secondary => - disambiguatedMethod(t.name, Nil, t.paramss, Kind.CONSTRUCTOR) + disambiguatedMethod(t, t.name, Nil, t.paramss, Kind.CONSTRUCTOR) case t: Defn.Def => - disambiguatedMethod(t.name, t.tparams, t.paramss, Kind.METHOD) + disambiguatedMethod(t, t.name, t.tparams, t.paramss, Kind.METHOD) case t: Decl.Def => - disambiguatedMethod(t.name, t.tparams, t.paramss, Kind.METHOD) + disambiguatedMethod(t, t.name, t.tparams, t.paramss, Kind.METHOD) case _ => } continue() diff --git a/pc/core/src/main/resources/META-INF/services/scala.meta.pc.PresentationCompiler b/pc/core/src/main/resources/META-INF/services/scala.meta.pc.PresentationCompiler new file mode 100644 index 00000000000..5ad62387012 --- /dev/null +++ b/pc/core/src/main/resources/META-INF/services/scala.meta.pc.PresentationCompiler @@ -0,0 +1 @@ +scala.meta.internal.pc.ScalaPresentationCompiler diff --git a/pc/core/src/main/scala/scala/meta/internal/docstrings/Comment.scala b/pc/core/src/main/scala/scala/meta/internal/docstrings/Comment.scala new file mode 100644 index 00000000000..abece4027c5 --- /dev/null +++ b/pc/core/src/main/scala/scala/meta/internal/docstrings/Comment.scala @@ -0,0 +1,869 @@ +package scala.meta.internal.docstrings + +import scala.collection._ + +/** A type. Note that types and templates contain the same information only for the simplest types. For example, a type + * defines how a template's type parameters are instantiated (as in `List[Cow]`), what the template's prefix is + * (as in `johnsFarm.Cow`), and supports compound or structural types. */ +abstract class TypeEntity { + + /** The human-readable representation of this type. */ + def name: String + + /** Maps which parts of this type's name reference entities. The map is indexed by the position of the first + * character that reference some entity, and contains the entity and the position of the last referenced + * character. The referenced character ranges do not to overlap or nest. The map is sorted by position. */ + def refEntity: SortedMap[Int, (LinkTo, Int)] + + /** The human-readable representation of this type. */ + override def toString = name +} + +/** An entity in a Scaladoc universe. Entities are declarations in the program and correspond to symbols in the + * compiler. Entities model the following Scala concepts: + * - classes and traits; + * - objects and package; + * - constructors; + * - methods; + * - values, lazy values, and variables; + * - abstract type members and type aliases; + * - type and value parameters; + * - annotations. */ +trait Entity { + + /** The name of the entity. Note that the name does not qualify this entity uniquely; use its `qualifiedName` + * instead. */ + def name: String + + /** The qualified name of the entity. This is this entity's name preceded by the qualified name of the template + * of which this entity is a member. The qualified name is unique to this entity. */ + def qualifiedName: String + + /** The template of which this entity is a member. */ + def inTemplate: TemplateEntity + + /** The list of entities such that each is a member of the entity that follows it; the first entity is always this + * entity, the last the root package entity. */ + def toRoot: List[Entity] + + /** The qualified name of this entity. */ + override def toString = qualifiedName + +// /** The Scaladoc universe of which this entity is a member. */ +// def universe: Universe + + /** The annotations attached to this entity, if any. */ + def annotations: List[Annotation] + + /** The kind of the entity */ + def kind: String + + /** Whether or not the template was defined in a package object */ + def inPackageObject: Boolean + + /** Indicates whether this entity lives in the types namespace (classes, traits, abstract/alias types) */ + def isType: Boolean +} + +object Entity { + private def isDeprecated(x: Entity) = x match { + case x: MemberEntity => x.deprecation.isDefined + case _ => false + } + + private def isObject(x: Entity) = x match { + case x: TemplateEntity => x.isObject + case _ => false + } + + /** Ordering deprecated things last. */ + implicit lazy val EntityOrdering: Ordering[Entity] = + Ordering[(Boolean, String, Boolean)] on ( + x => (isDeprecated(x), x.qualifiedName, isObject(x)) + ) +} + +/** A template, which is either a class, trait, object or package. Depending on whether documentation is available + * or not, the template will be modeled as a [scala.tools.nsc.doc.model.NoDocTemplate] or a + * [scala.tools.nsc.doc.model.DocTemplateEntity]. */ +trait TemplateEntity extends Entity { + + /** Whether this template is a package (including the root package). */ + def isPackage: Boolean + + /** Whether this template is the root package. */ + def isRootPackage: Boolean + + /** Whether this template is a trait. */ + def isTrait: Boolean + + /** Whether this template is a class. */ + def isClass: Boolean + + /** Whether this template is an object. */ + def isObject: Boolean + + /** Whether documentation is available for this template. */ + def isDocTemplate: Boolean + + /** Whether this template is a case class. */ + def isCaseClass: Boolean + + /** The self-type of this template, if it differs from the template type. */ + def selfType: Option[TypeEntity] +} + +/** An entity that is a member of a template. All entities, including templates, are member of another entity + * except for parameters and annotations. Note that all members of a template are modelled, including those that are + * inherited and not declared locally. */ +trait MemberEntity extends Entity { + + /** The comment attached to this member, if any. */ + def comment: Option[Comment] + + /** The group this member is from */ + def group: String + + /** The template of which this entity is a member. */ + def inTemplate: DocTemplateEntity + + /** The list of entities such that each is a member of the entity that follows it; the first entity is always this + * member, the last the root package entity. */ + def toRoot: List[MemberEntity] + + /** The templates in which this member has been declared. The first element of the list is the template that contains + * the currently active declaration of this member, subsequent elements are declarations that have been overridden. If + * the first element is equal to `inTemplate`, the member is declared locally, if not, it has been inherited. All + * elements of this list are in the linearization of `inTemplate`. */ + def inDefinitionTemplates: List[TemplateEntity] + + /** The qualified name of the member in its currently active declaration template. */ + def definitionName: String + +// /** The visibility of this member. Note that members with restricted visibility may not be modeled in some +// * universes. */ +// def visibility: Visibility + + /** The flags that have been set for this entity. The following flags are supported: `implicit`, `sealed`, `abstract`, + * and `final`. */ + def flags: List[Paragraph] + + /** Some deprecation message if this member is deprecated, or none otherwise. */ + def deprecation: Option[Body] + + /** Some migration warning if this member has a migration annotation, or none otherwise. */ + def migration: Option[Body] + + /** For members representing values: the type of the value returned by this member; for members + * representing types: the type itself. */ + def resultType: TypeEntity + + /** Whether this member is a method. */ + def isDef: Boolean + + /** Whether this member is a value (this excludes lazy values). */ + def isVal: Boolean + + /** Whether this member is a lazy value. */ + def isLazyVal: Boolean + + /** Whether this member is a variable. */ + def isVar: Boolean + + /** Whether this member is a constructor. */ + def isConstructor: Boolean + + /** Whether this member is an alias type. */ + def isAliasType: Boolean + + /** Whether this member is an abstract type. */ + def isAbstractType: Boolean + + /** Whether this member is abstract. */ + def isAbstract: Boolean + + /** If this symbol is a use case, the useCaseOf will contain the member it was derived from, containing the full + * signature and the complete parameter descriptions. */ + def useCaseOf: Option[MemberEntity] + + /** If this member originates from an implicit conversion, we set the implicit information to the correct origin */ + def byConversion: Option[ImplicitConversion] + + /** The identity of this member, used for linking */ + def signature: String + + /** Compatibility signature, will be removed from future versions */ + def signatureCompat: String + + /** Indicates whether the member is inherited by implicit conversion */ + def isImplicitlyInherited: Boolean + + /** Indicates whether there is another member with the same name in the template that will take precedence */ + def isShadowedImplicit: Boolean + + /** Indicates whether there are other implicitly inherited members that have similar signatures (and thus they all + * become ambiguous) */ + def isAmbiguousImplicit: Boolean + + /** Indicates whether the implicitly inherited member is shadowed or ambiguous in its template */ + def isShadowedOrAmbiguousImplicit: Boolean +} + +object MemberEntity { + // Oh contravariance, contravariance, wherefore art thou contravariance? + // Note: the above works for both the commonly misunderstood meaning of the line and the real one. + implicit lazy val MemberEntityOrdering + : Ordering[MemberEntity] = Entity.EntityOrdering on (x => x) +} + +/** An entity that is parameterized by types */ +trait HigherKinded { + + /** The type parameters of this entity. */ + def typeParams: List[TypeParam] +} + +/** A template (class, trait, object or package) which is referenced in the universe, but for which no further + * documentation is available. Only templates for which a source file is given are documented by Scaladoc. */ +trait NoDocTemplate extends TemplateEntity { + def kind = + if (isClass) "class" + else if (isTrait) "trait" + else if (isObject) "object" + else "" +} + +/** An inherited template that was not documented in its original owner - example: + * in classpath: trait T { class C } -- T (and implicitly C) are not documented + * in the source: trait U extends T -- C appears in U as a MemberTemplateImpl + * -- that is, U has a member for it but C doesn't get its own page */ +trait MemberTemplateEntity + extends TemplateEntity + with MemberEntity + with HigherKinded { + + /** The value parameters of this case class, or an empty list if this class is not a case class. As case class value + * parameters cannot be curried, the outer list has exactly one element. */ + def valueParams: List[List[ValueParam]] + + /** The direct super-type of this template + e.g: {{{class A extends B[C[Int]] with D[E]}}} will have two direct parents: class B and D + NOTE: we are dropping the refinement here! */ + def parentTypes: List[(TemplateEntity, TypeEntity)] +} + +/** A template (class, trait, object or package) for which documentation is available. Only templates for which + * a source file is given are documented by Scaladoc. */ +trait DocTemplateEntity extends MemberTemplateEntity { + + /** The list of templates such that each is a member of the template that follows it; the first template is always + * this template, the last the root package entity. */ + def toRoot: List[DocTemplateEntity] + +// /** The source file in which the current template is defined and the line where the definition starts, if they exist. +// * A source file exists for all templates, except for those that are generated synthetically by Scaladoc. */ +// def inSource: Option[(io.AbstractFile, Int)] + + /** An HTTP address at which the source of this template is available, if it is available. An address is available + * only if the `docsourceurl` setting has been set. */ + def sourceUrl: Option[java.net.URL] + + /** All class, trait and object templates which are part of this template's linearization, in linearization order. + * This template's linearization contains all of its direct and indirect super-classes and super-traits. */ + def linearizationTemplates: List[TemplateEntity] + + /** All instantiated types which are part of this template's linearization, in linearization order. + * This template's linearization contains all of its direct and indirect super-types. */ + def linearizationTypes: List[TypeEntity] + + /** All class, trait and object templates for which this template is a *direct* super-class or super-trait. + * Only templates for which documentation is available in the universe (`DocTemplateEntity`) are listed. */ + def directSubClasses: List[DocTemplateEntity] + + /** All members of this template. If this template is a package, only templates for which documentation is available + * in the universe (`DocTemplateEntity`) are listed. */ + def members: List[MemberEntity] + + /** All templates that are members of this template. If this template is a package, only templates for which + * documentation is available in the universe (`DocTemplateEntity`) are listed. */ + def templates: List[TemplateEntity with MemberEntity] + + /** All methods that are members of this template. */ + def methods: List[Def] + + /** All values, lazy values and variables that are members of this template. */ + def values: List[Val] + + /** All abstract types that are members of this template. */ + def abstractTypes: List[AbstractType] + + /** All type aliases that are members of this template. */ + def aliasTypes: List[AliasType] + + /** The primary constructor of this class, if it has been defined. */ + def primaryConstructor: Option[Constructor] + + /** All constructors of this class, including the primary constructor. */ + def constructors: List[Constructor] + + /** The companion of this template, or none. If a class and an object are defined as a pair of the same name, the + * other entity of the pair is the companion. */ + def companion: Option[DocTemplateEntity] + + /** The implicit conversions this template (class or trait, objects and packages are not affected) */ + def conversions: List[ImplicitConversion] + + /** The shadowing information for the implicitly added members */ + def implicitsShadowing: Map[MemberEntity, ImplicitMemberShadowing] + + /** Classes that can be implicitly converted to this class */ + def incomingImplicitlyConvertedClasses + : List[(DocTemplateEntity, ImplicitConversion)] + + /** Classes to which this class can be implicitly converted to + NOTE: Some classes might not be included in the scaladoc run so they will be NoDocTemplateEntities */ + def outgoingImplicitlyConvertedClasses + : List[(TemplateEntity, TypeEntity, ImplicitConversion)] + +// /** If this template takes place in inheritance and implicit conversion relations, it will be shown in this diagram */ +// def inheritanceDiagram: Option[Diagram] +// +// /** If this template contains other templates, such as classes and traits, they will be shown in this diagram */ +// def contentDiagram: Option[Diagram] + + /** Returns the group description taken either from this template or its linearizationTypes */ + def groupDescription(group: String): Option[Body] + + /** Returns the group description taken either from this template or its linearizationTypes */ + def groupPriority(group: String): Int + + /** Returns the group description taken either from this template or its linearizationTypes */ + def groupName(group: String): String +} + +/** A trait template. */ +trait Trait extends MemberTemplateEntity { + def kind = "trait" +} + +/** A class template. */ +trait Class extends MemberTemplateEntity { + override def kind = "class" +} + +/** An object template. */ +trait Object extends MemberTemplateEntity { + def kind = "object" +} + +/** A package template. A package is in the universe if it is declared as a package object, or if it + * contains at least one template. */ +trait Package extends DocTemplateEntity { + + /** The package of which this package is a member. */ + def inTemplate: Package + + /** The package such that each is a member of the package that follows it; the first package is always this + * package, the last the root package. */ + def toRoot: List[Package] + + /** All packages that are member of this package. */ + def packages: List[Package] + + override def kind = "package" +} + +/** The root package, which contains directly or indirectly all members in the universe. A universe + * contains exactly one root package. */ +trait RootPackage extends Package + +/** A non-template member (method, value, lazy value, variable, constructor, alias type, and abstract type). */ +trait NonTemplateMemberEntity extends MemberEntity { + + /** Whether this member is a use case. A use case is a member which does not exist in the documented code. + * It corresponds to a real member, and provides a simplified, yet compatible signature for that member. */ + def isUseCase: Boolean +} + +/** A method (`def`) of a template. */ +trait Def extends NonTemplateMemberEntity with HigherKinded { + + /** The value parameters of this method. Each parameter block of a curried method is an element of the list. + * Each parameter block is a list of value parameters. */ + def valueParams: List[List[ValueParam]] + + def kind = "method" +} + +/** A constructor of a class. */ +trait Constructor extends NonTemplateMemberEntity { + + /** Whether this is the primary constructor of a class. The primary constructor is defined syntactically as part of + * the declaration of the class. */ + def isPrimary: Boolean + + /** The value parameters of this constructor. As constructors cannot be curried, the outer list has exactly one + * element. */ + def valueParams: List[List[ValueParam]] + + def kind = "constructor" +} + +/** A value (`val`), lazy val (`lazy val`) or variable (`var`) of a template. */ +trait Val extends NonTemplateMemberEntity { + def kind = "[lazy] value/variable" +} + +/** An abstract type member of a template. */ +trait AbstractType extends MemberTemplateEntity with HigherKinded { + + /** The lower bound for this abstract type, if it has been defined. */ + def lo: Option[TypeEntity] + + /** The upper bound for this abstract type, if it has been defined. */ + def hi: Option[TypeEntity] + + def kind = "abstract type" +} + +/** An type alias of a template. */ +trait AliasType extends MemberTemplateEntity with HigherKinded { + + /** The type aliased by this type alias. */ + def alias: TypeEntity + + def kind = "type alias" +} + +/** A parameter to an entity. */ +trait ParameterEntity { + + def name: String +} + +/** A type parameter to a class, trait, or method. */ +trait TypeParam extends ParameterEntity with HigherKinded { + + /** The variance of this type parameter. Valid values are "+", "-", and the empty string. */ + def variance: String + + /** The lower bound for this type parameter, if it has been defined. */ + def lo: Option[TypeEntity] + + /** The upper bound for this type parameter, if it has been defined. */ + def hi: Option[TypeEntity] +} + +/** A value parameter to a constructor or method. */ +trait ValueParam extends ParameterEntity { + + /** The type of this value parameter. */ + def resultType: TypeEntity + +// /** The default value of this value parameter, if it has been defined. */ +// def defaultValue: Option[TreeEntity] + + /** Whether this value parameter is implicit. */ + def isImplicit: Boolean +} + +/** An annotation to an entity. */ +trait Annotation extends Entity { + + /** The class of this annotation. */ + def annotationClass: TemplateEntity + +// /** The arguments passed to the constructor of the annotation class. */ +// def arguments: List[ValueArgument] + + def kind = "annotation" +} + +/** A trait that signals the member results from an implicit conversion */ +trait ImplicitConversion { + + /** The source of the implicit conversion*/ + def source: DocTemplateEntity + + /** The result type after the conversion */ + def targetType: TypeEntity + + /** The components of the implicit conversion type parents */ + def targetTypeComponents: List[(TemplateEntity, TypeEntity)] + + /** The entity for the method that performed the conversion, if it's documented (or just its name, otherwise) */ + def convertorMethod: Either[MemberEntity, String] + + /** A short name of the conversion */ + def conversionShortName: String + + /** A qualified name uniquely identifying the conversion (currently: the conversion method's qualified name) */ + def conversionQualifiedName: String + + /** The entity that performed the conversion */ + def convertorOwner: TemplateEntity + + /** The constraints that the transformations puts on the type parameters */ + def constraints: List[Constraint] + + /** The members inherited by this implicit conversion */ + def members: List[MemberEntity] + + /** Is this a hidden implicit conversion (as specified in the settings) */ + def isHiddenConversion: Boolean +} + +/** Shadowing captures the information that the member is shadowed by some other members + * There are two cases of implicitly added member shadowing: + * 1) shadowing from an original class member (the class already has that member) + * in this case, it won't be possible to call the member directly, the type checker will fail attempting to adapt + * the call arguments (or if they fit it will call the original class method) + * 2) shadowing from other possible implicit conversions () + * this will result in an ambiguous implicit conversion error + */ +trait ImplicitMemberShadowing { + + /** The members that shadow the current entry use .inTemplate to get to the template name */ + def shadowingMembers: List[MemberEntity] + + /** The members that ambiguate this implicit conversion + Note: for ambiguatingMembers you have the following invariant: + assert(ambiguatingMembers.foreach(_.byConversion.isDefined) */ + def ambiguatingMembers: List[MemberEntity] + + def isShadowed: Boolean = shadowingMembers.nonEmpty + def isAmbiguous: Boolean = ambiguatingMembers.nonEmpty +} + +/** A trait that encapsulates a constraint necessary for implicit conversion */ +trait Constraint + +/** A constraint involving a type parameter which must be in scope */ +trait ImplicitInScopeConstraint extends Constraint { + + /** The type of the implicit value required */ + def implicitType: TypeEntity + + /** toString for debugging */ + override def toString = + "an implicit _: " + implicitType.name + " must be in scope" +} + +trait TypeClassConstraint + extends ImplicitInScopeConstraint + with TypeParamConstraint { + + /** Type class name */ + def typeClassEntity: TemplateEntity + + /** toString for debugging */ + override def toString = + typeParamName + " is a class of type " + typeClassEntity.qualifiedName + " (" + + typeParamName + ": " + typeClassEntity.name + ")" +} + +trait KnownTypeClassConstraint extends TypeClassConstraint { + + /** Type explanation, takes the type parameter name and generates the explanation */ + def typeExplanation: (String) => String + + /** toString for debugging */ + override def toString = + typeExplanation(typeParamName) + " (" + typeParamName + ": " + typeClassEntity.name + ")" +} + +/** A constraint involving a type parameter */ +trait TypeParamConstraint extends Constraint { + + /** The type parameter involved */ + def typeParamName: String +} + +trait EqualTypeParamConstraint extends TypeParamConstraint { + + /** The rhs */ + def rhs: TypeEntity + + /** toString for debugging */ + override def toString = + typeParamName + " is " + rhs.name + " (" + typeParamName + " =:= " + rhs.name + ")" +} + +trait BoundedTypeParamConstraint extends TypeParamConstraint { + + /** The lower bound */ + def lowerBound: TypeEntity + + /** The upper bound */ + def upperBound: TypeEntity + + /** toString for debugging */ + override def toString = + typeParamName + " is a superclass of " + lowerBound.name + " and a subclass of " + + upperBound.name + " (" + typeParamName + " >: " + lowerBound.name + " <: " + upperBound.name + ")" +} + +trait LowerBoundedTypeParamConstraint extends TypeParamConstraint { + + /** The lower bound */ + def lowerBound: TypeEntity + + /** toString for debugging */ + override def toString = + typeParamName + " is a superclass of " + lowerBound.name + " (" + typeParamName + " >: " + + lowerBound.name + ")" +} + +trait UpperBoundedTypeParamConstraint extends TypeParamConstraint { + + /** The lower bound */ + def upperBound: TypeEntity + + /** toString for debugging */ + override def toString = + typeParamName + " is a subclass of " + upperBound.name + " (" + typeParamName + " <: " + + upperBound.name + ")" +} + +sealed trait LinkTo +final case class LinkToMember[Mbr, Tpl](mbr: Mbr, tpl: Tpl) extends LinkTo +final case class LinkToTpl[Tpl](tpl: Tpl) extends LinkTo +final case class LinkToExternalTpl( + name: String, + baseUrl: String, + tpl: TemplateEntity +) extends LinkTo +final case class Tooltip(name: String) extends LinkTo + +/** A body of text. A comment has a single body, which is composed of + * at least one block. Inside every body is exactly one summary. + * @see [[Summary]] + */ +final case class Body(blocks: Seq[Block]) { + + /** The summary text of the comment body. */ + lazy val summary: Option[Inline] = { + def summaryInBlock(block: Block): Seq[Inline] = block match { + case Title(text, _) => summaryInInline(text) + case Paragraph(text) => summaryInInline(text) + case UnorderedList(items) => items flatMap summaryInBlock + case OrderedList(items, _) => items flatMap summaryInBlock + case DefinitionList(items) => items.values.toSeq flatMap summaryInBlock + case _ => Nil + } + def summaryInInline(text: Inline): Seq[Inline] = text match { + case Summary(text) => List(text) + case Chain(items) => items flatMap summaryInInline + case Italic(text) => summaryInInline(text) + case Bold(text) => summaryInInline(text) + case Underline(text) => summaryInInline(text) + case Superscript(text) => summaryInInline(text) + case Subscript(text) => summaryInInline(text) + case Link(_, title) => summaryInInline(title) + case _ => Nil + } + (blocks flatMap { summaryInBlock(_) }).toList match { + case Nil => None + case inline :: Nil => Some(inline) + case inlines => Some(Chain(inlines)) + } + } +} + +/** A block-level element of text, such as a paragraph or code block. */ +sealed abstract class Block + +final case class Title(text: Inline, level: Int) extends Block +final case class Paragraph(text: Inline) extends Block +final case class Code(data: String) extends Block +final case class UnorderedList(items: Seq[Block]) extends Block +final case class OrderedList(items: Seq[Block], style: String) extends Block +final case class DefinitionList(items: SortedMap[Inline, Block]) extends Block +final case class HorizontalRule() extends Block +final case class Table( + header: Row, + columnOptions: Seq[ColumnOption], + rows: Seq[Row] +) extends Block +final case class ColumnOption(option: Char) { + require(option == 'L' || option == 'C' || option == 'R') +} +object ColumnOption { + val ColumnOptionLeft = ColumnOption('L') + val ColumnOptionCenter = ColumnOption('C') + val ColumnOptionRight = ColumnOption('R') +} +final case class Row(cells: Seq[Cell]) +final case class Cell(blocks: Seq[Block]) + +/** An section of text inside a block, possibly with formatting. */ +sealed abstract class Inline + +final case class Chain(items: Seq[Inline]) extends Inline +final case class Italic(text: Inline) extends Inline +final case class Bold(text: Inline) extends Inline +final case class Underline(text: Inline) extends Inline +final case class Superscript(text: Inline) extends Inline +final case class Subscript(text: Inline) extends Inline +final case class Link(target: String, title: Inline) extends Inline +final case class Monospace(text: Inline) extends Inline +final case class Text(text: String) extends Inline +abstract class EntityLink(val title: Inline) extends Inline { def link: LinkTo } +object EntityLink { + def apply(title: Inline, linkTo: LinkTo) = new EntityLink(title) { + def link: LinkTo = linkTo + } + def unapply(el: EntityLink): Option[(Inline, LinkTo)] = + Some((el.title, el.link)) +} +final case class HtmlTag(data: String) extends Inline { + private val (isEnd, tagName) = data match { + case HtmlTag.Pattern(s1, s2) => + (!s1.isEmpty, Some(s2.toLowerCase)) + case _ => + (false, None) + } + + def canClose(open: HtmlTag) = { + isEnd && tagName == open.tagName + } + + def close = tagName collect { + case name if !HtmlTag.TagsNotToClose(name) && !data.endsWith(s"") => + HtmlTag(s"") + } +} +object HtmlTag { + private val Pattern = """(?ms)\A<(/?)(.*?)[\s>].*\z""".r + private val TagsNotToClose = Set("br", "img") +} + +/** The summary of a comment, usually its first sentence. There must be exactly one summary per body. */ +final case class Summary(text: Inline) extends Inline + +/** A Scaladoc comment and all its tags. + * + * '''Note:''' the only instantiation site of this class is in [[model.CommentFactory]]. + * + * @author Manohar Jonnalagedda + * @author Gilles Dubochet */ +abstract class Comment { + + /** The main body of the comment that describes what the entity does and is. */ + def body: Body + + private def closeHtmlTags(inline: Inline): Inline = { + val stack = mutable.ListBuffer.empty[HtmlTag] + def scan(i: Inline) { + i match { + case Chain(list) => + list foreach scan + case tag: HtmlTag => { + if (stack.nonEmpty && tag.canClose(stack.last)) { + stack.remove(stack.length - 1) + } else { + tag.close match { + case Some(t) => + stack += t + case None => + ; + } + } + } + case _ => + ; + } + } + scan(inline) + Chain(List(inline) ++ stack.reverse) + } + + /** A shorter version of the body. Either from `@shortDescription` or the + * first sentence of the body. */ + def short: Inline = { + shortDescription orElse body.summary match { + case Some(s) => + closeHtmlTags(s) + case _ => + Text("") + } + } + + /** A list of authors. The empty list is used when no author is defined. */ + def authors: List[Body] + + /** A list of other resources to see, including links to other entities or + * to external documentation. The empty list is used when no other resource + * is mentioned. */ + def see: List[Body] + + /** A description of the result of the entity. Typically, this provides additional + * information on the domain of the result, contractual post-conditions, etc. */ + def result: Option[Body] + + /** A map of exceptions that the entity can throw when accessed, and a + * description of what they mean. */ + def throws: Map[String, Body] + + /** A map of value parameters, and a description of what they are. Typically, + * this provides additional information on the domain of the parameters, + * contractual pre-conditions, etc. */ + def valueParams: Map[String, Body] + + /** A map of type parameters, and a description of what they are. Typically, + * this provides additional information on the domain of the parameters. */ + def typeParams: Map[String, Body] + + /** The version number of the entity. There is no formatting or further + * meaning attached to this value. */ + def version: Option[Body] + + /** A version number of a containing entity where this member-entity was introduced. */ + def since: Option[Body] + + /** An annotation as to expected changes on this entity. */ + def todo: List[Body] + + /** Whether the entity is deprecated. Using the `@deprecated` Scala attribute + * is preferable to using this Scaladoc tag. */ + def deprecated: Option[Body] + + /** An additional note concerning the contract of the entity. */ + def note: List[Body] + + /** A usage example related to the entity. */ + def example: List[Body] + + /** A description for the primary constructor */ + def constructor: Option[Body] + + /** A set of diagram directives for the inheritance diagram */ + def inheritDiagram: List[String] + + /** A set of diagram directives for the content diagram */ + def contentDiagram: List[String] + + /** The group this member is part of */ + def group: Option[String] + + /** Member group descriptions */ + def groupDesc: Map[String, Body] + + /** Member group names (overriding the short tag) */ + def groupNames: Map[String, String] + + /** Member group priorities */ + def groupPrio: Map[String, Int] + + /** A list of implicit conversions to hide */ + def hideImplicitConversions: List[String] + + /** A short description used in the entity-view and search results */ + def shortDescription: Option[Text] + + override def toString = + body.toString + "\n" + + (authors map ("@author " + _.toString)).mkString("\n") + + (result map ("@return " + _.toString)).mkString("\n") + + (version map ("@version " + _.toString)).mkString +} diff --git a/pc/core/src/main/scala/scala/meta/internal/docstrings/ScaladocParser.scala b/pc/core/src/main/scala/scala/meta/internal/docstrings/ScaladocParser.scala new file mode 100644 index 00000000000..eb4042cd400 --- /dev/null +++ b/pc/core/src/main/scala/scala/meta/internal/docstrings/ScaladocParser.scala @@ -0,0 +1,1407 @@ +package scala.meta.internal.docstrings + +import scala.annotation.tailrec +import scala.collection.Map +import scala.collection.Seq +import scala.collection.mutable +import scala.reflect.internal.util.NoPosition +import scala.reflect.internal.util.Position +import scala.util.matching.Regex + +/** + * A fork of the Scaladoc parser in the Scala compiler with a few removed features. + * + * Removed features: + * - linking to symbols + * - reporting warnings + * + */ +object ScaladocParser { + + /* Creates comments with necessary arguments */ + def createComment( + body0: Option[Body] = None, + authors0: List[Body] = List.empty, + see0: List[Body] = List.empty, + result0: Option[Body] = None, + throws0: Map[String, Body] = Map.empty, + valueParams0: Map[String, Body] = Map.empty, + typeParams0: Map[String, Body] = Map.empty, + version0: Option[Body] = None, + since0: Option[Body] = None, + todo0: List[Body] = List.empty, + deprecated0: Option[Body] = None, + note0: List[Body] = List.empty, + example0: List[Body] = List.empty, + constructor0: Option[Body] = None, + inheritDiagram0: List[String] = List.empty, + contentDiagram0: List[String] = List.empty, + group0: Option[Body] = None, + groupDesc0: Map[String, Body] = Map.empty, + groupNames0: Map[String, Body] = Map.empty, + groupPrio0: Map[String, Body] = Map.empty, + hideImplicitConversions0: List[Body] = List.empty, + shortDescription0: List[Body] = List.empty + ): Comment = new Comment { + val body = body0 getOrElse Body(Seq.empty) + val authors = authors0 + val see = see0 + val result = result0 + val throws = throws0 + val valueParams = valueParams0 + val typeParams = typeParams0 + val version = version0 + val since = since0 + val todo = todo0 + val deprecated = deprecated0 + val note = note0 + val example = example0 + val constructor = constructor0 + val inheritDiagram = inheritDiagram0 + val contentDiagram = contentDiagram0 + val groupDesc = groupDesc0 + val group = + group0 match { + case Some(Body(List(Paragraph(Chain(List(Summary(Text(groupId)))))))) => + Some(groupId.toString.trim) + case _ => None + } + val groupPrio = groupPrio0 flatMap { + case (group, body) => + try { + body match { + case Body(List(Paragraph(Chain(List(Summary(Text(prio))))))) => + List(group -> prio.trim.toInt) + case _ => List() + } + } catch { + case _: java.lang.NumberFormatException => List() + } + } + val groupNames = groupNames0 flatMap { + case (group, body) => + body match { + case Body(List(Paragraph(Chain(List(Summary(Text(name))))))) + if (!name.trim.contains("\n")) => + List(group -> (name.trim)) + case _ => List() + } + } + + override val shortDescription + : Option[Text] = shortDescription0.lastOption collect { + case Body(List(Paragraph(Chain(List(Summary(Text(e))))))) + if !e.trim.contains("\n") => + Text(e) + } + + override val hideImplicitConversions: List[String] = + hideImplicitConversions0 flatMap { + case Body(List(Paragraph(Chain(List(Summary(Text(e))))))) + if !e.trim.contains("\n") => + List(e) + case _ => List() + } + } + + private val endOfText = '\u0003' + private val endOfLine = '\u000A' + + /** Something that should not have happened, happened, and Scaladoc should exit. */ + private def oops(msg: String): Nothing = + sys.error("program logic: " + msg) + + /** The body of a line, dropping the (optional) start star-marker, + * one leading whitespace and all trailing whitespace. */ + private val CleanCommentLine = + new Regex("""(?:\s*\*\s?)?(.*)""") + + /** Dangerous HTML tags that should be replaced by something safer, + * such as wiki syntax, or that should be dropped. */ + private val DangerousTags = + new Regex("""<(/?(div|ol|ul|li|h[1-6]|p))( [^>]*)?/?>|""") + + /** Maps a dangerous HTML tag to a safe wiki replacement, or an empty string + * if it cannot be salvaged. */ + private def htmlReplacement(mtch: Regex.Match): String = mtch.group(1) match { + case "p" | "div" => "\n\n" + case "h1" => "\n= " + case "/h1" => " =\n" + case "h2" => "\n== " + case "/h2" => " ==\n" + case "h3" => "\n=== " + case "/h3" => " ===\n" + case "h4" | "h5" | "h6" => "\n==== " + case "/h4" | "/h5" | "/h6" => " ====\n" + case "li" => "\n * - " + case _ => "" + } + + /** Javadoc tags that should be replaced by something useful, such as wiki + * syntax, or that should be dropped. */ + private val JavadocTags = + new Regex( + """\{\@(code|docRoot|linkplain|link|literal|value)\p{Zs}*([^}]*)\}""" + ) + + /** Maps a javadoc tag to a useful wiki replacement, or an empty string if it cannot be salvaged. */ + private def javadocReplacement(mtch: Regex.Match): String = { + mtch.group(1) match { + case "code" => "" + mtch.group(2) + "" + case "docRoot" => "" + case "link" => "`[[" + mtch.group(2) + "]]`" + case "linkplain" => "[[" + mtch.group(2) + "]]" + case "literal" => "`" + mtch.group(2) + "`" + case "value" => "`" + mtch.group(2) + "`" + case _ => "" + } + } + + /** Safe HTML tags that can be kept. */ + private val SafeTags = + new Regex( + """((&\w+;)|(&#\d+;)|(]*)?/?>))""" + ) + + private val safeTagMarker = '\u000E' + + /** A Scaladoc tag not linked to a symbol and not followed by text */ + private val SingleTagRegex = + new Regex("""\s*@(\S+)\s*""") + + /** A Scaladoc tag not linked to a symbol. Returns the name of the tag, and the rest of the line. */ + private val SimpleTagRegex = + new Regex("""\s*@(\S+)\s+(.*)""") + + /** A Scaladoc tag linked to a symbol. Returns the name of the tag, the name + * of the symbol, and the rest of the line. */ + private val SymbolTagRegex = + new Regex( + """\s*@(param|tparam|throws|groupdesc|groupname|groupprio)\s+(\S*)\s*(.*)""" + ) + + /** The start of a Scaladoc code block */ + private val CodeBlockStartRegex = + new Regex("""(.*?)((?:\{\{\{)|(?:\u000E]*)?>\u000E))(.*)""") + + /** The end of a Scaladoc code block */ + private val CodeBlockEndRegex = + new Regex("""(.*?)((?:\}\}\})|(?:\u000E\u000E))(.*)""") + + /** A key used for a tag map. The key is built from the name of the tag and + * from the linked symbol if the tag has one. + * Equality on tag keys is structural. */ + private sealed abstract class TagKey { + def name: String + } + + private final case class SimpleTagKey(name: String) extends TagKey + private final case class SymbolTagKey(name: String, symbol: String) + extends TagKey + + private val TrailingWhitespaceRegex = """\s+$""".r + + /** Parses a raw comment string into a `Comment` object. + * @param comment The expanded comment string (including start and end markers) to be parsed. + * @param pos The position of the comment in source. */ + def parseAtSymbol( + comment: String, + pos: Position = NoPosition +// site: Symbol = NoSymbol + ): Comment = { + + /** The cleaned raw comment as a list of lines. Cleaning removes comment + * start and end markers, line start markers and unnecessary whitespace. */ + def clean(comment: String): List[String] = { + def cleanLine(line: String): String = { + // Remove trailing whitespaces + TrailingWhitespaceRegex.replaceAllIn(line, "") match { + case CleanCommentLine(ctl) => ctl + case tl => tl + } + } + val strippedComment = comment.trim.stripPrefix("/*").stripSuffix("*/") + val safeComment = DangerousTags.replaceAllIn(strippedComment, { + htmlReplacement(_) + }) + val javadoclessComment = JavadocTags.replaceAllIn(safeComment, { + javadocReplacement(_) + }) + val markedTagComment = + SafeTags.replaceAllIn(javadoclessComment, { mtch => + java.util.regex.Matcher + .quoteReplacement(safeTagMarker + mtch.matched + safeTagMarker) + }) + markedTagComment.lines.toList map (cleanLine(_)) + } + + /** Parses a comment (in the form of a list of lines) to a `Comment` + * instance, recursively on lines. To do so, it splits the whole comment + * into main body and tag bodies, then runs the `WikiParser` on each body + * before creating the comment instance. + * + * @param docBody The body of the comment parsed until now. + * @param tags All tags parsed until now. + * @param lastTagKey The last parsed tag, or `None` if the tag section hasn't started. Lines that are not tagged + * are part of the previous tag or, if none exists, of the body. + * @param remaining The lines that must still recursively be parsed. + * @param inCodeBlock Whether the next line is part of a code block (in which no tags must be read). */ + def parse0( + docBody: StringBuilder, + tags: Map[TagKey, List[String]], + lastTagKey: Option[TagKey], + remaining: List[String], + inCodeBlock: Boolean + ): Comment = remaining match { + + case CodeBlockStartRegex(before, marker, after) :: ls if (!inCodeBlock) => + if (!before.trim.isEmpty && !after.trim.isEmpty) + parse0( + docBody, + tags, + lastTagKey, + before :: marker :: after :: ls, + inCodeBlock = false + ) + else if (!before.trim.isEmpty) + parse0( + docBody, + tags, + lastTagKey, + before :: marker :: ls, + inCodeBlock = false + ) + else if (!after.trim.isEmpty) + parse0( + docBody, + tags, + lastTagKey, + marker :: after :: ls, + inCodeBlock = true + ) + else + lastTagKey match { + case Some(key) => + val value = + ((tags get key): @unchecked) match { + case Some(b :: bs) => (b + endOfLine + marker) :: bs + case None => oops("lastTagKey set when no tag exists for key") + } + parse0( + docBody, + tags + (key -> value), + lastTagKey, + ls, + inCodeBlock = true + ) + case None => + parse0( + docBody append endOfLine append marker, + tags, + lastTagKey, + ls, + inCodeBlock = true + ) + } + + case CodeBlockEndRegex(before, marker, after) :: ls => { + if (!before.trim.isEmpty && !after.trim.isEmpty) + parse0( + docBody, + tags, + lastTagKey, + before :: marker :: after :: ls, + inCodeBlock = true + ) + if (!before.trim.isEmpty) + parse0( + docBody, + tags, + lastTagKey, + before :: marker :: ls, + inCodeBlock = true + ) + else if (!after.trim.isEmpty) + parse0( + docBody, + tags, + lastTagKey, + marker :: after :: ls, + inCodeBlock = false + ) + else + lastTagKey match { + case Some(key) => + val value = + ((tags get key): @unchecked) match { + case Some(b :: bs) => (b + endOfLine + marker) :: bs + case None => oops("lastTagKey set when no tag exists for key") + } + parse0( + docBody, + tags + (key -> value), + lastTagKey, + ls, + inCodeBlock = false + ) + case None => + parse0( + docBody append endOfLine append marker, + tags, + lastTagKey, + ls, + inCodeBlock = false + ) + } + } + + case SymbolTagRegex(name, sym, body) :: ls if (!inCodeBlock) => { + val key = SymbolTagKey(name, sym) + val value = body :: tags.getOrElse(key, Nil) + parse0(docBody, tags + (key -> value), Some(key), ls, inCodeBlock) + } + + case SimpleTagRegex(name, body) :: ls if (!inCodeBlock) => { + val key = SimpleTagKey(name) + val value = body :: tags.getOrElse(key, Nil) + parse0(docBody, tags + (key -> value), Some(key), ls, inCodeBlock) + } + + case SingleTagRegex(name) :: ls if (!inCodeBlock) => { + val key = SimpleTagKey(name) + val value = "" :: tags.getOrElse(key, Nil) + parse0(docBody, tags + (key -> value), Some(key), ls, inCodeBlock) + } + + case line :: ls if (lastTagKey.isDefined) => { + val newtags = if (!line.isEmpty || inCodeBlock) { + val key = lastTagKey.get + val value = + ((tags get key): @unchecked) match { + case Some(b :: bs) => (b + endOfLine + line) :: bs + case None => oops("lastTagKey set when no tag exists for key") + } + tags + (key -> value) + } else tags + parse0(docBody, newtags, lastTagKey, ls, inCodeBlock) + } + + case line :: ls => { + if (docBody.nonEmpty) docBody append endOfLine + docBody append line + parse0(docBody, tags, lastTagKey, ls, inCodeBlock) + } + + case Nil => { + // Take the {inheritance, content} diagram keys aside, as it doesn't need any parsing + val inheritDiagramTag = SimpleTagKey("inheritanceDiagram") + val contentDiagramTag = SimpleTagKey("contentDiagram") + + val inheritDiagramText: List[String] = + tags.get(inheritDiagramTag) match { + case Some(list) => list + case None => List.empty + } + + val contentDiagramText: List[String] = + tags.get(contentDiagramTag) match { + case Some(list) => list + case None => List.empty + } + + val stripTags = List( + inheritDiagramTag, + contentDiagramTag, + SimpleTagKey("template"), + SimpleTagKey("documentable") + ) + val tagsWithoutDiagram = + tags.filterNot(pair => stripTags.contains(pair._1)) + + val bodyTags: mutable.Map[TagKey, List[Body]] = + mutable.Map(tagsWithoutDiagram mapValues { tag => + tag map (parseWikiAtSymbol(_, pos)) + } toSeq: _*) + + def oneTag( + key: SimpleTagKey, + filterEmpty: Boolean = true + ): Option[Body] = + (bodyTags.remove(key): @unchecked) match { + case Some(r :: rs) if !(filterEmpty && r.blocks.isEmpty) => +// if (rs.nonEmpty) +// reporter.warning(pos, s"Only one '@${key.name}' tag is allowed") + Some(r) + case _ => None + } + + def allTags(key: SimpleTagKey): List[Body] = + (bodyTags remove key) + .getOrElse(Nil) + .filterNot(_.blocks.isEmpty) + .reverse + + def allSymsOneTag( + key: TagKey, + filterEmpty: Boolean = true + ): Map[String, Body] = { + val keys: Seq[SymbolTagKey] = + bodyTags.keys.toSeq flatMap { + case stk: SymbolTagKey if (stk.name == key.name) => Some(stk) + case stk: SimpleTagKey if (stk.name == key.name) => +// reporter.warning( +// pos, +// s"Tag '@${stk.name}' must be followed by a symbol name" +// ) + None + case _ => None + } + val pairs: Seq[(String, Body)] = + for (key <- keys) yield { + val bs = (bodyTags remove key).get +// if (bs.length > 1) +// reporter.warning( +// pos, +// s"Only one '@${key.name}' tag for symbol ${key.symbol} is allowed" +// ) + (key.symbol, bs.head) + } + Map.empty[String, Body] ++ (if (filterEmpty) + pairs.filterNot(_._2.blocks.isEmpty) + else pairs) + } + + def linkedExceptions: Map[String, Body] = { + val m = allSymsOneTag(SimpleTagKey("throws"), filterEmpty = false) + + m.map { + case (name, body) => + val newBody = body match { + case Body(List(Paragraph(Chain(content)))) => +// val link = memberLookup(pos, name, site) +// val descr = Text(" ") +: content +// val entityLink = EntityLink(Monospace(Text(name)), link) +// Body(List(Paragraph(Chain(entityLink +: descr)))) + Body(List()) + case _ => body + } + (name, newBody) + } + } + + val com = createComment( + body0 = Some(parseWikiAtSymbol(docBody.toString, pos)), + authors0 = allTags(SimpleTagKey("author")), + see0 = allTags(SimpleTagKey("see")), + result0 = oneTag(SimpleTagKey("return")), + throws0 = linkedExceptions, + valueParams0 = allSymsOneTag(SimpleTagKey("param")), + typeParams0 = allSymsOneTag(SimpleTagKey("tparam")), + version0 = oneTag(SimpleTagKey("version")), + since0 = oneTag(SimpleTagKey("since")), + todo0 = allTags(SimpleTagKey("todo")), + deprecated0 = oneTag(SimpleTagKey("deprecated"), filterEmpty = false), + note0 = allTags(SimpleTagKey("note")), + example0 = allTags(SimpleTagKey("example")), + constructor0 = oneTag(SimpleTagKey("constructor")), + inheritDiagram0 = inheritDiagramText, + contentDiagram0 = contentDiagramText, + group0 = oneTag(SimpleTagKey("group")), + groupDesc0 = allSymsOneTag(SimpleTagKey("groupdesc")), + groupNames0 = allSymsOneTag(SimpleTagKey("groupname")), + groupPrio0 = allSymsOneTag(SimpleTagKey("groupprio")), + hideImplicitConversions0 = + allTags(SimpleTagKey("hideImplicitConversion")), + shortDescription0 = allTags(SimpleTagKey("shortDescription")) + ) + +// for ((key, _) <- bodyTags) +// reporter.warning(pos, s"Tag '@${key.name}' is not recognised") + + com + } + } + + parse0( + new StringBuilder(comment.size), + Map.empty, + None, + clean(comment), + inCodeBlock = false + ) + + } + + /** Parses a string containing wiki syntax into a `Comment` object. + * Note that the string is assumed to be clean: + * - Removed Scaladoc start and end markers. + * - Removed start-of-line star and one whitespace afterwards (if present). + * - Removed all end-of-line whitespace. + * - Only `endOfLine` is used to mark line endings. */ + def parseWikiAtSymbol( + string: String, + pos: Position +// site: Symbol + ): Body = + new WikiParser( + string, + pos +// site + ).document() + + /** TODO + * + * @author Ingo Maier + * @author Manohar Jonnalagedda + * @author Gilles Dubochet */ + final class WikiParser( + val buffer: String, + pos: Position +// site: Symbol + ) extends CharReader(buffer) { wiki => + var summaryParsed = false + + // TODO: Convert to Char + private val TableCellStart = "|" + + def document(): Body = { + val blocks = new mutable.ListBuffer[Block] + while (char != endOfText) blocks += block() + Body(blocks.toList) + } + + /* BLOCKS */ + + /** {{{ block ::= code | title | hrule | listBlock | table | para }}} */ + def block(): Block = { + if (checkSkipInitWhitespace("{{{")) + code() + else if (checkSkipInitWhitespace('=')) + title() + else if (checkSkipInitWhitespace("----")) + hrule() + else if (checkList) + listBlock + else if (checkTableRow) + table() + else { + para() + } + } + + /** listStyle ::= '-' spc | '1.' spc | 'I.' spc | 'i.' spc | 'A.' spc | 'a.' spc + * Characters used to build lists and their constructors */ + val listStyles = + Map[String, (Seq[Block] => Block)]( // TODO Should this be defined at some list companion? + "- " -> (UnorderedList(_)), + "1. " -> (OrderedList(_, "decimal")), + "I. " -> (OrderedList(_, "upperRoman")), + "i. " -> (OrderedList(_, "lowerRoman")), + "A. " -> (OrderedList(_, "upperAlpha")), + "a. " -> (OrderedList(_, "lowerAlpha")) + ) + + /** Checks if the current line is formed with more than one space and one the listStyles */ + def checkList = + (countWhitespace > 0) && (listStyles.keys exists { + checkSkipInitWhitespace(_) + }) + + /** {{{ + * nListBlock ::= nLine { mListBlock } + * nLine ::= nSpc listStyle para '\n' + * }}} + * Where n and m stand for the number of spaces. When `m > n`, a new list is nested. */ + def listBlock(): Block = { + + /** Consumes one list item block and returns it, or None if the block is + * not a list or a different list. */ + def listLine(indent: Int, style: String): Option[Block] = + if (countWhitespace > indent && checkList) + Some(listBlock) + else if (countWhitespace != indent || !checkSkipInitWhitespace(style)) + None + else { + jumpWhitespace() + jump(style) + val p = Paragraph(inline(isInlineEnd = false)) + blockEnded("end of list line") + Some(p) + } + + /** Consumes all list item blocks (possibly with nested lists) of the + * same list and returns the list block. */ + def listLevel(indent: Int, style: String): Block = { + val lines = mutable.ListBuffer.empty[Block] + var line: Option[Block] = listLine(indent, style) + while (line.isDefined) { + lines += line.get + line = listLine(indent, style) + } + val constructor = listStyles(style) + constructor(lines) + } + + val indent = countWhitespace + val style = (listStyles.keys find { checkSkipInitWhitespace(_) }) + .getOrElse(listStyles.keys.head) + listLevel(indent, style) + } + + def code(): Block = { + jumpWhitespace() + jump("{{{") + val str = readUntil("}}}") + if (char == endOfText) + reportError(pos, "unclosed code block") + else + jump("}}}") + blockEnded("code block") + Code(normalizeIndentation(str)) + } + + /** {{{ title ::= ('=' inline '=' | "==" inline "==" | ...) '\n' }}} */ + def title(): Block = { + jumpWhitespace() + val inLevel = repeatJump('=') + val text = inline(check("=" * inLevel)) + val outLevel = repeatJump('=', inLevel) + if (inLevel != outLevel) + reportError(pos, "unbalanced or unclosed heading") + blockEnded("heading") + Title(text, inLevel) + } + + /** {{{ hrule ::= "----" { '-' } '\n' }}} */ + def hrule(): Block = { + jumpWhitespace() + repeatJump('-') + blockEnded("horizontal rule") + HorizontalRule() + } + + /** Starts and end with a cell separator matching the minimal row || and all other possible rows */ + private val TableRow = """^\|.*\|$""".r + + /* Checks for a well-formed table row */ + private def checkTableRow = { + check(TableCellStart) && { + val newlineIdx = buffer.indexOf('\n', offset) + newlineIdx != -1 && + TableRow.findFirstIn(buffer.substring(offset, newlineIdx)).isDefined + } + } + + /** {{{ + * table ::= headerRow '\n' delimiterRow '\n' dataRows '\n' + * content ::= inline-content + * row ::= '|' { content '|' }+ + * headerRow ::= row + * dataRows ::= row* + * align ::= ':' '-'+ | '-'+ | '-'+ ':' | ':' '-'+ ':' + * delimiterRow :: = '|' { align '|' }+ + * }}} + */ + def table(): Block = { + + /* Helpers */ + + def peek(tag: String): Unit = { + val peek: String = buffer.substring(offset) + val limit = 60 + val limitedPeek = peek.substring(0, limit min peek.length) + println(s"peek: $tag: '$limitedPeek'") + } + + /* Accumulated state */ + + var header: Option[Row] = None + + val rows = mutable.ListBuffer.empty[Row] + + val cells = mutable.ListBuffer.empty[Cell] + + def finalizeCells(): Unit = { + if (cells.nonEmpty) { + rows += Row(cells.toList) + } + cells.clear() + } + + def finalizeHeaderCells(): Unit = { + if (cells.nonEmpty) { + if (header.isDefined) { + reportError(pos, "more than one table header") + } else { + header = Some(Row(cells.toList)) + } + } + cells.clear() + } + + val escapeChar = "\\" + + /* Poor man's negative lookbehind */ + def checkInlineEnd = + (check(TableCellStart) && !check(escapeChar, -1)) || check("\n") + + def decodeEscapedCellMark(text: String) = + text.replace(escapeChar + TableCellStart, TableCellStart) + + def isEndOfText = char == endOfText + + def isNewline = char == endOfLine + + def skipNewline() = jump(endOfLine) + + def isStartMarkNewline = check(TableCellStart + endOfLine) + + def skipStartMarkNewline() = jump(TableCellStart + endOfLine) + + def isStartMark = check(TableCellStart) + + def skipStartMark() = jump(TableCellStart) + + def contentNonEmpty(content: Inline) = content != Text("") + + /** + * @param cellStartMark The char indicating the start or end of a cell + * @param finalizeRow Function to invoke when the row has been fully parsed + */ + def parseCells(cellStartMark: String, finalizeRow: () => Unit): Unit = { + def jumpCellStartMark() = { + if (!jump(cellStartMark)) { + peek(s"Expected $cellStartMark") + sys.error(s"Precondition violated: Expected $cellStartMark.") + } + } + + val startPos = offset + + jumpCellStartMark() + + val content = Paragraph( + inline( + isInlineEnd = checkInlineEnd, + textTransform = decodeEscapedCellMark + ) + ) + + parseCells0(content :: Nil, finalizeRow, startPos, offset) + } + + // Continue parsing a table row. + // + // After reading inline content the following conditions will be encountered, + // + // Case : Next Chars + // .................. + // 1 : end-of-text + // 2 : '|' '\n' + // 3 : '|' + // 4 : '\n' + // + // Case 1. + // State : End of text + // Action: Store the current contents, close the row, report warning, stop parsing. + // + // Case 2. + // State : The cell separator followed by a newline + // Action: Store the current contents, skip the cell separator and newline, close the row, stop parsing. + // + // Case 3. + // State : The cell separator not followed by a newline + // Action: Store the current contents, skip the cell separator, continue parsing the row. + // + @tailrec def parseCells0( + contents: List[Block], + finalizeRow: () => Unit, + progressPreParse: Int, + progressPostParse: Int + ): Unit = { + + def storeContents() = cells += Cell(contents.reverse) + + val startPos = offset + + // The ordering of the checks ensures the state checks are correct. + if (progressPreParse == progressPostParse) { + peek("no-progress-table-row-parsing") + sys.error("No progress while parsing table row") + } else if (isEndOfText) { + // peek("1: end-of-text") + // Case 1 + storeContents() + finalizeRow() + reportError(pos, "unclosed table row") + } else if (isStartMarkNewline) { + // peek("2: start-mark-new-line/before") + // Case 2 + storeContents() + finalizeRow() + skipStartMarkNewline() + // peek("2: start-mark-new-line/after") + } else if (isStartMark) { + // peek("3: start-mark") + // Case 3 + storeContents() + skipStartMark() + val content = inline( + isInlineEnd = checkInlineEnd, + textTransform = decodeEscapedCellMark + ) + // TrailingCellsEmpty produces empty content + val accContents = + if (contentNonEmpty(content)) Paragraph(content) :: Nil else Nil + parseCells0(accContents, finalizeRow, startPos, offset) + } else { + // Case π√ⅈ + // When the impossible happens leave some clues. + reportError(pos, "unexpected table row markdown") + peek("parseCell0") + storeContents() + finalizeRow() + } + } + + /* Parsing */ + + jumpWhitespace() + + parseCells(TableCellStart, () => finalizeHeaderCells()) + + while (checkTableRow) { + val initialOffset = offset + + parseCells(TableCellStart, () => finalizeCells()) + + /* Progress should always be made */ + if (offset == initialOffset) { + peek("no-progress-table-parsing") + sys.error("No progress while parsing table") + } + } + + /* Finalize */ + + /* Structural consistency checks and coercion */ + + // https://github.github.com/gfm/#tables-extension- + // TODO: The header row must match the delimiter row in the number of cells. If not, a table will not be recognized: + // TODO: Break at following block level element: The table is broken at the first empty line, or beginning of another block-level structure: + // TODO: Do not return a table when: The header row must match the delimiter row in the number of cells. If not, a table will not be recognized + + if (cells.nonEmpty) { + reportError(pos, s"Parsed and unused content: $cells") + } + assert(header.isDefined, "table header was not parsed") + val enforcedCellCount = header.get.cells.size + + def applyColumnCountConstraint( + row: Row, + defaultCell: Cell, + rowType: String + ): Row = { + if (row.cells.size == enforcedCellCount) + row + else if (row.cells.size > enforcedCellCount) { + val excess = row.cells.size - enforcedCellCount + reportError( + pos, + s"Dropping $excess excess table $rowType cells from row." + ) + Row(row.cells.take(enforcedCellCount)) + } else { + val missing = enforcedCellCount - row.cells.size + Row(row.cells ++ List.fill(missing)(defaultCell)) + } + } + + // TODO: Abandon table parsing when the delimiter is missing instead of fixing and continuing. + val delimiterRow :: dataRows = + if (rows.nonEmpty) + rows.toList + else { + reportError(pos, "Fixing missing delimiter row") + Row(Cell(Paragraph(Text("-")) :: Nil) :: Nil) :: Nil + } + + if (delimiterRow.cells.isEmpty) + sys.error("TODO: Handle table with empty delimiter row") + + val constrainedDelimiterRow = applyColumnCountConstraint( + delimiterRow, + delimiterRow.cells(0), + "delimiter" + ) + + val constrainedDataRows = + dataRows.map(applyColumnCountConstraint(_, Cell(Nil), "data")) + + /* Convert the row following the header row to column options */ + + val leftAlignmentPattern = "^:?-++$".r + val centerAlignmentPattern = "^:-++:$".r + val rightAlignmentPattern = "^-++:$".r + + import ColumnOption._ + /* Encourage user to fix by defaulting to least ignorable fix. */ + val defaultColumnOption = ColumnOptionRight + val columnOptions = constrainedDelimiterRow.cells.map { + alignmentSpecifier => + alignmentSpecifier.blocks match { + // TODO: Parse the second row without parsing inline markdown + // TODO: Save pos when delimiter row is parsed and use here in reported errors + case Paragraph(Text(as)) :: Nil => + as.trim match { + case leftAlignmentPattern(_*) => ColumnOptionLeft + case centerAlignmentPattern(_*) => ColumnOptionCenter + case rightAlignmentPattern(_*) => ColumnOptionRight + case x => + reportError(pos, s"Fixing invalid column alignment: $x") + defaultColumnOption + } + case x => + reportError(pos, s"Fixing invalid column alignment: $x") + defaultColumnOption + } + } + + if (check("\n", -1)) { + prevChar() + } else { + peek("expected-newline-missing") + sys.error("table parsing left buffer in unexpected state") + } + + blockEnded("table") + Table(header.get, columnOptions, constrainedDataRows) + } + + /** {{{ para ::= inline '\n' }}} */ + def para(): Block = { + val p = + if (summaryParsed) + Paragraph(inline(isInlineEnd = false)) + else { + val s = summary() + val r = + if (checkParaEnded()) List(s) + else List(s, inline(isInlineEnd = false)) + summaryParsed = true + Paragraph(Chain(r)) + } + while (char == endOfLine && char != endOfText) nextChar() + p + } + + /* INLINES */ + + val OPEN_TAG = "^<([A-Za-z]+)( [^>]*)?(/?)>$".r + val CLOSE_TAG = "^$".r + private def readHTMLFrom(begin: HtmlTag): String = { + val list = mutable.ListBuffer.empty[String] + val stack = mutable.ListBuffer.empty[String] + + begin.close match { + case Some(HtmlTag(CLOSE_TAG(s))) => + stack += s + case _ => + return "" + } + + do { + val str = readUntil { char == safeTagMarker || char == endOfText } + nextChar() + + list += str + + str match { + case OPEN_TAG(s, _, standalone) => { + if (standalone != "/") { + stack += s + } + } + case CLOSE_TAG(s) => { + if (s == stack.last) { + stack.remove(stack.length - 1) + } + } + case _ => ; + } + } while (stack.nonEmpty && char != endOfText) + + list mkString "" + } + + def inline( + isInlineEnd: => Boolean, + textTransform: String => String = identity + ): Inline = { + + def inline0(): Inline = { + if (char == safeTagMarker) { + val tag = htmlTag() + HtmlTag(tag.data + readHTMLFrom(tag)) + } else if (check("'''")) bold() + else if (check("''")) italic() + else if (check("`")) monospace() + else if (check("__")) underline() + else if (check("^")) superscript() + else if (check(",,")) subscript() + else if (check("[[")) link() + else { + val str = readUntil { + char == safeTagMarker || check("''") || char == '`' || check("__") || char == '^' || check( + ",," + ) || check("[[") || isInlineEnd || checkParaEnded || char == endOfLine + } + Text(textTransform(str)) + } + } + + val inlines: List[Inline] = { + val iss = mutable.ListBuffer.empty[Inline] + iss += inline0() + while (!isInlineEnd && !checkParaEnded) { + val skipEndOfLine = if (char == endOfLine) { + nextChar() + true + } else { + false + } + + val current = inline0() + (iss.last, current) match { + case (Text(t1), Text(t2)) if skipEndOfLine => + iss.update(iss.length - 1, Text(t1 + endOfLine + t2)) + case (i1, i2) if skipEndOfLine => + iss ++= List(Text(endOfLine.toString), i2) + case _ => iss += current + } + } + iss.toList + } + + inlines match { + case Nil => Text("") + case i :: Nil => i + case is => Chain(is) + } + + } + + def htmlTag(): HtmlTag = { + jump(safeTagMarker) + val read = readUntil(safeTagMarker) + if (char != endOfText) jump(safeTagMarker) + HtmlTag(read) + } + + def bold(): Inline = { + jump("'''") + val i = inline(check("'''")) + jump("'''") + Bold(i) + } + + def italic(): Inline = { + jump("''") + val i = inline(check("''")) + jump("''") + Italic(i) + } + + def monospace(): Inline = { + jump("`") + val i = inline(check("`")) + jump("`") + Monospace(i) + } + + def underline(): Inline = { + jump("__") + val i = inline(check("__")) + jump("__") + Underline(i) + } + + def superscript(): Inline = { + jump("^") + val i = inline(check("^")) + if (jump("^")) { + Superscript(i) + } else { + Chain(Seq(Text("^"), i)) + } + } + + def subscript(): Inline = { + jump(",,") + val i = inline(check(",,")) + jump(",,") + Subscript(i) + } + + def summary(): Inline = { + val i = inline(checkSentenceEnded()) + Summary( + if (jump(".")) + Chain(List(i, Text("."))) + else + i + ) + } + + def link(): Inline = { + val SchemeUri = """([a-z]+:.*)""".r + jump("[[") + val parens = 2 + repeatJump('[') + val stop = "]" * parens + val target = readUntil { check(stop) || isWhitespaceOrNewLine(char) } + val title = + if (!check(stop)) Some({ + jumpWhitespaceOrNewLine() + inline(check(stop)) + }) + else None + jump(stop) + + (target, title) match { + case (SchemeUri(uri), optTitle) => + Link(uri, optTitle getOrElse Text(uri)) + case (qualName, optTitle) => + Text(qualName) +// makeEntityLink(optTitle getOrElse Text(target), pos, target, site) + } + } + + /* UTILITY */ + + /** {{{ eol ::= { whitespace } '\n' }}} */ + def blockEnded(blockType: String): Unit = { + if (char != endOfLine && char != endOfText) { + reportError( + pos, + "no additional content on same line after " + blockType + ) + jumpUntil(endOfLine) + } + while (char == endOfLine) nextChar() + } + + /** + * Eliminates the (common) leading spaces in all lines, based on the first line + * For indented pieces of code, it reduces the indent to the least whitespace prefix: + * {{{ + * indented example + * another indented line + * if (condition) + * then do something; + * ^ this is the least whitespace prefix + * }}} + */ + def normalizeIndentation(_code: String): String = { + + val code = _code.replaceAll("\\s+$", "").dropWhile(_ == '\n') // right-trim + remove all leading '\n' + val lines = code.split("\n") + + // maxSkip - size of the longest common whitespace prefix of non-empty lines + val nonEmptyLines = lines.filter(_.trim.nonEmpty) + val maxSkip = + if (nonEmptyLines.isEmpty) 0 + else nonEmptyLines.map(line => line.prefixLength(_ == ' ')).min + + // remove common whitespace prefix + lines + .map(line => if (line.trim.nonEmpty) line.substring(maxSkip) else line) + .mkString("\n") + } + + def checkParaEnded(): Boolean = { + (char == endOfText) || + ((char == endOfLine) && { + val poff = offset + nextChar() // read EOL + val ok = { + checkSkipInitWhitespace(endOfLine) || + checkSkipInitWhitespace('=') || + checkSkipInitWhitespace("{{{") || + checkList || + check(TableCellStart) || + checkSkipInitWhitespace('\u003D') + } + offset = poff + ok + }) + } + + def checkSentenceEnded(): Boolean = { + (char == '.') && { + val poff = offset + nextChar() // read '.' + val ok = char == endOfText || char == endOfLine || isWhitespace(char) + offset = poff + ok + } + } + + def reportError(pos: Position, message: String) { + pprint.log(message) +// reporter.warning(pos, message) + } + } + + sealed class CharReader(buffer: String) { reader => + + var offset: Int = 0 + def char: Char = + if (offset >= buffer.length) endOfText else buffer charAt offset + + final def nextChar() { + offset += 1 + } + + final def prevChar() { + offset -= 1 + } + + final def check(chars: String): Boolean = { + val poff = offset + val ok = jump(chars) + offset = poff + ok + } + + final def check(chars: String, checkOffset: Int): Boolean = { + val poff = offset + offset += checkOffset + val ok = jump(chars) + offset = poff + ok + } + + def checkSkipInitWhitespace(c: Char): Boolean = { + val poff = offset + jumpWhitespace() + val ok = jump(c) + offset = poff + ok + } + + def checkSkipInitWhitespace(chars: String): Boolean = { + val poff = offset + jumpWhitespace() + val (ok0, chars0) = + if (chars.charAt(0) == ' ') + (offset > poff, chars substring 1) + else + (true, chars) + val ok = ok0 && jump(chars0) + offset = poff + ok + } + + def countWhitespace: Int = { + var count = 0 + val poff = offset + while (isWhitespace(char) && char != endOfText) { + nextChar() + count += 1 + } + offset = poff + count + } + + /* JUMPERS */ + + /** jumps a character and consumes it + * @return true only if the correct character has been jumped */ + final def jump(ch: Char): Boolean = { + if (char == ch) { + nextChar() + true + } else false + } + + /** jumps all the characters in chars, consuming them in the process. + * @return true only if the correct characters have been jumped */ + final def jump(chars: String): Boolean = { + var index = 0 + while (index < chars.length && char == chars.charAt(index) && char != endOfText) { + nextChar() + index += 1 + } + index == chars.length + } + + final def repeatJump(c: Char, max: Int = Int.MaxValue): Int = { + var count = 0 + while (jump(c) && count < max) count += 1 + count + } + + final def jumpUntil(ch: Char): Int = { + var count = 0 + while (char != ch && char != endOfText) { + nextChar() + count += 1 + } + count + } + + final def jumpUntil(pred: => Boolean): Int = { + var count = 0 + while (!pred && char != endOfText) { + nextChar() + count += 1 + } + count + } + + def jumpWhitespace() = jumpUntil(!isWhitespace(char)) + + def jumpWhitespaceOrNewLine() = jumpUntil(!isWhitespaceOrNewLine(char)) + + /* READERS */ + + final def readUntil(c: Char): String = { + withRead { + while (char != c && char != endOfText) { + nextChar() + } + } + } + + final def readUntil(chars: String): String = { + assert(chars.length > 0) + withRead { + val c = chars.charAt(0) + while (!check(chars) && char != endOfText) { + nextChar() + while (char != c && char != endOfText) nextChar() + } + } + } + + final def readUntil(pred: => Boolean): String = { + withRead { + while (char != endOfText && !pred) { + nextChar() + } + } + } + + private def withRead(read: => Unit): String = { + val start = offset + read + buffer.substring(start, offset) + } + + /* CHARS CLASSES */ + + def isWhitespace(c: Char) = c == ' ' || c == '\t' + + def isWhitespaceOrNewLine(c: Char) = isWhitespace(c) || c == '\n' + } +} diff --git a/metals/src/main/scala/scala/meta/internal/metals/BloomFilters.scala b/pc/core/src/main/scala/scala/meta/internal/metals/BloomFilters.scala similarity index 100% rename from metals/src/main/scala/scala/meta/internal/metals/BloomFilters.scala rename to pc/core/src/main/scala/scala/meta/internal/metals/BloomFilters.scala diff --git a/pc/core/src/main/scala/scala/meta/internal/metals/CachedSymbolInformation.scala b/pc/core/src/main/scala/scala/meta/internal/metals/CachedSymbolInformation.scala new file mode 100644 index 00000000000..09b4a22152f --- /dev/null +++ b/pc/core/src/main/scala/scala/meta/internal/metals/CachedSymbolInformation.scala @@ -0,0 +1,20 @@ +package scala.meta.internal.metals + +import org.eclipse.{lsp4j => l} + +case class CachedSymbolInformation( + symbol: String, + kind: l.SymbolKind, + range: l.Range +) { + def toLSP(uri: String): l.SymbolInformation = { + import scala.meta.internal.semanticdb.Scala._ + val (desc, owner) = DescriptorParser(symbol) + new l.SymbolInformation( + desc.name.value, + kind, + new l.Location(uri, range), + owner.replace('/', '.') + ) + } +} diff --git a/pc/core/src/main/scala/scala/meta/internal/metals/Classfile.scala b/pc/core/src/main/scala/scala/meta/internal/metals/Classfile.scala new file mode 100644 index 00000000000..4600315af32 --- /dev/null +++ b/pc/core/src/main/scala/scala/meta/internal/metals/Classfile.scala @@ -0,0 +1,15 @@ +package scala.meta.internal.metals + +case class Classfile(pkg: String, filename: String) { + def isExact(query: WorkspaceSymbolQuery): Boolean = + name == query.query + def name: String = Classfile.name(filename) +} + +object Classfile { + def name(filename: String): String = { + val dollar = filename.indexOf('$') + if (dollar < 0) filename.stripSuffix(".class") + else filename.substring(0, dollar) + } +} diff --git a/pc/core/src/main/scala/scala/meta/internal/metals/ClassfileComparator.scala b/pc/core/src/main/scala/scala/meta/internal/metals/ClassfileComparator.scala new file mode 100644 index 00000000000..30279b35440 --- /dev/null +++ b/pc/core/src/main/scala/scala/meta/internal/metals/ClassfileComparator.scala @@ -0,0 +1,46 @@ +package scala.meta.internal.metals + +class ClassfileComparator(query: String) + extends java.util.Comparator[Classfile] { + def characterCount(string: String, ch: Char): Int = { + var i = 0 + var count = 0 + while (i < string.length) { + if (string.charAt(i) == ch) { + count += 1 + } + i += 1 + } + count + } + override def compare(o1: Classfile, o2: Classfile): Int = { + val byNameLength = Integer.compare( + Fuzzy.nameLength(o1.filename), + Fuzzy.nameLength(o2.filename) + ) + if (byNameLength != 0) byNameLength + else { + val byInnerclassDepth = Integer.compare( + characterCount(o1.filename, '$'), + characterCount(o2.filename, '$') + ) + if (byInnerclassDepth != 0) byInnerclassDepth + else { + val byFirstQueryCharacter = Integer.compare( + o1.filename.indexOf(query.head), + o2.filename.indexOf(query.head) + ) + if (byFirstQueryCharacter != 0) { + byFirstQueryCharacter + } else { + val byPackageDepth = Integer.compare( + characterCount(o1.pkg, '/'), + characterCount(o2.pkg, '/') + ) + if (byPackageDepth != 0) byPackageDepth + else o1.filename.compareTo(o2.filename) + } + } + } + } +} diff --git a/pc/core/src/main/scala/scala/meta/internal/metals/ClasspathSearch.scala b/pc/core/src/main/scala/scala/meta/internal/metals/ClasspathSearch.scala new file mode 100644 index 00000000000..d747dc91074 --- /dev/null +++ b/pc/core/src/main/scala/scala/meta/internal/metals/ClasspathSearch.scala @@ -0,0 +1,125 @@ +package scala.meta.internal.metals + +import java.nio.file.Path +import java.util +import java.util.Comparator +import java.util.PriorityQueue +import scala.collection.concurrent.TrieMap +import scala.meta.io.AbsolutePath +import scala.meta.pc.SymbolSearch +import scala.meta.pc.SymbolSearchVisitor +import scala.meta.internal.mtags.MtagsEnrichments._ + +class ClasspathSearch( + map: collection.Map[String, CompressedPackageIndex], + packagePriority: String => Int +) extends SymbolSearch { + // The maximum number of non-exact matches that we return for classpath queries. + // Generic queries like "Str" can returns several thousand results, so we need + // to limit it at some arbitrary point. Exact matches are always included. + private val maxNonExactMatches = 10 + private val byReferenceThenAlphabeticalComparator = new Comparator[String] { + override def compare(a: String, b: String): Int = { + val byReference = -Integer.compare(packagePriority(a), packagePriority(b)) + if (byReference != 0) byReference + else a.compare(b) + } + } + + override def search( + query: String, + buildTargetIdentifier: String, + visitor: SymbolSearchVisitor + ): SymbolSearch.Result = { + search(WorkspaceSymbolQuery.exact(query), visitor) + } + + def search( + query: WorkspaceSymbolQuery, + visitor: SymbolSearchVisitor + ): SymbolSearch.Result = { + val classfiles = + new PriorityQueue[Classfile](new ClassfileComparator(query.query)) + for { + classfile <- search( + query, + pkg => visitor.shouldVisitPackage(pkg), + () => visitor.isCancelled + ) + } { + classfiles.add(classfile) + } + var nonExactMatches = 0 + var searchResult = SymbolSearch.Result.COMPLETE + for { + hit <- classfiles.pollingIterator + if { + val isContinue = !visitor.isCancelled && + (nonExactMatches < maxNonExactMatches || hit.isExact(query)) + if (!isContinue) { + searchResult = SymbolSearch.Result.INCOMPLETE + } + isContinue + } + } { + val added = visitor.visitClassfile(hit.pkg, hit.filename) + if (added > 0 && !hit.isExact(query)) { + nonExactMatches += added + } + } + searchResult + } + + private def packagesSortedByReferences(): Array[String] = { + val packages = map.keys.toArray + util.Arrays.sort(packages, byReferenceThenAlphabeticalComparator) + packages + } + + private def search( + query: WorkspaceSymbolQuery, + visitPackage: String => Boolean, + isCancelled: () => Boolean + ): Iterator[Classfile] = { + val packages = packagesSortedByReferences() + for { + pkg <- packages.iterator + if visitPackage(pkg) + if !isCancelled() + compressed = map(pkg) + if query.matches(compressed.bloom) + member <- compressed.members + if member.endsWith(".class") + symbol = new ConcatSequence(pkg, member) + isMatch = query.matches(symbol) + if isMatch + } yield { + Classfile(pkg, member) + } + } + +} + +object ClasspathSearch { + def empty: ClasspathSearch = + new ClasspathSearch(Map.empty, _ => 0) + def fromPackages( + packages: PackageIndex, + packagePriority: String => Int + ): ClasspathSearch = { + val map = TrieMap.empty[String, CompressedPackageIndex] + map ++= CompressedPackageIndex.fromPackages(packages) + new ClasspathSearch(map, packagePriority) + } + def fromClasspath( + classpath: Seq[Path], + packagePriority: String => Int + ): ClasspathSearch = { + val packages = new PackageIndex() + packages.visitBootClasspath() + classpath.foreach { path => + packages.visit(AbsolutePath(path)) + } + fromPackages(packages, packagePriority) + } +} diff --git a/pc/core/src/main/scala/scala/meta/internal/metals/CompilerOffsetParams.scala b/pc/core/src/main/scala/scala/meta/internal/metals/CompilerOffsetParams.scala new file mode 100644 index 00000000000..bc50b045fdc --- /dev/null +++ b/pc/core/src/main/scala/scala/meta/internal/metals/CompilerOffsetParams.scala @@ -0,0 +1,20 @@ +package scala.meta.internal.metals + +import java.util.concurrent.CancellationException +import org.eclipse.lsp4j.jsonrpc.CancelChecker +import scala.meta.pc.OffsetParams + +case class CompilerOffsetParams( + filename: String, + text: String, + offset: Int, + token: CancelChecker = EmptyCancelChecker +) extends OffsetParams { + override def checkCanceled(): Unit = { + try token.checkCanceled() + catch { + case e: CancellationException => + throw new ControlCancellationException(e) + } + } +} diff --git a/pc/core/src/main/scala/scala/meta/internal/metals/CompressedPackageIndex.scala b/pc/core/src/main/scala/scala/meta/internal/metals/CompressedPackageIndex.scala new file mode 100644 index 00000000000..de206cdb4c3 --- /dev/null +++ b/pc/core/src/main/scala/scala/meta/internal/metals/CompressedPackageIndex.scala @@ -0,0 +1,60 @@ +package scala.meta.internal.metals + +import com.google.common.hash.BloomFilter +import scala.collection.JavaConverters._ + +/** + * The memory-compressed version of PackageIndex. + * + * @param bloom the fuzzy search bloom filter for all members of this package. + * @param memberBytes the GZIP compressed bytes representing Array[String] for + * all members of this package. The members are compressed because the strings + * consume a lot of memory and most queries only require looking at a few packages. + * We decompress the members only when a search query matches the bloom filter + * for this package. + */ +case class CompressedPackageIndex( + bloom: BloomFilter[CharSequence], + memberBytes: Array[Byte] +) { + def members: Array[String] = Compression.decompress(memberBytes) +} + +object CompressedPackageIndex { + private def isExcludedPackage(pkg: String): Boolean = { + // NOTE(olafur) At some point we may consider making this list configurable, I can + // imagine that some people wouldn't mind excluding more packages or including for + // example javax._. + pkg.startsWith("jdk/") || + pkg.startsWith("sun/") || + pkg.startsWith("javax/") || + pkg.startsWith("oracle/") || + pkg.startsWith("org/omg/") || + pkg.startsWith("com/oracle/") || + pkg.startsWith("com/sun/") || + pkg.startsWith("com/apple/") + } + def fromPackages( + packages: PackageIndex + ): Iterator[(String, CompressedPackageIndex)] = { + for { + (pkg, members) <- packages.packages.asScala.iterator + if !isExcludedPackage(pkg) + } yield { + val buf = Fuzzy.bloomFilterSymbolStrings(members.asScala) + buf ++= Fuzzy.bloomFilterSymbolStrings(List(pkg), buf) + val bloom = BloomFilters.create(buf.size) + buf.foreach { key => + bloom.put(key) + } + // Sort members for deterministic order for deterministic results. + members.sort(String.CASE_INSENSITIVE_ORDER) + // Compress members because they make up the bulk of memory usage in the classpath index. + // For a 140mb classpath with spark/linkerd/akka/.. the members take up 12mb uncompressed + // and ~900kb compressed. We are accummulating a lot of different custom indexes in Metals + // so we should try to keep each of them as small as possible. + val compressedMembers = Compression.compress(members.asScala.iterator) + (pkg, CompressedPackageIndex(bloom, compressedMembers)) + } + } +} diff --git a/metals/src/main/scala/scala/meta/internal/metals/Compression.scala b/pc/core/src/main/scala/scala/meta/internal/metals/Compression.scala similarity index 96% rename from metals/src/main/scala/scala/meta/internal/metals/Compression.scala rename to pc/core/src/main/scala/scala/meta/internal/metals/Compression.scala index d0f0bfbe566..4c39024dad0 100644 --- a/metals/src/main/scala/scala/meta/internal/metals/Compression.scala +++ b/pc/core/src/main/scala/scala/meta/internal/metals/Compression.scala @@ -12,7 +12,7 @@ object Compression { /** * Returns a GZIP deflated sequence of strings. */ - def compress(strings: Seq[String]): Array[Byte] = { + def compress(strings: Iterator[String]): Array[Byte] = { val baos = new ByteArrayOutputStream() val out = CodedOutputStream.newInstance(baos) strings.foreach { member => diff --git a/metals/src/main/scala/scala/meta/internal/metals/ConcatSequence.scala b/pc/core/src/main/scala/scala/meta/internal/metals/ConcatSequence.scala similarity index 100% rename from metals/src/main/scala/scala/meta/internal/metals/ConcatSequence.scala rename to pc/core/src/main/scala/scala/meta/internal/metals/ConcatSequence.scala diff --git a/pc/core/src/main/scala/scala/meta/internal/metals/ControlCancellationException.scala b/pc/core/src/main/scala/scala/meta/internal/metals/ControlCancellationException.scala new file mode 100644 index 00000000000..79b2b75bbc3 --- /dev/null +++ b/pc/core/src/main/scala/scala/meta/internal/metals/ControlCancellationException.scala @@ -0,0 +1,13 @@ +package scala.meta.internal.metals + +import java.util.concurrent.CancellationException +import scala.util.control.ControlThrowable + +/** + * Wrapper around `CancellationException` with mixed in `ControlThrowable` to play nicely with NonFatal. + */ +class ControlCancellationException(cause: CancellationException) + extends CancellationException(cause.getMessage) + with ControlThrowable { + override def getCause: Throwable = cause +} diff --git a/pc/core/src/main/scala/scala/meta/internal/metals/EmptyCancelChecker.scala b/pc/core/src/main/scala/scala/meta/internal/metals/EmptyCancelChecker.scala new file mode 100644 index 00000000000..886b3ab7bc1 --- /dev/null +++ b/pc/core/src/main/scala/scala/meta/internal/metals/EmptyCancelChecker.scala @@ -0,0 +1,7 @@ +package scala.meta.internal.metals + +import org.eclipse.lsp4j.jsonrpc.CancelChecker + +object EmptyCancelChecker extends CancelChecker { + override def checkCanceled(): Unit = () +} diff --git a/metals/src/main/scala/scala/meta/internal/metals/Fuzzy.scala b/pc/core/src/main/scala/scala/meta/internal/metals/Fuzzy.scala similarity index 82% rename from metals/src/main/scala/scala/meta/internal/metals/Fuzzy.scala rename to pc/core/src/main/scala/scala/meta/internal/metals/Fuzzy.scala index db53cc331e5..acf6fbb30c3 100644 --- a/metals/src/main/scala/scala/meta/internal/metals/Fuzzy.scala +++ b/pc/core/src/main/scala/scala/meta/internal/metals/Fuzzy.scala @@ -2,7 +2,6 @@ package scala.meta.internal.metals import scala.annotation.tailrec import scala.collection.mutable -import java.lang.StringBuilder /** * Metals fuzzy search for strings. @@ -59,25 +58,8 @@ object Fuzzy { def matches( query: CharSequence, symbol: CharSequence, - skipNames: Int + skipNames: Int = 0 ): Boolean = { - def lastDelimiter( - string: CharSequence, - fromIndex: Int - ): Delimiter = { - var curr = fromIndex - 2 - var continue = true - while (curr >= 0 && continue) { - string.charAt(curr) match { - case '.' | '/' | '#' | '$' => - continue = false - case _ => - curr -= 1 - } - } - if (curr < 0) new Delimiter(true, 0) - else new Delimiter(false, curr + 1) - } // Loops through all names in the query/symbol strings in reverse order (last names first) // and returns true if all query names match their corresponding symbol name. // For the query "col.imm.Li" and symbol "scala/collection/immutable/List" we do the following loops. @@ -109,18 +91,78 @@ object Fuzzy { } } } - val endOfSymbolDelimiter = symbol.charAt(symbol.length - 1) match { - case '.' | '/' | '#' | '$' => 1 - case _ => 0 - } loopDelimiters( query.length, - symbol.length - endOfSymbolDelimiter, + lastIndex(symbol), 0, skipNames ) } + private def lastIndex(symbol: CharSequence): Int = { + var end = symbol.length() - (if (endsWith(symbol, ".class")) + ".class".length + else 1) + while (end >= 0 && isDelimiter(symbol.charAt(end))) { + end -= 1 + } + end + 1 + } + + def isDelimiter(ch: Char): Boolean = ch match { + case '.' | '/' | '#' | '$' => true + case _ => false + } + + /** + * Returns the length of the last name in this symbol. + * + * Example: scala/Option$Some.class returns length of "Some" + */ + def nameLength(symbol: CharSequence): Int = { + val end = lastIndex(symbol) - 1 + var start = end + while (start >= 0 && !isDelimiter(symbol.charAt(start))) { + start -= 1 + } + if (start < 0) end + 1 + else end - start + } + + def endsWith(cs: CharSequence, string: String): Boolean = { + val a = cs.length() - 1 + val b = string.length() - 1 + if (b > a) false + else if (b == 0) false + else { + var i = 0 + while (i <= a && i <= b) { + if (cs.charAt(a - i) != + string.charAt(b - i)) return false + i += 1 + } + true + } + } + + private def lastDelimiter( + string: CharSequence, + fromIndex: Int + ): Delimiter = { + var curr = fromIndex - 2 + var continue = true + while (curr >= 0 && continue) { + string.charAt(curr) match { + case '.' | '/' | '#' | '$' => + continue = false + case _ => + curr -= 1 + } + } + if (curr < 0) new Delimiter(true, 0) + else new Delimiter(false, curr + 1) + } + // Compares two names like query "InStr" and "InputFileStream". // The substring are guaranteed to not have delimiters. private def matchesName( @@ -189,6 +231,7 @@ object Fuzzy { * @param symbols all symbols in a source file or a package. */ def bloomFilterSymbolStrings( + // TODO: can be iterator symbols: Iterable[String], result: mutable.Set[CharSequence] = mutable.Set.empty ): mutable.Set[CharSequence] = { @@ -232,12 +275,12 @@ object Fuzzy { val ch = query.charAt(i) ch match { case '.' | '/' | '#' | '$' => - result.add(query.subSequence(border, i)) + result.add(new ZeroCopySubSequence(query, border, i)) border = i + 1 case _ => if (ch.isUpper) { if (border != i) { - val exactName = query.subSequence(border, i) + val exactName = new ZeroCopySubSequence(query, border, i) result.add(exactName) } upper.append(ch) @@ -249,7 +292,7 @@ object Fuzzy { query.last match { case '.' | '/' | '#' | '$' => case _ => - result.add(query.subSequence(border, query.length)) + result.add(new ZeroCopySubSequence(query, border, query.length)) } if (includeTrigrams) { result ++= new TrigramSubstrings(upper.toString) diff --git a/pc/core/src/main/scala/scala/meta/internal/metals/JavaSymbolIndexer.scala b/pc/core/src/main/scala/scala/meta/internal/metals/JavaSymbolIndexer.scala new file mode 100644 index 00000000000..e319832a900 --- /dev/null +++ b/pc/core/src/main/scala/scala/meta/internal/metals/JavaSymbolIndexer.scala @@ -0,0 +1,59 @@ +package scala.meta.internal.metals + +import com.thoughtworks.qdox.model.JavaClass +import com.thoughtworks.qdox.model.JavaConstructor +import com.thoughtworks.qdox.model.JavaMethod +import scala.meta.inputs.Input +import scala.meta.inputs.Position +import scala.meta.internal.mtags.JavaMtags +import scala.meta.internal.semanticdb.Scala.Descriptor +import scala.meta.internal.semanticdb.SymbolInformation +import scala.meta.pc.SymbolIndexer +import scala.meta.pc.SymbolVisitor + +class JavaSymbolIndexer(input: Input.VirtualFile) extends SymbolIndexer { + override def visit(symbol: String, visitor: SymbolVisitor): Unit = { + val mtags = new JavaMtags(input) { + override def visitClass( + cls: JavaClass, + name: String, + pos: Position, + kind: SymbolInformation.Kind, + properties: Int + ): Unit = { + super.visitClass(cls, name, pos, kind, properties) + visitor.visitSymbol( + MetalsSymbolDocumentation.fromClass(currentOwner, cls) + ) + } + override def visitConstructor( + ctor: JavaConstructor, + disambiguator: String, + pos: Position, + properties: Int + ): Unit = { + visitor.visitSymbol( + MetalsSymbolDocumentation.fromConstructor( + symbol(Descriptor.Method("", disambiguator)), + ctor + ) + ) + } + override def visitMethod( + method: JavaMethod, + name: String, + disambiguator: String, + pos: Position, + properties: Int + ): Unit = { + visitor.visitSymbol( + MetalsSymbolDocumentation.fromMethod( + symbol(Descriptor.Method(name, disambiguator)), + method + ) + ) + } + } + mtags.indexRoot() + } +} diff --git a/metals/src/main/scala/scala/meta/internal/metals/JdkSources.scala b/pc/core/src/main/scala/scala/meta/internal/metals/JdkSources.scala similarity index 97% rename from metals/src/main/scala/scala/meta/internal/metals/JdkSources.scala rename to pc/core/src/main/scala/scala/meta/internal/metals/JdkSources.scala index 0fb953e7729..af9377b2d03 100644 --- a/metals/src/main/scala/scala/meta/internal/metals/JdkSources.scala +++ b/pc/core/src/main/scala/scala/meta/internal/metals/JdkSources.scala @@ -1,9 +1,9 @@ package scala.meta.internal.metals import java.nio.file.Files +import scala.collection.JavaConverters._ import java.nio.file.Paths import scala.meta.io.AbsolutePath -import MetalsEnrichments._ /** * Locates zip file on disk that contains the source code for the JDK. diff --git a/pc/core/src/main/scala/scala/meta/internal/metals/MetalsSymbolDocumentation.scala b/pc/core/src/main/scala/scala/meta/internal/metals/MetalsSymbolDocumentation.scala new file mode 100644 index 00000000000..d8d3b7cbdc2 --- /dev/null +++ b/pc/core/src/main/scala/scala/meta/internal/metals/MetalsSymbolDocumentation.scala @@ -0,0 +1,109 @@ +package scala.meta.internal.metals + +import com.thoughtworks.qdox.model.JavaAnnotatedElement +import com.thoughtworks.qdox.model.JavaClass +import com.thoughtworks.qdox.model.JavaConstructor +import com.thoughtworks.qdox.model.JavaGenericDeclaration +import com.thoughtworks.qdox.model.JavaMethod +import com.thoughtworks.qdox.model.JavaParameter +import com.thoughtworks.qdox.model.JavaTypeVariable +import java.util +import scala.collection.JavaConverters._ +import scala.meta.internal.semanticdb.Scala.Descriptor +import scala.meta.internal.semanticdb.Scala.Symbols +import scala.meta.pc.SymbolDocumentation + +case class MetalsSymbolDocumentation( + symbol: String, + name: String, + docstring: String, + defaultValue: String = "", + typeParameters: util.List[SymbolDocumentation] = Nil.asJava, + parameters: util.List[SymbolDocumentation] = Nil.asJava +) extends SymbolDocumentation + +object MetalsSymbolDocumentation { + def fromMethod(symbol: String, method: JavaMethod): SymbolDocumentation = { + new MetalsSymbolDocumentation( + symbol, + method.getName, + method.getComment, + "", + typeParameters(symbol, method, method.getTypeParameters), + parameters(symbol, method, method.getParameters) + ) + } + def fromClass( + symbol: String, + method: JavaClass + ): SymbolDocumentation = { + new MetalsSymbolDocumentation( + symbol, + method.getName, + method.getComment, + "", + typeParameters(symbol, method, method.getTypeParameters), + Nil.asJava + ) + } + def fromConstructor( + symbol: String, + method: JavaConstructor + ): SymbolDocumentation = { + new MetalsSymbolDocumentation( + symbol, + method.getName, + method.getComment, + "", + typeParameters(symbol, method, method.getTypeParameters), + parameters(symbol, method, method.getParameters) + ) + } + def param( + symbol: String, + name: String, + docstring: String + ): SymbolDocumentation = + new MetalsSymbolDocumentation( + symbol, + name, + if (docstring == null) "" else docstring, + "" + ) + def typeParameters[D <: JavaGenericDeclaration]( + owner: String, + method: JavaAnnotatedElement, + tparams: util.List[JavaTypeVariable[D]] + ): util.List[SymbolDocumentation] = { + tparams.asScala.map { tparam => + val tparamName = s"<${tparam.getName}>" + val docstring = method.getTagsByName("param").asScala.collectFirst { + case tag if tag.getValue.startsWith(tparamName) => + tag.getValue + } + this.param( + Symbols.Global(owner, Descriptor.TypeParameter(tparam.getName)), + tparam.getName, + docstring.getOrElse("") + ) + }.asJava + } + def parameters( + owner: String, + method: JavaAnnotatedElement, + params: util.List[JavaParameter] + ): util.List[SymbolDocumentation] = { + params.asScala.map { param => + val docstring = method.getTagsByName("param").asScala.collectFirst { + case tag if tag.getValue.startsWith(param.getName) => + tag.getValue + } + this.param( + Symbols.Global(owner, Descriptor.Parameter(param.getName)), + param.getName, + docstring.getOrElse("") + ) + }.asJava + } + +} diff --git a/pc/core/src/main/scala/scala/meta/internal/metals/MetalsSymbolIndexer.scala b/pc/core/src/main/scala/scala/meta/internal/metals/MetalsSymbolIndexer.scala new file mode 100644 index 00000000000..b5e2d367b7b --- /dev/null +++ b/pc/core/src/main/scala/scala/meta/internal/metals/MetalsSymbolIndexer.scala @@ -0,0 +1,215 @@ +package scala.meta.internal.metals + +import java.util.logging.Level +import java.util.logging.Logger +import scala.collection.JavaConverters._ +import scala.meta._ +import scala.meta.internal.mtags.Symbol +import scala.meta.internal.mtags.MtagsEnrichments._ +import scala.meta.internal.mtags.OnDemandSymbolIndex +import scala.meta.internal.mtags.ScalaMtags +import scala.meta.internal.semanticdb.Language +import scala.meta.internal.semanticdb.Scala.Descriptor +import scala.meta.internal.semanticdb.Scala.Symbols +import scala.meta.internal.semanticdb.SymbolInformation +import scala.meta.internal.semanticdb.SymbolOccurrence +import scala.meta.internal.trees.Origin +import scala.meta.pc.SymbolDocumentation +import scala.meta.pc.SymbolIndexer +import scala.meta.pc.SymbolVisitor +import scala.meta.tokens.Token +import scala.meta.tokens.Tokens +import scala.util.control.NonFatal +import scala.meta.internal.docstrings._ + +class MetalsSymbolIndexer(index: OnDemandSymbolIndex) extends SymbolIndexer { + private val logger = Logger.getLogger(classOf[MetalsSymbolIndexer].getName) + override def visit(symbol: String, visitor: SymbolVisitor): Unit = { + index.definition(Symbol(symbol)) match { + case Some(defn) => + defn.path.toLanguage match { + case Language.JAVA => + new JavaSymbolIndexer(defn.path.toInput).visit(symbol, visitor) + case Language.SCALA => + val input = defn.path.toInput + val mtags = new ScalaMtags(input) { + override def visitOccurrence( + occ: SymbolOccurrence, + sinfo: SymbolInformation, + owner: _root_.scala.Predef.String + ): Unit = { + val docstring = currentTree.origin match { + case Origin.None => "" + case parsed: Origin.Parsed => + val leadingDocstring = + findLeadingDocstring(source.tokens, parsed.pos.start - 1) + leadingDocstring match { + case Some(value) => value + case None => "" + } + } + class Param(name: String) { + def unapply(line: String): Option[String] = { + val idx = line.lastIndexOf(name) + if (idx < 0) None + else Some(line.substring(idx + name.length)) + } + } + def doc(name: String): String = { + val param = new Param(name) + docstring.lines + .collectFirst { + case param(line) => line + } + .getOrElse("") + } + lazy val markdown = toMarkdown(docstring) + def param(name: String, default: String): SymbolDocumentation = + new MetalsSymbolDocumentation( + Symbols.Global(owner, Descriptor.Parameter(name)), + name, + doc(name), + default + ) + def mparam(member: Member): SymbolDocumentation = { + val default = member match { + case Term.Param(_, _, _, Some(term)) => term.syntax + case _ => + "" + } + param(member.name.value, default) + } + val info = currentTree match { + case _: Defn.Trait | _: Pkg.Object | _: Defn.Val | + _: Defn.Var | _: Decl.Val | _: Decl.Var | _: Defn.Type | + _: Decl.Type => + Some( + new MetalsSymbolDocumentation( + occ.symbol, + sinfo.displayName, + markdown, + "" + ) + ) + case t: Defn.Def => + Some( + new MetalsSymbolDocumentation( + occ.symbol, + t.name.value, + markdown, + "", + t.tparams.map(mparam).asJava, + t.paramss.flatten.map(mparam).asJava + ) + ) + case t: Decl.Def => + Some( + new MetalsSymbolDocumentation( + occ.symbol, + t.name.value, + markdown, + "", + t.tparams.map(mparam).asJava, + t.paramss.flatten.map(mparam).asJava + ) + ) + case t: Defn.Class => + Some( + new MetalsSymbolDocumentation( + occ.symbol, + t.name.value, + markdown, + "", + // Type parameters are intentionally excluded because constructors + // cannot have type parameters. + Nil.asJava, + t.ctor.paramss.flatten.map(mparam).asJava + ) + ) + case t: Member => + Some( + new MetalsSymbolDocumentation( + occ.symbol, + t.name.value, + markdown, + "" + ) + ) + case _ => + None + } + info.foreach(visitor.visitSymbol) + } + } + try mtags.indexRoot() + catch { + case NonFatal(e) => + logger.log(Level.SEVERE, defn.path.toURI.toString, e) + } + case _ => + } + case None => + } + } + + def toMarkdown(docstring: String): String = { + val comment = ScaladocParser.parseAtSymbol(docstring) + val out = new StringBuilder() + def loop(i: Inline): Unit = i match { + case Chain(items) => + items.foreach(loop) + case Italic(text) => + out.append('*') + loop(text) + out.append('*') + case Bold(text) => + out.append("**") + loop(text) + out.append("**") + case Underline(text) => + out.append("_") + loop(text) + out.append("_") + case Superscript(text) => + loop(text) + case Subscript(text) => + loop(text) + case Link(target, title) => + out.append("[") + loop(title) + out + .append("](") + .append(target) + .append(")") + case Monospace(text) => + out.append("`") + loop(text) + out.append("`") + case Text(text) => + out.append(text) + case _: EntityLink => + case HtmlTag(data) => + out.append(data) + case Summary(text) => + loop(text) + } + loop(comment.short) + out.toString().trim + } + + def findLeadingDocstring(tokens: Tokens, start: Int): Option[String] = + if (start < 0) None + else { + tokens(start) match { + case c: Token.Comment => + val syntax = c.syntax + if (syntax.startsWith("/**")) Some(syntax) + else None + case _: Token.Space | _: Token.LF | _: Token.CR | _: Token.LFLF | + _: Token.Tab => + findLeadingDocstring(tokens, start - 1) + case _ => + None + } + } +} diff --git a/pc/core/src/main/scala/scala/meta/internal/metals/PCEnrichments.scala b/pc/core/src/main/scala/scala/meta/internal/metals/PCEnrichments.scala new file mode 100644 index 00000000000..dbf07600203 --- /dev/null +++ b/pc/core/src/main/scala/scala/meta/internal/metals/PCEnrichments.scala @@ -0,0 +1,90 @@ +package scala.meta.internal.metals + +import com.google.gson.Gson +import com.google.gson.JsonElement +import java.util.logging.Level +import java.util.logging.Logger +import org.eclipse.lsp4j.CompletionItem +import scala.meta.internal.{semanticdb => s} +import org.eclipse.{lsp4j => l} +import scala.meta.internal.pc.CompletionItemData +import scala.meta.internal.semanticdb.SymbolInformation.{Kind => k} +import scala.util.control.NonFatal + +object PCEnrichments extends PCEnrichments + +trait PCEnrichments { + + private def logger: Logger = Logger.getLogger(classOf[PCEnrichments].getName) + + protected def decodeJson[T]( + obj: AnyRef, + cls: java.lang.Class[T] + ): Option[T] = + for { + data <- Option(obj) + value <- try { + Some( + new Gson().fromJson[T]( + data.asInstanceOf[JsonElement], + cls + ) + ) + } catch { + case NonFatal(e) => + logger.log(Level.SEVERE, s"decode error: $cls", e) + None + } + } yield value + + implicit class XtensionCompletionItemData(item: CompletionItem) { + def data: Option[CompletionItemData] = + item.getData match { + case d: CompletionItemData => + Some(d) + case data => + decodeJson(data, classOf[CompletionItemData]) + } + } + implicit class XtensionRangeBuildProtocol(range: s.Range) { + def toLocation(uri: String): l.Location = { + new l.Location(uri, range.toLSP) + } + def toLSP: l.Range = { + val start = new l.Position(range.startLine, range.startCharacter) + val end = new l.Position(range.endLine, range.endCharacter) + new l.Range(start, end) + } + def encloses(other: l.Position): Boolean = { + range.startLine <= other.getLine && + range.endLine >= other.getLine && + range.startCharacter <= other.getCharacter && + range.endCharacter > other.getCharacter + } + def encloses(other: l.Range): Boolean = { + encloses(other.getStart) && + encloses(other.getEnd) + } + } + + implicit class XtensionSymbolInformation(kind: s.SymbolInformation.Kind) { + def toLSP: l.SymbolKind = kind match { + case k.LOCAL => l.SymbolKind.Variable + case k.FIELD => l.SymbolKind.Field + case k.METHOD => l.SymbolKind.Method + case k.CONSTRUCTOR => l.SymbolKind.Constructor + case k.MACRO => l.SymbolKind.Method + case k.TYPE => l.SymbolKind.Class + case k.PARAMETER => l.SymbolKind.Variable + case k.SELF_PARAMETER => l.SymbolKind.Variable + case k.TYPE_PARAMETER => l.SymbolKind.TypeParameter + case k.OBJECT => l.SymbolKind.Object + case k.PACKAGE => l.SymbolKind.Module + case k.PACKAGE_OBJECT => l.SymbolKind.Module + case k.CLASS => l.SymbolKind.Class + case k.TRAIT => l.SymbolKind.Interface + case k.INTERFACE => l.SymbolKind.Interface + case _ => l.SymbolKind.Class + } + } +} diff --git a/metals/src/main/scala/scala/meta/internal/metals/PackageIndex.scala b/pc/core/src/main/scala/scala/meta/internal/metals/PackageIndex.scala similarity index 80% rename from metals/src/main/scala/scala/meta/internal/metals/PackageIndex.scala rename to pc/core/src/main/scala/scala/meta/internal/metals/PackageIndex.scala index 34b6f648cfd..5b1a5ee77b6 100644 --- a/metals/src/main/scala/scala/meta/internal/metals/PackageIndex.scala +++ b/pc/core/src/main/scala/scala/meta/internal/metals/PackageIndex.scala @@ -7,6 +7,8 @@ import java.nio.file.SimpleFileVisitor import java.nio.file.attribute.BasicFileAttributes import java.util import java.util.jar.JarFile +import java.util.logging.Level +import java.util.logging.Logger import scala.meta.internal.io.PathIO import scala.meta.internal.mtags.MtagsEnrichments._ import scala.meta.io.AbsolutePath @@ -16,7 +18,8 @@ import scala.util.control.NonFatal /** * An index to lookup classfiles contained in a given classpath. */ -class PackageIndex { +class PackageIndex() { + val logger = Logger.getLogger(classOf[PackageIndex].getName) val packages = new util.HashMap[String, util.ArrayList[String]]() private val isVisited = new util.HashSet[AbsolutePath]() private val enterPackage = @@ -37,14 +40,16 @@ class PackageIndex { } } catch { case NonFatal(e) => - scribe.error(s"failed to process classpath entry $entry", e) + logger.log(Level.SEVERE, entry.toURI.toString, e) } } } def addMember(pkg: String, member: String): Unit = { - val members = packages.computeIfAbsent(pkg, enterPackage) - members.add(member) + if (!member.contains("module-info.class")) { + val members = packages.computeIfAbsent(pkg, enterPackage) + members.add(member) + } } private def visitDirectoryEntry(dir: AbsolutePath): Unit = { @@ -57,8 +62,8 @@ class PackageIndex { ): FileVisitResult = { val member = file.getFileName.toString if (member.endsWith(".class")) { - val relpath = AbsolutePath(file).toRelative(dir) - val pkg = relpath.toURI(isDirectory = false).toString + val relpath = AbsolutePath(file.getParent).toRelative(dir) + val pkg = relpath.toURI(isDirectory = true).toString addMember(pkg, member) } FileVisitResult.CONTINUE @@ -105,14 +110,20 @@ class PackageIndex { } } - def expandJdkClasspath(): Unit = { - sys.props - .collectFirst { - case (k, v) if k.endsWith(".boot.class.path") => - Classpath(v).entries - .filter(_.isFile) - .foreach(jar => visitJarEntry(jar)) - } + def visitBootClasspath(): Unit = { + PackageIndex.bootClasspath.foreach(visit) } } + +object PackageIndex { + def bootClasspath: List[AbsolutePath] = + for { + entries <- sys.props.collectFirst { + case (k, v) if k.endsWith(".boot.class.path") => + Classpath(v).entries + }.toList + entry <- entries + if entry.isFile + } yield entry +} diff --git a/metals/src/main/scala/scala/meta/internal/metals/SemanticdbDefinition.scala b/pc/core/src/main/scala/scala/meta/internal/metals/SemanticdbDefinition.scala similarity index 89% rename from metals/src/main/scala/scala/meta/internal/metals/SemanticdbDefinition.scala rename to pc/core/src/main/scala/scala/meta/internal/metals/SemanticdbDefinition.scala index 7cb9fc53040..5618e15ff89 100644 --- a/metals/src/main/scala/scala/meta/internal/metals/SemanticdbDefinition.scala +++ b/pc/core/src/main/scala/scala/meta/internal/metals/SemanticdbDefinition.scala @@ -2,7 +2,7 @@ package scala.meta.internal.metals import org.eclipse.{lsp4j => l} import scala.meta.inputs.Input -import scala.meta.internal.metals.MetalsEnrichments._ +import PCEnrichments._ import scala.meta.internal.mtags.JavaMtags import scala.meta.internal.mtags.MtagsEnrichments._ import scala.meta.internal.mtags.ScalaToplevelMtags @@ -11,6 +11,7 @@ import scala.meta.internal.semanticdb.SymbolInformation import scala.meta.internal.semanticdb.SymbolOccurrence import scala.meta.tokenizers.TokenizeException import scala.util.control.NonFatal +import scala.meta.internal.{semanticdb => s} /** * A definition of a global symbol produced by mtags. @@ -20,6 +21,10 @@ case class SemanticdbDefinition( occ: SymbolOccurrence, owner: String ) { + def toCached: CachedSymbolInformation = { + val range = occ.range.getOrElse(s.Range()) + CachedSymbolInformation(info.symbol, info.kind.toLSP, range.toLSP) + } def toLSP(uri: String): l.SymbolInformation = { new l.SymbolInformation( info.displayName, diff --git a/metals/src/main/scala/scala/meta/internal/metals/TrigramSubstrings.scala b/pc/core/src/main/scala/scala/meta/internal/metals/TrigramSubstrings.scala similarity index 100% rename from metals/src/main/scala/scala/meta/internal/metals/TrigramSubstrings.scala rename to pc/core/src/main/scala/scala/meta/internal/metals/TrigramSubstrings.scala diff --git a/metals/src/main/scala/scala/meta/internal/metals/WorkspaceSymbolQuery.scala b/pc/core/src/main/scala/scala/meta/internal/metals/WorkspaceSymbolQuery.scala similarity index 82% rename from metals/src/main/scala/scala/meta/internal/metals/WorkspaceSymbolQuery.scala rename to pc/core/src/main/scala/scala/meta/internal/metals/WorkspaceSymbolQuery.scala index e652c6dd5fe..5d08491ecab 100644 --- a/metals/src/main/scala/scala/meta/internal/metals/WorkspaceSymbolQuery.scala +++ b/pc/core/src/main/scala/scala/meta/internal/metals/WorkspaceSymbolQuery.scala @@ -2,6 +2,8 @@ package scala.meta.internal.metals import com.google.common.hash.BloomFilter import scala.meta.internal.metals.WorkspaceSymbolQuery.AlternativeQuery +import scala.meta.internal.semanticdb.SymbolInformation +import scala.meta.internal.semanticdb.SymbolInformation.Kind /** * A query for workspace/symbol. @@ -21,9 +23,20 @@ case class WorkspaceSymbolQuery( alternatives.exists(_.matches(bloom)) def matches(symbol: CharSequence): Boolean = alternatives.exists(_.matches(symbol, isTrailingDot)) + def matches(info: SymbolInformation): Boolean = { + WorkspaceSymbolQuery.isRelevantKind(info.kind) && + this.matches(info.symbol) + } } object WorkspaceSymbolQuery { + def exact(query: String): WorkspaceSymbolQuery = { + WorkspaceSymbolQuery( + query, + Array(AlternativeQuery(query)), + isTrailingDot = false + ) + } def fromTextQuery(query: String): WorkspaceSymbolQuery = { val isTrailingDot = query.endsWith(".") val actualQuery = @@ -78,4 +91,13 @@ object WorkspaceSymbolQuery { } } } + def isRelevantKind(kind: Kind): Boolean = { + kind match { + case Kind.OBJECT | Kind.PACKAGE_OBJECT | Kind.CLASS | Kind.TRAIT | + Kind.INTERFACE => + true + case _ => + false + } + } } diff --git a/pc/core/src/main/scala/scala/meta/internal/metals/ZeroCopySubSequence.scala b/pc/core/src/main/scala/scala/meta/internal/metals/ZeroCopySubSequence.scala new file mode 100644 index 00000000000..3d70c04d396 --- /dev/null +++ b/pc/core/src/main/scala/scala/meta/internal/metals/ZeroCopySubSequence.scala @@ -0,0 +1,16 @@ +package scala.meta.internal.metals + +class ZeroCopySubSequence(underlying: CharSequence, start: Int, end: Int) + extends CharSequence { + override def length(): Int = end - start + + override def charAt(index: Int): Char = { + underlying.charAt(start + index) + } + + override def subSequence(newStart: Int, newEnd: Int): CharSequence = { + new ZeroCopySubSequence(underlying, start + newStart, start + end) + } + + override def toString: String = underlying.subSequence(start, end).toString +} diff --git a/pc/core/src/main/scala/scala/meta/internal/pc/CompilerAccess.scala b/pc/core/src/main/scala/scala/meta/internal/pc/CompilerAccess.scala new file mode 100644 index 00000000000..5a64cfb29cf --- /dev/null +++ b/pc/core/src/main/scala/scala/meta/internal/pc/CompilerAccess.scala @@ -0,0 +1,41 @@ +package scala.meta.internal.pc + +import java.util.logging.Level +import java.util.logging.Logger +import scala.tools.nsc.reporters.StoreReporter +import scala.util.control.NonFatal + +class CompilerAccess(logger: Logger, newCompiler: () => MetalsGlobal) { + def isEmpty: Boolean = _compiler == null + def isDefined: Boolean = !isEmpty + def reporter: StoreReporter = + if (isEmpty) new StoreReporter() + else _compiler.reporter.asInstanceOf[StoreReporter] + def shutdown(): Unit = { + if (_compiler != null) { + _compiler.askShutdown() + _compiler = null + } + } + def withCompiler[T](default: T)(thunk: MetalsGlobal => T): T = { + lock.synchronized { + try thunk(loadCompiler()) + catch { + case NonFatal(e) => + CompilerThrowable.trimStackTrace(e) + logger.log(Level.SEVERE, e.getMessage, e) + shutdown() + default + } + } + } + private var _compiler: MetalsGlobal = _ + private val lock = new Object + private def loadCompiler(): MetalsGlobal = { + if (_compiler == null) { + _compiler = newCompiler() + } + _compiler.reporter.reset() + _compiler + } +} diff --git a/pc/core/src/main/scala/scala/meta/internal/pc/CompilerEnrichments.scala b/pc/core/src/main/scala/scala/meta/internal/pc/CompilerEnrichments.scala new file mode 100644 index 00000000000..c7754341316 --- /dev/null +++ b/pc/core/src/main/scala/scala/meta/internal/pc/CompilerEnrichments.scala @@ -0,0 +1,44 @@ +package scala.meta.internal.pc + +import scala.reflect.internal.util.Position +import scala.tools.nsc.interactive.Global + +object CompilerEnrichments { + def safeCompletionsAt[G <: Global]( + global: Global, + position: Position + ): List[global.CompletionResult#M] = { + def expected(e: Throwable): List[Nothing] = { + println(s"Expected error '${e.getMessage}'") + Nil + } + try { + global.completionsAt(position).matchingResults().distinct + } catch { + case e: global.CyclicReference + if e.getMessage.contains("illegal cyclic reference") => + // A quick google search reveals this happens regularly and there is + // no general fix for it. + expected(e) + case e: ScalaReflectionException + if e.getMessage.contains("not a module") => + // Do nothing, seems to happen regularly + // scala.ScalaReflectionException: value is not a module + // at scala.reflect.api.Symbols$SymbolApi.asModule(Symbols.scala:250) + // at scala.reflect.api.Symbols$SymbolApi.asModule$(Symbols.scala:250) + expected(e) + case e: NullPointerException => + // do nothing, seems to happen regularly + // java.lang.NullPointerException: null + // at scala.tools.nsc.Global$GlobalPhase.cancelled(Global.scala:408) + // at scala.tools.nsc.Global$GlobalPhase.applyPhase(Global.scala:418) + // at scala.tools.nsc.Global$Run.$anonfun$compileLate$3(Global.scala:1572) + expected(e) + case e: StringIndexOutOfBoundsException => + // NOTE(olafur) Let's log this for now while we are still learning more + // about the PC. However, I haven't been able to avoid this exception + // in some cases so I suspect it's here to stay until we fix it upstream. + expected(e) + } + } +} diff --git a/pc/core/src/main/scala/scala/meta/internal/pc/CompilerSearchVisitor.scala b/pc/core/src/main/scala/scala/meta/internal/pc/CompilerSearchVisitor.scala new file mode 100644 index 00000000000..fe6160fa89a --- /dev/null +++ b/pc/core/src/main/scala/scala/meta/internal/pc/CompilerSearchVisitor.scala @@ -0,0 +1,37 @@ +package scala.meta.internal.pc + +import java.nio.file.Path +import scala.meta.pc.SymbolSearchVisitor +import org.eclipse.{lsp4j => l} + +class CompilerSearchVisitor( + query: String, + containsPackage: String => Boolean, + visit: SymbolSearchCandidate => Int +) extends SymbolSearchVisitor { + def visitClassfile(pkg: String, filename: String): Int = { + visit(SymbolSearchCandidate.Classfile(pkg, filename)) + } + def visitWorkspaceSymbol( + path: Path, + symbol: String, + kind: l.SymbolKind, + range: l.Range + ): Int = { + visit(SymbolSearchCandidate.Workspace(symbol)) + } + + override def shouldVisitPath(path: Path): Boolean = { + // TODO: filter out paths that are guaranteed on a source dependency + true + } + def shouldVisitPackage(pkg: String): Boolean = { + // TODO come up with less hacky check, maybe staticPackageSymbol(..) + containsPackage(pkg.stripSuffix("/").replace('/', '.')) + } + + override def isCancelled: Boolean = { + // TODO(olafur) integrate CancelChecker + false + } +} diff --git a/pc/core/src/main/scala/scala/meta/internal/pc/CompilerThrowable.scala b/pc/core/src/main/scala/scala/meta/internal/pc/CompilerThrowable.scala new file mode 100644 index 00000000000..c1d15f43553 --- /dev/null +++ b/pc/core/src/main/scala/scala/meta/internal/pc/CompilerThrowable.scala @@ -0,0 +1,23 @@ +package scala.meta.internal.pc + +import java.util.Collections +import scala.annotation.tailrec + +object CompilerThrowable { + def trimStackTrace(e: Throwable): Unit = { + val isVisited = Collections.newSetFromMap( + new java.util.IdentityHashMap[Throwable, java.lang.Boolean] + ) + @tailrec def loop(ex: Throwable): Unit = { + isVisited.add(ex) + val stacktrace = + ex.getStackTrace.takeWhile(!_.getClassName.contains("ScalaPC")) + ex.setStackTrace(stacktrace) + // avoid infinite loop when traversing exceptions cyclic dependencies between causes. + if (e.getCause != null && !isVisited.contains(e.getCause)) { + loop(e.getCause) + } + } + loop(e) + } +} diff --git a/pc/core/src/main/scala/scala/meta/internal/pc/CompletionItemData.scala b/pc/core/src/main/scala/scala/meta/internal/pc/CompletionItemData.scala new file mode 100644 index 00000000000..ef0d6c0fabd --- /dev/null +++ b/pc/core/src/main/scala/scala/meta/internal/pc/CompletionItemData.scala @@ -0,0 +1,14 @@ +package scala.meta.internal.pc + +import com.google.gson.JsonElement +import com.google.gson.JsonObject +import com.google.gson.JsonPrimitive + +case class CompletionItemData(symbol: String, target: String) { + def toJson: JsonElement = { + val obj = new JsonObject() + obj.add("symbol", new JsonPrimitive(symbol)) + obj.add("target", new JsonPrimitive(target)) + obj + } +} diff --git a/pc/core/src/main/scala/scala/meta/internal/pc/CompletionItemResolver.scala b/pc/core/src/main/scala/scala/meta/internal/pc/CompletionItemResolver.scala new file mode 100644 index 00000000000..a3bfcfd5f5d --- /dev/null +++ b/pc/core/src/main/scala/scala/meta/internal/pc/CompletionItemResolver.scala @@ -0,0 +1,97 @@ +package scala.meta.internal.pc + +import org.eclipse.lsp4j.CompletionItem +import org.eclipse.lsp4j.MarkupContent +import scala.collection.JavaConverters._ + +class CompletionItemResolver( + val compiler: MetalsGlobal +) { + import compiler._ + def resolve(item: CompletionItem, msym: String): CompletionItem = { + val gsym = inverseSemanticdbSymbol(msym) + if (gsym != NoSymbol) { + methodInfo(gsym).orElse(methodInfo(gsym.companion)) match { + case Some(info) if item.getDetail != null => + if (isJavaSymbol(gsym)) { + val newDetail = info + .parameters() + .asScala + .iterator + .zipWithIndex + .foldLeft(item.getDetail) { + case (detail, (param, i)) => + detail.replaceAllLiterally(s"x$$${i + 1}", param.name()) + } + item.setDetail(newDetail) + } else { + val defaults = info + .parameters() + .asScala + .iterator + .map(_.defaultValue()) + .filterNot(_.isEmpty) + val matcher = "= \\{\\}".r.pattern.matcher(item.getDetail) + val out = new StringBuffer() + while (matcher.find()) { + if (defaults.hasNext) { + matcher.appendReplacement(out, s"= ${defaults.next()}") + } + } + matcher.appendTail(out) + item.setDetail(out.toString) + } + val docstring = fullDocstring(gsym) + val content = new MarkupContent() + content.setKind("markdown") + content.setValue(docstring) + item.setDocumentation(content) + case _ => + } + item + } else { + item + } + } + + def fullDocstring(gsym: Symbol): String = { + def docs(gsym: Symbol): String = + methodInfo(gsym).fold("")(_.docstring()) + val gsymDoc = docs(gsym) + def keyword(gsym: Symbol): String = + if (gsym.isClass) "class" + else if (gsym.isTrait) "trait" + else if (gsym.isJavaInterface) "interface" + else if (gsym.isModule) "object" + else "" + val companion = gsym.companion + if (companion == NoSymbol || isJavaSymbol(gsym)) { + if (gsymDoc.isEmpty) { + if (gsym.isAliasType) { + fullDocstring(gsym.info.dealias.typeSymbol) + } else if (gsym.isMethod) { + gsym.info.finalResultType match { + case SingleType(_, sym) => + fullDocstring(sym) + case _ => + "" + } + } else "" + } else { + gsymDoc + } + } else { + val companionDoc = docs(companion) + if (companionDoc.isEmpty) gsymDoc + else if (gsymDoc.isEmpty) companionDoc + else { + s"""|### ${keyword(companion)} ${companion.name} + |$companionDoc + | + |### ${keyword(gsym)} ${gsym.name} + |${gsymDoc} + |""".stripMargin + } + } + } +} diff --git a/pc/core/src/main/scala/scala/meta/internal/pc/CompletionProvider.scala b/pc/core/src/main/scala/scala/meta/internal/pc/CompletionProvider.scala new file mode 100644 index 00000000000..a459c712453 --- /dev/null +++ b/pc/core/src/main/scala/scala/meta/internal/pc/CompletionProvider.scala @@ -0,0 +1,429 @@ +package scala.meta.internal.pc + +import org.eclipse.lsp4j.CompletionItem +import org.eclipse.lsp4j.CompletionItemKind +import scala.collection.JavaConverters._ +import scala.collection.mutable +import scala.meta.internal.metals.Fuzzy +import scala.meta.pc.CompletionItems +import scala.meta.pc.CompletionItems.LookupKind +import scala.meta.pc.OffsetParams +import scala.meta.pc.SymbolSearch +import scala.util.control.NonFatal +import scala.meta.internal.semanticdb.Scala._ + +class CompletionProvider( + val compiler: MetalsGlobal, + params: OffsetParams +) { + import compiler._ + + val maxWorkspaceSymbolResults = 10 + + def completions(): CompletionItems = { + val unit = addCompilationUnit( + code = params.text, + filename = params.filename, + cursor = Some(params.offset) + ) + val position = unit.position(params.offset) + val shortenedNames = new ShortenedNames() + val (qual, kind, i) = safeCompletionsAt(position) + val history = new ShortenedNames() + def infoString(sym: Symbol, info: Type): String = sym match { + case m: MethodSymbol => + new SignaturePrinter(m, shortenedNames, info, includeDocs = false).defaultMethodSignature + case _ => + def fullName(s: Symbol): String = " " + s.owner.fullName + dealiasedValForwarder(sym) match { + case dealiased :: _ => + fullName(dealiased) + case _ => + if (sym.isModuleOrModuleClass || sym.hasPackageFlag || sym.isClass) { + fullName(sym) + } else { + val short = shortType(info, history) + sym.infoString(short) + } + } + } + def detailString(r: Member): String = { + qual match { + case Some(tpe) if !r.sym.hasPackageFlag => + // Compute type parameters based on the qualifier. + // Example: Map[Int, String].applyOrE@@ + // Before: getOrElse[V1 >: V] (key: K, default: => V1): V1 + // After: getOrElse[V1 >: String](key: Int, default: => V1): V1 + infoString(r.sym, tpe.memberType(r.sym)) + case _ => + if (r.sym.hasRawInfo) { + infoString(r.sym, r.sym.rawInfo) + } else { + "<_>" + } + } + } + val sorted = i.results.sorted(new Ordering[Member] { + override def compare(o1: Member, o2: Member): Int = { + val byRelevance = + Integer.compare(relevancePenalty(o1), relevancePenalty(o2)) + if (byRelevance != 0) byRelevance + else { + val byIdentifier = + IdentifierComparator.compare(o1.sym.name, o2.sym.name) + if (byIdentifier != 0) byIdentifier + else detailString(o1).compareTo(detailString(o2)) + } + } + }) + val items = sorted.iterator.zipWithIndex.map { + case (r, idx) => + params.checkCanceled() + val label = r.symNameDropLocal.decoded + val item = new CompletionItem(label) + // TODO(olafur): investigate TypeMembers.prefix field, maybe it can replace qual match here. + val detail = detailString(r) + r match { + case w: WorkspaceMember => + item.setInsertText(w.sym.fullName) + case _ => + } + item.setDetail(detail) + item.setData( + CompletionItemData(semanticdbSymbol(r.sym), buildTargetIdentifier).toJson + ) + item.setKind(completionItemKind(r)) + item.setSortText(f"${idx}%05d") + val commitCharacter = + if (r.sym.isMethod && !isNullary(r.sym)) "(" + else "." + item.setCommitCharacters(List(commitCharacter).asJava) + if (idx == 0) { + item.setPreselect(true) + } + item + } + val result = new CompletionItems(kind, items.toSeq.asJava) + result.setIsIncomplete(i.isIncomplete) + result + } + + def isNullary(sym: Symbol): Boolean = sym.info match { + case _: NullaryMethodType => true + case PolyType(_, _: NullaryMethodType) => true + case _ => false + } + + case class InterestingMembers( + results: List[Member], + searchResult: SymbolSearch.Result + ) { + def isIncomplete: Boolean = searchResult == SymbolSearch.Result.INCOMPLETE + } + + def dealiasedValForwarder(sym: Symbol): List[Symbol] = { + if (sym.isValue && sym.hasRawInfo && !semanticdbSymbol(sym).isLocal) { + sym.rawInfo match { + case SingleType(_, dealias) if dealias.isModule => + dealias :: dealias.companion :: Nil + case _ => + Nil + } + } else { + Nil + } + } + + private def filterInteresting( + completions: List[Member], + kind: LookupKind, + query: String, + pos: Position + ): InterestingMembers = { + val isUninterestingSymbol = Set[Symbol]( + // the methods == != ## are arguably "interesting" but they're here becuase + // - they're short so completing them doesn't save you keystrokes + // - they're available on everything so you + definitions.Any_==, + definitions.Any_!=, + definitions.Any_##, + definitions.Object_==, + definitions.Object_!=, + definitions.Object_##, + definitions.Object_eq, + definitions.Object_ne, + definitions.RepeatedParamClass, + definitions.ByNameParamClass, + definitions.JavaRepeatedParamClass, + definitions.Object_notify, + definitions.Object_notifyAll, + definitions.Object_notify, + definitions.getMemberMethod(definitions.ObjectClass, termNames.wait_), + definitions.getMemberMethod( + definitions.getMemberClass( + definitions.PredefModule, + TypeName("ArrowAssoc") + ), + TermName("→").encode + ) + ).flatMap(_.alternatives) + val isSeen = mutable.Set.empty[String] + val isIgnored = mutable.Set.empty[Symbol] + val buf = List.newBuilder[Member] + def visit(head: Member): Boolean = { + val id = + if (head.sym.isClass || head.sym.isModule) { + head.sym.fullName + } else { + semanticdbSymbol(head.sym) + } + def isIgnoredWorkspace: Boolean = + head.isInstanceOf[WorkspaceMember] && + (isIgnored(head.sym) || isIgnored(head.sym.companion)) + if (!isSeen(id) && + !isUninterestingSymbol(head.sym) && + !isIgnoredWorkspace) { + isSeen += id + buf += head + isIgnored ++= dealiasedValForwarder(head.sym) + true + } else { + false + } + } + completions.foreach(visit) + val searchResults = + if (kind == LookupKind.Scope) { + workspaceSymbolListMembers(query, pos, visit) + } else { + SymbolSearch.Result.COMPLETE + } + + InterestingMembers(buf.result(), searchResults) + } + + private def isFunction(symbol: Symbol): Boolean = { + compiler.definitions.isFunctionSymbol( + symbol.info.finalResultType.typeSymbol + ) + } + + private def completionItemKind(r: Member): CompletionItemKind = { + import org.eclipse.lsp4j.{CompletionItemKind => k} + val symbol = r.sym + val symbolIsFunction = isFunction(symbol) + if (symbol.hasPackageFlag) k.Module + else if (symbol.isPackageObject) k.Module + else if (symbol.isModuleOrModuleClass) k.Module + else if (symbol.isTrait) k.Interface + else if (symbol.isJava) k.Interface + else if (symbol.isClass) k.Class + else if (symbol.isMethod) k.Method + else if (symbol.isCaseAccessor) k.Field + else if (symbol.isVal && !symbolIsFunction) k.Value + else if (symbol.isVar && !symbolIsFunction) k.Variable + else if (symbol.isTypeParameterOrSkolem) k.TypeParameter + else if (symbolIsFunction) k.Function + else k.Value + } + + /** Computes the relative relevance of a symbol in the completion list + * This is an adaptation of + * https://github.com/scala-ide/scala-ide/blob/a17ace0ee1be1875b8992664069d8ad26162eeee/org.scala-ide.sdt.core/src/org/scalaide/core/completion/ProposalRelevanceCalculator.scala + */ + private def computeRelevancePenalty( + sym: Symbol, + viaImplicitConversion: Boolean, + isInherited: Boolean + ): Int = { + import MemberOrdering._ + var relevance = 0 + // local symbols are more relevant + if (!sym.isLocalToBlock) relevance |= IsNotLocalByBlock + // fields are more relevant than non fields + if (!sym.hasGetter) relevance |= IsNotGetter + // non-inherited members are more relevant + if (isInherited) relevance |= IsInherited + // symbols whose owner is a base class are less relevant + val isInheritedBaseMethod = sym.owner match { + case definitions.AnyClass | definitions.AnyRefClass | + definitions.ObjectClass => + true + case _ => + false + } + if (isInheritedBaseMethod) + relevance |= IsInheritedBaseMethod + // symbols not provided via an implicit are more relevant + if (viaImplicitConversion) relevance |= IsImplicitConversion + if (sym.hasPackageFlag) relevance |= IsPackage + // accessors of case class members are more relevant + if (!sym.isCaseAccessor) relevance |= IsNotCaseAccessor + // public symbols are more relevant + if (!sym.isPublic) relevance |= IsNotCaseAccessor + // synthetic symbols are less relevant (e.g. `copy` on case classes) + if (sym.isSynthetic) relevance |= IsSynthetic + relevance + } + + private def safeCompletionsAt( + position: Position + ): (Option[Type], LookupKind, InterestingMembers) = { + def expected(e: Throwable) = { + logger.warning(e.getMessage) + ( + None, + LookupKind.None, + InterestingMembers(Nil, SymbolSearch.Result.COMPLETE) + ) + } + try { + val completions = completionsAt(position) + params.checkCanceled() + val matchingResults = completions.matchingResults { entered => name => + Fuzzy.matches(entered, name) + } + val kind = completions match { + case _: CompletionResult.ScopeMembers => + LookupKind.Scope + case _: CompletionResult.TypeMembers => + LookupKind.Type + case _ => + LookupKind.None + } + val items = filterInteresting( + matchingResults, + kind, + completions.name.toString, + position + ) + params.checkCanceled() + val qual = completions match { + case t: CompletionResult.TypeMembers => + Option(t.qualifier.tpe) + case _ => + None + } + (qual, kind, items) + } catch { + case e: CyclicReference + if e.getMessage.contains("illegal cyclic reference") => + expected(e) + case e: ScalaReflectionException + if e.getMessage.contains("not a module") => + expected(e) + case e: NullPointerException => + expected(e) + case e: StringIndexOutOfBoundsException => + expected(e) + } + } + + /** + * Returns a high number for less relevant symbols and low number for relevant numbers. + * + * Relevance is computed based on several factors such as + * - local vs global + * - public vs private + * - synthetic vs non-synthetic + */ + def relevancePenalty(m: Member): Int = m match { + case TypeMember(sym, _, true, isInherited, _) => + computeRelevancePenalty(sym, m.implicitlyAdded, isInherited) + case w: WorkspaceMember => + MemberOrdering.IsWorkspaceSymbol + w.sym.name.length() + case ScopeMember(sym, _, true, _) => + computeRelevancePenalty(sym, m.implicitlyAdded, isInherited = false) + case _ => + Int.MaxValue + } + + class WorkspaceMember(sym: Symbol) + extends ScopeMember(sym, NoType, true, EmptyTree) + val packageSymbols = mutable.Map.empty[String, Option[Symbol]] + def packageSymbolFromString(symbol: String): Option[Symbol] = { + packageSymbols.getOrElseUpdate(symbol, { + val fqn = symbol.replace('/', '.').stripSuffix(".") + try { + Some(rootMirror.staticPackage(fqn)) + } catch { + case NonFatal(_) => + None + } + }) + } + + private def workspaceSymbolListMembers( + query: String, + pos: Position, + visit: Member => Boolean + ): SymbolSearch.Result = { + if (query.isEmpty) SymbolSearch.Result.COMPLETE + else { + val context = doLocateContext(pos) + val visitor = new CompilerSearchVisitor( + query, + pkg => packageSymbolFromString(pkg).isDefined, + top => { + var added = 0 + for { + sym <- loadSymbolFromClassfile(top) + if context.lookupSymbol(sym.name, _ => true).symbol != sym + } { + if (visit(new WorkspaceMember(sym))) { + added += 1 + } + } + added + } + ) + search.search(query, buildTargetIdentifier, visitor) + } + } + + private def loadSymbolFromClassfile( + classfile: SymbolSearchCandidate + ): List[Symbol] = { + def isAccessible(sym: Symbol): Boolean = { + sym != NoSymbol && { + sym.info // needed to fill complete symbol + sym.isPublic + } + } + try { + classfile match { + case SymbolSearchCandidate.Classfile(pkgString, filename) => + val pkg = packageSymbolFromString(pkgString).getOrElse( + throw new NoSuchElementException(pkgString) + ) + val names = filename + .stripSuffix(".class") + .split('$') + .iterator + .filterNot(_.isEmpty) + .toList + val members = names.foldLeft(List[Symbol](pkg)) { + case (accum, name) => + accum.flatMap { sym => + if (!isAccessible(sym) || !sym.isModuleOrModuleClass) Nil + else { + sym.info.member(TermName(name)) :: + sym.info.member(TypeName(name)) :: + Nil + } + } + } + members.filter(sym => isAccessible(sym)) + case SymbolSearchCandidate.Workspace(symbol) => + val gsym = inverseSemanticdbSymbol(symbol) + if (isAccessible(gsym)) gsym :: Nil + else Nil + } + } catch { + case NonFatal(_) => + logger.warning(s"no such symbol: $classfile") + Nil + } + } + +} diff --git a/pc/core/src/main/scala/scala/meta/internal/pc/ConsoleLogger.scala b/pc/core/src/main/scala/scala/meta/internal/pc/ConsoleLogger.scala new file mode 100644 index 00000000000..46d4793b7c9 --- /dev/null +++ b/pc/core/src/main/scala/scala/meta/internal/pc/ConsoleLogger.scala @@ -0,0 +1,17 @@ +package scala.meta.internal.pc +import java.util.logging.ConsoleHandler +import java.util.logging.Level +import java.util.logging.Logger +import java.util.logging.SimpleFormatter + +object ConsoleLogger { + def apply(name: String = "metals"): Logger = { + val logger = Logger.getLogger(name) + logger.setLevel(Level.ALL) + val handler = new ConsoleHandler + handler.setLevel(Level.ALL) + handler.setFormatter(new SimpleFormatter) + logger.addHandler(handler) + logger + } +} diff --git a/pc/core/src/main/scala/scala/meta/internal/pc/EmptySymbolIndexer.scala b/pc/core/src/main/scala/scala/meta/internal/pc/EmptySymbolIndexer.scala new file mode 100644 index 00000000000..ef38dcb0d0b --- /dev/null +++ b/pc/core/src/main/scala/scala/meta/internal/pc/EmptySymbolIndexer.scala @@ -0,0 +1,8 @@ +package scala.meta.internal.pc + +import scala.meta.pc.SymbolIndexer +import scala.meta.pc.SymbolVisitor + +object EmptySymbolIndexer extends SymbolIndexer { + override def visit(symbol: String, visitor: SymbolVisitor): Unit = {} +} diff --git a/pc/core/src/main/scala/scala/meta/internal/pc/EmptySymbolSearch.scala b/pc/core/src/main/scala/scala/meta/internal/pc/EmptySymbolSearch.scala new file mode 100644 index 00000000000..75f936426aa --- /dev/null +++ b/pc/core/src/main/scala/scala/meta/internal/pc/EmptySymbolSearch.scala @@ -0,0 +1,14 @@ +package scala.meta.internal.pc + +import scala.meta.pc.SymbolSearch +import scala.meta.pc.SymbolSearchVisitor + +object EmptySymbolSearch extends SymbolSearch { + override def search( + query: String, + buildTargetIdentifier: String, + visitor: SymbolSearchVisitor + ): SymbolSearch.Result = { + SymbolSearch.Result.COMPLETE + } +} diff --git a/pc/core/src/main/scala/scala/meta/internal/pc/HoverProvider.scala b/pc/core/src/main/scala/scala/meta/internal/pc/HoverProvider.scala new file mode 100644 index 00000000000..6e47957c4f8 --- /dev/null +++ b/pc/core/src/main/scala/scala/meta/internal/pc/HoverProvider.scala @@ -0,0 +1,51 @@ +package scala.meta.internal.pc + +import org.eclipse.lsp4j.jsonrpc.messages.{Either => JEither} +import org.eclipse.lsp4j.Hover +import org.eclipse.lsp4j.MarkedString +import scala.collection.JavaConverters._ +import scala.meta.pc.OffsetParams + +class HoverProvider(compiler: MetalsGlobal) { + import compiler._ + def hover(params: OffsetParams): Option[Hover] = { + val unit = addCompilationUnit( + code = params.text(), + filename = params.filename(), + cursor = None + ) + val pos = unit.position(params.offset()) + val typedTree = compiler.typedTreeAt(pos) + for { + tpeName <- typeOfTree(typedTree) + } yield + new Hover( + List( + JEither.forRight[String, MarkedString]( + new MarkedString("scala", tpeName) + ) + ).asJava + ) + } + + private def typeOfTree(t: Tree): Option[String] = { + val stringOrTree = t match { + case t: DefDef => Right(t.symbol.asMethod.info.toLongString) + case t: ValDef if t.tpt != null => Left(t.tpt) + case t: ValDef if t.rhs != null => Left(t.rhs) + case x => Left(x) + } + + stringOrTree match { + case Right(string) => Some(string) + case Left(null) => None + case Left(tree) + if tree.tpe != null && + tree.tpe != NoType && + !tree.tpe.isErroneous => + Some(tree.tpe.widen.toString) + case _ => None + } + + } +} diff --git a/pc/core/src/main/scala/scala/meta/internal/pc/IdentifierComparator.scala b/pc/core/src/main/scala/scala/meta/internal/pc/IdentifierComparator.scala new file mode 100644 index 00000000000..9cfc6e6d805 --- /dev/null +++ b/pc/core/src/main/scala/scala/meta/internal/pc/IdentifierComparator.scala @@ -0,0 +1,48 @@ +package scala.meta.internal.pc + +import java.util.Comparator + +/** + * A comparator for identifier like "Predef" or "Function10". + * + * Differences from the default string comparator: + * - works with CharSequences like compiler `Name` + * - orders numbers by their numerical value instead of lexicographical + * - Good: `Function1`, `Function2`, `Function10` + * - Bad: `Function1`, `Function10`, `Function2` + */ +object IdentifierComparator extends Comparator[CharSequence] { + override def compare(o1: CharSequence, o2: CharSequence): Int = { + val len = math.min(o1.length(), o2.length()) + var i = 0 + while (i < len) { + val a = o1.charAt(i) + val b = o2.charAt(i) + if (a.isDigit && b.isDigit) { + val byDigit = Integer.compare(toDigit(o1, i), toDigit(o2, i)) + if (byDigit != 0) return byDigit + else { + i = seekNonDigit(o1, i) + } + } else { + val result = Character.compare(a, b) + if (result != 0) { + return result + } + i += 1 + } + } + Integer.compare(o1.length(), o2.length()) + } + private def seekNonDigit(cs: CharSequence, i: Int): Int = { + var curr = i + while (curr < cs.length() && cs.charAt(curr).isDigit) { + curr += 1 + } + curr + } + private def toDigit(cs: CharSequence, i: Int): Int = { + val digit = cs.subSequence(i, seekNonDigit(cs, i)) + Integer.parseUnsignedInt(digit.toString) + } +} diff --git a/pc/core/src/main/scala/scala/meta/internal/pc/MemberOrdering.scala b/pc/core/src/main/scala/scala/meta/internal/pc/MemberOrdering.scala new file mode 100644 index 00000000000..46e2bac8f2f --- /dev/null +++ b/pc/core/src/main/scala/scala/meta/internal/pc/MemberOrdering.scala @@ -0,0 +1,14 @@ +package scala.meta.internal.pc + +object MemberOrdering { + val IsWorkspaceSymbol = 1 << 30 + val IsInheritedBaseMethod = 1 << 29 + val IsImplicitConversion = 1 << 28 + val IsInherited = 1 << 27 + val IsNotLocalByBlock = 1 << 26 + val IsNotGetter = 1 << 25 + val IsPackage = 1 << 24 + val IsNotCaseAccessor = 1 << 23 + val IsNotPublic = 1 << 22 + val IsSynthetic = 1 << 21 +} diff --git a/pc/core/src/main/scala/scala/meta/internal/pc/MetalsGlobal.scala b/pc/core/src/main/scala/scala/meta/internal/pc/MetalsGlobal.scala new file mode 100644 index 00000000000..da9daeee4a6 --- /dev/null +++ b/pc/core/src/main/scala/scala/meta/internal/pc/MetalsGlobal.scala @@ -0,0 +1,370 @@ +package scala.meta.internal.pc + +import java.util.logging.Logger +import scala.collection.JavaConverters._ +import scala.collection.concurrent.TrieMap +import scala.collection.mutable +import scala.language.implicitConversions +import scala.meta.internal.semanticdb.scalac.SemanticdbOps +import scala.meta.pc +import scala.meta.pc.SymbolDocumentation +import scala.meta.pc.SymbolIndexer +import scala.meta.pc.SymbolSearch +import scala.meta.pc.SymbolVisitor +import scala.reflect.internal.{Flags => gf} +import scala.tools.nsc.Settings +import scala.tools.nsc.interactive.Global +import scala.tools.nsc.reporters.Reporter + +class MetalsGlobal( + settings: Settings, + reporter: Reporter, + val indexer: SymbolIndexer, + val search: SymbolSearch, + val buildTargetIdentifier: String, + val logger: Logger +) extends Global(settings, reporter) { compiler => + + def isDocs: Boolean = System.getProperty("metals.signature-help") != "no-docs" + def isJavaSymbol(sym: Symbol): Boolean = + !sym.hasPackageFlag && sym.isJava + + lazy val semanticdbOps: SemanticdbOps { + val global: compiler.type + } = new SemanticdbOps { + val global: compiler.type = compiler + } + + def semanticdbSymbol(symbol: Symbol): String = { + import semanticdbOps._ + symbol.toSemantic + } + + def printPretty(pos: Position): Unit = { + println(pretty(pos)) + } + def pretty(pos: Position): String = { + if (pos.isDefined) { + val lineCaret = + if (pos.isRange) { + val indent = " " * (pos.column - 1) + val caret = "^" * (pos.end - pos.start) + indent + caret + } else { + pos.lineCaret + } + pos.lineContent + "\n" + lineCaret + } else { + "" + } + } + + def treePos(tree: Tree): Position = { + if (tree.pos == null) { + NoPosition + } else if (tree.symbol != null && + tree.symbol.name.startsWith("x$") && + tree.symbol.isArtifact) { + tree.symbol.pos + } else { + tree.pos + } + } + + def methodInfoSymbol(symbol: Symbol): Symbol = + if (!symbol.isJava && symbol.isPrimaryConstructor) symbol.owner + else symbol + def rawMethodInfo(symbol: Symbol): Option[SymbolDocumentation] = { + for { + info <- methodInfos.get(semanticdbSymbol(methodInfoSymbol(symbol))) + if info != null + } yield info + } + def methodInfo(symbol: Symbol): Option[SymbolDocumentation] = { + val sym = compiler.semanticdbSymbol(methodInfoSymbol(symbol)) + methodInfos.get(sym) match { + case Some(null) => None + case s: Some[t] => s + case None => + index(sym) + val result = methodInfos.get(sym) + if (result.isEmpty) { + methodInfos.put(sym, null) + } + result + } + } + + def index(symbol: String): Unit = { + indexer.visit( + symbol, + new SymbolVisitor { + override def visitSymbol(method: SymbolDocumentation): Unit = { + methodInfos(method.symbol()) = method + } + } + ) + } + + // The following pattern match is an adaptation of this pattern match: + // https://github.com/scalameta/scalameta/blob/dc639c83f1c26627c39aef9bfb3dae779ecdb237/semanticdb/scalac/library/src/main/scala/scala/meta/internal/semanticdb/scalac/TypeOps.scala + def shortType(longType: Type, history: ShortenedNames): Type = { + def loop(tpe: Type, name: Option[Name]): Type = tpe match { + case TypeRef(pre, sym, args) => + TypeRef( + loop(pre, Some(sym.name)), + sym, + args.map(arg => loop(arg, name)) + ) + case SingleType(pre, sym) => + if (sym.hasPackageFlag) { + if (history.tryShortenName(name, sym)) NoPrefix + else tpe + } else { + SingleType(loop(pre, Some(sym.name)), sym) + } + case ThisType(sym) => + if (sym.hasPackageFlag) { + if (history.tryShortenName(name, sym)) NoPrefix + else tpe + } else { + TypeRef(NoPrefix, sym, Nil) + } + case ConstantType(Constant(sym: TermSymbol)) + if sym.hasFlag(gf.JAVA_ENUM) => + loop(SingleType(sym.owner.thisPrefix, sym), None) + case ConstantType(Constant(tpe: Type)) => + ConstantType(Constant(loop(tpe, None))) + case SuperType(thistpe, supertpe) => + SuperType(loop(thistpe, None), loop(supertpe, None)) + case RefinedType(parents, decls) => + RefinedType(parents.map(parent => loop(parent, None)), decls) + case AnnotatedType(annotations, underlying) => + AnnotatedType(annotations, loop(underlying, None)) + case ExistentialType(quantified, underlying) => + ExistentialType(quantified, loop(underlying, None)) + case PolyType(tparams, resultType) => + PolyType(tparams, resultType.map(t => loop(t, None))) + case NullaryMethodType(resultType) => + loop(resultType, None) + case TypeBounds(lo, hi) => + TypeBounds(loop(lo, None), loop(hi, None)) + case t => t + } + longType match { + case ThisType(_) => longType + case _ => loop(longType, None) + } + } + + def metalsToLongString(tpe: Type, history: ShortenedNames): String = { + shortType(tpe, history).toLongString + } + +// def metalsToShortString(tpe: Type): String = { +// val sb = new StringBuilder() +// def loop(t: Type): Unit = {} +// sb.toString() +// } + + val methodInfos = TrieMap.empty[String, SymbolDocumentation] + + // Only needed for 2.11 where `Name` doesn't extend CharSequence. + implicit def nameToCharSequence(name: Name): CharSequence = + name.toString + + class ShortenedNames(history: mutable.Map[Name, Symbol] = mutable.Map.empty) { + def tryShortenName(name: Option[Name], sym: Symbol): Boolean = + name match { + case Some(n) => + history.get(n) match { + case Some(other) => + if (other == sym) true + else false + case _ => + history(n) = sym + true + } + case _ => + false + } + } + + def inverseSemanticdbSymbol(symbol: String): Symbol = { + import scala.meta.internal.semanticdb.Scala._ + if (!symbol.isGlobal) return NoSymbol + def loop(s: String): List[Symbol] = { + if (s.isNone || s.isRootPackage) rootMirror.RootPackage :: Nil + else if (s.isEmptyPackage) rootMirror.EmptyPackage :: Nil + else { + val (desc, parent) = DescriptorParser(s) + val parentSymbol = loop(parent) + def tryMember(sym: Symbol): List[Symbol] = + sym match { + case NoSymbol => + Nil + case owner => + desc match { + case Descriptor.None => + Nil + case Descriptor.Type(value) => + val member = owner.info.member(TypeName(value)) :: Nil + if (sym.isJava) owner.info.member(TermName(value)) :: member + else member + case Descriptor.Term(value) => + owner.info.member(TermName(value)) :: Nil + case Descriptor.Package(value) => + owner.info.member(TermName(value)) :: Nil + case Descriptor.Parameter(value) => + owner.paramss.flatten.filter(_.name.containsName(value)) + case Descriptor.TypeParameter(value) => + owner.typeParams.filter(_.name.containsName(value)) + case Descriptor.Method(value, _) => + owner.info + .member(TermName(value)) + .alternatives + .iterator + .filter(sym => semanticdbSymbol(sym) == s) + .toList + } + } + parentSymbol.flatMap(tryMember) + } + } + loop(symbol) match { + case head :: _ => + head + case Nil => + NoSymbol + } + } + + class SignaturePrinter( + gsym: Symbol, + shortenedNames: ShortenedNames, + gtpe: Type, + includeDocs: Boolean + ) { + private val info = + if (includeDocs || (gsym.isMethod && isJavaSymbol(gsym))) { + methodInfo(gsym) + } else { + rawMethodInfo(gsym) + } + private val infoParamsA: Seq[pc.SymbolDocumentation] = info match { + case Some(value) => + value.typeParameters().asScala ++ + value.parameters().asScala + case None => + IndexedSeq.empty + } + private val infoParams = + infoParamsA.lift + private val returnType = + metalsToLongString(gtpe.finalResultType, shortenedNames) + + def methodDocstring: String = { + if (isDocs) info.fold("")(_.docstring()) + else "" + } + def isTypeParameters: Boolean = gtpe.typeParams.nonEmpty + def isImplicit: Boolean = gtpe.paramss.lastOption match { + case Some(head :: _) => head.isImplicit + case _ => false + } + def mparamss: List[List[Symbol]] = + gtpe.typeParams match { + case Nil => gtpe.paramss + case tparams => tparams :: gtpe.paramss + } + def defaultMethodSignature: String = { + var i = 0 + val paramss = gtpe.typeParams match { + case Nil => gtpe.paramss + case tparams => tparams :: gtpe.paramss + } + val params = paramss.iterator.map { params => + val labels = params.iterator.map { param => + val result = paramLabel(param, i) + i += 1 + result + } + labels + } + methodSignature(params, name = "") + } + + def methodSignature( + paramLabels: Iterator[Iterator[String]], + name: String = gsym.nameString + ): String = { + paramLabels + .zip(mparamss.iterator) + .map { + case (params, syms) => + paramsKind(syms) match { + case Params.TypeParameterKind => + params.mkString("[", ", ", "]") + case Params.NormalKind => + params.mkString("(", ", ", ")") + case Params.ImplicitKind => + params.mkString("(implicit ", ", ", ")") + } + } + .mkString(name, "", s": ${returnType}") + } + def paramsKind(syms: List[Symbol]): Params.Kind = { + syms match { + case head :: _ => + if (head.isType) Params.TypeParameterKind + else if (head.isImplicit) Params.ImplicitKind + else Params.NormalKind + case Nil => Params.NormalKind + } + } + def paramDocstring(paramIndex: Int): String = { + if (isDocs) infoParams(paramIndex).fold("")(_.docstring()) + else "" + } + def paramLabel(param: Symbol, index: Int): String = { + val paramTypeString = metalsToLongString(param.info, shortenedNames) + val name = infoParams(index) match { + case Some(value) => value.name() + case None => param.nameString + } + if (param.isTypeParameter) { + name + paramTypeString + } else { + val default = + if (param.isParamWithDefault) { + val defaultValue = infoParams(index).map(_.defaultValue()) match { + case Some(value) if !value.isEmpty => value + case _ => "{}" + } + s" = $defaultValue" + } else { + "" + } + s"$name: ${paramTypeString}$default" + } + } + } + + def addCompilationUnit( + code: String, + filename: String, + cursor: Option[Int], + cursorName: String = "_CURSOR_" + ): RichCompilationUnit = { + val codeWithCursor = cursor match { + case Some(offset) => + code.take(offset) + cursorName + code.drop(offset) + case _ => code + } + val unit = newCompilationUnit(codeWithCursor, filename) + val richUnit = new RichCompilationUnit(unit.source) + unitOfFile(richUnit.source.file) = richUnit + richUnit + } + +} diff --git a/pc/core/src/main/scala/scala/meta/internal/pc/Params.scala b/pc/core/src/main/scala/scala/meta/internal/pc/Params.scala new file mode 100644 index 00000000000..e79fb172902 --- /dev/null +++ b/pc/core/src/main/scala/scala/meta/internal/pc/Params.scala @@ -0,0 +1,13 @@ +package scala.meta.internal.pc + +case class Params( + labels: Seq[String], + kind: Params.Kind +) + +object Params { + sealed abstract class Kind + case object TypeParameterKind extends Kind + case object NormalKind extends Kind + case object ImplicitKind extends Kind +} diff --git a/pc/core/src/main/scala/scala/meta/internal/pc/PresentationCompilerParams.scala b/pc/core/src/main/scala/scala/meta/internal/pc/PresentationCompilerParams.scala new file mode 100644 index 00000000000..a551f331fdf --- /dev/null +++ b/pc/core/src/main/scala/scala/meta/internal/pc/PresentationCompilerParams.scala @@ -0,0 +1,9 @@ +package scala.meta.internal.pc + +import scala.meta.pc.SymbolIndexer +import scala.meta.pc.SymbolSearch + +case class PresentationCompilerParams( + search: SymbolSearch, + indexer: SymbolIndexer +) diff --git a/pc/core/src/main/scala/scala/meta/internal/pc/ScalaPresentationCompiler.scala b/pc/core/src/main/scala/scala/meta/internal/pc/ScalaPresentationCompiler.scala new file mode 100644 index 00000000000..d1e45c45751 --- /dev/null +++ b/pc/core/src/main/scala/scala/meta/internal/pc/ScalaPresentationCompiler.scala @@ -0,0 +1,138 @@ +package scala.meta.internal.pc + +import java.io.File +import java.nio.file.Path +import java.util +import java.util.logging.Logger +import org.eclipse.lsp4j.CompletionItem +import org.eclipse.lsp4j.Hover +import org.eclipse.lsp4j.SignatureHelp +import scala.collection.JavaConverters._ +import scala.meta.pc.CompletionItems +import scala.meta.pc.CompletionItems.LookupKind +import scala.meta.pc.OffsetParams +import scala.meta.pc.PresentationCompiler +import scala.meta.pc.SymbolIndexer +import scala.meta.pc.SymbolSearch +import scala.reflect.io.VirtualDirectory +import scala.tools.nsc.Settings +import scala.tools.nsc.reporters.StoreReporter + +case class ScalaPresentationCompiler( + buildTargetIdentifier: String = "", + classpath: Seq[Path] = Nil, + options: Seq[String] = Nil, + indexer: SymbolIndexer = EmptySymbolIndexer, + search: SymbolSearch = EmptySymbolSearch +) extends PresentationCompiler { + val logger = Logger.getLogger(classOf[ScalaPresentationCompiler].getName) + override def withIndexer(indexer: SymbolIndexer): PresentationCompiler = + copy(indexer = indexer) + override def withSearch(search: SymbolSearch): PresentationCompiler = + copy(search = search) + def this() = this(buildTargetIdentifier = "") + + val access = new CompilerAccess(logger, () => newCompiler()) + override def shutdown(): Unit = { + access.shutdown() + } + + override def newInstance( + buildTargetIdentifier: String, + classpath: util.List[Path], + options: util.List[String] + ): PresentationCompiler = { + copy( + buildTargetIdentifier = buildTargetIdentifier, + classpath = classpath.asScala, + options = options.asScala + ) + } + + override def diagnostics(): util.List[String] = { + access.reporter + .asInstanceOf[StoreReporter] + .infos + .iterator + .map( + info => + new StringBuilder() + .append(info.pos.source.file.path) + .append(":") + .append(info.pos.column) + .append(" ") + .append(info.msg) + .append("\n") + .append(info.pos.lineContent) + .append("\n") + .append(info.pos.lineCaret) + .toString + ) + .filterNot(_.contains("_CURSOR_")) + .toList + .asJava + } + + def emptyCompletion = new CompletionItems(LookupKind.None, Nil.asJava) + override def complete(params: OffsetParams): CompletionItems = + access.withCompiler(emptyCompletion) { global => + new CompletionProvider(global, params).completions() + } + override def completionItemResolve( + item: CompletionItem, + symbol: String + ): CompletionItem = + access.withCompiler(item) { global => + new CompletionItemResolver(global).resolve(item, symbol) + } + + override def hover(params: OffsetParams): Hover = + access.withCompiler(new Hover()) { global => + new HoverProvider(global).hover(params).orNull + } + + override def signatureHelp(params: OffsetParams): SignatureHelp = + access.withCompiler(new SignatureHelp()) { global => + new SignatureHelpProvider(global, indexer).signatureHelp(params) + } + + override def symbol(params: OffsetParams): String = { + access.withCompiler("") { global => + val unit = global.addCompilationUnit( + code = params.text(), + filename = params.filename(), + cursor = None + ) + val pos = unit.position(params.offset()) + global.typedTreeAt(pos).symbol.fullName + } + } + def newCompiler(): MetalsGlobal = { + val classpath = this.classpath.mkString(File.pathSeparator) + val options = this.options.iterator.filterNot { o => + o.contains("semanticdb") || + o.contains("scalajs") + }.toList + val vd = new VirtualDirectory("(memory)", None) + val settings = new Settings + settings.outputDirs.setSingleOutput(vd) + settings.classpath.value = classpath + settings.YpresentationAnyThread.value = true + // settings.YcachePluginClassLoader.value = "last-modified" + if (classpath.isEmpty) { + settings.usejavacp.value = true + } + val (isSuccess, unprocessed) = + settings.processArguments(options, processAll = true) + require(isSuccess, unprocessed) + require(unprocessed.isEmpty, unprocessed) + new MetalsGlobal( + settings, + new StoreReporter, + indexer, + search, + buildTargetIdentifier, + logger + ) + } +} diff --git a/pc/core/src/main/scala/scala/meta/internal/pc/SignatureHelpProvider.scala b/pc/core/src/main/scala/scala/meta/internal/pc/SignatureHelpProvider.scala new file mode 100644 index 00000000000..811747cebc2 --- /dev/null +++ b/pc/core/src/main/scala/scala/meta/internal/pc/SignatureHelpProvider.scala @@ -0,0 +1,436 @@ +package scala.meta.internal.pc + +import org.eclipse.lsp4j.MarkupContent +import org.eclipse.lsp4j.ParameterInformation +import org.eclipse.lsp4j.SignatureHelp +import org.eclipse.lsp4j.SignatureInformation +import scala.collection.JavaConverters._ +import scala.meta.pc.OffsetParams +import scala.meta.pc.SymbolIndexer + +class SignatureHelpProvider( + val compiler: MetalsGlobal, + indexer: SymbolIndexer +) { + import compiler._ + + def signatureHelp( + params: OffsetParams + ): SignatureHelp = { + val unit = addCompilationUnit( + code = params.text(), + filename = params.filename(), + cursor = cursor(params.offset(), params.text()) + ) + val pos = unit.position(params.offset()) + // TODO(olafur) validate we need `typeCheck` and `typedTreeAtPos` is not sufficient. + compiler.typeCheck(unit) + EnclosingMethodCall + .fromPosition(pos, unit.body) + .map(toSignatureHelp) + .getOrElse(new SignatureHelp()) + } + + case class Arg( + tree: Tree, + paramsIndex: Int, + paramIndex: Int + ) { + def matches(param: Symbol, i: Int, j: Int): Boolean = + paramsIndex == i && { + paramIndex == j || + (param.tpe != null && paramIndex > j && + definitions.isRepeatedParamType(param.tpe)) + } + } + + // A method call like `function[A, B](a, b)(c, d)` + case class MethodCall( + qual: Tree, + symbol: Symbol, + tparams: List[Tree], + argss: List[List[Tree]] + ) { + def qualTpe: Type = { + val fromOverload = qual.tpe match { + case OverloadedType(pre, alts) => + val toFind = nonOverload + pre.memberType(alts.find(_ == toFind).getOrElse(alts.head)) + case tpe => tpe + } + if (fromOverload == null) symbol.info + else fromOverload + } + def alternatives: List[Symbol] = symbol match { + case o: ModuleSymbol => + o.info.member(compiler.nme.apply).alternatives + case o: ClassSymbol => + o.info.member(compiler.termNames.CONSTRUCTOR).alternatives + case m: MethodSymbol => + m.owner.info.member(symbol.name).alternatives + case _ => + symbol.alternatives + } + def nonOverload: Symbol = + if (!symbol.isOverloaded) symbol + else alternatives.headOption.getOrElse(symbol) + def gparamss: List[List[Symbol]] = { + if (symbol.typeParams.isEmpty) nonOverload.paramLists + else nonOverload.typeParams :: nonOverload.paramLists + } + def all: List[List[Tree]] = + if (tparams.isEmpty) argss + else tparams :: argss + def paramTree(i: Int, j: Int): List[Tree] = + all.lift(i).flatMap(_.lift(j)).toList + def margss: List[List[Tree]] = { + all + } + } + object MethodCall { + def unapply(tree: Tree): Option[MethodCall] = { + tree match { + case TypeApply(qual, targs) => + Some(MethodCall(qual, treeSymbol(tree), targs, Nil)) + case Apply(qual, args) => + var tparams: List[Tree] = Nil + def loop( + t: Tree, + paramss: List[List[Symbol]], + accum: List[List[Tree]] + ): (Tree, List[List[Tree]]) = { + (t, paramss) match { + case (Apply(qual0, args0), _ :: tail) => + loop(qual0, tail, args0 :: accum) + case (TypeApply(qual0, args0), _) => + tparams = args0 + (qual0, accum) + case _ => + (t, accum) + } + } + val symbol = treeSymbol(tree) + Option(symbol.info).map { info => + val (refQual, argss) = info.paramss match { + case _ :: tail => + loop(qual, tail, args :: Nil) + case _ => + (qual, args :: Nil) + } + MethodCall(refQual, symbol, tparams, argss) + } + case _ => None + } + } + } + + // Returns a cursor offset only if the cursor is between two delimiters + // Insert cursor: + // foo(@@) + // foo(@@,) + // foo(1,@@) + // Don't insert cursor: + // foo(a@@) + def cursor(offset: Int, text: String): Option[Int] = { + if (offset >= text.length) return None + var leadingDelimiter = offset - 1 + while (leadingDelimiter > 0 && text.charAt(leadingDelimiter).isWhitespace) { + leadingDelimiter -= 1 + } + if (leadingDelimiter >= 0) { + text.charAt(leadingDelimiter) match { + case '(' | '[' | ',' => + var trailingDelimiter = offset + while (trailingDelimiter < text.length && + text.charAt(trailingDelimiter).isWhitespace) { + trailingDelimiter += 1 + } + if (trailingDelimiter < text.length) { + text.charAt(trailingDelimiter) match { + case ')' | ']' | ',' => + Some(offset) + case _ => + None + } + } else { + None + } + + case _ => + None + } + } else { + None + } + } + + // Extractor for both term and type applications like `foo(1)` and foo[T]` + object TreeApply { + def unapply(tree: Tree): Option[(Tree, List[Tree])] = tree match { + case TypeApply(qual, args) => Some(qual -> args) + case Apply(qual, args) => Some(qual -> args) + case _ => None + } + } + + case class EnclosingMethodCall( + call: MethodCall, + activeArg: Arg + ) { + def alternatives: List[Symbol] = call.alternatives + def symbol: Symbol = call.symbol + } + + object EnclosingMethodCall { + def fromPosition(pos: Position, body: Tree): Option[EnclosingMethodCall] = + new MethodCallTraverser(pos).fromTree(body) + } + + // A traverser that finds the nearest enclosing method call for a given position. + class MethodCallTraverser(pos: Position) extends Traverser { + private var activeCallsite: MethodCall = _ + private var activeArg: Arg = _ + def fromTree(body: Tree): Option[EnclosingMethodCall] = { + traverse(body) + if (activeCallsite == null) { + None + } else { + if (activeCallsite.alternatives.isEmpty) { + None + } else { + Some( + EnclosingMethodCall( + activeCallsite, + activeArg + ) + ) + } + } + } + + def toVisit(tree: Tree): Option[Tree] = { + tree match { + // Special case: a method call with named arguments like `foo(a = 1, b = 2)` gets desugared into the following: + // { + // val x$1 = 1 + // val x$2 = 2 + // foo(x$1, x$2) + // } + // In this case, the `foo(x$1, x$2)` has a transparent position, which we don't visit by default, so we + // make an exception and visit it nevertheless. + case Block(stats, expr) + if tree.symbol == null && + stats.forall { stat => + stat.symbol != null && stat.symbol.isArtifact + } => + Some(expr) + case _ => + if (tree.pos.isTransparent) None + else Some(tree) + } + } + override def traverse(tree: compiler.Tree): Unit = { + toVisit(tree) match { + case Some(value) => + visit(value) + case None => + } + } + def visit(tree: Tree): Unit = tree match { + case MethodCall(call) => + var start = call.qual.pos.end + for { + (args, i) <- call.margss.zipWithIndex + (arg, j) <- args.zipWithIndex + } { + val realPos = treePos(arg) + if (realPos.isRange) { + // NOTE(olafur): We don't use `arg.pos` because it does not enclose the full + // range from the previous argument. Instead, we use + val end = arg.pos.end + val isEnclosed = start <= pos.start && pos.end <= end + if (isEnclosed) { + activeCallsite = call + activeArg = Arg(arg, i, j) + } + start = end + } + traverse(arg) + } + super.traverse(call.qual) + case _ => + super.traverse(tree) + } + } + + // Same as `tree.symbol` but tries to recover from type errors + // by using the completions API. + def treeSymbol(tree0: Tree): Symbol = { + if (tree0.symbol != NoSymbol && !tree0.symbol.isError) { + tree0.symbol + } else { + def applyQualifier(tree: Tree): Option[RefTree] = tree match { + case Select(New(t: RefTree), _) => Some(t) + case t: RefTree => Some(t) + case TreeApply(qual, _) => applyQualifier(qual) + case _ => + None + } + val completionFallback = for { + qual <- applyQualifier(tree0) + completion <- completionsAt(qual.pos.focus).results + .find(_.sym.name == qual.name) + if !completion.sym.isErroneous + } yield completion.sym + completionFallback + .orElse { + val qual = tree0 match { + case TreeApply(q @ Select(New(_), _), _) => q + case _ => tree0 + } + Option(compiler.typedTreeAt(qual.pos).symbol) + } + .getOrElse(NoSymbol) + } + } + + case class ParamIndex(j: Int, param: Symbol) + + def toSignatureHelp(t: EnclosingMethodCall): SignatureHelp = { + val activeParent = t.call.nonOverload + var activeSignature: Integer = null + var activeParameter: Integer = null + val shortenedNames = new ShortenedNames() + val infos = t.alternatives.zipWithIndex.collect { + case (method: MethodSymbol, i) => + val isActiveSignature = method == activeParent + val tpe = + if (isActiveSignature) t.call.qualTpe + else method.info + val paramss: List[List[Symbol]] = + if (!isActiveSignature) { + mparamss(tpe) + } else { + activeSignature = i + val paramss = this.mparamss(tpe) + val gparamss = for { + (params, i) <- paramss.zipWithIndex + (param, j) <- params.zipWithIndex + } yield (param, i, j) + val activeIndex = gparamss.zipWithIndex.collectFirst { + case ((param, i, j), flat) if t.activeArg.matches(param, i, j) => + flat + } + activeIndex match { + case Some(value) => + val paramCount = math.max(0, gparamss.length - 1) + activeParameter = math.min(value, paramCount) + case _ => + } + paramss + } + toSignatureInformation( + t, + method, + tpe, + paramss, + isActiveSignature, + shortenedNames + ) + } + new SignatureHelp(infos.asJava, activeSignature, activeParameter) + } + + def mparamss(method: Type): List[List[compiler.Symbol]] = { + if (method.typeParams.isEmpty) method.paramLists + else method.typeParams :: method.paramLists + } + + def toSignatureInformation( + t: EnclosingMethodCall, + method: MethodSymbol, + methodType: Type, + mparamss: List[List[Symbol]], + isActiveSignature: Boolean, + shortenedNames: ShortenedNames + ): SignatureInformation = { + def arg(i: Int, j: Int): Option[Tree] = + t.call.all.lift(i).flatMap(_.lift(j)) + var k = 0 + val printer = new SignaturePrinter( + method, + shortenedNames, + methodType, + includeDocs = true + ) + val paramLabels = mparamss.zipWithIndex.map { + case (params, i) => + val byName: Map[Name, Int] = + if (isActiveSignature) { + (for { + args <- t.call.all.lift(i).toList + (AssignOrNamedArg(Ident(arg), _), argIndex) <- args.zipWithIndex + } yield arg -> argIndex).toMap + } else { + Map.empty[Name, Int] + } + def byNamedArgumentPosition(symbol: Symbol): Int = { + byName.getOrElse(symbol.name, Int.MaxValue) + } + val sortedByName = params.zipWithIndex + .sortBy { + case (sym, pos) => + (byNamedArgumentPosition(sym), pos) + } + .map { + case (sym, _) => sym + } + val isByNamedOrdered = sortedByName.zip(params).exists { + case (a, b) => a != b + } + sortedByName.zipWithIndex.map { + case (param, j) => + val index = k + k += 1 + val label = printer.paramLabel(param, index) + val docstring = printer.paramDocstring(index) + val byNameLabel = + if (isByNamedOrdered) s"<$label>" + else label + val lparam = new ParameterInformation(byNameLabel, docstring) + // TODO(olafur): use LSP 3.14.0 ParameterInformation.label offsets instead of strings + // once this issue is fixed https://github.com/eclipse/lsp4j/issues/300 + if (isActiveSignature && t.activeArg.matches(param, i, j)) { + arg(i, j) match { + case Some(a) if a.tpe != null && !a.tpe.isErroneous => + val tpe = metalsToLongString(a.tpe.widen, shortenedNames) + val typeString = + if (tpe.endsWith("=> Null")) { + tpe.stripSuffix("=> Null") + "=> ???" + } else { + tpe + } + if (!lparam.getLabel.endsWith(typeString)) { + val content = new MarkupContent() + content.setKind("markdown") + content.setValue( + "```scala\n" + typeString + "\n```\n" + docstring + ) + lparam.setDocumentation(content) + } + case _ => + } + } + lparam + } + } + new SignatureInformation( + printer.methodSignature( + paramLabels.iterator.map(_.iterator.map(_.getLabel)) + ), + printer.methodDocstring, + paramLabels.iterator.flatten.toSeq.asJava + ) + } + +} diff --git a/pc/core/src/main/scala/scala/meta/internal/pc/SymbolSearchCandidate.scala b/pc/core/src/main/scala/scala/meta/internal/pc/SymbolSearchCandidate.scala new file mode 100644 index 00000000000..54c681c9693 --- /dev/null +++ b/pc/core/src/main/scala/scala/meta/internal/pc/SymbolSearchCandidate.scala @@ -0,0 +1,76 @@ +package scala.meta.internal.pc + +import scala.meta.internal.metals.Fuzzy +import scala.meta.internal.semanticdb.Scala._ + +sealed abstract class SymbolSearchCandidate { + final def nameLength(query: String): Int = Fuzzy.nameLength(nameString) + final def innerClassDepth: Int = + SymbolSearchCandidate.characterCount(nameString, termCharacter) + def termCharacter: Char + def nameString: String + def packageString: String +} +object SymbolSearchCandidate { + final case class Classfile(pkg: String, filename: String) + extends SymbolSearchCandidate { + def nameString: String = filename + override def packageString: String = pkg + override def termCharacter: Char = '$' + } + final case class Workspace(symbol: String) extends SymbolSearchCandidate { + def nameString: String = symbol + override def packageString: String = { + def loop(s: String): String = { + if (s.isNone) s + else if (s.isPackage) s + else loop(s.owner) + } + loop(symbol) + } + override def termCharacter: Char = '.' + } + class Comparator(query: String) + extends java.util.Comparator[SymbolSearchCandidate] { + override def compare( + o1: SymbolSearchCandidate, + o2: SymbolSearchCandidate + ): Int = { + val byNameLength = + Integer.compare(o1.nameLength(query), o2.nameLength(query)) + if (byNameLength != 0) byNameLength + else { + val byInnerclassDepth = + Integer.compare(o1.innerClassDepth, o2.innerClassDepth) + if (byInnerclassDepth != 0) byInnerclassDepth + else { + val byFirstQueryCharacter = Integer.compare( + o1.nameString.indexOf(query.head), + o2.nameString.indexOf(query.head) + ) + if (byFirstQueryCharacter != 0) { + byFirstQueryCharacter + } else { + val byPackageDepth = Integer.compare( + characterCount(o1.packageString, '/'), + characterCount(o2.packageString, '/') + ) + if (byPackageDepth != 0) byPackageDepth + else o1.nameString.compareTo(o2.nameString) + } + } + } + } + } + private def characterCount(string: CharSequence, ch: Char): Int = { + var i = 0 + var count = 0 + while (i < string.length) { + if (string.charAt(i) == ch) { + count += 1 + } + i += 1 + } + count + } +} diff --git a/pc/interfaces/src/main/java/scala/meta/pc/CompletionItems.java b/pc/interfaces/src/main/java/scala/meta/pc/CompletionItems.java new file mode 100644 index 00000000000..5645bd37a7d --- /dev/null +++ b/pc/interfaces/src/main/java/scala/meta/pc/CompletionItems.java @@ -0,0 +1,23 @@ +package scala.meta.pc; + +import org.eclipse.lsp4j.CompletionItem; +import org.eclipse.lsp4j.CompletionList; + +import java.util.List; + +public class CompletionItems extends CompletionList { + + public LookupKind lookupKind; + + public CompletionItems(LookupKind lookupKind, List items) { + super(); + this.lookupKind = lookupKind; + super.setItems(items); + } + + public enum LookupKind { + None, + Scope, + Type + } +} diff --git a/pc/interfaces/src/main/java/scala/meta/pc/OffsetParams.java b/pc/interfaces/src/main/java/scala/meta/pc/OffsetParams.java new file mode 100644 index 00000000000..b45f70acaed --- /dev/null +++ b/pc/interfaces/src/main/java/scala/meta/pc/OffsetParams.java @@ -0,0 +1,8 @@ +package scala.meta.pc; + +public interface OffsetParams { + String filename(); + String text(); + int offset(); + void checkCanceled(); +} diff --git a/pc/interfaces/src/main/java/scala/meta/pc/ParameterInformation.java b/pc/interfaces/src/main/java/scala/meta/pc/ParameterInformation.java new file mode 100644 index 00000000000..fbb9349b18d --- /dev/null +++ b/pc/interfaces/src/main/java/scala/meta/pc/ParameterInformation.java @@ -0,0 +1,7 @@ +package scala.meta.pc; + +public interface ParameterInformation { + String name(); + String docstring(); + String defaultValue(); +} diff --git a/pc/interfaces/src/main/java/scala/meta/pc/PresentationCompiler.java b/pc/interfaces/src/main/java/scala/meta/pc/PresentationCompiler.java new file mode 100644 index 00000000000..3fdf658f832 --- /dev/null +++ b/pc/interfaces/src/main/java/scala/meta/pc/PresentationCompiler.java @@ -0,0 +1,21 @@ +package scala.meta.pc; + +import org.eclipse.lsp4j.CompletionItem; +import org.eclipse.lsp4j.Hover; +import org.eclipse.lsp4j.SignatureHelp; + +import java.nio.file.Path; +import java.util.List; + +public abstract class PresentationCompiler { + public abstract void shutdown(); + public abstract String symbol(OffsetParams params); + public abstract SignatureHelp signatureHelp(OffsetParams params); + public abstract Hover hover(OffsetParams params); + public abstract CompletionItems complete(OffsetParams params); + public abstract CompletionItem completionItemResolve(CompletionItem item, String symbol); + public abstract List diagnostics(); + public abstract PresentationCompiler withIndexer(SymbolIndexer indexer); + public abstract PresentationCompiler withSearch(SymbolSearch search); + public abstract PresentationCompiler newInstance(String buildTargetIdentifier, List classpath, List options); +} \ No newline at end of file diff --git a/pc/interfaces/src/main/java/scala/meta/pc/SymbolDocumentation.java b/pc/interfaces/src/main/java/scala/meta/pc/SymbolDocumentation.java new file mode 100644 index 00000000000..b24cba9922d --- /dev/null +++ b/pc/interfaces/src/main/java/scala/meta/pc/SymbolDocumentation.java @@ -0,0 +1,12 @@ +package scala.meta.pc; + +import java.util.List; + +public interface SymbolDocumentation { + String symbol(); + String name(); + String docstring(); + String defaultValue(); + List typeParameters(); + List parameters(); +} diff --git a/pc/interfaces/src/main/java/scala/meta/pc/SymbolIndexer.java b/pc/interfaces/src/main/java/scala/meta/pc/SymbolIndexer.java new file mode 100644 index 00000000000..37b04ac9296 --- /dev/null +++ b/pc/interfaces/src/main/java/scala/meta/pc/SymbolIndexer.java @@ -0,0 +1,5 @@ +package scala.meta.pc; + +public interface SymbolIndexer { + void visit(String symbol, SymbolVisitor visitor); +} diff --git a/pc/interfaces/src/main/java/scala/meta/pc/SymbolSearch.java b/pc/interfaces/src/main/java/scala/meta/pc/SymbolSearch.java new file mode 100644 index 00000000000..f692e49bcad --- /dev/null +++ b/pc/interfaces/src/main/java/scala/meta/pc/SymbolSearch.java @@ -0,0 +1,11 @@ +package scala.meta.pc; + +public interface SymbolSearch { + Result search(String query, + String buildTargetIdentifier, + SymbolSearchVisitor visitor); + enum Result { + COMPLETE, + INCOMPLETE + } +} diff --git a/pc/interfaces/src/main/java/scala/meta/pc/SymbolSearchVisitor.java b/pc/interfaces/src/main/java/scala/meta/pc/SymbolSearchVisitor.java new file mode 100644 index 00000000000..865f2e8d4aa --- /dev/null +++ b/pc/interfaces/src/main/java/scala/meta/pc/SymbolSearchVisitor.java @@ -0,0 +1,18 @@ +package scala.meta.pc; + +import org.eclipse.lsp4j.Range; +import org.eclipse.lsp4j.SymbolKind; + +import java.nio.file.Path; + +public abstract class SymbolSearchVisitor { + + abstract public boolean shouldVisitPackage(String pkg); + abstract public int visitClassfile(String pkg, String filename); + + abstract public boolean shouldVisitPath(Path path); + abstract public int visitWorkspaceSymbol(Path path, String symbol, SymbolKind kind, Range range); + + abstract public boolean isCancelled(); + +} diff --git a/pc/interfaces/src/main/java/scala/meta/pc/SymbolVisitor.java b/pc/interfaces/src/main/java/scala/meta/pc/SymbolVisitor.java new file mode 100644 index 00000000000..e331301df5a --- /dev/null +++ b/pc/interfaces/src/main/java/scala/meta/pc/SymbolVisitor.java @@ -0,0 +1,5 @@ +package scala.meta.pc; + +public abstract class SymbolVisitor { + public abstract void visitSymbol(SymbolDocumentation symbol); +} diff --git a/test-workspace/src/main/scala/example/User.scala b/test-workspace/src/main/scala/example/User.scala index dc0973af7f8..2b0859e7818 100644 --- a/test-workspace/src/main/scala/example/User.scala +++ b/test-workspace/src/main/scala/example/User.scala @@ -1,12 +1,5 @@ -package example +package myexample -import java.nio.file.Paths - -case class User( - name: String, - age: Int -) - -object User { - val path = Paths.get("build.sbt") +object A { + Iterator.empty.map(e => e) } diff --git a/tests/cross/src/main/scala/tests/BaseCompletionSuite.scala b/tests/cross/src/main/scala/tests/BaseCompletionSuite.scala new file mode 100644 index 00000000000..866aedfa0b0 --- /dev/null +++ b/tests/cross/src/main/scala/tests/BaseCompletionSuite.scala @@ -0,0 +1,72 @@ +package tests + +import scala.collection.JavaConverters._ +import scala.meta.internal.metals.CompilerOffsetParams +import scala.meta.pc.CompletionItems +import scala.meta.internal.metals.PCEnrichments._ + +abstract class BaseCompletionSuite extends BasePCSuite { + + def checkLength( + name: String, + original: String, + expected: Int, + compat: Map[String, Int] = Map.empty + ): Unit = { + test(name) { + val (code, offset) = params(original) + val result = pc.complete(CompilerOffsetParams("A.scala", code, offset)) + assertEquals( + result.getItems.size(), + getExpected(expected, compat) + ) + } + } + + private def resolvedCompletions( + params: CompilerOffsetParams + ): CompletionItems = { + val result = pc.complete(params) + val newItems = result.getItems.asScala.map { item => + val symbol = item.data.get.symbol + pc.completionItemResolve(item, symbol) + } + result.setItems(newItems.asJava) + result + } + + def check( + name: String, + original: String, + expected: String, + includeDocs: Boolean = false, + includeCommitCharacter: Boolean = false, + compat: Map[String, String] = Map.empty + )(implicit filename: sourcecode.File, line: sourcecode.Line): Unit = { + test(name) { + val (code, offset) = params(original) + val result = + resolvedCompletions(CompilerOffsetParams("A.scala", code, offset)) + val out = new StringBuilder() + result.getItems.asScala.sortBy(_.getSortText).foreach { item => + val label = + if (item.getInsertText == null) item.getLabel else item.getInsertText + val commitCharacter = + if (includeCommitCharacter) + item.getCommitCharacters.asScala.mkString(" (commit: '", " ", "')") + else "" + val documentation = doc(item.getDocumentation) + if (includeDocs && documentation.nonEmpty) { + out.append("> ").append(documentation).append("\n") + } + out + .append(label) + .append(item.getDetail) + .append(commitCharacter) + .append("\n") + } + assertNoDiff(out.toString(), getExpected(expected, compat)) + } + } + +} diff --git a/tests/cross/src/main/scala/tests/BasePCSuite.scala b/tests/cross/src/main/scala/tests/BasePCSuite.scala new file mode 100644 index 00000000000..b01abd5ba89 --- /dev/null +++ b/tests/cross/src/main/scala/tests/BasePCSuite.scala @@ -0,0 +1,97 @@ +package tests + +import com.geirsson.coursiersmall.CoursierSmall +import com.geirsson.coursiersmall.Dependency +import com.geirsson.coursiersmall.Settings +import java.net.URLClassLoader +import java.nio.file.Path +import java.nio.file.Paths +import org.eclipse.lsp4j.MarkupContent +import org.eclipse.lsp4j.jsonrpc.messages.{Either => JEither} +import scala.collection.JavaConverters._ +import scala.meta.internal.metals.ClasspathSearch +import scala.meta.internal.metals.JdkSources +import scala.meta.internal.metals.MetalsSymbolIndexer +import scala.meta.internal.mtags.OnDemandSymbolIndex +import scala.meta.internal.pc.ScalaPresentationCompiler +import scala.meta.io.AbsolutePath +import scala.util.Properties + +abstract class BasePCSuite extends BaseSuite { + val scalaLibrary: Seq[Path] = + this.getClass.getClassLoader + .asInstanceOf[URLClassLoader] + .getURLs + .iterator + .filter(_.getPath.contains("scala-library")) + .map(url => Paths.get(url.toURI)) + .toSeq + def extraClasspath: List[Path] = Nil + val myclasspath: List[Path] = extraClasspath ++ scalaLibrary.toList + val index = OnDemandSymbolIndex() + val indexer = new MetalsSymbolIndexer(index) + val search = new SimpleSymbolSearch( + ClasspathSearch.fromClasspath(myclasspath, _ => 0) + ) + val pc = new ScalaPresentationCompiler() + .withIndexer(indexer) + .withSearch(search) + .newInstance("", myclasspath.asJava, Nil.asJava) + + def indexJDK(): Unit = { + index.addSourceJar(JdkSources().get) + } + def indexScalaLibrary(): Unit = { + val sources = CoursierSmall.fetch( + new Settings() + .withClassifiers(List("sources")) + .withDependencies( + List( + new Dependency( + "org.scala-lang", + "scala-library", + BuildInfoVersions.scala212 + ) + ) + ) + ) + sources.foreach { jar => + index.addSourceJar(AbsolutePath(jar)) + } + } + + override def beforeAll(): Unit = { + indexJDK() + indexScalaLibrary() + } + override def afterAll(): Unit = { + pc.shutdown() + } + def params(code: String): (String, Int) = { + val code2 = code.replaceAllLiterally("@@", "") + val offset = code.indexOf("@@") + if (offset < 0) { + fail("missing @@") + } + search.source = code2 + (code2, offset) + } + def doc(e: JEither[String, MarkupContent]): String = { + if (e == null) "" + else if (e.isLeft) { + " " + e.getLeft + } else { + " " + e.getRight.getValue + } + }.trim + private def scalaVersion: String = + Properties.versionNumberString + private def scalaBinary: String = + scalaVersion.split("\\.").take(2).mkString(".") + def getExpected[T](default: T, compat: Map[String, T]): T = { + compat + .get(scalaBinary) + .orElse(compat.get(scalaVersion)) + .getOrElse(default) + } +} diff --git a/tests/cross/src/main/scala/tests/BaseSignatureHelpSuite.scala b/tests/cross/src/main/scala/tests/BaseSignatureHelpSuite.scala new file mode 100644 index 00000000000..bb413111589 --- /dev/null +++ b/tests/cross/src/main/scala/tests/BaseSignatureHelpSuite.scala @@ -0,0 +1,68 @@ +package tests + +import scala.collection.JavaConverters._ +import scala.meta.internal.metals.CompilerOffsetParams + +abstract class BaseSignatureHelpSuite extends BasePCSuite { + def checkDoc(name: String, code: String, expected: String): Unit = { + check(name, code, expected, includeDocs = true) + } + def check( + name: String, + original: String, + expected: String, + includeDocs: Boolean = false, + compat: Map[String, String] = Map.empty + ): Unit = { + test(name) { + val (code, offset) = params(original) + val result = + pc.signatureHelp(CompilerOffsetParams("A.scala", code, offset)) + val out = new StringBuilder() + if (result != null) { + result.getSignatures.asScala.zipWithIndex.foreach { + case (signature, i) => + if (includeDocs) { + val sdoc = doc(signature.getDocumentation) + if (sdoc.nonEmpty) { + out.append(sdoc).append("\n") + } + } + out + .append(signature.getLabel) + .append("\n") + if (result.getActiveSignature == i && result.getActiveParameter != null) { + val param = signature.getParameters.get(result.getActiveParameter) + val column = signature.getLabel.indexOf(param.getLabel) + if (column < 0) { + fail(s"""invalid parameter label + | param.label : ${param.getLabel} + | signature.label: ${signature.getLabel} + |""".stripMargin) + } + val indent = " " * column + out + .append(indent) + .append("^" * param.getLabel.length) + .append("\n") + signature.getParameters.asScala.foreach { param => + val pdoc = doc(param.getDocumentation) + .stripPrefix("```scala\n") + .stripSuffix("\n```") + .replaceAllLiterally("\n```\n", " ") + if (includeDocs && pdoc.nonEmpty) { + out + .append(" @param ") + .append(param.getLabel.replaceFirst(":.*", "")) + .append(" ") + .append(pdoc) + .append("\n") + } + } + } + } + } + assertNoDiff(out.toString(), getExpected(expected, compat)) + } + } +} diff --git a/tests/cross/src/main/scala/tests/SimpleSymbolSearch.scala b/tests/cross/src/main/scala/tests/SimpleSymbolSearch.scala new file mode 100644 index 00000000000..1b3b389118a --- /dev/null +++ b/tests/cross/src/main/scala/tests/SimpleSymbolSearch.scala @@ -0,0 +1,28 @@ +package tests + +import java.nio.file.Paths +import scala.meta.inputs.Input +import scala.meta.internal.metals.ClasspathSearch +import scala.meta.internal.metals.SemanticdbDefinition +import scala.meta.internal.metals.WorkspaceSymbolQuery +import scala.meta.pc.SymbolSearch +import scala.meta.pc.SymbolSearchVisitor + +class SimpleSymbolSearch(classpath: ClasspathSearch) extends SymbolSearch { + var source = "" + val path = Paths.get("A.scala") + override def search( + textQuery: String, + buildTargetIdentifier: String, + visitor: SymbolSearchVisitor + ): SymbolSearch.Result = { + val query = WorkspaceSymbolQuery.exact(textQuery) + SemanticdbDefinition.foreach(Input.VirtualFile("A.scala", source)) { defn => + if (query.matches(defn.info)) { + val c = defn.toCached + visitor.visitWorkspaceSymbol(path, c.symbol, c.kind, c.range) + } + } + classpath.search(query, visitor) + } +} diff --git a/tests/cross/src/test/scala/tests/pc/CompletionDocSuite.scala b/tests/cross/src/test/scala/tests/pc/CompletionDocSuite.scala new file mode 100644 index 00000000000..81982ead984 --- /dev/null +++ b/tests/cross/src/test/scala/tests/pc/CompletionDocSuite.scala @@ -0,0 +1,262 @@ +package tests.pc + +import tests.BaseCompletionSuite + +object CompletionDocSuite extends BaseCompletionSuite { + + check( + "java", + """ + |object A { + | "".substrin@@ + |} + """.stripMargin, + """|substring(beginIndex: Int): String + |substring(beginIndex: Int, endIndex: Int): String + |""".stripMargin + ) + + check( + "java2", + """ + |object A { + | String.join@@ + |} + """.stripMargin, + """|join(delimiter: CharSequence, elements: CharSequence*): String + |join(delimiter: CharSequence, elements: Iterable[_ <: CharSequence]): String + |""".stripMargin + ) + + check( + "java3", + """ + |import scala.collection.JavaConverters._ + |object A { + | new java.util.HashMap[String, Int]().entrySet.asScala.foreach { entry => + | entry.setV@@ + | } + |} + """.stripMargin, + """|setValue(value: Int): Int + |""".stripMargin + ) + check( + "java4", + """ + |object A { + | java.util.Collections.singletonLis@@ + |} + """.stripMargin, + """|singletonList[T](o: T): List[T] + |""".stripMargin + ) + check( + "java5", + """ + |object A { + | java.util.OptionalInt@@ + |} + """.stripMargin, + """|> A container object which may or may not contain a {@code int} value. + |If a value is present, {@code isPresent()} will return {@code true} and + |{@code getAsInt()} will return the value. + | + |

Additional methods that depend on the presence or absence of a contained + |value are provided, such as {@link #orElse(int) orElse()} + |(return a default value if value not present) and + |{@link #ifPresent(java.util.function.IntConsumer) ifPresent()} (execute a block + |of code if the value is present). + | + |

This is a value-based + |class; use of identity-sensitive operations (including reference equality + |({@code ==}), identity hash code, or synchronization) on instances of + |{@code OptionalInt} may have unpredictable results and should be avoided. + |OptionalInt java.util + |""".stripMargin, + includeDocs = true + ) + check( + "scala", + """ + |object A { + | val source: io.Source = ??? + | source.reportWarn@@ + |} + """.stripMargin, + """|reportWarning(pos: Int, msg: String, out: PrintStream = Console.out): Unit + |""".stripMargin + ) + + check( + "scala1", + """ + |object A { + | List(1).iterator.sliding@@ + |} + """.stripMargin, + """|sliding[B >: Int](size: Int, step: Int = 1): Iterator[Int]#GroupedIterator[B] + |""".stripMargin + ) + + check( + "scala2", + """ + |object A { + | println@@ + |} + """.stripMargin, + """|> Prints a newline character on the default output. + |println(): Unit + |> Prints out an object to the default output, followed by a newline character. + |println(x: Any): Unit + |""".stripMargin, + includeDocs = true + ) + check( + "scala3", + """ + |object A { + | Predef@@ + |} + """.stripMargin, + """|DeprecatedPredef scala + |> The `Predef` object provides definitions that are accessible in all Scala + | compilation units without explicit qualification. + |Predef scala + |""".stripMargin, + includeDocs = true + ) + check( + "scala4", + """ + |object A { + | scala.collection.Iterator@@ + |} + """.stripMargin, + """|> Explicit instantiation of the `Iterator` trait to reduce class file size in subclasses. + |AbstractIterator scala.collection + |> Buffered iterators are iterators which provide a method `head` + | that inspects the next element without discarding it. + |BufferedIterator scala.collection + |> ### class Iterator + |Iterators are data structures that allow to iterate over a sequence + | of elements. + | + |### object Iterator + |The `Iterator` object provides various functions for creating specialized iterators. + |Iterator scala.collection + |""".stripMargin, + includeDocs = true + ) + check( + "scala5", + """ + |object A { + | scala.concurrent.ExecutionContext.Implicits.global@@ + |} + """.stripMargin, + """|> The implicit global `ExecutionContext`. + |global: ExecutionContext + |""".stripMargin, + includeDocs = true, + compat = Map( + "2.11" -> """|> The implicit global `ExecutionContext`. + |global: ExecutionContextExecutor + |""".stripMargin + ) + ) + check( + "scala6", + """ + |object A { + | scala.util.Try@@ + |} + """.stripMargin, + """|> The `Try` type represents a computation that may either result in an exception, or return a + |successfully computed value. + |Try scala.util + |""".stripMargin, + includeDocs = true + ) + check( + "scala7", + """ + |object A { + | scala.collection.mutable.StringBuilder@@ + |} + """.stripMargin, + """|> A builder for mutable sequence of characters. + |StringBuilder scala.collection.mutable + |""".stripMargin, + includeDocs = true + ) + check( + "scala8", + """ + |object A { + | scala.Vector@@ + |} + """.stripMargin, + """|> ### class Vector + |Vector is a general-purpose, immutable data structure. + | + |### object Vector + |Companion object to the Vector class + |Vector scala.collection.immutable + |""".stripMargin, + includeDocs = true + ) + check( + "scala9", + """ + |object A { + | new Catch@@ + |} + """.stripMargin, + """|> A container class for catch/finally logic. + |scala.util.control.Exception.Catch scala.util.control.Exception + |""".stripMargin, + includeDocs = true + ) + + check( + "scala10", + """ + |object A { + | scala.util.Failure@@ + |} + """.stripMargin, + """|Failure scala.util + |""".stripMargin, + includeDocs = true + ) + check( + "scala11", + """ + |object A { + | new scala.util.DynamicVariable@@ + |} + """.stripMargin, + """|> `DynamicVariables` provide a binding mechanism where the current + | value is found through dynamic scope, but where access to the + | variable itself is resolved through static scope. + |DynamicVariable scala.util + |""".stripMargin, + includeDocs = true + ) + + check( + "local", + """ + |object A { + | locally { + | val myNumbers = Vector(1) + | myNumbers@@ + | } + |} + """.stripMargin, + """|myNumbers: Vector[Int] + |""".stripMargin + ) +} diff --git a/tests/cross/src/test/scala/tests/pc/CompletionSuite.scala b/tests/cross/src/test/scala/tests/pc/CompletionSuite.scala new file mode 100644 index 00000000000..c1dab6d1721 --- /dev/null +++ b/tests/cross/src/test/scala/tests/pc/CompletionSuite.scala @@ -0,0 +1,455 @@ +package tests.pc + +import tests.BaseCompletionSuite + +object CompletionSuite extends BaseCompletionSuite { + override def beforeAll(): Unit = { + indexJDK() + } + + // The following method tests too many results so we only assert the total number of results + // to catch at least regressions. It's OK to update the expected number, but at least double check + // the output makes sense before doing so. + checkLength( + "open", + """ + |object Local { + | @@ + |}""".stripMargin, + 457, + compat = Map("2.11" -> 456) + ) + + check( + "scope", + """ + |object A { + | Lis@@ + |}""".stripMargin, + """|List scala.collection.immutable + |java.awt.List java.awt + |java.util.List java.util + |scala.collection.immutable.ListMap scala.collection.immutable + |scala.collection.mutable.ListMap scala.collection.mutable + |scala.collection.immutable.ListSet scala.collection.immutable + |java.awt.peer.ListPeer java.awt.peer + |org.w3c.dom.NameList org.w3c.dom + |org.w3c.dom.NodeList org.w3c.dom + |java.util.ArrayList java.util + |org.w3c.dom.stylesheets.MediaList org.w3c.dom.stylesheets + |""".stripMargin + ) + + check( + "member", + """ + |object A { + | List.emp@@ + |}""".stripMargin, + """ + |empty[A]: List[A] + |""".stripMargin + ) + + check( + "extension", + """ + |object A { + | "".stripSu@@ + |}""".stripMargin, + """|stripSuffix(suffix: String): String + |""".stripMargin + ) + + check( + "tparam", + """ + |class Foo[A] { + | def identity[B >: A](a: B): B = a + |} + |object Foo { + | new Foo[Int].ident@@ + |}""".stripMargin, + """|identity[B >: Int](a: B): B + |""".stripMargin + ) + + check( + "tparam1", + """ + |class Foo[A] { + | def identity(a: A): A = a + |} + |object Foo { + | new Foo[Int].ident@@ + |}""".stripMargin, + """|identity(a: Int): Int + |""".stripMargin + ) + check( + "tparam2", + """ + |object A { + | Map.empty[Int, String].getOrEl@@ + |}""".stripMargin, + """|getOrElse[V1 >: String](key: Int, default: => V1): V1 + |""".stripMargin, + compat = Map( + "2.11" -> "getOrElse[B1 >: String](key: Int, default: => B1): B1" + ) + ) + + check( + "cursor", + """ + |object A { + | val default = 1 + | def@@ + |}""".stripMargin, + """|default: Int + |""".stripMargin + ) + + check( + "dot", + """ + |object A { + | List.@@ + |}""".stripMargin, + """|apply[A](xs: A*): List[A] + |canBuildFrom[A]: CanBuildFrom[List.Coll,A,List[A]] + |empty[A]: List[A] + |newBuilder[A]: Builder[A,List[A]] + |GenericCanBuildFrom scala.collection.generic.GenTraversableFactory + |ReusableCBF: List.GenericCanBuildFrom[Nothing] + |concat[A](xss: Traversable[A]*): List[A] + |fill[A](n1: Int, n2: Int)(elem: => A): List[List[A]] + |fill[A](n1: Int, n2: Int, n3: Int)(elem: => A): List[List[List[A]]] + |fill[A](n1: Int, n2: Int, n3: Int, n4: Int)(elem: => A): List[List[List[List[A]]]] + |fill[A](n1: Int, n2: Int, n3: Int, n4: Int, n5: Int)(elem: => A): List[List[List[List[List[A]]]]] + |fill[A](n: Int)(elem: => A): List[A] + |iterate[A](start: A, len: Int)(f: A => A): List[A] + |range[T](start: T, end: T)(implicit evidence$1: Integral[T]): List[T] + |range[T](start: T, end: T, step: T)(implicit evidence$2: Integral[T]): List[T] + |tabulate[A](n1: Int, n2: Int)(f: (Int, Int) => A): List[List[A]] + |tabulate[A](n1: Int, n2: Int, n3: Int)(f: (Int, Int, Int) => A): List[List[List[A]]] + |tabulate[A](n1: Int, n2: Int, n3: Int, n4: Int)(f: (Int, Int, Int, Int) => A): List[List[List[List[A]]]] + |tabulate[A](n1: Int, n2: Int, n3: Int, n4: Int, n5: Int)(f: (Int, Int, Int, Int, Int) => A): List[List[List[List[List[A]]]]] + |tabulate[A](n: Int)(f: Int => A): List[A] + |unapplySeq[A](x: List[A]): Some[List[A]] + |->[B](y: B): (A, B) + |+(other: String): String + |ensuring(cond: A => Boolean): A + |ensuring(cond: A => Boolean, msg: => Any): A + |ensuring(cond: Boolean): A + |ensuring(cond: Boolean, msg: => Any): A + |formatted(fmtstr: String): String + |asInstanceOf[T0]: T0 + |equals(obj: Any): Boolean + |getClass(): Class[_] + |hashCode(): Int + |isInstanceOf[T0]: Boolean + |synchronized[T0](x$1: T0): T0 + |toString(): String + |""".stripMargin + ) + + check( + "implicit-class", + """ + |object A { + | implicit class XtensionMethod(a: Int) { + | def increment = a + 1 + | } + | Xtension@@ + |}""".stripMargin, + """|XtensionMethod(a: Int): A.XtensionMethod + |""".stripMargin + ) + + check( + "fuzzy", + """ + |object A { + | def userService = 1 + | uService@@ + |}""".stripMargin, + """|userService: Int + |""".stripMargin + ) + + check( + "fuzzy1", + """ + |object A { + | new PBuil@@ + |}""".stripMargin, + """|ProcessBuilder java.lang + |scala.sys.process.ProcessBuilder scala.sys.process + |java.security.cert.CertPathBuilder java.security.cert + |java.security.cert.CertPathBuilderSpi java.security.cert + |scala.sys.process.ProcessBuilderImpl scala.sys.process + |java.security.cert.CertPathBuilderResult java.security.cert + |java.security.cert.PKIXBuilderParameters java.security.cert + |java.security.cert.CertPathBuilderException java.security.cert + |java.security.cert.PKIXCertPathBuilderResult java.security.cert + |""".stripMargin + ) + + check( + "companion", + """ + |import scala.collection.concurrent._ + |object A { + | TrieMap@@ + |}""".stripMargin, + """|TrieMap scala.collection.concurrent + |scala.collection.parallel.mutable.ParTrieMap scala.collection.parallel.mutable + |scala.collection.immutable.HashMap.HashTrieMap scala.collection.immutable.HashMap + |scala.collection.parallel.mutable.ParTrieMapCombiner scala.collection.parallel.mutable + |scala.collection.parallel.mutable.ParTrieMapSplitter scala.collection.parallel.mutable + |scala.collection.concurrent.TrieMapSerializationEnd scala.collection.concurrent + |""".stripMargin + ) + + check( + "pkg", + """ + |import scala.collection.conc@@ + |""".stripMargin, + """|concurrent scala.collection + |""".stripMargin + ) + + check( + "import", + """ + |import JavaCon@@ + |""".stripMargin, + """|scala.collection.JavaConverters scala.collection + |scala.collection.JavaConversions scala.collection + |scala.concurrent.JavaConversions scala.concurrent + |scala.collection.convert.AsJavaConverters scala.collection.convert + |""".stripMargin, + compat = Map( + "2.11" -> """|scala.collection.JavaConverters scala.collection + |scala.collection.JavaConversions scala.collection + |scala.concurrent.JavaConversions scala.concurrent + |""".stripMargin + ) + ) + + check( + "import1", + """ + |import Paths@@ + |""".stripMargin, + """|java.nio.file.Paths java.nio.file + |""".stripMargin + ) + + check( + "import2", + """ + |import Catch@@ + |""".stripMargin, + """|scala.util.control.Exception.Catch scala.util.control.Exception + |""".stripMargin + ) + + check( + "import3", + """ + |import Path@@ + |""".stripMargin, + """|java.nio.file.Path java.nio.file + |java.nio.file.Paths java.nio.file + |java.awt.geom.Path2D java.awt.geom + |java.security.cert.CertPath java.security.cert + |java.awt.font.LayoutPath java.awt.font + |java.awt.geom.GeneralPath java.awt.geom + |java.nio.file.PathMatcher java.nio.file + |org.w3c.dom.xpath.XPathResult org.w3c.dom.xpath + |java.awt.geom.PathIterator java.awt.geom + |org.w3c.dom.xpath.XPathEvaluator org.w3c.dom.xpath + |org.w3c.dom.xpath.XPathException org.w3c.dom.xpath + |""".stripMargin + ) + + check( + "accessible", + """ + |package a + |import MetaData@@ + |""".stripMargin, + """|java.sql.DatabaseMetaData java.sql + |java.sql.ParameterMetaData java.sql + |java.sql.ResultSetMetaData java.sql + |""".stripMargin + ) + + check( + "source", + """ + |package a + |object Main { + | import Inner@@ + |} + |object Outer { + | class Inner + |} + |""".stripMargin, + """|a.Outer.Inner a.Outer + |""".stripMargin + ) + + check( + "duplicate", + """ + |package a + |object Main { + | import a.Outer.Inner + | import Inner@@ + |} + |object Outer { + | class Inner + |} + |""".stripMargin, + "" + ) + + check( + "duplicate2", + """ + |package a + |import java.nio.file.Files + | + |final class AbsolutePath private (val underlying: String) extends AnyVal { + | def syntax: String = Files@@ + |} + | + |object Outer { + | object Files + |} + |""".stripMargin, + """Files java.nio.file + |a.Outer.Files a.Outer + |""".stripMargin + ) + + check( + "commit", + """ + |package a + | + |object Main{ + | Map.emp@@ + |} + |""".stripMargin, + """|empty[K, V]: Map[K,V] (commit: '.') + |""".stripMargin, + includeCommitCharacter = true, + compat = Map( + "2.11" -> "empty[A, B]: Map[A,B] (commit: '.')" + ) + ) + + check( + "numeric-sort", + """ + |package a + | + |object Main{ + | scala.Function@@ + |} + |""".stripMargin, + // assert that we don't sort lexicographically: Function1, Function11, ..., Function2, ... + """|Function scala + |Function0 scala + |Function1 scala + |Function2 scala + |Function3 scala + |Function4 scala + |Function5 scala + |Function6 scala + |Function7 scala + |Function8 scala + |Function9 scala + |Function10 scala + |Function11 scala + |Function12 scala + |Function13 scala + |Function14 scala + |Function15 scala + |Function16 scala + |Function17 scala + |Function18 scala + |Function19 scala + |Function20 scala + |Function21 scala + |Function22 scala + |PartialFunction scala + |""".stripMargin + ) + + check( + "sam", + """ + |object A { + | new java.util.ArrayList[String]().forEach(p => p.toChar@@) + |} + """.stripMargin, + """|toCharArray(): Array[Char] + |""".stripMargin, + compat = Map( + "2.11" -> "" // SAM was introduced in Scala 2.12 + ) + ) + + check( + "implicit", + """ + |object A { + | Array.concat@@ + |} + """.stripMargin, + """|concat[T](xss: Array[T]*)(implicit evidence$8: ClassTag[T]): Array[T] + |""".stripMargin + ) + check( + "bounds", + """ + |object A { + | java.nio.file.Files.readAttributes@@ + |} + """.stripMargin, + """|readAttributes(path: Path, attributes: String, options: LinkOption*): Map[String,Object] + |readAttributes[A <: BasicFileAttributes](path: Path, type: Class[A], options: LinkOption*): A + |""".stripMargin + ) + + check( + "local", + """ + |object A { + | locally { + | val thisIsLocal = 1 + | thisIsLoc@@ + | } + |} + """.stripMargin, + """|thisIsLocal: Int + |""".stripMargin + ) + check( + "singleton", + """ + |class A { + | def incrementThisType(): this.type = x + | incrementThisType@@ + |} + """.stripMargin, + """|incrementThisType(): A.this.type (with underlying type A) + |""".stripMargin + ) +} diff --git a/tests/cross/src/test/scala/tests/pc/SignatureHelpDocSuite.scala b/tests/cross/src/test/scala/tests/pc/SignatureHelpDocSuite.scala new file mode 100644 index 00000000000..52a12afbb7a --- /dev/null +++ b/tests/cross/src/test/scala/tests/pc/SignatureHelpDocSuite.scala @@ -0,0 +1,201 @@ +package tests.pc + +import tests.BaseSignatureHelpSuite + +object SignatureHelpDocSuite extends BaseSignatureHelpSuite { + checkDoc( + "curry", + """ + |object a { + | Option(1).fold("")(_ => @@) + |} + """.stripMargin, + """|Returns the result of applying $f to this $option's + | value if the $option is nonempty. + |fold[B](ifEmpty: => B)(f: Int => B): B + | ^^^^^^^^^^^ + | @param ifEmpty `. + | @param f Int => ??? to this $option's + |""".stripMargin + ) + + checkDoc( + "curry2", + """ + |object a { + | Option(1).fold("@@") + |} + """.stripMargin, + """|Returns the result of applying $f to this $option's + | value if the $option is nonempty. + |fold[B](ifEmpty: => B)(f: Int => B): B + | ^^^^^^^^^^^^^ + | @param ifEmpty String `. + | @param f to this $option's + |""".stripMargin + ) + checkDoc( + "curry3", + """ + |object a { + | List(1).foldLeft(0) { + | case @@ + | } + |} + """.stripMargin, + """| + |foldLeft[B](z: B)(op: (B, Int) => B): B + | ^^^^^^^^^^^^^^^^^ + | @param op (Int, Int) => Int + |""".stripMargin + ) + checkDoc( + "curry4", + """ + |object a { + | def curry(a: Int, b: Int)(c: Int) = a + | curry(1)(3@@) + |} + """.stripMargin, + """| + |curry(a: Int, b: Int)(c: Int): Int + | ^^^^^^ + |""".stripMargin + ) + checkDoc( + "canbuildfrom", + """ + |object a { + | List(1).map(x => @@) + |} + """.stripMargin, + """| + |map[B, That](f: Int => B)(implicit bf: CanBuildFrom[List[Int],B,That]): That + | ^^^^^^^^^^^ + | @param f Int => ??? + |""".stripMargin + ) + checkDoc( + "too-many", + """ + |object a { + | Option(1, 2, @@2) + |} + """.stripMargin, + // FIXME: https://github.com/scalameta/metals/issues/518 + // The expected output is broken here. + """|An Option factory which creates Some(x) if the argument is not null, + | and None if it is null. + |apply[A](x: A): Option[A] + | ^^^^ + | @param A n Option factory which creates Some(x) if the argument is not null, + | @param x (Int, Int, Int) ) if the argument is not null, + |""".stripMargin + ) + checkDoc( + "java5", + """ + |object a { + | java.util.Collections.singleton(@@) + |} + """.stripMargin, + """| Returns an immutable set containing only the specified object. + |The returned set is serializable. + |singleton[T](o: T): Set[T] + | ^^^^ + | @param T the class of the objects in the set + | @param o o the sole object to be stored in the returned set. + |""".stripMargin + ) + checkDoc( + "default", + """ + |object A { + | new scala.util.control.Exception.Catch(@@) + |} + """.stripMargin, + """|A container class for catch/finally logic. + |(pf: Exception.Catcher[T], fin: Option[Exception.Finally] = None, rethrow: Throwable => Boolean = shouldRethrow): Exception.Catch[T] + | ^^^^^^^^^^^^^^^^^^^^^^^^ + | @param pf Partial function used when applying catch logic to determine result value + | @param fin ally logic. + | @param rethrow if you want to probably + |""".stripMargin + ) + check( + "java", + """ + |object a { + | new java.io.File(@@) + |} + """.stripMargin, + """|(uri: URI): File + |(parent: File, child: String): File + |(parent: String, child: String): File + |(pathname: String): File + |""".stripMargin + ) + check( + "java2", + """ + |object a { + | "".substring(1@@) + |} + """.stripMargin, + """|substring(beginIndex: Int, endIndex: Int): String + |substring(beginIndex: Int): String + | ^^^^^^^^^^^^^^^ + |""".stripMargin + ) + check( + "java3", + """ + |object a { + | String.valueOf(1@@) + |} + """.stripMargin, + """|valueOf(d: Double): String + |valueOf(f: Float): String + |valueOf(l: Long): String + |valueOf(i: Int): String + | ^^^^^^ + |valueOf(c: Char): String + |valueOf(b: Boolean): String + |valueOf(data: Array[Char], offset: Int, count: Int): String + |valueOf(data: Array[Char]): String + |valueOf(obj: Any): String + |""".stripMargin + ) + check( + "java4", + """ + |object a { + | String.valueOf(@@) + |} + """.stripMargin, + """|valueOf(d: Double): String + | ^^^^^^^^^ + |valueOf(f: Float): String + |valueOf(l: Long): String + |valueOf(i: Int): String + |valueOf(c: Char): String + |valueOf(b: Boolean): String + |valueOf(data: Array[Char]): String + |valueOf(obj: Any): String + |""".stripMargin + ) + checkDoc( + "ctor2", + """ + |object a { + | new Some(10@@) + |} + """.stripMargin, + """|Class `Some[A]` represents existing values of type + | `A`. + |(value: Int): Some[Int] + | ^^^^^^^^^^ + | @param value s of type + |""".stripMargin + ) +} diff --git a/tests/cross/src/test/scala/tests/pc/SignatureHelpSuite.scala b/tests/cross/src/test/scala/tests/pc/SignatureHelpSuite.scala new file mode 100644 index 00000000000..9c7d979bb75 --- /dev/null +++ b/tests/cross/src/test/scala/tests/pc/SignatureHelpSuite.scala @@ -0,0 +1,368 @@ +package tests.pc + +import tests.BaseSignatureHelpSuite + +object SignatureHelpSuite extends BaseSignatureHelpSuite { + override def beforeAll(): Unit = () + check( + "method", + """ + |object a { + | assert(true, ms@@) + |} + """.stripMargin, + """|assert(assertion: Boolean, message: => Any): Unit + | ^^^^^^^^^^^^^^^ + |assert(assertion: Boolean): Unit + |""".stripMargin + ) + check( + "empty", + """ + |object a { + | assert(@@) + |} + """.stripMargin, + """|assert(assertion: Boolean, message: => Any): Unit + |assert(assertion: Boolean): Unit + | ^^^^^^^^^^^^^^^^^^ + |""".stripMargin + ) + check( + "erroneous", + """ + |object a { + | Option(1).fold("")(_ => a@@) + |} + """.stripMargin, + """|fold[B](ifEmpty: => B)(f: Int => B): B + | ^^^^^^^^^^^ + |""".stripMargin + ) + check( + "canbuildfrom2", + """ + |object a { + | List(1).map(@@) + |} + """.stripMargin, + """|map[B, That](f: Int => B)(implicit bf: CanBuildFrom[List[Int],B,That]): That + | ^^^^^^^^^^^ + |""".stripMargin + ) + check( + "ctor", + """ + |object a { + | new scala.util.Random(@@) + |} + """.stripMargin, + """|(): Random + |(seed: Int): Random + |(seed: Long): Random + |(self: java.util.Random): Random + |""".stripMargin + ) + check( + "ctor1", + """ + |object a { + | new ProcessBuilder(@@) + |} + """.stripMargin, + """|(x$1: String*): ProcessBuilder + |(x$1: List[String]): ProcessBuilder + |""".stripMargin + ) + check( + "ctor2", + """ + |object a { + | new Some(10@@) + |} + """.stripMargin, + """|(value: Int): Some[Int] + | ^^^^^^^^^^ + |""".stripMargin, + compat = Map( + "2.11" -> + """|(x: Int): Some[Int] + | ^^^^^^ + |""".stripMargin + ) + ) + + check( + "apply", + """ + |object a { + | def apply(a: Int): Int = a + | def apply(b: String): String = b + | a(""@@) + |} + """.stripMargin, + """|apply(b: String): String + | ^^^^^^^^^ + |apply(a: Int): Int + |""".stripMargin + ) + check( + "partial", + """ + |object a { + | Option(1).collect { + | case@@ + | } + |} + """.stripMargin, + """|collect[B](pf: PartialFunction[Int,B]): Option[B] + | ^^^^^^^^^^^^^^^^^^^^^^^^^^ + |""".stripMargin + ) + check( + "nested", + """ + |object a { + | List(Option(1@@)) + |} + """.stripMargin, + """|apply[A](x: A): Option[A] + | ^^^^ + |""".stripMargin + ) + check( + "nested2", + """ + |object a { + | List(Opt@@ion(1)) + |} + """.stripMargin, + """|apply[A](xs: A*): List[A] + | ^^^^^^ + |""".stripMargin + ) + check( + "nested3", + """ + |object a { + | List(Option(@@)) + |} + """.stripMargin, + """|apply[A](x: A): Option[A] + |""".stripMargin + ) + check( + "vararg", + """ + |object a { + | List(1, 2@@ + |} + """.stripMargin, + """|apply[A](xs: A*): List[A] + | ^^^^^^ + |""".stripMargin + ) + check( + "tparam", + """ + |object a { + | identity[I@@] + |} + """.stripMargin, + """|identity[A](x: A): A + | ^ + |""".stripMargin + ) + check( + "tparam2", + """ + |object a { + | Option.empty[I@@] + |} + """.stripMargin, + """|empty[A]: Option[A] + | ^ + |""".stripMargin + ) + check( + "tparam3", + """ + |object a { + | Option[I@@] + |} + """.stripMargin, + """|apply[A](x: A): Option[A] + | ^ + |""".stripMargin + ) + check( + "tparam4", + """ + |object a { + | Map.empty[I@@] + |} + """.stripMargin, + """|empty[K, V]: Map[K,V] + | ^ + |""".stripMargin, + compat = Map( + "2.11" -> + """|empty[A, B]: Map[A,B] + | ^ + |""".stripMargin + ) + ) + check( + "tparam5", + """ + |object a { + | List[String](1).lengthCompare(@@) + |} + """.stripMargin, + """|lengthCompare(len: Int): Int + | ^^^^^^^^ + |""".stripMargin + ) + check( + "error1", + """ + |object a { + | Map[Int](1 @@-> "").map { + | } + |} + """.stripMargin, + "" + ) + check( + "for", + """ + |object a { + | for { + | i <- Option(1) + | j < 1.to(i) + | if i > j + | k@@ = i + j + | l <- j.to(k) + | } yield l + |} + """.stripMargin, + """|flatMap[B](f: Int => Option[B]): Option[B] + | ^^^^^^^^^^^^^^^^^^^ + |""".stripMargin + ) + + check( + "bounds", + """ + |object a { + | Map.empty[Int, String].applyOrElse(1@@) + |} + """.stripMargin, + """|applyOrElse[K1 <: Int, V1 >: String](x: K1, default: K1 => V1): V1 + | ^^^^^ + |""".stripMargin, + compat = Map( + "2.11" -> + """|applyOrElse[A1 <: Int, B1 >: String](x: A1, default: A1 => B1): B1 + | ^^^^^ + |""".stripMargin + ) + ) + + check( + "error", + """ + |object a { + | Map[Int](1 @@-> "").map { + | } + |} + """.stripMargin, + "" + ) + + check( + "named", + """ + |case class User(name: String = "John", age: Int = 42) + |object A { + | User(age = 1, @@) + |} + """.stripMargin, + """|apply(, ): User + | ^^^^^^^^^^^^^^^^^^^ + |""".stripMargin + ) + check( + "named1", + """ + |case class User(name: String = "John", age: Int = 42) + |object A { + | User(name = "", @@) + |} + """.stripMargin, + """|apply(name: String = {}, age: Int = {}): User + | ^^^^^^^^^^^^^ + |""".stripMargin + ) + check( + "named2", + """ + |object A { + | def user(name: String, age: Int) = age + | user(na@@me = "", age = 42) + |} + """.stripMargin, + """|user(name: String, age: Int): Int + | ^^^^^^^^^^^^ + |""".stripMargin + ) + check( + "named3", + """ + |object A { + | def user(name: String, age: Int): Int = age + | def user(name: String, age: Int, street: Int): Int = age + | def x = user(str@@eet = 42, name = "", age = 2) + |} + """.stripMargin, + """|user(name: String, age: Int, street: Int): Int + | ^^^^^^^^^^^^ + |user(name: String, age: Int): Int + |""".stripMargin + ) + check( + "short-name", + """ + |object A { + | new scala.util.control.Exception.Catch(@@) + |} + """.stripMargin, + """|(pf: Exception.Catcher[T], fin: Option[Exception.Finally] = {}, rethrow: Throwable => Boolean = {}): Exception.Catch[T] + | ^^^^^^^^^^^^^^^^^^^^^^^^ + |""".stripMargin + ) + check( + "short-name1", + """ + |object A { + | new java.util.HashMap[String, Int]().computeIfAbsent(@@) + |} + """.stripMargin, + """|computeIfAbsent(x$1: String, x$2: Function[_ >: String, _ <: Int]): Int + | ^^^^^^^^^^^ + |""".stripMargin + ) + check( + "curry5", + """ + |object a { + | def curry(a: Int)(c: Int) = a + | curry(1)(3@@) + |} + """.stripMargin, + """| + |curry(a: Int)(c: Int): Int + | ^^^^^^ + |""".stripMargin + ) +} diff --git a/tests/input/src/main/java/example/JavaClass.java b/tests/input/src/main/java/example/JavaClass.java index 8211e757f87..10c6eaafec6 100644 --- a/tests/input/src/main/java/example/JavaClass.java +++ b/tests/input/src/main/java/example/JavaClass.java @@ -2,6 +2,9 @@ public class JavaClass { + private JavaClass() { + + } public JavaClass(int d) { this.d = d; } diff --git a/tests/unit/src/main/scala/tests/BaseSuite.scala b/tests/mtest/src/main/scala/tests/BaseSuite.scala similarity index 97% rename from tests/unit/src/main/scala/tests/BaseSuite.scala rename to tests/mtest/src/main/scala/tests/BaseSuite.scala index 9ae141058d6..9453d60c440 100644 --- a/tests/unit/src/main/scala/tests/BaseSuite.scala +++ b/tests/mtest/src/main/scala/tests/BaseSuite.scala @@ -4,7 +4,6 @@ import scala.concurrent.Await import scala.concurrent.Future import scala.concurrent.duration.Duration import scala.language.experimental.macros -import scala.meta.internal.metals.MetalsLogger import scala.meta.io.AbsolutePath import scala.reflect.ClassTag import utest.TestSuite @@ -23,7 +22,6 @@ import utest.ufansi.Str * */ class BaseSuite extends TestSuite { - MetalsLogger.updateDefaultFormat() System.setProperty("metals.testing", "true") def isAppveyor: Boolean = "True" == System.getenv("APPVEYOR") def beforeAll(): Unit = () @@ -95,9 +93,11 @@ class BaseSuite extends TestSuite { override def exceptionStackFrameHighlighter( s: StackTraceElement ): Boolean = { + s.getClassName.startsWith("scala.meta.internal.pc.") || s.getClassName.startsWith("scala.meta.internal.mtags.") || s.getClassName.startsWith("scala.meta.internal.metals.") || s.getClassName.startsWith("scala.meta.metals.") || + s.getClassName.startsWith("scala.meta.pc.") || (s.getClassName.startsWith("tests") && !s.getClassName.startsWith("tests.DiffAssertions") && !s.getClassName.startsWith("tests.MegaSuite")) diff --git a/tests/unit/src/main/scala/tests/DiffAssertions.scala b/tests/mtest/src/main/scala/tests/DiffAssertions.scala similarity index 100% rename from tests/unit/src/main/scala/tests/DiffAssertions.scala rename to tests/mtest/src/main/scala/tests/DiffAssertions.scala diff --git a/tests/unit/src/main/scala/tests/TestFailedException.scala b/tests/mtest/src/main/scala/tests/TestFailedException.scala similarity index 100% rename from tests/unit/src/main/scala/tests/TestFailedException.scala rename to tests/mtest/src/main/scala/tests/TestFailedException.scala diff --git a/tests/slow/src/test/scala/tests/ClasspathSymbolRegressionSuite.scala b/tests/slow/src/test/scala/tests/ClasspathSymbolRegressionSuite.scala index f26868139b6..6b819983ac4 100644 --- a/tests/slow/src/test/scala/tests/ClasspathSymbolRegressionSuite.scala +++ b/tests/slow/src/test/scala/tests/ClasspathSymbolRegressionSuite.scala @@ -33,24 +33,11 @@ object ClasspathSymbolRegressionSuite extends BaseWorkspaceSymbolSuite { ) check( "Map.Entry", - """|com.esotericsoftware.kryo.util.IntMap#Entry Class + """|com.esotericsoftware.kryo.util.IdentityMap#Entry Class + |com.esotericsoftware.kryo.util.IntMap#Entry Class |com.esotericsoftware.kryo.util.ObjectMap#Entry Class - |com.google.common.collect.Maps#EntryFunction Class - |com.google.common.collect.Maps#EntrySet Class - |com.google.common.collect.Maps#EntryTransformer Interface - |com.google.common.collect.Maps#FilteredEntryBiMap Class - |com.google.common.collect.Maps#FilteredEntryMap Class - |com.google.common.collect.Maps#FilteredEntryMap#EntrySet Class - |com.google.common.collect.Maps#FilteredEntryNavigableMap Class - |com.google.common.collect.Maps#FilteredEntrySortedMap Class - |com.google.common.collect.Maps#UnmodifiableEntrySet Class |com.twitter.util.tunable.TunableMap.Entry Class - |java.util.EnumMap#EntryIterator Class - |java.util.EnumMap#EntryIterator#Entry Class - |java.util.EnumMap#EntrySet Class - |java.util.HashMap#EntryIterator Class - |java.util.HashMap#EntrySet Class - |java.util.HashMap#EntrySpliterator Class + |io.netty.util.collection.IntObjectMap#PrimitiveEntry Interface |java.util.Map#Entry Interface |java.util.TreeMap#AscendingSubMap#AscendingEntrySetView Class |java.util.TreeMap#DescendingSubMap#DescendingEntrySetView Class @@ -62,15 +49,12 @@ object ClasspathSymbolRegressionSuite extends BaseWorkspaceSymbolSuite { |java.util.TreeMap#NavigableSubMap#EntrySetView Class |java.util.TreeMap#NavigableSubMap#SubMapEntryIterator Class |java.util.TreeMap#PrivateEntryIterator Class - |jersey.repackaged.com.google.common.collect.Maps#EntryFunction Class - |jersey.repackaged.com.google.common.collect.Maps#EntrySet Class - |jersey.repackaged.com.google.common.collect.Maps#EntryTransformer Interface - |jersey.repackaged.com.google.common.collect.Maps#FilteredEntryBiMap Class - |jersey.repackaged.com.google.common.collect.Maps#FilteredEntryMap Class - |jersey.repackaged.com.google.common.collect.Maps#FilteredEntryMap#EntrySet Class - |jersey.repackaged.com.google.common.collect.Maps#FilteredEntryNavigableMap Class - |jersey.repackaged.com.google.common.collect.Maps#FilteredEntrySortedMap Class - |jersey.repackaged.com.google.common.collect.Maps#UnmodifiableEntrySet Class + |java.util.WeakHashMap#Entry Class + |java.util.WeakHashMap#EntryIterator Class + |java.util.WeakHashMap#EntrySet Class + |java.util.WeakHashMap#EntrySpliterator Class + |org.apache.commons.collections.ReferenceMap#Entry Class + |org.apache.commons.collections.ReferenceMap#EntryIterator Class |org.apache.commons.lang.IntHashMap#Entry Class |""".stripMargin ) @@ -98,6 +82,11 @@ object ClasspathSymbolRegressionSuite extends BaseWorkspaceSymbolSuite { |com.google.common.io.Files#FileByteSink Class |com.google.common.io.Files#FileByteSource Class |com.google.common.io.Files#FilePredicate Class + |com.google.protobuf.compiler.PluginProtos#CodeGeneratorResponse#File Class + |com.google.protobuf.compiler.PluginProtos#CodeGeneratorResponse#FileOrBuilder Interface + |com.google.protobuf.compiler.plugin.CodeGeneratorResponse.File Class + |com.google.protobuf.compiler.plugin.CodeGeneratorResponse.File Object + |com.google.protobuf.compiler.plugin.CodeGeneratorResponse.File.FileLens Class |com.twitter.io.Files Object |io.buoyant.config.types.File Class |io.buoyant.config.types.FileDeserializer Class @@ -105,16 +94,13 @@ object ClasspathSymbolRegressionSuite extends BaseWorkspaceSymbolSuite { |java.io.File Class |java.nio.file.Files Class |java.nio.file.Files#FileTypeDetectors Class - |javax.annotation.processing.Filer Interface - |org.apache.hadoop.io.file.tfile.TFile Class - |org.apache.hadoop.io.file.tfile.TFile#TFileIndex Class - |org.apache.hadoop.io.file.tfile.TFile#TFileIndexEntry Class - |org.apache.hadoop.io.file.tfile.TFile#TFileMeta Class |org.apache.hadoop.mapred.IFile Class |org.apache.hadoop.record.compiler.JFile Class |org.apache.jute.compiler.JFile Class |org.apache.parquet.Files Class - |scala.meta.internal.io.FileIO Object + |scala.meta.inputs.Input.Stream.SerializationProxy#File Class + |scala.meta.inputs.Input.Stream.SerializationProxy#File Object + |scala.meta.inputs.Input.Stream.SerializationProxy#File.SerializationProxy#VirtualFile Class |scala.reflect.io.File Class |scala.reflect.io.File Object |sourcecode.File Class @@ -129,7 +115,6 @@ object ClasspathSymbolRegressionSuite extends BaseWorkspaceSymbolSuite { |java.nio.file.Files Class |org.apache.hadoop.mapred.MROutputFiles Class |org.apache.hadoop.mapred.YarnOutputFiles Class - |org.apache.hadoop.mapreduce.JobSubmissionFiles Class |org.apache.ivy.ant.IvyCacheFileset Class |org.apache.parquet.Files Class |org.apache.spark.SparkFiles Object @@ -137,22 +122,24 @@ object ClasspathSymbolRegressionSuite extends BaseWorkspaceSymbolSuite { |org.apache.spark.sql.execution.streaming.FileStreamSource.SeenFilesMap Class |org.glassfish.jersey.server.internal.scanning.FilesScanner Class |scala.meta.internal.io.ListFiles Class + |scala.tools.nsc.interactive.CompilerControl#FilesDeletedItem Class |""".stripMargin ) check( "Implicits", - """|com.fasterxml.jackson.module.scala.util.Implicits Object + """|akka.stream.extra.Implicits Object + |akka.stream.scaladsl.GraphDSL.Implicits Object + |com.fasterxml.jackson.module.scala.util.Implicits Object |fastparse.core.Implicits Object |kafka.javaapi.Implicits Object - |kafka.javaapi.MetadataListImplicits Object |kafka.utils.Implicits Object |org.apache.spark.sql.LowPrioritySQLImplicits Interface |org.apache.spark.sql.SQLImplicits Class |org.json4s.Implicits Interface - |scala.LowPriorityImplicits Class |scala.collection.convert.ToJavaImplicits Interface |scala.collection.convert.ToScalaImplicits Interface + |scala.concurrent.ExecutionContext.Implicits Object |scala.math.Fractional.ExtraImplicits Interface |scala.math.Fractional.Implicits Object |scala.math.Integral.ExtraImplicits Interface @@ -163,9 +150,9 @@ object ClasspathSymbolRegressionSuite extends BaseWorkspaceSymbolSuite { |scala.math.Ordering.ExtraImplicits Interface |scala.math.Ordering.Implicits Object |scala.meta.internal.fastparse.core.Implicits Object - |scala.sys.process.ProcessImplicits Interface |scala.tools.nsc.interpreter.Power#LowPriorityPrettifier#AnyPrettifier.Implicits1 Interface |scala.tools.nsc.interpreter.Power#LowPriorityPrettifier#AnyPrettifier.Implicits2 Interface + |scala.tools.nsc.interpreter.StdReplVals#ReplImplicits Class |scala.tools.nsc.typechecker.Implicits Interface |scala.tools.nsc.typechecker.ImplicitsStats Object |""".stripMargin diff --git a/tests/slow/src/test/scala/tests/CompletionCrossSlowSuite.scala b/tests/slow/src/test/scala/tests/CompletionCrossSlowSuite.scala new file mode 100644 index 00000000000..5a1f530fe82 --- /dev/null +++ b/tests/slow/src/test/scala/tests/CompletionCrossSlowSuite.scala @@ -0,0 +1,10 @@ +package tests + +import scala.meta.internal.metals.{BuildInfo => V} + +object CompletionCrossSlowSuite + extends BaseCompletionSlowSuite("completion-cross") { + testAsync("basic-211") { + basicTest(V.scala211) + } +} diff --git a/tests/unit/src/main/scala/tests/BaseCompletionSlowSuite.scala b/tests/unit/src/main/scala/tests/BaseCompletionSlowSuite.scala new file mode 100644 index 00000000000..4c62e629406 --- /dev/null +++ b/tests/unit/src/main/scala/tests/BaseCompletionSlowSuite.scala @@ -0,0 +1,83 @@ +package tests + +import scala.concurrent.Future + +abstract class BaseCompletionSlowSuite(name: String) + extends BaseSlowSuite(name) { + def assertCompletion( + query: String, + expected: String, + project: Char = 'a' + )(implicit file: sourcecode.File, line: sourcecode.Line): Future[Unit] = { + val filename = s"$project/src/main/scala/$project/${project.toUpper}.scala" + server.completion(filename, query).map { completion => + assertNoDiff(completion, expected) + } + } + + def basicTest(scalaVersion: String): Future[Unit] = { + cleanWorkspace() + for { + _ <- server.initialize( + s"""/metals.json + |{ + | "a": { "scalaVersion": "${scalaVersion}" } + |} + |/a/src/main/scala/a/A.scala + |package a + |object A { + | val x = "".substrin + | Stream + | TrieMap + | locally { + | val myLocalVariable = Array("") + | myLocalVariable + | val source = "" + | } + |} + |""".stripMargin + ) + _ <- server.didOpen("a/src/main/scala/a/A.scala") + _ = assertNotEmpty(client.workspaceDiagnostics) + _ <- assertCompletion( + "substrin@@", + """|substring(beginIndex: Int): String + |substring(beginIndex: Int, endIndex: Int): String + |""".stripMargin + ) + _ <- assertCompletion( + "Stream@@", + """|Stream scala.collection.immutable + |Stream java.util.stream + |IntStream java.util.stream + |LogStream java.rmi.server + |BaseStream java.util.stream + |LongStream java.util.stream + |StreamView scala.collection.immutable + |InputStream java.io + |PrintStream java.io + |DoubleStream java.util.stream + |OutputStream java.io + |StreamBuilder scala.collection.immutable.Stream + |StreamHandler java.util.logging + |StreamCanBuildFrom scala.collection.immutable.Stream + |""".stripMargin + ) + _ <- assertCompletion( + "TrieMap@@", + """|TrieMap scala.collection.concurrent + |ParTrieMap scala.collection.parallel.mutable + |HashTrieMap scala.collection.immutable.HashMap + |ParTrieMapCombiner scala.collection.parallel.mutable + |ParTrieMapSplitter scala.collection.parallel.mutable + |TrieMapSerializationEnd scala.collection.concurrent + |""".stripMargin + ) + _ <- assertCompletion( + " myLocalVariable@@", + """|myLocalVariable: Array[String] + |""".stripMargin + ) + } yield () + } +} diff --git a/tests/unit/src/main/scala/tests/BaseSlowSuite.scala b/tests/unit/src/main/scala/tests/BaseSlowSuite.scala index c19f5303586..7c682b9dba8 100644 --- a/tests/unit/src/main/scala/tests/BaseSlowSuite.scala +++ b/tests/unit/src/main/scala/tests/BaseSlowSuite.scala @@ -9,6 +9,7 @@ import scala.meta.internal.metals.BloopProtocol import scala.meta.internal.metals.Buffers import scala.meta.internal.metals.ExecuteClientCommandConfig import scala.meta.internal.metals.Icons +import scala.meta.internal.metals.MetalsLogger import scala.meta.internal.metals.MetalsServerConfig import scala.meta.internal.metals.RecursivelyDelete import scala.meta.internal.metals.Time @@ -20,6 +21,7 @@ import scala.util.control.NonFatal * Full end to end integration tests against a full metals language server. */ abstract class BaseSlowSuite(suiteName: String) extends BaseSuite { + MetalsLogger.updateDefaultFormat() def protocol: BloopProtocol = BloopProtocol.auto def icons: Icons = Icons.default def userConfig: UserConfiguration = UserConfiguration() diff --git a/tests/unit/src/main/scala/tests/Library.scala b/tests/unit/src/main/scala/tests/Library.scala index dcca1226fec..119148da645 100644 --- a/tests/unit/src/main/scala/tests/Library.scala +++ b/tests/unit/src/main/scala/tests/Library.scala @@ -3,6 +3,8 @@ package tests import com.geirsson.coursiersmall.CoursierSmall import com.geirsson.coursiersmall.Dependency import com.geirsson.coursiersmall.Settings +import scala.meta.internal.metals.JdkSources +import scala.meta.internal.metals.PackageIndex import scala.meta.io.AbsolutePath import scala.meta.io.Classpath @@ -13,6 +15,12 @@ case class Library( ) object Library { + def jdk: Library = + Library( + "JDK", + Classpath(PackageIndex.bootClasspath), + Classpath(JdkSources().get :: Nil) + ) def all: List[Library] = { val settings = new Settings() .withDependencies( @@ -27,6 +35,11 @@ object Library { "akka-testkit_2.12", "2.5.9" ), + new Dependency( + "com.typesafe.akka", + "akka-stream_2.12", + "2.5.9" + ), new Dependency( "org.apache.spark", "spark-sql_2.11", diff --git a/tests/unit/src/main/scala/tests/MetalsTestEnrichments.scala b/tests/unit/src/main/scala/tests/MetalsTestEnrichments.scala index 516593cbeee..51ade30d0ad 100644 --- a/tests/unit/src/main/scala/tests/MetalsTestEnrichments.scala +++ b/tests/unit/src/main/scala/tests/MetalsTestEnrichments.scala @@ -11,6 +11,7 @@ import org.eclipse.lsp4j.TextDocumentIdentifier import org.eclipse.lsp4j.TextDocumentPositionParams import org.eclipse.{lsp4j => l} import scala.collection.mutable.ArrayBuffer +import scala.meta.internal.metals.CachedSymbolInformation import scala.meta.internal.metals.JdkSources import scala.meta.internal.metals.Memory import scala.meta.internal.metals.MetalsEnrichments._ @@ -45,11 +46,11 @@ object MetalsTestEnrichments { if source.isScalaOrJava } { val input = source.toInput - val symbols = ArrayBuffer.empty[String] + val symbols = ArrayBuffer.empty[CachedSymbolInformation] SemanticdbDefinition.foreach(input) { - case SemanticdbDefinition(info, _, _) => + case defn @ SemanticdbDefinition(info, _, _) => if (WorkspaceSymbolProvider.isRelevantKind(info.kind)) { - symbols += info.symbol + symbols += defn.toCached } } wsp.didChange(source, symbols) diff --git a/tests/unit/src/main/scala/tests/SimpleJavaSymbolIndexer.scala b/tests/unit/src/main/scala/tests/SimpleJavaSymbolIndexer.scala new file mode 100644 index 00000000000..638553b90c6 --- /dev/null +++ b/tests/unit/src/main/scala/tests/SimpleJavaSymbolIndexer.scala @@ -0,0 +1,24 @@ +package tests + +import java.net.URLClassLoader +import java.nio.charset.StandardCharsets +import scala.meta.inputs.Input +import scala.meta.internal.io.InputStreamIO +import scala.meta.internal.metals.JavaSymbolIndexer +import scala.meta.internal.mtags.Symbol +import scala.meta.io.AbsolutePath +import scala.meta.pc.SymbolIndexer +import scala.meta.pc.SymbolVisitor + +class SimpleJavaSymbolIndexer(jars: List[AbsolutePath]) extends SymbolIndexer { + val classpath = new URLClassLoader(jars.map(_.toURI.toURL).toArray, null) + override def visit(symbol: String, visitor: SymbolVisitor): Unit = { + val filename = Symbol(symbol).toplevel.value.stripSuffix("#") + ".java" + val in = classpath.getResourceAsStream(filename) + if (in != null) { + val text = new String(InputStreamIO.readBytes(in), StandardCharsets.UTF_8) + new JavaSymbolIndexer(Input.VirtualFile(filename, text)) + .visit(symbol, visitor) + } + } +} diff --git a/tests/unit/src/main/scala/tests/TestingServer.scala b/tests/unit/src/main/scala/tests/TestingServer.scala index bcf8d9c3970..4fa110883eb 100644 --- a/tests/unit/src/main/scala/tests/TestingServer.scala +++ b/tests/unit/src/main/scala/tests/TestingServer.scala @@ -12,6 +12,7 @@ import java.util import java.util.Collections import java.util.concurrent.ScheduledExecutorService import org.eclipse.lsp4j.ClientCapabilities +import org.eclipse.lsp4j.CompletionParams import scala.meta.internal.metals.PositionSyntax._ import org.eclipse.lsp4j.DidChangeConfigurationParams import org.eclipse.lsp4j.DidChangeTextDocumentParams @@ -329,6 +330,31 @@ final class TestingServer( } } + def completion( + filename: String, + query: String + ): Future[String] = { + val path = toPath(filename) + val input = path.toInputFromBuffers(buffers) + val offset = query.indexOf("@@") + if (offset < 0) sys.error("missing @@") + val start = input.text.indexOf(query.substring(0, offset)) + if (start < 0) sys.error(s"missing '$query'") + val point = start + offset + val pos = m.Position.Range(input, point, point) + val params = + new CompletionParams(path.toTextDocumentIdentifier, pos.toLSP.getStart) + for { + completion <- server.completion(params).asScala + } yield { + val items = + completion.getItems.asScala.map(server.completionItemResolveSync) + items.iterator + .map(item => item.getLabel + item.getDetail) + .mkString("\n") + } + } + def references( filename: String, substring: String diff --git a/tests/unit/src/main/scala/tests/TestingWorkspaceSymbolProvider.scala b/tests/unit/src/main/scala/tests/TestingWorkspaceSymbolProvider.scala index caf7eab663c..a2fb69645a0 100644 --- a/tests/unit/src/main/scala/tests/TestingWorkspaceSymbolProvider.scala +++ b/tests/unit/src/main/scala/tests/TestingWorkspaceSymbolProvider.scala @@ -14,7 +14,7 @@ object TestingWorkspaceSymbolProvider { buildTargets: BuildTargets = new BuildTargets, statistics: StatisticsConfig = StatisticsConfig.default, index: OnDemandSymbolIndex = OnDemandSymbolIndex(), - isReferencedPackage: String => Boolean = _ => false + isReferencedPackage: String => Int = _ => 0 ): WorkspaceSymbolProvider = { new WorkspaceSymbolProvider( workspace = workspace, diff --git a/tests/unit/src/test/resources/mtags/example/JavaClass.java b/tests/unit/src/test/resources/mtags/example/JavaClass.java index 20d84541171..2b9234dfc0a 100644 --- a/tests/unit/src/test/resources/mtags/example/JavaClass.java +++ b/tests/unit/src/test/resources/mtags/example/JavaClass.java @@ -2,6 +2,9 @@ public class JavaClass/*example.JavaClass#*/ { + private JavaClass() { + + } public JavaClass/*example.JavaClass#``().*/(int d) { this.d = d; } diff --git a/tests/unit/src/test/resources/semanticdb/example/JavaThenScala.scala b/tests/unit/src/test/resources/semanticdb/example/JavaThenScala.scala index 79811ad2236..955b8d0d582 100644 --- a/tests/unit/src/test/resources/semanticdb/example/JavaThenScala.scala +++ b/tests/unit/src/test/resources/semanticdb/example/JavaThenScala.scala @@ -1,5 +1,5 @@ package example class JavaThenScala/*example.JavaThenScala#*/ { - new JavaClass/*example.JavaClass#*//*example.JavaClass#``().*/(42) + new JavaClass/*example.JavaClass#*//*example.JavaClass#``(+1).*/(42) } diff --git a/tests/unit/src/test/resources/workspace-symbol/example/JavaClass.java b/tests/unit/src/test/resources/workspace-symbol/example/JavaClass.java index bcf94411764..fb8ed41a0ad 100644 --- a/tests/unit/src/test/resources/workspace-symbol/example/JavaClass.java +++ b/tests/unit/src/test/resources/workspace-symbol/example/JavaClass.java @@ -2,6 +2,9 @@ public class JavaClass/*example.JavaClass#*/ { + private JavaClass() { + + } public JavaClass(int d) { this.d = d; } diff --git a/tests/unit/src/test/scala/tests/BspCli.scala b/tests/unit/src/test/scala/tests/BspCli.scala index f01e07b694d..2bb97b875a3 100644 --- a/tests/unit/src/test/scala/tests/BspCli.scala +++ b/tests/unit/src/test/scala/tests/BspCli.scala @@ -53,8 +53,12 @@ object BspCli { val config = MetalsServerConfig.default val icons = Icons.none val time = Time.system - val statusBar: StatusBar = - new StatusBar(() => loggingLangaugeClient, time, ProgressTicks.none) + val statusBar = new StatusBar( + () => loggingLangaugeClient, + time, + ProgressTicks.none, + icons + ) val embedded = new Embedded(icons, statusBar, () => UserConfiguration()) val server = new BloopServers( sh, diff --git a/tests/unit/src/test/scala/tests/CompletionSlowSuite.scala b/tests/unit/src/test/scala/tests/CompletionSlowSuite.scala new file mode 100644 index 00000000000..d1b428415d8 --- /dev/null +++ b/tests/unit/src/test/scala/tests/CompletionSlowSuite.scala @@ -0,0 +1,55 @@ +package tests + +import scala.meta.internal.metals.{BuildInfo => V} + +object CompletionSlowSuite extends BaseCompletionSlowSuite("completion") { + + testAsync("basic-212") { + basicTest(V.scala212) + } + + testAsync("workspace") { + cleanWorkspace() + for { + _ <- server.initialize( + """/metals.json + |{ + | "b": {}, + | "c": {}, + | "a": { "dependsOn": ["c"] } + |} + |/b/src/main/scala/b/DefinedInB.scala + |package b + |object DefinedInB { + |} + |/c/src/main/scala/c/DefinedInC.scala + |package c + |object DefinedInC { + |} + |/a/src/main/scala/a/DefinedInA.scala + |package a + |object Outer { + | class DefinedInA + |} + |/a/src/main/scala/a/A.scala + |package a + |object Main { + | // DefinedIn + |} + |""".stripMargin + ) + _ <- server.didOpen("a/src/main/scala/a/A.scala") + _ = assertEmpty(client.workspaceDiagnostics) + _ <- server.didChange("a/src/main/scala/a/A.scala")( + _.replaceAllLiterally("// ", "") + ) + // assert that "DefinedInB" does not appear in results + _ <- assertCompletion( + "DefinedIn@@", + """|DefinedInA a.Outer + |DefinedInC c + |""".stripMargin + ) + } yield () + } +} diff --git a/tests/unit/src/test/scala/tests/CompressionSuite.scala b/tests/unit/src/test/scala/tests/CompressionSuite.scala index 2631ec53463..60262addebe 100644 --- a/tests/unit/src/test/scala/tests/CompressionSuite.scala +++ b/tests/unit/src/test/scala/tests/CompressionSuite.scala @@ -6,7 +6,7 @@ object CompressionSuite extends BaseSuite { def checkRoundtrip(a: String): Unit = { val nonempty = a.trim.linesIterator.map(_.trim).filterNot(_.isEmpty).toArray test(nonempty.headOption.getOrElse("")) { - val compressed = Compression.compress(nonempty) + val compressed = Compression.compress(nonempty.iterator) val decompressed = Compression.decompress(compressed) assertNoDiff(decompressed.mkString("\n"), nonempty.mkString("\n")) } diff --git a/tests/unit/src/test/scala/tests/DidFocusSlowSuite.scala b/tests/unit/src/test/scala/tests/DidFocusSlowSuite.scala index 36dded20e87..f84f43789ba 100644 --- a/tests/unit/src/test/scala/tests/DidFocusSlowSuite.scala +++ b/tests/unit/src/test/scala/tests/DidFocusSlowSuite.scala @@ -11,6 +11,7 @@ object DidFocusSlowSuite extends BaseSlowSuite("did-focus") { super.utestBeforeEach(path) } testAsync("is-compiled") { + cleanWorkspace() for { _ <- server.initialize( """ diff --git a/tests/unit/src/test/scala/tests/FilenameLength.scala b/tests/unit/src/test/scala/tests/FilenameLength.scala new file mode 100644 index 00000000000..18291033545 --- /dev/null +++ b/tests/unit/src/test/scala/tests/FilenameLength.scala @@ -0,0 +1,15 @@ +package tests + +import scala.meta.internal.metals.Fuzzy + +object FilenameLength extends BaseSuite { + def check(filename: String, expected: String): Unit = { + test(expected) { + val obtained = Fuzzy.nameLength(filename) + assertNoDiff(obtained.toString, expected.length.toString) + } + } + + check("Path.class", "Path") + check("Polygon$PolygonPathIterator.class", "PolygonPathIterator") +} diff --git a/tests/unit/src/test/scala/tests/FuzzySuite.scala b/tests/unit/src/test/scala/tests/FuzzySuite.scala index fd668dc3dc7..842bb32b913 100644 --- a/tests/unit/src/test/scala/tests/FuzzySuite.scala +++ b/tests/unit/src/test/scala/tests/FuzzySuite.scala @@ -54,9 +54,9 @@ object FuzzySuite extends BaseSuite { .toSeq .map(_.toString) .sorted - val isPrefix = Fuzzy.bloomFilterSymbolStrings(Seq(in)) + val isPrefix = Fuzzy.bloomFilterSymbolStrings(Seq(in)).map(_.toString) assertNoDiff(obtained.mkString("\n"), expected) - val allWords = Fuzzy.bloomFilterQueryStrings(in) + val allWords = Fuzzy.bloomFilterQueryStrings(in).map(_.toString) val isNotPrefix = allWords.filterNot(isPrefix) assert(isNotPrefix.isEmpty) } diff --git a/tests/unit/src/test/scala/tests/StatusBarSuite.scala b/tests/unit/src/test/scala/tests/StatusBarSuite.scala index 61fc7d361dd..1747b86569c 100644 --- a/tests/unit/src/test/scala/tests/StatusBarSuite.scala +++ b/tests/unit/src/test/scala/tests/StatusBarSuite.scala @@ -3,13 +3,15 @@ package tests import scala.concurrent.Promise import scala.meta.internal.io.PathIO import scala.meta.internal.metals.Buffers +import scala.meta.internal.metals.Icons import scala.meta.internal.metals.ProgressTicks import scala.meta.internal.metals.StatusBar object StatusBarSuite extends BaseSuite { val time = new FakeTime val client = new TestingClient(PathIO.workingDirectory, Buffers()) - var status: StatusBar = new StatusBar(() => client, time, ProgressTicks.dots) + var status = + new StatusBar(() => client, time, ProgressTicks.dots, Icons.default) override def utestBeforeEach(path: Seq[String]): Unit = { client.statusParams.clear() status.cancel()