@api-client/ui 0.5.12 → 0.5.14

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 (44) hide show
  1. package/build/src/elements/code-editor/{ui-code-editor.d.ts → code-editor.d.ts} +3 -2
  2. package/build/src/elements/code-editor/code-editor.d.ts.map +1 -0
  3. package/build/src/elements/code-editor/{ui-code-editor.js → code-editor.js} +1 -1
  4. package/build/src/elements/code-editor/code-editor.js.map +1 -0
  5. package/build/src/elements/code-editor/internals/CodeEditor.d.ts +22 -59
  6. package/build/src/elements/code-editor/internals/CodeEditor.d.ts.map +1 -1
  7. package/build/src/elements/code-editor/internals/CodeEditor.js +139 -106
  8. package/build/src/elements/code-editor/internals/CodeEditor.js.map +1 -1
  9. package/build/src/elements/code-editor/internals/CodeEditor.styles.d.ts.map +1 -1
  10. package/build/src/elements/code-editor/internals/CodeEditor.styles.js +4 -0
  11. package/build/src/elements/code-editor/internals/CodeEditor.styles.js.map +1 -1
  12. package/build/src/elements/code-editor/internals/Linter.d.ts +2 -2
  13. package/build/src/elements/code-editor/internals/Linter.d.ts.map +1 -1
  14. package/build/src/elements/code-editor/internals/Linter.js +33 -37
  15. package/build/src/elements/code-editor/internals/Linter.js.map +1 -1
  16. package/build/src/elements/code-editor/internals/PlaceholderWidget.d.ts +20 -0
  17. package/build/src/elements/code-editor/internals/PlaceholderWidget.d.ts.map +1 -0
  18. package/build/src/elements/code-editor/internals/PlaceholderWidget.js +46 -0
  19. package/build/src/elements/code-editor/internals/PlaceholderWidget.js.map +1 -0
  20. package/build/src/elements/code-editor/internals/SuggestionMatchDecorator.d.ts +17 -0
  21. package/build/src/elements/code-editor/internals/SuggestionMatchDecorator.d.ts.map +1 -0
  22. package/build/src/elements/code-editor/internals/SuggestionMatchDecorator.js +31 -0
  23. package/build/src/elements/code-editor/internals/SuggestionMatchDecorator.js.map +1 -0
  24. package/build/src/elements/code-editor/internals/types.d.ts +26 -0
  25. package/build/src/elements/code-editor/internals/types.d.ts.map +1 -0
  26. package/build/src/elements/code-editor/internals/types.js +2 -0
  27. package/build/src/elements/code-editor/internals/types.js.map +1 -0
  28. package/build/src/index.d.ts +2 -2
  29. package/build/src/index.d.ts.map +1 -1
  30. package/build/src/index.js +1 -1
  31. package/build/src/index.js.map +1 -1
  32. package/demo/elements/code-editor/CodeEditorDemo.ts +19 -22
  33. package/package.json +3 -2
  34. package/src/elements/code-editor/README.md +1 -1
  35. package/src/elements/code-editor/{ui-code-editor.ts → code-editor.ts} +2 -7
  36. package/src/elements/code-editor/internals/CodeEditor.styles.ts +4 -0
  37. package/src/elements/code-editor/internals/CodeEditor.ts +170 -173
  38. package/src/elements/code-editor/internals/Linter.ts +38 -39
  39. package/src/elements/code-editor/internals/PlaceholderWidget.ts +50 -0
  40. package/src/elements/code-editor/internals/SuggestionMatchDecorator.ts +36 -0
  41. package/src/elements/code-editor/internals/types.ts +28 -0
  42. package/src/index.ts +2 -8
  43. package/build/src/elements/code-editor/ui-code-editor.d.ts.map +0 -1
  44. package/build/src/elements/code-editor/ui-code-editor.js.map +0 -1
@@ -1,8 +1,15 @@
1
1
  import { html, LitElement, PropertyValues, TemplateResult, nothing } from 'lit'
2
2
  import { property, query, state } from 'lit/decorators.js'
3
3
  import { classMap } from 'lit/directives/class-map.js'
