UNPKG

@neo4j-cypher/editor-support

Version:

Core functionality to support Cypher integration into editors

254 lines (250 loc) 7.68 kB
import _defineProperty from "@babel/runtime/helpers/defineProperty"; import Fuse from "fuse.js"; import _find from "lodash.find"; import * as CypherTypes from "../lang/CypherTypes"; import * as CompletionTypes from "./CompletionTypes"; import CypherKeywords from "../lang/CypherKeywords"; import { TreeUtils } from "../util/TreeUtils"; import { ecsapeCypher } from "../util/ecsapeCypher"; export const KEYWORD_ITEMS = CypherKeywords.map(keyword => ({ type: CompletionTypes.KEYWORD, view: keyword, content: keyword, postfix: null })); const fuzzySearch = (list, text, key) => { const fuse = new Fuse(list, { keys: [key] }); return fuse.search(text).map(({ item }) => item); }; class AbstractCachingCompletion { constructor(cache = {}) { _defineProperty(this, "cache", {}); this.cache = cache; } // eslint-disable-next-line class-methods-use-this, no-unused-vars calculateItems(type, query = null) { return []; } complete(types, query) { return types.map(typeData => { const cached = this.cache[typeData.type]; if (cached != null) { return cached; } return this.calculateItems(typeData, query); }).reduce((acc, items) => [...acc, ...items], []); } } class SchemaBasedCompletion extends AbstractCachingCompletion { constructor(schema = {}) { super({ [CompletionTypes.KEYWORD]: KEYWORD_ITEMS, [CompletionTypes.LABEL]: (schema.labels || []).map(label => ({ type: CompletionTypes.LABEL, view: label, content: ecsapeCypher(label), postfix: null })), [CompletionTypes.RELATIONSHIP_TYPE]: (schema.relationshipTypes || []).map(relType => ({ type: CompletionTypes.RELATIONSHIP_TYPE, view: relType, content: ecsapeCypher(relType), postfix: null })), [CompletionTypes.PROPERTY_KEY]: (schema.propertyKeys || []).map(propKey => ({ type: CompletionTypes.PROPERTY_KEY, view: propKey, content: ecsapeCypher(propKey), postfix: null })), [CompletionTypes.FUNCTION_NAME]: (schema.functions || []).map(({ name, signature }) => ({ type: CompletionTypes.FUNCTION_NAME, view: name, content: ecsapeCypher(name), postfix: signature })), [CompletionTypes.PROCEDURE_NAME]: (schema.procedures || []).map(({ name, signature }) => ({ type: CompletionTypes.PROCEDURE_NAME, view: name, content: name, postfix: signature })), [CompletionTypes.CONSOLE_COMMAND_NAME]: (schema.consoleCommands || []).map(consoleCommandName => ({ type: CompletionTypes.CONSOLE_COMMAND_NAME, view: consoleCommandName.name, content: consoleCommandName.name, postfix: consoleCommandName.description || null })), [CompletionTypes.PARAMETER]: (schema.parameters || []).map(parameter => ({ type: CompletionTypes.PARAMETER, view: parameter, content: parameter, postfix: null })) }); _defineProperty(this, "schema", {}); this.schema = schema; } calculateItems(typeData) { return (SchemaBasedCompletion.providers[typeData.type] || (() => []))(this.schema, typeData); } } _defineProperty(SchemaBasedCompletion, "providers", { [CompletionTypes.PROCEDURE_OUTPUT]: (schema, typeData) => { const findByName = e => e.name === typeData.name && e.returnItems !== []; const procedure = _find(schema.procedures, findByName); if (procedure) { return procedure.returnItems.map(({ name, signature }) => ({ type: CompletionTypes.PROCEDURE_OUTPUT, view: name, content: name, postfix: ` :: ${signature}` })); } return []; }, [CompletionTypes.CONSOLE_COMMAND_SUBCOMMAND]: (schema, typeData) => { const { filterLastElement, path } = typeData; const length = filterLastElement ? path.length - 1 : path.length; let currentLevel = schema.consoleCommands; for (let i = 0; i < length; i += 1) { const foundCommand = _find(currentLevel, ["name", path[i]]); if (foundCommand) { currentLevel = foundCommand.commands || []; } else { return []; } } return currentLevel.map(({ name, description }) => ({ type: CompletionTypes.CONSOLE_COMMAND_SUBCOMMAND, view: name, content: name, postfix: description || null })); } }); class QueryBasedCompletion extends AbstractCachingCompletion { constructor(referenceProviders = {}) { super(); _defineProperty(this, "providers", {}); _defineProperty(this, "emptyProvider", { getNames: () => [] }); this.providers = { [CompletionTypes.VARIABLE]: query => (referenceProviders.get(CypherTypes.VARIABLE_CONTEXT) || this.emptyProvider).getNames(query).map(name => ({ type: CompletionTypes.VARIABLE, view: name, content: name, postfix: null })) }; } calculateItems(typeData, query) { return (this.providers[typeData.type] || (() => []))(query); } } export class AutoCompletion { constructor() { _defineProperty(this, "queryBased", null); _defineProperty(this, "schemaBased", null); this.updateSchema({}); } getItems(types, { query = null, filter = "" }) { const text = filter.toLowerCase(); const filteredText = AutoCompletion.filterText(text); const completionItemFilter = () => true; const list = [this.queryBased, this.schemaBased].filter(s => s != null).map(t => t.complete(types, query)).reduce((acc, items) => [...acc, ...items], []).filter(completionItemFilter); if (filteredText) { return fuzzySearch(list, filteredText, "view"); } if (text) { return fuzzySearch(list, text, "view"); } return list; } updateSchema(schema) { this.schemaBased = new SchemaBasedCompletion(schema); } updateReferenceProviders(referenceProviders) { this.queryBased = new QueryBasedCompletion(referenceProviders); } /** * Define whether element should be replaced or not. */ static shouldBeReplaced(element) { if (element == null) { return false; } const text = element.getText(); const parent = element.parentCtx; // If element is whitespace if (/^\s+$/.test(text)) { return false; } // If element is opening bracket (e.g. start of relationship pattern) if (text === "[") { return false; } // If element is opening brace (e.g. start of node pattern) if (text === "(") { return false; } if (text === ".") { return false; } if (text === "{") { return false; } if (text === "$") { return false; } if (text === ":" && parent != null && parent instanceof CypherTypes.MAP_LITERAL_ENTRY) { return false; } return true; } static filterText(text) { if (text.startsWith("$")) { return text.slice(1); } return text; } // eslint-disable-next-line no-unused-vars static calculateSmartReplaceRange(element, start, stop) { // If we are in relationship type or label and we have error nodes in there. // This means that we typed in just ':' and Antlr consumed other tokens in element // In this case replace only ':' if (element instanceof CypherTypes.RELATIONSHIP_TYPE_CONTEXT || element instanceof CypherTypes.NODE_LABEL_CONTEXT) { if (TreeUtils.hasErrorNode(element)) { return { filterText: ":", start, stop: start }; } } return null; } }