fix: highlight bash fences in Markdown source mode (#20592)

* fix: highlight bash fences in Markdown source mode

* refactor: trim shell fence alias registration

Drop the speculative exports and document the alias-resolution rationale in
one WHY comment; the idempotency guard stays.

---------

Co-authored-by: Jinjing <6427696+AmethystLiang@users.noreply.github.com>
This commit is contained in:
BAEK'space
2026-09-15 17:32:54 -07:00
committed by GitHub
co-authored by Jinjing
parent a9232e8db6
commit 3520e8eb41
4 changed files with 136 additions and 0 deletions
@@ -0,0 +1,32 @@
import { describe, expect, it, vi } from 'vitest'
import { registerShellMarkdownAliases } from './register-shell-markdown-aliases'
function createMonacoMock(aliases: string[] = ['Shell', 'sh']) {
return {
languages: {
getLanguages: vi.fn(() => [{ id: 'shell', aliases }]),
register: vi.fn()
}
}
}
describe('registerShellMarkdownAliases', () => {
it('registers bash alongside the built-in shell aliases', () => {
const monaco = createMonacoMock()
registerShellMarkdownAliases(monaco)
expect(monaco.languages.register).toHaveBeenCalledWith({
id: 'shell',
aliases: ['Shell', 'sh', 'bash']
})
})
it('does not register the alias again when Monaco already exposes it', () => {
const monaco = createMonacoMock(['Shell', 'sh', 'Bash'])
registerShellMarkdownAliases(monaco)
expect(monaco.languages.register).not.toHaveBeenCalled()
})
})
@@ -0,0 +1,24 @@
import type * as Monaco from 'monaco-editor'
type MonacoModule = typeof Monaco
// Why: Monaco resolves Markdown fences by alias (never extension) and its shell
// language declares `bash` only as an extension, so ```bash rendered plain while
// ```sh highlighted. Re-registering id 'shell' merges the alias and keeps the
// built-in tokenizer; `Shell` stays first because Monaco uses the first alias as
// the language's display name.
export function registerShellMarkdownAliases(monaco: {
languages: Pick<MonacoModule['languages'], 'getLanguages' | 'register'>
}): void {
const bashAlreadyRegistered = monaco.languages
.getLanguages()
.some(
({ id, aliases }) =>
id === 'shell' && aliases?.some((alias) => alias.toLowerCase() === 'bash')
)
if (bashAlreadyRegistered) {
return
}
monaco.languages.register({ id: 'shell', aliases: ['Shell', 'sh', 'bash'] })
}
+2
View File
@@ -10,6 +10,7 @@ import tsWorker from 'monaco-editor/esm/vs/language/typescript/ts.worker?worker'
import { registerAstroLanguage } from './monaco-languages/register-astro'
import { registerJsonlLanguage } from './monaco-languages/register-jsonl'
import { registerNimLanguage } from './monaco-languages/register-nim'
import { registerShellMarkdownAliases } from './monaco-languages/register-shell-markdown-aliases'
import { registerSvelteLanguage } from './monaco-languages/register-svelte'
import { registerVueLanguage } from './monaco-languages/register-vue'
import { installMonacoDelayerCancellationGuard } from './monaco-delayer-cancellation-guard'
@@ -79,6 +80,7 @@ registerSvelteLanguage(monaco)
registerAstroLanguage(monaco)
registerNimLanguage(monaco)
registerJsonlLanguage(monaco)
registerShellMarkdownAliases(monaco)
installMonacoDelayerCancellationGuard()
installMonacoDiffEditorDisposalGuard(monaco)
installMonacoPeekReferencesPreviewOptions()
@@ -0,0 +1,78 @@
import type { Locator, Page } from '@stablyai/playwright-test'
import { test, expect } from './helpers/orca-app'
import { waitForActiveWorktree, waitForSessionReady } from './helpers/store'
import {
cleanupMarkdownFixture,
createMarkdownFixture,
getActiveWorktreeContext,
openMarkdownFixture,
waitForRichMarkdownEditor
} from './helpers/markdown-editor-fixture'
const MARKDOWN = `\`\`\`bash
printf '%s\\n' "build complete" # bash-highlight-marker
\`\`\`
\`\`\`sh
printf '%s\\n' "build complete" # shell-control-marker
\`\`\`
`
async function switchToSourceMode(page: Page): Promise<void> {
await page.evaluate(() => {
const store = window.__store
if (!store) {
throw new Error('window.__store is not available')
}
const state = store.getState()
if (!state.activeFileId) {
throw new Error('No active editor file')
}
state.setMarkdownViewMode(state.activeFileId, 'source')
})
}
async function distinctLeafTokenColors(line: Locator): Promise<number> {
return line.locator('span').evaluateAll((spans) => {
const colors = spans
.filter((span) => span.childElementCount === 0 && span.textContent?.trim())
.map((span) => window.getComputedStyle(span).color)
return new Set(colors).size
})
}
test('highlights bash and sh fences in Markdown Source mode', async ({ orcaPage }, testInfo) => {
await waitForSessionReady(orcaPage)
await waitForActiveWorktree(orcaPage)
const context = await getActiveWorktreeContext(orcaPage)
let filePath: string | null = null
try {
filePath = await createMarkdownFixture(
context,
'.orca-e2e-markdown-source-highlighting',
'bash-and-sh',
testInfo.workerIndex,
MARKDOWN
)
await openMarkdownFixture(orcaPage, context, filePath)
await waitForRichMarkdownEditor(orcaPage)
await switchToSourceMode(orcaPage)
const monaco = orcaPage.locator('.monaco-editor').first()
await expect(monaco).toBeVisible({ timeout: 25_000 })
for (const marker of ['bash-highlight-marker', 'shell-control-marker']) {
const line = monaco.locator('.view-line').filter({ hasText: marker })
await expect(line).toHaveCount(1)
await expect
.poll(() => distinctLeafTokenColors(line), {
message: `${marker} should render with distinct shell token colors`
})
.toBeGreaterThan(1)
}
} finally {
await cleanupMarkdownFixture(filePath)
}
})