4
+ import { linter } from '@codemirror/lint'
4
5
  import { EditorState, Extension } from '@codemirror/state'
5
- import { autocompletion, CompletionContext, CompletionResult, CompletionSource } from '@codemirror/autocomplete'
6
+ import {
7
+ autocompletion,
8
+ CompletionContext,
9
+ type CompletionResult,
10
+ type CompletionSource,
11
+ type Completion,
12
+ } from '@codemirror/autocomplete'
6
13
  import { javascript } from '@codemirror/lang-javascript'
7
14
  import { syntaxHighlighting, defaultHighlightStyle } from '@codemirror/language'
8
15
  import { oneDark } from '@codemirror/theme-one-dark'
@@ -10,120 +17,18 @@ import { keymap } from '@codemirror/view'
10
17
  import { defaultKeymap } from '@codemirror/commands'
11
18
  import {
12
19
  EditorView,
13
- MatchDecorator,
14
20
  Decoration,
15
21
  DecorationSet,
16
22
  ViewPlugin,
17
23
  ViewUpdate,
18
- WidgetType,
24
+ hoverTooltip,
25
+ type Tooltip,
19
26
  } from '@codemirror/view'
20
-
21
- export interface FunctionSchema {
22
- /** Unique identifier for the function */
23
- id: string
24
- /** Function name */
25
- name: string
26
- /** Function description */
27
- description?: string
28
- /** Function parameters */
29
- parameters?: FunctionParameter[]
30
- /** Return type description */
31
- returns?: string
32
- /** Additional metadata */
33
- metadata?: Record<string, unknown>
34
- }
35
-
36
- export interface FunctionParameter {
37
- /** Parameter name */
38
- name: string
39
- /** Parameter type */
40
- type: string
41
- /** Parameter description */
42
- description?: string
43
- /** Whether parameter is required */
44
- required?: boolean
45
- /** Default value */
46
- defaultValue?: unknown
47
- }
48
-
49
- export interface Suggestion {
50
- /** Unique identifier for the suggestion */
51
- id: string
52
- /** Main label displayed */
53
- label: string
54
- /** Supporting description text */
55
- description?: string
56
- /** Suffix text (e.g., type, category) */
57
- suffix?: string
58
- /** Additional data associated with the suggestion */
59
- data?: Record<string, unknown>
60
- }
61
-
62
- export interface FunctionInsertEvent {
63
- /** The inserted function schema */
64
- functionSchema: FunctionSchema
65
- /** The position where the function was inserted */
66
- position: number
67
- }
68
-
69
- export interface SuggestionInsertEvent {
70
- /** The inserted suggestion */
71
- suggestion: Suggestion
72
- /** The position where the suggestion was inserted */
73
- position: number
74
- }
75
-
76
- class PlaceholderWidget extends WidgetType {
77
- constructor(public placeholderText: string) {
78
- super()
79
- }
80
-
81
- override eq(other: WidgetType): boolean {
82
- return this.placeholderText == (other as PlaceholderWidget).placeholderText
83
- }
84
-
85
- toDOM(): HTMLElement {
86
- const placeholder = document.createElement('span')
87
- placeholder.className = 'cm-pill'
88
- placeholder.textContent = this.placeholderText
89
- return placeholder
90
- }
91
-
92
- override ignoreEvent(): boolean {
93
- return false
94
- }
95
- }
96
-
97
- // We do variables Salesforce style, with double curly braces.
98
- // This decorator replaces {{variable}} with a placeholder widget.
99
- const placeholderMatcher = new MatchDecorator({
100
- regexp: /\{\{(\w+)\}\}/g,
101
- decoration: (match) =>
102
- Decoration.replace({
103
- widget: new PlaceholderWidget(match[1]),
104
- }),
105
- })
106
-
107
- /**
108
- * @see https://codemirror.net/examples/decoration/
109
- */
110
- class AtomicDecorationRange {
111
- placeholders: DecorationSet
112
- constructor(view: EditorView) {
113
- this.placeholders = placeholderMatcher.createDeco(view)
114
- }
115
- update(update: ViewUpdate) {
116
- this.placeholders = placeholderMatcher.updateDeco(update, this.placeholders)
117
- }
118
- }
119
-
120
- const placeholders = ViewPlugin.fromClass(AtomicDecorationRange, {
121
- decorations: (instance) => instance.placeholders,
122
- provide: (plugin) =>
123
- EditorView.atomicRanges.of((view) => {
124
- return view.plugin(plugin)?.placeholders || Decoration.none
125
- }),
126
- })
27
+ import type { FunctionSchema } from '@pawel-up/jexl/schemas/types.js'
28
+ import { getTypeStringFromSchema } from '@pawel-up/jexl/schemas/utils.js'
29
+ import { functionLinter } from './Linter.js'
30
+ import type { FunctionInsertEvent, Suggestion, SuggestionInsertEvent } from './types.js'
31
+ import { SuggestionMatchDecorator } from './SuggestionMatchDecorator.js'
127
32
 
