@neo4j-cypher/editor-support
Version:
Core functionality to support Cypher integration into editors
194 lines (192 loc) • 7.38 kB
JavaScript
import _defineProperty from "@babel/runtime/helpers/defineProperty";
function ownKeys(object, enumerableOnly) { var keys = Object.keys(object); if (Object.getOwnPropertySymbols) { var symbols = Object.getOwnPropertySymbols(object); enumerableOnly && (symbols = symbols.filter(function (sym) { return Object.getOwnPropertyDescriptor(object, sym).enumerable; })), keys.push.apply(keys, symbols); } return keys; }
function _objectSpread(target) { for (var i = 1; i < arguments.length; i++) { var source = null != arguments[i] ? arguments[i] : {}; i % 2 ? ownKeys(Object(source), !0).forEach(function (key) { _defineProperty(target, key, source[key]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)) : ownKeys(Object(source)).forEach(function (key) { Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key)); }); } return target; }
import { CompletionTypeResolver } from "./completion/CompletionTypeResolver";
import { AutoCompletion } from "./completion/AutoCompletion";
import * as CypherTypes from "./lang/CypherTypes";
import { CypherSyntaxHighlight } from "./highlight/CypherSyntaxHighlight";
import { TreeUtils } from "./util/TreeUtils";
import { PositionConverter } from "./util/PositionConverter";
import { retryOperation } from "./util/retryOperation";
import { parse } from "./util/parse";
export class CypherEditorSupport {
constructor(input = "") {
_defineProperty(this, "schema", {});
_defineProperty(this, "input", null);
_defineProperty(this, "positionConverter", new PositionConverter(""));
_defineProperty(this, "parseTree", null);
_defineProperty(this, "parseErrors", []);
_defineProperty(this, "referencesProviders", new Map());
_defineProperty(this, "completion", new AutoCompletion());
_defineProperty(this, "queriesAndCommands", []);
_defineProperty(this, "statements", []);
_defineProperty(this, "listeners", []);
_defineProperty(this, "version", 0);
_defineProperty(this, "ensureVersion", (version, delay = 30, times = 5) => retryOperation(() => new Promise((resolve, reject) => {
if (version === this.version) {
return resolve();
}
return reject();
}), delay, times));
this.update(input);
}
on(eventName, cb) {
this.listeners[eventName] = Array.isArray(this.listeners[eventName]) ? this.listeners[eventName].concat([cb]) : this.listeners[eventName] = [cb];
}
off(eventName, cb) {
if (!this.listeners[eventName]) return;
const index = this.listeners[eventName].indexOf(cb);
if (index > -1) {
this.listeners[eventName].splice(index, 1);
}
}
trigger(eventName, args = []) {
if (!this.listeners[eventName]) return;
this.listeners[eventName].forEach(cb => cb(...args));
}
update(input = "", version) {
this.trigger("update");
if (input === this.input) {
this.version = version || this.version;
this.trigger("updated", [{
queriesAndCommands: this.queriesAndCommands,
referencesProviders: this.referencesProviders
}]);
return;
}
this.positionConverter = new PositionConverter(input);
this.input = input;
// const startTime = new Date();
const {
parseTree,
referencesListener,
errorListener,
referencesProviders
} = parse(input);
this.parseTree = parseTree;
// console.log('updated parse tree: ', input, version, 'time: ' + (new Date() - startTime));
this.parseErrors = errorListener.errors;
const {
queriesAndCommands,
statements
} = referencesListener;
this.statements = statements;
this.queriesAndCommands = queriesAndCommands;
this.referencesProviders = referencesProviders;
this.completion.updateReferenceProviders(this.referencesProviders);
this.version = version || this.version;
this.trigger("updated", [{
queriesAndCommands: this.queriesAndCommands,
referencesProviders: this.referencesProviders
}]);
}
setSchema(schema) {
this.schema = schema;
this.completion.updateSchema(this.schema);
}
getElement(line, column) {
const abs = this.positionConverter.toAbsolute(line, column);
function getElement(pt) {
const pos = TreeUtils.getPosition(pt);
if (pos != null && (abs < pos.start || abs > pos.stop)) {
return null;
}
const c = pt.getChildCount();
if (c === 0 && pos != null) {
return pt;
}
for (let i = 0; i < c; i += 1) {
const e = getElement(pt.getChild(i));
if (e != null) {
return e;
}
}
return pos != null ? pt : null;
}
return getElement(this.parseTree);
}
getReferences(line, column) {
const e = TreeUtils.findAnyParent(this.getElement(line, column), CypherTypes.SYMBOLIC_CONTEXTS);
if (e == null) {
return [];
}
const query = e instanceof CypherTypes.VARIABLE_CONTEXT ? TreeUtils.findAnyParent(e, [CypherTypes.QUERY_CONTEXT]) : null;
return this.referencesProviders.get(e).getReferences(e.getText(), query);
}
getCompletionInfo(line, column) {
const element = this.getElementForCompletion(line, column);
const query = TreeUtils.findAnyParent(element, [CypherTypes.QUERY_CONTEXT]);
const {
found,
types
} = CompletionTypeResolver.getTypes(element);
return {
element,
query,
found,
types
};
}
getElementForCompletion(line, column) {
const e = this.getElement(line, column);
return TreeUtils.findAnyParent(e, CypherTypes.COMPLETION_CANDIDATES) || e;
}
getCompletion(line, column, doFilter = true) {
let info = this.getCompletionInfo(line, column);
// Shift by one symbol back and try again.
if (!info.found && column > 0) {
const prevInfo = this.getCompletionInfo(line, column - 1);
if (prevInfo.found) {
info = prevInfo;
}
}
const {
element,
query,
found,
types
} = info;
const replaceRange = {
from: {
line,
column
},
to: {
line,
column
}
};
let filter = null;
const shouldBeReplaced = AutoCompletion.shouldBeReplaced(element);
if (found && shouldBeReplaced) {
// There are number of situations where we need to be smarter than default behavior
const {
start,
stop
} = TreeUtils.getPosition(element);
const smartReplaceRange = AutoCompletion.calculateSmartReplaceRange(element, start, stop);
if (smartReplaceRange) {
replaceRange.from = this.positionConverter.toRelative(smartReplaceRange.start);
replaceRange.to = this.positionConverter.toRelative(smartReplaceRange.stop + 1);
if (smartReplaceRange.filterText) {
filter = smartReplaceRange.filterText;
}
} else {
replaceRange.from = this.positionConverter.toRelative(start);
replaceRange.to = this.positionConverter.toRelative(stop + 1);
}
}
if (filter === null) {
filter = doFilter && found && shouldBeReplaced ? element.getText() : "";
}
return _objectSpread({
items: this.completion.getItems(types, {
filter,
query
})
}, replaceRange);
}
applyHighlighthing(callback) {
CypherSyntaxHighlight.process(this.parseTree, callback);
}
}