Skip to content

Commit 61aa1e6

Browse files
Merge pull request #291 from wttech/groovy-highlight-regex-fix
Groovy highlight regex fix + types
2 parents 80c02d9 + c659f57 commit 61aa1e6

File tree

4 files changed

+71
-33
lines changed

4 files changed

+71
-33
lines changed

ui.frontend/src/components/CodeEditor.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import { debounce } from '../utils/debounce';
99
import { modelStorage } from '../utils/modelStorage';
1010
import { GROOVY_LANGUAGE_ID, registerGroovyLanguage } from '../utils/monaco/groovy';
1111
import { LOG_LANGUAGE_ID, LOG_THEME_ID, registerLogLanguage } from '../utils/monaco/log';
12-
import { DEFAULT_THEME_ID } from '../utils/monaco/theme';
12+
import { DEFAULT_THEME_ID, registerTheme } from '../utils/monaco/theme';
1313

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

50+
registerTheme(monacoRef);
51+
5052
if (language === GROOVY_LANGUAGE_ID) {
5153
registerGroovyLanguage(monacoRef);
5254
} else if (language === LOG_LANGUAGE_ID) {

ui.frontend/src/utils/monaco/groovy/syntax.ts

Lines changed: 53 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -17,65 +17,86 @@ export function registerSyntax(instance: Monaco) {
1717
const groovyKeywords = ['def', 'as', 'in', 'trait', 'with'];
1818
groovyLanguage.keywords = [...(groovyLanguage.keywords || []), ...groovyKeywords];
1919

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

2823
// Extend the tokenizer with Groovy-specific features
2924
groovyLanguage.tokenizer = {
3025
...groovyLanguage.tokenizer,
3126
root: [
32-
...javaRootRules,
27+
// Groovy-specific rules MUST come before Java rules
28+
29+
// Import statements - color the whole qualified name
30+
[/(import)(\s+)([\w.]+)/, ['keyword', 'white', 'type.identifier']],
3331

34-
// multiline strings
32+
// Slashy strings (regex): /pattern/
33+
// Only match when followed by regex-indicating chars, not division or comments
34+
[/\/(?=[[(^.\\a-zA-Z])/, { token: 'regexp', next: '@slashy_string' }],
35+
36+
// Triple-quoted strings (must be before double quote)
3537
[/"""/, { token: 'string.quote', bracket: '@open', next: '@string_multiline' }],
3638

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

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

43-
// Groovy closures
44-
[/\{/, { token: 'delimiter.curly', next: '@closure' }],
45+
// Constants (UPPER_SNAKE_CASE) - before types to take precedence
46+
[/[A-Z][A-Z0-9_]+\b/, 'constant'],
47+
48+
// Type names (PascalCase identifiers)
49+
[/[A-Z][\w$]*/, 'type.identifier'],
50+
51+
// Java rules come after
52+
...javaRootRules,
53+
],
54+
55+
// Slashy string state (regex literal)
56+
slashy_string: [
57+
[/\\./, 'regexp.escape'], // Escaped chars (including \/)
58+
[/\//, { token: 'regexp', next: '@pop' }], // Closing /
59+
[/[^\\/\r\n]+/, 'regexp'], // Content
60+
[/\r?\n/, { token: '', next: '@pop' }], // Newline = exit (error recovery)
4561
],
4662

63+
// Double-quoted string with GString interpolation
4764
string_double: [
48-
[/\\\$/, 'string.escape'],
49-
[/\$\{/, { token: 'identifier', bracket: '@open', next: '@gstring_expression' }],
50-
[/\\./, 'string.escape'],
51-
[/[^\\"$]+/, 'string'],
52-
[/"/, { token: 'string.quote', bracket: '@close', next: '@pop' }],
53-
[/[$]/, 'string'],
65+
[/\\\$/, 'string.escape'], // Escaped $
66+
[/\$\{/, { token: 'identifier', bracket: '@open', next: '@gstring_expression' }], // ${...}
67+
[/\\./, 'string.escape'], // Escape sequences
68+
[/[^\\"$]+/, 'string'], // Regular content
69+
[/"/, { token: 'string.quote', bracket: '@close', next: '@pop' }], // Closing "
70+
[/[$]/, 'string'], // Lone $ at end
5471
],
5572

73+
// Single-quoted string (no interpolation)
5674
string_single: [
57-
[/[^\\']+/, 'string'],
58-
[/\\./, 'string.escape'],
59-
[/'/, { token: 'string.quote', bracket: '@close', next: '@pop' }],
75+
[/[^\\']+/, 'string'], // Regular content
76+
[/\\./, 'string.escape'], // Escape sequences
77+
[/'/, { token: 'string.quote', bracket: '@close', next: '@pop' }], // Closing '
6078
],
6179

80+
// Triple-quoted multiline string with GString interpolation
6281
string_multiline: [
63-
[/\\\$/, 'string.escape'],
64-
[/\$\{/, { token: 'identifier', bracket: '@open', next: '@gstring_expression_multiline' }],
65-
[/\\./, 'string.escape'],
66-
[/[^\\"$]+/, 'string'],
67-
[/"""/, { token: 'string.quote', bracket: '@close', next: '@pop' }],
68-
[/"/, 'string'],
69-
[/[$]/, 'string'],
82+
[/\\\$/, 'string.escape'], // Escaped $
83+
[/\$\{/, { token: 'identifier', bracket: '@open', next: '@gstring_expression_multiline' }], // ${...}
84+
[/\\./, 'string.escape'], // Escape sequences
85+
[/[^\\"$]+/, 'string'], // Regular content
86+
[/"""/, { token: 'string.quote', bracket: '@close', next: '@pop' }], // Closing """
87+
[/"/, 'string'], // Single " inside multiline
88+
[/[$]/, 'string'], // Lone $
7089
],
7190

91+
// GString expression ${...}
7292
gstring_expression: [
7393
[/'/, { token: 'string.quote', bracket: '@open', next: '@string_in_gstring_single' }],
7494
[/\{/, { token: 'delimiter.curly', bracket: '@open', next: '@closure' }],
7595
[/\}/, { token: 'identifier', bracket: '@close', next: '@pop' }],
7696
[/[^{}'"]+/, 'identifier'],
7797
],
7898

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

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

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

122+
// Groovy closure { ... } - simple version, relies on root rules for nested content
99123
closure: [
100124
[/[^{}]+/, ''],
101125
[/\{/, 'delimiter.curly', '@push'],

ui.frontend/src/utils/monaco/log.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { Monaco } from '@monaco-editor/react';
2-
import { DEFAULT_THEME_ID } from './theme';
2+
import { BASE_THEME_ID } from './theme';
33

44
export const LOG_LANGUAGE_ID = 'acmLog';
55
export const LOG_THEME_ID = 'acmLog';
@@ -26,7 +26,7 @@ export function registerLogLanguage(instance: Monaco) {
2626
});
2727

2828
instance.editor.defineTheme(LOG_THEME_ID, {
29-
base: DEFAULT_THEME_ID,
29+
base: BASE_THEME_ID,
3030
inherit: true,
3131
rules: [
3232
{ token: 'log-error', foreground: 'f14c4c', fontStyle: 'bold' },
Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,13 @@
1-
export const DEFAULT_THEME_ID = 'vs-dark';
1+
import { Monaco } from '@monaco-editor/react';
2+
3+
export const BASE_THEME_ID = 'vs-dark';
4+
export const DEFAULT_THEME_ID = 'acm-dark';
5+
6+
export function registerTheme(instance: Monaco) {
7+
instance.editor.defineTheme(DEFAULT_THEME_ID, {
8+
base: BASE_THEME_ID,
9+
inherit: true,
10+
rules: [],
11+
colors: {},
12+
});
13+
}

0 commit comments

Comments
 (0)