feat(composer): offer to format pasted code as a code block (#107)
CI / Build & Quality Checks (push) Successful in 1m52s
CI / Docker image build & smoke test (push) Skipped
CI / Secret scan (gitleaks) (push) Successful in 10s
CI / Trigger Desktop Build (push) Successful in 4s
CI / Playwright smoke (e2e) (push) Successful in 10m45s

When a multi-line paste looks like code, a chip above the composer asks
"That looks like code. Format it as a code block (js)?" with Format as code /
No thanks. It never converts on its own; typing, No, or 8 s dismiss it, and
Settings → Editor → "Offer to Format Pasted Code" turns it off.

- utils/looksLikeCode.ts (pure, tested): ≥ 3 lines and ≥ 2 of indentation
  with depth changes, statement terminators, operators + brackets, keywords,
  monospace/<pre> clipboard HTML, or SQL clause lines. Prose guards: quoted
  replies, URL lists, long sentence-punctuated lines, plain-word lines,
  markdown lists. Language guess only when fairly sure (js/ts/python/sql/
  rust/c/php).
- Accept rebuilds the pasted paragraphs as one code block from their plain
  text (code lines hold text only) and leaves the caret after it.
- Code blocks carry an optional lang → <code class="language-js"> (whitelisted
  identifier only).

Verified in Chromium: pasting a JS function shows the chip; Format as code →
sent formatted_body is <pre><code class="language-js"> with indentation intact;
a three-line prose paste shows no chip; typing after a paste dismisses it.
Unit tests: 14 detector fixtures + 2 output tests; chromium e2e 19 passed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PPmy3tPq869XDW4njjVaKA
This commit is contained in:
Lotus CI
2026-09-25 11:05:49 -04:00
co-authored by Claude Opus 5.5
parent e91b5fe10e
commit 75d55e861b
8 changed files with 391 additions and 4 deletions
+19
View File
@@ -89,3 +89,22 @@ test('markdown: no math conversion inside a backtick span, math outside still co
assert.ok(/<code[^>]*>\$x\$<\/code>/.test(out), 'backtick span stays literal');
assert.ok(out.includes('data-mx-maths="y"'));
});
test('[#107] a code block language becomes class="language-…"', () => {
const block = {
...(el(BlockType.CodeBlock, [el(BlockType.CodeLine, [txt('let a = 1;')])]) as object),
lang: 'js',
} as unknown as Descendant;
assert.equal(
toMatrixCustomHTML(block, OPTS),
'<pre><code class="language-js">let a = 1;\n</code></pre>',
);
});
test('[#107] an unsafe language value is dropped', () => {
const block = {
...(el(BlockType.CodeBlock, [el(BlockType.CodeLine, [txt('x')])]) as object),
lang: 'js" onclick="x',
} as unknown as Descendant;
assert.equal(toMatrixCustomHTML(block, OPTS), '<pre><code>x\n</code></pre>');
});
+4 -1
View File
@@ -86,7 +86,10 @@ const elementToCustomHtml = (node: CustomElement, children: string): string => {
case BlockType.CodeLine:
return `${children}\n`;
case BlockType.CodeBlock:
return `<pre><code>${children}</code></pre>`;
// [Gitea #107] Only a plain identifier is ever put in the class.
return node.lang && /^[a-z0-9+#-]{1,20}$/.test(node.lang)
? `<pre><code class="language-${node.lang}">${children}</code></pre>`
: `<pre><code>${children}</code></pre>`;
case BlockType.QuoteLine:
return `${children}<br/>`;
case BlockType.BlockQuote:
+2
View File
@@ -64,6 +64,8 @@ export type CodeLineElement = {
};
export type CodeBlockElement = {
type: BlockType.CodeBlock;
/** [Gitea #107] Optional language → `<code class="language-…">`. */
lang?: string;
children: CodeLineElement[];
};
export type QuoteLineElement = {