2020-11-03 13:05:40 +00:00
|
|
|
/* eslint-disable @typescript-eslint/ban-ts-comment */
|
2020-05-20 11:30:53 +00:00
|
|
|
// Heavily inspired from https://github.com/codemirror/CodeMirror/blob/master/addon/tern/tern.js
|
|
|
|
|
import { DataTree } from "entities/DataTree/dataTreeFactory";
|
2020-06-18 14:16:49 +00:00
|
|
|
import tern, { Server, Def } from "tern";
|
2020-05-20 11:30:53 +00:00
|
|
|
import ecma from "tern/defs/ecmascript.json";
|
2020-06-12 10:56:46 +00:00
|
|
|
import lodash from "constants/defs/lodash.json";
|
2020-08-14 07:43:01 +00:00
|
|
|
import base64 from "constants/defs/base64-js.json";
|
2020-09-08 08:43:46 +00:00
|
|
|
import moment from "constants/defs/moment.json";
|
2020-12-15 08:46:33 +00:00
|
|
|
import xmlJs from "constants/defs/xmlParser.json";
|
2020-05-20 11:30:53 +00:00
|
|
|
import { dataTreeTypeDefCreator } from "utils/autocomplete/dataTreeTypeDefCreator";
|
2021-02-16 10:29:08 +00:00
|
|
|
import { customTreeTypeDefCreator } from "utils/autocomplete/customTreeTypeDefCreator";
|
2020-05-20 11:30:53 +00:00
|
|
|
import CodeMirror, { Hint, Pos, cmpPos } from "codemirror";
|
2020-07-14 10:30:33 +00:00
|
|
|
import {
|
|
|
|
|
getDynamicStringSegments,
|
|
|
|
|
isDynamicValue,
|
|
|
|
|
} from "utils/DynamicBindingUtils";
|
2020-05-20 11:30:53 +00:00
|
|
|
|
2020-12-15 08:46:33 +00:00
|
|
|
const DEFS = [ecma, lodash, base64, moment, xmlJs];
|
2020-05-20 11:30:53 +00:00
|
|
|
const bigDoc = 250;
|
|
|
|
|
const cls = "CodeMirror-Tern-";
|
|
|
|
|
const hintDelay = 1700;
|
|
|
|
|
|
|
|
|
|
type Completion = Hint & {
|
|
|
|
|
origin: string;
|
2020-08-05 07:33:44 +00:00
|
|
|
type: DataType;
|
2020-05-20 11:30:53 +00:00
|
|
|
data: {
|
|
|
|
|
doc: string;
|
|
|
|
|
};
|
2021-05-27 10:01:26 +00:00
|
|
|
render?: any;
|
|
|
|
|
isHeader?: boolean;
|
2020-05-20 11:30:53 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
type TernDocs = Record<string, TernDoc>;
|
|
|
|
|
|
|
|
|
|
type TernDoc = {
|
|
|
|
|
doc: CodeMirror.Doc;
|
|
|
|
|
name: string;
|
|
|
|
|
changed: { to: number; from: number } | null;
|
|
|
|
|
};
|
|
|
|
|
|
2020-08-05 07:33:44 +00:00
|
|
|
export type DataType =
|
|
|
|
|
| "OBJECT"
|
|
|
|
|
| "NUMBER"
|
|
|
|
|
| "ARRAY"
|
|
|
|
|
| "FUNCTION"
|
|
|
|
|
| "BOOLEAN"
|
|
|
|
|
| "STRING"
|
|
|
|
|
| "UNKNOWN";
|
|
|
|
|
|
2020-05-20 11:30:53 +00:00
|
|
|
type ArgHints = {
|
|
|
|
|
start: CodeMirror.Position;
|
|
|
|
|
type: { args: any[]; rettype: null | string };
|
|
|
|
|
name: string;
|
|
|
|
|
guess: boolean;
|
|
|
|
|
doc: CodeMirror.Doc;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
class TernServer {
|
|
|
|
|
server: Server;
|
|
|
|
|
docs: TernDocs = Object.create(null);
|
|
|
|
|
cachedArgHints: ArgHints | null = null;
|
2021-05-27 10:01:26 +00:00
|
|
|
active: any;
|
|
|
|
|
expected?: string;
|
2020-05-20 11:30:53 +00:00
|
|
|
|
2021-02-16 10:29:08 +00:00
|
|
|
constructor(
|
|
|
|
|
dataTree: DataTree,
|
|
|
|
|
additionalDataTree?: Record<string, Record<string, unknown>>,
|
|
|
|
|
) {
|
2020-05-20 11:30:53 +00:00
|
|
|
const dataTreeDef = dataTreeTypeDefCreator(dataTree);
|
2021-02-16 10:29:08 +00:00
|
|
|
let customDataTreeDef = undefined;
|
|
|
|
|
if (additionalDataTree) {
|
|
|
|
|
customDataTreeDef = customTreeTypeDefCreator(additionalDataTree);
|
|
|
|
|
}
|
2020-05-20 11:30:53 +00:00
|
|
|
this.server = new tern.Server({
|
|
|
|
|
async: true,
|
2021-02-16 10:29:08 +00:00
|
|
|
defs: customDataTreeDef
|
|
|
|
|
? [...DEFS, dataTreeDef, customDataTreeDef]
|
|
|
|
|
: [...DEFS, dataTreeDef],
|
2020-05-20 11:30:53 +00:00
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2021-05-27 10:01:26 +00:00
|
|
|
complete(cm: CodeMirror.Editor, expected: string) {
|
|
|
|
|
this.expected = expected;
|
|
|
|
|
cm.showHint({
|
|
|
|
|
hint: this.getHint.bind(this),
|
|
|
|
|
completeSingle: false,
|
|
|
|
|
extraKeys: {
|
|
|
|
|
Up: (cm: CodeMirror.Editor, handle: any) => {
|
|
|
|
|
handle.moveFocus(-1);
|
|
|
|
|
if (this.active.isHeader === true) {
|
|
|
|
|
handle.moveFocus(-1);
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
Down: (cm: CodeMirror.Editor, handle: any) => {
|
|
|
|
|
handle.moveFocus(1);
|
|
|
|
|
if (this.active.isHeader === true) {
|
|
|
|
|
handle.moveFocus(1);
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
});
|
2020-05-20 11:30:53 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
showType(cm: CodeMirror.Editor) {
|
|
|
|
|
this.showContextInfo(cm, "type");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
showDocs(cm: CodeMirror.Editor) {
|
|
|
|
|
this.showContextInfo(cm, "documentation", (data: any) => {
|
|
|
|
|
if (data.url) {
|
|
|
|
|
window.open(data.url, "_blank");
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2020-06-18 14:16:49 +00:00
|
|
|
updateDef(name: string, def: Def) {
|
|
|
|
|
this.server.deleteDefs(name);
|
2020-11-03 13:05:40 +00:00
|
|
|
// @ts-ignore: No types available
|
2020-06-18 14:16:49 +00:00
|
|
|
this.server.addDefs(def, true);
|
|
|
|
|
}
|
|
|
|
|
|
2020-11-03 13:05:40 +00:00
|
|
|
requestCallback(error: any, data: any, cm: CodeMirror.Editor, resolve: any) {
|
2020-07-14 10:30:33 +00:00
|
|
|
if (error) return this.showError(cm, error);
|
|
|
|
|
if (data.completions.length === 0) {
|
|
|
|
|
return this.showError(cm, "No suggestions");
|
|
|
|
|
}
|
|
|
|
|
const doc = this.findDoc(cm.getDoc());
|
|
|
|
|
const cursor = cm.getCursor();
|
|
|
|
|
const lineValue = this.lineValue(doc);
|
|
|
|
|
const focusedValue = this.getFocusedDynamicValue(doc);
|
|
|
|
|
const index = lineValue.indexOf(focusedValue);
|
|
|
|
|
let completions: Completion[] = [];
|
|
|
|
|
let after = "";
|
2021-05-13 08:35:39 +00:00
|
|
|
const { end, start } = data;
|
2020-07-14 10:30:33 +00:00
|
|
|
const from = {
|
|
|
|
|
...start,
|
|
|
|
|
ch: start.ch + index,
|
|
|
|
|
line: cursor.line,
|
|
|
|
|
};
|
|
|
|
|
const to = {
|
|
|
|
|
...end,
|
|
|
|
|
ch: end.ch + index,
|
|
|
|
|
line: cursor.line,
|
|
|
|
|
};
|
|
|
|
|
if (
|
|
|
|
|
cm.getRange(Pos(from.line, from.ch - 2), from) === '["' &&
|
|
|
|
|
cm.getRange(to, Pos(to.line, to.ch + 2)) !== '"]'
|
|
|
|
|
) {
|
|
|
|
|
after = '"]';
|
|
|
|
|
}
|
|
|
|
|
for (let i = 0; i < data.completions.length; ++i) {
|
|
|
|
|
const completion = data.completions[i];
|
|
|
|
|
let className = this.typeToIcon(completion.type);
|
2021-05-27 10:01:26 +00:00
|
|
|
const dataType = this.getDataType(completion.type);
|
2020-07-14 10:30:33 +00:00
|
|
|
if (data.guess) className += " " + cls + "guess";
|
|
|
|
|
completions.push({
|
|
|
|
|
text: completion.name + after,
|
|
|
|
|
displayText: completion.displayName || completion.name,
|
|
|
|
|
className: className,
|
|
|
|
|
data: completion,
|
|
|
|
|
origin: completion.origin,
|
2021-05-27 10:01:26 +00:00
|
|
|
type: dataType,
|
2020-07-14 10:30:33 +00:00
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
completions = this.sortCompletions(completions);
|
2021-05-27 10:01:26 +00:00
|
|
|
const indexToBeSelected = completions.length > 1 ? 1 : 0;
|
|
|
|
|
const obj = {
|
|
|
|
|
from: from,
|
|
|
|
|
to: to,
|
|
|
|
|
list: completions,
|
|
|
|
|
selectedHint: indexToBeSelected,
|
|
|
|
|
};
|
2020-07-14 10:30:33 +00:00
|
|
|
let tooltip: HTMLElement | undefined = undefined;
|
|
|
|
|
CodeMirror.on(obj, "close", () => this.remove(tooltip));
|
|
|
|
|
CodeMirror.on(obj, "update", () => this.remove(tooltip));
|
|
|
|
|
CodeMirror.on(
|
|
|
|
|
obj,
|
|
|
|
|
"select",
|
|
|
|
|
(cur: { data: { doc: string } }, node: any) => {
|
2021-05-27 10:01:26 +00:00
|
|
|
this.active = cur;
|
2020-07-14 10:30:33 +00:00
|
|
|
this.remove(tooltip);
|
|
|
|
|
const content = cur.data.doc;
|
|
|
|
|
if (content) {
|
|
|
|
|
tooltip = this.makeTooltip(
|
|
|
|
|
node.parentNode.getBoundingClientRect().right + window.pageXOffset,
|
|
|
|
|
node.getBoundingClientRect().top + window.pageYOffset,
|
|
|
|
|
content,
|
|
|
|
|
);
|
|
|
|
|
tooltip.className += " " + cls + "hint-doc";
|
2020-08-19 09:21:32 +00:00
|
|
|
CodeMirror.on(
|
|
|
|
|
cm,
|
|
|
|
|
"keyup",
|
|
|
|
|
(cm: CodeMirror.Editor, keyboardEvent: KeyboardEvent) => {
|
|
|
|
|
if (
|
|
|
|
|
keyboardEvent.code === "Space" &&
|
|
|
|
|
keyboardEvent.ctrlKey &&
|
|
|
|
|
tooltip
|
|
|
|
|
) {
|
2020-08-20 06:23:25 +00:00
|
|
|
tooltip.className += " visible";
|
2020-08-19 09:21:32 +00:00
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
);
|
2020-07-14 10:30:33 +00:00
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
);
|
|
|
|
|
resolve(obj);
|
|
|
|
|
|
|
|
|
|
return obj;
|
|
|
|
|
}
|
|
|
|
|
|
2020-05-20 11:30:53 +00:00
|
|
|
getHint(cm: CodeMirror.Editor) {
|
2020-12-24 04:32:25 +00:00
|
|
|
return new Promise((resolve) => {
|
2020-05-20 11:30:53 +00:00
|
|
|
this.request(
|
|
|
|
|
cm,
|
|
|
|
|
{
|
|
|
|
|
type: "completions",
|
|
|
|
|
types: true,
|
|
|
|
|
docs: true,
|
|
|
|
|
urls: true,
|
|
|
|
|
origins: true,
|
2020-07-01 10:01:07 +00:00
|
|
|
caseInsensitive: true,
|
2020-08-19 09:21:32 +00:00
|
|
|
guess: false,
|
2020-05-20 11:30:53 +00:00
|
|
|
},
|
2020-07-14 10:30:33 +00:00
|
|
|
(error, data) => this.requestCallback(error, data, cm, resolve),
|
2020-05-20 11:30:53 +00:00
|
|
|
);
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
sortCompletions(completions: Completion[]) {
|
|
|
|
|
// Add data tree completions before others
|
2021-05-27 10:01:26 +00:00
|
|
|
const expectedDataType = this.getExpectedDataType();
|
2020-05-20 11:30:53 +00:00
|
|
|
const dataTreeCompletions = completions
|
2020-12-24 04:32:25 +00:00
|
|
|
.filter((c) => c.origin === "dataTree")
|
2020-08-05 07:33:44 +00:00
|
|
|
.sort((a: Completion, b: Completion) => {
|
|
|
|
|
if (a.type === "FUNCTION" && b.type !== "FUNCTION") {
|
|
|
|
|
return 1;
|
|
|
|
|
} else if (a.type !== "FUNCTION" && b.type === "FUNCTION") {
|
|
|
|
|
return -1;
|
|
|
|
|
}
|
2020-05-20 11:30:53 +00:00
|
|
|
return a.text.toLowerCase().localeCompare(b.text.toLowerCase());
|
|
|
|
|
});
|
2021-05-27 10:01:26 +00:00
|
|
|
const sameDataType = dataTreeCompletions.filter(
|
|
|
|
|
(c) => c.type === expectedDataType,
|
|
|
|
|
);
|
|
|
|
|
const otherDataType = dataTreeCompletions.filter(
|
|
|
|
|
(c) => c.type !== expectedDataType,
|
|
|
|
|
);
|
|
|
|
|
if (otherDataType.length && sameDataType.length) {
|
|
|
|
|
const otherDataTitle: Completion = {
|
|
|
|
|
text: "Search results",
|
|
|
|
|
displayText: "Search results",
|
|
|
|
|
className: "CodeMirror-hint-header",
|
|
|
|
|
data: { doc: "" },
|
|
|
|
|
origin: "",
|
|
|
|
|
type: "UNKNOWN",
|
|
|
|
|
isHeader: true,
|
|
|
|
|
};
|
|
|
|
|
const sameDataTitle: Completion = {
|
|
|
|
|
text: "Best Match",
|
|
|
|
|
displayText: "Best Match",
|
|
|
|
|
className: "CodeMirror-hint-header",
|
|
|
|
|
data: { doc: "" },
|
|
|
|
|
origin: "",
|
|
|
|
|
type: "UNKNOWN",
|
|
|
|
|
isHeader: true,
|
|
|
|
|
};
|
|
|
|
|
sameDataType.unshift(sameDataTitle);
|
|
|
|
|
otherDataType.unshift(otherDataTitle);
|
|
|
|
|
}
|
2020-12-24 04:32:25 +00:00
|
|
|
const docCompletetions = completions.filter((c) => c.origin === "[doc]");
|
2020-06-03 17:27:24 +00:00
|
|
|
const otherCompletions = completions.filter(
|
2020-12-24 04:32:25 +00:00
|
|
|
(c) => c.origin !== "dataTree" && c.origin !== "[doc]",
|
2020-06-03 17:27:24 +00:00
|
|
|
);
|
2021-05-27 10:01:26 +00:00
|
|
|
return [
|
|
|
|
|
...docCompletetions,
|
|
|
|
|
...sameDataType,
|
|
|
|
|
...otherDataType,
|
|
|
|
|
...otherCompletions,
|
|
|
|
|
];
|
2020-05-20 11:30:53 +00:00
|
|
|
}
|
|
|
|
|
|
2020-08-05 07:33:44 +00:00
|
|
|
getDataType(type: string): DataType {
|
|
|
|
|
if (type === "?") return "UNKNOWN";
|
|
|
|
|
else if (type === "number") return "NUMBER";
|
|
|
|
|
else if (type === "string") return "STRING";
|
|
|
|
|
else if (type === "bool") return "BOOLEAN";
|
2021-05-27 10:01:26 +00:00
|
|
|
else if (type === "array") return "ARRAY";
|
2020-08-05 07:33:44 +00:00
|
|
|
else if (/^fn\(/.test(type)) return "FUNCTION";
|
|
|
|
|
else if (/^\[/.test(type)) return "ARRAY";
|
|
|
|
|
else return "OBJECT";
|
|
|
|
|
}
|
|
|
|
|
|
2021-05-27 10:01:26 +00:00
|
|
|
getExpectedDataType() {
|
|
|
|
|
const type = this.expected;
|
|
|
|
|
if (type === "Array<Object>" || type === "Array") return "ARRAY";
|
|
|
|
|
if (type === "boolean") return "BOOLEAN";
|
|
|
|
|
if (type === "string") return "STRING";
|
|
|
|
|
if (type === "number") return "NUMBER";
|
|
|
|
|
if (type === "object" || type === "JSON") return "OBJECT";
|
|
|
|
|
if (type === undefined) return "UNKNOWN";
|
|
|
|
|
return undefined;
|
|
|
|
|
}
|
|
|
|
|
|
2020-05-20 11:30:53 +00:00
|
|
|
typeToIcon(type: string) {
|
|
|
|
|
let suffix;
|
|
|
|
|
if (type === "?") suffix = "unknown";
|
|
|
|
|
else if (type === "number" || type === "string" || type === "bool")
|
|
|
|
|
suffix = type;
|
|
|
|
|
else if (/^fn\(/.test(type)) suffix = "fn";
|
|
|
|
|
else if (/^\[/.test(type)) suffix = "array";
|
|
|
|
|
else suffix = "object";
|
|
|
|
|
return cls + "completion " + cls + "completion-" + suffix;
|
|
|
|
|
}
|
|
|
|
|
|
2020-11-03 13:05:40 +00:00
|
|
|
showContextInfo(cm: CodeMirror.Editor, queryName: string, callbackFn?: any) {
|
2020-05-20 11:30:53 +00:00
|
|
|
this.request(cm, { type: queryName }, (error, data) => {
|
|
|
|
|
if (error) return this.showError(cm, error);
|
|
|
|
|
const tip = this.elt(
|
|
|
|
|
"span",
|
|
|
|
|
null,
|
|
|
|
|
this.elt("strong", null, data.type || "not found"),
|
|
|
|
|
);
|
|
|
|
|
if (data.doc) tip.appendChild(document.createTextNode(" — " + data.doc));
|
|
|
|
|
if (data.url) {
|
|
|
|
|
tip.appendChild(document.createTextNode(" "));
|
|
|
|
|
const child = tip.appendChild(this.elt("a", null, "[docs]"));
|
2020-11-03 13:05:40 +00:00
|
|
|
// @ts-ignore: No types available
|
2020-05-20 11:30:53 +00:00
|
|
|
child.href = data.url;
|
|
|
|
|
|
2020-11-03 13:05:40 +00:00
|
|
|
// @ts-ignore: No types available
|
2020-05-20 11:30:53 +00:00
|
|
|
child.target = "_blank";
|
|
|
|
|
}
|
|
|
|
|
this.tempTooltip(cm, tip);
|
|
|
|
|
if (callbackFn) callbackFn(data);
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
request(
|
|
|
|
|
cm: CodeMirror.Editor,
|
|
|
|
|
query: {
|
|
|
|
|
type: string;
|
|
|
|
|
types?: boolean;
|
|
|
|
|
docs?: boolean;
|
|
|
|
|
urls?: boolean;
|
|
|
|
|
origins?: boolean;
|
2020-07-01 10:01:07 +00:00
|
|
|
caseInsensitive?: boolean;
|
2020-05-20 11:30:53 +00:00
|
|
|
preferFunction?: boolean;
|
|
|
|
|
end?: CodeMirror.Position;
|
2020-08-19 09:21:32 +00:00
|
|
|
guess?: boolean;
|
2020-05-20 11:30:53 +00:00
|
|
|
},
|
|
|
|
|
callbackFn: (error: any, data: any) => void,
|
|
|
|
|
pos?: CodeMirror.Position,
|
|
|
|
|
) {
|
|
|
|
|
const doc = this.findDoc(cm.getDoc());
|
|
|
|
|
const request = this.buildRequest(doc, query, pos);
|
2020-11-03 13:05:40 +00:00
|
|
|
// @ts-ignore: No types available
|
2020-05-20 11:30:53 +00:00
|
|
|
this.server.request(request, callbackFn);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
findDoc(doc: CodeMirror.Doc, name?: string): TernDoc {
|
|
|
|
|
for (const n in this.docs) {
|
|
|
|
|
const cur = this.docs[n];
|
|
|
|
|
if (cur.doc === doc) return cur;
|
|
|
|
|
}
|
|
|
|
|
if (!name) {
|
|
|
|
|
let n;
|
|
|
|
|
for (let i = 0; ; ++i) {
|
|
|
|
|
n = "[doc" + (i || "") + "]";
|
|
|
|
|
if (!this.docs[n]) {
|
|
|
|
|
name = n;
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return this.addDoc(name, doc);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
addDoc(name: string, doc: CodeMirror.Doc) {
|
|
|
|
|
const data = { doc: doc, name: name, changed: null };
|
2020-07-14 10:30:33 +00:00
|
|
|
this.server.addFile(name, this.getFocusedDynamicValue(data));
|
2020-05-20 11:30:53 +00:00
|
|
|
CodeMirror.on(doc, "change", this.trackChange.bind(this));
|
|
|
|
|
return (this.docs[name] = data);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
buildRequest(
|
|
|
|
|
doc: TernDoc,
|
|
|
|
|
query: {
|
|
|
|
|
type?: string;
|
|
|
|
|
types?: boolean;
|
|
|
|
|
docs?: boolean;
|
|
|
|
|
urls?: boolean;
|
|
|
|
|
origins?: boolean;
|
|
|
|
|
fullDocs?: any;
|
|
|
|
|
lineCharPositions?: any;
|
|
|
|
|
end?: any;
|
|
|
|
|
start?: any;
|
|
|
|
|
file?: any;
|
|
|
|
|
},
|
|
|
|
|
pos?: CodeMirror.Position,
|
|
|
|
|
) {
|
|
|
|
|
const files = [];
|
|
|
|
|
let offsetLines = 0;
|
|
|
|
|
const allowFragments = !query.fullDocs;
|
|
|
|
|
if (!allowFragments) delete query.fullDocs;
|
|
|
|
|
query.lineCharPositions = true;
|
|
|
|
|
if (!query.end) {
|
2020-07-14 10:30:33 +00:00
|
|
|
const lineValue = this.lineValue(doc);
|
|
|
|
|
const focusedValue = this.getFocusedDynamicValue(doc);
|
|
|
|
|
const index = lineValue.indexOf(focusedValue);
|
|
|
|
|
|
|
|
|
|
const positions = pos || doc.doc.getCursor("end");
|
|
|
|
|
const queryChPosition = positions.ch - index;
|
|
|
|
|
|
|
|
|
|
query.end = {
|
|
|
|
|
...positions,
|
|
|
|
|
line: 0,
|
|
|
|
|
ch: queryChPosition,
|
|
|
|
|
};
|
|
|
|
|
|
2020-05-20 11:30:53 +00:00
|
|
|
if (doc.doc.somethingSelected()) {
|
|
|
|
|
query.start = doc.doc.getCursor("start");
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
const startPos = query.start || query.end;
|
|
|
|
|
if (doc.changed) {
|
|
|
|
|
if (
|
|
|
|
|
doc.doc.lineCount() > bigDoc &&
|
|
|
|
|
allowFragments &&
|
|
|
|
|
doc.changed.to - doc.changed.from < 100 &&
|
|
|
|
|
doc.changed.from <= startPos.line &&
|
|
|
|
|
doc.changed.to > query.end.line
|
|
|
|
|
) {
|
|
|
|
|
files.push(this.getFragmentAround(doc, startPos, query.end));
|
|
|
|
|
query.file = "#0";
|
|
|
|
|
offsetLines = files[0].offsetLines;
|
|
|
|
|
if (query.start) {
|
|
|
|
|
query.start = Pos(query.start.line - -offsetLines, query.start.ch);
|
|
|
|
|
}
|
|
|
|
|
query.end = Pos(query.end.line - offsetLines, query.end.ch);
|
|
|
|
|
} else {
|
|
|
|
|
files.push({
|
|
|
|
|
type: "full",
|
|
|
|
|
name: doc.name,
|
2020-07-14 10:30:33 +00:00
|
|
|
text: this.getFocusedDynamicValue(doc),
|
2020-05-20 11:30:53 +00:00
|
|
|
});
|
|
|
|
|
query.file = doc.name;
|
|
|
|
|
doc.changed = null;
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
query.file = doc.name;
|
|
|
|
|
}
|
|
|
|
|
for (const name in this.docs) {
|
|
|
|
|
const cur = this.docs[name];
|
|
|
|
|
if (cur.changed && cur !== doc) {
|
|
|
|
|
files.push({
|
|
|
|
|
type: "full",
|
|
|
|
|
name: cur.name,
|
2020-07-14 10:30:33 +00:00
|
|
|
text: this.getFocusedDynamicValue(cur),
|
2020-05-20 11:30:53 +00:00
|
|
|
});
|
|
|
|
|
cur.changed = null;
|
|
|
|
|
}
|
|
|
|
|
}
|
2020-07-14 10:30:33 +00:00
|
|
|
|
2020-05-20 11:30:53 +00:00
|
|
|
return { query: query, files: files };
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
trackChange(
|
|
|
|
|
doc: CodeMirror.Doc,
|
|
|
|
|
change: {
|
|
|
|
|
to: CodeMirror.Position;
|
|
|
|
|
from: CodeMirror.Position;
|
|
|
|
|
text: string | any[];
|
|
|
|
|
},
|
|
|
|
|
) {
|
|
|
|
|
const data = this.findDoc(doc);
|
|
|
|
|
|
|
|
|
|
const argHints = this.cachedArgHints;
|
|
|
|
|
if (
|
|
|
|
|
argHints &&
|
|
|
|
|
argHints.doc === doc &&
|
|
|
|
|
cmpPos(argHints.start, change.to) >= 0
|
|
|
|
|
)
|
|
|
|
|
this.cachedArgHints = null;
|
|
|
|
|
|
|
|
|
|
let changed = data.changed;
|
|
|
|
|
if (changed === null)
|
|
|
|
|
data.changed = changed = { from: change.from.line, to: change.from.line };
|
|
|
|
|
const end = change.from.line + (change.text.length - 1);
|
|
|
|
|
if (change.from.line < changed.to)
|
|
|
|
|
changed.to = changed.to - (change.to.line - end);
|
|
|
|
|
if (end >= changed.to) changed.to = end + 1;
|
|
|
|
|
if (changed.from > change.from.line) changed.from = change.from.line;
|
|
|
|
|
|
|
|
|
|
if (doc.lineCount() > bigDoc && changed.to - changed.from > 100)
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
if (data.changed && data.changed.to - data.changed.from > 100)
|
|
|
|
|
this.sendDoc(data);
|
|
|
|
|
}, 200);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
sendDoc(doc: TernDoc) {
|
|
|
|
|
this.server.request(
|
2020-07-14 10:30:33 +00:00
|
|
|
{
|
2020-11-03 13:05:40 +00:00
|
|
|
// @ts-ignore: No types available
|
2020-07-14 10:30:33 +00:00
|
|
|
files: [
|
2020-11-03 13:05:40 +00:00
|
|
|
// @ts-ignore: No types available
|
2020-07-14 10:30:33 +00:00
|
|
|
{
|
|
|
|
|
type: "full",
|
|
|
|
|
name: doc.name,
|
|
|
|
|
text: this.getFocusedDynamicValue(doc),
|
|
|
|
|
},
|
|
|
|
|
],
|
|
|
|
|
},
|
2020-05-20 11:30:53 +00:00
|
|
|
function(error: Error) {
|
|
|
|
|
if (error) window.console.error(error);
|
|
|
|
|
else doc.changed = null;
|
|
|
|
|
},
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2020-07-14 10:30:33 +00:00
|
|
|
lineValue(doc: TernDoc) {
|
|
|
|
|
const cursor = doc.doc.getCursor();
|
|
|
|
|
|
|
|
|
|
return doc.doc.getLine(cursor.line);
|
|
|
|
|
}
|
|
|
|
|
|
2020-05-20 11:30:53 +00:00
|
|
|
docValue(doc: TernDoc) {
|
|
|
|
|
return doc.doc.getValue();
|
|
|
|
|
}
|
|
|
|
|
|
2020-07-14 10:30:33 +00:00
|
|
|
getFocusedDynamicValue(doc: TernDoc) {
|
|
|
|
|
const cursor = doc.doc.getCursor();
|
|
|
|
|
const value = this.lineValue(doc);
|
|
|
|
|
const stringSegments = getDynamicStringSegments(value);
|
2020-12-24 04:32:25 +00:00
|
|
|
const dynamicStrings = stringSegments.filter((segment) => {
|
2020-07-14 10:30:33 +00:00
|
|
|
if (isDynamicValue(segment)) {
|
|
|
|
|
const index = value.indexOf(segment);
|
|
|
|
|
|
|
|
|
|
if (cursor.ch >= index && cursor.ch <= index + segment.length) {
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return false;
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
return dynamicStrings.length ? dynamicStrings[0] : value;
|
|
|
|
|
}
|
|
|
|
|
|
2020-05-20 11:30:53 +00:00
|
|
|
getFragmentAround(
|
|
|
|
|
data: TernDoc,
|
|
|
|
|
start: CodeMirror.Position,
|
|
|
|
|
end: CodeMirror.Position,
|
|
|
|
|
) {
|
|
|
|
|
const doc = data.doc;
|
|
|
|
|
let minIndent = null;
|
|
|
|
|
let minLine = null;
|
|
|
|
|
let endLine;
|
|
|
|
|
const tabSize = 4;
|
|
|
|
|
for (let p = start.line - 1, min = Math.max(0, p - 50); p >= min; --p) {
|
|
|
|
|
const line = doc.getLine(p),
|
|
|
|
|
fn = line.search(/\bfunction\b/);
|
|
|
|
|
if (fn < 0) continue;
|
|
|
|
|
const indent = CodeMirror.countColumn(line, null, tabSize);
|
|
|
|
|
if (minIndent != null && minIndent <= indent) continue;
|
|
|
|
|
minIndent = indent;
|
|
|
|
|
minLine = p;
|
|
|
|
|
}
|
|
|
|
|
if (minLine === null) minLine = Math.max(0, start.line - 1);
|
|
|
|
|
const max = Math.min(doc.lastLine(), end.line + 20);
|
|
|
|
|
if (
|
|
|
|
|
minIndent === null ||
|
|
|
|
|
minIndent ===
|
|
|
|
|
CodeMirror.countColumn(doc.getLine(start.line), null, tabSize)
|
|
|
|
|
)
|
|
|
|
|
endLine = max;
|
|
|
|
|
else
|
|
|
|
|
for (endLine = end.line + 1; endLine < max; ++endLine) {
|
|
|
|
|
const indent = CodeMirror.countColumn(
|
|
|
|
|
doc.getLine(endLine),
|
|
|
|
|
null,
|
|
|
|
|
tabSize,
|
|
|
|
|
);
|
|
|
|
|
if (indent <= minIndent) break;
|
|
|
|
|
}
|
|
|
|
|
const from = Pos(minLine, 0);
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
type: "part",
|
|
|
|
|
name: data.name,
|
|
|
|
|
offsetLines: from.line,
|
|
|
|
|
text: doc.getRange(
|
|
|
|
|
from,
|
|
|
|
|
Pos(endLine, end.line === endLine ? undefined : 0),
|
|
|
|
|
),
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
showError(cm: CodeMirror.Editor, msg: string) {
|
|
|
|
|
this.tempTooltip(cm, String(msg));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
tempTooltip(cm: CodeMirror.Editor, content: HTMLElement | string) {
|
|
|
|
|
if (cm.state.ternTooltip) this.remove(cm.state.ternTooltip);
|
|
|
|
|
if (cm.state.completionActive) {
|
2020-11-03 13:05:40 +00:00
|
|
|
// @ts-ignore: No types available
|
2020-05-20 11:30:53 +00:00
|
|
|
cm.closeHint();
|
|
|
|
|
}
|
|
|
|
|
const where = cm.cursorCoords();
|
|
|
|
|
const tip = (cm.state.ternTooltip = this.makeTooltip(
|
2020-11-03 13:05:40 +00:00
|
|
|
// @ts-ignore: No types available
|
2020-05-20 11:30:53 +00:00
|
|
|
where.right + 1,
|
|
|
|
|
where.bottom,
|
|
|
|
|
content,
|
|
|
|
|
));
|
|
|
|
|
const maybeClear = () => {
|
|
|
|
|
old = true;
|
|
|
|
|
if (!mouseOnTip) clear();
|
|
|
|
|
};
|
|
|
|
|
const clear = () => {
|
|
|
|
|
cm.state.ternTooltip = null;
|
|
|
|
|
if (tip.parentNode) this.fadeOut(tip);
|
|
|
|
|
clearActivity();
|
|
|
|
|
};
|
|
|
|
|
let mouseOnTip = false;
|
|
|
|
|
let old = false;
|
|
|
|
|
CodeMirror.on(tip, "mousemove", function() {
|
|
|
|
|
mouseOnTip = true;
|
|
|
|
|
});
|
|
|
|
|
CodeMirror.on(tip, "mouseout", function(e: MouseEvent) {
|
|
|
|
|
const related = e.relatedTarget;
|
2020-11-03 13:05:40 +00:00
|
|
|
// @ts-ignore: No types available
|
2020-05-20 11:30:53 +00:00
|
|
|
if (!related || !CodeMirror.contains(tip, related)) {
|
|
|
|
|
if (old) clear();
|
|
|
|
|
else mouseOnTip = false;
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
setTimeout(maybeClear, hintDelay);
|
|
|
|
|
const clearActivity = this.onEditorActivity(cm, clear);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
onEditorActivity(
|
|
|
|
|
cm: CodeMirror.Editor,
|
|
|
|
|
f: (instance: CodeMirror.Editor) => void,
|
|
|
|
|
) {
|
|
|
|
|
cm.on("cursorActivity", f);
|
|
|
|
|
cm.on("blur", f);
|
|
|
|
|
cm.on("scroll", f);
|
|
|
|
|
cm.on("setDoc", f);
|
|
|
|
|
return function() {
|
|
|
|
|
cm.off("cursorActivity", f);
|
|
|
|
|
cm.off("blur", f);
|
|
|
|
|
cm.off("scroll", f);
|
|
|
|
|
cm.off("setDoc", f);
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
makeTooltip(x: number, y: number, content: HTMLElement | string) {
|
|
|
|
|
const node = this.elt("div", cls + "tooltip", content);
|
|
|
|
|
node.style.left = x + "px";
|
|
|
|
|
node.style.top = y + "px";
|
|
|
|
|
document.body.appendChild(node);
|
|
|
|
|
return node;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
remove(node?: HTMLElement) {
|
|
|
|
|
if (node) {
|
|
|
|
|
const p = node.parentNode;
|
|
|
|
|
if (p) p.removeChild(node);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
elt(
|
|
|
|
|
tagName: string,
|
|
|
|
|
cls: string | null,
|
|
|
|
|
content: string | HTMLElement,
|
|
|
|
|
): HTMLElement {
|
|
|
|
|
const e = document.createElement(tagName);
|
|
|
|
|
if (cls) e.className = cls;
|
|
|
|
|
if (content) {
|
|
|
|
|
const eltNode =
|
|
|
|
|
typeof content === "string"
|
|
|
|
|
? document.createTextNode(content)
|
|
|
|
|
: content;
|
|
|
|
|
e.appendChild(eltNode);
|
|
|
|
|
}
|
|
|
|
|
return e;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fadeOut(tooltip: HTMLElement) {
|
2020-06-04 13:49:22 +00:00
|
|
|
this.remove(tooltip);
|
2020-05-20 11:30:53 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export default TernServer;
|