diff --git a/mobile/src/session/mobile-file-syntax.ts b/mobile/src/session/mobile-file-syntax.ts index 9756b3eb6c3..4156291b2f4 100644 --- a/mobile/src/session/mobile-file-syntax.ts +++ b/mobile/src/session/mobile-file-syntax.ts @@ -50,7 +50,9 @@ const LANGUAGE_ALIASES: Record = { zsh: 'bash', fish: 'bash', yml: 'yaml', - 'cuda-cpp': 'cpp' + 'cuda-cpp': 'cpp', + 'glimmer-js': 'javascript', + 'glimmer-ts': 'typescript' } export function resolveMobileSyntaxLanguage(filePath: string, preferredLanguage?: string): string { diff --git a/src/renderer/src/lib/monaco-languages/monarch-embed-entry-recursion.test.ts b/src/renderer/src/lib/monaco-languages/monarch-embed-entry-recursion.test.ts index f0d05443cb9..2a28ab57c48 100644 --- a/src/renderer/src/lib/monaco-languages/monarch-embed-entry-recursion.test.ts +++ b/src/renderer/src/lib/monaco-languages/monarch-embed-entry-recursion.test.ts @@ -11,6 +11,7 @@ import { tokenizeLines } from './monarch-tokenizer-test-harness' import { astroMonarchLanguage } from './register-astro' +import { glimmerTsMonarchLanguage } from './register-glimmer' import { svelteMonarchLanguage } from './register-svelte' import { vueMonarchLanguage } from './register-vue' @@ -202,3 +203,42 @@ describe('vue embed-entry recursion', () => { expect(measurement.maxNestedDepth).toBeGreaterThan(0) }) }) + +describe('glimmer embed-entry recursion', () => { + const islandLine = (count: number): string => + `const a = 1; ${''.repeat(count)}` + + it('stays within the embed budget for a line of template islands', () => { + const ramp = [50, 200, 500, 900].filter( + (count) => islandLine(count).length < MAX_TOKENIZATION_LINE_LENGTH + ) + expect(ramp.length).toBeGreaterThanOrEqual(3) + + const depths = ramp.map((count) => + measureNestedDepth(createMonarchTokenizer('glimmer-ts', glimmerTsMonarchLanguage), [ + islandLine(count) + ]) + ) + + for (const measurement of depths) { + expect(measurement.error).toBeUndefined() + expect(measurement.maxNestedDepth).toBeLessThanOrEqual(EMBED_ENTRY_REST_OF_LINE_BUDGET) + } + }) + + it('re-embeds script and template after over-budget lines', () => { + const long = 'x'.repeat(EMBED_ENTRY_REST_OF_LINE_BUDGET) + const embeds = endEmbeddedLanguages( + tokenizeLines(createMonarchTokenizer('glimmer-ts', glimmerTsMonarchLanguage), [ + `const s = "${long}"`, + 'const b = 1', + `', + 'const c = 1' + ]) + ) + + expect(embeds).toEqual([null, 'typescript', null, 'handlebars', null, 'typescript']) + }) +}) diff --git a/src/renderer/src/lib/monaco-languages/register-glimmer.test.ts b/src/renderer/src/lib/monaco-languages/register-glimmer.test.ts new file mode 100644 index 00000000000..cd3db94134d --- /dev/null +++ b/src/renderer/src/lib/monaco-languages/register-glimmer.test.ts @@ -0,0 +1,89 @@ +import { describe, expect, it, vi } from 'vitest' +import { formatTokenizedLines, tokenizeMonarchDocument } from './monarch-tokenizer-test-harness' +import { + glimmerJsMonarchLanguage, + glimmerLanguageConfiguration, + glimmerTsMonarchLanguage, + registerGlimmerLanguages +} from './register-glimmer' + +describe('registerGlimmerLanguages', () => { + // Tokenizing cannot observe registration IDs/extensions/idempotence; structural checks cover that contract. + it('registers both Glimmer languages, tokenizers, and configuration once', () => { + const languages: { id: string }[] = [{ id: 'typescript' }] + const register = vi.fn((entry: { id: string }) => { + languages.push({ id: entry.id }) + }) + const setMonarchTokensProvider = vi.fn() + const setLanguageConfiguration = vi.fn() + const monacoMock = { + languages: { + register, + setMonarchTokensProvider, + setLanguageConfiguration, + getLanguages: () => languages + } + } + + registerGlimmerLanguages(monacoMock) + registerGlimmerLanguages(monacoMock) + + expect(register.mock.calls).toEqual([ + [{ id: 'glimmer-ts', extensions: ['.gts'], aliases: ['Glimmer TS'] }], + [{ id: 'glimmer-js', extensions: ['.gjs'], aliases: ['Glimmer JS'] }] + ]) + expect(setMonarchTokensProvider.mock.calls).toEqual([ + ['glimmer-ts', glimmerTsMonarchLanguage], + ['glimmer-js', glimmerJsMonarchLanguage] + ]) + expect(setLanguageConfiguration.mock.calls).toEqual([ + ['glimmer-ts', glimmerLanguageConfiguration], + ['glimmer-js', glimmerLanguageConfiguration] + ]) + }) +}) + +describe('glimmer tokenization', () => { + // Real Monarch tokenization: an embed popped without a matching push throws per line and drops highlighting. + it('alternates the script embed with Handlebars template islands', () => { + const fixture = `import Component from '@glimmer/component' +export default class Hello extends Component { + +} +const Inline = ; const Empty = ;` + + expect( + formatTokenizedLines(tokenizeMonarchDocument('glimmer-ts', glimmerTsMonarchLanguage, fixture)) + ).toMatchInlineSnapshot(` + [ + "import Component from '@glimmer/component' | 0:-@typescript | embed=typescript", + "export default class Hello extends Component { | 0:-@typescript | embed=typescript", + " | 0:-@handlebars 2:tag.gts@glimmer-ts | embed=none", + "} | 0:-@typescript | embed=typescript", + "const Inline = ; const Empty = ; | 0:-@typescript 15:tag.gts@glimmer-ts 25:-@handlebars 31:tag.gts@glimmer-ts 42:-@typescript 58:tag.gts@glimmer-ts 79:-@typescript | embed=typescript", + ] + `) + }) + + it('starts a template-only component in Handlebars and uses the javascript embed for .gjs', () => { + const fixture = ` +export const x = 1` + + expect( + formatTokenizedLines(tokenizeMonarchDocument('glimmer-js', glimmerJsMonarchLanguage, fixture)) + ).toMatchInlineSnapshot(` + [ + " | 0:tag.gjs@glimmer-js | embed=none", + "export const x = 1 | 0:-@javascript | embed=javascript", + ] + `) + }) +}) diff --git a/src/renderer/src/lib/monaco-languages/register-glimmer.ts b/src/renderer/src/lib/monaco-languages/register-glimmer.ts new file mode 100644 index 00000000000..b305dc862a6 --- /dev/null +++ b/src/renderer/src/lib/monaco-languages/register-glimmer.ts @@ -0,0 +1,109 @@ +import type * as Monaco from 'monaco-editor' +import { restOfLineWithinEmbedBudget } from './monarch-embed-entry-budget' + +type GlimmerMonaco = { + languages: Pick< + typeof Monaco.languages, + 'getLanguages' | 'register' | 'setLanguageConfiguration' | 'setMonarchTokensProvider' + > +} +type GlimmerBaseLanguage = 'typescript' | 'javascript' + +// Script is the background embed and each `