Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
70 changes: 70 additions & 0 deletions scaladoc/resources/dotty_res/scripts/featureToggles.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
/* Interactive toggles for experimental language features (e.g. capture checking).
*
* Signature fragments that depend on such a feature are rendered twice by
* scaladoc: a `.feature-on` variant (with the feature's annotations) and a
* `.feature-off` variant (without them), wrapped in a `.feature-<id>` span.
* CSS displays exactly one of the two variants, keyed on a `<id>-hidden`
* class on the root element that is maintained here.
*
* Like theme.js, this script is loaded without `defer` so the stored
* preference is applied before first paint.
*/
; (function () {
const supportsLocalStorage = (() => {
try {
localStorage.setItem('test', 'test');
localStorage.removeItem('test');
return true;
} catch (e) {
return false;
}
})();

const features = [
{
name: "capture checking",
storageKey: "hide-cc",
rootClass: "cc-hidden",
toggleId: "cc-toggle",
mobileToggleId: "mobile-cc-toggle",
},
];

features.forEach(feature => {
let hidden =
supportsLocalStorage && localStorage.getItem(feature.storageKey) === "true";

/* Applied ASAP so we don't get a flash of feature-specific content before
* the stored preference kicks in */
document.documentElement.classList.toggle(feature.rootClass, hidden);

window.addEventListener("DOMContentLoaded", () => {
const toggle = document.getElementById(feature.toggleId);
const mobileToggle = document.getElementById(feature.mobileToggleId);

function render() {
document.documentElement.classList.toggle(feature.rootClass, hidden);
if (toggle !== null) {
toggle.classList.toggle("feature-toggle-off", hidden);
toggle.setAttribute("aria-pressed", !hidden);
}
if (mobileToggle !== null) {
mobileToggle.textContent = (hidden ? "Show " : "Hide ") + feature.name;
}
}

function flip() {
hidden = !hidden;
supportsLocalStorage && localStorage.setItem(feature.storageKey, hidden);
render();
/* Let other components (e.g. the inheritance diagram, whose labels are
* measured at render time) react to the changed feature state */
window.dispatchEvent(new CustomEvent("feature-toggled"));
}

toggle && toggle.addEventListener("click", flip);
mobileToggle && mobileToggle.addEventListener("click", flip);
render();
});
});
})();
14 changes: 14 additions & 0 deletions scaladoc/resources/dotty_res/scripts/ux.js
Original file line number Diff line number Diff line change
Expand Up @@ -506,6 +506,20 @@ window.addEventListener("popstate", (e) => {
var zoom;
var transform;

/* The diagram's node sizes are measured when it is first rendered, so a
* feature toggle (which changes the visible signature fragments) invalidates
* the layout: drop it and re-render if it is currently shown. */
window.addEventListener("feature-toggled", () => {
const graph = document.querySelector("svg#graph");
const diagram = document.getElementById("inheritance-diagram");
if (graph != null && graph.children.length > 0) {
graph.innerHTML = "";
if (diagram != null && diagram.classList.contains("shown")) {
showGraph();
}
}
});

function showGraph() {
document.getElementById("inheritance-diagram").classList.add("shown");
const graph = document.querySelector("svg#graph");
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
/* Interactive toggles for experimental language features (e.g. capture checking).
*
* Signature fragments that depend on a toggleable feature carry both variants
* in the DOM (see featureToggles.js); these rules display exactly one of them.
* By default the feature's annotations are shown; a `<id>-hidden` class on the
* root element (e.g. `cc-hidden`) swaps every fragment to its plain variant.
*/

.feature-off {
display: none;
}

:root.cc-hidden .feature-cc > .feature-on {
display: none;
}

:root.cc-hidden .feature-cc > .feature-off {
display: inline;
}

/* The header button toggling capture checking annotations */

#cc-toggle {
border: 1px solid var(--border-default);
border-radius: 4px;
background: none;
padding: calc(0.5 * var(--base-spacing)) calc(1 * var(--base-spacing));
font-family: var(--code-font-family);
font-size: 14px;
line-height: 16px;
}

#cc-toggle.feature-toggle-off {
text-decoration: line-through;
opacity: 0.6;
}
4 changes: 4 additions & 0 deletions scaladoc/src/dotty/tools/scaladoc/DocContext.scala
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,10 @@ extension (r: report.type)
case class NavigationNode(name: String, dri: DRI, nested: Seq[NavigationNode])

case class DocContext(args: Scaladoc.Args, compilerContext: CompilerContext):
// Set during parsing when any documented source file opts into capture
// checking; renderers use it to decide whether to emit the cc toggle button.
var ccFeatureDetected: Boolean = false

lazy val sourceLinks = SourceLinks.load(args.sourceLinks, args.revision)(using compilerContext)

lazy val commentSyntaxArgs = tasty.comments.CommentSyntaxArgs.load(args.defaultSyntax)(using compilerContext)
Expand Down
25 changes: 18 additions & 7 deletions scaladoc/src/dotty/tools/scaladoc/api.scala
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,8 @@ case class TermParameter(
dri: DRI,
signature: Signature,
isExtendedSymbol: Boolean = false,
isGrouped: Boolean = false
isGrouped: Boolean = false,
ccModifiers: String = "" // modifiers specific to capture checking, e.g. "consume "
)

type TypeParameterList = Seq[TypeParameter]
Expand All @@ -139,6 +140,21 @@ case class Type(override val name: String, dri: Option[DRI]) extends SignaturePa
case class Keyword(override val name: String) extends SignaturePart
case class Plain(override val name: String) extends SignaturePart

/** An experimental language feature whose signature fragments can be toggled
* on and off interactively in the rendered documentation.
*/
enum ToggleableFeature(val cssClass: String):
case CaptureChecking extends ToggleableFeature("cc")

/** A signature fragment that depends on a toggleable language feature.
* `on` is rendered when the feature's annotations are shown, `off` (usually
* empty) when they are hidden. For example, a pure function arrow under
* capture checking is `Toggleable(CaptureChecking, on = ->, off = =>)`,
* while a capture set `^{io}` is purely additive and has an empty `off`.
*/
case class Toggleable(feature: ToggleableFeature, on: Signature, off: Signature = Nil) extends SignaturePart:
override val name: String = on.map(_.name).mkString

type Signature = List[SignaturePart]

case class MemberSignature(prefix: Signature, kind: Signature, name: Signature, suffix: Signature)
Expand Down Expand Up @@ -257,12 +273,7 @@ extension (m: Module)

extension (s: Signature)
def getName: String =
s.map {
case Name(s, _) => s
case Plain(s) => s
case Type(s, _) => s
case Keyword(s) => s
}.mkString
s.map(_.name).mkString

case class TastyMemberSource(path: java.nio.file.Path, lineNumber: Int)

Expand Down
11 changes: 11 additions & 0 deletions scaladoc/src/dotty/tools/scaladoc/renderers/HtmlRenderer.scala
Original file line number Diff line number Diff line change
Expand Up @@ -275,6 +275,14 @@ class HtmlRenderer(rootPackage: Member, members: Map[DRI, Member])(using ctx: Do
div(cls:="header-container-right")(
button(id := "search-toggle", cls := "icon-button"),
quickLinks(),
Option.when(ctx.ccFeatureDetected)(
button(
id := "cc-toggle",
cls := "text-button",
titleAttr := "Show or hide capture checking annotations",
Attr("aria-label") := "Show or hide capture checking annotations",
)("cc")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can this be a tad more descriptive for the average beginner? 😇

).toSeq,
span(id := "theme-toggle", cls := "icon-button"),
span(id := "mobile-menu-toggle", cls := "icon-button hamburger"),
),
Expand All @@ -291,6 +299,9 @@ class HtmlRenderer(rootPackage: Member, members: Map[DRI, Member])(using ctx: Do
div(cls := "mobile-menu-container body-medium")(
input(id := "mobile-scaladoc-searchbar-input", cls := "scaladoc-searchbar-input", `type` := "search", `placeholder`:= "Find anything"),
quickLinks(mobile = true),
Option.when(ctx.ccFeatureDetected)(
span(id := "mobile-cc-toggle", cls := "mobile-menu-item")("Hide capture checking")
).toSeq,
span(id := "mobile-theme-toggle", cls := "mobile-menu-item mode"),
)
),
Expand Down
3 changes: 2 additions & 1 deletion scaladoc/src/dotty/tools/scaladoc/renderers/Resources.scala
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,8 @@ trait Resources(using ctx: DocContext) extends Locations, Writer:

val earlyCommonResources: Seq[Resource] =
List(
"scripts/theme.js"
"scripts/theme.js",
"scripts/featureToggles.js"
).map(dottyRes)

val commonResources: Seq[Resource] = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,3 +38,10 @@ trait SignatureRenderer:
case Type(name, None) => span(Attr("t") := "t")(name)
case Keyword(name) => span(Attr("t") := "k")(name)
case Plain(name) => raw(name)
case Toggleable(feature, on, off) =>
// Both variants are emitted; CSS shows exactly one of them, depending on
// whether the feature is toggled on (default) or off in the browser.
span(cls := s"feature-${feature.cssClass}")(
span(cls := "feature-on")(on.map(renderElement(_))) +:
(if off.isEmpty then Nil else Seq(span(cls := "feature-off")(off.map(renderElement(_)))))
)
Original file line number Diff line number Diff line change
Expand Up @@ -458,12 +458,13 @@ trait ClassLikeSupport:
val defaultValue = Option.when(symbol.flags.is(Flags.HasDefault))(Plain(" = ..."))
api.TermParameter(
symbol.getAnnotations(),
consumePrefix + inlinePrefix + prefix(symbol),
inlinePrefix + prefix(symbol),
nameIfNotSynthetic,
symbol.dri,
argument.tpt.asSignature(classDef, symbol.owner) :++ defaultValue,
isExtendedSymbol = isExtendedSymbol,
isGrouped = isGrouped
isGrouped = isGrouped,
ccModifiers = consumePrefix
)

def mkTypeArgument(
Expand Down Expand Up @@ -521,7 +522,7 @@ trait ClassLikeSupport:
// For capset members, prepend ^ to the signature (the bounds rendering
// already elides the CapSet lower/upper defaults, so we just need the caret).
val sig = tpeTree.asSignature(classDef, symbol.owner)
val sigWithCaret = if isCaptureVar then Plain("^") :: sig else sig
val sigWithCaret = if isCaptureVar then Toggleable(ToggleableFeature.CaptureChecking, Keyword("^") :: Nil) :: sig else sig

if symbol.flags.is(Flags.Exported)
then {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ trait PackageSupport:
case CCImport() => ccFlag = true
case _ =>
}
if ccEnabled then ctx.ccFeatureDetected = true
(name, Member(name, "", pck.symbol.dri, Kind.Package))

def parsePackageObject(pckObj: ClassDef): (String, Member) =
Expand Down
36 changes: 24 additions & 12 deletions scaladoc/src/dotty/tools/scaladoc/tasty/TypesSupport.scala
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,14 @@ trait TypesSupport:
protected def inParens(s: SSignature, wrap: Boolean = true) =
if wrap then plain("(").l ++ s ++ plain(")").l else s

/** Wrap capture-checking-specific signature fragments so they can be toggled
* interactively in the rendered documentation. `off` is what to render when
* cc annotations are hidden (usually nothing).
*/
protected def ccToggle(on: SSignature, off: SSignature = Nil): SSignature =
if on.isEmpty && off.isEmpty then Nil
else List(Toggleable(ToggleableFeature.CaptureChecking, on, off))

extension (on: SignaturePart) def l: List[SignaturePart] = List(on)

private def tpe(using Quotes)(symbol: reflect.Symbol)(using inCC: Option[Any]): SSignature =
Expand Down Expand Up @@ -140,9 +148,10 @@ trait TypesSupport:
functionType(base, args, skipThisTypePrefix)(using inCC = Some(refs))
case t : Refinement if t.isFunctionType =>
inner(base, skipThisTypePrefix)(using indent = indent, skipTypeSuffix = skipTypeSuffix, inCC = Some(refs))
case t if t.isCapSet => emitCaptureSet(refs, skipThisTypePrefix, omitCap = false)
case t if t.isCapSet =>
ccToggle(emitCaptureSet(refs, skipThisTypePrefix, omitCap = false), tpe(t.typeSymbol)(using None))
case t if t.isPureClass(elideThis) => inner(base, skipThisTypePrefix)
case t => inner(base, skipThisTypePrefix) ++ emitCapturing(refs, skipThisTypePrefix)
case t => inner(base, skipThisTypePrefix) ++ ccToggle(emitCapturing(refs, skipThisTypePrefix))
case AnnotatedType(tpe, _) =>
inner(tpe, skipThisTypePrefix)
case FlexibleType(tpe) =>
Expand All @@ -156,7 +165,7 @@ trait TypesSupport:
case tl @ TypeLambda(params, paramBounds, resType) =>
plain("[").l ++ commas(params.zip(paramBounds).map { (name, typ) =>
val normalizedName = if name.matches("_\\$\\d*") then "_" else name
val suffix = if ccEnabled && typ.derivesFrom(CaptureDefs.Caps_CapSet) then List(Keyword("^")) else Nil
val suffix = if ccEnabled && typ.derivesFrom(CaptureDefs.Caps_CapSet) then ccToggle(Keyword("^").l) else Nil
tpe(normalizedName).l ++ suffix ++ inner(typ, skipThisTypePrefix)
}) ++ plain("]").l
++ keyword(" =>> ").l
Expand All @@ -179,7 +188,7 @@ trait TypesSupport:
def getParamBounds(t: PolyType): SSignature = commas(
t.paramNames.zip(t.paramBounds.map(inner(_, skipThisTypePrefix))).zipWithIndex
.map { case ((name, bound), idx) =>
val suffix = if ccEnabled && t.param(idx).derivesFrom(CaptureDefs.Caps_CapSet) then List(Keyword("^")) else Nil
val suffix = if ccEnabled && t.param(idx).derivesFrom(CaptureDefs.Caps_CapSet) then ccToggle(Keyword("^").l) else Nil
tpe(name).l ++ suffix ++ bound
}
)
Expand Down Expand Up @@ -246,9 +255,9 @@ trait TypesSupport:
val arrow =
if ccEnabled then
inCC0 match
case None | Some(Nil) => keyword(arrPrefix + "->").l
case None | Some(Nil) => ccToggle(keyword(arrPrefix + "->").l, keyword(arrPrefix + "=>").l)
case Some(List(c)) if c.isCaptureRoot => keyword(arrPrefix + "=>").l
case Some(refs) => keyword(arrPrefix + "->") :: emitCaptureSet(refs, skipThisTypePrefix)
case Some(refs) => ccToggle(keyword(arrPrefix + "->") :: emitCaptureSet(refs, skipThisTypePrefix), keyword(arrPrefix + "=>").l)
else keyword(arrPrefix + "=>").l
val resType = inner(m.resType, skipThisTypePrefix)
paramList ++ (plain(" ") :: arrow) ++ (plain(" ") :: resType)
Expand Down Expand Up @@ -314,7 +323,8 @@ trait TypesSupport:
case _ => topLevelProcess(t, skipThisTypePrefix)
}) ++ plain("]").l

case t : TypeRef if ccEnabled && t.isCapSet => emitCaptureSet(Nil, skipThisTypePrefix)
case t : TypeRef if ccEnabled && t.isCapSet =>
ccToggle(emitCaptureSet(Nil, skipThisTypePrefix), tpe(t.typeSymbol)(using None))

case tp @ TypeRef(qual, typeName) =>
inline def wrapping = shouldWrapInParens(inner = qual, outer = tp, isLeft = true)
Expand Down Expand Up @@ -612,21 +622,23 @@ trait TypesSupport:
else
val isPureFun = funTy.isAnyFunction || funTy.isAnyContextFunction
val isImpureFun = funTy.isAnyImpureFunction || funTy.isAnyImpureContextFunction
val impureArrow = List(Keyword(prefix + "=>"))
def pureArrow = ccToggle(List(Keyword(prefix + "->")), impureArrow)
captures match
case None => // means an explicit retains* annotation is missing
if isPureFun then
List(Keyword(prefix + "->"))
pureArrow
else if isImpureFun then
List(Keyword(prefix + "=>"))
impureArrow
else
report.error(s"Cannot emit function arrow: expected a (Context)Function* or Impure(Context)Function*, but got: ${funTy.show}")
Nil
case Some(refs) =>
// there is some capture set
refs match
case Nil => List(Keyword(prefix + "->"))
case List(ref) if ref.isCaptureRoot => List(Keyword(prefix + "=>"))
case refs => Keyword(prefix + "->") :: emitCaptureSet(refs, skipThisTypePrefix)
case Nil => pureArrow
case List(ref) if ref.isCaptureRoot => impureArrow
case refs => ccToggle(Keyword(prefix + "->") :: emitCaptureSet(refs, skipThisTypePrefix), impureArrow)

private def emitByNameArrow(using Quotes)(captures: Option[List[reflect.TypeRepr]], skipThisTypePrefix: Boolean)(using elideThis: reflect.ClassDef, originalOwner: reflect.Symbol): SSignature =
emitFunctionArrow(CaptureDefs.Function1.typeRef, captures, skipThisTypePrefix)
Loading
Loading