Skip to content
Merged
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
4 changes: 3 additions & 1 deletion ui.frontend/src/components/CodeEditor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { debounce } from '../utils/debounce';
import { modelStorage } from '../utils/modelStorage';
import { GROOVY_LANGUAGE_ID, registerGroovyLanguage } from '../utils/monaco/groovy';
import { LOG_LANGUAGE_ID, LOG_THEME_ID, registerLogLanguage } from '../utils/monaco/log';
import { DEFAULT_THEME_ID } from '../utils/monaco/theme';
import { DEFAULT_THEME_ID, registerTheme } from '../utils/monaco/theme';

type CodeEditorProps<C extends ColorVersion> = editor.IStandaloneEditorConstructionOptions & {
id: string;
Expand Down Expand Up @@ -47,6 +47,8 @@ const CodeEditor = <C extends ColorVersion>({ containerProps, syntaxError, onCha
return;
}

registerTheme(monacoRef);

if (language === GROOVY_LANGUAGE_ID) {
registerGroovyLanguage(monacoRef);
} else if (language === LOG_LANGUAGE_ID) {
Expand Down
82 changes: 53 additions & 29 deletions ui.frontend/src/utils/monaco/groovy/syntax.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,65 +17,86 @@ export function registerSyntax(instance: Monaco) {
const groovyKeywords = ['def', 'as', 'in', 'trait', 'with'];
groovyLanguage.keywords = [...(groovyLanguage.keywords || []), ...groovyKeywords];

const javaRootRules = [...(groovyLanguage.tokenizer.root || [])].filter((rule) => {
// Removes Java's single quote interpretation from tokenizer
if (Array.isArray(rule) && rule[0] instanceof RegExp && typeof rule[1] === 'string') {
return !rule[1].includes('string');
}
return true;
});
// Copy Java root rules as base
const javaRootRules = [...(groovyLanguage.tokenizer.root || [])];

// Extend the tokenizer with Groovy-specific features
groovyLanguage.tokenizer = {
...groovyLanguage.tokenizer,
root: [
...javaRootRules,
// Groovy-specific rules MUST come before Java rules

// Import statements - color the whole qualified name
[/(import)(\s+)([\w.]+)/, ['keyword', 'white', 'type.identifier']],

// multiline strings
// Slashy strings (regex): /pattern/
// Only match when followed by regex-indicating chars, not division or comments
[/\/(?=[[(^.\\a-zA-Z])/, { token: 'regexp', next: '@slashy_string' }],

// Triple-quoted strings (must be before double quote)
[/"""/, { token: 'string.quote', bracket: '@open', next: '@string_multiline' }],

// double quoted strings
// Double-quoted strings with GString interpolation
[/"/, { token: 'string.quote', bracket: '@open', next: '@string_double' }],

// single quoted strings
// Single-quoted strings (Groovy treats these as strings, not char literals)
[/'/, { token: 'string.quote', bracket: '@open', next: '@string_single' }],

// Groovy closures
[/\{/, { token: 'delimiter.curly', next: '@closure' }],
// Constants (UPPER_SNAKE_CASE) - before types to take precedence
[/[A-Z][A-Z0-9_]+\b/, 'constant'],

// Type names (PascalCase identifiers)
[/[A-Z][\w$]*/, 'type.identifier'],

// Java rules come after
...javaRootRules,
],

// Slashy string state (regex literal)
slashy_string: [
[/\\./, 'regexp.escape'], // Escaped chars (including \/)
[/\//, { token: 'regexp', next: '@pop' }], // Closing /
[/[^\\/\r\n]+/, 'regexp'], // Content
[/\r?\n/, { token: '', next: '@pop' }], // Newline = exit (error recovery)
],

// Double-quoted string with GString interpolation
string_double: [
[/\\\$/, 'string.escape'],
[/\$\{/, { token: 'identifier', bracket: '@open', next: '@gstring_expression' }],
[/\\./, 'string.escape'],
[/[^\\"$]+/, 'string'],
[/"/, { token: 'string.quote', bracket: '@close', next: '@pop' }],
[/[$]/, 'string'],
[/\\\$/, 'string.escape'], // Escaped $
[/\$\{/, { token: 'identifier', bracket: '@open', next: '@gstring_expression' }], // ${...}
[/\\./, 'string.escape'], // Escape sequences
[/[^\\"$]+/, 'string'], // Regular content
[/"/, { token: 'string.quote', bracket: '@close', next: '@pop' }], // Closing "
[/[$]/, 'string'], // Lone $ at end
],

// Single-quoted string (no interpolation)
string_single: [
[/[^\\']+/, 'string'],
[/\\./, 'string.escape'],
[/'/, { token: 'string.quote', bracket: '@close', next: '@pop' }],
[/[^\\']+/, 'string'], // Regular content
[/\\./, 'string.escape'], // Escape sequences
[/'/, { token: 'string.quote', bracket: '@close', next: '@pop' }], // Closing '
],

// Triple-quoted multiline string with GString interpolation
string_multiline: [
[/\\\$/, 'string.escape'],
[/\$\{/, { token: 'identifier', bracket: '@open', next: '@gstring_expression_multiline' }],
[/\\./, 'string.escape'],
[/[^\\"$]+/, 'string'],
[/"""/, { token: 'string.quote', bracket: '@close', next: '@pop' }],
[/"/, 'string'],
[/[$]/, 'string'],
[/\\\$/, 'string.escape'], // Escaped $
[/\$\{/, { token: 'identifier', bracket: '@open', next: '@gstring_expression_multiline' }], // ${...}
[/\\./, 'string.escape'], // Escape sequences
[/[^\\"$]+/, 'string'], // Regular content
[/"""/, { token: 'string.quote', bracket: '@close', next: '@pop' }], // Closing """
[/"/, 'string'], // Single " inside multiline
[/[$]/, 'string'], // Lone $
],

// GString expression ${...}
gstring_expression: [
[/'/, { token: 'string.quote', bracket: '@open', next: '@string_in_gstring_single' }],
[/\{/, { token: 'delimiter.curly', bracket: '@open', next: '@closure' }],
[/\}/, { token: 'identifier', bracket: '@close', next: '@pop' }],
[/[^{}'"]+/, 'identifier'],
],

// GString expression for multiline strings
gstring_expression_multiline: [
[/'/, { token: 'string.quote', bracket: '@open', next: '@string_in_gstring_single' }],
[/"/, { token: 'string.quote', bracket: '@open', next: '@string_in_gstring_double' }],
Expand All @@ -84,18 +105,21 @@ export function registerSyntax(instance: Monaco) {
[/[^{}'"]+/, 'identifier'],
],

// Single-quoted string inside GString expression
string_in_gstring_single: [
[/[^\\']+/, 'string'],
[/\\./, 'string.escape'],
[/'/, { token: 'string.quote', bracket: '@close', next: '@pop' }],
],

// Double-quoted string inside GString expression
string_in_gstring_double: [
[/[^\\"]+/, 'string'],
[/\\./, 'string.escape'],
[/"/, { token: 'string.quote', bracket: '@close', next: '@pop' }],
],

// Groovy closure { ... } - simple version, relies on root rules for nested content
closure: [
[/[^{}]+/, ''],
[/\{/, 'delimiter.curly', '@push'],
Expand Down
4 changes: 2 additions & 2 deletions ui.frontend/src/utils/monaco/log.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Monaco } from '@monaco-editor/react';
import { DEFAULT_THEME_ID } from './theme';
import { BASE_THEME_ID } from './theme';

export const LOG_LANGUAGE_ID = 'acmLog';
export const LOG_THEME_ID = 'acmLog';
Expand All @@ -26,7 +26,7 @@ export function registerLogLanguage(instance: Monaco) {
});

instance.editor.defineTheme(LOG_THEME_ID, {
base: DEFAULT_THEME_ID,
base: BASE_THEME_ID,
inherit: true,
rules: [
{ token: 'log-error', foreground: 'f14c4c', fontStyle: 'bold' },
Expand Down
14 changes: 13 additions & 1 deletion ui.frontend/src/utils/monaco/theme.ts
Original file line number Diff line number Diff line change
@@ -1 +1,13 @@
export const DEFAULT_THEME_ID = 'vs-dark';
import { Monaco } from '@monaco-editor/react';

export const BASE_THEME_ID = 'vs-dark';
export const DEFAULT_THEME_ID = 'acm-dark';
Comment thread
krystian-panek-vmltech marked this conversation as resolved.

export function registerTheme(instance: Monaco) {
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

future hook

instance.editor.defineTheme(DEFAULT_THEME_ID, {
base: BASE_THEME_ID,
inherit: true,
rules: [],
colors: {},
});
Comment thread
krystian-panek-vmltech marked this conversation as resolved.
}