Spaces:
Build error
Build error
| /** | |
| * @import {SyntaxNode} from '@lezer/common' | |
| * @import {EditorState} from '@codemirror/state' | |
| */ | |
| import { syntaxTree } from "@codemirror/language"; | |
| import { startCompletion } from "@codemirror/autocomplete"; | |
| /** | |
| * Creates a delayed info function that shows documentation after a timeout | |
| * only if the user hasn't changed the selection | |
| * @param {string} doc - The documentation text to show | |
| * @returns {Function} Async function that returns documentation with delay | |
| */ | |
| function createDelayedInfo(doc) { | |
| return function() { | |
| return new Promise((resolve) => { | |
| const delay = 800; // 800ms delay before showing documentation | |
| setTimeout(() => { | |
| // Create a DOM node containing the documentation | |
| const div = document.createElement('div'); | |
| div.textContent = doc; | |
| div.style.maxWidth = '300px'; | |
| div.style.padding = '8px'; | |
| div.style.lineHeight = '1.4'; | |
| resolve(div); | |
| }, delay); | |
| }); | |
| }; | |
| } | |
| /** | |
| * Walks the Lezer Syntax upwards to find all tag names | |
| * @param {SyntaxNode} node | |
| * @param {EditorState} state | |
| * @returns {string[]} | |
| */ | |
| function getParentTagNames(node, state) { | |
| const tagNames = []; | |
| let parentNode = node.parent; | |
| while (parentNode) { | |
| if (parentNode.name === "Element") { | |
| const tagPortion = state.doc.sliceString(parentNode.from, parentNode.to); | |
| const match = tagPortion.match(/^<([a-zA-Z0-9:]+)/); | |
| if (match) { | |
| tagNames.push(match[1]); // Add the captured tag name to the list | |
| } | |
| } | |
| parentNode = parentNode.parent; | |
| } | |
| return tagNames; | |
| } | |
| /** | |
| * Given a data structure containing permissible children and attributes of | |
| * nodes with a given tag, create the completionSource data for autocompletion | |
| * @param {*} tagData | |
| * @returns | |
| */ | |
| export function createCompletionSource(tagData) { | |
| return (context) => { | |
| const state = context.state; | |
| const pos = context.pos; | |
| let node = syntaxTree(state).resolveInner(pos, -1); | |
| let type = node.type.name; | |
| //let text = context.state.sliceDoc(node.from, context.pos); | |
| let options = []; | |
| const parentTags = getParentTagNames(node, state); | |
| let completionType = "keyword"; | |
| switch (type) { | |
| case "StartTag": | |
| options = (tagData[parentTags[0]]?.children || []).map(childName => { | |
| const childData = tagData[childName]; | |
| const doc = childData?.doc; | |
| return { | |
| label: childName, | |
| type: "keyword", | |
| detail: doc ? (doc.length > 50 ? doc.substring(0, 47) + "..." : doc) : undefined, | |
| info: doc ? createDelayedInfo(doc) : undefined | |
| }; | |
| }); | |
| break; | |
| case "TagName": | |
| options = (tagData[parentTags[1]]?.children || []).map(childName => { | |
| const childData = tagData[childName]; | |
| const doc = childData?.doc; | |
| return { | |
| label: childName, | |
| type: "keyword", | |
| detail: doc ? (doc.length > 50 ? doc.substring(0, 47) + "..." : doc) : undefined, | |
| info: doc ? createDelayedInfo(doc) : undefined | |
| }; | |
| }); | |
| break; | |
| case "OpenTag": | |
| case "AttributeName": | |
| options = Object.keys(tagData[parentTags[0]]?.attrs || {}) | |
| .map(attrName => { | |
| const attrData = tagData[parentTags[0]].attrs[attrName]; | |
| const doc = (typeof attrData === 'object' && attrData.doc) ? attrData.doc : undefined; | |
| return { | |
| displayLabel: attrName, | |
| label: `${attrName}=""`, | |
| type: "property", | |
| detail: doc ? (doc.length > 30 ? doc.substring(0, 27) + "..." : doc) : undefined, | |
| info: doc ? createDelayedInfo(doc) : undefined, | |
| apply: (view, completion, from, to) => { | |
| view.dispatch({ | |
| changes: { from, to, insert: completion.label }, | |
| selection: { anchor: from + completion.label.length - 1 } | |
| }); | |
| // start new autocomplete | |
| setTimeout(() => startCompletion(view), 20); | |
| } | |
| }; | |
| }); | |
| break; | |
| case "AttributeValue": | |
| const attributeNode = node.prevSibling?.prevSibling; | |
| if (!attributeNode) break; | |
| const attributeTag = context.state.sliceDoc(attributeNode.from, attributeNode.to); | |
| const attrs = tagData[parentTags[0]]?.attrs; | |
| const attrData = attrs && attrs[attributeTag]; | |
| // Handle both old format (array) and new format (object with values and doc) | |
| let values = []; | |
| let attrDoc = undefined; | |
| if (Array.isArray(attrData)) { | |
| values = attrData; | |
| } else if (typeof attrData === 'object' && attrData) { | |
| values = attrData.values || []; | |
| attrDoc = attrData.doc; | |
| } | |
| options = values.map(value => ({ | |
| label: value, | |
| type: "property", | |
| detail: attrDoc ? (attrDoc.length > 40 ? attrDoc.substring(0, 37) + "..." : attrDoc) : undefined, | |
| info: attrDoc ? createDelayedInfo(attrDoc) : undefined, | |
| apply: (view, completion, from, to) => { | |
| view.dispatch({ | |
| changes: { from, to, insert: completion.label }, | |
| selection: { anchor: to + completion.label.length + 1 }, // Move cursor after the closing quote | |
| }); | |
| } | |
| })); | |
| break; | |
| } | |
| if (options.length === 0) { | |
| return null; | |
| } | |
| // Suggest from cursor position for StartTag/OpenTag. | |
| const from = ["StartTag", "OpenTag", "AttributeValue"].includes(type) ? pos : node.from; | |
| const to = pos; | |
| // convert string options to completionResult objects (only for legacy format) | |
| options = options.map(option => { | |
| if (typeof option === "string") { | |
| return { label: option, type: completionType }; | |
| } | |
| return option; | |
| }); | |
| return { | |
| from, to, options, | |
| validFor: /^[\w@:]*$/ | |
| }; | |
| }; | |
| } | |