@bpmn-io/feel-editor 1.4.0 → 1.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (4) hide show
  1. package/README.md +10 -1
  2. package/dist/index.es.js +555 -354
  3. package/dist/index.js +567 -366
  4. package/package.json +22 -22
package/dist/index.es.js CHANGED
@@ -1,18 +1,116 @@
1
- import { snippetCompletion, autocompletion as autocompletion$1, completeFromList, closeBrackets } from '@codemirror/autocomplete';
1
+ import { snippetCompletion, autocompletion, closeBrackets } from '@codemirror/autocomplete';
2
2
  import { defaultKeymap } from '@codemirror/commands';
3
- import { syntaxTree, LanguageSupport, syntaxHighlighting, HighlightStyle, bracketMatching, indentOnInput } from '@codemirror/language';
3
+ import { syntaxHighlighting, HighlightStyle, syntaxTree, bracketMatching, indentOnInput } from '@codemirror/language';
4
4
  import { linter as linter$1, setDiagnosticsEffect } from '@codemirror/lint';
5
5
  import { Facet, Compartment, EditorState } from '@codemirror/state';
6
- import { EditorView, tooltips, keymap } from '@codemirror/view';
7
- import { snippets, keywordCompletions, feelLanguage } from 'lang-feel';
8
- import { domify } from 'min-dom';
6
+ import { EditorView, tooltips, keymap, placeholder } from '@codemirror/view';
9
7
  import { cmFeelLinter } from '@bpmn-io/feel-lint';
10
- import { tags as tags$1 } from '@lezer/highlight';
8
+ import { tags } from '@lezer/highlight';
9
+ import { snippetCompletion as snippetCompletion$1, snippets, keywordCompletions, feel } from 'lang-feel';
10
+ import { domify } from 'min-dom';
11
+
12
+ var linter = [ linter$1(cmFeelLinter()) ];
13
+
14
+ const baseTheme = EditorView.theme({
15
+ '& .cm-content': {
16
+ padding: '0px',
17
+ },
18
+ '& .cm-line': {
19
+ padding: '0px',
20
+ },
21
+ '&.cm-editor.cm-focused': {
22
+ outline: 'none',
23
+ },
24
+ '& .cm-completionInfo': {
25
+ whiteSpace: 'pre-wrap',
26
+ overflow: 'hidden',
27
+ textOverflow: 'ellipsis'
28
+ },
29
+
30
+ // Don't wrap whitespace for custom HTML
31
+ '& .cm-completionInfo > *': {
32
+ whiteSpace: 'normal'
33
+ },
34
+ '& .cm-completionInfo ul': {
35
+ margin: 0,
36
+ paddingLeft: '15px'
37
+ },
38
+ '& .cm-completionInfo pre': {
39
+ marginBottom: 0,
40
+ whiteSpace: 'pre-wrap'
41
+ },
42
+ '& .cm-completionInfo p': {
43
+ marginTop: 0,
44
+ },
45
+ '& .cm-completionInfo p:not(:last-of-type)': {
46
+ marginBottom: 0,
47
+ }
48
+ });
49
+
50
+ const highlightTheme = EditorView.baseTheme({
51
+ '& .variableName': {
52
+ color: '#10f'
53
+ },
54
+ '& .number': {
55
+ color: '#164'
56
+ },
57
+ '& .string': {
58
+ color: '#a11'
59
+ },
60
+ '& .bool': {
61
+ color: '#219'
62
+ },
63
+ '& .function': {
64
+ color: '#aa3731',
65
+ fontWeight: 'bold'
66
+ },
67
+ '& .control': {
68
+ color: '#708'
69
+ }
70
+ });
71
+
72
+ const syntaxClasses = syntaxHighlighting(
73
+ HighlightStyle.define([
74
+ { tag: tags.variableName, class: 'variableName' },
75
+ { tag: tags.name, class: 'variableName' },
76
+ { tag: tags.number, class: 'number' },
77
+ { tag: tags.string, class: 'string' },
78
+ { tag: tags.bool, class: 'bool' },
79
+ { tag: tags.function(tags.variableName), class: 'function' },
80
+ { tag: tags.function(tags.special(tags.variableName)), class: 'function' },
81
+ { tag: tags.controlKeyword, class: 'control' },
82
+ { tag: tags.operatorKeyword, class: 'control' }
83
+ ])
84
+ );
85
+
86
+ var theme = [ baseTheme, highlightTheme, syntaxClasses ];
11
87
 
