Skip to content

Commit e95933b

Browse files
committed
Add external location providing between scala3doc instances
1 parent b2e3665 commit e95933b

11 files changed

+274
-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
JList()
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

@@ -169,6 +171,25 @@ class DottyDokkaPlugin extends DokkaJavaPlugin:
169171
.overrideExtension(dokkaBase.getLocationProvider)
170172
)
171173

174+
val scalaPackageListCreator = extend(
175+
_.extensionPoint(dokkaBase.getHtmlPreprocessors)
176+
.fromRecipe(c => ScalaPackageListCreator(c, RecognizedLinkFormat.DokkaHtml))
177+
.overrideExtension(dokkaBase.getPackageListCreator)
178+
.after(
179+
customDocumentationProvider.getValue
180+
)
181+
)
182+
183+
val scalaExternalLocationProviderFactory = extend(
184+
_.extensionPoint(dokkaBase.getExternalLocationProviderFactory)
185+
.fromRecipe{ case c as given DokkaContext => new ScalaExternalLocationProviderFactory }
186+
.overrideExtension(dokkaBase.getDokkaLocationProvider)
187+
)
188+
189+
extension (ctx: DokkaContext):
190+
def siteContext: Option[StaticSiteContext] = ctx.getConfiguration.asInstanceOf[DocContext].staticSiteContext
191+
def args: Scala3doc.Args = ctx.getConfiguration.asInstanceOf[DocContext].args
192+
172193
// TODO (https://github.com/lampepfl/scala3doc/issues/232): remove once problem is fixed in Dokka
173194
extension [T] (builder: ExtensionBuilder[T]):
174195
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
@@ -148,4 +148,13 @@ extension[T] (member: Member):
148148
extension (module: DModule):
149149
def driMap: Map[DRI, Member] = ModuleExtension.getFrom(module).fold(Map.empty)(_.driMap)
150150

151+
extension (dri: DRI):
152+
def withNoOrigin = DRI(
153+
dri.getPackageName,
154+
dri.getClassNames,
155+
dri.getCallable,
156+
dri.getTarget,
157+
Option(dri.getExtra).fold(null)(e => raw"\[origin:(.*)\]".r.replaceAllIn(e, ""))
158+
)
159+
151160
case class TastyDocumentableSource(val path: String, val lineNumber: Int)
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
@@ -108,12 +108,20 @@ class SymOps[Q <: Quotes](val q: Q):
108108
else if (sym.maybeOwner.isDefDef) Some(sym.owner)
109109
else None
110110

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

scala3doc/src/dotty/dokka/translators/ScalaPageCreator.scala

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,8 @@ class ScalaPageCreator(
4343
p.allMembers.filter(_.origin == Origin.DefinedWithin).collect {
4444
case f: DFunction => updatePageNameForMember(pageForFunction(f), f)
4545
case c: DClass => updatePageNameForMember(pageForDClass(c), c)
46+
case p: DProperty if p.kind.isInstanceOf[Kind.Type] => updatePageNameForMember(pageForProperty(p), p)
47+
//Currently only types because of dri conflict for all properties
4648
}
4749

4850
override def pageForPackage(p: DPackage): PackagePageNode =

0 commit comments

Comments
 (0)