This commit is contained in:
Adria Navarro 2025-02-13 14:22:18 +01:00
parent 60b605e6f0
commit 29a84758d7
5 changed files with 126 additions and 4 deletions

View File

@ -2,6 +2,7 @@
import { Label } from "@budibase/bbui" import { Label } from "@budibase/bbui"
import { onMount, createEventDispatcher, onDestroy } from "svelte" import { onMount, createEventDispatcher, onDestroy } from "svelte"
import { FIND_ANY_HBS_REGEX } from "@budibase/string-templates" import { FIND_ANY_HBS_REGEX } from "@budibase/string-templates"
import Handlebars from "handlebars"
import { import {
autocompletion, autocompletion,
@ -40,16 +41,18 @@
indentMore, indentMore,
indentLess, indentLess,
} from "@codemirror/commands" } from "@codemirror/commands"
import { setDiagnostics } from "@codemirror/lint"
import type { Diagnostic } from "@codemirror/lint"
import { Compartment, EditorState } from "@codemirror/state" import { Compartment, EditorState } from "@codemirror/state"
import { javascript } from "@codemirror/lang-javascript" import { javascript } from "@codemirror/lang-javascript"
import { EditorModes } from "./" import { EditorModes } from "./"
import { themeStore } from "@/stores/portal" import { themeStore } from "@/stores/portal"
import type { EditorMode } from "@budibase/types" import type { EditorMode } from "@budibase/types"
import type { BindingCompletion } from "@/types" import type { BindingCompletion, BindingCompletionOption } from "@/types"
export let label: string | undefined = undefined export let label: string | undefined = undefined
// TODO: work out what best type fits this
export let completions: BindingCompletion[] = [] export let completions: BindingCompletion[] = []
export let options: BindingCompletionOption[] = []
export let mode: EditorMode = EditorModes.Handlebars export let mode: EditorMode = EditorModes.Handlebars
export let value: string | null = "" export let value: string | null = ""
export let placeholder: string | null = null export let placeholder: string | null = null
@ -245,6 +248,95 @@
] ]
} }
async function validateHbsTemplate(
editor: EditorView,
template: string,
helpers: Record<string, any[]>
): Promise<Diagnostic[]> {
const diagnostics: Diagnostic[] = []
try {
const ast = Handlebars.parse(template)
function traverseNodes(nodes: any[]) {
nodes.forEach(node => {
if (
node.type === "MustacheStatement" &&
node.path.type === "PathExpression"
) {
const helperName = node.path.original
const from =
editor.state.doc.line(node.loc.start.line).from +
node.loc.start.column
const to =
editor.state.doc.line(node.loc.end.line).from +
node.loc.end.column
if (!(helperName in helpers)) {
diagnostics.push({
from,
to,
severity: "warning",
message: `"${helperName}" handler does not exist.`,
})
return
}
const expectedParams = helpers[helperName]
if (expectedParams) {
const providedParams = node.params
if (providedParams.length !== expectedParams.length) {
diagnostics.push({
from,
to,
severity: "error",
message: `Helper "${helperName}" expects ${
expectedParams.length
} parameters (${expectedParams.join(", ")}), but got ${
providedParams.length
}.`,
})
}
}
}
if (node.program) {
traverseNodes(node.program.body)
}
})
}
traverseNodes(ast.body)
} catch (e: any) {
diagnostics.push({
from: 0,
to: template.length,
severity: "error",
message: `Syntax error: ${e.message}`,
})
}
return diagnostics
}
// function getCompletions(): ((_: CompletionContext) => any)[] {
// switch (mode.name) {
// case "handlebars":
// return [hbAutocomplete([...completions])]
// case "javascript":
// return [jsAutocomplete([...completions])]
// case "text/html":
// return []
// default:
// throw utils.unreachable(mode)
// }
// }
// None of this is reactive, but it never has been, so we just assume most // None of this is reactive, but it never has been, so we just assume most
// config flags aren't changed at runtime // config flags aren't changed at runtime
// TODO: work out type for base // TODO: work out type for base
@ -255,6 +347,7 @@
complete.push( complete.push(
autocompletion({ autocompletion({
override: [...completions], override: [...completions],
// override: [...completions.map(c => c.completionDelegate)],
closeOnBlur: true, closeOnBlur: true,
icons: false, icons: false,
optionClass: completion => optionClass: completion =>
@ -340,6 +433,31 @@
return complete return complete
} }
function validate(
value: string | null,
editor: EditorView,
mode: EditorMode,
options: BindingCompletionOption[]
) {
if (!value) {
return
}
const expectedHelpers: Record<string, any[]> = {}
for (const option of options) {
expectedHelpers[option.label] = option.args || []
}
if (mode === EditorModes.Handlebars) {
validateHbsTemplate(editor, value, expectedHelpers).then(diagnostics => {
editor?.dispatch(setDiagnostics(editor.state, diagnostics))
})
}
}
$: validate(value, editor, mode, options)
const initEditor = () => { const initEditor = () => {
const baseExtensions = buildBaseExtensions() const baseExtensions = buildBaseExtensions()
@ -366,7 +484,6 @@
<Label size="S">{label}</Label> <Label size="S">{label}</Label>
</div> </div>
{/if} {/if}
<div class={`code-editor ${mode?.name || ""}`}> <div class={`code-editor ${mode?.name || ""}`}>
<div tabindex="-1" bind:this={textarea} /> <div tabindex="-1" bind:this={textarea} />
</div> </div>

View File

@ -83,6 +83,7 @@ const helpersToCompletion = (
const helper = helpers[helperName] const helper = helpers[helperName]
return { return {
label: helperName, label: helperName,
args: helper.args,
info: () => buildHelperInfoNode(helper), info: () => buildHelperInfoNode(helper),
type: "helper", type: "helper",
section: helperSection, section: helperSection,

View File

@ -373,6 +373,7 @@
bind:getCaretPosition bind:getCaretPosition
bind:insertAtPos bind:insertAtPos
{completions} {completions}
options={autocompleteOptions}
autofocus={autofocusEditor} autofocus={autofocusEditor}
placeholder={placeholder || placeholder={placeholder ||
"Add bindings by typing {{ or use the menu on the right"} "Add bindings by typing {{ or use the menu on the right"}

View File

@ -5,4 +5,6 @@ export type BindingCompletion = (context: CompletionContext) => {
options: Completion[] options: Completion[]
} | null } | null
export type BindingCompletionOption = Completion export interface BindingCompletionOption extends Completion {
args?: any[]
}

View File

@ -1,4 +1,5 @@
export interface Helper { export interface Helper {
example: string example: string
description: string description: string
args: any[]
} }