128
33
  /**
129
34
  * A CodeMirror 6 based editor component that supports function autocomplete and suggestion placeholders.
@@ -232,9 +137,11 @@ export default class CodeEditor extends LitElement {
232
137
  private accessor isEditorFocus = false
233
138
 
234
139
  private editorView?: EditorView
235
- private suggestionMap = new Map<string, Suggestion>()
236
- private functionMap = new Map<string, FunctionSchema>()
237
140
  private _previousValue = ''
141
+ /**
142
+ * Matcher for suggestion placeholders in the editor.
143
+ */
144
+ placeholderMatcher?: SuggestionMatchDecorator
238
145
 
239
146
  /**
240
147
  * Get all suggestions (placeholders) currently in the editor
@@ -254,11 +161,6 @@ export default class CodeEditor extends LitElement {
254
161
  return suggestions
255
162
  }
256
163
 
257
- override connectedCallback(): void {
258
- super.connectedCallback()
259
- this.setupSuggestionMaps()
260
- }
261
-
262
164
  override disconnectedCallback(): void {
263
165
  super.disconnectedCallback()
264
166
  this.editorView?.destroy()
@@ -272,8 +174,10 @@ export default class CodeEditor extends LitElement {
272
174
  override willUpdate(changedProperties: PropertyValues): void {
273
175
  super.willUpdate(changedProperties)
274
176
 
275
- if (changedProperties.has('suggestions') || changedProperties.has('functionSchemas')) {
276
- this.setupSuggestionMaps()
177
+ if (changedProperties.has('suggestions')) {
178
+ if (this.placeholderMatcher) {
179
+ this.placeholderMatcher.suggestions = this.suggestions || []
180
+ }
277
181
  }
278
182
 
279
183
  if (changedProperties.has('value') && this.editorView) {
@@ -305,8 +209,9 @@ export default class CodeEditor extends LitElement {
305
209
  this.handleFocusChange(update.view.hasFocus)
306
210
  }
307
211
  }),
308
- placeholders,
309
- // linter((view) => functionLinter(view as unknown as EditorView, this.functionSchemas)),
212
+ this.createPlaceholderPlugin(),
213
+ hoverTooltip(this.createHoverTooltipSource),
214
+ linter((view) => functionLinter(view, this.functionSchemas, (e) => this.dispatchEvent(e))),
310
215
  ]
311
216
 
312
217
  // Add language support
@@ -339,6 +244,30 @@ export default class CodeEditor extends LitElement {
339
244
  })
340
245
  }
341
246
 
247
+ /**
248
+ * Creates the ViewPlugin for rendering suggestion placeholders.
249
+ * This is created as a method to get access to the component's `suggestions`.
250
+ */
251
+ private createPlaceholderPlugin(): Extension {
252
+ const placeholderMatcher = new SuggestionMatchDecorator(/\{\{(\w+)\}\}/g, this.suggestions)
253
+ this.placeholderMatcher = placeholderMatcher
254
+ // This class needs to be defined here to have access to the `placeholderMatcher`
255
+ class AtomicDecorationRange {
256
+ placeholders: DecorationSet
257
+ constructor(view: EditorView) {
258
+ this.placeholders = placeholderMatcher.createDeco(view)
259
+ }
260
+ update(update: ViewUpdate) {
261
+ this.placeholders = placeholderMatcher.updateDeco(update, this.placeholders)
262
+ }
263
+ }
264
+
265
+ return ViewPlugin.fromClass(AtomicDecorationRange, {
266
+ decorations: (instance) => instance.placeholders,
267
+ provide: (plugin) => EditorView.atomicRanges.of((view) => view.plugin(plugin)?.placeholders || Decoration.none),
268
+ })
269
+ }
270
+
342
271
  /**
343
272
  * Create completion source for functions and suggestions
344
273
  */
@@ -354,19 +283,7 @@ export default class CodeEditor extends LitElement {
354
283
  const prefix = functionMatch[1]
355
284
  const functions = this.functionSchemas
356
285
  .filter((fn) => fn.name.toLowerCase().startsWith(prefix.toLowerCase()))
357
- .map((fn) => ({
358
- label: fn.name,
359
- detail: fn.description || '',
360
- info: this.formatFunctionInfo(fn),
361
- apply: (view: EditorView, completion: unknown, from: number, to: number) => {
362
- const functionCall = this.formatFunctionCall(fn)
363
- view.dispatch({
364
- changes: { from, to, insert: functionCall },
365
- selection: { anchor: from + functionCall.length },
366
- })
367
- this.dispatchFunctionInsert(fn, from)
368
- },
369
- }))
286
+ .map((fn) => this.createFunctionCompletion(fn))
370
287
 
371
288
  if (functions.length > 0) {
372
289
  return {
@@ -382,19 +299,7 @@ export default class CodeEditor extends LitElement {
382
299
  const prefix = suggestionMatch[1]
383
300
  const suggestions = this.suggestions
384
301
  .filter((suggestion) => suggestion.label.toLowerCase().startsWith(prefix.toLowerCase()))
385
- .map((suggestion) => ({
386
- label: suggestion.label,
387
- detail: suggestion.description || '',
388
- info: suggestion.suffix || '',
389
- apply: (view: EditorView, completion: unknown, from: number, to: number) => {
390
- const placeholderText = `{{${suggestion.label}}}`
391
- view.dispatch({
392
- changes: { from: from - 2, to, insert: placeholderText }, // -2 to include the {{
393
- selection: { anchor: from - 2 + placeholderText.length },
394
- })
395
- this.dispatchSuggestionInsert(suggestion, from - 2)
396
- },
397
- }))
302
+ .map((suggestion) => this.createSuggestionCompletion(suggestion))
398
303
 
399
304
  if (suggestions.length > 0) {
400
305
  return {
@@ -408,25 +313,133 @@ export default class CodeEditor extends LitElement {
408
313
  }
409
314
  }
410
315
 
316
+ private createFunctionCompletion(schema: FunctionSchema): Completion {
317
+ const result: Completion = {
318
+ label: schema.name,
319
+ detail: schema.description || '',
320
+ info: () => this.createFunctionInfoElement(schema),
321
+ apply: (view: EditorView, completion: unknown, from: number, to: number) => {
322
+ const functionCall = this.formatFunctionCall(schema)
323
+ view.dispatch({
324
+ changes: { from, to, insert: functionCall },
325
+ selection: { anchor: from + functionCall.length },
326
+ })
327
+ this.dispatchFunctionInsert(schema, from)
328
+ },
329
+ }
330
+
331
+ return result
332
+ }
333
+
334
+ private createSuggestionCompletion(suggestion: Suggestion): Completion {
335
+ const result: Completion = {
336
+ label: suggestion.label,
337
+ detail: suggestion.description || '',
338
+ info: suggestion.suffix || '',
339
+ apply: (view: EditorView, completion: unknown, from: number, to: number) => {
340
+ const placeholderText = `{{${suggestion.label}}}`
341
+ view.dispatch({
342
+ changes: { from: from - 2, to, insert: placeholderText }, // -2 to include the {{
343
+ selection: { anchor: from - 2 + placeholderText.length },
344
+ })
345
+ this.dispatchSuggestionInsert(suggestion, from - 2)
346
+ },
347
+ }
348
+
349
+ return result
350
+ }
351
+
411
352
  /**
412
- * Format function information for autocomplete
353
+ * Creates the source for the hover tooltips.
354
+ * This is an arrow function to automatically bind `this`.
413
355
  */
414
- private formatFunctionInfo(fn: FunctionSchema): string {
415
- let info = fn.description || ''
356
+ private createHoverTooltipSource = (view: EditorView, pos: number): Tooltip | null => {
357
+ const word = view.state.wordAt(pos)
358
+ if (!word) {
359
+ return null
360
+ }
361
+
362
+ const functionName = view.state.doc.sliceString(word.from, word.to)
363
+ const fnSchema = this.functionSchemas.find((schema) => schema.name === functionName)
364
+
365
+ if (!fnSchema) {
366
+ return null
367
+ }
368
+
369
+ return {
370
+ pos: word.from,
371
+ end: word.to,
372
+ above: true,
373
+ create: () => ({ dom: this.createFunctionInfoElement(fnSchema) }),
374
+ }
375
+ }
376
+
377
+ /**
378
+ * Creates a styled HTML element to display function documentation.
379
+ * This is used for both hover tooltips and autocomplete info panels.
380
+ */
381
+ private createFunctionInfoElement(fn: FunctionSchema): HTMLElement {
382
+ const container = document.createElement('div')
383
+ container.className = 'function-info' // for styling
384
+
385
+ if (fn.description) {
386
+ const description = document.createElement('p')
387
+ description.className = 'description'
388
+ description.textContent = fn.description
389
+ container.appendChild(description)
390
+ }
391
+
416
392
  if (fn.parameters && fn.parameters.length > 0) {
417
- info += '\n\nParameters:\n'
393
+ const paramsContainer = document.createElement('div')
394
+ paramsContainer.className = 'parameters'
395
+
396
+ const paramsHeader = document.createElement('h4')
397
+ paramsHeader.textContent = 'Parameters'
398
+ paramsContainer.appendChild(paramsHeader)
399
+
400
+ const paramsList = document.createElement('ul')
418
401
  fn.parameters.forEach((param) => {
419
- info += `• ${param.name}: ${param.type}`
420
- if (param.description) {
421
- info += ` - ${param.description}`
402
+ const listItem = document.createElement('li')
403
+
404
+ const name = document.createElement('span')
405
+ name.className = 'param-name'
406
+ name.textContent = param.name
407
+
408
+ const type = document.createElement('span')
409
+ type.className = 'param-type'
410
+ type.textContent = `: ${getTypeStringFromSchema(param.schema)}`
411
+
412
+ listItem.appendChild(name)
413
+ listItem.appendChild(type)
414
+
415
+ if (param.schema.description) {
416
+ const paramDesc = document.createElement('p')
417
+ paramDesc.className = 'param-description'
418
+ paramDesc.textContent = param.schema.description
419
+ listItem.appendChild(paramDesc)
422
420
  }
423
- info += '\n'
421
+ paramsList.appendChild(listItem)
424
422
  })
423
+ paramsContainer.appendChild(paramsList)
424
+ container.appendChild(paramsContainer)
425
425
  }
426
+
426
427
  if (fn.returns) {
427
- info += `\nReturns: ${fn.returns}`
428
+ const returnsContainer = document.createElement('div')
429
+ returnsContainer.className = 'returns'
430
+
431
+ const returnsHeader = document.createElement('h4')
432
+ returnsHeader.textContent = 'Returns'
433
+ returnsContainer.appendChild(returnsHeader)
434
+
435
+ const returnsDesc = document.createElement('p')
436
+ const returnType = getTypeStringFromSchema(fn.returns)
437
+ returnsDesc.textContent = fn.returns.description ? `${returnType}: ${fn.returns.description}` : returnType
438
+ returnsContainer.appendChild(returnsDesc)
439
+ container.appendChild(returnsContainer)
428
440
  }
429
- return info
441
+
442
+ return container
430
443
  }
431
444
 
432
445
  /**
@@ -449,22 +462,6 @@ export default class CodeEditor extends LitElement {
449
462
  return `${fn.name}(${params})`
450
463
  }
451
464
 
452
- /**
453
- * Setup suggestion and function maps for quick lookup
454
- */
455
- private setupSuggestionMaps(): void {
456
- this.suggestionMap.clear()
457
- this.functionMap.clear()
458
-
459
- this.suggestions.forEach((suggestion) => {
460
- this.suggestionMap.set(suggestion.id, suggestion)
461
- })
462
-
463
- this.functionSchemas.forEach((fn) => {
464
- this.functionMap.set(fn.id, fn)
465
- })
466
- }
467
-
468
465
  /**
469
466
  * Update editor content when value changes
470
467
  */
@@ -1,13 +1,17 @@
1
1
  import type { Diagnostic } from '@codemirror/lint'
2
2
  import type { EditorView } from '@codemirror/view'
3
- import Element from './CodeEditor.js'
3
+ import type { FunctionSchema } from '@pawel-up/jexl/schemas/types.js'
4
4
 
5
- export function functionLinter(view: EditorView, element: Element): readonly Diagnostic[] {
5
+ export function functionLinter(
6
+ view: EditorView,
7
+ functionSchemas: readonly FunctionSchema[],
8
+ dispatchEvent: (e: CustomEvent) => void
9
+ ): readonly Diagnostic[] {
6
10
  const diagnostics: Diagnostic[] = []
7
11
  const doc = view.state.doc
8
12
  const text = doc.toString()
9
13
 
10
- const functions = element.functionSchemas
14
+ const functions = functionSchemas
11
15
  if (!functions || functions.length === 0) {
12
16
  return diagnostics
13
17
  }
@@ -22,22 +26,21 @@ export function functionLinter(view: EditorView, element: Element): readonly Dia
22
26
  const endPos = startPos + functionName.length
23
27
 
24
28
  // Check if this function exists in our schemas
25
- const functionExists = functions.some((schema) => schema.name === functionName)
29
+ const fn = functions.find((schema) => schema.name === functionName)
26
30
 
27
- if (!functionExists) {
31
+ if (!fn) {
28
32
  diagnostics.push({
29
33
  from: startPos,
30
34
  to: endPos,
31
35
  severity: 'error',
32
- message: `Unknown function "${functionName}". Only functions defined in the schema are allowed.`,
36
+ message: `Unknown function "${functionName}". Make sure you have the correct syntax for the function call.`,
33
37
  actions: [
34
38
  {
35
39
  name: 'View available functions',
36
40
  apply: () => {
37
- // This could trigger showing a help dialog or documentation
38
- element.dispatchEvent(
41
+ dispatchEvent(
39
42
  new CustomEvent('show-available-functions', {
40
- detail: { availableFunctions: functions.map((s) => s.name) },
43
+ detail: { availableFunctions: [...functions] },
41
44
  bubbles: true,
42
45
  })
43
46
  )
@@ -46,38 +49,34 @@ export function functionLinter(view: EditorView, element: Element): readonly Dia
46
49
  ],
47
50
  })
48
51
  } else {
49
- // Optional: Validate function usage (parameters, etc.)
50
- const fn = functions.find((schema) => schema.name === functionName)
52
+ // Extract the function call content to validate parameters
53
+ const functionCallMatch = text.substring(startPos).match(/(\w+)\s*\(([^)]*)\)/)
54
+ if (functionCallMatch && fn.parameters && fn.parameters.length > 0) {
55
+ const argsString = functionCallMatch[2].trim()
56
+ const args = argsString ? argsString.split(',').map((arg) => arg.trim()) : []
51
57
 
52
- if (fn) {
53
- // Extract the function call content to validate parameters
54
- const functionCallMatch = text.substring(startPos).match(/(\w+)\s*\(([^)]*)\)/)
55
- if (functionCallMatch && fn.parameters) {
56
- const argsString = functionCallMatch[2].trim()
57
- const args = argsString ? argsString.split(',').map((arg) => arg.trim()) : []
58
-
59
- // Check required parameters
60
- const requiredParams = fn.parameters.filter((p) => p.required)
61
- if (args.length < requiredParams.length) {
62
- const functionEndPos = startPos + functionCallMatch[0].length
63
- diagnostics.push({
64
- from: startPos,
65
- to: functionEndPos,
66
- severity: 'error',
67
- message: `Function "${functionName}" requires ${requiredParams.length} parameters but got ${args.length}. Required: ${requiredParams.map((p) => p.name).join(', ')}`,
68
- })
69
- }
58
+ // Check required parameters
59
+ const requiredParams = fn.parameters.filter((p) => p.required)
60
+ if (args.length < requiredParams.length) {
61
+ const functionEndPos = startPos + functionCallMatch[0].length
62
+ diagnostics.push({
63
+ from: startPos,
64
+ to: functionEndPos,
65
+ severity: 'error',
66
+ message: `Function "${functionName}" requires ${requiredParams.length} parameters but got ${args.length}. Required: ${requiredParams.map((p) => p.name).join(', ')}`,
67
+ })
68
+ }
70
69
 
71
- // Check if too many parameters
72
- if (fn.parameters && args.length > fn.parameters.length) {
73
- const functionEndPos = startPos + functionCallMatch[0].length
74
- diagnostics.push({
75
- from: startPos,
76
- to: functionEndPos,
77
- severity: 'warning',
78
- message: `Function "${functionName}" expects at most ${fn.parameters.length} parameters but got ${args.length}`,
79
- })
80
- }
70
+ // Check if too many parameters
71
+ const hasVariadicParam = fn.parameters.length > 0 && !!fn.parameters[fn.parameters.length - 1].variadic
72
+ if (!hasVariadicParam && args.length > fn.parameters.length) {
73
+ const functionEndPos = startPos + functionCallMatch[0].length
74
+ diagnostics.push({
75
+ from: startPos,
76
+ to: functionEndPos,
77
+ severity: 'warning',
78
+ message: `Function "${functionName}" expects at most ${fn.parameters.length} parameters, but got ${args.length}.`,
79
+ })
81
80
  }
82
81
  }
83
82
  }
@@ -0,0 +1,50 @@
1
+ import { EditorView, WidgetType } from '@codemirror/view'
2
+ import type { Suggestion } from './types.js'
3
+ import '../../../md/chip/ui-chip.js'
4
+
5
+ /**
6
+ * A widget that represents a placeholder in the code editor,
7
+ * specifically for suggestions that are replaced with chips.
8
+ *
9
+ * This widget is used to create a visual representation of a suggestion
10
+ * in the code editor, allowing users to see and interact with suggestions
11
+ * as chips. When a chip is removed, the corresponding text in the editor
12
+ * is also removed.
13
+ */
14
+ export class ChipWidget extends WidgetType {
15
+ constructor(public suggestion: Suggestion) {
16
+ super()
17
+ }
18
+
19
+ override eq(other: WidgetType): boolean {
20
+ return this.suggestion.id == (other as ChipWidget).suggestion.id
21
+ }
22
+
23
+ toDOM(view: EditorView): HTMLElement {
24
+ const wrapper = document.createElement('span')
25
+ wrapper.className = 'mention-chip'
26
+ wrapper.setAttribute('data-mention-id', this.suggestion.id)
27
+
28
+ const chip = document.createElement('ui-chip')
29
+ chip.setAttribute('type', 'input')
30
+ chip.setAttribute('removable', 'true')
31
+ chip.textContent = this.suggestion.label
32
+ chip.addEventListener('remove', () => {
33
+ const pos = view.posAtDOM(wrapper)
34
+ if (pos === null) return
35
+
36
+ const originalText = `{{${this.suggestion.label}}}`
37
+ view.dispatch({
38
+ changes: { from: pos, to: pos + originalText.length, insert: '' },
39
+ })
40
+ view.focus()
41
+ })
42
+
43
+ wrapper.appendChild(chip)
44
+ return wrapper
45
+ }
46
+
47
+ override ignoreEvent(): boolean {
48
+ return false
49
+ }
50
+ }
@@ -0,0 +1,36 @@
1
+ import { MatchDecorator, Decoration } from '@codemirror/view'
2
+ import type { Suggestion } from './types.js'
3
+ import { ChipWidget } from './PlaceholderWidget.js'
4
+
5
+ /**
6
+ * A class that specializes in creating and managing decorations for suggestion matches in a code editor.
7
+ */
8
+ export class SuggestionMatchDecorator extends MatchDecorator {
9
+ /**
10
+ * Creates a new instance of SuggestionMatchDecorator.
11
+ * @param regexp - The regular expression used to match suggestions in the code.
12
+ * @param suggestions - An array of suggestions that will be used to create decorations.
13
+ * Each suggestion should have a unique `id` and a `label` that will be displayed in the editor.
14
+ */
15
+ constructor(
16
+ regexp: RegExp,
17
+ public suggestions: Suggestion[]
18
+ ) {
19
+ super({
20
+ regexp,
21
+ decoration: (match) => this.#decoration(match),
22
+ })
23
+ }
24
+
25
+ #decoration(match: RegExpExecArray): Decoration {
26
+ const label = match[1]
27
+ const suggestion = this.suggestions.find((s) => s.label === label)
28
+
29
+ // If no suggestion is found, create a default one
30
+ const suggestionData: Suggestion = suggestion || { id: label, label }
31
+
32
+ return Decoration.replace({
33
+ widget: new ChipWidget(suggestionData),
34
+ })
35
+ }
36
+ }
@@ -0,0 +1,28 @@
1
+ import type { FunctionSchema } from '@pawel-up/jexl/schemas/types.js'
2
+
3
+ export interface Suggestion {
4
+ /** Unique identifier for the suggestion */
5
+ id: string
6
+ /** Main label displayed */
7
+ label: string
8
+ /** Supporting description text */
9
+ description?: string
10
+ /** Suffix text (e.g., type, category) */
11
+ suffix?: string
12
+ /** Additional data associated with the suggestion */
13
+ data?: Record<string, unknown>
14
+ }
15
+
16
+ export interface FunctionInsertEvent {
17
+ /** The inserted function schema */
18
+ functionSchema: FunctionSchema
19
+ /** The position where the function was inserted */
20
+ position: number
21
+ }
22
+
23
+ export interface SuggestionInsertEvent {
24
+ /** The inserted suggestion */
25
+ suggestion: Suggestion
26
+ /** The position where the suggestion was inserted */
27
+ position: number
28
+ }
package/src/index.ts CHANGED
@@ -5,14 +5,8 @@ export { default as PrismHighlightElement } from './elements/highlight/PrismHigh
5
5
  export { default as HarViewerElement } from './elements/har/HarViewer.js'
6
6
 
7
7
  // Code Editor
8
- export { default as CodeEditorElement } from './elements/code-editor/ui-code-editor.js'
9
- export type {
10
- FunctionSchema,
11
- FunctionParameter,
12
- Suggestion,
13
- FunctionInsertEvent,
14
- SuggestionInsertEvent,
15
- } from './elements/code-editor/ui-code-editor.js'
8
+ export { default as CodeEditorElement } from './elements/code-editor/code-editor.js'
9
+ export type { Suggestion, FunctionInsertEvent, SuggestionInsertEvent } from './elements/code-editor/code-editor.js'
16
10
 
17
11
  // Menu Components
18
12
  export { default as Menu } from './md/menu/internal/Menu.js'
@@ -1 +0,0 @@
1
- {"version":3,"file":"ui-code-editor.d.ts","sourceRoot":"","sources":["../../../../src/elements/code-editor/ui-code-editor.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,KAAK,CAAA;AAE5C,OAAO,OAAO,MAAM,2BAA2B,CAAA;AAG/C,qBACa,iBAAkB,SAAQ,OAAO;IAC5C,OAAgB,MAAM,EAAE,iBAAiB,EAAE,CAAW;CACvD;AAED,OAAO,CAAC,MAAM,CAAC;IACb,UAAU,qBAAqB;QAC7B,aAAa,EAAE,iBAAiB,CAAA;KACjC;CACF;AAED,eAAe,iBAAiB,CAAA;AAChC,YAAY,EACV,cAAc,EACd,iBAAiB,EACjB,UAAU,EACV,mBAAmB,EACnB,qBAAqB,GACtB,MAAM,2BAA2B,CAAA"}