Skip to content
Merged
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
6 changes: 6 additions & 0 deletions .changeset/stepper-hover-text-popover.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@sourceacademy/common-stepper": patch
"@sourceacademy/web-stepper": patch
---

Add a `hoverText` `SyntaxProfile` capability: a node type can now show a fixed-text hover popover (e.g. `built-in function print`) alongside its normal inline rendering, without collapsing/replacing that rendering the way `functionValues`'s mu-term does. Unlike a function value's popover (the node's own template, i.e. a body, rendered on demand), this shows a single already-formatted line the language stashed on the node ahead of time — there is no body to expand. A language opts in by listing the node type and the property holding that text in its `SyntaxProfile.hoverText`; `web-stepper` renders the popover generically from the rule, no per-language host code.
27 changes: 27 additions & 0 deletions src/common/stepper/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,28 @@ export interface FunctionValueRule {
nameProp: string;
}

/**
* Declares a node type that shows a fixed hover popover alongside its normal inline rendering.
*
* Unlike {@link FunctionValueRule} — whose popover is the node's own template, i.e. a function's body,
* rendered on demand — this popover is a single line of *plain text* the language computed ahead of
* time and stashed on the node (e.g. `"built-in function print"` for a builtin referenced as a value,
* or `"module function stack"` for a name imported from a module): there is no body to expand, so
* nothing is collapsed or replaced — the node still renders exactly as `templates[type]` produces it,
* with a popover merely added on top. The host implements this generically from these rules, so any
* language gets the behaviour for any node type by listing it here — no per-language host code.
*/
export interface HoverTextRule {
/** The node `type` this applies to, e.g. `"Builtin"`. */
type: string;
/**
* Dotted path to the node property holding the popover's already-formatted plain-text content (e.g.
* `"hoverText"`, or `"decl.hoverText"` for a node whose text lives on a child). When the path
* resolves to an empty value, no popover is added for that particular node.
*/
textProp: string;
}

/**
* A language's complete rendering rules: a per-node-type template table plus the precedence maps the
* host uses to insert parentheses generically. Authored once per language and shipped by its runner.
Expand All @@ -178,6 +200,11 @@ export interface SyntaxProfile {
* collapsed mu-term + hover popover instead of expanding its body inline. See {@link FunctionValueRule}.
*/
functionValues?: FunctionValueRule[];
/**
* Node types that show a fixed-text hover popover alongside their normal inline rendering. See
* {@link HoverTextRule}.
*/
hoverText?: HoverTextRule[];
}

/* -------------------------------------------------------------------------- */
Expand Down
39 changes: 39 additions & 0 deletions src/web/stepper/src/SubstVisualizer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -380,6 +380,23 @@ function ProfileFunctionDefinitionPopover({
);
}

/**
* The popover body for a {@link HoverTextRule}: a single already-formatted line the language
* computed ahead of time (e.g. `"built-in function print"`), unlike
* {@link ProfileFunctionDefinitionPopover}'s expanded function body — there is no body to render here,
* just the text, inside the same chrome the function-definition popover uses.
*/
function ProfileHoverTextPopover({ text }: { text: string }) {
return (
<div className={classNames("stepper-popover", Classes.DARK)}>
<div className="stepper-display">
<Icon icon="info-sign" />
<span>{` ${text}`}</span>
</div>
</div>
);
}

/**
* renderNode renders a serialized Stepper AST node to a React ReactNode.
*/
Expand Down Expand Up @@ -941,6 +958,28 @@ function renderNode(
const template = profile.templates[currentNode.type];
result = template ? renderTemplate(currentNode, template) : `<${currentNode.type}>`;
}

// A fixed-text hover popover (e.g. a builtin's `<built-in function print>`) is independent of the
// function-value mu-term collapse above and applies regardless of which branch produced `result` —
// a node type can be listed in both `functionValues` and `hoverText` and get both behaviours, not
// just whichever branch happened to run first. See SyntaxProfile.hoverText.
const hoverRule = profile.hoverText?.find(rule => rule.type === currentNode.type);
const hoverText = hoverRule ? readNodeProp(currentNode, hoverRule.textProp) : undefined;
if (typeof hoverText === "string" && hoverText !== "") {
const content = result;
result = (
<Popover
interactionKind="hover"
placement="bottom"
usePortal={popoverDepth === 0}
lazy
popoverClassName="stepper-popover"
content={<ProfileHoverTextPopover text={hoverText} />}
>
{content}
</Popover>
);
}
} else {
const renderer = (
renderers as unknown as Record<string, (node: StepperNode) => React.ReactNode>
Expand Down
Loading