/
SourceLinksTransformer.kt
124 lines (111 loc) · 5.05 KB
/
SourceLinksTransformer.kt
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
package org.jetbrains.dokka.base.transformers.pages.sourcelinks
import com.intellij.psi.PsiDocumentManager
import com.intellij.psi.PsiElement
import org.jetbrains.dokka.DokkaConfiguration
import org.jetbrains.dokka.DokkaConfiguration.DokkaSourceSet
import org.jetbrains.dokka.analysis.DescriptorDocumentableSource
import org.jetbrains.dokka.analysis.PsiDocumentableSource
import org.jetbrains.dokka.base.DokkaBase
import org.jetbrains.dokka.base.translators.documentables.PageContentBuilder
import org.jetbrains.dokka.links.DRI
import org.jetbrains.dokka.model.*
import org.jetbrains.dokka.pages.*
import org.jetbrains.dokka.plugability.DokkaContext
import org.jetbrains.dokka.plugability.plugin
import org.jetbrains.dokka.plugability.querySingle
import org.jetbrains.dokka.transformers.pages.PageTransformer
import org.jetbrains.kotlin.descriptors.DeclarationDescriptorWithSource
import org.jetbrains.kotlin.resolve.source.getPsi
import org.jetbrains.kotlin.utils.addToStdlib.cast
import org.jetbrains.kotlin.utils.addToStdlib.ifNotEmpty
import java.io.File
class SourceLinksTransformer(val context: DokkaContext) : PageTransformer {
private val builder : PageContentBuilder = PageContentBuilder(
context.plugin<DokkaBase>().querySingle { commentsToContentConverter },
context.plugin<DokkaBase>().querySingle { signatureProvider },
context.logger
)
override fun invoke(input: RootPageNode) =
input.transformContentPagesTree { node ->
when (node) {
is WithDocumentables -> {
val sources = node.documentables.filterIsInstance<WithSources>()
.associate { (it as Documentable).dri to resolveSources(it) }
if (sources.isNotEmpty())
node.modified(content = transformContent(node.content, sources))
else
node
}
else -> node
}
}
private fun getSourceLinks() = context.configuration.sourceSets
.flatMap { it.sourceLinks.map { sl -> SourceLink(sl, it) } }
private fun resolveSources(documentable: WithSources) = documentable.sources
.mapNotNull { entry ->
getSourceLinks().find { File(entry.value.path).startsWith(it.path) && it.sourceSetData == entry.key }?.let {
entry.key to
entry.value.toLink(it)
}
}
private fun DocumentableSource.toLink(sourceLink: SourceLink): String {
val sourcePath = File(this.path).canonicalPath.replace("\\", "/")
val sourceLinkPath = File(sourceLink.path).canonicalPath.replace("\\", "/")
val lineNumber = when (this) {
is DescriptorDocumentableSource -> this.descriptor
.cast<DeclarationDescriptorWithSource>()
.source.getPsi()
?.lineNumber()
is PsiDocumentableSource -> this.psi.lineNumber()
else -> null
}
return sourceLink.url +
sourcePath.split(sourceLinkPath)[1] +
sourceLink.lineSuffix +
"${lineNumber ?: 1}"
}
private fun PsiElement.lineNumber(): Int? {
val doc = PsiDocumentManager.getInstance(project).getDocument(containingFile)
// IJ uses 0-based line-numbers; external source browsers use 1-based
return doc?.getLineNumber(textRange.startOffset)?.plus(1)
}
private fun ContentNode.signatureGroupOrNull() =
(this as? ContentGroup)?.takeIf { it.dci.kind == ContentKind.Symbol }
private fun transformContent(
contentNode: ContentNode, sources: Map<DRI, List<Pair<DokkaSourceSet, String>>>
): ContentNode =
contentNode.signatureGroupOrNull()?.let { cg ->
sources[cg.dci.dri.singleOrNull()]?.let { sourceLinks ->
sourceLinks.filter { it.first.sourceSetID in cg.sourceSets.sourceSetIDs }.ifNotEmpty {
cg.copy(children = cg.children + sourceLinks.map {
buildContentLink(
cg.dci.dri.first(),
it.first,
it.second
)
})
}
}
} ?: when (contentNode) {
is ContentComposite -> contentNode.transformChildren { transformContent(it, sources) }
else -> contentNode
}
private fun buildContentLink(dri: DRI, sourceSet: DokkaSourceSet, link: String) = builder.contentFor(
dri,
setOf(sourceSet),
ContentKind.Source,
setOf(TextStyle.RightAligned)
) {
text("(")
link("source", link)
text(")")
}
}
data class SourceLink(val path: String, val url: String, val lineSuffix: String?, val sourceSetData: DokkaSourceSet) {
constructor(sourceLinkDefinition: DokkaConfiguration.SourceLinkDefinition, sourceSetData: DokkaSourceSet) : this(
sourceLinkDefinition.localDirectory,
sourceLinkDefinition.remoteUrl.toExternalForm(),
sourceLinkDefinition.remoteLineSuffix,
sourceSetData
)
}