diff --git a/project/Build.scala b/project/Build.scala index 112c591b433e..fa2d8ab49915 100644 --- a/project/Build.scala +++ b/project/Build.scala @@ -1267,7 +1267,7 @@ object Build { scalaSrcLink(stdLibVersion, srcManaged(dottyNonBootstrappedVersion, "scala") + "="), dottySrcLink(referenceVersion, srcManaged(dottyNonBootstrappedVersion, "dotty") + "=", "#library/src"), dottySrcLink(referenceVersion), - ) ++ scalacOptionsDocSettings ++ revision ++ params ++ targets + ) ++ scalacOptionsDocSettings ++ revision ++ params ++ targets ++ Seq("-Ygenerate-inkuire") import _root_.scala.sys.process._ val escapedCmd = cmd.map(arg => if(arg.contains(" ")) s""""$arg"""" else arg) Def.task { diff --git a/scaladoc-js/resources/scaladoc-searchbar.css b/scaladoc-js/resources/scaladoc-searchbar.css index fded722670b3..a248eeb5c328 100644 --- a/scaladoc-js/resources/scaladoc-searchbar.css +++ b/scaladoc-js/resources/scaladoc-searchbar.css @@ -50,6 +50,9 @@ box-shadow: 0 2px 16px 0 rgba(0, 42, 76, 0.15); font-size: 13px; font-family: system-ui, -apple-system, Segoe UI, Roboto, Noto Sans, Ubuntu, Cantarell, Helvetica Neue, Arial, sans-serif; + background-color: var(--leftbar-bg); + color: var(--leftbar-fg); + box-shadow: 0 0 2px var(--shadow); } #scaladoc-searchbar-input { @@ -58,6 +61,8 @@ border: none; border-bottom: 1px solid #bbb; padding: 10px; + background-color: var(--leftbar-bg); + color: var(--leftbar-fg); } #scaladoc-searchbar-input:focus { @@ -65,7 +70,6 @@ } #scaladoc-searchbar-results { - background: var(--white); display: flex; flex-direction: column; max-height: 500px; @@ -73,7 +77,8 @@ } .scaladoc-searchbar-result { - background: var(--white); + background-color: var(--leftbar-bg); + color: var(--leftbar-fg); line-height: 24px; display: flex; padding: 4px 10px 4px 10px; @@ -90,11 +95,11 @@ } .scaladoc-searchbar-result[selected] { - background-color: var(--blue100); + background-color: var(--leftbar-hover-bg); + color: var(--leftbar-hover-fg); } .scaladoc-searchbar-result a { - color: var(--grey900); /* for some reason, with display:block if there's a wrap between the * search result text and the location span, the dead space to the * left of the location span doesn't get treated as part of the block, @@ -107,10 +112,6 @@ padding-left: 20px; } -.scaladoc-searchbar-result .scaladoc-searchbar-location { - color: gray; -} - #searchBar { display: inline-flex; } diff --git a/scaladoc-js/src/searchbar/Searchbar.scala b/scaladoc-js/src/searchbar/Searchbar.scala index a3dce6bb3a09..687888cbae61 100644 --- a/scaladoc-js/src/searchbar/Searchbar.scala +++ b/scaladoc-js/src/searchbar/Searchbar.scala @@ -2,7 +2,8 @@ package dotty.tools.scaladoc class Searchbar { val pages = SearchbarGlobals.pages.toList.map(PageEntry.apply) - val engine = SearchbarEngine(pages) val parser = QueryParser() - val component = SearchbarComponent(q => engine.query(parser.parse(q))) -} + val searchEngine = SearchbarEngine(pages) + val inkuireEngine = InkuireJSSearchEngine() + val component = SearchbarComponent(searchEngine, inkuireEngine, parser) +} \ No newline at end of file diff --git a/scaladoc-js/src/searchbar/SearchbarComponent.scala b/scaladoc-js/src/searchbar/SearchbarComponent.scala index 4c710f876c8b..2ca7aa310985 100644 --- a/scaladoc-js/src/searchbar/SearchbarComponent.scala +++ b/scaladoc-js/src/searchbar/SearchbarComponent.scala @@ -2,11 +2,13 @@ package dotty.tools.scaladoc import org.scalajs.dom._ import org.scalajs.dom.html.Input +import scala.scalajs.js.timers._ +import scala.concurrent.duration._ -class SearchbarComponent(val callback: (String) => List[PageEntry]): +class SearchbarComponent(engine: SearchbarEngine, inkuireEngine: InkuireJSSearchEngine, parser: QueryParser): val resultsChunkSize = 100 extension (p: PageEntry) - def toHTML = + def toHTML(inkuire: Boolean = false) = val wrapper = document.createElement("div").asInstanceOf[html.Div] wrapper.classList.add("scaladoc-searchbar-result") wrapper.classList.add("monospace") @@ -16,7 +18,7 @@ class SearchbarComponent(val callback: (String) => List[PageEntry]): icon.classList.add(p.kind.take(2)) val resultA = document.createElement("a").asInstanceOf[html.Anchor] - resultA.href = Globals.pathToRoot + p.location + resultA.href = if inkuire then p.location else Globals.pathToRoot + p.location resultA.text = s"${p.fullName}" val location = document.createElement("span") @@ -32,8 +34,8 @@ class SearchbarComponent(val callback: (String) => List[PageEntry]): }) wrapper - def handleNewQuery(query: String) = - val result = callback(query).map(_.toHTML) + def handleNewFluffQuery(matchers: List[Matchers]) = + val result = engine.query(matchers).map(_.toHTML(inkuire = false)) resultsDiv.scrollTop = 0 while (resultsDiv.hasChildNodes()) resultsDiv.removeChild(resultsDiv.lastChild) val fragment = document.createDocumentFragment() @@ -41,17 +43,58 @@ class SearchbarComponent(val callback: (String) => List[PageEntry]): resultsDiv.appendChild(fragment) def loadMoreResults(result: List[raw.HTMLElement]): Unit = { resultsDiv.onscroll = (event: Event) => { - if (resultsDiv.scrollHeight - resultsDiv.scrollTop == resultsDiv.clientHeight) - { - val fragment = document.createDocumentFragment() - result.take(resultsChunkSize).foreach(fragment.appendChild) - resultsDiv.appendChild(fragment) - loadMoreResults(result.drop(resultsChunkSize)) - } + if (resultsDiv.scrollHeight - resultsDiv.scrollTop == resultsDiv.clientHeight) { + val fragment = document.createDocumentFragment() + result.take(resultsChunkSize).foreach(fragment.appendChild) + resultsDiv.appendChild(fragment) + loadMoreResults(result.drop(resultsChunkSize)) + } } } loadMoreResults(result.drop(resultsChunkSize)) + extension (s: String) + def toHTMLError = + val wrapper = document.createElement("div").asInstanceOf[html.Div] + wrapper.classList.add("scaladoc-searchbar-result") + wrapper.classList.add("monospace") + + val errorSpan = document.createElement("span").asInstanceOf[html.Span] + errorSpan.classList.add("search-error") + errorSpan.textContent = s + + wrapper.appendChild(errorSpan) + wrapper + + var timeoutHandle: SetTimeoutHandle = null + def handleNewQuery(query: String) = + clearTimeout(timeoutHandle) + resultsDiv.scrollTop = 0 + resultsDiv.onscroll = (event: Event) => { } + while (resultsDiv.hasChildNodes()) resultsDiv.removeChild(resultsDiv.lastChild) + val fragment = document.createDocumentFragment() + parser.parse(query) match { + case EngineMatchersQuery(matchers) => + handleNewFluffQuery(matchers) + case BySignature(signature) => + timeoutHandle = setTimeout(1.second) { + val properResultsDiv = document.createElement("div").asInstanceOf[html.Div] + resultsDiv.appendChild(properResultsDiv) + val loading = document.createElement("div").asInstanceOf[html.Div] + loading.classList.add("loading-wrapper") + val animation = document.createElement("div").asInstanceOf[html.Div] + animation.classList.add("loading") + loading.appendChild(animation) + properResultsDiv.appendChild(loading) + inkuireEngine.query(query) { (p: PageEntry) => + properResultsDiv.appendChild(p.toHTML(inkuire = true)) + } { (s: String) => + animation.classList.remove("loading") + properResultsDiv.appendChild(s.toHTMLError) + } + } + } + private val searchIcon: html.Div = val span = document.createElement("span").asInstanceOf[html.Span] span.innerHTML = """""" diff --git a/scaladoc-js/src/searchbar/engine/InkuireJSSearchEngine.scala b/scaladoc-js/src/searchbar/engine/InkuireJSSearchEngine.scala new file mode 100644 index 000000000000..5b91512a46f7 --- /dev/null +++ b/scaladoc-js/src/searchbar/engine/InkuireJSSearchEngine.scala @@ -0,0 +1,50 @@ +package dotty.tools.scaladoc + +import scala.io.Source +import dotty.tools.scaladoc.PageEntry +import org.scalajs.dom.webworkers.Worker +import org.scalajs.dom._ +import scala.scalajs.js.{ JSON, Dynamic } +import scala.collection.mutable.ListBuffer +import scala.scalajs.js +import scala.scalajs.js.timers._ +import org.scalajs.dom.ext.Ajax +import scala.scalajs.js.URIUtils + +class InkuireJSSearchEngine { + + val scriptPath = Globals.pathToRoot + "scripts/" + val worker: Worker = new Worker(scriptPath + "inkuire-worker.js") + + def dynamicToPageEntry(d: Dynamic): PageEntry = { + PageEntry( + d.functionName.asInstanceOf[String], + d.prettifiedSignature.asInstanceOf[String], + d.pageLocation.asInstanceOf[String], + d.functionName.asInstanceOf[String], + "def", + List.empty + ) + } + + def query(s: String)(callback: PageEntry => Unit)(endCallback: String => Unit): List[PageEntry] = { + worker.onmessage = _ => () + val res = ListBuffer[PageEntry]() + val func = (msg: MessageEvent) => { + msg.data.asInstanceOf[String] match { + case "engine_ready" => + case "new_query" => + case endMsg if endMsg.startsWith("query_ended") => + endCallback(endMsg.drop("query_ended".length)) + case q => + val matches = JSON.parse(q).matches + val actualMatches = matches.asInstanceOf[js.Array[Dynamic]].map(dynamicToPageEntry) + actualMatches.foreach(callback) + } + } + worker.onmessage = func + worker.postMessage(s) + res.toList + } + +} \ No newline at end of file diff --git a/scaladoc-js/src/searchbar/engine/Matchers.scala b/scaladoc-js/src/searchbar/engine/Matchers.scala index ceca8afd357e..14b7eeea9653 100644 --- a/scaladoc-js/src/searchbar/engine/Matchers.scala +++ b/scaladoc-js/src/searchbar/engine/Matchers.scala @@ -1,5 +1,9 @@ package dotty.tools.scaladoc +sealed trait EngineQuery +case class EngineMatchersQuery(matchers: List[Matchers]) extends EngineQuery +case class BySignature(signature: String) extends EngineQuery + sealed trait Matchers extends Function1[PageEntry, Int] case class ByName(query: String) extends Matchers: diff --git a/scaladoc-js/src/searchbar/engine/QueryParser.scala b/scaladoc-js/src/searchbar/engine/QueryParser.scala index f5996484188c..56c40c0003ef 100644 --- a/scaladoc-js/src/searchbar/engine/QueryParser.scala +++ b/scaladoc-js/src/searchbar/engine/QueryParser.scala @@ -19,10 +19,16 @@ class QueryParser: val kindRegex = ("(?i)" + kinds.mkString("(","|",")") + " (.*)").r val restRegex = raw"(.*)".r val escapedRegex = raw"`(.*)`".r + val signatureRegex = raw"([^=>]+=>.*)".r - def parse(query: String): List[Matchers] = query match { + def parseMatchers(query: String): List[Matchers] = query match { case escapedRegex(rest) => List(ByName(rest)) - case kindRegex(kind, rest) => List(ByKind(kind)) ++ parse(rest) + case kindRegex(kind, rest) => List(ByKind(kind)) ++ parseMatchers(rest) case restRegex(name) => List(ByName(name)) case _ => List() + } + + def parse(query: String): EngineQuery = query match { + case signatureRegex(signature) => BySignature(signature) + case other => EngineMatchersQuery(parseMatchers(other)) } \ No newline at end of file diff --git a/scaladoc-js/src/searchbar/engine/SearchbarEngine.scala b/scaladoc-js/src/searchbar/engine/SearchbarEngine.scala index cbc43eaec051..c7b56b482047 100644 --- a/scaladoc-js/src/searchbar/engine/SearchbarEngine.scala +++ b/scaladoc-js/src/searchbar/engine/SearchbarEngine.scala @@ -1,6 +1,7 @@ package dotty.tools.scaladoc import math.Ordering.Implicits.seqOrdering +import org.scalajs.dom.Node class SearchbarEngine(pages: List[PageEntry]): def query(query: List[Matchers]): List[PageEntry] = diff --git a/scaladoc-js/test/dotty/dokka/QueryParserTest.scala b/scaladoc-js/test/dotty/dokka/QueryParserTest.scala index b89c7502714f..bd5ab8f9ea8a 100644 --- a/scaladoc-js/test/dotty/dokka/QueryParserTest.scala +++ b/scaladoc-js/test/dotty/dokka/QueryParserTest.scala @@ -17,7 +17,7 @@ class QueryParserTest: "given", "type" ) - private def testCase(query: String, result: List[Matchers]) = { + private def testCase(query: String, result: EngineQuery) = { val parsed = queryParser.parse(query) assertEquals( s"Query parser test error: for query: $query expected $result but found $parsed", @@ -28,8 +28,8 @@ class QueryParserTest: @Test def queryParserTests() = { - kinds.foreach(k => testCase(s"$k ", List(ByKind(k), ByName("")))) - testCase("trait", List(ByName("trait"))) - testCase("trait A", List(ByKind("trait"), ByName("A"))) - testCase("`trait A`", List(ByName("trait A"))) + kinds.foreach(k => testCase(s"$k ", EngineMatchersQuery(List(ByKind(k), ByName(""))))) + testCase("trait", EngineMatchersQuery(List(ByName("trait")))) + testCase("trait A", EngineMatchersQuery(List(ByKind("trait"), ByName("A")))) + testCase("`trait A`", EngineMatchersQuery(List(ByName("trait A")))) } \ No newline at end of file diff --git a/scaladoc-testcases/src/tests/inkuire.scala b/scaladoc-testcases/src/tests/inkuire.scala new file mode 100644 index 000000000000..1f50fc63858c --- /dev/null +++ b/scaladoc-testcases/src/tests/inkuire.scala @@ -0,0 +1,15 @@ +package tests.inkuire + +trait InType1 +trait InType2 extends InType1 + +trait OutType1 +trait OutType2 extends OutType1 + +class JustAClass { + def mathod(l: InType1): OutType1 = ??? +} + +class JustAnotherClass extends JustAClass { + def method(i: InType2): OutType2 = ??? +} diff --git a/scaladoc/resources/dotty_res/images/scaladoc_logo_dark.svg b/scaladoc/resources/dotty_res/images/scaladoc_logo_dark.svg new file mode 100644 index 000000000000..a203e185dd7a --- /dev/null +++ b/scaladoc/resources/dotty_res/images/scaladoc_logo_dark.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/scaladoc/resources/dotty_res/scripts/inkuire-worker.js b/scaladoc/resources/dotty_res/scripts/inkuire-worker.js new file mode 100644 index 000000000000..0b37ba58335e --- /dev/null +++ b/scaladoc/resources/dotty_res/scripts/inkuire-worker.js @@ -0,0 +1,2 @@ +importScripts("inkuire.js"); +WorkerMain.main(); diff --git a/scaladoc/resources/dotty_res/scripts/inkuire.md b/scaladoc/resources/dotty_res/scripts/inkuire.md new file mode 100644 index 000000000000..387f58e788ef --- /dev/null +++ b/scaladoc/resources/dotty_res/scripts/inkuire.md @@ -0,0 +1,59 @@ +# Inkuire + +Inkuire is a Hoogle-like search engine for Scala 3 (and Kotlin). + +# Usage + +To include Inkuire in scaladoc one should add `-Ygenerate-inkuire` flag. This will allow the usage of Inkuire for generated Scaladoc with project sources and available sources from external-mappings. + +The Inkuire worker works in Scaladoc searchbar. It is triggered once an input containing `=>` is detected in searchbar. There is 1s debounce on searches with Inkuire. + +## Generated Files + +When including `-Ygenerate-inkuire` flag database for project sources and a config should be generated. Which are namely files: `inkuire-db.json` and `scripts/inkuire-config.json`. Config file includes addresses of possible inkuire-db files. There always is at least one - generated one. But also links for external mappings are addes on relative path `../inkuire-db.json`. + +`inkuire-db.json` contains a json with Inkuire engine's representation of: +- types (or rather classes and objects for now) +- functions +- implicit conversions + + +## Code + +The source code is available [here](https://github.com/VirtusLab/Inkuire). + +Important parts are: +- engineCommon - provides most of the logic for search engine +- engineJs - provides the way of using Inkuire as a Web Worker + +## Integration with Scaladoc + +Since Inkuire has quite a lot of dependencies it's sources cannot be easily integrated into Scaladoc. +That is why Inkuire is included as a resource, namely `inkuire.js` file and loaded as Web Worker. + +Web worker accepts String messages. Each message should be a requested signature. +Web worker can send different messages: +- engine_ready - sent after database has been loaded +- new_query - sent once a new signature is accepted +- query_ended(`msg`) - After processing a single signature has finished. `msg` is optional and contains an error to be displayed. +- json of format [`ResultFormat`](https://github.com/VirtusLab/Inkuire/blob/68d1e0bb2732deda714de9cfca3fe45f75fb5239/engineCommon/shared/src/main/scala/org/virtuslab/inkuire/engine/common/model/OutputFormat.scala#L12) - resulting function found with some additional information, like package location and documentation link. + +## Input format + +Signature format accepted by Inkuire is pretty much a Scala curried function. With some minor changes: +- Types with names as single letters or single letters with digits are considered by default type variables. But other type variables can be declared with [polymorphic function types syntax](https://dotty.epfl.ch/docs/reference/new-types/polymorphic-function-types.html). +- `_` is treated as sort of wildcard, so matches to any type on any position. So searching for any one-argument function from `Int` can be done like this: `Int => _`. + +Some example signatures with expected (not exclusive)results: +- `Seq[Int] => (Int => Long) => Seq[Long]` -> `IterableOps.map` +- `(A, B) => A` -> `Product2._1` +- `Set[Long] => Long => Boolean` -> `SetOps.contains` +- `BigDecimal => Byte` -> `ScalaNumericAnyConversions.toByte` +- `Int => Long => Int` -> `Function.const` +- `String => Int => Char` -> `StringOps.apply` + +## Signature processing + +Some improvements done on engine side: +- first argument can be interpreted as a receiver. More specifically there is no difference between a function on type `A` and a function with the first parameter as `A` (provided other parameters and return type are the same). +- permutating arguments. This may have to be dropped/limited in the future. But for now every permutation of arguments can be matched, so for example: `Int => String => String` matches `Function.const`, even though the arguments are switched. \ No newline at end of file diff --git a/scaladoc/resources/dotty_res/styles/colors.css b/scaladoc/resources/dotty_res/styles/colors.css index 5712bbf21474..d6de713b0cbe 100644 --- a/scaladoc/resources/dotty_res/styles/colors.css +++ b/scaladoc/resources/dotty_res/styles/colors.css @@ -80,10 +80,14 @@ --icon-color: var(--grey400); --selected-fg: var(--blue900); --selected-bg: var(--blue200); + + --shadow: var(--black); } /* Dark Mode */ :root.theme-dark { + color-scheme: dark; + --border-light: var(--blue800); --border-medium: var(--blue700); @@ -124,4 +128,6 @@ --tab-selected: var(--white); --tab-default: var(--grey300); + + --shadow: var(--white); } diff --git a/scaladoc/resources/dotty_res/styles/scalastyle.css b/scaladoc/resources/dotty_res/styles/scalastyle.css index 11bb67d7421e..679fbc846fb7 100644 --- a/scaladoc/resources/dotty_res/styles/scalastyle.css +++ b/scaladoc/resources/dotty_res/styles/scalastyle.css @@ -187,6 +187,20 @@ th { margin-left: -16px; } +.theme-dark .scaladoc_logo { + display: none; +} + +.scaladoc_logo_dark { + display: none; +} + +.theme-dark .scaladoc_logo_dark { + width: 116px; + margin-left: -16px; + display: block; +} + /* Navigation */ #sideMenu2 { overflow: auto; @@ -491,7 +505,7 @@ footer { border-top: 1px solid var(--border-light); font-size: 14px; } -.theme-dark footer img { +.theme-dark footer .social-icon { /* "Poor man's dark mode" for images. * This works great with black images, * and just-okay with colored images. diff --git a/scaladoc/resources/dotty_res/styles/search-bar.css b/scaladoc/resources/dotty_res/styles/search-bar.css index fc5c7ed45c15..cb0167d43503 100644 --- a/scaladoc/resources/dotty_res/styles/search-bar.css +++ b/scaladoc/resources/dotty_res/styles/search-bar.css @@ -63,3 +63,47 @@ width: auto !important; } } + +/* Loading */ +.loading-wrapper { + align-self: center; + padding: 4px; +} + +.loading, .loading::before, .loading::after { + content: ''; + width: 10px; + height: 10px; + border-radius: 5px; + background-color: white; + color: white; + animation: dotFlashing 1s infinite alternate; + display: inline-block; + position: absolute; + top: 0; +} + +.loading { + left: 50%; + position: relative; + animation-delay: .5s; +} + +.loading::before { + left: -15px; + animation-delay: 0s; +} + +.loading::after { + left: 15px; + animation-delay: 1s; +} + +@keyframes dotFlashing { + 0% { + background-color: var(--leftbar-bg); + } + 100% { + background-color: white; + } +} \ No newline at end of file diff --git a/scaladoc/src/dotty/tools/scaladoc/Inkuire.scala b/scaladoc/src/dotty/tools/scaladoc/Inkuire.scala new file mode 100644 index 000000000000..1d4da3af6928 --- /dev/null +++ b/scaladoc/src/dotty/tools/scaladoc/Inkuire.scala @@ -0,0 +1,246 @@ +package dotty.tools.scaladoc + +import dotty.tools.scaladoc.util._ +import scala.collection.mutable.{ Map => MMap} + +object Inkuire { + + var db = InkuireDb(Seq.empty, Map.empty, Seq.empty) + + def generateInkuireConfig(externalMappings: Seq[String]): String = { + val paths = ("../inkuire-db.json" +: externalMappings.map(_ + "../inkuire-db.json")).map(jsonString) + jsonObject(("inkuirePaths", jsonList(paths))).toString + } + + case class InkuireDb( + functions: Seq[ExternalSignature], + types: Map[ITID, (Type, Seq[Type])], + implicitConversions: Seq[(ITID, Type)] + ) + + case class ITID(uuid: String, isParsed: Boolean) + + case class Signature( + receiver: Option[Contravariance], + arguments: Seq[Contravariance], + result: Covariance, + context: SignatureContext + ) { + def typesWithVariances: Seq[Variance] = receiver.toSeq ++ arguments ++ Seq(result) + } + + object Signature { + def apply(receiver: Option[Type], arguments: Seq[Type], result: Type, context: SignatureContext): Signature = + Signature(receiver.map(Contravariance(_)), arguments.map(Contravariance(_)), Covariance(result), context) + } + + case class ExternalSignature( + signature: Signature, + name: String, + packageName: String, + uri: String + ) + + case class Type( + name: TypeName, + params: Seq[Variance] = Seq.empty, + nullable: Boolean = false, + itid: Option[ITID] = None, + isVariable: Boolean = false, + isStarProjection: Boolean = false, + isUnresolved: Boolean = false + ) + + object Type { + def unresolved: Type = + Type( + name = TypeName(""), + itid = Some( + ITID( + uuid = "", + isParsed = false + ) + ) + ) + } + + case class TypeName(name: String) { + override def hashCode: Int = name.toLowerCase.hashCode + + override def equals(obj: Any): Boolean = { + obj match { + case o: TypeName => this.name.toLowerCase == o.name.toLowerCase + case _ => false + } + } + + override def toString: String = name + } + + case class SignatureContext( + vars: Set[String], + constraints: Map[String, Seq[Type]] + ) { + override def hashCode: Int = vars.size.hashCode + + override def equals(obj: Any): Boolean = + obj match { + case other: SignatureContext if this.vars.size == other.vars.size => true + case _ => false + } + } + + object SignatureContext { + def empty: SignatureContext = SignatureContext(Set.empty, Map.empty) + } + + sealed abstract class Variance { + val typ: Type + } + + case class Covariance(typ: Type) extends Variance + + case class Contravariance(typ: Type) extends Variance + + case class Invariance(typ: Type) extends Variance + + case class UnresolvedVariance(typ: Type) extends Variance + + object EngineModelSerializers { + def serialize(db: InkuireDb): JSON = { + jsonObject( + ("types", serialize(db.types)), + ("functions", jsonList(db.functions.map(serialize))), + ("implicitConversions", jsonList(db.implicitConversions.map(serializeConversion))) + ) + } + + private def serializeConversion(conversion: (ITID, Type)): JSON = { + jsonList( + Seq( + serialize(conversion._1), + serialize(conversion._2) + ) + ) + } + + private def serialize(types: Map[ITID, (Type, Seq[Type])]): JSON = { + jsonObject(( + types.toList.map { + case (itid, v) => + (serializeAsKey(itid), serialize(v)) + } + )*) + } + + private def serializeAsKey(itid: ITID): String = { + s"""${itid.isParsed}=${itid.uuid}""" + } + + private def serialize(v: (Type, Seq[Type])): JSON = { + jsonList( + Seq( + serialize(v._1), + jsonList(v._2.map(serialize)) + ) + ) + } + + private def serialize(t: Type): JSON = { + jsonObject( + ("name", serialize(t.name)), + ("params", jsonList(t.params.map(serialize))), + ("nullable", serialize(t.nullable)), + ("itid", serialize(t.itid.get)), + ("isVariable", serialize(t.isVariable)), + ("isStarProjection", serialize(t.isStarProjection)), + ("isUnresolved", serialize(t.isUnresolved)) + ) + } + + private def serialize(b: Boolean): JSON = { + if b then rawJSON("true") else rawJSON("false") + } + + private def serialize(itid: ITID): JSON = { + jsonObject( + ("uuid", serialize(itid.uuid)), + ("isParsed", serialize(itid.isParsed)) + ) + } + + private def serialize(s: TypeName): JSON = { + jsonObject( + ("name", serialize(s.name)) + ) + } + + private def serialize(v: Variance): JSON = v match { + case _: Invariance => + jsonObject( + ("typ", serialize(v.typ)), + ("variancekind", serialize("invariance")) + ) + case _: Covariance => + jsonObject( + ("typ", serialize(v.typ)), + ("variancekind", serialize("covariance")) + ) + case _: Contravariance => + jsonObject( + ("typ", serialize(v.typ)), + ("variancekind", serialize("contravariance")) + ) + case _: UnresolvedVariance => + jsonObject( + ("typ", serialize(v.typ)), + ("variancekind", serialize("unresolved")) + ) + } + + private def serialize(e: ExternalSignature): JSON = { + jsonObject( + ("signature", serialize(e.signature)), + ("name", serialize(e.name)), + ("packageName", serialize(e.packageName)), + ("uri", serialize(e.uri)) + ) + } + + private def serialize(s: String): JSON = { + jsonString(s) + } + + private def serialize(s: Signature): JSON = { + jsonObject( + ("receiver", serialize(s.receiver)), + ("arguments", jsonList(s.arguments.map(serialize))), + ("result", serialize(s.result)), + ("context", serialize(s.context)) + ) + } + + private def serialize(o: Option[Contravariance]): JSON = { + o.fold(rawJSON("null")) { v => + serialize(v) + } + } + + private def serialize(c: SignatureContext): JSON = { + jsonObject( + ("vars", jsonList(c.vars.toSeq.map(serialize))), + ("constraints", serializeConstraints(c.constraints)) + ) + } + + private def serializeConstraints(constraints: Map[String, Seq[Type]]): JSON = { + jsonObject(( + constraints.toList.map { + case (name, vs) => + (name, jsonList(vs.map(serialize))) + } + )*) + } + } + +} diff --git a/scaladoc/src/dotty/tools/scaladoc/Scaladoc.scala b/scaladoc/src/dotty/tools/scaladoc/Scaladoc.scala index 1c2610ca62d7..1b357d385172 100644 --- a/scaladoc/src/dotty/tools/scaladoc/Scaladoc.scala +++ b/scaladoc/src/dotty/tools/scaladoc/Scaladoc.scala @@ -2,17 +2,21 @@ package dotty.tools.scaladoc import java.util.ServiceLoader import java.io.File +import java.io.FileWriter import java.util.jar._ import collection.JavaConverters._ import collection.immutable.ArraySeq -import java.nio.file.Files +import java.nio.file.{ Files, Paths } import dotty.tools.dotc.config.Settings._ import dotty.tools.dotc.config.{ CommonScalaSettings, AllScalaSettings } import dotty.tools.dotc.reporting.Reporter import dotty.tools.dotc.core.Contexts._ +import dotty.tools.scaladoc.Inkuire +import dotty.tools.scaladoc.Inkuire._ + object Scaladoc: enum CommentSyntax: case Wiki @@ -53,7 +57,8 @@ object Scaladoc: snippetCompiler: List[String] = Nil, snippetCompilerDebug: Boolean = false, noLinkWarnings: Boolean = false, - versionsDictionaryUrl: Option[String] = None + versionsDictionaryUrl: Option[String] = None, + generateInkuire : Boolean = false ) def run(args: Array[String], rootContext: CompilerContext): Reporter = @@ -78,9 +83,26 @@ object Scaladoc: report.inform("Done") else report.error("Failure") + if parsedArgs.generateInkuire then dumpInkuireDB(parsedArgs.output.getAbsolutePath, parsedArgs) } + ctx.reporter + def dumpInkuireDB(output: String, parsedArgs: Args) = { + val dbPath = Paths.get(output, "inkuire-db.json") + val dbFile = dbPath.toFile() + dbFile.createNewFile() + val dbWriter = new FileWriter(dbFile, false) + dbWriter.write(s"${EngineModelSerializers.serialize(Inkuire.db)}") + dbWriter.close() + + val configPath = Paths.get(output, "scripts/inkuire-config.json") + val configFile = configPath.toFile() + configFile.createNewFile() + val configWriter = new FileWriter(configFile, false) + configWriter.write(Inkuire.generateInkuireConfig(parsedArgs.externalMappings.map(_.documentationUrl.toString))) + configWriter.close() + } def extract(args: Array[String], rootCtx: CompilerContext): (Option[Scaladoc.Args], CompilerContext) = val newContext = rootCtx.fresh @@ -199,7 +221,8 @@ object Scaladoc: snippetCompiler.get, noLinkWarnings.get, snippetCompilerDebug.get, - versionsDictionaryUrl.nonDefault + versionsDictionaryUrl.nonDefault, + generateInkuire.get ) (Some(docArgs), newContext) } diff --git a/scaladoc/src/dotty/tools/scaladoc/ScaladocSettings.scala b/scaladoc/src/dotty/tools/scaladoc/ScaladocSettings.scala index 742d51ee8441..b5b725ffcca0 100644 --- a/scaladoc/src/dotty/tools/scaladoc/ScaladocSettings.scala +++ b/scaladoc/src/dotty/tools/scaladoc/ScaladocSettings.scala @@ -117,5 +117,8 @@ class ScaladocSettings extends SettingGroup with AllScalaSettings: val snippetCompilerDebug: Setting[Boolean] = BooleanSetting("-Ysnippet-compiler-debug", snippets.SnippetCompilerArgs.debugUsage, false) + val generateInkuire: Setting[Boolean] = + BooleanSetting("-Ygenerate-inkuire", "Generates InkuireDB and enables Hoogle-like searches", false) + def scaladocSpecificSettings: Set[Setting[_]] = - Set(sourceLinks, syntax, revision, externalDocumentationMappings, socialLinks, skipById, skipByRegex, deprecatedSkipPackages, docRootContent, snippetCompiler, snippetCompilerDebug) + Set(sourceLinks, syntax, revision, externalDocumentationMappings, socialLinks, skipById, skipByRegex, deprecatedSkipPackages, docRootContent, snippetCompiler, snippetCompilerDebug, generateInkuire) diff --git a/scaladoc/src/dotty/tools/scaladoc/renderers/HtmlRenderer.scala b/scaladoc/src/dotty/tools/scaladoc/renderers/HtmlRenderer.scala index e73d515c7de9..08e35dc3024c 100644 --- a/scaladoc/src/dotty/tools/scaladoc/renderers/HtmlRenderer.scala +++ b/scaladoc/src/dotty/tools/scaladoc/renderers/HtmlRenderer.scala @@ -289,6 +289,11 @@ class HtmlRenderer(rootPackage: Member, val members: Map[DRI, Member])(using ctx src := resolveRoot(link.dri, "images/scaladoc_logo.svg"), alt := "scaladoc", cls := "scaladoc_logo" + ), + img( + src := resolveRoot(link.dri, "images/scaladoc_logo_dark.svg"), + alt := "scaladoc", + cls := "scaladoc_logo_dark" ) ) ), diff --git a/scaladoc/src/dotty/tools/scaladoc/renderers/Resources.scala b/scaladoc/src/dotty/tools/scaladoc/renderers/Resources.scala index e170dda5394b..99051b20645d 100644 --- a/scaladoc/src/dotty/tools/scaladoc/renderers/Resources.scala +++ b/scaladoc/src/dotty/tools/scaladoc/renderers/Resources.scala @@ -21,6 +21,7 @@ enum Resource(val path: String): case Classpath(override val path: String, name: String) extends Resource(path) case File(override val path: String, file: Path) extends Resource(path) case URL(url: String) extends Resource(url) + case URLToCopy(url: String, dest: String) extends Resource(url) trait Resources(using ctx: DocContext) extends Locations, Writer: private def dynamicJsData = @@ -77,7 +78,8 @@ trait Resources(using ctx: DocContext) extends Locations, Writer: "scripts/components/Input.js", "scripts/components/FilterGroup.js", "scripts/components/Filter.js", - "scripts/searchbar.js" + "scripts/searchbar.js", + "scripts/inkuire-worker.js" ).map(dottyRes) val urls = List( @@ -87,7 +89,13 @@ trait Resources(using ctx: DocContext) extends Locations, Writer: "https://cdnjs.cloudflare.com/ajax/libs/dagre-d3/0.6.1/dagre-d3.min.js", ).map(Resource.URL.apply) - fromResources ++ urls ++ projectLogo ++ Seq(scaladocVersionFile, dynamicJsData) + val urlToPathMappings = List( + ("https://github.com/VirtusLab/Inkuire/releases/download/1.0.0-M1/inkuire.js", "scripts/inkuire.js"), + ).map { case (url, path) => + Resource.URLToCopy(url, path) + } + + fromResources ++ urls ++ urlToPathMappings ++ projectLogo ++ Seq(scaladocVersionFile, dynamicJsData) val searchDataPath = "scripts/searchData.js" val memberResourcesPaths = Seq(searchDataPath) ++ memberResources.map(_.path) @@ -136,6 +144,7 @@ trait Resources(using ctx: DocContext) extends Locations, Writer: dottyRes("fonts/dotty-icons.woff"), dottyRes("fonts/dotty-icons.ttf"), dottyRes("images/scaladoc_logo.svg"), + dottyRes("images/scaladoc_logo_dark.svg"), dottyRes("images/class.svg"), dottyRes("images/class_comp.svg"), dottyRes("images/object.svg"), @@ -176,3 +185,5 @@ trait Resources(using ctx: DocContext) extends Locations, Writer: Seq(copy(file, path)) case Resource.URL(url) => Nil + case Resource.URLToCopy(url, dest) => + Seq(copy(new URL(url).openStream(), dest)) diff --git a/scaladoc/src/dotty/tools/scaladoc/tasty/ClassLikeSupport.scala b/scaladoc/src/dotty/tools/scaladoc/tasty/ClassLikeSupport.scala index 27f023fecc39..53cf0ced6fbb 100644 --- a/scaladoc/src/dotty/tools/scaladoc/tasty/ClassLikeSupport.scala +++ b/scaladoc/src/dotty/tools/scaladoc/tasty/ClassLikeSupport.scala @@ -3,6 +3,7 @@ package dotty.tools.scaladoc.tasty import collection.JavaConverters._ import dotty.tools.scaladoc._ import dotty.tools.scaladoc.{Signature => DSignature} +import dotty.tools.scaladoc.Inkuire import scala.quoted._ @@ -93,6 +94,77 @@ trait ClassLikeSupport: deprecated = classDef.symbol.isDeprecated() ) + if summon[DocContext].args.generateInkuire then { + + val classType = classDef.asInkuire(Set.empty, true) + val variableNames = classType.params.map(_.typ.name.name).toSet + + val parents = classDef.parents.map(_.asInkuire(variableNames, false)) + + val isModule = classDef.symbol.flags.is(Flags.Module) + + if !isModule then Inkuire.db = Inkuire.db.copy(types = Inkuire.db.types.updated(classType.itid.get, (classType, parents))) + + classDef.symbol.declaredTypes.foreach { + case typeSymbol: Symbol => + val typeDef = typeSymbol.tree.asInstanceOf[TypeDef] + if typeDef.rhs.symbol.fullName.contains("java") then + val t = typeSymbol.tree.asInkuire(variableNames, false) // TODO [Inkuire] Hack until type aliases are supported + val tJava = typeDef.rhs.symbol.tree.asInkuire(variableNames, false) + Inkuire.db = Inkuire.db.copy(types = Inkuire.db.types.updated(t.itid.get, (t, Seq.empty))) // TODO [Inkuire] Hack until type aliases are supported + Inkuire.db = Inkuire.db.copy(types = Inkuire.db.types.updated(tJava.itid.get, (tJava, Seq.empty))) + } + + classDef.symbol.declaredMethods + .filter { (s: Symbol) => + !s.flags.is(Flags.Private) && + !s.flags.is(Flags.Protected) && + !s.flags.is(Flags.Override) + } + .foreach { + case implicitConversion: Symbol if implicitConversion.flags.is(Flags.Implicit) + && classDef.symbol.flags.is(Flags.Module) + && implicitConversion.owner.fullName == ("scala.Predef$") => + val defdef = implicitConversion.tree.asInstanceOf[DefDef] + val to = defdef.returnTpt.asInkuire(variableNames, false) + val from = defdef.paramss.flatMap(_.params).collectFirst { + case v: ValDef => v.tpt.asInkuire(variableNames, false) + } + from match + case Some(from) => Inkuire.db = Inkuire.db.copy(implicitConversions = Inkuire.db.implicitConversions :+ (from.itid.get -> to)) + case None => + + case methodSymbol: Symbol => + val defdef = methodSymbol.tree.asInstanceOf[DefDef] + val methodVars = defdef.paramss.flatMap(_.params).collect { + case TypeDef(name, _) => name + } + val vars = variableNames ++ methodVars + val receiver: Option[Inkuire.Type] = + Some(classType) + .filter(_ => !isModule) + .orElse(methodSymbol.extendedSymbol.flatMap(s => partialAsInkuire(vars, false).lift(s.tpt))) + val sgn = Inkuire.ExternalSignature( + signature = Inkuire.Signature( + receiver = receiver, + arguments = methodSymbol.nonExtensionParamLists.flatMap(_.params).collect { + case ValDef(_, tpe, _) => tpe.asInkuire(vars, false) + }, + result = defdef.returnTpt.asInkuire(vars, false), + context = Inkuire.SignatureContext( + vars = vars.toSet, + constraints = Map.empty //TODO [Inkuire] Type bounds + ) + ), + name = methodSymbol.name, + packageName = methodSymbol.dri.location, + uri = methodSymbol.dri.externalLink.getOrElse("") + ) + Inkuire.db = Inkuire.db.copy(functions = Inkuire.db.functions :+ sgn) + } + + } + if signatureOnly then baseMember else baseMember.copy( members = classDef.extractPatchedMembers.sortBy(m => (m.name, m.kind.name)), directParents = classDef.getParentsAsLinkToTypes, @@ -341,13 +413,7 @@ trait ClassLikeSupport: specificKind: (Kind.Def => Kind) = identity ): Member = val method = methodSymbol.tree.asInstanceOf[DefDef] - val paramLists: List[TermParamClause] = - if emptyParamsList then Nil - else if methodSymbol.isExtensionMethod then - val params = method.termParamss - if methodSymbol.isLeftAssoc || params.size == 1 then params.tail - else params.head :: params.tail.drop(1) - else method.termParamss + val paramLists: List[TermParamClause] = methodSymbol.nonExtensionParamLists val genericTypes = if (methodSymbol.isClassConstructor) Nil else method.leadingTypeParams val memberInfo = unwrapMemberInfo(c, methodSymbol) diff --git a/scaladoc/src/dotty/tools/scaladoc/tasty/InkuireSupport.scala b/scaladoc/src/dotty/tools/scaladoc/tasty/InkuireSupport.scala new file mode 100644 index 000000000000..fbb466ffc1e6 --- /dev/null +++ b/scaladoc/src/dotty/tools/scaladoc/tasty/InkuireSupport.scala @@ -0,0 +1,155 @@ +package dotty.tools.scaladoc.tasty + +import collection.JavaConverters._ +import dotty.tools.scaladoc._ +import dotty.tools.scaladoc.{Signature => DSignature} +import dotty.tools.scaladoc.Inkuire + +import scala.quoted._ + +import SymOps._ +import NameNormalizer._ +import SyntheticsSupport._ + +trait InkuireSupport: + self: TastyParser => + import qctx.reflect._ + + private given qctx.type = qctx + + private def paramsForClass(classDef: ClassDef, vars: Set[String], isVariable: Boolean): Seq[Inkuire.Variance] = + classDef.getTypeParams.map(mkTypeArgumentInkuire(_, vars, isVariable)) + + given TreeSyntaxInkuire: AnyRef with + extension (tpeTree: Tree) + def asInkuire(vars: Set[String], isVariable: Boolean): Inkuire.Type = + partialAsInkuire(vars, isVariable)(tpeTree) + + def partialAsInkuire(vars: Set[String], isVariable: Boolean): PartialFunction[Tree, Inkuire.Type] = { + case TypeBoundsTree(low, high) => inner(low.tpe, vars) //TODO [Inkuire] Type bounds + case tpeTree: Applied => + inner(tpeTree.tpe, vars).copy( + params = tpeTree.args.map(p => Inkuire.Invariance(p.asInkuire(vars, isVariable))) + ) + case tpeTree: TypeTree => + inner(tpeTree.tpe, vars) + case term: Term => inner(term.tpe, vars) + case classDef: ClassDef => mkTypeFromClassDef(classDef, vars, isVariable) + case typeDef: TypeDef => + Inkuire.Type( + name = Inkuire.TypeName(typeDef.name), + itid = typeDef.symbol.itid + ) + } + + def mkTypeFromClassDef(classDef: ClassDef, vars: Set[String], isVariable: Boolean): Inkuire.Type = { + Inkuire.Type( + name = Inkuire.TypeName(classDef.name), + itid = classDef.symbol.itid, + params = paramsForClass(classDef, vars, isVariable) + ) + } + + given SymbolSyntaxInkuire: AnyRef with + extension (symbol: Symbol) + def itid(using dctx: DocContext): Option[Inkuire.ITID] = Some(Inkuire.ITID(symbol.dri.symbolUUID, isParsed = false)) + + given TypeSyntaxInkuire: AnyRef with + extension (tpe: TypeRepr) + def asInkuire(vars: Set[String]): Inkuire.Type = inner(tpe, vars) + + def mkTypeArgumentInkuire(argument: TypeDef, vars: Set[String] = Set.empty, isVariable: Boolean = false): Inkuire.Variance = + val name = argument.symbol.normalizedName + val normalizedName = if name.matches("_\\$\\d*") then "_" else name + val t = Inkuire.Type( + name = Inkuire.TypeName(normalizedName), + itid = argument.symbol.itid, + isVariable = vars.contains(normalizedName) || isVariable, + params = Seq.empty //TODO [Inkuire] Type Lambdas + ) + if argument.symbol.flags.is(Flags.Covariant) then Inkuire.Covariance(t) + else if argument.symbol.flags.is(Flags.Contravariant) then Inkuire.Contravariance(t) + else Inkuire.Invariance(t) + + private def isRepeatedAnnotation(term: Term) = + term.tpe match + case t: TypeRef => t.name == "Repeated" && t.qualifier.match + case ThisType(tref: TypeRef) if tref.name == "internal" => true + case _ => false + case _ => false + + private def isRepeated(typeRepr: TypeRepr) = + typeRepr match + case t: TypeRef => t.name == "" && t.qualifier.match + case ThisType(tref: TypeRef) if tref.name == "scala" => true + case _ => false + case _ => false + + private def inner(tp: TypeRepr, vars: Set[String]): Inkuire.Type = tp match + case OrType(left, right) => inner(left, vars) //TODO [Inkuire] Or/AndTypes + case AndType(left, right) => inner(left, vars) //TODO [Inkuire] Or/AndTypes + case ByNameType(tpe) => inner(tpe, vars) + case ConstantType(constant) => + Inkuire.Type( + name = Inkuire.TypeName(constant.toString), + params = Seq.empty, + itid = Some(Inkuire.ITID(constant.toString, isParsed = false)) + ) + case ThisType(tpe) => inner(tpe, vars) + case AnnotatedType(AppliedType(_, Seq(tpe)), annotation) if isRepeatedAnnotation(annotation) => + inner(tpe, vars) //TODO [Inkuire] Repeated types + case AppliedType(repeatedClass, Seq(tpe)) if isRepeated(repeatedClass) => + inner(tpe, vars) //TODO [Inkuire] Repeated types + case AnnotatedType(tpe, _) => + inner(tpe, vars) + case tl @ TypeLambda(params, paramBounds, resType) => + inner(resType, vars) //TODO [Inkuire] Type lambdas + case r: Refinement => + inner(r.info, vars) //TODO [Inkuire] Refinements + case t @ AppliedType(tpe, typeList) => + import dotty.tools.dotc.util.Chars._ + if t.isFunctionType then + val name = s"Function${typeList.size-1}" + Inkuire.Type( + name = Inkuire.TypeName(name), + params = typeList.init.map(p => Inkuire.Contravariance(inner(p, vars))) :+ Inkuire.Covariance(inner(typeList.last, vars)), + itid = Some(Inkuire.ITID(s"${name}scala.${name}//[]", isParsed = false)) + ) + else if t.isTupleType then + val name = s"Tuple${typeList.size}" + Inkuire.Type( + name = Inkuire.TypeName(name), + params = typeList.map(p => Inkuire.Covariance(inner(p, vars))), + itid = Some(Inkuire.ITID(s"${name}scala.${name}//[]", isParsed = false)) + ) + else + inner(tpe, vars).copy( + params = typeList.map(p => Inkuire.Invariance(inner(p, vars))) + ) + case tp: TypeRef => + Inkuire.Type( + name = Inkuire.TypeName(tp.name), + itid = tp.typeSymbol.itid, + params = Seq.empty, + isVariable = vars.contains(tp.name) + ) + case tr @ TermRef(qual, typeName) => + inner(qual, vars) + case TypeBounds(low, hi) => + inner(low, vars) //TODO [Inkuire] Type bounds + case NoPrefix() => + Inkuire.Type.unresolved //TODO [Inkuire] <- should be handled by Singleton case, but didn't work + case MatchType(bond, sc, cases) => + inner(sc, vars) + case ParamRef(TypeLambda(names, _, resType), i) => + Inkuire.Type( + name = Inkuire.TypeName(names(i)), + itid = Some(Inkuire.ITID(s"external-itid-${names(i)}", isParsed = false)), + isVariable = true + ) + case ParamRef(m: MethodType, i) => + inner(m.paramTypes(i), vars) + case RecursiveType(tp) => + inner(tp, vars) + case MethodType(_, params, resType) => + inner(resType, vars) //TODO [Inkuire] Method type diff --git a/scaladoc/src/dotty/tools/scaladoc/tasty/SymOps.scala b/scaladoc/src/dotty/tools/scaladoc/tasty/SymOps.scala index 72c8a8073d8a..17500897b238 100644 --- a/scaladoc/src/dotty/tools/scaladoc/tasty/SymOps.scala +++ b/scaladoc/src/dotty/tools/scaladoc/tasty/SymOps.scala @@ -141,6 +141,15 @@ object SymOps: else termParamss(1).params(0) } + def nonExtensionParamLists: List[reflect.TermParamClause] = + import reflect.* + val method = sym.tree.asInstanceOf[DefDef] + if sym.isExtensionMethod then + val params = method.termParamss + if sym.isLeftAssoc || params.size == 1 then params.tail + else params.head :: params.tail.drop(1) + else method.termParamss + end extension end SymOps diff --git a/scaladoc/src/dotty/tools/scaladoc/tasty/TastyParser.scala b/scaladoc/src/dotty/tools/scaladoc/tasty/TastyParser.scala index 69d83e126b36..12b1c013e4fd 100644 --- a/scaladoc/src/dotty/tools/scaladoc/tasty/TastyParser.scala +++ b/scaladoc/src/dotty/tools/scaladoc/tasty/TastyParser.scala @@ -178,7 +178,7 @@ case class TastyParser( isSkipped: qctx.reflect.Symbol => Boolean )( using val ctx: DocContext -) extends BasicSupport with TypesSupport with ClassLikeSupport with PackageSupport: +) extends BasicSupport with TypesSupport with ClassLikeSupport with PackageSupport with InkuireSupport: import qctx.reflect._ private given qctx.type = qctx diff --git a/scaladoc/src/dotty/tools/scaladoc/util/JSON.scala b/scaladoc/src/dotty/tools/scaladoc/util/JSON.scala index d822bf4cf898..468300db6616 100644 --- a/scaladoc/src/dotty/tools/scaladoc/util/JSON.scala +++ b/scaladoc/src/dotty/tools/scaladoc/util/JSON.scala @@ -5,6 +5,8 @@ import scala.annotation.tailrec opaque type JSON = String +def rawJSON(s: String): JSON = s + def jsonList(elems: Seq[JSON]): JSON = elems.mkString("[", ",\n", "]") def jsonObject(fields: (String, JSON)*): JSON =