Skip to content

Commit 83b3bf5

Browse files
pikinier20romanowski
authored andcommitted
Add external location providing between scala3doc instances
1 parent 9effbe4 commit 83b3bf5

10 files changed

+272
-5
lines changed

scala3doc/src/dotty/dokka/DocContext.scala

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import dotty.tools.dotc.util.Spans
1818
import java.io.ByteArrayOutputStream
1919
import java.io.PrintStream
2020
import scala.io.Codec
21+
import java.net.URL
2122

2223
type CompilerContext = dotty.tools.dotc.core.Contexts.Context
2324

@@ -93,6 +94,24 @@ case class DocContext(args: Scala3doc.Args, compilerContext: CompilerContext)
9394
sourceLinks
9495
)(using compilerContext))
9596

97+
val externalDocumentationLinks: List[Scala3docExternalDocumentationLink] = List(
98+
Scala3docExternalDocumentationLink(
99+
List(raw".*scala\/quoted.*".r),
100+
new URL("http://127.0.0.1:5500/scala3doc/output/scala3/"),
101+
DocumentationKind.Scala3doc
102+
).withPackageList(new URL("http://127.0.0.1:5500/scala3doc/output/scala3/-scala%203/package-list")),
103+
Scala3docExternalDocumentationLink(
104+
List(raw".*java.*".r),
105+
new URL("https://docs.oracle.com/javase/8/docs/api/"),
106+
DocumentationKind.Javadoc
107+
).withPackageList(new URL("https://docs.oracle.com/javase/8/docs/api/package-list")),
108+
Scala3docExternalDocumentationLink(
109+
List(raw".*scala.*".r),
110+
new URL("https://www.scala-lang.org/api/current/"),
111+
DocumentationKind.Scaladoc
112+
)
113+
)
114+
96115
override def getPluginsConfiguration: JList[DokkaConfiguration.PluginConfiguration] =
97116
JNil
98117

scala3doc/src/dotty/dokka/DottyDokkaPlugin.scala

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import org.jetbrains.dokka.pages._
2222
import dotty.dokka.model.api._
2323
import org.jetbrains.dokka.CoreExtensions
2424
import org.jetbrains.dokka.base.DokkaBase
25+
import org.jetbrains.dokka.base.resolvers.shared._
2526

2627
import dotty.dokka.site.NavigationCreator
2728
import dotty.dokka.site.SitePagesCreator
@@ -71,6 +72,7 @@ class DottyDokkaPlugin extends DokkaJavaPlugin:
7172
val scalaResourceInstaller = extend(
7273
_.extensionPoint(dokkaBase.getHtmlPreprocessors)
7374
.fromRecipe{ case ctx @ given DokkaContext => new ScalaResourceInstaller }
75+
.name("scalaResourceInstaller")
7476
.after(dokkaBase.getCustomResourceInstaller)
7577
)
7678

@@ -170,6 +172,25 @@ class DottyDokkaPlugin extends DokkaJavaPlugin:
170172
.overrideExtension(dokkaBase.getLocationProvider)
171173
)
172174

