UNPKG

jsoneditor

Version:

A web-based tool to view, edit, format, and validate JSON

233 lines (219 loc) 8.33 kB
'use strict' import * as jsonMap from 'json-source-map' import { isArray, isObject, uniqueMergeArrays, asyncExec } from './util' /** * SchemaTextCompleter class implements the ace ext-language_tools completer API, * and suggests completions for the text editor that are relative * to the cursor position and the json schema */ export class SchemaTextCompleter { constructor (schema, schemaRefs) { this.schema = schema this.schemaRefs = schemaRefs || {} this.suggestions = {} this.suggestionsRefs = {} this._buildSuggestions() } _buildSuggestions () { this._handleSchemaEntry('', this.schema, this.suggestions) for (const refName in this.schemaRefs) { this.suggestionsRefs[refName] = {} this._handleSchemaEntry('', this.schemaRefs[refName], this.suggestionsRefs[refName]) } } _handleRef (currectPath, refName, suggestionsObj) { suggestionsObj[currectPath] = suggestionsObj[currectPath] || {} suggestionsObj[currectPath].refs = suggestionsObj[currectPath].refs || [] suggestionsObj[currectPath].refs = uniqueMergeArrays(suggestionsObj[currectPath].refs, [refName]) } _handleSchemaEntry (currectPath, schemaNode, suggestionsObj) { if (!schemaNode) { console.error('SchemaTextCompleter: schema node is missing for path', currectPath) return } if (schemaNode.$ref) { this._handleRef(currectPath, schemaNode.$ref, suggestionsObj) return } const ofConditionEntry = this._checkOfConditon(schemaNode) if (ofConditionEntry) { this._handleOfCondition(currectPath, schemaNode[ofConditionEntry], suggestionsObj) return } switch (schemaNode.type) { case 'object': this._handleObject(currectPath, schemaNode, suggestionsObj) break case 'string': case 'number': case 'integer': this._handlePrimitive(currectPath, schemaNode, suggestionsObj) break case 'boolean': this._handleBoolean(currectPath, schemaNode, suggestionsObj) break case 'array': this._handleArray(currectPath, schemaNode, suggestionsObj) } } _handleObject (currectPath, schemaNode, suggestionsObj) { if (isObject(schemaNode.properties)) { const props = Object.keys(schemaNode.properties) suggestionsObj[currectPath] = suggestionsObj[currectPath] || {} suggestionsObj[currectPath].props = suggestionsObj[currectPath].props || [] suggestionsObj[currectPath].props = uniqueMergeArrays(suggestionsObj[currectPath].props, props) props.forEach((prop) => { asyncExec(() => { this._handleSchemaEntry(`${currectPath}/${prop}`, schemaNode.properties[prop], suggestionsObj) }) }) } } _handlePrimitive (currectPath, schemaNode, suggestionsObj) { suggestionsObj[currectPath] = suggestionsObj[currectPath] || {} if (isArray(schemaNode.examples)) { suggestionsObj[currectPath].examples = suggestionsObj[currectPath].examples || [] suggestionsObj[currectPath].examples = uniqueMergeArrays(suggestionsObj[currectPath].examples, schemaNode.examples) } if (isArray(schemaNode.enum)) { suggestionsObj[currectPath].enum = suggestionsObj[currectPath].enum || [] suggestionsObj[currectPath].enum = uniqueMergeArrays(suggestionsObj[currectPath].enum, schemaNode.enum) } } _handleBoolean (currectPath, schemaNode, suggestionsObj) { if (!suggestionsObj[currectPath]) { suggestionsObj[currectPath] = { bool: [true, false] } } } _handleArray (currectPath, schemaNode, suggestionsObj) { if (schemaNode.items) { asyncExec(() => { this._handleSchemaEntry(`${currectPath}/\\d+`, schemaNode.items, suggestionsObj) }) } } _handleOfCondition (currectPath, schemaNode, suggestionsObj) { if (schemaNode && schemaNode.length) { schemaNode.forEach(schemaEntry => { asyncExec(() => { this._handleSchemaEntry(currectPath, schemaEntry, suggestionsObj) }) }) } } _checkOfConditon (entry) { if (!entry) { return } if (entry.oneOf) { return 'oneOf' } if (entry.anyOf) { return 'anyOf' } if (entry.allOf) { return 'allOf' } } getCompletions (editor, session, pos, prefix, callback) { try { const map = jsonMap.parse(session.getValue()) const pointers = map.pointers || {} const processCompletionsCallback = (suggestions) => { let completions = [] let score = 0 const appendSuggesions = (type) => { const typeTitle = { props: 'property', enum: 'enum', bool: 'boolean', examples: 'examples' } if (suggestions && suggestions[type]?.length) { completions = completions.concat(suggestions[type].map(term => { return { caption: term + '', meta: `schema [${typeTitle[type]}]`, score: score++, value: term + '' } })) } } appendSuggesions('props') appendSuggesions('enum') appendSuggesions('bool') appendSuggesions('examples') if (completions.length) { callback(null, completions) } } Object.keys(pointers).forEach((ptr) => { asyncExec(() => { const matchPointersToPath = (pointer, currentSuggestions, path) => { const option = Object.keys(currentSuggestions).reduce((last, key) => { if (new RegExp(`^${path}${key}`).test(pointer)) { if (!last || last.length < key.length) { return key } } return last }, null) if (typeof option === 'string') { if (currentSuggestions[option]?.refs?.length) { const mergedSuggestions = {} for (const idx in currentSuggestions[option].refs) { const refName = currentSuggestions[option].refs[idx] if (this.suggestionsRefs[refName]) { const refSuggestion = matchPointersToPath(pointer, this.suggestionsRefs[refName], `${path}${option}`) if (refSuggestion?.enum) { mergedSuggestions.enum = uniqueMergeArrays(mergedSuggestions.enum, refSuggestion.enum) } if (refSuggestion?.examples) { mergedSuggestions.examples = uniqueMergeArrays(mergedSuggestions.examples, refSuggestion.examples) } if (refSuggestion?.bool) { mergedSuggestions.bool = uniqueMergeArrays(mergedSuggestions.bool, refSuggestion.bool) } if (refSuggestion?.props) { mergedSuggestions.props = uniqueMergeArrays(mergedSuggestions.props, refSuggestion.props) } } } return mergedSuggestions } else if (new RegExp(`^${path}${option}$`).test(pointer)) { // console.log('SchemaTextCompleter: Text suggestion match', { path: pointer, schemaPath: `${path}${option}`, suggestions: currentSuggestions[option] }) return currentSuggestions[option] } } } let selectedPtr if (pointers[ptr].key?.line === pos.row) { if (pos.column >= pointers[ptr].key.column && pos.column <= pointers[ptr].keyEnd.column) { selectedPtr = ptr.slice(0, ptr.lastIndexOf('/')) } } if (pointers[ptr].value?.line === pos.row && pointers[ptr].value?.line === pointers[ptr].valueEnd?.line) { // multiline values are objects if (pos.column >= pointers[ptr].value.column && pos.column <= pointers[ptr].valueEnd.column) { selectedPtr = ptr } } if (selectedPtr) { const chosenCompletions = matchPointersToPath(selectedPtr, this.suggestions, '') processCompletionsCallback(chosenCompletions) } }) }) } catch (e) { // probably not valid json, ignore. } } }