@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.
- package/build/src/elements/code-editor/code-editor.d.ts +13 -0
- package/build/src/elements/code-editor/code-editor.d.ts.map +1 -0
- package/build/src/elements/code-editor/code-editor.js +28 -0
- package/build/src/elements/code-editor/code-editor.js.map +1 -0
- package/build/src/elements/code-editor/internals/CodeEditor.d.ts +159 -0
- package/build/src/elements/code-editor/internals/CodeEditor.d.ts.map +1 -0
- package/build/src/elements/code-editor/internals/CodeEditor.js +643 -0
- package/build/src/elements/code-editor/internals/CodeEditor.js.map +1 -0
- package/build/src/elements/code-editor/internals/CodeEditor.styles.d.ts +3 -0
- package/build/src/elements/code-editor/internals/CodeEditor.styles.d.ts.map +1 -0
- package/build/src/elements/code-editor/internals/CodeEditor.styles.js +154 -0
- package/build/src/elements/code-editor/internals/CodeEditor.styles.js.map +1 -0
- package/build/src/elements/code-editor/internals/Linter.d.ts +5 -0
- package/build/src/elements/code-editor/internals/Linter.d.ts.map +1 -0
- package/build/src/elements/code-editor/internals/Linter.js +69 -0
- package/build/src/elements/code-editor/internals/Linter.js.map +1 -0
- package/build/src/elements/code-editor/internals/PlaceholderWidget.d.ts +20 -0
- package/build/src/elements/code-editor/internals/PlaceholderWidget.d.ts.map +1 -0
- package/build/src/elements/code-editor/internals/PlaceholderWidget.js +46 -0
- package/build/src/elements/code-editor/internals/PlaceholderWidget.js.map +1 -0
- package/build/src/elements/code-editor/internals/SuggestionMatchDecorator.d.ts +17 -0
- package/build/src/elements/code-editor/internals/SuggestionMatchDecorator.d.ts.map +1 -0
- package/build/src/elements/code-editor/internals/SuggestionMatchDecorator.js +31 -0
- package/build/src/elements/code-editor/internals/SuggestionMatchDecorator.js.map +1 -0
- package/build/src/elements/code-editor/internals/types.d.ts +51 -0
- package/build/src/elements/code-editor/internals/types.d.ts.map +1 -0
- package/build/src/elements/code-editor/internals/types.js +2 -0
- package/build/src/elements/code-editor/internals/types.js.map +1 -0
- package/build/src/index.d.ts +2 -0
- package/build/src/index.d.ts.map +1 -1
- package/build/src/index.js +2 -0
- package/build/src/index.js.map +1 -1
- package/build/src/md/chip/internals/Chip.styles.d.ts.map +1 -1
- package/build/src/md/chip/internals/Chip.styles.js +1 -0
- package/build/src/md/chip/internals/Chip.styles.js.map +1 -1
- package/demo/elements/code-editor/CodeEditorDemo.ts +212 -0
- package/demo/elements/code-editor/index.html +19 -0
- package/demo/elements/index.html +3 -0
- package/package.json +10 -2
- package/src/elements/code-editor/README.md +204 -0
- package/src/elements/code-editor/code-editor.ts +24 -0
- package/src/elements/code-editor/internals/CodeEditor.styles.ts +154 -0
- package/src/elements/code-editor/internals/CodeEditor.ts +589 -0
- package/src/elements/code-editor/internals/Linter.ts +85 -0
- package/src/elements/code-editor/internals/PlaceholderWidget.ts +50 -0
- package/src/elements/code-editor/internals/SuggestionMatchDecorator.ts +36 -0
- package/src/elements/code-editor/internals/types.ts +54 -0
- package/src/index.ts +10 -0
- 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
|
+
}
|