Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions src/renderer/highlight-common.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
136 changes: 136 additions & 0 deletions src/renderer/monaco-grammars/asciidoc.ts
Original file line number Diff line number Diff line change
@@ -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 <<id>> or <<id,text>>
[/<<[^>]+>>/, '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' }],
[/[^\]]+/, ''],
],
},
}
172 changes: 172 additions & 0 deletions src/renderer/monaco-grammars/groovy.ts
Original file line number Diff line number Diff line change
@@ -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: /[=><!~?:&|+\-*/^%.]+/,
escapes: /\\(?:[abfnrtv\\"'$]|x[0-9A-Fa-f]{1,4}|u[0-9A-Fa-f]{4})/,
digits: /\d+(_+\d+)*/,
hexdigits: /[0-9a-fA-F]+(_+[0-9a-fA-F]+)*/,
tokenizer: {
root: [
[/[a-zA-Z_$][\w$]*/, {
cases: {
'@keywords': 'keyword',
'@typeKeywords': 'type',
'@constants': 'constant',
'@dslNamespaces': 'type.identifier',
'@default': 'identifier',
},
}],

{ include: '@whitespace' },

[/@\s*[a-zA-Z_$][\w$]*/, 'annotation'],

[/[{}()[\]]/, '@brackets'],
[/[<>](?!@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' })
39 changes: 36 additions & 3 deletions src/renderer/monaco-setup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, string> = {
editor: EditorWorkerUrl,
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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<string, string> = {
dockerfile: 'dockerfile',
makefile: 'makefile',
'cmakelists.txt': 'cmake'
}
if (byName[name.toLowerCase()]) return byName[name.toLowerCase()]
if (byName[lower]) return byName[lower]
const byExt: Record<string, string> = {
ts: 'typescript', tsx: 'typescript', mts: 'typescript', cts: 'typescript',
js: 'javascript', jsx: 'javascript', mjs: 'javascript', cjs: 'javascript',
Expand All @@ -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'
}
Loading
Loading