Skip to content

Commit 6e6423b

Browse files
committed
Generate links to external projects in scaladoc.
Review by @VladUreche.
1 parent 1682c0d commit 6e6423b

File tree

12 files changed

+209
-126
lines changed

12 files changed

+209
-126
lines changed

src/compiler/scala/tools/nsc/doc/Settings.scala

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -194,6 +194,12 @@ class Settings(error: String => Unit, val printMsg: String => Unit = println(_))
194194
"Expand all type aliases and abstract types into full template pages. (locally this can be done with the @template annotation)"
195195
)
196196

197+
val docExternalUrls = MultiStringSetting (
198+
"-external-urls",
199+
"externalUrl(s)",
200+
"comma-separated list of package_names=doc_URL for external dependencies, where package names are ':'-separated"
201+
)
202+
197203
val docGroups = BooleanSetting (
198204
"-groups",
199205
"Group similar functions together (based on the @group annotation)"
@@ -238,6 +244,22 @@ class Settings(error: String => Unit, val printMsg: String => Unit = println(_))
238244
}
239245
}
240246

247+
// TODO: Enable scaladoc to scoop up the package list from another scaladoc site, just as javadoc does
248+
// -external-urls 'http://www.scala-lang.org/archives/downloads/distrib/files/nightly/docs/library'
249+
// should trigger scaldoc to fetch the package-list file. The steps necessary:
250+
// 1 - list all packages generated in scaladoc in the package-list file, exactly as javadoc:
251+
// see http://docs.oracle.com/javase/6/docs/api/package-list for http://docs.oracle.com/javase/6/docs/api
252+
// 2 - download the file and add the packages to the list
253+
lazy val extUrlMapping: Map[String, String] = (Map.empty[String, String] /: docExternalUrls.value) {
254+
case (map, binding) =>
255+
val idx = binding indexOf "="
256+
val pkgs = binding substring (0, idx) split ":"
257+
var url = binding substring (idx + 1)
258+
val index = "/index.html"
259+
url = if (url.endsWith(index)) url else url + index
260+
map ++ (pkgs map (_ -> url))
261+
}
262+
241263
/**
242264
* This is the hardcoded area of Scaladoc. This is where "undesirable" stuff gets eliminated. I know it's not pretty,
243265
* but ultimately scaladoc has to be useful. :)

src/compiler/scala/tools/nsc/doc/html/HtmlPage.scala

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -138,7 +138,8 @@ abstract class HtmlPage extends Page { thisPage =>
138138
<span class="extmbr" name={ mbr.qualifiedName }>{ inlineToHtml(text) }</span>
139139
case Tooltip(tooltip) =>
140140
<span class="extype" name={ tooltip }>{ inlineToHtml(text) }</span>
141-
// TODO: add case LinkToExternal here
141+
case LinkToExternal(name, url) =>
142+
<a href={ url } class="extype" target="_top">{ inlineToHtml(text) }</a>
142143
case NoLink =>
143144
inlineToHtml(text)
144145
}

src/compiler/scala/tools/nsc/doc/html/page/Template.scala

Lines changed: 23 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ class Template(universe: doc.Universe, generator: DiagramGenerator, tpl: DocTemp
4141
<script type="text/javascript" src={ relativeLinkTo{List("jquery-ui.js", "lib")} }></script>
4242
<script type="text/javascript" src={ relativeLinkTo{List("template.js", "lib")} }></script>
4343
<script type="text/javascript" src={ relativeLinkTo{List("tools.tooltip.js", "lib")} }></script>
44-
{ if (universe.settings.docDiagrams.isSetByUser) {
44+
{ if (universe.settings.docDiagrams.value) {
4545
<script type="text/javascript" src={ relativeLinkTo{List("modernizr.custom.js", "lib")} }></script>
4646
<script type="text/javascript" src={ relativeLinkTo{List("diagrams.js", "lib")} } id="diagrams-js"></script>
4747
} else NodeSeq.Empty }
@@ -289,6 +289,7 @@ class Template(universe: doc.Universe, generator: DiagramGenerator, tpl: DocTemp
289289
fullComment={ if(memberComment.filter(_.label=="div").isEmpty) "no" else "yes" }
290290
group={ mbr.group }>
291291
<a id={ mbr.signature }/>
292+
<a id={ mbr.signatureCompat }/>
292293
{ signature(mbr, false) }
293294
{ memberComment }
294295
</li>
@@ -645,17 +646,28 @@ class Template(universe: doc.Universe, generator: DiagramGenerator, tpl: DocTemp
645646
case _ => NodeSeq.Empty
646647
}
647648

648-
val typeHierarchy = if (s.docDiagrams.isSetByUser) mbr match {
649-
case dtpl: DocTemplateEntity if isSelf && !isReduced =>
650-
makeDiagramHtml(dtpl, dtpl.inheritanceDiagram, "Type Hierarchy", "inheritance-diagram")
651-
case _ => NodeSeq.Empty
652-
} else NodeSeq.Empty // diagrams not generated
649+
def createDiagram(f: DocTemplateEntity => Option[Diagram], description: String, id: String): NodeSeq =
650+
if (s.docDiagrams.value) mbr match {
651+
case dtpl: DocTemplateEntity if isSelf && !isReduced =>
652+
val diagram = f(dtpl)
653+
if (diagram.isDefined) {
654+
val s = universe.settings
655+
val diagramSvg = generator.generate(diagram.get, tpl, this)
656+
if (diagramSvg != NodeSeq.Empty) {
657+
<div class="toggleContainer block diagram-container" id={ id + "-container"}>
658+
<span class="toggle diagram-link">{ description }</span>
659+
<a href="http://docs.scala-lang.org/overviews/scaladoc/usage.html#diagrams" target="_blank" class="diagram-help">Learn more about scaladoc diagrams</a>
660+
<div class="diagram" id={ id }>{
661+
diagramSvg
662+
}</div>
663+
</div>
664+
} else NodeSeq.Empty
665+
} else NodeSeq.Empty
666+
case _ => NodeSeq.Empty
667+
} else NodeSeq.Empty // diagrams not generated
653668

654-
val contentHierarchy = if (s.docDiagrams.isSetByUser) mbr match {
655-
case dtpl: DocTemplateEntity if isSelf && !isReduced =>
656-
makeDiagramHtml(dtpl, dtpl.contentDiagram, "Content Hierarchy", "content-diagram")
657-
case _ => NodeSeq.Empty
658-
} else NodeSeq.Empty // diagrams not generated
669+
val typeHierarchy = createDiagram(_.inheritanceDiagram, "Type Hierarchy", "inheritance-diagram")
670+
val contentHierarchy = createDiagram(_.contentDiagram, "Content Hierarchy", "content-diagram")
659671

660672
memberComment ++ paramComments ++ attributesBlock ++ linearization ++ subclasses ++ typeHierarchy ++ contentHierarchy
661673
}
@@ -946,22 +958,6 @@ class Template(universe: doc.Universe, generator: DiagramGenerator, tpl: DocTemp
946958
scala.xml.Text(ub.typeParamName + " is a subclass of " + ub.upperBound.name + " (" + ub.typeParamName + " <: ") ++
947959
typeToHtml(ub.upperBound, true) ++ scala.xml.Text(")")
948960
}
949-
950-
def makeDiagramHtml(tpl: DocTemplateEntity, diagram: Option[Diagram], description: String, id: String) = {
951-
if (diagram.isDefined) {
952-
val s = universe.settings
953-
val diagramSvg = generator.generate(diagram.get, tpl, this)
954-
if (diagramSvg != NodeSeq.Empty) {
955-
<div class="toggleContainer block diagram-container" id={ id + "-container"}>
956-
<span class="toggle diagram-link">{ description }</span>
957-
<a href="http://docs.scala-lang.org/overviews/scaladoc/usage.html#diagrams" target="_blank" class="diagram-help">Learn more about scaladoc diagrams</a>
958-
<div class="diagram" id={ id }>{
959-
diagramSvg
960-
}</div>
961-
</div>
962-
} else NodeSeq.Empty
963-
} else NodeSeq.Empty
964-
}
965961
}
966962

967963
object Template {

src/compiler/scala/tools/nsc/doc/model/Entity.scala

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -196,6 +196,9 @@ trait MemberEntity extends Entity {
196196
/** The identity of this member, used for linking */
197197
def signature: String
198198

