@api-client/ui 0.5.10 → 0.5.12
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/internals/CodeEditor.d.ts +197 -0
- package/build/src/elements/code-editor/internals/CodeEditor.d.ts.map +1 -0
- package/build/src/elements/code-editor/internals/CodeEditor.js +612 -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 +150 -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 +74 -0
- package/build/src/elements/code-editor/internals/Linter.js.map +1 -0
- package/build/src/elements/code-editor/ui-code-editor.d.ts +13 -0
- package/build/src/elements/code-editor/ui-code-editor.d.ts.map +1 -0
- package/build/src/elements/code-editor/ui-code-editor.js +28 -0
- package/build/src/elements/code-editor/ui-code-editor.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 +11 -3
- package/src/elements/code-editor/README.md +204 -0
- package/src/elements/code-editor/internals/CodeEditor.styles.ts +150 -0
- package/src/elements/code-editor/internals/CodeEditor.ts +595 -0
- package/src/elements/code-editor/internals/Linter.ts +87 -0
- package/src/elements/code-editor/ui-code-editor.ts +24 -0
- package/src/index.ts +10 -0
- package/src/md/chip/internals/Chip.styles.ts +1 -0
|
@@ -0,0 +1,595 @@
|
|
|
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 { EditorState, Extension } from '@codemirror/state'
|
|
5
|
+
import { autocompletion, CompletionContext, CompletionResult, CompletionSource } from '@codemirror/autocomplete'
|
|
6
|
+
import { javascript } from '@codemirror/lang-javascript'
|
|
7
|
+
import { syntaxHighlighting, defaultHighlightStyle } from '@codemirror/language'
|
|
8
|
+
import { oneDark } from '@codemirror/theme-one-dark'
|
|
9
|
+
import { keymap } from '@codemirror/view'
|
|
10
|
+
import { defaultKeymap } from '@codemirror/commands'
|
|
11
|
+
import {
|
|
12
|
+
EditorView,
|
|
13
|
+
MatchDecorator,
|
|
14
|
+
Decoration,
|
|
15
|
+
DecorationSet,
|
|
16
|
+
ViewPlugin,
|
|
17
|
+
ViewUpdate,
|
|
18
|
+
WidgetType,
|
|
19
|
+
} 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
|
+
})
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* A CodeMirror 6 based editor component that supports function autocomplete and suggestion placeholders.
|
|
130
|
+
*
|
|
131
|
+
* Features:
|
|
132
|
+
* - Dynamic function schema loading
|
|
133
|
+
* - Autocomplete for functions and suggestions
|
|
134
|
+
* - Suggestion placeholders with double curly braces ({{suggestion}})
|
|
135
|
+
* - Keyboard navigation
|
|
136
|
+
* - Accessibility support
|
|
137
|
+
*
|
|
138
|
+
* @fires function-insert - When a function is inserted
|
|
139
|
+
* @fires suggestion-insert - When a suggestion is inserted
|
|
140
|
+
* @fires input - When the editor content changes
|
|
141
|
+
* @fires change - When the editor loses focus and content has changed
|
|
142
|
+
*/
|
|
143
|
+
export default class CodeEditor extends LitElement {
|
|
144
|
+
/**
|
|
145
|
+
* Shadow root configuration for the component.
|
|
146
|
+
* Uses 'open' mode for accessibility and delegates focus to enable proper focus management.
|
|
147
|
+
*/
|
|
148
|
+
static override shadowRootOptions: ShadowRootInit = {
|
|
149
|
+
mode: 'open',
|
|
150
|
+
delegatesFocus: true,
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* The label text displayed as placeholder/floating label
|
|
155
|
+
*/
|
|
156
|
+
@property({ type: String })
|
|
157
|
+
accessor label = ''
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Supporting text displayed below the editor
|
|
161
|
+
*/
|
|
162
|
+
@property({ type: String, attribute: 'supporting-text' })
|
|
163
|
+
accessor supportingText = ''
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Whether the component is disabled
|
|
167
|
+
*/
|
|
168
|
+
@property({ type: Boolean, reflect: true })
|
|
169
|
+
accessor disabled = false
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Whether the component is in an invalid state
|
|
173
|
+
*/
|
|
174
|
+
@property({ type: Boolean, reflect: true })
|
|
175
|
+
accessor invalid = false
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* The name attribute for form integration
|
|
179
|
+
*/
|
|
180
|
+
@property({ type: String })
|
|
181
|
+
accessor name = ''
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Whether the input is required
|
|
185
|
+
*/
|
|
186
|
+
@property({ type: Boolean })
|
|
187
|
+
accessor required = false
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Placeholder text shown when editor is empty
|
|
191
|
+
*/
|
|
192
|
+
@property({ type: String })
|
|
193
|
+
accessor placeholder = ''
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* The editor content value
|
|
197
|
+
*/
|
|
198
|
+
@property({ type: String })
|
|
199
|
+
accessor value = ''
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Available function schemas for autocomplete
|
|
203
|
+
*/
|
|
204
|
+
@property({ type: Array, attribute: false })
|
|
205
|
+
accessor functionSchemas: FunctionSchema[] = []
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Available suggestions for autocomplete
|
|
209
|
+
*/
|
|
210
|
+
@property({ type: Array, attribute: false })
|
|
211
|
+
accessor suggestions: Suggestion[] = []
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Whether to use dark theme
|
|
215
|
+
*/
|
|
216
|
+
@property({ type: Boolean, attribute: 'dark-theme' })
|
|
217
|
+
accessor darkTheme = false
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Programming language for syntax highlighting
|
|
221
|
+
*/
|
|
222
|
+
@property({ type: String })
|
|
223
|
+
accessor language = 'javascript'
|
|
224
|
+
|
|
225
|
+
@query('.editor-container')
|
|
226
|
+
private accessor editorContainer!: HTMLDivElement
|
|
227
|
+
|
|
228
|
+
@state()
|
|
229
|
+
private accessor hasContent = false
|
|
230
|
+
|
|
231
|
+
@state()
|
|
232
|
+
private accessor isEditorFocus = false
|
|
233
|
+
|
|
234
|
+
private editorView?: EditorView
|
|
235
|
+
private suggestionMap = new Map<string, Suggestion>()
|
|
236
|
+
private functionMap = new Map<string, FunctionSchema>()
|
|
237
|
+
private _previousValue = ''
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Get all suggestions (placeholders) currently in the editor
|
|
241
|
+
*/
|
|
242
|
+
get activeSuggestions(): Suggestion[] {
|
|
243
|
+
const suggestions: Suggestion[] = []
|
|
244
|
+
const placeholderPattern = /\{\{(\w+)\}\}/g
|
|
245
|
+
let match: RegExpExecArray | null
|
|
246
|
+
while ((match = placeholderPattern.exec(this.value)) !== null) {
|
|
247
|
+
const placeholderText = match[1]
|
|
248
|
+
// Find suggestion by label
|
|
249
|
+
const suggestion = this.suggestions.find((s) => s.label.toLowerCase() === placeholderText.toLowerCase())
|
|
250
|
+
if (suggestion) {
|
|
251
|
+
suggestions.push(suggestion)
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
return suggestions
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
override connectedCallback(): void {
|
|
258
|
+
super.connectedCallback()
|
|
259
|
+
this.setupSuggestionMaps()
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
override disconnectedCallback(): void {
|
|
263
|
+
super.disconnectedCallback()
|
|
264
|
+
this.editorView?.destroy()
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
override firstUpdated(): void {
|
|
268
|
+
this.initializeCodeMirror()
|
|
269
|
+
this.updateEditorContent()
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
override willUpdate(changedProperties: PropertyValues): void {
|
|
273
|
+
super.willUpdate(changedProperties)
|
|
274
|
+
|
|
275
|
+
if (changedProperties.has('suggestions') || changedProperties.has('functionSchemas')) {
|
|
276
|
+
this.setupSuggestionMaps()
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
if (changedProperties.has('value') && this.editorView) {
|
|
280
|
+
this.updateEditorContent()
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
if (changedProperties.has('disabled')) {
|
|
284
|
+
this.updateEditorState()
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* Initialize CodeMirror editor with extensions
|
|
290
|
+
*/
|
|
291
|
+
private initializeCodeMirror(): void {
|
|
292
|
+
const extensions: Extension[] = [
|
|
293
|
+
keymap.of(defaultKeymap),
|
|
294
|
+
syntaxHighlighting(defaultHighlightStyle),
|
|
295
|
+
autocompletion({
|
|
296
|
+
override: [this.createCompletionSource()],
|
|
297
|
+
activateOnTyping: true,
|
|
298
|
+
maxRenderedOptions: 10,
|
|
299
|
+
}),
|
|
300
|
+
EditorView.updateListener.of((update: ViewUpdate) => {
|
|
301
|
+
if (update.docChanged) {
|
|
302
|
+
this.handleEditorChange()
|
|
303
|
+
}
|
|
304
|
+
if (update.focusChanged) {
|
|
305
|
+
this.handleFocusChange(update.view.hasFocus)
|
|
306
|
+
}
|
|
307
|
+
}),
|
|
308
|
+
placeholders,
|
|
309
|
+
// linter((view) => functionLinter(view as unknown as EditorView, this.functionSchemas)),
|
|
310
|
+
]
|
|
311
|
+
|
|
312
|
+
// Add language support
|
|
313
|
+
if (this.language === 'javascript') {
|
|
314
|
+
extensions.push(javascript())
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// Add theme
|
|
318
|
+
if (this.darkTheme) {
|
|
319
|
+
extensions.push(oneDark)
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
// Add placeholder
|
|
323
|
+
if (this.placeholder) {
|
|
324
|
+
extensions.push(
|
|
325
|
+
EditorView.contentAttributes.of({
|
|
326
|
+
'aria-placeholder': this.placeholder,
|
|
327
|
+
})
|
|
328
|
+
)
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
const state = EditorState.create({
|
|
332
|
+
doc: this.value,
|
|
333
|
+
extensions,
|
|
334
|
+
})
|
|
335
|
+
|
|
336
|
+
this.editorView = new EditorView({
|
|
337
|
+
state,
|
|
338
|
+
parent: this.editorContainer,
|
|
339
|
+
})
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
/**
|
|
343
|
+
* Create completion source for functions and suggestions
|
|
344
|
+
*/
|
|
345
|
+
private createCompletionSource(): CompletionSource {
|
|
346
|
+
return (context: CompletionContext): CompletionResult | null => {
|
|
347
|
+
const { pos } = context
|
|
348
|
+
const line = context.state.doc.lineAt(pos)
|
|
349
|
+
const textBefore = line.text.slice(0, pos - line.from)
|
|
350
|
+
|
|
351
|
+
// Check if we're typing a function name
|
|
352
|
+
const functionMatch = textBefore.match(/(\w+)$/)
|
|
353
|
+
if (functionMatch) {
|
|
354
|
+
const prefix = functionMatch[1]
|
|
355
|
+
const functions = this.functionSchemas
|
|
356
|
+
.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
|
+
}))
|
|
370
|
+
|
|
371
|
+
if (functions.length > 0) {
|
|
372
|
+
return {
|
|
373
|
+
from: pos - prefix.length,
|
|
374
|
+
options: functions,
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
// Check if we're typing a suggestion trigger (e.g., {{)
|
|
380
|
+
const suggestionMatch = textBefore.match(/\{\{(\w*)$/)
|
|
381
|
+
if (suggestionMatch) {
|
|
382
|
+
const prefix = suggestionMatch[1]
|
|
383
|
+
const suggestions = this.suggestions
|
|
384
|
+
.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
|
+
}))
|
|
398
|
+
|
|
399
|
+
if (suggestions.length > 0) {
|
|
400
|
+
return {
|
|
401
|
+
from: pos - prefix.length,
|
|
402
|
+
options: suggestions,
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
return null
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
/**
|
|
412
|
+
* Format function information for autocomplete
|
|
413
|
+
*/
|
|
414
|
+
private formatFunctionInfo(fn: FunctionSchema): string {
|
|
415
|
+
let info = fn.description || ''
|
|
416
|
+
if (fn.parameters && fn.parameters.length > 0) {
|
|
417
|
+
info += '\n\nParameters:\n'
|
|
418
|
+
fn.parameters.forEach((param) => {
|
|
419
|
+
info += `• ${param.name}: ${param.type}`
|
|
420
|
+
if (param.description) {
|
|
421
|
+
info += ` - ${param.description}`
|
|
422
|
+
}
|
|
423
|
+
info += '\n'
|
|
424
|
+
})
|
|
425
|
+
}
|
|
426
|
+
if (fn.returns) {
|
|
427
|
+
info += `\nReturns: ${fn.returns}`
|
|
428
|
+
}
|
|
429
|
+
return info
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
/**
|
|
433
|
+
* Format function call with parameters
|
|
434
|
+
*/
|
|
435
|
+
private formatFunctionCall(fn: FunctionSchema): string {
|
|
436
|
+
if (!fn.parameters || fn.parameters.length === 0) {
|
|
437
|
+
return `${fn.name}()`
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
const params = fn.parameters
|
|
441
|
+
.map((param) => {
|
|
442
|
+
if (param.required) {
|
|
443
|
+
return param.name
|
|
444
|
+
}
|
|
445
|
+
return `${param.name}?`
|
|
446
|
+
})
|
|
447
|
+
.join(', ')
|
|
448
|
+
|
|
449
|
+
return `${fn.name}(${params})`
|
|
450
|
+
}
|
|
451
|
+
|
|
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
|
+
/**
|
|
469
|
+
* Update editor content when value changes
|
|
470
|
+
*/
|
|
471
|
+
private updateEditorContent(): void {
|
|
472
|
+
if (!this.editorView) return
|
|
473
|
+
|
|
474
|
+
const currentValue = this.editorView.state.doc.toString()
|
|
475
|
+
if (currentValue !== this.value) {
|
|
476
|
+
this.editorView.dispatch({
|
|
477
|
+
changes: {
|
|
478
|
+
from: 0,
|
|
479
|
+
to: this.editorView.state.doc.length,
|
|
480
|
+
insert: this.value,
|
|
481
|
+
},
|
|
482
|
+
})
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
/**
|
|
487
|
+
* Update editor state (e.g., disabled state)
|
|
488
|
+
*/
|
|
489
|
+
private updateEditorState(): void {
|
|
490
|
+
if (!this.editorView) return
|
|
491
|
+
|
|
492
|
+
// For now, we'll handle disabled state differently
|
|
493
|
+
// CodeMirror 6 doesn't use reconfigure for editable
|
|
494
|
+
if (this.disabled) {
|
|
495
|
+
this.editorView.contentDOM.setAttribute('contenteditable', 'false')
|
|
496
|
+
} else {
|
|
497
|
+
this.editorView.contentDOM.setAttribute('contenteditable', 'true')
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
/**
|
|
502
|
+
* Handle editor content change
|
|
503
|
+
*/
|
|
504
|
+
private handleEditorChange(): void {
|
|
505
|
+
if (!this.editorView) return
|
|
506
|
+
|
|
507
|
+
const newValue = this.editorView.state.doc.toString()
|
|
508
|
+
if (newValue !== this._previousValue) {
|
|
509
|
+
this._previousValue = newValue
|
|
510
|
+
this.value = newValue
|
|
511
|
+
this.hasContent = newValue.length > 0
|
|
512
|
+
|
|
513
|
+
this.dispatchEvent(new Event('input', { bubbles: true }))
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
/**
|
|
518
|
+
* Handle focus change
|
|
519
|
+
*/
|
|
520
|
+
private handleFocusChange(hasFocus: boolean): void {
|
|
521
|
+
this.isEditorFocus = hasFocus
|
|
522
|
+
|
|
523
|
+
if (!hasFocus && this.value !== this._previousValue) {
|
|
524
|
+
this.dispatchEvent(new Event('change', { bubbles: true }))
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
/**
|
|
529
|
+
* Dispatch function insert event
|
|
530
|
+
*/
|
|
531
|
+
private dispatchFunctionInsert(functionSchema: FunctionSchema, position: number): void {
|
|
532
|
+
const event = new CustomEvent<FunctionInsertEvent>('function-insert', {
|
|
533
|
+
detail: { functionSchema, position },
|
|
534
|
+
bubbles: true,
|
|
535
|
+
})
|
|
536
|
+
this.dispatchEvent(event)
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
/**
|
|
540
|
+
* Dispatch suggestion insert event
|
|
541
|
+
*/
|
|
542
|
+
private dispatchSuggestionInsert(suggestion: Suggestion, position: number): void {
|
|
543
|
+
const event = new CustomEvent<SuggestionInsertEvent>('suggestion-insert', {
|
|
544
|
+
detail: { suggestion, position },
|
|
545
|
+
bubbles: true,
|
|
546
|
+
})
|
|
547
|
+
this.dispatchEvent(event)
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
/**
|
|
551
|
+
* Focus the editor
|
|
552
|
+
*/
|
|
553
|
+
override focus(): void {
|
|
554
|
+
this.editorView?.focus()
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
/**
|
|
558
|
+
* Get the editor's current selection
|
|
559
|
+
*/
|
|
560
|
+
getSelection(): { from: number; to: number } | null {
|
|
561
|
+
if (!this.editorView) return null
|
|
562
|
+
const { from, to } = this.editorView.state.selection.main
|
|
563
|
+
return { from, to }
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
/**
|
|
567
|
+
* Insert text at the current cursor position
|
|
568
|
+
*/
|
|
569
|
+
insertText(text: string): void {
|
|
570
|
+
if (!this.editorView) return
|
|
571
|
+
|
|
572
|
+
const { from, to } = this.editorView.state.selection.main
|
|
573
|
+
this.editorView.dispatch({
|
|
574
|
+
changes: { from, to, insert: text },
|
|
575
|
+
selection: { anchor: from + text.length },
|
|
576
|
+
})
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
override render(): TemplateResult {
|
|
580
|
+
const hasLabel = !!this.label
|
|
581
|
+
const hasSupportingText = !!this.supportingText
|
|
582
|
+
|
|
583
|
+
return html`
|
|
584
|
+
<div class="surface ${classMap({ 'has-focus': this.isEditorFocus, 'invalid': this.invalid })}">
|
|
585
|
+
<div class="content">
|
|
586
|
+
${hasLabel ? html`<div class="label">${this.label}</div>` : nothing}
|
|
587
|
+
|
|
588
|
+
<div class="editor-container" part="editor"></div>
|
|
589
|
+
</div>
|
|
590
|
+
|
|
591
|
+
${hasSupportingText ? html`<div class="supporting-text">${this.supportingText}</div>` : nothing}
|
|
592
|
+
</div>
|
|
593
|
+
`
|
|
594
|
+
}
|
|
595
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import type { Diagnostic } from '@codemirror/lint'
|
|
2
|
+
import type { EditorView } from '@codemirror/view'
|
|
3
|
+
import Element from './CodeEditor.js'
|
|
4
|
+
|
|
5
|
+
export function functionLinter(view: EditorView, element: Element): readonly Diagnostic[] {
|
|
6
|
+
const diagnostics: Diagnostic[] = []
|
|
7
|
+
const doc = view.state.doc
|
|
8
|
+
const text = doc.toString()
|
|
9
|
+
|
|
10
|
+
const functions = element.functionSchemas
|
|
11
|
+
if (!functions || functions.length === 0) {
|
|
12
|
+
return diagnostics
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// Regular expression to match function calls like functionName(args)
|
|
16
|
+
const functionCallRegex = /(\w+)\s*\(/g
|
|
17
|
+
let match: RegExpExecArray | null
|
|
18
|
+
|
|
19
|
+
while ((match = functionCallRegex.exec(text)) !== null) {
|
|
20
|
+
const functionName = match[1]
|
|
21
|
+
const startPos = match.index
|
|
22
|
+
const endPos = startPos + functionName.length
|
|
23
|
+
|
|
24
|
+
// Check if this function exists in our schemas
|
|
25
|
+
const functionExists = functions.some((schema) => schema.name === functionName)
|
|
26
|
+
|
|
27
|
+
if (!functionExists) {
|
|
28
|
+
diagnostics.push({
|
|
29
|
+
from: startPos,
|
|
30
|
+
to: endPos,
|
|
31
|
+
severity: 'error',
|
|
32
|
+
message: `Unknown function "${functionName}". Only functions defined in the schema are allowed.`,
|
|
33
|
+
actions: [
|
|
34
|
+
{
|
|
35
|
+
name: 'View available functions',
|
|
36
|
+
apply: () => {
|
|
37
|
+
// This could trigger showing a help dialog or documentation
|
|
38
|
+
element.dispatchEvent(
|
|
39
|
+
new CustomEvent('show-available-functions', {
|
|
40
|
+
detail: { availableFunctions: functions.map((s) => s.name) },
|
|
41
|
+
bubbles: true,
|
|
42
|
+
})
|
|
43
|
+
)
|
|
44
|
+
},
|
|
45
|
+
},
|
|
46
|
+
],
|
|
47
|
+
})
|
|
48
|
+
} else {
|
|
49
|
+
// Optional: Validate function usage (parameters, etc.)
|
|
50
|
+
const fn = functions.find((schema) => schema.name === functionName)
|
|
51
|
+
|
|
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
|
+
}
|
|
70
|
+
|
|
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
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return diagnostics
|
|
87
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { CSSResultOrNative } from 'lit'
|
|
2
|
+
import { customElement } from 'lit/decorators.js'
|
|
3
|
+
import Element from './internals/CodeEditor.js'
|
|
4
|
+
import styles from './internals/CodeEditor.styles.js'
|
|
5
|
+
|
|
6
|
+
@customElement('code-editor')
|
|
7
|
+
export class CodeEditorElement extends Element {
|
|
8
|
+
static override styles: CSSResultOrNative[] = [styles]
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
declare global {
|
|
12
|
+
interface HTMLElementTagNameMap {
|
|
13
|
+
'code-editor': CodeEditorElement
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export default CodeEditorElement
|
|
18
|
+
export type {
|
|
19
|
+
FunctionSchema,
|
|
20
|
+
FunctionParameter,
|
|
21
|
+
Suggestion,
|
|
22
|
+
FunctionInsertEvent,
|
|
23
|
+
SuggestionInsertEvent,
|
|
24
|
+
} from './internals/CodeEditor.js'
|