diff --git a/vue3-src/app/components/ResourceEditor.vue b/vue3-src/app/components/ResourceEditor.vue
index 503d273e..b07b581f 100644
--- a/vue3-src/app/components/ResourceEditor.vue
+++ b/vue3-src/app/components/ResourceEditor.vue
@@ -59,41 +59,51 @@
-
-
+
+
+
+ mdi-undo
+
+
+ mdi-redo
+
+
+
+ mdi-arrow-up
+
+
+ mdi-arrow-down
+
+
+ mdi-arrow-left
+
+
+ mdi-arrow-right
+
+
+ mdi-keyboard-tab
+
+
+
+ mdi-magnify
+
+
+ mdi-format-indent-increase
+
+
+
diff --git a/vue3-src/app/pages/fhirpath.vue b/vue3-src/app/pages/fhirpath.vue
index 36f979c7..7b5ee807 100644
--- a/vue3-src/app/pages/fhirpath.vue
+++ b/vue3-src/app/pages/fhirpath.vue
@@ -589,6 +589,10 @@ const tabDetails = computed(() => [
tabSubName: "(test input)",
show: true,
enabled: true,
+ // Keep the Monaco-backed ResourceEditor mounted across single-pane tab
+ // switches; tearing it down and re-creating it on every switch is very
+ // expensive (workers + ResizeObserver + DOM) and can crash/hang the UI.
+ keepMounted: true,
},
{
iconName: "mdi-chart-box-multiple-outline",
@@ -623,6 +627,10 @@ const tabDetails = computed(() => [
tabName: "DEBUG",
show: true,
enabled: true,
+ // The DEBUG tab hosts a Monaco-backed ResourceEditor; keep it mounted
+ // across single-pane tab switches so Monaco isn't disposed/re-created
+ // on every switch (which can crash/hang the UI).
+ keepMounted: true,
},
])
diff --git a/vue3-src/app/pages/fml.vue b/vue3-src/app/pages/fml.vue
index 03c6a841..f4d045e3 100644
--- a/vue3-src/app/pages/fml.vue
+++ b/vue3-src/app/pages/fml.vue
@@ -224,6 +224,10 @@ const tabDetails = computed(() => [
tabName: "Map",
show: true,
enabled: true,
+ // Keep Monaco-backed ResourceEditor tabs mounted across single-pane tab
+ // switches; tearing down and recreating Monaco on every switch is very
+ // expensive (workers + ResizeObserver + DOM) and can crash/hang the UI.
+ keepMounted: true,
},
{
iconName: "mdi-clipboard-text-outline",
@@ -232,6 +236,7 @@ const tabDetails = computed(() => [
tabHeaderName: "Test Input Resource",
show: true,
enabled: true,
+ keepMounted: true,
},
{
iconName: "mdi-tree-outline",
@@ -240,6 +245,7 @@ const tabDetails = computed(() => [
title: "Logical Models Required\n(Structure Definitions)",
show: true,
enabled: true,
+ keepMounted: true,
},
{
iconName: "mdi-format-list-bulleted",
@@ -254,6 +260,7 @@ const tabDetails = computed(() => [
title: "Map Output",
show: true,
enabled: true,
+ keepMounted: true,
},
{
iconName: "mdi-alert-circle-outline",
@@ -275,6 +282,7 @@ const tabDetails = computed(() => [
tabName: "Debug",
show: showAdvancedSettings.value,
enabled: true,
+ keepMounted: true,
},
])
diff --git a/vue3-src/app/utils/monaco-fhirpath.ts b/vue3-src/app/utils/monaco-fhirpath.ts
new file mode 100644
index 00000000..9dbf190f
--- /dev/null
+++ b/vue3-src/app/utils/monaco-fhirpath.ts
@@ -0,0 +1,190 @@
+/**
+ * Monaco Monarch language definition for FHIRPath / FHIR Mapping Language (FML).
+ *
+ * Ported from `helpers/fhirpath_highlighter.ts` (Ace highlight rules) so that the
+ * vue3-src Monaco-based ResourceEditor highlights FML maps and FHIRPath expressions
+ * with the same token semantics as the legacy Ace editor.
+ *
+ * Language id: `fhirpath`
+ * Theme rules below are merged into the default Monaco "vs" theme via
+ * `monaco.editor.defineTheme` so the FHIRPath tokens render with sensible colours
+ * even though they are not part of Monaco's built-in token palette.
+ */
+import type * as monacoType from 'monaco-editor/esm/vs/editor/editor.api'
+
+let registered = false
+
+export function setupFhirPathLanguage(monaco: typeof monacoType): void {
+ if (registered) return
+ registered = true
+
+ monaco.languages.register({ id: 'fhirpath' })
+
+ monaco.languages.setLanguageConfiguration('fhirpath', {
+ 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: '`' }
+ ]
+ })
+
+ // Monarch tokenizer ported from helpers/fhirpath_highlighter.ts
+ monaco.languages.setMonarchTokensProvider('fhirpath', {
+ defaultToken: '',
+ tokenPostfix: '.fhirpath',
+
+ existence: ['empty', 'exists', 'all', 'allTrue', 'anyTrue', 'allFalse', 'anyFalse', 'subsetOf', 'supersetOf', 'count', 'distinct', 'isDistinct'],
+ filtering: ['where', 'select', 'repeat', 'ofType'],
+ subsetting: ['single', 'first', 'last', 'tail', 'skip', 'take', 'intersect', 'exclude'],
+ combining: ['union', 'combine'],
+ conversion: ['iif', 'toBoolean', 'convertsToBoolean', 'toInteger', 'convertsToInteger', 'toDate', 'convertsToDate', 'toDateTime', 'convertsToDateTime', 'toDecimal', 'convertsToDecimal', 'toQuantity', 'convertsToQuantity', 'toString', 'convertsToString', 'toTime', 'convertsToTime'],
+ strings: ['indexOf', 'substring', 'startsWith', 'endsWith', 'contains', 'upper', 'lower', 'replace', 'matches', 'replaceMatches', 'length', 'toChars', 'split', 'join', 'encode', 'decode'],
+ math: ['abs', 'ceiling', 'exp', 'floor', 'ln', 'log', 'power', 'round', 'sqrt', 'truncate'],
+ treenav: ['children', 'descendants'],
+ utility: ['trace', 'now', 'timeOfDay', 'today', 'not'],
+ types: ['as', 'is'],
+ keywords: ['as', 'is', 'in', 'contains', 'and', 'or', 'xor', 'implies'],
+ mathKeywords: ['div', 'mod'],
+ units: ['years', 'months', 'weeks', 'days', 'hours', 'minutes', 'seconds', 'milliseconds', 'year', 'month', 'week', 'day', 'hour', 'minute', 'second', 'millisecond'],
+
+ tokenizer: {
+ root: [
+ // Strings (single-quoted)
+ [/'/, { token: 'string.quote', next: '@fhirString' }],
+ // Backtick identifiers
+ [/`/, { token: 'identifier.quote', next: '@bstring' }],
+
+ // Numbers / quantities / temporals (order matters: most specific first)
+ [/0[xX][0-9a-fA-F]+\b/, 'number.hex'],
+ [/\b\d+(?:(?:\.\d*)?(?:[eE][+-]?\d+)?)?\b\s+'/, { token: 'number.quantity', next: '@fhirQString' }],
+ [/\b\d+(?:(?:\.\d*)?(?:[eE][+-]?\d+)?)?\b\s+(?:years|months|weeks|days|hours|minutes|seconds|milliseconds|year|month|week|day|hour|minute|second|millisecond)\b/, 'number.quantity'],
+ [/\b\d+L\b/, 'number'],
+ [/\b\d+(?:(?:\.\d*)?(?:[eE][+-]?\d+)?)?\b/, 'number.float'],
+ // Use a character class for the leading `@` so Monaco's Monarch tokenizer
+ // does not treat `@T`/`@[`... as an attribute reference (which produces
+ // "language definition does not contain attribute 'T'" errors).
+ [/[@]T[0-9][0-9]:[0-9][0-9](?::[0-9][0-9])?(?:\.[0-9]+)?/, 'string.time'],
+ [/[@][0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}(?::[0-9][0-9])?(?:\.[0-9]+)?(?:Z|[+\-][0-9]{2}:[0-9]{2})/, 'string.datetime'],
+ [/[@][0-9]{4}(?:-[0-9]{2}(?:-[0-9]{2})?)?/, 'string.date'],
+
+ // Boolean literals
+ [/\b(?:true|false)\b/, 'constant.language'],
+
+ // Comments
+ [/\/\/.*$/, 'comment'],
+ [/\/\*/, { token: 'comment', next: '@blockComment' }],
+
+ // Function names followed by `(`
+ [/\b(empty|exists|all|allTrue|anyTrue|allFalse|anyFalse|subsetOf|supersetOf|count|distinct|isDistinct)\b(?=\s*\()/, 'support.function.existence'],
+ [/\b(where|select|repeat|ofType)\b(?=\s*\()/, 'support.function.filtering'],
+ [/\b(single|first|last|tail|skip|take|intersect|exclude)\b(?=\s*\()/, 'support.function.subsetting'],
+ [/\b(union|combine)\b(?=\s*\()/, 'support.function.combining'],
+ [/\b(iif|toBoolean|convertsToBoolean|toInteger|convertsToInteger|toDate|convertsToDate|toDateTime|convertsToDateTime|toDecimal|convertsToDecimal|toQuantity|convertsToQuantity|toString|convertsToString|toTime|convertsToTime)\b(?=\s*\()/, 'support.function.conversion'],
+ [/\b(indexOf|substring|startsWith|endsWith|contains|upper|lower|replace|matches|replaceMatches|length|toChars|split|join|encode|decode)\b(?=\s*\()/, 'support.function.strings'],
+ [/\b(abs|ceiling|exp|floor|ln|log|power|round|sqrt|truncate)\b(?=\s*\()/, 'support.function.math'],
+ [/\b(children|descendants)\b(?=\s*\()/, 'support.function.treenav'],
+ [/\b(trace|now|timeOfDay|today|not)\b(?=\s*\()/, 'support.function.utility'],
+ [/\b(as|is)\b(?=\s*\()/, 'support.function.types'],
+ [/\baggregate\b/, 'support.function.aggregates'],
+
+ // Keywords
+ [/\b(?:as|is|in|contains|and|or|xor|implies)\b/, 'keyword'],
+ [/\$this\b/, 'variable.language.this'],
+ [/\$index\b/, 'variable.language.index'],
+ [/\$total\b/, 'variable.language.total'],
+ [/\b(?:div|mod)\b/, 'keyword.operator.math'],
+
+ // External constants (%foo or %`escaped name`)
+ [/%`/, { token: 'variable.parameter', next: '@fhirVariable' }],
+ [/%\w+/, 'variable.parameter'],
+
+ // Operators / punctuation
+ [/[!=<>~]+|[+\-*|]/, 'operator'],
+ [/[,]/, 'delimiter'],
+ [/\./, 'delimiter.dot'],
+ [/[(){}[\]]/, '@brackets'],
+
+ // Identifiers
+ [/[A-Za-z_][A-Za-z0-9_]*/, 'identifier'],
+
+ // Whitespace
+ [/\s+/, 'white']
+ ],
+
+ fhirString: [
+ [/\\(?:x[0-9a-fA-F]{2}|u[0-9a-fA-F]{4}|["\\/fnrt'])/, 'string.escape'],
+ [/'/, { token: 'string.quote', next: '@pop' }],
+ [/./, 'string']
+ ],
+
+ fhirQString: [
+ [/\\(?:x[0-9a-fA-F]{2}|u[0-9a-fA-F]{4}|["\\/fnrt'])/, 'string.escape'],
+ [/'/, { token: 'number.quantity', next: '@pop' }],
+ [/./, 'number.quantity']
+ ],
+
+ bstring: [
+ [/\\(?:x[0-9a-fA-F]{2}|u[0-9a-fA-F]{4}|["\\/fnrt`])/, 'string.escape'],
+ [/`/, { token: 'identifier.quote', next: '@pop' }],
+ [/./, 'identifier']
+ ],
+
+ fhirVariable: [
+ [/\\(?:x[0-9a-fA-F]{2}|u[0-9a-fA-F]{4}|["\\/fnrt`])/, 'string.escape'],
+ [/`/, { token: 'variable.parameter', next: '@pop' }],
+ [/./, 'variable.parameter']
+ ],
+
+ blockComment: [
+ [/\*\//, { token: 'comment', next: '@pop' }],
+ [/./, 'comment']
+ ]
+ }
+ } as monacoType.languages.IMonarchLanguage)
+
+ // Provide colours for the custom token classes (Monaco "vs" theme has no defaults
+ // for `support.function.*`, `number.quantity`, etc.).
+ monaco.editor.defineTheme('fhirpath-lab', {
+ base: 'vs',
+ inherit: true,
+ rules: [
+ { token: 'support.function.existence', foreground: '795E26', fontStyle: 'bold' },
+ { token: 'support.function.filtering', foreground: '795E26', fontStyle: 'bold' },
+ { token: 'support.function.subsetting', foreground: '795E26' },
+ { token: 'support.function.combining', foreground: '795E26' },
+ { token: 'support.function.conversion', foreground: '267F99' },
+ { token: 'support.function.strings', foreground: '795E26' },
+ { token: 'support.function.math', foreground: '795E26' },
+ { token: 'support.function.treenav', foreground: '795E26' },
+ { token: 'support.function.utility', foreground: '795E26' },
+ { token: 'support.function.types', foreground: 'AF00DB' },
+ { token: 'support.function.aggregates', foreground: '795E26' },
+ { token: 'variable.language.this', foreground: '0070C1', fontStyle: 'italic' },
+ { token: 'variable.language.index', foreground: '0070C1', fontStyle: 'italic' },
+ { token: 'variable.language.total', foreground: '0070C1', fontStyle: 'italic' },
+ { token: 'variable.parameter', foreground: '001080' },
+ { token: 'keyword.operator.math', foreground: 'AF00DB' },
+ { token: 'number.quantity', foreground: '098658' },
+ { token: 'string.time', foreground: '098658' },
+ { token: 'string.date', foreground: '098658' },
+ { token: 'string.datetime', foreground: '098658' },
+ { token: 'delimiter.dot', foreground: '000000' }
+ ],
+ colors: {}
+ })
+}
diff --git a/vue3-src/package-lock.json b/vue3-src/package-lock.json
index 1763b875..f5bd3be6 100644
--- a/vue3-src/package-lock.json
+++ b/vue3-src/package-lock.json
@@ -12,7 +12,6 @@
"@nuxt/fonts": "^0.14.0",
"@types/fhir": "^0.0.41",
"@types/lz-string": "^1.5.0",
- "ace-builds": "^1.43.6",
"antlr4": "^4.13.2",
"axios": "^1.16.0",
"fhir-extension-helpers": "^0.3.0",
@@ -21,10 +20,10 @@
"luxon": "^3.7.2",
"lz-string": "^1.5.0",
"marked": "^14.1.4",
+ "monaco-editor": "^0.52.2",
"nuxt": "^4.4.2",
"vue": "^3.5.32",
"vue-router": "^5.0.4",
- "vue3-ace-editor": "^2.2.4",
"vuetify": "^4.0.5",
"vuetify-nuxt-module": "^0.19.5",
"xml-formatter": "^3.7.0"
@@ -4077,12 +4076,6 @@
"node": ">=6.5"
}
},
- "node_modules/ace-builds": {
- "version": "1.43.6",
- "resolved": "https://registry.npmjs.org/ace-builds/-/ace-builds-1.43.6.tgz",
- "integrity": "sha512-L1ddibQ7F3vyXR2k2fg+I8TQTPWVA6CKeDQr/h2+8CeyTp3W6EQL8xNFZRTztuP8xNOAqL3IYPqdzs31GCjDvg==",
- "license": "BSD-3-Clause"
- },
"node_modules/acorn": {
"version": "8.16.0",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz",
@@ -7995,6 +7988,12 @@
"integrity": "sha512-aF7yRQr/Q0O2/4pIXm6PZ5G+jAd7QS4Yu8m+WEeEHGnbo+7mE36CbLSDQiXYV8bVL3NfmdeqPJct0tUlnjVSnA==",
"license": "MIT"
},
+ "node_modules/monaco-editor": {
+ "version": "0.52.2",
+ "resolved": "https://registry.npmjs.org/monaco-editor/-/monaco-editor-0.52.2.tgz",
+ "integrity": "sha512-GEQWEZmfkOGLdd3XK8ryrfWz3AIP8YymVXiPHEdewrUq7mh0qrKrfHLNCXcbB6sTnMLnOZ3ztSiKcciFUkIJwQ==",
+ "license": "MIT"
+ },
"node_modules/mrmime": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.1.tgz",
@@ -9339,12 +9338,6 @@
"regexp-tree": "bin/regexp-tree"
}
},
- "node_modules/resize-observer-polyfill": {
- "version": "1.5.1",
- "resolved": "https://registry.npmjs.org/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz",
- "integrity": "sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg==",
- "license": "MIT"
- },
"node_modules/resolve": {
"version": "1.22.12",
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.12.tgz",
@@ -11245,19 +11238,6 @@
}
}
},
- "node_modules/vue3-ace-editor": {
- "version": "2.2.4",
- "resolved": "https://registry.npmjs.org/vue3-ace-editor/-/vue3-ace-editor-2.2.4.tgz",
- "integrity": "sha512-FZkEyfpbH068BwjhMyNROxfEI8135Sc+x8ouxkMdCNkuj/Tuw83VP/gStFQqZHqljyX9/VfMTCdTqtOnJZGN8g==",
- "license": "MIT",
- "dependencies": {
- "resize-observer-polyfill": "^1.5.1"
- },
- "peerDependencies": {
- "ace-builds": "*",
- "vue": "^3"
- }
- },
"node_modules/vuetify": {
"version": "4.0.5",
"resolved": "https://registry.npmjs.org/vuetify/-/vuetify-4.0.5.tgz",
diff --git a/vue3-src/package.json b/vue3-src/package.json
index 039459e3..9fed661b 100644
--- a/vue3-src/package.json
+++ b/vue3-src/package.json
@@ -15,7 +15,6 @@
"@nuxt/fonts": "^0.14.0",
"@types/fhir": "^0.0.41",
"@types/lz-string": "^1.5.0",
- "ace-builds": "^1.43.6",
"antlr4": "^4.13.2",
"axios": "^1.16.0",
"fhir-extension-helpers": "^0.3.0",
@@ -24,10 +23,10 @@
"luxon": "^3.7.2",
"lz-string": "^1.5.0",
"marked": "^14.1.4",
+ "monaco-editor": "^0.52.2",
"nuxt": "^4.4.2",
"vue": "^3.5.32",
"vue-router": "^5.0.4",
- "vue3-ace-editor": "^2.2.4",
"vuetify": "^4.0.5",
"vuetify-nuxt-module": "^0.19.5",
"xml-formatter": "^3.7.0"