Merge pull request #15521 from Budibase/BUDI-9038/fix-js-completions

Fix js completions
This commit is contained in:
Adria Navarro 2025-02-13 12:13:53 +01:00 committed by GitHub
commit 31d375cd22
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 197 additions and 141 deletions

View File

@ -53,7 +53,7 @@
"@budibase/shared-core": "*",
"@budibase/string-templates": "*",
"@budibase/types": "*",
"@codemirror/autocomplete": "^6.7.1",
"@codemirror/autocomplete": "6.9.0",
"@codemirror/commands": "^6.2.4",
"@codemirror/lang-javascript": "^6.1.8",
"@codemirror/language": "^6.6.0",

View File

@ -45,10 +45,11 @@
import { EditorModes } from "./"
import { themeStore } from "@/stores/portal"
import type { EditorMode } from "@budibase/types"
import type { BindingCompletion } from "@/types"
export let label: string | undefined = undefined
// TODO: work out what best type fits this
export let completions: any[] = []
export let completions: BindingCompletion[] = []
export let mode: EditorMode = EditorModes.Handlebars
export let value: string | null = ""
export let placeholder: string | null = null

View File

@ -1,13 +1,10 @@
import { getManifest } from "@budibase/string-templates"
import sanitizeHtml from "sanitize-html"
import { groupBy } from "lodash"
import {
BindingCompletion,
EditorModesMap,
Helper,
Snippet,
} from "@budibase/types"
import { EditorModesMap, Helper, Snippet } from "@budibase/types"
import { CompletionContext } from "@codemirror/autocomplete"
import { EditorView } from "@codemirror/view"
import { BindingCompletion, BindingCompletionOption } from "@/types"
export const EditorModes: EditorModesMap = {
JS: {
@ -25,15 +22,7 @@ export const EditorModes: EditorModesMap = {
},
}
export const SECTIONS = {
HB_HELPER: {
name: "Helper",
type: "helper",
icon: "Code",
},
}
export const buildHelperInfoNode = (completion: any, helper: Helper) => {
const buildHelperInfoNode = (helper: Helper) => {
const ele = document.createElement("div")
ele.classList.add("info-bubble")
@ -65,7 +54,7 @@ const toSpectrumIcon = (name: string) => {
</svg>`
}
export const buildSectionHeader = (
const buildSectionHeader = (
type: string,
sectionName: string,
icon: string,
@ -84,30 +73,27 @@ export const buildSectionHeader = (
}
}
export const helpersToCompletion = (
const helpersToCompletion = (
helpers: Record<string, Helper>,
mode: { name: "javascript" | "handlebars" }
) => {
const { type, name: sectionName, icon } = SECTIONS.HB_HELPER
const helperSection = buildSectionHeader(type, sectionName, icon, 99)
): BindingCompletionOption[] => {
const helperSection = buildSectionHeader("helper", "Helpers", "Code", 99)
return Object.keys(helpers).flatMap(helperName => {
let helper = helpers[helperName]
const helper = helpers[helperName]
return {
label: helperName,
info: (completion: BindingCompletion) => {
return buildHelperInfoNode(completion, helper)
},
info: () => buildHelperInfoNode(helper),
type: "helper",
section: helperSection,
detail: "Function",
apply: (
view: any,
completion: BindingCompletion,
view: EditorView,
_completion: BindingCompletionOption,
from: number,
to: number
) => {
insertBinding(view, from, to, helperName, mode)
insertBinding(view, from, to, helperName, mode, AutocompleteType.HELPER)
},
}
})
@ -115,7 +101,7 @@ export const helpersToCompletion = (
export const getHelperCompletions = (mode: {
name: "javascript" | "handlebars"
}) => {
}): BindingCompletionOption[] => {
// TODO: manifest needs to be properly typed
const manifest: any = getManifest()
return Object.keys(manifest).flatMap(key => {
@ -123,49 +109,33 @@ export const getHelperCompletions = (mode: {
})
}
export const snippetAutoComplete = (snippets: Snippet[]) => {
return function myCompletions(context: CompletionContext) {
if (!snippets?.length) {
return null
}
const word = context.matchBefore(/\w*/)
if (!word || (word.from == word.to && !context.explicit)) {
return null
}
return {
from: word.from,
options: snippets.map(snippet => ({
label: `snippets.${snippet.name}`,
type: "text",
simple: true,
apply: (
view: any,
completion: BindingCompletion,
from: number,
to: number
) => {
insertSnippet(view, from, to, completion.label)
},
})),
}
}
export const snippetAutoComplete = (snippets: Snippet[]): BindingCompletion => {
return setAutocomplete(
snippets.map(snippet => ({
section: buildSectionHeader("snippets", "Snippets", "Code", 100),
label: `snippets.${snippet.name}`,
displayLabel: snippet.name,
}))
)
}
const bindingFilter = (options: BindingCompletion[], query: string) => {
const bindingFilter = (options: BindingCompletionOption[], query: string) => {
return options.filter(completion => {
const section_parsed = completion.section.name.toLowerCase()
const section_parsed = completion.section?.toString().toLowerCase()
const label_parsed = completion.label.toLowerCase()
const query_parsed = query.toLowerCase()
return (
section_parsed.includes(query_parsed) ||
section_parsed?.includes(query_parsed) ||
label_parsed.includes(query_parsed)
)
})
}
export const hbAutocomplete = (baseCompletions: BindingCompletion[]) => {
async function coreCompletion(context: CompletionContext) {
export const hbAutocomplete = (
baseCompletions: BindingCompletionOption[]
): BindingCompletion => {
function coreCompletion(context: CompletionContext) {
let bindingStart = context.matchBefore(EditorModes.Handlebars.match)
let options = baseCompletions || []
@ -191,9 +161,15 @@ export const hbAutocomplete = (baseCompletions: BindingCompletion[]) => {
return coreCompletion
}
export const jsAutocomplete = (baseCompletions: BindingCompletion[]) => {
async function coreCompletion(context: CompletionContext) {
let jsBinding = context.matchBefore(/\$\("[\s\w]*/)
function wrappedAutocompleteMatch(context: CompletionContext) {
return context.matchBefore(/\$\("[\s\w]*/)
}
export const jsAutocomplete = (
baseCompletions: BindingCompletionOption[]
): BindingCompletion => {
function coreCompletion(context: CompletionContext) {
let jsBinding = wrappedAutocompleteMatch(context)
let options = baseCompletions || []
if (jsBinding) {
@ -217,10 +193,42 @@ export const jsAutocomplete = (baseCompletions: BindingCompletion[]) => {
return coreCompletion
}
export const buildBindingInfoNode = (
completion: BindingCompletion,
binding: any
) => {
export const jsHelperAutocomplete = (
baseCompletions: BindingCompletionOption[]
): BindingCompletion => {
return setAutocomplete(
baseCompletions.map(helper => ({
...helper,
displayLabel: helper.label,
label: `helpers.${helper.label}()`,
}))
)
}
function setAutocomplete(
options: BindingCompletionOption[]
): BindingCompletion {
return function (context: CompletionContext) {
if (wrappedAutocompleteMatch(context)) {
return null
}
const word = context.matchBefore(/\b\w*(\.\w*)?/)
if (!word || (word.from == word.to && !context.explicit)) {
return null
}
return {
from: word.from,
options,
}
}
}
const buildBindingInfoNode = (binding: {
valueHTML: string
value: string | null
}) => {
if (!binding.valueHTML || binding.value == null) {
return null
}
@ -278,18 +286,28 @@ export function jsInsert(
return parsedInsert
}
const enum AutocompleteType {
BINDING,
HELPER,
TEXT,
}
// Autocomplete apply behaviour
export const insertBinding = (
view: any,
const insertBinding = (
view: EditorView,
from: number,
to: number,
text: string,
mode: { name: "javascript" | "handlebars" }
mode: { name: "javascript" | "handlebars" },
type: AutocompleteType
) => {
let parsedInsert
if (mode.name == "javascript") {
parsedInsert = jsInsert(view.state.doc?.toString(), from, to, text)
parsedInsert = jsInsert(view.state.doc?.toString(), from, to, text, {
helper: type === AutocompleteType.HELPER,
disableWrapping: type === AutocompleteType.TEXT,
})
} else if (mode.name == "handlebars") {
parsedInsert = hbInsert(view.state.doc?.toString(), from, to, text)
} else {
@ -319,30 +337,11 @@ export const insertBinding = (
})
}
export const insertSnippet = (
view: any,
from: number,
to: number,
text: string
) => {
let cursorPos = from + text.length
view.dispatch({
changes: {
from,
to,
insert: text,
},
selection: {
anchor: cursorPos,
},
})
}
// TODO: typing in this function isn't great
export const bindingsToCompletions = (
bindings: any,
mode: { name: "javascript" | "handlebars" }
) => {
): BindingCompletionOption[] => {
const bindingByCategory = groupBy(bindings, "category")
const categoryMeta = bindings?.reduce((acc: any, ele: any) => {
acc[ele.category] = acc[ele.category] || {}
@ -356,46 +355,54 @@ export const bindingsToCompletions = (
return acc
}, {})
const completions = Object.keys(bindingByCategory).reduce(
(comps: any, catKey: string) => {
const { icon, rank } = categoryMeta[catKey] || {}
const completions = Object.keys(bindingByCategory).reduce<
BindingCompletionOption[]
>((comps, catKey) => {
const { icon, rank } = categoryMeta[catKey] || {}
const bindingSectionHeader = buildSectionHeader(
// @ts-ignore something wrong with this - logically this should be dictionary
bindingByCategory.type,
catKey,
icon || "",
typeof rank == "number" ? rank : 1
)
const bindingSectionHeader = buildSectionHeader(
// @ts-ignore something wrong with this - logically this should be dictionary
bindingByCategory.type,
catKey,
icon || "",
typeof rank == "number" ? rank : 1
)
return [
...comps,
...bindingByCategory[catKey].reduce((acc, binding) => {
comps.push(
...bindingByCategory[catKey].reduce<BindingCompletionOption[]>(
(acc, binding) => {
let displayType = binding.fieldSchema?.type || binding.display?.type
acc.push({
label:
binding.display?.name || binding.readableBinding || "NO NAME",
info: (completion: BindingCompletion) => {
return buildBindingInfoNode(completion, binding)
},
info: () => buildBindingInfoNode(binding),
type: "binding",
detail: displayType,
section: bindingSectionHeader,
apply: (
view: any,
completion: BindingCompletion,
view: EditorView,
_completion: BindingCompletionOption,
from: number,
to: number
) => {
insertBinding(view, from, to, binding.readableBinding, mode)
insertBinding(
view,
from,
to,
binding.readableBinding,
mode,
AutocompleteType.BINDING
)
},
})
return acc
}, []),
]
},
[]
)
},
[]
)
)
return comps
}, [])
return completions
}

View File

@ -23,6 +23,7 @@
snippetAutoComplete,
EditorModes,
bindingsToCompletions,
jsHelperAutocomplete,
} from "../CodeEditor"
import BindingSidePanel from "./BindingSidePanel.svelte"
import EvaluationSidePanel from "./EvaluationSidePanel.svelte"
@ -34,7 +35,6 @@
import { BindingMode, SidePanel } from "@budibase/types"
import type {
EnrichedBinding,
BindingCompletion,
Snippet,
Helper,
CaretPositionFn,
@ -42,7 +42,7 @@
JSONValue,
} from "@budibase/types"
import type { Log } from "@budibase/string-templates"
import type { CompletionContext } from "@codemirror/autocomplete"
import type { BindingCompletion, BindingCompletionOption } from "@/types"
const dispatch = createEventDispatcher()
@ -91,7 +91,10 @@
$: bindingCompletions = bindingsToCompletions(enrichedBindings, editorMode)
$: bindingHelpers = new BindingHelpers(getCaretPosition, insertAtPos)
$: hbsCompletions = getHBSCompletions(bindingCompletions)
$: jsCompletions = getJSCompletions(bindingCompletions, snippets, useSnippets)
$: jsCompletions = getJSCompletions(bindingCompletions, snippets, {
useHelpers: allowHelpers,
useSnippets,
})
$: {
// Ensure a valid side panel option is always selected
if (sidePanel && !sidePanelOptions.includes(sidePanel)) {
@ -99,7 +102,7 @@
}
}
const getHBSCompletions = (bindingCompletions: BindingCompletion[]) => {
const getHBSCompletions = (bindingCompletions: BindingCompletionOption[]) => {
return [
hbAutocomplete([
...bindingCompletions,
@ -109,17 +112,23 @@
}
const getJSCompletions = (
bindingCompletions: BindingCompletion[],
bindingCompletions: BindingCompletionOption[],
snippets: Snippet[] | null,
useSnippets?: boolean
config: {
useHelpers: boolean
useSnippets: boolean
}
) => {
const completions: ((_: CompletionContext) => any)[] = [
jsAutocomplete([
...bindingCompletions,
...getHelperCompletions(EditorModes.JS),
]),
]
if (useSnippets && snippets) {
const completions: BindingCompletion[] = []
if (bindingCompletions.length) {
completions.push(jsAutocomplete([...bindingCompletions]))
}
if (config.useHelpers) {
completions.push(
jsHelperAutocomplete([...getHelperCompletions(EditorModes.JS)])
)
}
if (config.useSnippets && snippets) {
completions.push(snippetAutoComplete(snippets))
}
return completions
@ -381,7 +390,7 @@
autofocus={autofocusEditor}
placeholder={placeholder ||
"Add bindings by typing $ or use the menu on the right"}
jsBindingWrapping
jsBindingWrapping={bindingCompletions.length > 0}
/>
{/key}
{/if}

View File

@ -118,6 +118,7 @@
allowHBS={false}
allowJS
allowSnippets={false}
allowHelpers={false}
showTabBar={false}
placeholder="return function(input) &#10100; ... &#10101;"
value={code}

View File

@ -0,0 +1,8 @@
import { CompletionContext, Completion } from "@codemirror/autocomplete"
export type BindingCompletion = (context: CompletionContext) => {
from: number
options: Completion[]
} | null
export type BindingCompletionOption = Completion

View File

@ -0,0 +1 @@
export * from "./bindings"

View File

@ -1,10 +1,3 @@
export interface BindingCompletion {
section: {
name: string
}
label: string
}
export interface EnrichedBinding {
runtimeBinding: string
readableBinding: string

View File

@ -2834,16 +2834,26 @@
dependencies:
"@bull-board/api" "5.10.2"
"@codemirror/autocomplete@^6.0.0", "@codemirror/autocomplete@^6.7.1":
version "6.7.1"
resolved "https://registry.yarnpkg.com/@codemirror/autocomplete/-/autocomplete-6.7.1.tgz#3364799b78dff70fb8f81615536c52ea53ce40b2"
integrity sha512-hSxf9S0uB+GV+gBsjY1FZNo53e1FFdzPceRfCfD1gWOnV6o21GfB5J5Wg9G/4h76XZMPrF0A6OCK/Rz5+V1egg==
"@codemirror/autocomplete@6.9.0":
version "6.9.0"
resolved "https://registry.yarnpkg.com/@codemirror/autocomplete/-/autocomplete-6.9.0.tgz#1a1e63122288b8f8e1e9d7aff2eb39a83e04d8a9"
integrity sha512-Fbwm0V/Wn3BkEJZRhr0hi5BhCo5a7eBL6LYaliPjOSwCyfOpnjXY59HruSxOUNV+1OYer0Tgx1zRNQttjXyDog==
dependencies:
"@codemirror/language" "^6.0.0"
"@codemirror/state" "^6.0.0"
"@codemirror/view" "^6.6.0"
"@lezer/common" "^1.0.0"
"@codemirror/autocomplete@^6.0.0":
version "6.18.4"
resolved "https://registry.yarnpkg.com/@codemirror/autocomplete/-/autocomplete-6.18.4.tgz#4394f55d6771727179f2e28a871ef46bbbeb11b1"
integrity sha512-sFAphGQIqyQZfP2ZBsSHV7xQvo9Py0rV0dW7W3IMRdS+zDuNb2l3no78CvUaWKGfzFjI4FTrLdUSj86IGb2hRA==
dependencies:
"@codemirror/language" "^6.0.0"
"@codemirror/state" "^6.0.0"
"@codemirror/view" "^6.17.0"
"@lezer/common" "^1.0.0"
"@codemirror/commands@^6.2.4":
version "6.2.4"
resolved "https://registry.yarnpkg.com/@codemirror/commands/-/commands-6.2.4.tgz#b8a0e5ce72448c092ba4c4b1d902e6f183948aec"
@ -2893,6 +2903,13 @@
resolved "https://registry.yarnpkg.com/@codemirror/state/-/state-6.2.0.tgz#a0fb08403ced8c2a68d1d0acee926bd20be922f2"
integrity sha512-69QXtcrsc3RYtOtd+GsvczJ319udtBf1PTrr2KbLWM/e2CXUPnh0Nz9AUo8WfhSQ7GeL8dPVNUmhQVgpmuaNGA==
"@codemirror/state@^6.5.0":
version "6.5.2"
resolved "https://registry.yarnpkg.com/@codemirror/state/-/state-6.5.2.tgz#8eca3a64212a83367dc85475b7d78d5c9b7076c6"
integrity sha512-FVqsPqtPWKVVL3dPSxy8wEF/ymIEuVzF1PK3VbUgrxXpJUSHQWWZz4JMToquRxnkw+36LTamCZG2iua2Ptq0fA==
dependencies:
"@marijn/find-cluster-break" "^1.0.0"
"@codemirror/theme-one-dark@^6.1.2":
version "6.1.2"
resolved "https://registry.yarnpkg.com/@codemirror/theme-one-dark/-/theme-one-dark-6.1.2.tgz#fcef9f9cfc17a07836cb7da17c9f6d7231064df8"
@ -2912,6 +2929,15 @@
style-mod "^4.0.0"
w3c-keyname "^2.2.4"
"@codemirror/view@^6.17.0":
version "6.36.2"
resolved "https://registry.yarnpkg.com/@codemirror/view/-/view-6.36.2.tgz#aeb644e161440734ac5a153bf6e5b4a4355047be"
integrity sha512-DZ6ONbs8qdJK0fdN7AB82CgI6tYXf4HWk1wSVa0+9bhVznCuuvhQtX8bFBoy3dv8rZSQqUd8GvhVAcielcidrA==
dependencies:
"@codemirror/state" "^6.5.0"
style-mod "^4.1.0"
w3c-keyname "^2.2.4"
"@colors/colors@1.6.0", "@colors/colors@^1.6.0":
version "1.6.0"
resolved "https://registry.yarnpkg.com/@colors/colors/-/colors-1.6.0.tgz#ec6cd237440700bc23ca23087f513c75508958b0"
@ -4133,6 +4159,11 @@
semver "^7.3.5"
tar "^6.1.11"
"@marijn/find-cluster-break@^1.0.0":
version "1.0.2"
resolved "https://registry.yarnpkg.com/@marijn/find-cluster-break/-/find-cluster-break-1.0.2.tgz#775374306116d51c0c500b8c4face0f9a04752d8"
integrity sha512-l0h88YhZFyKdXIFNfSWpyjStDjGHwZ/U7iobcK1cQQD8sejsONdQtTVU+1wVN1PBw40PiiHB1vA5S7VTfQiP9g==
"@mongodb-js/saslprep@^1.1.5":
version "1.1.7"
resolved "https://registry.yarnpkg.com/@mongodb-js/saslprep/-/saslprep-1.1.7.tgz#d1700facfd6916c50c2c88fd6d48d363a56c702f"
@ -19961,6 +19992,11 @@ style-mod@^4.0.0:
resolved "https://registry.yarnpkg.com/style-mod/-/style-mod-4.0.3.tgz#136c4abc905f82a866a18b39df4dc08ec762b1ad"
integrity sha512-78Jv8kYJdjbvRwwijtCevYADfsI0lGzYJe4mMFdceO8l75DFFDoqBhR1jVDicDRRaX4//g1u9wKeo+ztc2h1Rw==
style-mod@^4.1.0:
version "4.1.2"
resolved "https://registry.yarnpkg.com/style-mod/-/style-mod-4.1.2.tgz#ca238a1ad4786520f7515a8539d5a63691d7bf67"
integrity sha512-wnD1HyVqpJUI2+eKZ+eo1UwghftP6yuFheBqqe+bWCotBjC2K1YnteJILRMs3SM4V/0dLEW1SC27MWP5y+mwmw==
stylus-lookup@^3.0.1:
version "3.0.2"
resolved "https://registry.yarnpkg.com/stylus-lookup/-/stylus-lookup-3.0.2.tgz#c9eca3ff799691020f30b382260a67355fefdddd"