Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
"version": "3.4.0-rc1",
"scripts": {
"ng": "ng",
"generate-grammar": "lezer-generator --typeScript src/framework/codemirror-lang-typeql/typeql.grammar -o src/framework/codemirror-lang-typeql/generated/typeql.grammar.generated",
"start": "ng serve",
"build": "ng build",
"watch": "ng build --watch --configuration development",
Expand All @@ -23,9 +24,12 @@
"@angular/platform-browser-dynamic": "20.0.3",
"@angular/router": "20.0.3",
"@codemirror/lint": "6.8.5",
"@codemirror/autocomplete": "6.18.6",
"@customerio/cdp-analytics-browser": "0.2.0",
"@codemirror/commands": "6.8.1",
"@hhangular/resizable": "1.18.1",
"@intercom/messenger-js-sdk": "0.0.14",
"@lezer/common": "^1.0.0",
"@lezer/lr": "1.4.2",
"@sigma/edge-curve": "3.1.0",
"@sigma/node-square": "3.0.0",
Expand All @@ -52,6 +56,7 @@
"@codemirror/language": "6.11.0",
"@codemirror/state": "6.5.2",
"@codemirror/view": "6.36.5",
"@lezer/generator": "1.7.3",
"@lezer/highlight": "1.2.1",
"@sanity/asset-utils": "1.3.0",
"@sanity/icons": "3.4.0",
Expand Down
12 changes: 12 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

176 changes: 176 additions & 0 deletions src/framework/codemirror-lang-typeql/complete.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@

import { CompletionContext, Completion, CompletionResult } from "@codemirror/autocomplete";
import { syntaxTree } from "@codemirror/language"
import { SyntaxNode, NodeType, Tree } from "@lezer/common"

type TokenID = number;
export interface SuggestionMap<STATE> {
[key: TokenID]: SuffixOfPrefixSuggestion<STATE>[]
}


export type SuffixCandidate = TokenID[]; // A SuffixCandidate 's' "matches" a prefix if prefix[-s.length:] == s
export interface SuffixOfPrefixSuggestion<STATE> {
suffixes: SuffixCandidate[], // If any of the suffix candidates match, the suggestions will be used.
suggestions: SuggestionFunction<STATE>[]
}

export type SuggestionFunction<STATE> = (context: CompletionContext, tree: Tree, parseAt: SyntaxNode, climbedTo: SyntaxNode, prefix: NodeType[], state: STATE) => Completion[] | null;

export function suggest(type: string, label: string, boost: number = 0): Completion {
// type (docs): used to pick an icon to show for the completion. Icons are styled with a CSS class created by appending the type name to "cm-completionIcon-".
return {
label: label,
type: type,
apply: label,
info: type,
boost: boost,
};
}

interface NodePrefixAutoCompleteState {
mayUpdateFromEditorState(context: CompletionContext, tree: Tree): void;
}

// See: https://codemirror.net/examples/autocompletion/ and maybe the SQL / HTML Example there.
export class NodePrefixAutoComplete<STATE extends NodePrefixAutoCompleteState> {
suggestionMap: SuggestionMap<STATE>;
suggestorState: STATE;

constructor(suggestionMap: SuggestionMap<STATE>, suggestorState: STATE) {
// This is where we would set up the autocompletion, but we do it in the index.ts file.
// See: https://codemirror.net/docs/ref/#autocomplete.autocompletion
this.suggestionMap = suggestionMap;
this.suggestorState = suggestorState;
}

getState(): STATE {
return this.suggestorState;
}

autocomplete(context: CompletionContext): CompletionResult | null {
let tree: Tree = syntaxTree(context.state);
this.suggestorState.mayUpdateFromEditorState(context, tree);
let currentNode: SyntaxNode = tree.resolveInner(context.pos, -1); // https://lezer.codemirror.net/docs/ref/#common.SyntaxNode
let options = this.getSuggestions(context, tree, currentNode);
if (options != null) {
// And once we figure out, we have to create a list of completion objects
// It may be worth changing the grammar to be able to do this more easily, rather than replicate the original TypeQL grammar.
// https://codemirror.net/docs/ref/#autocomplete.Completion
let from = findStartOfCompletion(context) + 1;
return {
from: from,
options: options,
// Docs: "regular expression that tells the extension that, as long as the updated input (the range between the result's from property and the completion point) matches that value, it can continue to use the list of completions."
validFor: /^([\w\$]+)?$/
}
} else {
return null;
}
}

getSuggestions(context: CompletionContext, tree: Tree, parseAt: SyntaxNode): Completion[] | null {
return this.climbTillWeRecogniseSomething(context, tree, parseAt, parseAt, collectPrecedingChildrenOf(context, parseAt));
}


climbTillWeRecogniseSomething(context: CompletionContext, tree: Tree, parseAt: SyntaxNode, climbedTo: SyntaxNode | null, prefix: NodeType[]): Completion[] | null {
if (climbedTo == null) {
// Uncomment this if you don't see suggestions
// this.logInterestingStuff(context, tree, parseAt, climbedTo, prefix);
return null;
}
let suggestionEither = this.suggestionMap[climbedTo.type.id];
if (suggestionEither != null) {
for (let sops of (suggestionEither as SuffixOfPrefixSuggestion<STATE>[])) {
if (prefixHasAnyOfSuffixes(prefix, sops.suffixes)) {
return this.combineSuggestions(context, tree, parseAt, climbedTo, prefix, sops.suggestions);
}
}
// None match? Fall through.
// console.log("Fell through!!!: ", climbedTo.type.name, "with prefix", prefix);
}
let newPrefix = collectSiblingsOf(climbedTo).concat(prefix);
return this.climbTillWeRecogniseSomething(context, tree, parseAt, climbedTo.parent, newPrefix);
}


combineSuggestions(context: CompletionContext, tree: Tree, parseAt: SyntaxNode, climbedTo: SyntaxNode, prefix: NodeType[], suggestionFunctions: SuggestionFunction<STATE>[]): Completion[] {
let suggestions = suggestionFunctions.map((f) => {
return f(context, tree, parseAt, climbedTo, prefix, this.suggestorState);
}).reduce((acc, curr) => {
return (curr == null) ? acc : acc!.concat(curr);
}, []);
// console.log("Matched:", climbedTo.type.name, "with prefix", prefix, ". Suggestions:", suggestions);
return suggestions!;
}

logInterestingStuff(context: CompletionContext, tree: Tree, parseAt: SyntaxNode, climbedTo: SyntaxNode | null, prefix: NodeType[]) {
console.log("Current Node:", parseAt.name);
console.log("ClimbedTo Node:", climbedTo?.name);

let at: SyntaxNode | null = parseAt;
let climbThrough = [];
while (at != null && at.name != climbedTo?.name) {
climbThrough.push(at.name);
at = at.parent;
}
climbThrough.push(at?.name);
console.log("Climbed through", climbThrough);
console.log("Prefix:", prefix);
}
}

function isPartOfWord(s: string): boolean {
let matches = s.match(/^[A-Za-z0-9_\-\$]+/);
return matches != null && matches.length > 0;
}

function findStartOfCompletion(context: CompletionContext): number {
let str = context.state.doc.sliceString(0, context.pos);
let at = context.pos - 1;
while (at >= 0 && isPartOfWord(str.charAt(at))) {
at -= 1;
}
return at;
}

function collectSiblingsOf(node: SyntaxNode): NodeType[] {
let siblings = [];
let prev: SyntaxNode | null = node;
while (null != (prev = prev.prevSibling)) {
siblings.push(prev.type);
}
return siblings.reverse();
}

function collectPrecedingChildrenOf(context: CompletionContext, node: SyntaxNode): NodeType[] {
let lastChild = node.childBefore(context.pos);
if (lastChild == null) {
return [];
}
let precedingChildren = collectSiblingsOf(lastChild);
precedingChildren.push(lastChild.type);
return precedingChildren;
}

function prefixHasAnyOfSuffixes(prefix: NodeType[], suffixes: SuffixCandidate[]): boolean {
for (let i = 0; i < suffixes.length; i++) {
if (prefixHasSuffix(prefix, suffixes[i])) {
return true;
}
}
return false;
}

function prefixHasSuffix(prefix: NodeType[], suffix: TokenID[]): boolean {
if (prefix.length < suffix.length) {
return false;
}
for (let i = 0; i < suffix.length; i++) {
if (prefix[prefix.length - suffix.length + i].id != suffix[i]) {
return false;
}
}
return true;
}
Loading