Skip to content

Fix autocomplete suggesting erratically & add more suggestions #866

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 36 commits into
base: development
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
057c08a
Merge branch 'development'
alexjpwalker Apr 30, 2025
2458fb1
Merge branch 'development'
alexjpwalker May 1, 2025
ea03618
Merge branch 'development'
alexjpwalker May 7, 2025
845388a
Merge branch 'development'
alexjpwalker May 7, 2025
4c83b95
Merge branch 'development'
alexjpwalker May 9, 2025
3618375
Merge branch 'development'
alexjpwalker May 9, 2025
0165af9
Release 3.3.0 (#864)
alexjpwalker May 20, 2025
8cfe668
Merge branch 'development'
alexjpwalker May 22, 2025
da83ee8
Merge branch 'development'
alexjpwalker May 29, 2025
5ad6ef6
Merge branch 'development'
alexjpwalker Jun 10, 2025
7d600c5
Introduce schema tree view on Query page
alexjpwalker Jun 12, 2025
a880ed6
Merge branch 'development' into schema-tree
alexjpwalker Jun 17, 2025
d75f7c4
Enrich schema tree view with owns, plays and relates links
alexjpwalker Jun 17, 2025
0a2ea88
Represent schema as a map of labels to concepts. Standardise terminology
alexjpwalker Jun 18, 2025
9cd9551
Clear schema tool window on unloading schema
alexjpwalker Jun 18, 2025
1db29e8
Clean up comments
alexjpwalker Jun 18, 2025
7ff002a
Typo squatting
alexjpwalker Jun 18, 2025
55b4533
Schema tool window scrolling behaviour
alexjpwalker Jun 18, 2025
6947f73
Schema tool window: add top border to scrollable area when scrolled
alexjpwalker Jun 18, 2025
9b9b565
Update autocomplete
krishnangovindraj May 26, 2025
3f120a7
Cleanup some comments
krishnangovindraj Jun 12, 2025
6b802e4
Eeps. Why can't i select autocomplete stuff
krishnangovindraj Jun 12, 2025
9e735df
OOf, works somewhat ok
krishnangovindraj Jun 12, 2025
16edf3d
Rearrange
krishnangovindraj Jun 12, 2025
1255bd3
More clenaups, esp with navigation helpers
krishnangovindraj Jun 12, 2025
ba9299f
Suggestions for has and links constraints are nicely scoped
krishnangovindraj Jun 12, 2025
3ce4391
Fix boost in variables not being used
krishnangovindraj Jun 12, 2025
4269c36
Minor refactors
krishnangovindraj Jun 12, 2025
9107094
Add TODO to remove window.OC_lastQueryAnswers
krishnangovindraj Jun 12, 2025
e456b05
Retry CI
krishnangovindraj Jun 13, 2025
4477aba
Post rebase working in
krishnangovindraj Jun 18, 2025
fa6b1d7
Update deriving partial schema from text editor state
krishnangovindraj Jun 18, 2025
e292510
Plug in schema from schema state to autocomplete
krishnangovindraj Jun 18, 2025
463cefe
Small cleanup of todos and unused imports
krishnangovindraj Jun 18, 2025
c9990bb
Move lezer to the appropriate line in devDependencies
krishnangovindraj Jun 18, 2025
7982e8c
Update names for rebase
krishnangovindraj Jun 18, 2025
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",
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We have to run this step every time we update the typeql.grammar file. I should probably add a comment in that file.

"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
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