Skip to content

Render <kbd> keyboard-shortcut tags as keycap badges in the Markdown viewer#13916

Open
fbartho wants to merge 10 commits into
warpdotdev:masterfrom
fbartho:fb/13733-markdown-kbd-implementation
Open

Render <kbd> keyboard-shortcut tags as keycap badges in the Markdown viewer#13916
fbartho wants to merge 10 commits into
warpdotdev:masterfrom
fbartho:fb/13733-markdown-kbd-implementation

Conversation

@fbartho

@fbartho fbartho commented Jul 17, 2026

Copy link
Copy Markdown

Closes #13733

Summary

Renders Markdown <kbd> keyboard-shortcut elements as a distinct keycap badge, addressing #13733. A keycap is now visually differentiated from an inline-code span, and nested <kbd> no longer leaks literal tags.

What changed

Parsing & round-trip (pre-existing commits in this stack):

  • Parse <kbd> / </kbd> (case-insensitive) into a dedicated kbd inline style, kept distinct from inline_code so it round-trips back to <kbd> rather than a backtick code span.
  • Thread the kbd style through the buffer and Markdown/HTML serialization.

Keycap styling (this stack):

  • Distinct border. A keycap draws a visible outline (the run's foreground color, which contrasts with the chip background by construction). An inline-code chip stays borderless (its border color equals its background). This is the core fix: previously the keycap reused the inline-code chip verbatim, so the two were indistinguishable.
  • Raised-key bottom edge. The keycap additionally gets a heavier bottom edge for a "raised key" cue, matching the GitHub/MDN keycap convention. TextBorder gained an optional bottom_width; the GPU paths already carry per-side border widths, so no shader work was needed. The macOS CoreText attribute round-trip carries the new field.
  • Both renderers (editor style_and_font and the warpui_core formatted-text element) apply this consistently.

Nested <kbd> flat-collapse (this stack):

  • <kbd><kbd>Ctrl</kbd>+<kbd>N</kbd></kbd> previously leaked literal <kbd>/</kbd> text (the parser copied the non-nesting <u> logic, which deactivated the still-open outer delimiter on the first inner close). It now collapses deterministically into one flat keycap over the inner content (Ctrl+N), for any nesting depth. Serialization emits the canonical flat form <kbd>Ctrl+N</kbd>.

Testing

  • Parser: flat-collapse shape, deep nesting, no-tag-leak guarantee, unmatched/unterminated edge cases.
  • Render: keycap styling asserted to differ from inline-code (visible border + bottom edge on the keycap, neither on the code chip) in both renderers — these fail against the pre-fix shared styling.
  • Serialization: authored text (and the canonical flat form for nested input) round-trips through Markdown, HTML, and plain buffer text; the rendered form never leaks into the data model.
  • Gates: cargo test on markdown_parser (156), warp_editor (478 pass; the 8 render::tests::* failures are pre-existing parking_lot::once/env_logger static-init poisoning, unrelated to this change), warpui_core; clippy -D warnings; fmt --check — all clean.

Known limitations (deliberate scope splits)

Test document used for the screenshot
# `<kbd>` keycap verification

Each line is labeled so kbd-vs-code-vs-other can be judged from a screenshot alone.

1. RAW-KBD: Press <kbd>Cmd</kbd> + <kbd>K</kbd> to open the command palette.
2. CODE-SPAN: Press `Cmd` + `K` to open the command palette.
3. BOLD: Press **Cmd** + **K** to open the command palette.
4. LITERAL-ESCAPED: Press &lt;kbd&gt;Cmd&lt;/kbd&gt; (should show literal angle-bracket text, not a chip).
5. KBD-NESTED-IN-BOLD: **Press <kbd>Cmd</kbd> now** (chip should survive inside bold).
6. MALFORMED-UNCLOSED: Press <kbd>Cmd to continue (no closing tag — should NOT render as a chip; tag and text should show literally or gracefully degrade, not crash).
7. MULTI-KEY-COMBO: <kbd>Ctrl</kbd>+<kbd>Shift</kbd>+<kbd>P</kbd> for command search.
8. GLYPH-CMD-ALONE: <kbd>Cmd</kbd> — glyph substitution is OUT OF SCOPE for this PR (deferred to follow-up #13913); should render as a keycap containing the literal word "Cmd", NOT the mac symbol ⌘. This line pins that substitution does not happen.
9. GLYPH-K-PASSTHROUGH: <kbd>K</kbd> — non-modifier key, should render as a keycap containing literal "K" (no glyph substitution, consistent with #8 above — nothing gets substituted in this PR).
10. GLYPH-SPACE-UNMAPPED: <kbd>Space</kbd> — should render as a keycap containing the literal word "Space" (no substitution, same as #8/#9 — this line has no unmapped-vs-mapped distinction to make until #13913 lands).
11. NESTED-KBD: <kbd><kbd>Ctrl</kbd>+<kbd>N</kbd></kbd> — scope is deterministic FLAT COLLAPSE for this PR (deep nesting deferred to follow-up #13912): must render as a SINGLE keycap badge containing "Ctrl+N" as its content. Must NOT leak literal `<kbd>`/`</kbd>` tag text (today's bug), must NOT double-badge, and must NOT render per-inner-key badges (that's the deferred depth-aware behavior in #13912).
12. NESTED-KBD-DEEP: <kbd><kbd><kbd><kbd><kbd>X</kbd></kbd></kbd></kbd></kbd> — pathological depth (5 levels); same flat-collapse rule applies — should degrade deterministically into a single keycap containing "X", not crash or produce garbled/leaked-tag output.
13. KBD-IN-TABLE:

| Shortcut | Action |
| --- | --- |
| <kbd>Cmd</kbd>+<kbd>K</kbd> | Open command palette |
| `Cmd`+`K` | (code-span comparison row) |
Screenshot demonstrating the kbd tag's behavior as of this PR

fbartho added 8 commits July 17, 2026 13:18
Add a kbd inline style flag to FormattedTextStyles and recognize
<kbd>text</kbd> in both the file-viewer Markdown grammar (a delimiter
pair mirroring <u>) and the paste-to-HTML path. Tags are matched
case-insensitively; unmatched or unterminated tags degrade to literal
text. Round-trips back to <kbd> in the table Markdown serializer.

The kbd flag is kept distinct from inline_code so it can serialize back
to <kbd> rather than a backtick span and later carry a differentiated
keycap visual.
…rpdotdev#13733)

Add a first-class BufferTextStyle::Kbd so the file Markdown viewer's
piece-table markers carry the kbd style, mirroring InlineCode across
TextStyles, TextStylesWithMetadata, and StyleSummary. Serialize kbd runs
back to <kbd> in the local Markdown table serializer and as a <kbd>
element in the HTML export and table-cell paths.

The table cell offset map already skips source-only markers generically,
so <kbd> tags need no special handling there; a regression test locks
that in. Round-trip and HTML tests exercise the full buffer path. The
random fuzz golden is refreshed since Kbd joins the style generator.
Give kbd-styled runs the same monospace font and background/border chip
treatment as inline code in style_and_font, reusing inline_code_style so
there is no new theming surface. Keep it a shared branch so a future
differentiated keycap look can diverge without touching inline code.
…arpdotdev#13733)

The GUI formatted-text element (used for AI/notebook formatted text)
renders FormattedTextInline directly rather than via the editor buffer,
so it needs its own kbd branch. Give kbd runs the same inline-code chip
background and monospace font here too, for parity with the file
Markdown viewer.
Render <kbd> keycaps as a bordered badge that is visually distinct from
an inline-code chip, rather than reusing the identical chip styling.

Both renderers (editor style_and_font and warpui_core formatted-text
element) now split the shared inline_code/kbd styling branch: a keycap
draws a visible outline in the run's foreground color, while an
inline-code chip stays borderless (its border color matches its
background). The foreground color contrasts with the chip background by
construction, guaranteeing the outline is visible.

Render tests now assert the discriminator (keycap border visible and
distinct from the inline-code chip, which stays borderless); the shared
styling made these fail before the split.
Give the <kbd> keycap a heavier bottom edge for a "raised key" cue,
matching the GitHub/MDN keycap convention, on top of the visible border
that already distinguishes it from an inline-code chip.

TextBorder gains an optional bottom_width. When set, paint_run_background
draws a second bottom-only border overlay thicker than the uniform border
(TextBorder.width alone cannot express an asymmetric edge). The GPU paths
already carry per-side border widths, so no shader work is needed; the
macOS CoreText attribute round-trip carries the new field so the border
survives native line-breaking. Only the kbd keycap sets bottom_width; the
inline-code chip leaves it None.
)

Add a guardrail test asserting every export path (markdown, HTML, plain
buffer text) round-trips the authored `<kbd>Cmd</kbd>` text and never the
rendered key glyph (⌘). Key-glyph substitution is a render-only concern
and must never leak into the data model or serializers; this test locks
that invariant in before any glyph-substitution work lands.
Nested `<kbd><kbd>Ctrl</kbd>+<kbd>N</kbd></kbd>` previously leaked literal
`<kbd>`/`</kbd>` text because parse_kbd (copied from the non-nesting <u>
logic) deactivated the still-open outer delimiter when the first inner
close was seen. Track open-kbd nesting depth and drop inner `<kbd>`/`</kbd>`
tags so the whole span renders as one flat keycap over the inner content,
deterministically for any depth.

The nesting is discarded at parse time, so serialization emits the
canonical flat form `<kbd>Ctrl+N</kbd>` rather than the authored nested
markup. Depth-aware per-key badging (outer keycap wrapping inner keycaps)
is deferred to issue warpdotdev#13912.

Tests cover the flat-collapse shape, deep nesting, the no-tag-leak
guarantee, unmatched/unterminated edge cases, and the flat serialization.
@cla-bot cla-bot Bot added the cla-signed label Jul 17, 2026
@github-actions github-actions Bot added the external-contributor Indicates that a PR has been opened by someone outside the Warp team. label Jul 17, 2026
@oz-for-oss

oz-for-oss Bot commented Jul 17, 2026

Copy link
Copy Markdown
Contributor

@fbartho

I'm starting a first review of this pull request.

You can view the conversation on Warp.

I completed the review and no human review was requested for this pull request.

Comment /oz-review on this pull request to retrigger a review (up to 3 times on the same pull request).

Powered by Oz

@oz-for-oss oz-for-oss Bot left a comment

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.

Overview

This PR adds first-class <kbd> inline styling through Markdown parsing, buffer conversion, HTML/Markdown serialization, and both text rendering paths so keyboard shortcuts render as keycap badges instead of inline-code chips.

Concerns

  • Combined <kbd> + underline styles can serialize with crossed tags because the Markdown serializer now opens <kbd> before <u> but closes </kbd> before </u>.
  • Case-insensitive <kbd> parsing does not preserve the original matched tag text on malformed/unmatched fallbacks, so mixed-case literal tags can be silently rewritten.

Verdict

Found: 0 critical, 2 important, 0 suggestions

Request changes

Comment /oz-review on this pull request to retrigger a review (up to 3 times on the same pull request).

Powered by Oz

Comment thread crates/editor/src/content/markdown.rs Outdated
buf.push_str("~~");
}

if start_kbd {

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.

⚠️ [IMPORTANT] Starting <kbd> before <u> conflicts with the new close order, so a run that is both kbd and underline serializes as <kbd><u>…</kbd></u>. Move the kbd start after the underline start so exported tags remain properly nested.

) -> IResult<&'a str, InlineToken<'a>, E> {
context(
"kbd_start",
map(tag_no_case("<kbd>"), |_| InlineToken::Delimiter {

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.

⚠️ [IMPORTANT] tag_no_case discards the matched spelling, so malformed mixed-case tags fall back as synthesized lowercase literals (for example, <KBD>Esc becomes <kbd>Esc, and </KBD> becomes </kbd>). Preserve the matched slice for literal fallback or keep malformed tags case-sensitive.

fbartho added 2 commits July 17, 2026 15:32
…#13733)

A run that is both kbd and underlined serialized as crossed tags
(<kbd><u>…</kbd></u>) because the Markdown open order was <kbd> then
<u> while the close order is </kbd> then </u>. Open <u> before <kbd> so
the tags mirror the close order and nest properly (<u><kbd>…</kbd></u>),
which also round-trips back to the same styles. The HTML export paths
already nested correctly and are unchanged.
…arpdotdev#13733)

Case-insensitively matched <kbd>/</kbd> tags that degraded to literal
text were rewritten to canonical lowercase, so <KBD>Esc became <kbd>Esc
and a stray </KBD> became </kbd>. tag_no_case discarded the matched
slice, and the literal fallback reconstructed the tag from the canonical
spelling. Thread the matched source slice through the delimiter and
kbd-end tokens and echo it verbatim on the literal-text path. The
well-formed case-insensitive path (<KBD>x</KBD> to keycap) is unchanged.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

cla-signed external-contributor Indicates that a PR has been opened by someone outside the Warp team.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Markdown viewer: support <kbd> keyboard-shortcut styling

1 participant