199+
/** Compatibility signature, will be removed from future versions */
200+
def signatureCompat: String
201+
199202
/** Indicates whether the member is inherited by implicit conversion */
200203
def isImplicitlyInherited: Boolean
201204

@@ -625,4 +628,4 @@ trait UpperBoundedTypeParamConstraint extends TypeParamConstraint {
625628
/** toString for debugging */
626629
override def toString = typeParamName + " is a subclass of " + upperBound.name + " (" + typeParamName + " <: " +
627630
upperBound.name + ")"
628-
}
631+
}

src/compiler/scala/tools/nsc/doc/model/LinkTo.scala

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,10 @@ package model
99
import scala.collection._
1010

1111
abstract sealed class LinkTo
12-
case class LinkToTpl(tpl: DocTemplateEntity) extends LinkTo
13-
case class LinkToMember(mbr: MemberEntity, inTpl: DocTemplateEntity) extends LinkTo
14-
case class Tooltip(name: String) extends LinkTo { def this(tpl: TemplateEntity) = this(tpl.qualifiedName) }
15-
// case class LinkToExternal(name: String, url: String) extends LinkTo // for SI-191, whenever Manohar will have time
12+
final case class LinkToTpl(tpl: DocTemplateEntity) extends LinkTo
13+
final case class LinkToMember(mbr: MemberEntity, inTpl: DocTemplateEntity) extends LinkTo
14+
final case class Tooltip(name: String) extends LinkTo { def this(tpl: TemplateEntity) = this(tpl.qualifiedName) }
15+
final case class LinkToExternal(name: String, url: String) extends LinkTo
1616
case object NoLink extends LinkTo // you should use Tooltip if you have a name from the user, this is only in case all fails
1717

