From 8f746cd3d0e30e3b0eda84f6a0e83082b3702860 Mon Sep 17 00:00:00 2001 From: Sterling Greene Date: Thu, 4 Jun 2026 21:16:24 -0400 Subject: [PATCH] syntax: highlight groovy, gradle, properties, asciidoc, kts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add syntax highlighting for Gradle-ecosystem and AsciiDoc files in both the FileView/diff viewers (highlight.js) and the Monaco file editor. highlight.js (src/renderer/syntax.ts): - register groovy, properties, asciidoc languages - EXT_TO_LANG: .groovy → groovy, .gradle → groovy, .properties → properties, .adoc/.asciidoc → asciidoc; .gradle.kts resolves to kotlin via its ext Monaco (src/renderer/monaco-setup.ts + monaco-grammars/): - hand-rolled Monarch grammars for groovy and asciidoc - .groovy/.gvy/.gy/.gsh/.gradle → groovy - .gradle.kts → kotlin (built-in; compound ext checked before single) - .adoc/.asciidoc → asciidoc, .properties → ini Build scripts use the stock Groovy/Kotlin languages rather than bespoke Gradle DSL grammars. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/renderer/highlight-common.d.ts | 6 + src/renderer/monaco-grammars/asciidoc.ts | 136 ++++++++++++++++++ src/renderer/monaco-grammars/groovy.ts | 172 +++++++++++++++++++++++ src/renderer/monaco-setup.ts | 39 ++++- src/renderer/syntax.ts | 13 ++ 5 files changed, 363 insertions(+), 3 deletions(-) create mode 100644 src/renderer/monaco-grammars/asciidoc.ts create mode 100644 src/renderer/monaco-grammars/groovy.ts diff --git a/src/renderer/highlight-common.d.ts b/src/renderer/highlight-common.d.ts index 6ac4bfd3..199a0513 100644 --- a/src/renderer/highlight-common.d.ts +++ b/src/renderer/highlight-common.d.ts @@ -3,3 +3,9 @@ declare module 'highlight.js/lib/common' { const hljs: HLJSApi export default hljs } + +declare module 'highlight.js/lib/languages/*' { + import type { LanguageFn } from 'highlight.js' + const lang: LanguageFn + export default lang +} diff --git a/src/renderer/monaco-grammars/asciidoc.ts b/src/renderer/monaco-grammars/asciidoc.ts new file mode 100644 index 00000000..5c888bd7 --- /dev/null +++ b/src/renderer/monaco-grammars/asciidoc.ts @@ -0,0 +1,136 @@ +import * as monaco from 'monaco-editor' + +export const conf: monaco.languages.LanguageConfiguration = { + comments: { lineComment: '//', blockComment: ['////', '////'] }, + brackets: [ + ['{', '}'], + ['[', ']'], + ['(', ')'], + ], + autoClosingPairs: [ + { open: '{', close: '}' }, + { open: '[', close: ']' }, + { open: '(', close: ')' }, + { open: '"', close: '"' }, + { open: "'", close: "'" }, + { open: '`', close: '`' }, + ], + surroundingPairs: [ + { open: '*', close: '*' }, + { open: '_', close: '_' }, + { open: '`', close: '`' }, + { open: '[', close: ']' }, + ], +} + +export const language: monaco.languages.IMonarchLanguage = { + defaultToken: '', + tokenPostfix: '.asciidoc', + admonitions: ['NOTE', 'TIP', 'WARNING', 'CAUTION', 'IMPORTANT'], + tokenizer: { + root: [ + // Block comment delimiter (4+ slashes on their own line) + [/^\/{4,}\s*$/, 'comment', '@blockComment'], + // Line comment + [/^\/\/.*$/, 'comment'], + + // Headers: leading = signs followed by space + [/^={1,6}\s+.*$/, 'keyword'], + // Setext-style header underline (= or - line on its own) + [/^={3,}\s*$/, 'keyword'], + [/^-{3,}\s*$/, 'keyword'], + + // Source block delimiter ---- (and start source block state) + [/^-{4,}\s*$/, { token: 'delimiter', next: '@sourceBlock' }], + // Literal block delimiter .... + [/^\.{4,}\s*$/, { token: 'delimiter', next: '@literalBlock' }], + // Other block delimiters (example/sidebar/quote/passthrough) + [/^={4,}\s*$/, 'delimiter'], + [/^\*{4,}\s*$/, 'delimiter'], + [/^_{4,}\s*$/, 'delimiter'], + [/^\+{4,}\s*$/, 'delimiter'], + [/^\|={3,}\s*$/, 'delimiter'], + + // Block attribute lines: [source,java], [NOTE], [verse, author], etc. + [/^\[.*\]\s*$/, 'attribute.name'], + + // Block titles: .Title + [/^\.[^.\s].*$/, 'string'], + + // Document attribute definition: :name: value or :!name: + [/^:[!a-zA-Z0-9_-]+:.*$/, 'variable'], + + // Admonition paragraph: NOTE: WARNING: etc. + [/^(NOTE|TIP|WARNING|CAUTION|IMPORTANT):\s/, 'keyword'], + + // List markers + [/^\s*\*+\s/, 'keyword'], + [/^\s*-\s/, 'keyword'], + [/^\s*\.+\s/, 'keyword'], + [/^\s*\d+\.\s/, 'keyword'], + // Description list: term:: + [/^\s*[^\s].*::\s*$/, 'type'], + + // Table cell prefix in tables (don't enter table state — just colorize) + [/^\|/, 'delimiter'], + + // Inline rules fall through to common handling + { include: '@inline' }, + ], + + inline: [ + // Attribute reference {name} + [/\{[a-zA-Z_][\w-]*\}/, 'variable'], + + // Inline macros: image::path[], video::, include::, link:, xref:, kbd:, btn:, menu:, footnote: + [/(image|video|audio|include|link|xref|kbd|btn|menu|footnote|footnoteref|icon|mailto|http|https|ftp|irc)(::?)([^[\s]*)(\[)/, + ['keyword', 'delimiter', 'string.link', { token: 'delimiter.bracket', next: '@macroArgs' }]], + + // Cross reference <> or <> + [/<<[^>]+>>/, 'string.link'], + + // URLs (bare) + [/\bhttps?:\/\/[^\s\[\]]+/, 'string.link'], + + // Inline monospace `...` + [/`[^`\n]+`/, 'string'], + + // Inline bold *...* (constrained — bounded by non-word chars on the outside) + [/(^|\s|[(\[{])(\*[^*\n]+\*)/, ['', 'strong']], + // Constrained bold **...** + [/\*\*[^*\n]+\*\*/, 'strong'], + + // Inline italic _..._ + [/(^|\s|[(\[{])(_[^_\n]+_)/, ['', 'emphasis']], + [/__[^_\n]+__/, 'emphasis'], + + // Superscript / subscript / highlight + [/\^[^\^\s\n]+\^/, 'string'], + [/~[^~\s\n]+~/, 'string'], + [/#[^#\s\n]+#/, 'string'], + + // Escaped characters + [/\\./, ''], + ], + + sourceBlock: [ + [/^-{4,}\s*$/, { token: 'delimiter', next: '@pop' }], + [/.*$/, 'string'], + ], + + literalBlock: [ + [/^\.{4,}\s*$/, { token: 'delimiter', next: '@pop' }], + [/.*$/, 'string'], + ], + + blockComment: [ + [/^\/{4,}\s*$/, { token: 'comment', next: '@pop' }], + [/.*$/, 'comment'], + ], + + macroArgs: [ + [/\]/, { token: 'delimiter.bracket', next: '@pop' }], + [/[^\]]+/, ''], + ], + }, +} diff --git a/src/renderer/monaco-grammars/groovy.ts b/src/renderer/monaco-grammars/groovy.ts new file mode 100644 index 00000000..930862d5 --- /dev/null +++ b/src/renderer/monaco-grammars/groovy.ts @@ -0,0 +1,172 @@ +import * as monaco from 'monaco-editor' + +export const conf: monaco.languages.LanguageConfiguration = { + comments: { lineComment: '//', blockComment: ['/*', '*/'] }, + brackets: [ + ['{', '}'], + ['[', ']'], + ['(', ')'], + ], + autoClosingPairs: [ + { open: '{', close: '}' }, + { open: '[', close: ']' }, + { open: '(', close: ')' }, + { open: '"', close: '"', notIn: ['string'] }, + { open: "'", close: "'", notIn: ['string'] }, + ], + surroundingPairs: [ + { open: '{', close: '}' }, + { open: '[', close: ']' }, + { open: '(', close: ')' }, + { open: '"', close: '"' }, + { open: "'", close: "'" }, + ], +} + +const GROOVY_KEYWORDS = [ + 'abstract', 'as', 'assert', 'break', 'case', 'catch', 'class', 'const', + 'continue', 'def', 'default', 'do', 'else', 'enum', 'extends', 'final', + 'finally', 'for', 'goto', 'if', 'implements', 'import', 'in', 'instanceof', + 'interface', 'native', 'new', 'package', 'private', 'protected', 'public', + 'return', 'static', 'strictfp', 'super', 'switch', 'synchronized', 'this', + 'threadsafe', 'throw', 'throws', 'trait', 'transient', 'try', 'void', + 'volatile', 'while', +] + +const GROOVY_TYPE_KEYWORDS = [ + 'boolean', 'byte', 'char', 'double', 'float', 'int', 'long', 'short', + 'String', 'Object', 'Integer', 'Long', 'Float', 'Double', 'Boolean', + 'Character', 'Number', 'BigInteger', 'BigDecimal', 'List', 'Map', 'Set', + 'Collection', 'Iterable', 'Closure', 'Class', +] + +const GROOVY_CONSTANTS = ['true', 'false', 'null'] + +interface GroovyOptions { + tokenPostfix: string + dslNamespaces?: string[] +} + +export function createGroovyLanguage(opts: GroovyOptions): monaco.languages.IMonarchLanguage { + return { + defaultToken: '', + tokenPostfix: opts.tokenPostfix, + keywords: GROOVY_KEYWORDS, + typeKeywords: GROOVY_TYPE_KEYWORDS, + constants: GROOVY_CONSTANTS, + dslNamespaces: opts.dslNamespaces ?? [], + operators: [ + '=', '>', '<', '!', '~', '?', ':', '==', '<=', '>=', '!=', '<=>', '===', + '!==', '&&', '||', '++', '--', '+', '-', '*', '/', '&', '|', '^', '%', + '<<', '>>', '>>>', '+=', '-=', '*=', '/=', '&=', '|=', '^=', '%=', '?:', + '?.', '*.', '.&', '..', '..<', '->', + ], + symbols: /[=>](?!@symbols)/, '@brackets'], + [/@symbols/, { + cases: { + '@operators': 'delimiter', + '@default': '', + }, + }], + + [/(@digits)[eE]([-+]?(@digits))?[fFdDgG]?/, 'number.float'], + [/(@digits)\.(@digits)([eE][-+]?(@digits))?[fFdDgG]?/, 'number.float'], + [/0[xX](@hexdigits)[Ll]?[gG]?/, 'number.hex'], + [/0[bB][01]+(_+[01]+)*[Ll]?[gG]?/, 'number.binary'], + [/(@digits)[fFdDgG]/, 'number.float'], + [/(@digits)[lLgGiI]?/, 'number'], + + [/[;,.]/, 'delimiter'], + + [/"""/, 'string', '@tripleString'], + [/'''/, 'string', '@tripleStringSingle'], + [/"/, 'string', '@string'], + [/'/, 'string', '@stringSingle'], + ], + + whitespace: [ + [/[ \t\r\n]+/, ''], + [/\/\*\*(?!\/)/, 'comment.doc', '@javadoc'], + [/\/\*/, 'comment', '@comment'], + [/\/\/.*$/, 'comment'], + [/#!.*$/, 'comment'], + ], + + comment: [ + [/[^/*]+/, 'comment'], + [/\*\//, 'comment', '@pop'], + [/[/*]/, 'comment'], + ], + + javadoc: [ + [/[^/*]+/, 'comment.doc'], + [/\*\//, 'comment.doc', '@pop'], + [/[/*]/, 'comment.doc'], + ], + + string: [ + [/[^\\"$]+/, 'string'], + [/@escapes/, 'string.escape'], + [/\\./, 'string.escape.invalid'], + [/\$[a-zA-Z_][\w]*/, 'identifier'], + [/\$\{/, { token: 'delimiter.bracket', next: '@interp' }], + [/\$/, 'string'], + [/"/, 'string', '@pop'], + ], + + stringSingle: [ + [/[^\\']+/, 'string'], + [/@escapes/, 'string.escape'], + [/\\./, 'string.escape.invalid'], + [/'/, 'string', '@pop'], + ], + + tripleString: [ + [/[^\\"$]+/, 'string'], + [/@escapes/, 'string.escape'], + [/\\./, 'string.escape.invalid'], + [/\$[a-zA-Z_][\w]*/, 'identifier'], + [/\$\{/, { token: 'delimiter.bracket', next: '@interp' }], + [/\$/, 'string'], + [/"""/, 'string', '@pop'], + [/"/, 'string'], + ], + + tripleStringSingle: [ + [/[^\\']+/, 'string'], + [/@escapes/, 'string.escape'], + [/\\./, 'string.escape.invalid'], + [/'''/, 'string', '@pop'], + [/'/, 'string'], + ], + + interp: [ + [/\}/, { token: 'delimiter.bracket', next: '@pop' }], + { include: 'root' }, + ], + }, + } +} + +export const language = createGroovyLanguage({ tokenPostfix: '.groovy' }) diff --git a/src/renderer/monaco-setup.ts b/src/renderer/monaco-setup.ts index 94db4b98..688ee415 100644 --- a/src/renderer/monaco-setup.ts +++ b/src/renderer/monaco-setup.ts @@ -12,6 +12,8 @@ import JsonWorkerUrl from 'monaco-editor/esm/vs/language/json/json.worker?worker import CssWorkerUrl from 'monaco-editor/esm/vs/language/css/css.worker?worker&url' import HtmlWorkerUrl from 'monaco-editor/esm/vs/language/html/html.worker?worker&url' import TsWorkerUrl from 'monaco-editor/esm/vs/language/typescript/ts.worker?worker&url' +import * as groovyLang from './monaco-grammars/groovy' +import * as asciidocLang from './monaco-grammars/asciidoc' const workerUrlByLabel: Record = { editor: EditorWorkerUrl, @@ -127,6 +129,30 @@ function configureTypescriptDefaults(): void { configureTypescriptDefaults() +function registerCustomLanguages(): void { + const langs: Array<{ + id: string + extensions?: string[] + filenamePatterns?: string[] + conf: monaco.languages.LanguageConfiguration + language: monaco.languages.IMonarchLanguage + }> = [ + { id: 'groovy', extensions: ['.groovy', '.gvy', '.gy', '.gsh', '.gradle'], ...groovyLang }, + { id: 'asciidoc', extensions: ['.adoc', '.asciidoc'], ...asciidocLang }, + ] + for (const l of langs) { + monaco.languages.register({ + id: l.id, + extensions: l.extensions, + filenamePatterns: l.filenamePatterns, + }) + monaco.languages.setLanguageConfiguration(l.id, l.conf) + monaco.languages.setMonarchTokensProvider(l.id, l.language) + } +} + +registerCustomLanguages() + // Pull current Tailwind theme tokens from the document and build a Monaco // theme that tracks them. Called once at boot and on theme changes. function readVar(name: string, fallback: string): string { @@ -182,13 +208,16 @@ export function defineHarnessTheme(): void { export function detectMonacoLanguage(filePath: string | undefined): string { if (!filePath) return 'plaintext' const name = filePath.split('/').pop() || '' + const lower = name.toLowerCase() + // Compound extensions checked before the single-ext fallback below. + if (lower.endsWith('.gradle.kts')) return 'kotlin' const ext = name.includes('.') ? name.split('.').pop()!.toLowerCase() : '' const byName: Record = { dockerfile: 'dockerfile', makefile: 'makefile', 'cmakelists.txt': 'cmake' } - if (byName[name.toLowerCase()]) return byName[name.toLowerCase()] + if (byName[lower]) return byName[lower] const byExt: Record = { ts: 'typescript', tsx: 'typescript', mts: 'typescript', cts: 'typescript', js: 'javascript', jsx: 'javascript', mjs: 'javascript', cjs: 'javascript', @@ -201,9 +230,13 @@ export function detectMonacoLanguage(filePath: string | undefined): string { sh: 'shell', bash: 'shell', zsh: 'shell', yml: 'yaml', yaml: 'yaml', toml: 'ini', ini: 'ini', sql: 'sql', xml: 'xml', svg: 'xml', - php: 'php', swift: 'swift', kt: 'kotlin', dart: 'dart', lua: 'lua', + php: 'php', swift: 'swift', kt: 'kotlin', kts: 'kotlin', dart: 'dart', lua: 'lua', graphql: 'graphql', gql: 'graphql', - dockerfile: 'dockerfile' + dockerfile: 'dockerfile', + groovy: 'groovy', gvy: 'groovy', gy: 'groovy', gsh: 'groovy', + gradle: 'groovy', + adoc: 'asciidoc', asciidoc: 'asciidoc', + properties: 'ini' } return byExt[ext] || 'plaintext' } diff --git a/src/renderer/syntax.ts b/src/renderer/syntax.ts index 5ab164c5..cc34d978 100644 --- a/src/renderer/syntax.ts +++ b/src/renderer/syntax.ts @@ -3,6 +3,13 @@ // of its own — the local module declaration in `highlight-common.d.ts` // fills that in. import hljs from 'highlight.js/lib/common' +import groovy from 'highlight.js/lib/languages/groovy' +import properties from 'highlight.js/lib/languages/properties' +import asciidoc from 'highlight.js/lib/languages/asciidoc' + +hljs.registerLanguage('groovy', groovy) +hljs.registerLanguage('properties', properties) +hljs.registerLanguage('asciidoc', asciidoc) const EXT_TO_LANG: Record = { ts: 'typescript', @@ -20,6 +27,12 @@ const EXT_TO_LANG: Record = { rs: 'rust', java: 'java', kt: 'kotlin', + kts: 'kotlin', + groovy: 'groovy', + gradle: 'groovy', + properties: 'properties', + adoc: 'asciidoc', + asciidoc: 'asciidoc', swift: 'swift', c: 'c', h: 'c',