@api-client/ui 0.5.12 → 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/{ui-code-editor.d.ts → code-editor.d.ts} +2 -2
- package/build/src/elements/code-editor/code-editor.d.ts.map +1 -0
- package/build/src/elements/code-editor/{ui-code-editor.js → code-editor.js} +1 -1
- package/build/src/elements/code-editor/code-editor.js.map +1 -0
- package/build/src/elements/code-editor/internals/CodeEditor.d.ts +21 -59
- package/build/src/elements/code-editor/internals/CodeEditor.d.ts.map +1 -1
- package/build/src/elements/code-editor/internals/CodeEditor.js +136 -105
- package/build/src/elements/code-editor/internals/CodeEditor.js.map +1 -1
- package/build/src/elements/code-editor/internals/CodeEditor.styles.d.ts.map +1 -1
- package/build/src/elements/code-editor/internals/CodeEditor.styles.js +4 -0
- package/build/src/elements/code-editor/internals/CodeEditor.styles.js.map +1 -1
- package/build/src/elements/code-editor/internals/Linter.d.ts +2 -2
- package/build/src/elements/code-editor/internals/Linter.d.ts.map +1 -1
- package/build/src/elements/code-editor/internals/Linter.js +32 -37
- package/build/src/elements/code-editor/internals/Linter.js.map +1 -1
- 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 -2
- package/build/src/index.d.ts.map +1 -1
- package/build/src/index.js +1 -1
- package/build/src/index.js.map +1 -1
- package/demo/elements/code-editor/CodeEditorDemo.ts +3 -3
- package/package.json +2 -2
- package/src/elements/code-editor/README.md +1 -1
- package/src/elements/code-editor/{ui-code-editor.ts → code-editor.ts} +1 -1
- package/src/elements/code-editor/internals/CodeEditor.styles.ts +4 -0
- package/src/elements/code-editor/internals/CodeEditor.ts +166 -172
- package/src/elements/code-editor/internals/Linter.ts +37 -39
- 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 +2 -2
- package/build/src/elements/code-editor/ui-code-editor.d.ts.map +0 -1
- 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 {
|
|
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,16 @@ 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
|
-
|
|
24
|
+
hoverTooltip,
|
|
25
|
+
type Tooltip,
|
|
19
26
|
} from '@codemirror/view'
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
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 { functionLinter } from './Linter.js'
|
|
28
|
+
import type { FunctionInsertEvent, FunctionSchema, Suggestion, SuggestionInsertEvent } from './types.js'
|
|
29
|
+
import { SuggestionMatchDecorator } from './SuggestionMatchDecorator.js'
|
|
127
30
|
|
|
128
31
|
/**
|
|
129
32
|
* A CodeMirror 6 based editor component that supports function autocomplete and suggestion placeholders.
|
|
@@ -232,9 +135,11 @@ export default class CodeEditor extends LitElement {
|
|
|
232
135
|
private accessor isEditorFocus = false
|
|
233
136
|
|
|
234
137
|
private editorView?: EditorView
|
|
235
|
-
private suggestionMap = new Map<string, Suggestion>()
|
|
236
|
-
private functionMap = new Map<string, FunctionSchema>()
|
|
237
138
|
private _previousValue = ''
|
|
139
|
+
/**
|
|
140
|
+
* Matcher for suggestion placeholders in the editor.
|
|
141
|
+
*/
|
|
142
|
+
placeholderMatcher?: SuggestionMatchDecorator
|
|
238
143
|
|
|
239
144
|
/**
|
|
240
145
|
* Get all suggestions (placeholders) currently in the editor
|
|
@@ -254,11 +159,6 @@ export default class CodeEditor extends LitElement {
|
|
|
254
159
|
return suggestions
|
|
255
160
|
}
|
|
256
161
|
|
|
257
|
-
override connectedCallback(): void {
|
|
258
|
-
super.connectedCallback()
|
|
259
|
-
this.setupSuggestionMaps()
|
|
260
|
-
}
|
|
261
|
-
|
|
262
162
|
override disconnectedCallback(): void {
|
|
263
163
|
super.disconnectedCallback()
|
|
264
164
|
this.editorView?.destroy()
|
|
@@ -272,8 +172,10 @@ export default class CodeEditor extends LitElement {
|
|
|
272
172
|
override willUpdate(changedProperties: PropertyValues): void {
|
|
273
173
|
super.willUpdate(changedProperties)
|
|
274
174
|
|
|
275
|
-
if (changedProperties.has('suggestions')
|
|
276
|
-
this.
|
|
175
|
+
if (changedProperties.has('suggestions')) {
|
|
176
|
+
if (this.placeholderMatcher) {
|
|
177
|
+
this.placeholderMatcher.suggestions = this.suggestions || []
|
|
178
|
+
}
|
|
277
179
|
}
|
|
278
180
|
|
|
279
181
|
if (changedProperties.has('value') && this.editorView) {
|
|
@@ -305,8 +207,9 @@ export default class CodeEditor extends LitElement {
|
|
|
305
207
|
this.handleFocusChange(update.view.hasFocus)
|
|
306
208
|
}
|
|
307
209
|
}),
|
|
308
|
-
|
|
309
|
-
|
|
210
|
+
this.createPlaceholderPlugin(),
|
|
211
|
+
hoverTooltip(this.createHoverTooltipSource),
|
|
212
|
+
linter((view) => functionLinter(view, this.functionSchemas, (e) => this.dispatchEvent(e))),
|
|
310
213
|
]
|
|
311
214
|
|
|
312
215
|
// Add language support
|
|
@@ -339,6 +242,30 @@ export default class CodeEditor extends LitElement {
|
|
|
339
242
|
})
|
|
340
243
|
}
|
|
341
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
|
+
|
|
342
269
|
/**
|
|
343
270
|
* Create completion source for functions and suggestions
|
|
344
271
|
*/
|
|
@@ -354,19 +281,7 @@ export default class CodeEditor extends LitElement {
|
|
|
354
281
|
const prefix = functionMatch[1]
|
|
355
282
|
const functions = this.functionSchemas
|
|
356
283
|
.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
|
-
}))
|
|
284
|
+
.map((fn) => this.createFunctionCompletion(fn))
|
|
370
285
|
|
|
371
286
|
if (functions.length > 0) {
|
|
372
287
|
return {
|
|
@@ -382,19 +297,7 @@ export default class CodeEditor extends LitElement {
|
|
|
382
297
|
const prefix = suggestionMatch[1]
|
|
383
298
|
const suggestions = this.suggestions
|
|
384
299
|
.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
|
-
}))
|
|
300
|
+
.map((suggestion) => this.createSuggestionCompletion(suggestion))
|
|
398
301
|
|
|
399
302
|
if (suggestions.length > 0) {
|
|
400
303
|
return {
|
|
@@ -408,25 +311,132 @@ export default class CodeEditor extends LitElement {
|
|
|
408
311
|
}
|
|
409
312
|
}
|
|
410
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
|
+
|
|
411
375
|
/**
|
|
412
|
-
*
|
|
376
|
+
* Creates a styled HTML element to display function documentation.
|
|
377
|
+
* This is used for both hover tooltips and autocomplete info panels.
|
|
413
378
|
*/
|
|
414
|
-
private
|
|
415
|
-
|
|
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
|
+
|
|
416
390
|
if (fn.parameters && fn.parameters.length > 0) {
|
|
417
|
-
|
|
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')
|
|
418
399
|
fn.parameters.forEach((param) => {
|
|
419
|
-
|
|
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
|
+
|
|
420
413
|
if (param.description) {
|
|
421
|
-
|
|
414
|
+
const paramDesc = document.createElement('p')
|
|
415
|
+
paramDesc.className = 'param-description'
|
|
416
|
+
paramDesc.textContent = param.description
|
|
417
|
+
listItem.appendChild(paramDesc)
|
|
422
418
|
}
|
|
423
|
-
|
|
419
|
+
paramsList.appendChild(listItem)
|
|
424
420
|
})
|
|
421
|
+
paramsContainer.appendChild(paramsList)
|
|
422
|
+
container.appendChild(paramsContainer)
|
|
425
423
|
}
|
|
424
|
+
|
|
426
425
|
if (fn.returns) {
|
|
427
|
-
|
|
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)
|
|
428
437
|
}
|
|
429
|
-
|
|
438
|
+
|
|
439
|
+
return container
|
|
430
440
|
}
|
|
431
441
|
|
|
432
442
|
/**
|
|
@@ -449,22 +459,6 @@ export default class CodeEditor extends LitElement {
|
|
|
449
459
|
return `${fn.name}(${params})`
|
|
450
460
|
}
|
|
451
461
|
|
|
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
462
|
/**
|
|
469
463
|
* Update editor content when value changes
|
|
470
464
|
*/
|
|
@@ -1,13 +1,17 @@
|
|
|
1
1
|
import type { Diagnostic } from '@codemirror/lint'
|
|
2
2
|
import type { EditorView } from '@codemirror/view'
|
|
3
|
-
import
|
|
3
|
+
import type { FunctionSchema } from './types.js'
|
|
4
4
|
|
|
5
|
-
export function functionLinter(
|
|
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 =
|
|
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
|
|
29
|
+
const fn = functions.find((schema) => schema.name === functionName)
|
|
26
30
|
|
|
27
|
-
if (!
|
|
31
|
+
if (!fn) {
|
|
28
32
|
diagnostics.push({
|
|
29
33
|
from: startPos,
|
|
30
34
|
to: endPos,
|
|
31
35
|
severity: 'error',
|
|
32
|
-
message: `Unknown function "${functionName}".
|
|
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
|
-
|
|
38
|
-
element.dispatchEvent(
|
|
41
|
+
dispatchEvent(
|
|
39
42
|
new CustomEvent('show-available-functions', {
|
|
40
|
-
detail: { availableFunctions: functions
|
|
43
|
+
detail: { availableFunctions: [...functions] },
|
|
41
44
|
bubbles: true,
|
|
42
45
|
})
|
|
43
46
|
)
|
|
@@ -46,38 +49,33 @@ export function functionLinter(view: EditorView, element: Element): readonly Dia
|
|
|
46
49
|
],
|
|
47
50
|
})
|
|
48
51
|
} else {
|
|
49
|
-
//
|
|
50
|
-
const
|
|
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
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
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
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
}
|
|
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
|
+
})
|
|
81
79
|
}
|
|
82
80
|
}
|
|
83
81
|
}
|
|
@@ -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,54 @@
|
|
|
1
|
+
export interface FunctionSchema {
|
|
2
|
+
/** Unique identifier for the function */
|
|
3
|
+
id: string
|
|
4
|
+
/** Function name */
|
|
5
|
+
name: string
|
|
6
|
+
/** Function description */
|
|
7
|
+
description?: string
|
|
8
|
+
/** Function parameters */
|
|
9
|
+
parameters?: FunctionParameter[]
|
|
10
|
+
/** Return type description */
|
|
11
|
+
returns?: string
|
|
12
|
+
/** Additional metadata */
|
|
13
|
+
metadata?: Record<string, unknown>
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface FunctionParameter {
|
|
17
|
+
/** Parameter name */
|
|
18
|
+
name: string
|
|
19
|
+
/** Parameter type */
|
|
20
|
+
type: string
|
|
21
|
+
/** Parameter description */
|
|
22
|
+
description?: string
|
|
23
|
+
/** Whether parameter is required */
|
|
24
|
+
required?: boolean
|
|
25
|
+
/** Default value */
|
|
26
|
+
defaultValue?: unknown
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface Suggestion {
|
|
30
|
+
/** Unique identifier for the suggestion */
|
|
31
|
+
id: string
|
|
32
|
+
/** Main label displayed */
|
|
33
|
+
label: string
|
|
34
|
+
/** Supporting description text */
|
|
35
|
+
description?: string
|
|
36
|
+
/** Suffix text (e.g., type, category) */
|
|
37
|
+
suffix?: string
|
|
38
|
+
/** Additional data associated with the suggestion */
|
|
39
|
+
data?: Record<string, unknown>
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface FunctionInsertEvent {
|
|
43
|
+
/** The inserted function schema */
|
|
44
|
+
functionSchema: FunctionSchema
|
|
45
|
+
/** The position where the function was inserted */
|
|
46
|
+
position: number
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export interface SuggestionInsertEvent {
|
|
50
|
+
/** The inserted suggestion */
|
|
51
|
+
suggestion: Suggestion
|
|
52
|
+
/** The position where the suggestion was inserted */
|
|
53
|
+
position: number
|
|
54
|
+
}
|