1818
object LinkToTpl {

src/compiler/scala/tools/nsc/doc/model/MemberLookup.scala

Lines changed: 88 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ trait MemberLookup {
1111
thisFactory: ModelFactory =>
1212

1313
import global._
14+
import rootMirror.RootPackage, rootMirror.EmptyPackage
1415

1516
def makeEntityLink(title: Inline, pos: Position, query: String, inTplOpt: Option[DocTemplateImpl]) =
1617
new EntityLink(title) { lazy val link = memberLookup(pos, query, inTplOpt) }
@@ -21,23 +22,44 @@ trait MemberLookup {
2122
var members = breakMembers(query)
2223
//println(query + " => " + members)
2324

24-
// (1) Lookup in the root package, as most of the links are qualified
25-
var linkTo: List[LinkTo] = lookupInRootPackage(pos, members)
25+
// (1) First look in the root package, as most of the links are qualified
26+
val fromRoot = lookupInRootPackage(pos, members)
2627

27-
// (2) Recursively go into each
28-
if (inTplOpt.isDefined) {
29-
var currentTpl = inTplOpt.get
30-
while (currentTpl != null && !currentTpl.isRootPackage && (linkTo.isEmpty)) {
31-
linkTo = lookupInTemplate(pos, members, currentTpl)
32-
currentTpl = currentTpl.inTemplate
33-
}
28+
// (2) Or recursively go into each containing template.
29+
val fromParents = inTplOpt.fold(Stream.empty[DocTemplateImpl]) { tpl =>
30+
Stream.iterate(tpl)(_.inTemplate)
31+
}.takeWhile (tpl => tpl != null && !tpl.isRootPackage).map { tpl =>
32+
lookupInTemplate(pos, members, tpl.asInstanceOf[EntityImpl].sym)
3433
}
3534

36-
// (3) Look at external links
37-
if (linkTo.isEmpty) {
38-
// TODO: IF THIS IS THE ROOT PACKAGE, LOOK AT EXTERNAL LINKS
35+
val syms = (fromRoot +: fromParents) find (!_.isEmpty) getOrElse Nil
36+
val linkTo = createLinks(syms) match {
37+
case Nil if !syms.isEmpty =>
38+
// (3) Look at external links
39+
syms.flatMap { case (sym, owner) =>
40+
41+
// reconstruct the original link
42+
def linkName(sym: Symbol) = {
43+
def isRoot(s: Symbol) = s.isRootSymbol || s.isEmptyPackage || s.isEmptyPackageClass
44+
def nameString(s: Symbol) = s.nameString + (if ((s.isModule || s.isModuleClass) && !s.isPackage) "$" else "")
45+
val packageSuffix = if (sym.isPackage) ".package" else ""
46+
47+
sym.ownerChain.reverse.filterNot(isRoot(_)).map(nameString(_)).mkString(".") + packageSuffix
48+
}
49+
50+
if (sym.isClass || sym.isModule || sym.isTrait || sym.isPackage)
51+
findExternalLink(linkName(sym))
52+
else if (owner.isClass || owner.isModule || owner.isTrait || owner.isPackage)
53+
findExternalLink(linkName(owner) + "@" + externalSignature(sym))
54+
else
55+
None
56+
}
57+
case links => links
3958
}
4059

60+
//println(createLinks(syms))
61+
//println(linkTo)
62+
4163
// (4) if we still haven't found anything, create a tooltip, if we found too many, report
4264
if (linkTo.isEmpty){
4365
if (!settings.docNoLinkWarnings.value)
@@ -97,9 +119,23 @@ trait MemberLookup {
97119
private object OnlyType extends SearchStrategy
98120
private object OnlyTerm extends SearchStrategy
99121

100-
private def lookupInRootPackage(pos: Position, members: List[String]) = lookupInTemplate(pos, members, makeRootPackage)
122+
private def lookupInRootPackage(pos: Position, members: List[String]) =
123+
if (members.length == 1)
124+
lookupInTemplate(pos, members, EmptyPackage) ::: lookupInTemplate(pos, members, RootPackage)
125+
else
126+
lookupInTemplate(pos, members, RootPackage)
101127

102-
private def lookupInTemplate(pos: Position, members: List[String], inTpl: DocTemplateImpl): List[LinkTo] = {
128+
private def createLinks(syms: List[(Symbol, Symbol)]): List[LinkTo] =
129+
syms.flatMap { case (sym, owner) =>
130+
if (sym.isClass || sym.isModule || sym.isTrait || sym.isPackage)
131+
findTemplateMaybe(sym) map (LinkToTpl(_))
132+
else
133+
findTemplateMaybe(owner) flatMap { inTpl =>
134+
inTpl.members find (_.asInstanceOf[EntityImpl].sym == sym) map (LinkToMember(_, inTpl))
135+
}
136+
}
137+
138+
private def lookupInTemplate(pos: Position, members: List[String], container: Symbol): List[(Symbol, Symbol)] = {
103139
// Maintaining compatibility with previous links is a bit tricky here:
104140
// we have a preference for term names for all terms except for the last, where we prefer a class:
105141
// How to do this:
@@ -108,53 +144,56 @@ trait MemberLookup {
108144
// * we look for terms with the last member's name
109145
// * we look for types with the same name, all the way up
110146
val result = members match {
111-
case Nil =>
112-
Nil
147+
case Nil => Nil
113148
case mbrName::Nil =>
114-
var members = lookupInTemplate(pos, mbrName, inTpl, OnlyType)
115-
if (members.isEmpty)
116-
members = lookupInTemplate(pos, mbrName, inTpl, OnlyTerm)
117-
118-
members.map(_ match {
119-
case tpl: DocTemplateEntity => LinkToTpl(tpl)
120-
case mbr => LinkToMember(mbr, inTpl)
121-
})
149+
var syms = lookupInTemplate(pos, mbrName, container, OnlyType) map ((_, container))
150+
if (syms.isEmpty)
151+
syms = lookupInTemplate(pos, mbrName, container, OnlyTerm) map ((_, container))
152+
syms
122153

123154
case tplName::rest =>
155+
def completeSearch(syms: List[Symbol]) =
156+
syms filter {sym => sym.isPackage || sym.isClass || sym.isModule} flatMap (lookupInTemplate(pos, rest, _))
124157

125-
def completeSearch(mbrs: List[MemberImpl]) =
126-
mbrs.collect({case d:DocTemplateImpl => d}).flatMap(tpl => lookupInTemplate(pos, rest, tpl))
127-
128-
var members = completeSearch(lookupInTemplate(pos, tplName, inTpl, OnlyTerm))
129-
if (members.isEmpty)
130-
members = completeSearch(lookupInTemplate(pos, tplName, inTpl, OnlyType))
131-
132-
members
158+
completeSearch(lookupInTemplate(pos, tplName, container, OnlyTerm)) match {
159+
case Nil => completeSearch(lookupInTemplate(pos, tplName, container, OnlyType))
160+
case syms => syms
161+
}
133162
}
134-
//println("lookupInTemplate(" + members + ", " + inTpl + ") => " + result)
163+
//println("lookupInTemplate(" + members + ", " + container + ") => " + result)
135164
result
136165
}
137166

138-
private def lookupInTemplate(pos: Position, member: String, inTpl: DocTemplateImpl, strategy: SearchStrategy): List[MemberImpl] = {
167+
private def lookupInTemplate(pos: Position, member: String, container: Symbol, strategy: SearchStrategy): List[Symbol] = {
139168
val name = member.stripSuffix("$").stripSuffix("!").stripSuffix("*")
169+
def signatureMatch(sym: Symbol): Boolean = externalSignature(sym).startsWith(name)
170+
171+
// We need to cleanup the bogus classes created by the .class file parser. For example, [[scala.Predef]] resolves
172+
// to (bogus) class scala.Predef loaded by the class loader -- which we need to eliminate by looking at the info
173+
// and removing NoType classes
174+
def cleanupBogusClasses(syms: List[Symbol]) = { syms.filter(_.info != NoType) }
175+
176+
def syms(name: Name) = container.info.nonPrivateMember(name).alternatives
177+
def termSyms = cleanupBogusClasses(syms(newTermName(name)))
178+
def typeSyms = cleanupBogusClasses(syms(newTypeName(name)))
179+
140180
val result = if (member.endsWith("$"))
141-
inTpl.members.filter(mbr => (mbr.name == name) && (mbr.isTerm))
181+
termSyms
142182
else if (member.endsWith("!"))
143-
inTpl.members.filter(mbr => (mbr.name == name) && (mbr.isType))
183+
typeSyms
144184
else if (member.endsWith("*"))
145-
inTpl.members.filter(mbr => (mbr.signature.startsWith(name)))
146-
else {
185+
cleanupBogusClasses(container.info.nonPrivateDecls) filter signatureMatch
186+
else
147187
if (strategy == BothTypeAndTerm)
148-
inTpl.members.filter(_.name == name)
188+
termSyms ::: typeSyms
149189
else if (strategy == OnlyType)
150-
inTpl.members.filter(mbr => (mbr.name == name) && (mbr.isType))
190+
typeSyms
151191
else if (strategy == OnlyTerm)
152-
inTpl.members.filter(mbr => (mbr.name == name) && (mbr.isTerm))
192+
termSyms
153193
else
154194
Nil
155-
}
156195

157-
//println("lookupInTemplate(" + member + ", " + inTpl + ") => " + result)
196+
//println("lookupInTemplate(" + member + ", " + container + ") => " + result)
158197
result
159198
}
160199

@@ -170,7 +209,11 @@ trait MemberLookup {
170209
if ((query.charAt(index) == '.' || query.charAt(index) == '#') &&
171210
((index == 0) || (query.charAt(index-1) != '\\'))) {
172211

173-
members ::= query.substring(last_index, index).replaceAll("\\\\([#\\.])", "$1")
212+
val member = query.substring(last_index, index).replaceAll("\\\\([#\\.])", "$1")
213+
// we want to allow javadoc-style links [[#member]] -- which requires us to remove empty members from the first
214+
// elemnt in the list
215+
if ((member != "") || (!members.isEmpty))
216+
members ::= member
174217
last_index = index + 1
175218
}
176219
index += 1
@@ -184,4 +227,4 @@ trait MemberLookup {
184227
object MemberLookup {
185228
private[this] var _showExplanation = true
186229
def showExplanation: Boolean = if (_showExplanation) { _showExplanation = false; true } else false
187-
}
230+
}

src/compiler/scala/tools/nsc/doc/model/ModelFactory.scala

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -209,7 +209,8 @@ class ModelFactory(val global: Global, val settings: doc.Settings) {
209209
((!sym.isTrait && ((sym hasFlag Flags.ABSTRACT) || (sym hasFlag Flags.DEFERRED)) && (!isImplicitlyInherited)) ||
210210
sym.isAbstractClass || sym.isAbstractType) && !sym.isSynthetic
211211
def isTemplate = false
212-
lazy val signature = {
212+
def signature = externalSignature(sym)
213+
lazy val signatureCompat = {
213214

214215
def defParams(mbr: Any): String = mbr match {
215216
case d: MemberEntity with Def =>
@@ -1082,5 +1083,17 @@ class ModelFactory(val global: Global, val settings: doc.Settings) {
10821083
(settings.docExpandAllTypes.value && (bSym.sourceFile != null)) ||
10831084
{ val rawComment = global.expandedDocComment(bSym, inTpl.sym)
10841085
rawComment.contains("@template") || rawComment.contains("@documentable") }
1086+
1087+
def findExternalLink(name: String): Option[LinkTo] =
1088+
settings.extUrlMapping find {
1089+
case (pkg, _) => name startsWith pkg
1090+
} map {
1091+
case (_, url) => LinkToExternal(name, url + "#" + name)
1092+
}
1093+
1094+
def externalSignature(sym: Symbol) = {
1095+
sym.info // force it, otherwise we see lazy types
1096+
(sym.nameString + sym.signatureString).replaceAll("\\s", "")
1097+
}
10851098
}
10861099

0 commit comments

Comments
 (0)