@api-client/ui 0.5.11 → 0.5.13

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 (49) hide show
  1. package/build/src/elements/code-editor/code-editor.d.ts +13 -0
  2. package/build/src/elements/code-editor/code-editor.d.ts.map +1 -0
  3. package/build/src/elements/code-editor/code-editor.js +28 -0
  4. package/build/src/elements/code-editor/code-editor.js.map +1 -0
  5. package/build/src/elements/code-editor/internals/CodeEditor.d.ts +159 -0
  6. package/build/src/elements/code-editor/internals/CodeEditor.d.ts.map +1 -0
  7. package/build/src/elements/code-editor/internals/CodeEditor.js +643 -0
  8. package/build/src/elements/code-editor/internals/CodeEditor.js.map +1 -0
  9. package/build/src/elements/code-editor/internals/CodeEditor.styles.d.ts +3 -0
  10. package/build/src/elements/code-editor/internals/CodeEditor.styles.d.ts.map +1 -0
  11. package/build/src/elements/code-editor/internals/CodeEditor.styles.js +154 -0
  12. package/build/src/elements/code-editor/internals/CodeEditor.styles.js.map +1 -0
  13. package/build/src/elements/code-editor/internals/Linter.d.ts +5 -0
  14. package/build/src/elements/code-editor/internals/Linter.d.ts.map +1 -0
  15. package/build/src/elements/code-editor/internals/Linter.js +69 -0
  16. package/build/src/elements/code-editor/internals/Linter.js.map +1 -0
  17. package/build/src/elements/code-editor/internals/PlaceholderWidget.d.ts +20 -0
  18. package/build/src/elements/code-editor/internals/PlaceholderWidget.d.ts.map +1 -0
  19. package/build/src/elements/code-editor/internals/PlaceholderWidget.js +46 -0
  20. package/build/src/elements/code-editor/internals/PlaceholderWidget.js.map +1 -0
  21. package/build/src/elements/code-editor/internals/SuggestionMatchDecorator.d.ts +17 -0
  22. package/build/src/elements/code-editor/internals/SuggestionMatchDecorator.d.ts.map +1 -0
  23. package/build/src/elements/code-editor/internals/SuggestionMatchDecorator.js +31 -0
  24. package/build/src/elements/code-editor/internals/SuggestionMatchDecorator.js.map +1 -0
  25. package/build/src/elements/code-editor/internals/types.d.ts +51 -0
  26. package/build/src/elements/code-editor/internals/types.d.ts.map +1 -0
  27. package/build/src/elements/code-editor/internals/types.js +2 -0
  28. package/build/src/elements/code-editor/internals/types.js.map +1 -0
  29. package/build/src/index.d.ts +2 -0
  30. package/build/src/index.d.ts.map +1 -1
  31. package/build/src/index.js +2 -0
  32. package/build/src/index.js.map +1 -1
  33. package/build/src/md/chip/internals/Chip.styles.d.ts.map +1 -1
  34. package/build/src/md/chip/internals/Chip.styles.js +1 -0
  35. package/build/src/md/chip/internals/Chip.styles.js.map +1 -1
  36. package/demo/elements/code-editor/CodeEditorDemo.ts +212 -0
  37. package/demo/elements/code-editor/index.html +19 -0
  38. package/demo/elements/index.html +3 -0
  39. package/package.json +10 -2
  40. package/src/elements/code-editor/README.md +204 -0
  41. package/src/elements/code-editor/code-editor.ts +24 -0
  42. package/src/elements/code-editor/internals/CodeEditor.styles.ts +154 -0
  43. package/src/elements/code-editor/internals/CodeEditor.ts +589 -0
  44. package/src/elements/code-editor/internals/Linter.ts +85 -0
  45. package/src/elements/code-editor/internals/PlaceholderWidget.ts +50 -0
  46. package/src/elements/code-editor/internals/SuggestionMatchDecorator.ts +36 -0
  47. package/src/elements/code-editor/internals/types.ts +54 -0
  48. package/src/index.ts +10 -0
  49. package/src/md/chip/internals/Chip.styles.ts +1 -0
