Skip to content

Read code as literal text, and put its panel on the theme - #2

Open
scottjones wants to merge 3 commits into
omacom-io:masterfrom
scottjones:literal-code-spans
Open

Read code as literal text, and put its panel on the theme#2
scottjones wants to merge 3 commits into
omacom-io:masterfrom
scottjones:literal-code-spans

Conversation

@scottjones

@scottjones scottjones commented Jul 29, 2026

Copy link
Copy Markdown

Identifiers with underscores were being mangled in the editor. Between
backticks, default_line_height came out italic with its underscores hidden,
and the caret skipped over them as though they were markup — the inline rules
ran over the raw line and never noticed the code span. Fenced blocks had the
same problem for a different reason: the highlighter had no memory of being
inside one, so a code block was read as prose.

Before:
image

After:
image

Three changes: inline code is read as literal text; underscores now need a word
boundary to open emphasis, the way CommonMark and GitHub read them, so
snake_case_name survives in plain prose too; and fenced blocks are left alone
end to end. Asterisks still delimit anywhere, and Ctrl+B / Ctrl+I insert
asterisks, so the toolbar is unaffected. While in there, the panel code sits on
was a hardcoded #1c1a1a that clashed with every theme — it now takes the
lighter background the Omarchy theme names, falling back to a shade mixed from
the page for the themes that omit it or set it equal to the background.

One deliberate rough edge: the panel is painted per character, so a fenced
block's right edge steps with line length and a blank line inside a block gets
no panel at all. A continuous one needs either a block format the highlighter
can't set or a rectangle layer in QML, and neither seemed worth it here.

Test plan

  • bin/test passes (16 tests)
  • Inline code with underscores stays literal, and the caret walks through it
    a character at a time instead of jumping
  • Emphasis still works: _italic_, *italic*, **bold**, __bold__,
    Ctrl+B and Ctrl+I
  • snake_case_name in prose is left alone
  • Fenced block contents are unstyled, and a # comment on its own line
    inside a block is not set as a heading
  • The code panel follows the theme in both light and dark mode

Verified by hand on osaka-jade (dark) and in light mode.

🤖 Generated with Claude Code

scottjones and others added 3 commits July 29, 2026 09:32
An identifier like `default_line_height` came out italic with its
underscores hidden: the inline rules ran over the raw line, so every
underscore between backticks looked like an emphasis marker. Collect the
code spans first and drop any markup whose markers land inside one.
Matching on the markers rather than the whole span keeps emphasis that
merely wraps code, as in **bold with `code` inside**, intact.

Underscores now also need a word boundary to open emphasis, the way
CommonMark and GitHub read them, so snake_case_name is left alone in
prose too. Asterisks still delimit anywhere, and Ctrl+B and Ctrl+I
insert asterisks, so nothing changes for the toolbar.

Both fixes live in inlineMarkup, the source the editor shares through
Backend::hiddenRangesAt, so the caret stops skipping over underscores it
should never have hidden.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Everything between a pair of ``` fences is literal, but the highlighter
had no memory of where it stood, so a code block was read as prose:
snake_case_name came out italic with its underscores hidden, and a
comment opening a line was set as a bold heading. The editor already
counted fences for smartReturn, so it knew what the highlighter did not.

Carry the state from block to block instead. Inside a fence nothing is
markup and no markers are hidden; the code sits on the same panel inline
code gets, and the fences themselves recede the way a heading's `#` does.
The state lives on the block, which is how Backend::hiddenRangesAt learns
to stop skipping the caret over underscores in code.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Inline code and fenced blocks sat on a hardcoded #1c1a1a, a warm grey
left over from before the editor followed the desktop. On the current
theme it read as a smudge against a green page while every colour around
it tracked the theme.

Omarchy themes name a lighter background for panels exactly like this
one, so use the shade the theme chose. Not every theme sets the key, and
a few set it to the page background, which would leave code with no
panel at all; those fall back to a shade mixed from the page towards the
text. Mixing rather than lightening keeps the shade moving on a pure
black background, and lands on the theme's own hue either way.

Backend resolves the colour and publishes it, the way it already does
for the page and the accent, so the highlighter is handed a colour to
paint rather than a theme to interpret.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@omarchybot

Copy link
Copy Markdown
Collaborator

Reviewed both halves against the source and on a disposable VM. bin/test builds clean and all 16 tests pass on Qt 6.11.1. Nothing pushed — everything below is minor, and the two behaviour questions are yours to call.

The theme half holds up under the checks I had doubts about. m_themeLighterBackground.clear() at backend.cpp:599 is unconditional with no early return above it, and loadOmarchyTheme() is the only writer, so switching themes at runtime cannot inherit the previous theme's panel. .name() is never taken on an invalid QColorlighter.isValid() guards the theme string and blend() always returns a valid fromRgbF colour — and that guard is load-bearing, since QColor("garbage").name() is #000000, which would have put a black panel on a light theme. The 6% mix measures #0f0f0f (lightness 15) on a pure black page and #f0f0f0 (240) on a pure white one, so the comment's claim about mixing beating lightening is right. All 22 stock themes ship lighter_background, and the only two where it equals the page — last-horizon and solitude — are exactly the fallback case; they land on #1a191a and #1b1e20.

Three small things:

  • markdownhighlighter.cpp:118 stores the closing fence line as Prose — opening and interior lines get InsideFence, the closing one does not. That is what your own test asserts and it matches the fence-parity convention smartReturn() already uses in Main.qml, so it is defensible. The consequence is that backend.cpp:387 does not take its early return on that line: hiddenRangesAt() parses it as prose while highlightBlock() has painted the whole line with m_fenceFormat and hidden nothing. For a bare ``` no ranges come back and it is harmless, but on a line like ``` **x** the caret skips over four characters the user can see. Cheapest fix is to widen the guard to cover a fence line as well as an interior one.
  • markdownhighlighter.cpp:225 — codeSpans() does not honour a backslash-escaped backtick, and it now decides whether emphasis is dropped rather than only where a panel is drawn. use \ in code like `x`renderscode` italic on master and renders it literally here. CommonMark agrees with your version for that particular line, so this is arguably a fix, but escaped backticks are a real divergence.
  • The comment at markdownhighlighter.cpp:245 says markup that merely wraps a code span still applies, and the markup does — but the code span loses its panel while doing so. setFormat() replaces rather than merges, so the bold format applied over the content wipes the background: for **\file_name` only**` the whole run comes back with no background and visible backticks. This is not something you introduced — master does exactly the same — but the comment reads like a promise the rendering does not quite keep.

Unterminated fences behave correctly: every remaining block stays InsideFence to end of document, which is what CommonMark specifies, and the initial previousBlockState() == -1 is handled. Qt's incremental rehighlighting cascades properly when a fence is typed, deleted or split.

Heads up that PRs #5, #8, #15 and #17 all touch src/markdownhighlighter.{h,cpp} and tests/tst_omawrite.cpp; #15 adds another inline kind and will want the same code-span exclusion this PR introduces.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants