Skip to content

CON-20: replace a typed-out 😄 on its closing colon - #86

Open
molgerz wants to merge 2 commits into
mainfrom
feat/con-20-emoji-shortcode-on-typing
Open

molgerz wants to merge 2 commits into
mainfrom
feat/con-20-emoji-shortcode-on-typing

Conversation

@molgerz

@molgerz molgerz commented Sep 14, 2026

Copy link
Copy Markdown
Owner

Closes CON-20.

A shortcode typed out in full is replaced on its closing colon: :smile: becomes 😄 without touching the : dropdown.

What is in it

Three guards keep it from firing where a colon is not a shortcode:

  • The opening : sits on a word boundary — start of line, whitespace, or an opening bracket ((, [, {). That leaves a:b:c, 12:30:45 and host:8080/x:y: exactly as typed.
  • The name must be one we know. An unknown :foo: stays text.
  • Never inside code. Closed code is caught by the syntax tree; a span the writer has only opened — `a :smile has no closing backtick, so lezer sees a plain paragraph — is caught by counting the backticks on the line. Before this, the replacement fired inside it and swallowed a code literal.

The look-back is a 64-character slice rather than a line start, so a word running past it used to look like a word boundary and a:b:c was replaced again on long lines; the character in front of the colon is read from the line now.

It runs as an inputHandler, so the typed colon never enters the document and the replacement is a single change. Note that one Ctrl+Z therefore removes the emoji together with the shortcode — it cannot put :smile: back whole, and docs/13 now says so instead of the opposite.

How to test

In the editor:

  1. Type ship it :rocket: — converts on the last colon. Ctrl+Z once removes the whole thing (documented behaviour, not a bug).
  2. Type a:b:c, 12:30:45, host:8080/x:y: — unchanged. Repeat on a line longer than 64 characters before the colon: still unchanged.
  3. Type :foo: — stays text.
  4. Type a fenced code block and an inline span containing :smile: — unchanged. Then type `a :smile: while the span is still open: unchanged (this is the regression fixed here).
  5. (:smile:), [:smile:], {:smile:} — converts.
  6. The : dropdown still works as before.
  7. Paste a chat log containing :tada: — it stays as pasted; only a retyped closing colon converts.

Automated: typecheck, lint, tests and build are green.

M and others added 2 commits September 14, 2026 14:10
Only the `:` dropdown converted a shortcode. Somebody who knows the name and
types it through — or pastes a line out of a chat log — kept `:smile:` in the
text, and the page then showed the shortcode where every other client shows
the character.

docs/13 left this open with the reason to be careful named: it "risks firing
inside things like a:b:c". Three guards answer that, and they are the whole
substance of the new module:

- The opening colon must sit on a word boundary — start of line, whitespace or
  an opening bracket. That is the dropdown's own rule, and it is what leaves
  a:b:c, 12:30:45 and host:8080/x:y: as typed: in each of them a word
  character stands in front of the colon that would open a shortcode.
- The name must be one of ours. An unknown :foo: stays text; the curated list
  being small is the feature here, not the limitation.
- Never inside code, where a shortcode is a string literal, a Ruby symbol or a
  YAML key. Known limit, written down: an inline span still being typed has no
  closing backtick, so the parser sees no span and the replacement does fire
  inside it. Closed code is caught.

It runs as an inputHandler, so the typed colon never reaches the document and
the replacement is one change — one Ctrl+Z brings :smile: back whole instead
of peeling off a colon.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…the window

An inline code span that is still being typed has no closing backtick, so lezer
sees a plain paragraph and the replacement fired inside it — swallowing a code
literal somebody meant to keep. Closed code is still caught by the syntax tree;
an open span is caught by counting the backticks on the line, which is the only
thing that can answer while the span is incomplete.

The look-back is a 64-character slice, not a line start, so a word running past
it came out looking like a word boundary and `a:b:c` was replaced again on a
long line. The character in front of the colon is read from the line now.

The boundary rule also admits `[` and `{`, and the tests no longer lean on
unknown names: with `a:b` the name guard answers first, so every one of those
rows would have passed with the boundary check deleted.

docs/13-editing.md said a pasted chat log stops showing `:tada:` and that one
Ctrl+Z brings `:smile:` back whole. Neither is true — it is the typed colon
that converts, and that colon never enters the document.
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.

1 participant