@@ -0,0 +1,589 @@
1
+ import { html, LitElement, PropertyValues, TemplateResult, nothing } from 'lit'
2
+ import { property, query, state } from 'lit/decorators.js'
3
+ import { classMap } from 'lit/directives/class-map.js'
4
+ import { linter } from '@codemirror/lint'
5
+ import { EditorState, Extension } from '@codemirror/state'
6
+ import {
7
+ autocompletion,
8
+ CompletionContext,
9
+ type CompletionResult,
10
+ type CompletionSource,
11
+ type Completion,
12
+ } from '@codemirror/autocomplete'
13
+ import { javascript } from '@codemirror/lang-javascript'
14
+ import { syntaxHighlighting, defaultHighlightStyle } from '@codemirror/language'
15
+ import { oneDark } from '@codemirror/theme-one-dark'
16
+ import { keymap } from '@codemirror/view'
17
+ import { defaultKeymap } from '@codemirror/commands'
18
+ import {
19
+ EditorView,
20
+ Decoration,
21
+ DecorationSet,
22
+ ViewPlugin,
23
+ ViewUpdate,
24
+ hoverTooltip,
25
+ type Tooltip,
26
+ } from '@codemirror/view'
27
+ import { functionLinter } from './Linter.js'
28
+ import type { FunctionInsertEvent, FunctionSchema, Suggestion, SuggestionInsertEvent } from './types.js'
29
+ import { SuggestionMatchDecorator } from './SuggestionMatchDecorator.js'
30
+
31
+ /**
32
+ * A CodeMirror 6 based editor component that supports function autocomplete and suggestion placeholders.
33
+ *
34
+ * Features:
35
+ * - Dynamic function schema loading
36
+ * - Autocomplete for functions and suggestions
37
+ * - Suggestion placeholders with double curly braces ({{suggestion}})
38
+ * - Keyboard navigation
39
+ * - Accessibility support
40
+ *
41
+ * @fires function-insert - When a function is inserted
42
+ * @fires suggestion-insert - When a suggestion is inserted
43
+ * @fires input - When the editor content changes
44
+ * @fires change - When the editor loses focus and content has changed
45
+ */
46
+ export default class CodeEditor extends LitElement {
47
+ /**
48
+ * Shadow root configuration for the component.
49
+ * Uses 'open' mode for accessibility and delegates focus to enable proper focus management.
50
+ */
51
+ static override shadowRootOptions: ShadowRootInit = {
52
+ mode: 'open',
53
+ delegatesFocus: true,
54
+ }
55
+
56
+ /**
57
+ * The label text displayed as placeholder/floating label
58
+ */
59
+ @property({ type: String })
60
+ accessor label = ''
61
+
62
+ /**
63
+ * Supporting text displayed below the editor
64
+ */
65
+ @property({ type: String, attribute: 'supporting-text' })
66
+ accessor supportingText = ''
67
+
68
+ /**
69
+ * Whether the component is disabled
70
+ */
71
+ @property({ type: Boolean, reflect: true })
72
+ accessor disabled = false
73
+
74
+ /**
75
+ * Whether the component is in an invalid state
76
+ */
77
+ @property({ type: Boolean, reflect: true })
78
+ accessor invalid = false
79
+
80
+ /**
81
+ * The name attribute for form integration
82
+ */
83
+ @property({ type: String })
84
+ accessor name = ''
85
+
86
+ /**
87
+ * Whether the input is required
88
+ */
89
+ @property({ type: Boolean })
90
+ accessor required = false
91
+
92
+ /**
93
+ * Placeholder text shown when editor is empty
94
+ */
95
+ @property({ type: String })
96
+ accessor placeholder = ''
97
+
98
+ /**
99
+ * The editor content value
100
+ */
101
+ @property({ type: String })
102
+ accessor value = ''
103
+
104
+ /**
105
+ * Available function schemas for autocomplete
106
+ */
107
+ @property({ type: Array, attribute: false })
108
+ accessor functionSchemas: FunctionSchema[] = []
109
+
110
+ /**
111
+ * Available suggestions for autocomplete
112
+ */
113
+ @property({ type: Array, attribute: false })
114
+ accessor suggestions: Suggestion[] = []
115
+
116
+ /**
117
+ * Whether to use dark theme
118
+ */
119
+ @property({ type: Boolean, attribute: 'dark-theme' })
120
+ accessor darkTheme = false
121
+
122
+ /**
123
+ * Programming language for syntax highlighting
124
+ */
125
+ @property({ type: String })
126
+ accessor language = 'javascript'
127
+
128
+ @query('.editor-container')
129
+ private accessor editorContainer!: HTMLDivElement
130
+
131
+ @state()
132
+ private accessor hasContent = false
133
+
134
+ @state()
135
+ private accessor isEditorFocus = false
136
+
137
+ private editorView?: EditorView
138
+ private _previousValue = ''
139
+ /**
140
+ * Matcher for suggestion placeholders in the editor.
141
+ */
142
+ placeholderMatcher?: SuggestionMatchDecorator
143
+
144
+ /**
145
+ * Get all suggestions (placeholders) currently in the editor
146
+ */
147
+ get activeSuggestions(): Suggestion[] {
148
+ const suggestions: Suggestion[] = []
149
+ const placeholderPattern = /\{\{(\w+)\}\}/g
150
+ let match: RegExpExecArray | null
151
+ while ((match = placeholderPattern.exec(this.value)) !== null) {
152
+ const placeholderText = match[1]
153
+ // Find suggestion by label
154
+ const suggestion = this.suggestions.find((s) => s.label.toLowerCase() === placeholderText.toLowerCase())
155
+ if (suggestion) {
156
+ suggestions.push(suggestion)
157
+ }
158
+ }
159
+ return suggestions
160
+ }
161
+
162
+ override disconnectedCallback(): void {
163
+ super.disconnectedCallback()
164
+ this.editorView?.destroy()
165
+ }
166
+
167
+ override firstUpdated(): void {
168
+ this.initializeCodeMirror()
169
+ this.updateEditorContent()
170
+ }
171
+
172
+ override willUpdate(changedProperties: PropertyValues): void {
173
+ super.willUpdate(changedProperties)
174
+
175
+ if (changedProperties.has('suggestions')) {
176
+ if (this.placeholderMatcher) {
177
+ this.placeholderMatcher.suggestions = this.suggestions || []
178
+ }
179
+ }
180
+
181
+ if (changedProperties.has('value') && this.editorView) {
182
+ this.updateEditorContent()
183
+ }
184
+
185
+ if (changedProperties.has('disabled')) {
186
+ this.updateEditorState()
187
+ }
188
+ }
189
+
190
+ /**
191
+ * Initialize CodeMirror editor with extensions
192
+ */
193
+ private initializeCodeMirror(): void {
194
+ const extensions: Extension[] = [
195
+ keymap.of(defaultKeymap),
196
+ syntaxHighlighting(defaultHighlightStyle),
197
+ autocompletion({
198
+ override: [this.createCompletionSource()],
199
+ activateOnTyping: true,
200
+ maxRenderedOptions: 10,
201
+ }),
202
+ EditorView.updateListener.of((update: ViewUpdate) => {
203
+ if (update.docChanged) {
204
+ this.handleEditorChange()
205
+ }
206
+ if (update.focusChanged) {
207
+ this.handleFocusChange(update.view.hasFocus)
208
+ }
209
+ }),
210
+ this.createPlaceholderPlugin(),
211
+ hoverTooltip(this.createHoverTooltipSource),
212
+ linter((view) => functionLinter(view, this.functionSchemas, (e) => this.dispatchEvent(e))),
213
+ ]
214
+
215
+ // Add language support
216
+ if (this.language === 'javascript') {
217
+ extensions.push(javascript())
218
+ }
219
+
220
+ // Add theme
221
+ if (this.darkTheme) {
222
+ extensions.push(oneDark)
223
+ }
224
+
225
+ // Add placeholder
226
+ if (this.placeholder) {
227
+ extensions.push(
228
+ EditorView.contentAttributes.of({
229
+ 'aria-placeholder': this.placeholder,
230
+ })
231
+ )
232
+ }
233
+
234
+ const state = EditorState.create({
235
+ doc: this.value,
236
+ extensions,
237
+ })
238
+
239
+ this.editorView = new EditorView({
240
+ state,
241
+ parent: this.editorContainer,
242
+ })
243
+ }
244
+
245
+ /**
246
+ * Creates the ViewPlugin for rendering suggestion placeholders.
247
+ * This is created as a method to get access to the component's `suggestions`.
248
+ */
249
+ private createPlaceholderPlugin(): Extension {
250
+ const placeholderMatcher = new SuggestionMatchDecorator(/\{\{(\w+)\}\}/g, this.suggestions)
251
+ this.placeholderMatcher = placeholderMatcher
252
+ // This class needs to be defined here to have access to the `placeholderMatcher`
253
+ class AtomicDecorationRange {
254
+ placeholders: DecorationSet
255
+ constructor(view: EditorView) {
256
+ this.placeholders = placeholderMatcher.createDeco(view)
257
+ }
258
+ update(update: ViewUpdate) {
259
+ this.placeholders = placeholderMatcher.updateDeco(update, this.placeholders)
260
+ }
261
+ }
262
+
263
+ return ViewPlugin.fromClass(AtomicDecorationRange, {
264
+ decorations: (instance) => instance.placeholders,
265
+ provide: (plugin) => EditorView.atomicRanges.of((view) => view.plugin(plugin)?.placeholders || Decoration.none),
266
+ })
267
+ }
268
+
269
+ /**
270
+ * Create completion source for functions and suggestions
271
+ */
272
+ private createCompletionSource(): CompletionSource {
273
+ return (context: CompletionContext): CompletionResult | null => {
274
+ const { pos } = context
275
+ const line = context.state.doc.lineAt(pos)
276
+ const textBefore = line.text.slice(0, pos - line.from)
277
+
278
+ // Check if we're typing a function name
279
+ const functionMatch = textBefore.match(/(\w+)$/)
280
+ if (functionMatch) {
281
+ const prefix = functionMatch[1]
282
+ const functions = this.functionSchemas
283
+ .filter((fn) => fn.name.toLowerCase().startsWith(prefix.toLowerCase()))
284
+ .map((fn) => this.createFunctionCompletion(fn))
285
+
286
+ if (functions.length > 0) {
287
+ return {
288
+ from: pos - prefix.length,
289
+ options: functions,
290
+ }
291
+ }
292
+ }
293
+
294
+ // Check if we're typing a suggestion trigger (e.g., {{)
295
+ const suggestionMatch = textBefore.match(/\{\{(\w*)$/)
296
+ if (suggestionMatch) {
297
+ const prefix = suggestionMatch[1]
298
+ const suggestions = this.suggestions
299
+ .filter((suggestion) => suggestion.label.toLowerCase().startsWith(prefix.toLowerCase()))
300
+ .map((suggestion) => this.createSuggestionCompletion(suggestion))
301
+
302
+ if (suggestions.length > 0) {
303
+ return {
304
+ from: pos - prefix.length,
305
+ options: suggestions,
306
+ }
307
+ }
308
+ }
309
+
310
+ return null
311
+ }
312
+ }
313
+
314
+ private createFunctionCompletion(schema: FunctionSchema): Completion {
315
+ const result: Completion = {
316
+ label: schema.name,
317
+ detail: schema.description || '',
318
+ info: () => this.createFunctionInfoElement(schema),
319
+ apply: (view: EditorView, completion: unknown, from: number, to: number) => {
320
+ const functionCall = this.formatFunctionCall(schema)
321
+ view.dispatch({
322
+ changes: { from, to, insert: functionCall },
323
+ selection: { anchor: from + functionCall.length },
324
+ })
325
+ this.dispatchFunctionInsert(schema, from)
326
+ },
327
+ }
328
+
329
+ return result
330
+ }
331
+
332
+ private createSuggestionCompletion(suggestion: Suggestion): Completion {
333
+ const result: Completion = {
334
+ label: suggestion.label,
335
+ detail: suggestion.description || '',
336
+ info: suggestion.suffix || '',
337
+ apply: (view: EditorView, completion: unknown, from: number, to: number) => {
338
+ const placeholderText = `{{${suggestion.label}}}`
339
+ view.dispatch({
340
+ changes: { from: from - 2, to, insert: placeholderText }, // -2 to include the {{
341
+ selection: { anchor: from - 2 + placeholderText.length },
342
+ })
343
+ this.dispatchSuggestionInsert(suggestion, from - 2)
344
+ },
345
+ }
346
+
347
+ return result
348
+ }
349
+
350
+ /**
351
+ * Creates the source for the hover tooltips.
352
+ * This is an arrow function to automatically bind `this`.
353
+ */
354
+ private createHoverTooltipSource = (view: EditorView, pos: number): Tooltip | null => {
355
+ const word = view.state.wordAt(pos)
356
+ if (!word) {
357
+ return null
358
+ }
359
+
360
+ const functionName = view.state.doc.sliceString(word.from, word.to)
361
+ const fnSchema = this.functionSchemas.find((schema) => schema.name === functionName)
362
+
363
+ if (!fnSchema) {
364
+ return null
365
+ }
366
+
367
+ return {
368
+ pos: word.from,
369
+ end: word.to,
370
+ above: true,
371
+ create: () => ({ dom: this.createFunctionInfoElement(fnSchema) }),
372
+ }
373
+ }
374
+
375
+ /**
376
+ * Creates a styled HTML element to display function documentation.
377
+ * This is used for both hover tooltips and autocomplete info panels.
378
+ */
379
+ private createFunctionInfoElement(fn: FunctionSchema): HTMLElement {
380
+ const container = document.createElement('div')
381
+ container.className = 'function-info' // for styling
382
+
383
+ if (fn.description) {
384
+ const description = document.createElement('p')
385
+ description.className = 'description'
386
+ description.textContent = fn.description
387
+ container.appendChild(description)
388
+ }
389
+
390
+ if (fn.parameters && fn.parameters.length > 0) {
391
+ const paramsContainer = document.createElement('div')
392
+ paramsContainer.className = 'parameters'
393
+
394
+ const paramsHeader = document.createElement('h4')
395
+ paramsHeader.textContent = 'Parameters'
396
+ paramsContainer.appendChild(paramsHeader)
397
+
398
+ const paramsList = document.createElement('ul')
399
+ fn.parameters.forEach((param) => {
400
+ const listItem = document.createElement('li')
401
+
402
+ const name = document.createElement('span')
403
+ name.className = 'param-name'
404
+ name.textContent = param.name
405
+
406
+ const type = document.createElement('span')
407
+ type.className = 'param-type'
408
+ type.textContent = `: ${param.type}`
409
+
410
+ listItem.appendChild(name)
411
+ listItem.appendChild(type)
412
+
413
+ if (param.description) {
414
+ const paramDesc = document.createElement('p')
415
+ paramDesc.className = 'param-description'
416
+ paramDesc.textContent = param.description
417
+ listItem.appendChild(paramDesc)
418
+ }
419
+ paramsList.appendChild(listItem)
420
+ })
421
+ paramsContainer.appendChild(paramsList)
422
+ container.appendChild(paramsContainer)
423
+ }
424
+
425
+ if (fn.returns) {
426
+ const returnsContainer = document.createElement('div')
427
+ returnsContainer.className = 'returns'
428
+
429
+ const returnsHeader = document.createElement('h4')
430
+ returnsHeader.textContent = 'Returns'
431
+ returnsContainer.appendChild(returnsHeader)
432
+
433
+ const returnsDesc = document.createElement('p')
434
+ returnsDesc.textContent = fn.returns
435
+ returnsContainer.appendChild(returnsDesc)
436
+ container.appendChild(returnsContainer)
437
+ }
438
+
439
+ return container
440
+ }
441
+
442
+ /**
443
+ * Format function call with parameters
444
+ */
445
+ private formatFunctionCall(fn: FunctionSchema): string {
446
+ if (!fn.parameters || fn.parameters.length === 0) {
447
+ return `${fn.name}()`
448
+ }
449
+
450
+ const params = fn.parameters
451
+ .map((param) => {
452
+ if (param.required) {
453
+ return param.name
454
+ }
455
+ return `${param.name}?`
456
+ })
457
+ .join(', ')
458
+
459
+ return `${fn.name}(${params})`
460
+ }
461
+
462
+ /**
463
+ * Update editor content when value changes
464
+ */
465
+ private updateEditorContent(): void {
466
+ if (!this.editorView) return
467
+
468
+ const currentValue = this.editorView.state.doc.toString()
469
+ if (currentValue !== this.value) {
470
+ this.editorView.dispatch({
471
+ changes: {
472
+ from: 0,
473
+ to: this.editorView.state.doc.length,
474
+ insert: this.value,
475
+ },
476
+ })
477
+ }
478
+ }
479
+
480
+ /**
481
+ * Update editor state (e.g., disabled state)
482
+ */
483
+ private updateEditorState(): void {
484
+ if (!this.editorView) return
485
+
486
+ // For now, we'll handle disabled state differently
487
+ // CodeMirror 6 doesn't use reconfigure for editable
488
+ if (this.disabled) {
489
+ this.editorView.contentDOM.setAttribute('contenteditable', 'false')
490
+ } else {
491
+ this.editorView.contentDOM.setAttribute('contenteditable', 'true')
492
+ }
493
+ }
494
+
495
+ /**
496
+ * Handle editor content change
497
+ */
498
+ private handleEditorChange(): void {
499
+ if (!this.editorView) return
500
+
501
+ const newValue = this.editorView.state.doc.toString()
502
+ if (newValue !== this._previousValue) {
503
+ this._previousValue = newValue
504
+ this.value = newValue
505
+ this.hasContent = newValue.length > 0
506
+
507
+ this.dispatchEvent(new Event('input', { bubbles: true }))
508
+ }
509
+ }
510
+
511
+ /**
512
+ * Handle focus change
513
+ */
514
+ private handleFocusChange(hasFocus: boolean): void {
515
+ this.isEditorFocus = hasFocus
516
+
517
+ if (!hasFocus && this.value !== this._previousValue) {
518
+ this.dispatchEvent(new Event('change', { bubbles: true }))
519
+ }
520
+ }
521
+
522
+ /**
523
+ * Dispatch function insert event
524
+ */
525
+ private dispatchFunctionInsert(functionSchema: FunctionSchema, position: number): void {
526
+ const event = new CustomEvent<FunctionInsertEvent>('function-insert', {
527
+ detail: { functionSchema, position },
528
+ bubbles: true,
529
+ })
530
+ this.dispatchEvent(event)
531
+ }
532
+
533
+ /**
534
+ * Dispatch suggestion insert event
535
+ */
536
+ private dispatchSuggestionInsert(suggestion: Suggestion, position: number): void {
537
+ const event = new CustomEvent<SuggestionInsertEvent>('suggestion-insert', {
538
+ detail: { suggestion, position },
539
+ bubbles: true,
540
+ })
541
+ this.dispatchEvent(event)
542
+ }
543
+
544
+ /**
545
+ * Focus the editor
546
+ */
547
+ override focus(): void {
548
+ this.editorView?.focus()
549
+ }
550
+
551
+ /**
552
+ * Get the editor's current selection
553
+ */
554
+ getSelection(): { from: number; to: number } | null {
555
+ if (!this.editorView) return null
556
+ const { from, to } = this.editorView.state.selection.main
557
+ return { from, to }
558
+ }
559
+
560
+ /**
561
+ * Insert text at the current cursor position
562
+ */
563
+ insertText(text: string): void {
564
+ if (!this.editorView) return
565
+
566
+ const { from, to } = this.editorView.state.selection.main
567
+ this.editorView.dispatch({
568
+ changes: { from, to, insert: text },
569
+ selection: { anchor: from + text.length },
570
+ })
571
+ }
572
+
573
+ override render(): TemplateResult {
574
+ const hasLabel = !!this.label
575
+ const hasSupportingText = !!this.supportingText
576
+
577
+ return html`
578
+ <div class="surface ${classMap({ 'has-focus': this.isEditorFocus, 'invalid': this.invalid })}">
579
+ <div class="content">
580
+ ${hasLabel ? html`<div class="label">${this.label}</div>` : nothing}
581
+
582
+ <div class="editor-container" part="editor"></div>
583
+ </div>
584
+
585
+ ${hasSupportingText ? html`<div class="supporting-text">${this.supportingText}</div>` : nothing}
586
+ </div>
587
+ `
588
+ }
589
+ }
@@ -0,0 +1,85 @@
1
+ import type { Diagnostic } from '@codemirror/lint'
2
+ import type { EditorView } from '@codemirror/view'
3
+ import type { FunctionSchema } from './types.js'
4
+
5
+ export function functionLinter(
6
+ view: EditorView,
7
+ functionSchemas: readonly FunctionSchema[],
8
+ dispatchEvent: (e: CustomEvent) => void
9
+ ): readonly Diagnostic[] {
10
+ const diagnostics: Diagnostic[] = []
11
+ const doc = view.state.doc
12
+ const text = doc.toString()
13
+
14
+ const functions = functionSchemas
15
+ if (!functions || functions.length === 0) {
16
+ return diagnostics
17
+ }
18
+
19
+ // Regular expression to match function calls like functionName(args)
20
+ const functionCallRegex = /(\w+)\s*\(/g
21
+ let match: RegExpExecArray | null
22
+
23
+ while ((match = functionCallRegex.exec(text)) !== null) {
24
+ const functionName = match[1]
25
+ const startPos = match.index
26
+ const endPos = startPos + functionName.length
27
+
28
+ // Check if this function exists in our schemas
29
+ const fn = functions.find((schema) => schema.name === functionName)
30
+
31
+ if (!fn) {
32
+ diagnostics.push({
33
+ from: startPos,
34
+ to: endPos,
35
+ severity: 'error',
36
+ message: `Unknown function "${functionName}". Make sure you have the correct syntax for the function call.`,
37
+ actions: [
38
+ {
39
+ name: 'View available functions',
40
+ apply: () => {
41
+ dispatchEvent(
42
+ new CustomEvent('show-available-functions', {
43
+ detail: { availableFunctions: [...functions] },
44
+ bubbles: true,
45
+ })
46
+ )
47
+ },
48
+ },
49
+ ],
50
+ })
51
+ } else {
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()) : []
57
+
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
+ }
69
+
70
+ // Check if too many parameters
71
+ if (fn.parameters && args.length > fn.parameters.length) {
72
+ const functionEndPos = startPos + functionCallMatch[0].length
73
+ diagnostics.push({
74
+ from: startPos,
75
+ to: functionEndPos,
76
+ severity: 'warning',
77
+ message: `Function "${functionName}" expects at most ${fn.parameters.length} parameters but got ${args.length}`,
78
+ })
79
+ }
80
+ }
81
+ }
82
+ }
83
+
84
+ return diagnostics
85
+ }