175+
val scalaPackageListCreator = extend(
176+
_.extensionPoint(dokkaBase.getHtmlPreprocessors)
177+
.fromRecipe(c => ScalaPackageListCreator(c, RecognizedLinkFormat.DokkaHtml))
178+
.overrideExtension(dokkaBase.getPackageListCreator)
179+
.after(
180+
customDocumentationProvider.getValue
181+
)
182+
)
183+
184+
val scalaExternalLocationProviderFactory = extend(
185+
_.extensionPoint(dokkaBase.getExternalLocationProviderFactory)
186+
.fromRecipe{ case c as given DokkaContext => new ScalaExternalLocationProviderFactory }
187+
.overrideExtension(dokkaBase.getDokkaLocationProvider)
188+
)
189+
190+
extension (ctx: DokkaContext):
191+
def siteContext: Option[StaticSiteContext] = ctx.getConfiguration.asInstanceOf[DocContext].staticSiteContext
192+
def args: Scala3doc.Args = ctx.getConfiguration.asInstanceOf[DocContext].args
193+
173194
// TODO (https://github.com/lampepfl/scala3doc/issues/232): remove once problem is fixed in Dokka
174195
extension [T] (builder: ExtensionBuilder[T])
175196
def ordered(before: Seq[Extension[_, _, _]], after: Seq[Extension[_, _, _]]): ExtensionBuilder[T] =
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
package dotty.dokka
2+
3+
import org.jetbrains.dokka._
4+
import java.net.URL
5+
import scala.util.matching._
6+
7+
case class Scala3docExternalDocumentationLink(
8+
originRegexes: List[Regex],
9+
documentationUrl: URL,
10+
kind: DocumentationKind,
11+
packageListUrl: Option[URL] = None
12+
):
13+
def withPackageList(url: URL): Scala3docExternalDocumentationLink = copy(packageListUrl = Some(url))
14+
15+
enum DocumentationKind:
16+
case Javadoc extends DocumentationKind
17+
case Scaladoc extends DocumentationKind
18+
case Scala3doc extends DocumentationKind
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
package dotty.dokka
2+
3+
import org.jetbrains.dokka.base.resolvers.local._
4+
import org.jetbrains.dokka.base.DokkaBase
5+
import org.jetbrains.dokka.base.resolvers.external._
6+
import org.jetbrains.dokka.base.resolvers.shared._
7+
import org.jetbrains.dokka.base.resolvers.anchors._
8+
import org.jetbrains.dokka.links.DRI
9+
import org.jetbrains.dokka.model.DisplaySourceSet
10+
import org.jetbrains.dokka.pages.RootPageNode
11+
import org.jetbrains.dokka.plugability._
12+
import collection.JavaConverters._
13+
import java.util.{Set => JSet}
14+
15+
16+
class ScalaExternalLocationProvider(
17+
externalDocumentation: ExternalDocumentation,
18+
extension: String,
19+
kind: DocumentationKind
20+
)(using ctx: DokkaContext) extends DefaultExternalLocationProvider(externalDocumentation, extension, ctx):
21+
override def resolve(dri: DRI): String =
22+
Option(externalDocumentation.getPackageList).map(_.getLocations.asScala.toMap).flatMap(_.get(dri.toString))
23+
.fold(constructPath(dri))( l => {
24+
this.getDocURL + l
25+
}
26+
)
27+
28+
private val originRegex = raw"\[origin:(.*)\]".r
29+
30+
override def constructPath(dri: DRI): String = kind match {
31+
case DocumentationKind.Javadoc => constructPathForJavadoc(dri)
32+
case DocumentationKind.Scaladoc => constructPathForScaladoc(dri)
33+
case DocumentationKind.Scala3doc => constructPathForScala3doc(dri)
34+
}
35+
36+
private def constructPathForJavadoc(dri: DRI): String = {
37+
val packagePrefix = dri.getPackageName.replace(".","/")
38+
val origin = originRegex.findFirstIn(dri.getExtra)
39+
val className = origin match {
40+
case Some(path) =>
41+
path.split("/").last.stripSuffix(".class")
42+
case None => dri.getClassNames
43+
}
44+
getDocURL + packagePrefix + "/" + className + extension
45+
}
46+
47+
private def constructPathForScaladoc(dri: DRI): String = {
48+
val packagePrefix = dri.getPackageName.replace(".","/")
49+
val className = dri.getClassNames
50+
getDocURL + packagePrefix + "/" + className + extension
51+
}
52+
53+
private def constructPathForScala3doc(dri: DRI): String = super.constructPath(dri)
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
package dotty.dokka
2+
3+
import org.jetbrains.dokka.base.resolvers.local._
4+
import org.jetbrains.dokka.base.DokkaBase
5+
import org.jetbrains.dokka.base.resolvers.external._
6+
import org.jetbrains.dokka.base.resolvers.shared._
7+
import org.jetbrains.dokka.base.resolvers.anchors._
8+
import org.jetbrains.dokka.links.DRI
9+
import org.jetbrains.dokka.model.DisplaySourceSet
10+
import org.jetbrains.dokka.pages.RootPageNode
11+
import org.jetbrains.dokka.plugability._
12+
import collection.JavaConverters._
13+
import java.util.{Set => JSet}
14+
15+
class ScalaExternalLocationProviderFactory(using ctx: DokkaContext) extends ExternalLocationProviderFactory:
16+
override def getExternalLocationProvider(doc: ExternalDocumentation): ExternalLocationProvider =
17+
ScalaExternalLocationProvider(doc, ".html", DocumentationKind.Scala3doc)
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
package dotty.dokka
2+
3+
import org.jetbrains.dokka.base.renderers._
4+
import org.jetbrains.dokka.base.resolvers.local._
5+
import org.jetbrains.dokka.base._
6+
import org.jetbrains.dokka.links._
7+
import org.jetbrains.dokka.pages._
8+
import org.jetbrains.dokka.plugability._
9+
import collection.JavaConverters._
10+
import dotty.dokka.model.api.withNoOrigin
11+
12+
object ScalaPackageListService:
13+
val DOKKA_PARAM_PREFIX = "$dokka"
14+
15+
class ScalaPackageListService(context: DokkaContext, rootPage: RootPageNode):
16+
import ScalaPackageListService._ //Why I need to do this?
17+
18+
val locationProvider = PluginUtils.querySingle[DokkaBase, LocationProviderFactory](context, _.getLocationProviderFactory)
19+
.getLocationProvider(rootPage)
20+
21+
def createPackageList(format: String, linkExtension: String): String = {
22+
val packages = retrievePackageInfo(rootPage)
23+
val relocations = getRelocations(rootPage)
24+
s"$DOKKA_PARAM_PREFIX.format:$format\n" ++
25+
s"$DOKKA_PARAM_PREFIX.linkExtenstion:$linkExtension\n" ++
26+
relocations.map( (dri, link) =>
27+
s"$DOKKA_PARAM_PREFIX.location:${dri.withNoOrigin.toString}\u001f$link.$linkExtension"
28+
).mkString("","\n","\n") ++
29+
packages.mkString("","\n","\n")
30+
}
31+
32+
private def retrievePackageInfo(current: PageNode): Set[String] = current match {
33+
case p: PackagePageNode => p.getChildren.asScala.toSet.flatMap(retrievePackageInfo) ++ Option(p.getDocumentable.getDri.getPackageName)
34+
case other => other.getChildren.asScala.toSet.flatMap(retrievePackageInfo)
35+
}
36+
37+
private def getRelocations(current: PageNode): List[(DRI, String)] = current match {
38+
case c: ContentPage => getRelocation(c.getDri.asScala.toList, c) ++ c.getChildren.asScala.toList.flatMap(getRelocations)
39+
case other => other.getChildren.asScala.toList.flatMap(getRelocations)
40+
}
41+
42+
private def getRelocation(dris: List[DRI], node: ContentPage): List[(DRI, String)] =
43+
val link = locationProvider.resolve(node, rootPage, true)
44+
dris.map( dri =>
45+
if locationProvider.expectedLocationForDri(dri) != link then Some(dri, link) else None
46+
).flatten

scala3doc/src/dotty/dokka/model/api/api.scala

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -181,6 +181,15 @@ extension (members: Seq[Member]) def byInheritance =
181181
extension (module: DModule)
182182
def driMap: Map[DRI, Member] = ModuleExtension.getFrom(module).fold(Map.empty)(_.driMap)
183183

184+
extension (dri: DRI):
185+
def withNoOrigin = DRI(
186+
dri.getPackageName,
187+
dri.getClassNames,
188+
dri.getCallable,
189+
dri.getTarget,
190+
Option(dri.getExtra).fold(null)(e => raw"\[origin:(.*)\]".r.replaceAllIn(e, ""))
191+
)
192+
184193
case class TastyDocumentableSource(val path: String, val lineNumber: Int)
185194

186195
type DocPart = DocTag
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
package dotty.dokka
2+
3+
import org.jetbrains.dokka.transformers.pages.{PageTransformer}
4+
import org.jetbrains.dokka.base.renderers._
5+
import org.jetbrains.dokka.base.resolvers.local._
6+
import org.jetbrains.dokka.base.resolvers.shared._
7+
import org.jetbrains.dokka.base._
8+
import org.jetbrains.dokka.links._
9+
import org.jetbrains.dokka.pages._
10+
import org.jetbrains.dokka.plugability._
11+
import collection.JavaConverters._
12+
13+
class ScalaPackageListCreator(
14+
context: DokkaContext,
15+
format: LinkFormat,
16+
outputFileNames: List[String] = List("package-list"),
17+
removeModulePrefix: Boolean = true
18+
) extends PageTransformer:
19+
override def invoke(input: RootPageNode) = {
20+
input.modified(input.getName, (input.getChildren.asScala ++ packageList(input)).asJava)
21+
}
22+
23+
private def processPage(page: PageNode, input: RootPageNode): PageNode = page
24+
25+
private def packageList(rootPageNode: RootPageNode): List[RendererSpecificPage] = {
26+
val content = ScalaPackageListService(context, rootPageNode).createPackageList(
27+
format.getFormatName,
28+
format.getLinkExtension
29+
)
30+
outputFileNames.map( name => {
31+
RendererSpecificResourcePage(
32+
s"${rootPageNode.getName}/${name}",
33+
JList(),
34+
RenderingStrategy.Write(content)
35+
)
36+
}
37+
)
38+
}

scala3doc/src/dotty/dokka/site/StaticSiteLocationProvider.scala

Lines changed: 42 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,21 @@
11
package dotty.dokka
22
package site
33