12
88
  // helpers ///////////////////////////////
13
89
 
14
- function isNodeEmpty(node) {
15
- return node.from === node.to;
90
+ function _isEmpty(node) {
91
+ return node && node.from === node.to;
92
+ }
93
+
94
+ /**
95
+ * @param {any} node
96
+ * @param {number} pos
97
+ *
98
+ * @return {boolean}
99
+ */
100
+ function isEmpty(node, pos) {
101
+
102
+ // For the special case of empty nodes, we need to check the current node
103
+ // as well. The previous node could be part of another token, e.g.
104
+ // when typing functions "abs(".
105
+ const nextNode = node.nextSibling;
106
+
107
+ return _isEmpty(node) || (
108
+ nextNode && nextNode.from === pos && _isEmpty(nextNode)
109
+ );
110
+ }
111
+
112
+ function isVariableName(node) {
113
+ return node && node.parent && node.parent.name === 'VariableName';
16
114
  }
17
115
 
18
116
  function isPathExpression(node) {
@@ -27,7 +125,382 @@ function isPathExpression(node) {
27
125
  return isPathExpression(node.parent);
28
126
  }
29
127
 
30
- var tags = [
128
+ /**
129
+ * @typedef { import('../core').Variable } Variable
130
+ * @typedef { import('@codemirror/autocomplete').CompletionSource } CompletionSource
131
+ */
132
+
133
+ /**
134
+ * @param { {
135
+ * variables?: Variable[],
136
+ * } } options
137
+ *
138
+ * @return { CompletionSource }
139
+ */
140
+ function pathExpressionCompletion({ variables }) {
141
+
142
+ return (context) => {
143
+
144
+ const nodeBefore = syntaxTree(context.state).resolve(context.pos, -1);
145
+
146
+ if (!isPathExpression(nodeBefore)) {
147
+ return;
148
+ }
149
+
150
+ const expression = findPathExpression(nodeBefore);
151
+
152
+ // if the cursor is directly after the `.`, variable starts at the cursor position
153
+ const from = nodeBefore === expression ? context.pos : nodeBefore.from;
154
+
155
+ const path = getPath(expression, context);
156
+
157
+ let options = variables;
158
+ for (var i = 0; i < path.length - 1; i++) {
159
+ var childVar = options.find(val => val.name === path[i].name);
160
+
161
+ if (!childVar) {
162
+ return null;
163
+ }
164
+
165
+ // only suggest if variable type matches
166
+ if (
167
+ childVar.isList !== 'optional' &&
168
+ !!childVar.isList !== path[i].isList
169
+ ) {
170
+ return;
171
+ }
172
+
173
+ options = childVar.entries;
174
+ }
175
+
176
+ if (!options) return;
177
+
178
+ options = options.map(v => ({
179
+ label: v.name,
180
+ type: 'variable',
181
+ info: v.info,
182
+ detail: v.detail
183
+ }));
184
+
185
+ const result = {
186
+ from: from,
187
+ options: options
188
+ };
189
+
190
+ return result;
191
+ };
192
+ }
193
+
194
+
195
+ function findPathExpression(node) {
196
+ while (node) {
197
+ if (node.name === 'PathExpression') {
198
+ return node;
199
+ }
200
+ node = node.parent;
201
+ }
202
+ }
203
+
204
+ // parses the path expression into a list of variable names with type information
205
+ // e.g. foo[0].bar => [ { name: 'foo', isList: true }, { name: 'bar', isList: false } ]
206
+ function getPath(node, context) {
207
+ let path = [];
208
+
209
+ for (let child = node.firstChild; child; child = child.nextSibling) {
210
+ if (child.name === 'PathExpression') {
211
+ path.push(...getPath(child, context));
212
+ } else if (child.name === 'FilterExpression') {
213
+ path.push(...getFilter(child, context));
214
+ }
215
+ else {
216
+ path.push({
217
+ name: getNodeContent(child, context),
218
+ isList: false
219
+ });
220
+ }
221
+ }
222
+ return path;
223
+ }
224
+
225
+ function getFilter(node, context) {
226
+ const list = node.firstChild;
227
+
228
+ if (list.name === 'PathExpression') {
229
+ const path = getPath(list, context);
230
+ const last = path[path.length - 1];
231
+ last.isList = true;
232
+
233
+ return path;
234
+ }
235
+
236
+ return [ {
237
+ name: getNodeContent(list, context),
238
+ isList: true
239
+ } ];
240
+ }
241
+
242
+ function getNodeContent(node, context) {
243
+ return context.state.sliceDoc(node.from, node.to);
244
+ }
245
+
246
+ /**
247
+ * @typedef { import('../core').Variable } Variable
248
+ * @typedef { import('@codemirror/autocomplete').CompletionSource } CompletionSource
249
+ */
250
+
251
+ /**
252
+ * @param { {
253
+ * variables?: Variable[],
254
+ * builtins?: Variable[]
255
+ * } } options
256
+ *
257
+ * @return { CompletionSource }
258
+ */
259
+ function variableCompletion({ variables = [], builtins = [] }) {
260
+
261
+ const options = getVariableSuggestions(variables, builtins);
262
+
263
+ if (!options.length) {
264
+ return (context) => null;
265
+ }
266
+
267
+ return (context) => {
268
+
269
+ const {
270
+ pos,
271
+ state
272
+ } = context;
273
+
274
+ // in most cases, use what is typed before the cursor
275
+ const nodeBefore = syntaxTree(state).resolve(pos, -1);
276
+
277
+ if (isEmpty(nodeBefore, pos)) {
278
+ return context.explicit ? {
279
+ from: pos,
280
+ options
281
+ } : null;
282
+ }
283
+
284
+ // only auto-complete variables
285
+ if (!isVariableName(nodeBefore) || isPathExpression(nodeBefore)) {
286
+ return null;
287
+ }
288
+
289
+ return {
290
+ from: nodeBefore.from,
291
+ options
292
+ };
293
+ };
294
+ }
295
+
296
+ /**
297
+ * @param { Variable[] } variables
298
+ * @param { Variable[] } builtins
299
+ *
300
+ * @returns {import('@codemirror/autocomplete').Completion[]}
301
+ */
302
+ function getVariableSuggestions(variables, builtins) {
303
+ return [].concat(
304
+ variables.map(v => createVariableSuggestion(v)),
305
+ builtins.map(b => createVariableSuggestion(b))
306
+ );
307
+ }
308
+
309
+ /**
310
+ * @param {import('..').Variable} variable
311
+ * @param {number} boost
312
+
313
+ * @returns {import('@codemirror/autocomplete').Completion}
314
+ */
315
+ function createVariableSuggestion(variable, boost) {
316
+ if (variable.type === 'function') {
317
+ return createFunctionVariable(variable, boost);
318
+ }
319
+
320
+ return {
321
+ label: variable.name,
322
+ type: 'variable',
323
+ info: variable.info,
324
+ detail: variable.detail,
325
+ boost
326
+ };
327
+ }
328
+
329
+ /**
330
+ * @param {import('..').Variable} variable
331
+ * @param {number} boost
332
+ *
333
+ * @returns {import('@codemirror/autocomplete').Completion}
334
+ */
335
+ function createFunctionVariable(variable, boost) {
336
+ const {
337
+ name,
338
+ info,
339
+ detail,
340
+ params = []
341
+ } = variable;
342
+
343
+ const paramsWithNames = params.map(({ name, type }, index) => ({
344
+ name: name || `param ${index + 1}`,
345
+ type
346
+ }));
347
+
348
+ const template = `${name}(${paramsWithNames.map(p => '${' + p.name + '}').join(', ')})`;
349
+
350
+ const paramsSignature = paramsWithNames.map(({ name, type }) => (
351
+ type ? `${name}: ${type}` : name
352
+ )).join(', ');
353
+ const label = `${name}(${paramsSignature})`;
354
+
355
+ return snippetCompletion(template, {
356
+ label,
357
+ type: 'function',
358
+ info,
359
+ detail,
360
+ boost
361
+ });
362
+ }
363
+
364
+ /**
365
+ * @typedef { import('../core').Variable } Variable
366
+ * @typedef { import('@codemirror/autocomplete').CompletionSource } CompletionSource
367
+ */
368
+
369
+ /**
370
+ * @param { {
371
+ * variables?: Variable[],
372
+ * builtins?: Variable[]
373
+ * } } options
374
+ *
375
+ * @return { CompletionSource[] }
376
+ */
377
+ function completions({ variables = [], builtins = [] }) {
378
+
379
+ return [
380
+ pathExpressionCompletion({ variables }),
381
+ variableCompletion({ variables, builtins }),
382
+ snippetCompletion$1(snippets.map(snippet => ({ ...snippet, boost: -1 }))),
383
+ ...keywordCompletions
384
+ ];
385
+ }
386
+
387
+ /**
388
+ * @typedef { 'expression' | 'unaryTests' } Dialect
389
+ */
390
+
391
+ /**
392
+ * @param { {
393
+ * dialect?: Dialect,
394
+ * context?: Record<string, any>,
395
+ * completions?: import('@codemirror/autocomplete').CompletionSource[]
396
+ * } } options
397
+ *
398
+ * @return { import('@codemirror/language').LanguageSupport }
399
+ */
400
+ function language(options) {
401
+ return feel(options);
402
+ }
403
+
404
+ /**
405
+ * @param { import('../core').Variable[] } variables
406
+ *
407
+ * @return {Record<string, any>}
408
+ */
409
+ function createContext(variables, builtins) {
410
+ return variables.slice().reverse().reduce((context, builtin) => {
411
+ context[builtin.name] = new Function();
412
+
413
+ return context;
414
+ }, {});
415
+ }
416
+
417
+ /**
418
+ * @typedef { 'expression' | 'unaryTests' } Dialect
419
+ * @typedef { import('..').Variable } Variable
420
+ */
421
+
422
+ /**
423
+ * @type {Facet<Variable[]>}
424
+ */
425
+ const builtinsFacet = Facet.define();
426
+
427
+ /**
428
+ * @type {Facet<Variable[]>}
429
+ */
430
+ const variablesFacet = Facet.define();
431
+
432
+ /**
433
+ * @type {Facet<dialect>}
434
+ */
435
+ const dialectFacet = Facet.define();
436
+
437
+ /**
438
+ * @typedef {object} Variable
439
+ * @property {string} name name or key of the variable
440
+ * @property {string} [info] short information about the variable, e.g. type
441
+ * @property {string} [detail] longer description of the variable content
442
+ * @property {boolean} [isList] whether the variable is a list
443
+ * @property {Array<Variable>} [schema] array of child variables if the variable is a context or list
444
+ * @property {'function'|'variable'} [type] type of the variable
445
+ * @property {Array<{name: string, type: string}>} [params] function parameters
446
+ */
447
+
448
+ /**
449
+ * @typedef { {
450
+ * dialect?: import('../language').Dialect,
451
+ * variables?: Variable[],
452
+ * builtins?: Variable[]
453
+ * } } CoreConfig
454
+ *
455
+ * @typedef { import('@codemirror/autocomplete').CompletionSource } CompletionSource
456
+ * @typedef { import('@codemirror/state').Extension } Extension
457
+ */
458
+
459
+ /**
460
+ * @param { CoreConfig & { completions?: CompletionSource[] } } config
461
+ *
462
+ * @return { Extension }
463
+ */
464
+ function configure({
465
+ dialect = 'expression',
466
+ variables = [],
467
+ builtins = [],
468
+ completions: completions$1 = completions({ builtins, variables })
469
+ }) {
470
+
471
+ const context = createContext([ ...variables, ...builtins ]);
472
+
473
+ return [
474
+ dialectFacet.of(dialect),
475
+ builtinsFacet.of(builtins),
476
+ variablesFacet.of(variables),
477
+ language({
478
+ dialect,
479
+ context,
480
+ completions: completions$1
481
+ })
482
+ ];
483
+ }
484
+
485
+ /**
486
+ * @param {import('@codemirror/state').EditorState } state
487
+ *
488
+ * @return { CoreConfig }
489
+ */
490
+ function get(state) {
491
+
492
+ const builtins = state.facet(builtinsFacet)[0];
493
+ const variables = state.facet(variablesFacet)[0];
494
+ const dialect = state.facet(dialectFacet)[0];
495
+
496
+ return {
497
+ builtins,
498
+ variables,
499
+ dialect
500
+ };
501
+ }
502
+
503
+ var camundaTags = [
31
504
  {
32
505
  name: "not(negand)",
33
506
  description: "<p>Returns the logical negation of the given value.</p>\n<p><strong>Function signature</strong></p>\n<pre><code class=\"language-feel\">not(negand: boolean): boolean\n</code></pre>\n<p><strong>Examples</strong></p>\n<pre><code class=\"language-feel\">not(true)\n// false\n\nnot(null)\n// null\n</code></pre>\n"
@@ -106,7 +579,7 @@ var tags = [
106
579
  },
107
580
  {
108
581
  name: "date and time(from)",
109
- description: "<p>Parses the given string into a date and time.</p>\n<p><strong>Function signature</strong></p>\n<pre><code class=\"language-feel\">date and time(from: string): date and time\n</code></pre>\n<p><strong>Examples</strong></p>\n<pre><code class=\"language-feel\">date and time(&quot;2018-04-29T009:30:00&quot;)\n// date and time(&quot;2018-04-29T009:30:00&quot;)\n</code></pre>\n"
582
+ description: "<p>Parses the given string into a date and time.</p>\n<p><strong>Function signature</strong></p>\n<pre><code class=\"language-feel\">date and time(from: string): date and time\n</code></pre>\n<p><strong>Examples</strong></p>\n<pre><code class=\"language-feel\">date and time(&quot;2018-04-29T09:30:00&quot;)\n// date and time(&quot;2018-04-29T09:30:00&quot;)\n</code></pre>\n"
110
583
  },
111
584
  {
112
585
  name: "date and time(date, time)",
@@ -510,363 +983,59 @@ var tags = [
510
983
  }
511
984
  ];
512
985
 
513
- const options = tags.map(tag => {
514
- const match = tag.name.match(/^([\w\s]+)\((.*)\)$/);
515
- const functionName = match[1];
516
- const functionArguments = match[2];
517
-
518
- const placeHolders = functionArguments
519
- .split(', ')
520
- .map((arg) => `\${${arg}}`)
521
- .join(', ');
522
-
523
- return snippetCompletion(
524
- `${functionName}(${placeHolders})`,
525
- {
526
- label: tag.name,
527
- type: 'function',
528
- info: () => {
529
- const html = domify(`<div class="description">${tag.description}<div>`);
530
- return html;
531
- },
532
- boost: -1
533
- }
534
- );
535
- });
536
-
537
- var builtins = context => {
538
-
539
- let nodeBefore = syntaxTree(context.state).resolve(context.pos, -1);
540
-
541
- // For the special case of empty nodes, we need to check the current node
542
- // as well. The previous node could be part of another token, e.g.
543
- // when typing functions "abs(".
544
- let nextNode = nodeBefore.nextSibling;
545
- const isInEmptyNode =
546
- isNodeEmpty(nodeBefore) ||
547
- nextNode && nextNode.from === context.pos && isNodeEmpty(nextNode);
548
-
549
- if (isInEmptyNode) {
550
- return context.explicit ? {
551
- from: context.pos,
552
- options: options
553
- } : null;
554
- }
555
-
556
- // Don't auto-complete on path expressions/context keys/...
557
- if ((nodeBefore.parent && nodeBefore.parent.name !== 'VariableName') || isPathExpression(nodeBefore)) {
558
- return null;
559
- }
560
-
561
- return {
562
- from: nodeBefore.from,
563
- options: options
564
- };
565
- };
566
-
567
986
  /**
568
- * @type {Facet<import('..').Variable[]>} Variable
987
+ * @param { import('..').Builtin[] } builtins
988
+ *
989
+ * @returns {import('..').Variable[] } variable
569
990
  */
570
- const variablesFacet = Facet.define();
571
-
572
- var pathExpression = context => {
573
- const variables = context.state.facet(variablesFacet)[0];
574
- const nodeBefore = syntaxTree(context.state).resolve(context.pos, -1);
575
-
576
- if (!isPathExpression(nodeBefore)) {
577
- return;
578
- }
579
-
580
- const expression = findPathExpression(nodeBefore);
581
-
582
- // if the cursor is directly after the `.`, variable starts at the cursor position
583
- const from = nodeBefore === expression ? context.pos : nodeBefore.from;
584
-
585
- const path = getPath(expression, context);
586
-
587
- let options = variables;
588
- for (var i = 0; i < path.length - 1; i++) {
589
- var childVar = options.find(val => val.name === path[i].name);
590
-
591
- if (!childVar) {
592
- return null;
593
- }
594
-
595
- // only suggest if variable type matches
596
- if (
597
- childVar.isList !== 'optional' &&
598
- !!childVar.isList !== path[i].isList
599
- ) {
600
- return;
601
- }
602
-
603
- options = childVar.entries;
604
- }
605
-
606
- if (!options) return;
607
-
608
- options = options.map(v => ({
609
- label: v.name,
610
- type: 'variable',
611
- info: v.info,
612
- detail: v.detail
613
- }));
614
-
615
- const result = {
616
- from: from,
617
- options: options
618
- };
619
-
620
- return result;
621
- };
622
-
623
-
624
- function findPathExpression(node) {
625
- while (node) {
626
- if (node.name === 'PathExpression') {
627
- return node;
628
- }
629
- node = node.parent;
630
- }
631
- }
632
-
633
- // parses the path expression into a list of variable names with type information
634
- // e.g. foo[0].bar => [ { name: 'foo', isList: true }, { name: 'bar', isList: false } ]
635
- function getPath(node, context) {
636
- let path = [];
637
-
638
- for (let child = node.firstChild; child; child = child.nextSibling) {
639
- if (child.name === 'PathExpression') {
640
- path.push(...getPath(child, context));
641
- } else if (child.name === 'FilterExpression') {
642
- path.push(...getFilter(child, context));
643
- }
644
- else {
645
- path.push({
646
- name: getNodeContent(child, context),
647
- isList: false
648
- });
649
- }
650
- }
651
- return path;
652
- }
653
-
654
- function getFilter(node, context) {
655
- const list = node.firstChild;
656
-
657
- if (list.name === 'PathExpression') {
658
- const path = getPath(list, context);
659
- const last = path[path.length - 1];
660
- last.isList = true;
661
-
662
- return path;
663
- }
664
-
665
- return [ {
666
- name: getNodeContent(list, context),
667
- isList: true
668
- } ];
991
+ function parseBuiltins(builtins) {
992
+ return builtins.map(parseBuiltin);
669
993
  }
670
994
 
671
- function getNodeContent(node, context) {
672
- return context.state.sliceDoc(node.from, node.to);
673
- }
674
-
675
- /**
676
- * @type {import('@codemirror/autocomplete').CompletionSource}
677
- */
678
- var variables = context => {
679
-
680
- const variables = context.state.facet(variablesFacet)[0];
681
-
682
- const options = variables.map(v => createVariableSuggestion(v));
683
-
684
- // In most cases, use what is typed before the cursor
685
- let nodeBefore = syntaxTree(context.state).resolve(context.pos, -1);
686
-
687
- // For the special case of empty nodes, we need to check the current node
688
- // as well. The previous node could be part of another token, e.g.
689
- // when typing functions "abs(".
690
- let nextNode = nodeBefore.nextSibling;
691
- const isInEmptyNode =
692
- isNodeEmpty(nodeBefore) ||
693
- nextNode && nextNode.from === context.pos && isNodeEmpty(nextNode);
694
-
695
- if (isInEmptyNode) {
696
- return context.explicit ? {
697
- from: context.pos,
698
- options: options
699
- } : null;
700
- }
701
-
702
- const result = {
703
- from: nodeBefore.from,
704
- options: options
705
- };
706
-
707
- // Only auto-complete variables
708
- if ((nodeBefore.parent && nodeBefore.parent.name !== 'VariableName') || isPathExpression(nodeBefore)) {
709
- return null;
710
- }
711
-
712
- return result;
713
- };
714
-
715
995
  /**
716
- * @param {import('..').Variable} variable
717
- * @returns {import('@codemirror/autocomplete').Completion}
996
+ * @param { import('..').Builtin } builtin
997
+ *
998
+ * @returns { import('..').Variable } variable
718
999
  */
719
- function createVariableSuggestion(variable) {
720
- if (variable.type === 'function') {
721
- return createFunctionVariable(variable);
722
- }
723
-
724
- return {
725
- label: variable.name,
726
- type: 'variable',
727
- info: variable.info,
728
- detail: variable.detail
729
- };
730
- }
1000
+ function parseBuiltin(builtin) {
731
1001
 
732
- /**
733
- * @param {import('..').Variable} variable
734
- * @returns {import('@codemirror/autocomplete').Completion}
735
- */
736
- function createFunctionVariable(variable) {
737
1002
  const {
738
1003
  name,
739
- info,
740
- detail,
741
- params = []
742
- } = variable;
743
-
744
- const paramsWithNames = params.map(({ name, type }, index) => ({
745
- name: name || `param ${index + 1}`,
746
- type
747
- }));
1004
+ description
1005
+ } = builtin;
748
1006
 
749
- const template = `${name}(${paramsWithNames.map(p => '${' + p.name + '}').join(', ')})`;
1007
+ const match = name.match(/^([\w\s]+)\((.*)\)$/);
1008
+ const functionName = match[1];
1009
+ const functionArguments = match[2];
750
1010
 
751
- const paramsSignature = paramsWithNames.map(({ name, type }) => (
752
- type ? `${name}: ${type}` : name
753
- )).join(', ');
754
- const label = `${name}(${paramsSignature})`;
1011
+ const params = functionArguments.split(', ').map(name => ({ name }));
755
1012
 
756
- return snippetCompletion(template, {
757
- label,
1013
+ return {
1014
+ name: functionName,
758
1015
  type: 'function',
759
- info,
760
- detail
761
- });
762
- }
763
-
764
- function autocompletion() {
765
- return [
766
- autocompletion$1({
767
- override: [
768
- variables,
769
- builtins,
770
- completeFromList(snippets.map(s => ({ ...s, boost: -1 }))),
771
- pathExpression,
772
- ...keywordCompletions
773
- ]
774
- })
775
- ];
776
- }
777
-
778
- function language() {
779
- return new LanguageSupport(feelLanguage, [ ]);
1016
+ params,
1017
+ info: () => {
1018
+ return domify(`<div class="description">${description}<div>`);
1019
+ },
1020
+ boost: 0
1021
+ };
780
1022
  }
781
1023
 
782
- var linter = [ linter$1(cmFeelLinter()) ];
783
-
784
- const baseTheme = EditorView.theme({
785
- '& .cm-content': {
786
- padding: '0px',
787
- },
788
- '& .cm-line': {
789
- padding: '0px',
790
- },
791
- '&.cm-editor.cm-focused': {
792
- outline: 'none',
793
- },
794
- '& .cm-completionInfo': {
795
- whiteSpace: 'pre-wrap',
796
- overflow: 'hidden',
797
- textOverflow: 'ellipsis'
798
- },
799
-
800
- // Don't wrap whitespace for custom HTML
801
- '& .cm-completionInfo > *': {
802
- whiteSpace: 'normal'
803
- },
804
- '& .cm-completionInfo ul': {
805
- margin: 0,
806
- paddingLeft: '15px'
807
- },
808
- '& .cm-completionInfo pre': {
809
- marginBottom: 0,
810
- whiteSpace: 'pre-wrap'
811
- },
812
- '& .cm-completionInfo p': {
813
- marginTop: 0,
814
- },
815
- '& .cm-completionInfo p:not(:last-of-type)': {
816
- marginBottom: 0,
817
- }
818
- });
819
-
820
- const highlightTheme = EditorView.baseTheme({
821
- '& .variableName': {
822
- color: '#10f'
823
- },
824
- '& .number': {
825
- color: '#164'
826
- },
827
- '& .string': {
828
- color: '#a11'
829
- },
830
- '& .bool': {
831
- color: '#219'
832
- },
833
- '& .function': {
834
- color: '#aa3731',
835
- fontWeight: 'bold'
836
- },
837
- '& .control': {
838
- color: '#708'
839
- }
840
- });
841
-
842
- const syntaxClasses = syntaxHighlighting(
843
- HighlightStyle.define([
844
- { tag: tags$1.variableName, class: 'variableName' },
845
- { tag: tags$1.name, class: 'variableName' },
846
- { tag: tags$1.number, class: 'number' },
847
- { tag: tags$1.string, class: 'string' },
848
- { tag: tags$1.bool, class: 'bool' },
849
- { tag: tags$1.function(tags$1.variableName), class: 'function' },
850
- { tag: tags$1.function(tags$1.special(tags$1.variableName)), class: 'function' },
851
- { tag: tags$1.controlKeyword, class: 'control' },
852
- { tag: tags$1.operatorKeyword, class: 'control' }
853
- ])
854
- );
1024
+ const camunda = parseBuiltins(camundaTags);
855
1025
 
856
- var theme = [ baseTheme, highlightTheme, syntaxClasses ];
1026
+ /**
1027
+ * @typedef { import('./core').Variable } Variable
1028
+ */
857
1029
 
858
1030
  /**
859
- * @typedef {object} Variable
860
- * @property {string} name name or key of the variable
861
- * @property {string} [info] short information about the variable, e.g. type
862
- * @property {string} [detail] longer description of the variable content
863
- * @property {boolean} [isList] whether the variable is a list
864
- * @property {Array<Variable>} [schema] array of child variables if the variable is a context or list
865
- * @property {'function'|'variable'} [type] type of the variable
866
- * @property {Array<{name: string, type: string}>} [params] function parameters
1031
+ * @typedef {object} Builtin
1032
+ * @property {string} name
1033
+ * @property {string} description
867
1034
  */
868
1035
 
869
- const autocompletionConf = new Compartment();
1036
+ const coreConf = new Compartment();
1037
+ const placeholderConf = new Compartment();
1038
+
870
1039
 
871
1040
  /**
872
1041
  * Creates a FEEL editor in the supplied container
@@ -874,6 +1043,7 @@ const autocompletionConf = new Compartment();
874
1043
  * @param {Object} config
875
1044
  * @param {DOMNode} config.container
876
1045
  * @param {Extension[]} [config.extensions]
1046
+ * @param {Dialect} [config.dialect='expression']
877
1047
  * @param {DOMNode|String} [config.tooltipContainer]
878
1048
  * @param {Function} [config.onChange]
879
1049
  * @param {Function} [config.onKeyDown]
@@ -881,19 +1051,23 @@ const autocompletionConf = new Compartment();
881
1051
  * @param {Boolean} [config.readOnly]
882
1052
  * @param {String} [config.value]
883
1053
  * @param {Variable[]} [config.variables]
1054
+ * @param {Variable[]} [config.builtins]
884
1055
  *
885
1056
  * @returns {Object} editor
886
1057
  */
887
1058
  function FeelEditor({
888
1059
  extensions: editorExtensions = [],
1060
+ dialect = 'expression',
889
1061
  container,
890
1062
  contentAttributes = {},
891
1063
  tooltipContainer,
892
1064
  onChange = () => {},
893
1065
  onKeyDown = () => {},
894
1066
  onLint = () => {},
1067
+ placeholder: placeholder$1 = '',
895
1068
  readOnly = false,
896
1069
  value = '',
1070
+ builtins = camunda,
897
1071
  variables = []
898
1072
  }) {
899
1073
 
@@ -934,21 +1108,25 @@ function FeelEditor({
934
1108
  }) : [];
935
1109
 
936
1110
  const extensions = [
937
- autocompletionConf.of(variablesFacet.of(variables)),
938
1111
  autocompletion(),
1112
+ coreConf.of(configure({
1113
+ dialect,
1114
+ builtins,
1115
+ variables
1116
+ })),
939
1117
  bracketMatching(),
940
- changeHandler,
1118
+ indentOnInput(),
941
1119
  closeBrackets(),
942
1120
  EditorView.contentAttributes.of(contentAttributes),
943
- indentOnInput(),
1121
+ changeHandler,
944
1122
  keyHandler,
945
1123
  keymap.of([
946
1124
  ...defaultKeymap,
947
1125
  ]),
948
- language(),
949
1126
  linter,
950
1127
  lintHandler,
951
1128
  tooltipLayout,
1129
+ placeholderConf.of(placeholder(placeholder$1)),
952
1130
  theme,
953
1131
  ...editorExtensions
954
1132
  ];
@@ -960,7 +1138,7 @@ function FeelEditor({
960
1138
  this._cmEditor = new EditorView({
961
1139
  state: EditorState.create({
962
1140
  doc: value,
963
- extensions: extensions
1141
+ extensions
964
1142
  }),
965
1143
  parent: container
966
1144
  });
@@ -1013,12 +1191,35 @@ FeelEditor.prototype.getSelection = function() {
1013
1191
 
1014
1192
  /**
1015
1193
  * Set variables to be used for autocompletion.
1194
+ *
1016
1195
  * @param {Variable[]} variables
1017
- * @returns {void}
1018
1196
  */
1019
1197
  FeelEditor.prototype.setVariables = function(variables) {
1198
+
1199
+ const {
1200
+ dialect,
1201
+ builtins
1202
+ } = get(this._cmEditor.state);
1203
+
1204
+ this._cmEditor.dispatch({
1205
+ effects: [
1206
+ coreConf.reconfigure(configure({
1207
+ dialect,
1208
+ builtins,
1209
+ variables
1210
+ }))
1211
+ ]
1212
+ });
1213
+ };
1214
+
1215
+ /**
1216
+ * Update placeholder text.
1217
+ *
1218
+ * @param {string} placeholder
1219
+ */
1220
+ FeelEditor.prototype.setPlaceholder = function(placeholder$1) {
1020
1221
  this._cmEditor.dispatch({
1021
- effects: autocompletionConf.reconfigure(variablesFacet.of(variables))
1222
+ effects: placeholderConf.reconfigure(placeholder(placeholder$1))
1022
1223
  });
1023
1224
  };
1024
1225