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 { onMount, createEventDispatcher, onDestroy } from "svelte"
import { FIND_ANY_HBS_REGEX } from "@budibase/string-templates"
import Handlebars from "handlebars"
import {
autocompletion,
@ -40,16 +41,18 @@
indentMore,
indentLess,
} from "@codemirror/commands"
import { setDiagnostics } from "@codemirror/lint"
import type { Diagnostic } from "@codemirror/lint"
import { Compartment, EditorState } from "@codemirror/state"
import { javascript } from "@codemirror/lang-javascript"
import { EditorModes } from "./"
import { themeStore } from "@/stores/portal"
import type { EditorMode } from "@budibase/types"
import type { BindingCompletion } from "@/types"
import type { BindingCompletion, BindingCompletionOption } from "@/types"
export let label: string | undefined = undefined
// TODO: work out what best type fits this
export let completions: BindingCompletion[] = []
export let options: BindingCompletionOption[] = []
export let mode: EditorMode = EditorModes.Handlebars
export let value: string | 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
// config flags aren't changed at runtime
// TODO: work out type for base
@ -255,6 +347,7 @@
complete.push(
autocompletion({
override: [...completions],
// override: [...completions.map(c => c.completionDelegate)],
closeOnBlur: true,
icons: false,
optionClass: completion =>
@ -340,6 +433,31 @@
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 baseExtensions = buildBaseExtensions()
@ -366,7 +484,6 @@
<Label size="S">{label}</Label>
</div>
{/if}
<div class={`code-editor ${mode?.name || ""}`}>
<div tabindex="-1" bind:this={textarea} />
</div>

View File

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

View File

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

View File

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

View File

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