mirror of
https://github.com/stablyai/orca.git
synced 2026-10-08 00:02:38 +00:00
fix(monaco): highlight Svelte block closers inside markup
Return Svelte block closers to the existing markup tokenizer state. Co-authored-by: Wooseong Kim <innocarpe@gmail.com> Co-authored-by: Cursor <cursoragent@cursor.com> Co-authored-by: Neil <neil@stably.ai>
This commit is contained in:
co-authored by
Wooseong Kim
Cursor
Neil
parent
a5f28f265c
commit
df9acd83d4
@@ -1,7 +1,10 @@
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import { EMBED_ENTRY_REST_OF_LINE_BUDGET } from './monarch-embed-entry-budget'
|
||||
import {
|
||||
createMonarchTokenizer,
|
||||
endEmbeddedLanguages,
|
||||
formatTokenizedLines,
|
||||
measureNestedDepth,
|
||||
tokenizeMonarchDocument,
|
||||
tokenLanguages,
|
||||
tokenLanguagesPerLine,
|
||||
@@ -94,8 +97,8 @@ describe('svelte tokenization', () => {
|
||||
" <p>{count} clicked</p> | 0:-@html 5:delimiter.curly.svelte@svelte 6:-@typescript 11:delimiter.curly.svelte@svelte 12:-@html | embed=html",
|
||||
"{:else} | 0:keyword.control.svelte@svelte | embed=none",
|
||||
" <p>not yet</p> | 0:-@html | embed=html",
|
||||
"{/if} | 0:-@html | embed=html",
|
||||
" | 0:-@html | embed=html",
|
||||
"{/if} | 0:keyword.control.svelte@svelte | embed=none",
|
||||
" | | embed=html",
|
||||
"<button on:click={increment}>{count}</button> | 0:-@html 17:delimiter.curly.svelte@svelte 18:-@typescript 27:delimiter.curly.svelte@svelte 28:-@html 29:delimiter.curly.svelte@svelte 30:-@typescript 35:delimiter.curly.svelte@svelte 36:-@html | embed=html",
|
||||
"{@html '<em>raw</em>'} | 0:keyword.control.svelte@svelte 6:-@typescript 21:delimiter.curly.svelte@svelte | embed=none",
|
||||
" | | embed=html",
|
||||
@@ -159,11 +162,83 @@ describe('svelte tokenization', () => {
|
||||
['html'],
|
||||
['svelte'],
|
||||
['html'],
|
||||
['html'],
|
||||
['svelte'],
|
||||
['html']
|
||||
])
|
||||
})
|
||||
|
||||
it.each(['if', 'each', 'await', 'key', 'snippet'])(
|
||||
'highlights %s closers and resumes HTML with or without whitespace',
|
||||
(name) => {
|
||||
for (const closer of [`{/${name}}`, `{ \t/${name} \t}`]) {
|
||||
const lines = tokenizeSvelte(`<p>before</p>\n${closer}\n<p>after</p>`)
|
||||
expect(tokenTypeAt(lines[1], 0)).toBe('keyword.control')
|
||||
expect(tokenLanguages(lines[1])).toEqual(['svelte'])
|
||||
expect(tokenLanguages(lines[2])).toEqual(['html'])
|
||||
|
||||
const [sameLine] = tokenizeSvelte(`<p>before</p>${closer}<p>after</p>`)
|
||||
expect(tokenTypeAt(sameLine, '<p>before</p>'.length)).toBe('keyword.control')
|
||||
expect(tokenLanguages(sameLine)).toEqual(['html', 'svelte', 'html'])
|
||||
|
||||
const [fileStartCloser] = tokenizeSvelte(closer)
|
||||
expect(tokenTypeAt(fileStartCloser, 0)).toBe('keyword.control')
|
||||
expect(tokenLanguages(fileStartCloser)).toEqual(['svelte'])
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
it('preserves a CSS block after a Svelte closer', () => {
|
||||
expect(
|
||||
languagesPerLine(
|
||||
'{#each items as item}\n <p>{item}</p>\n{/each}\n<style>\n p { color: red; }\n</style>'
|
||||
)
|
||||
).toEqual([
|
||||
['svelte', 'typescript', 'svelte'],
|
||||
['html', 'svelte', 'typescript', 'svelte', 'html'],
|
||||
['svelte'],
|
||||
['svelte'],
|
||||
['css'],
|
||||
['svelte']
|
||||
])
|
||||
})
|
||||
|
||||
it('resumes HTML, expressions, script and CSS after an inline closer', () => {
|
||||
const source =
|
||||
'<p>before</p>{/if}<p>{value}</p><script>let a = 1</script><style>p {color:red}</style>'
|
||||
expect(tokenLanguages(tokenizeSvelte(source)[0])).toEqual([
|
||||
'html',
|
||||
'svelte',
|
||||
'html',
|
||||
'svelte',
|
||||
'typescript',
|
||||
'svelte',
|
||||
'html',
|
||||
'svelte',
|
||||
'typescript',
|
||||
'svelte',
|
||||
'css',
|
||||
'svelte'
|
||||
])
|
||||
})
|
||||
|
||||
it('keeps repeated closer reentry within the embed budget and recovers on the next line', () => {
|
||||
// HTML text between closers forces repeated embed reentry after the fallback.
|
||||
const closerWithText = '{/if}a'
|
||||
const longLine = `<p>${closerWithText.repeat(1000)}<p>after</p>`
|
||||
const tokenizer = createMonarchTokenizer('svelte', svelteMonarchLanguage)
|
||||
const measurement = measureNestedDepth(tokenizer, ['<p>before</p>', longLine, '<p>next</p>'])
|
||||
expect(measurement.error).toBeUndefined()
|
||||
expect(measurement.maxNestedDepth).toBeGreaterThan(1)
|
||||
expect(measurement.maxNestedDepth).toBeLessThanOrEqual(EMBED_ENTRY_REST_OF_LINE_BUDGET)
|
||||
|
||||
const lines = tokenizeSvelte(`<p>before</p>\n${longLine}\n<p>next</p>`)
|
||||
expect(tokenTypeAt(lines[1], '<p>'.length)).toBe('keyword.control')
|
||||
expect(tokenTypeAt(lines[1], '<p>'.length + closerWithText.length * 999)).toBe(
|
||||
'keyword.control'
|
||||
)
|
||||
expect(tokenLanguages(lines[2])).toEqual(['html'])
|
||||
})
|
||||
|
||||
it('keeps markup highlighted across a whole multi-line file', () => {
|
||||
// A grammar that drops the embed leaves plain `svelte` on these rows, which
|
||||
// is the silently-unhighlighted failure a rule-table walk cannot see.
|
||||
|
||||
@@ -79,7 +79,11 @@ export const svelteMonarchLanguage: Monaco.languages.IMonarchLanguage = {
|
||||
],
|
||||
[/<style(?=\s|>)/, { token: 'tag', switchTo: '@styleOpen.css', nextEmbedded: '@pop' }],
|
||||
[/<!--/, { token: 'comment', switchTo: '@comment', nextEmbedded: '@pop' }],
|
||||
[/\{\s*\/(if|each|await|key|snippet)\s*\}/, 'keyword.control'],
|
||||
// Only pop rules can interrupt the active HTML embed for a Svelte closer.
|
||||
[
|
||||
/\{\s*\/(if|each|await|key|snippet)\s*\}/,
|
||||
{ token: 'keyword.control', switchTo: '@markupReenter', nextEmbedded: '@pop' }
|
||||
],
|
||||
[
|
||||
/\{\s*#(if|each|await|key|snippet)\b/,
|
||||
{ token: 'keyword.control', switchTo: '@svelteBlockExpressionEnter', nextEmbedded: '@pop' }
|
||||
|
||||
Reference in New Issue
Block a user