4-
import org.jetbrains.dokka.base.resolvers.local.DokkaLocationProvider
5-
import org.jetbrains.dokka.base.resolvers.local.LocationProvider
6-
import org.jetbrains.dokka.base.resolvers.local.LocationProviderFactory
74
import org.jetbrains.dokka.pages.ContentPage
85
import org.jetbrains.dokka.pages.PageNode
96
import org.jetbrains.dokka.pages.RootPageNode
107
import org.jetbrains.dokka.pages.ModulePage
118
import org.jetbrains.dokka.plugability.DokkaContext
9+
import org.jetbrains.dokka.base.resolvers.external._
10+
import org.jetbrains.dokka.base.resolvers.shared._
11+
import org.jetbrains.dokka.base.resolvers.local._
12+
import org.jetbrains.dokka.model.DisplaySourceSet
13+
import dotty.dokka.model.api.withNoOrigin
1214

1315
import scala.collection.JavaConverters._
1416
import java.nio.file.Paths
1517
import java.nio.file.Path
18+
import scala.util.matching._
1619

1720
class StaticSiteLocationProviderFactory(using ctx: DokkaContext) extends LocationProviderFactory:
1821
override def getLocationProvider(pageNode: RootPageNode): LocationProvider =
@@ -84,4 +87,39 @@ class StaticSiteLocationProvider(pageNode: RootPageNode)(using ctx: DokkaContext
8487
val nodePath = nodePaths.drop(commonPaths) match
8588
case l if l.isEmpty => Seq("index")
8689
case l => l
87-
(contextPath ++ nodePath).mkString("/")
90+
(contextPath ++ nodePath).mkString("/")
91+
92+
val externalLocationProviders: List[(List[Regex], ExternalLocationProvider)] =
93+
val sourceSet = ctx.getConfiguration.getSourceSets.asScala(0)
94+
ctx.getConfiguration
95+
.asInstanceOf[DocContext]
96+
.externalDocumentationLinks
97+
.map { link =>
98+
val emptyExtDoc = ExternalDocumentation(
99+
link.documentationUrl,
100+
PackageList(
101+
RecognizedLinkFormat.Javadoc1, JSet(), JMap(), link.documentationUrl
102+
)
103+
)
104+
val extDoc = link.packageListUrl.fold(emptyExtDoc)( pl => ExternalDocumentation(
105+
link.documentationUrl,
106+
PackageList.Companion.load(pl, sourceSet.getJdkVersion, ctx.getConfiguration.getOfflineMode)
107+
)
108+
)
109+
(extDoc, link)
110+
}
111+
.map { (extDoc, link) =>
112+
113+
val externalLocationProvider = ScalaExternalLocationProvider(extDoc, ".html", link.kind)
114+
link.originRegexes -> externalLocationProvider
115+
}.toList
116+
117+
override def getExternalLocation(dri: DRI, sourceSets: JSet[DisplaySourceSet]): String =
118+
val regex = raw"\[origin:(.*)\]".r
119+
val origin = regex.findFirstIn(Option(dri.getExtra).getOrElse(""))
120+
origin match {
121+
case Some(path) => externalLocationProviders.find { (regexes, provider) =>
122+
regexes.exists(r => r.matches(path))
123+
}.fold(null)(_(1).resolve(dri.withNoOrigin))
124+
case None => null
125+
}

scala3doc/src/dotty/dokka/tasty/SymOps.scala

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -111,12 +111,20 @@ class SymOps[Q <: Quotes](val q: Q):
111111
else if (sym.maybeOwner.isDefDef) Some(sym.owner)
112112
else None
113113

114+
val originPath = {
115+
import q.reflect._
116+
import dotty.tools.dotc
117+
given ctx as dotc.core.Contexts.Context = q.asInstanceOf[scala.quoted.runtime.impl.QuotesImpl].ctx
118+
val csym = sym.asInstanceOf[dotc.core.Symbols.Symbol]
119+
Option(csym.associatedFile).map(_.path).fold("")(p => s"[origin:$p]")
120+
}
121+
114122
new DRI(
115123
sym.packageName,
116124
sym.topLevelEntryName.orNull, // TODO do we need any of this fields?
117125
method.map(s => new org.jetbrains.dokka.links.Callable(s.name, null, JList())).orNull,
118126
pointsTo,
119127
// sym.show returns the same signature for def << = 1 and def >> = 2.
120128
// For some reason it contains `$$$` instrad of symbol name
121-
s"${sym.name}${sym.fullName}/${sym.signature.resultSig}/[${sym.signature.paramSigs.mkString("/")}]"
129+
s"${sym.name}${sym.fullName}/${sym.signature.resultSig}/[${sym.signature.paramSigs.mkString("/")}]$originPath"
122130
)

0 commit comments

Comments
 (0)