mirror of
https://github.com/stablyai/orca.git
synced 2026-09-22 00:02:31 +00:00
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:
@@ -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'] })
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
})
|
||||
Reference in New Issue
Block a user