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"$name>") =>
+ HtmlTag(s"$name>")
+ }
+}
+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+;)|(?(abbr|acronym|address|area|a|bdo|big|blockquote|br|button|b|caption|cite|code|col|colgroup|dd|del|dfn|em|fieldset|form|hr|img|input|ins|i|kbd|label|legend|link|map|object|optgroup|option|param|pre|q|samp|select|small|span|strong|sub|sup|table|tbody|td|textarea|tfoot|th|thead|tr|tt|var)( [^>]*)?/?>))"""
+ )
+
+ 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 = "^([A-Za-z]+)>$".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("
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