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