@@ -40,11 +45,14 @@ export const getDebugTokenHTML = (tokens: Token[], selection: Selection, mapping
Literal
`;
+
const invertedMapping = mapping.invert();
const mappedFrom = invertedMapping.map(selection.from);
const mappedTo = invertedMapping.map(selection.to);
- tokens.forEach((token, index) => {
+ const pmTokens = toProseMirrorTokens(tokens);
+
+ pmTokens.forEach((token, index) => {
html += `${Array(Math.max(1, token.lexeme.length))
.fill(undefined)
.map((_, index) => {
@@ -54,7 +62,7 @@ export const getDebugTokenHTML = (tokens: Token[], selection: Selection, mapping
);
const literalChar = token.literal?.[index - literalOffset];
- const globalIndex = token.start + index
+ const globalIndex = token.from + index;
return `
${globalIndex}
@@ -64,7 +72,9 @@ export const getDebugTokenHTML = (tokens: Token[], selection: Selection, mapping
: ""
}
${
- mappedTo === globalIndex ? `
$
` : ""
+ mappedTo === globalIndex
+ ? `
$
`
+ : ""
}
${
lexemeChar !== undefined
@@ -85,12 +95,18 @@ export const getDebugTokenHTML = (tokens: Token[], selection: Selection, mapping
})
.join("")}
${
- tokens[index + 1]?.start > token.end + 1 &&
- tokens[index + 1]?.tokenType !== "EOF" &&
- token.tokenType !== "EOF"
+ pmTokens[index + 1]?.from > token.to && token.tokenType !== "EOF"
? `
`
+ token.to
+ }
${
+ mappedFrom === token.to
+ ? `
^
`
+ : ""
+ }${
+ mappedTo === token.to
+ ? `
$
`
+ : ""
+ }
`
: ""
}`;
});
@@ -173,10 +189,12 @@ export const getDebugMappingHTML = (
`
${pos}
- ${(queryPosMap[pos] ?? []).map(
- ({ char }) =>
- `
${char}
`,
- ).join(" ")}
+ ${(queryPosMap[pos] ?? [])
+ .map(
+ ({ char }) =>
+ `
${char}
`,
+ )
+ .join(" ")}
${
diff --git a/lib/cql/src/cqlInput/editor/plugins/cql.spec.ts b/lib/cql/src/cqlInput/editor/plugins/cql.spec.ts
index ef85bf9..4162c96 100644
--- a/lib/cql/src/cqlInput/editor/plugins/cql.spec.ts
+++ b/lib/cql/src/cqlInput/editor/plugins/cql.spec.ts
@@ -538,7 +538,7 @@ describe("cql plugin", () => {
const { editor, container, moveCaretToQueryPos } =
createCqlEditor(queryStr);
- await moveCaretToQueryPos(queryStr.length - 1);
+ await moveCaretToQueryPos(queryStr.length);
await findByText(container, "1 day ago");
await editor.press("Enter");
@@ -549,7 +549,7 @@ describe("cql plugin", () => {
const { editor, waitFor, container, moveCaretToQueryPos } =
createCqlEditor(queryStr);
- await moveCaretToQueryPos(queryStr.length - 1);
+ await moveCaretToQueryPos(queryStr.length);
const popoverContainer = await findByTestId(container, typeaheadTestId);
await findByText(popoverContainer, "1 day ago");
@@ -565,7 +565,7 @@ describe("cql plugin", () => {
const { editor, waitFor, container, moveCaretToQueryPos } =
createCqlEditor(queryStr);
- await moveCaretToQueryPos(queryStr.length - 1);
+ await moveCaretToQueryPos(queryStr.length);
const popoverContainer = await findByTestId(container, typeaheadTestId);
await findByText(popoverContainer, "1 day ago");
diff --git a/lib/cql/src/cqlInput/editor/plugins/cql.ts b/lib/cql/src/cqlInput/editor/plugins/cql.ts
index 0801259..0c8cddd 100644
--- a/lib/cql/src/cqlInput/editor/plugins/cql.ts
+++ b/lib/cql/src/cqlInput/editor/plugins/cql.ts
@@ -135,10 +135,10 @@ export const createCqlPlugin = ({
const queryBeforeParse = docToCqlStr(originalDoc);
- const result = parser(queryBeforeParse);
- const { tokens, queryAst, error, mapping } = mapResult(result);
-
- const newDoc = tokensToDoc(tokens);
+ const newDoc = tokensToDoc(mapResult(parser(queryBeforeParse)).tokens);
+ const queryAfterParse = docToCqlStr(newDoc);
+ const result = parser(queryAfterParse);
+ const { mapping, queryAst, tokens, error } = mapResult(result);
const docSelection = new AllSelection(tr.doc);
@@ -505,8 +505,15 @@ export const createCqlPlugin = ({
typeaheadPopover?.setIsPending();
}
+ if (view.state.selection.from !== view.state.selection.to) {
+ return;
+ }
+
try {
- const suggestions = await typeahead.getSuggestions(queryAst);
+ const suggestions = await typeahead.getSuggestions(
+ queryAst,
+ mapping.invert().map(view.state.selection.from),
+ );
const mappedSuggestions = toMappedSuggestions(suggestions, mapping);
if (view.hasFocus()) {
typeaheadPopover?.updateSuggestions(mappedSuggestions);
diff --git a/lib/cql/src/cqlInput/editor/utils.ts b/lib/cql/src/cqlInput/editor/utils.ts
index 49c8d3c..72c3f6f 100644
--- a/lib/cql/src/cqlInput/editor/utils.ts
+++ b/lib/cql/src/cqlInput/editor/utils.ts
@@ -572,25 +572,29 @@ export type ProseMirrorToken = Omit
& {
* 0 1 2 3
*/
export const toProseMirrorTokens = (tokens: Token[]): ProseMirrorToken[] =>
- tokens.map(({ start, end, ...token }) => ({
- ...token,
- from: start,
- to: end + 1,
- }));
+ tokens.map(toProseMirrorToken);
+
+export const toProseMirrorToken = ({ start, end, ...token }: Token) => ({
+ ...token,
+ from: start,
+ to: end + 1,
+});
export const toMappedSuggestions = (
- typeaheadSuggestions: TypeaheadSuggestion[],
+ typeaheadSuggestion: TypeaheadSuggestion | undefined,
mapping: Mapping,
-) =>
- typeaheadSuggestions.map((suggestion) => {
- const from = mapping.map(suggestion.from);
- const to = mapping.map(
- suggestion.to + 1,
- suggestion.position === "chipKey" ? -1 : 0,
- );
+) => {
+ if (!typeaheadSuggestion) {
+ return undefined;
+ }
+ const from = mapping.map(typeaheadSuggestion.from);
+ const to = mapping.map(
+ typeaheadSuggestion.to + 1,
+ typeaheadSuggestion.position === "chipKey" ? -1 : 0,
+ );
- return { ...suggestion, from, to } as MappedTypeaheadSuggestion;
- });
+ return { ...typeaheadSuggestion, from, to } as MappedTypeaheadSuggestion;
+};
const toMappedError = (error: CqlError, mapping: Mapping) => ({
message: error.message,
diff --git a/lib/cql/src/cqlInput/popover/TypeaheadPopover.ts b/lib/cql/src/cqlInput/popover/TypeaheadPopover.ts
index 5e89c06..2ad694d 100644
--- a/lib/cql/src/cqlInput/popover/TypeaheadPopover.ts
+++ b/lib/cql/src/cqlInput/popover/TypeaheadPopover.ts
@@ -95,15 +95,16 @@ export class TypeaheadPopover extends Popover {
});
}
- public isRenderingNavigableMenu = () => this.isVisible && !!this.currentSuggestion?.suggestions.length;
+ public isRenderingNavigableMenu = () =>
+ this.isVisible && !!this.currentSuggestion?.suggestions.length;
public updateSuggestions = (
- typeaheadSuggestions: MappedTypeaheadSuggestion[],
+ typeaheadSuggestion?: MappedTypeaheadSuggestion,
) => {
this.isPending = false;
if (
this.view.isDestroyed ||
- !typeaheadSuggestions.length ||
+ !typeaheadSuggestion ||
this.view.state.selection.from !== this.view.state.selection.to
) {
this.currentSuggestion = undefined;
@@ -112,19 +113,7 @@ export class TypeaheadPopover extends Popover {
return;
}
- const { selection: currentSelection } = this.view.state;
- const suggestionThatCoversSelection = typeaheadSuggestions.find(
- ({ from, to }) =>
- currentSelection.from >= from && currentSelection.to <= to,
- );
-
- if (!suggestionThatCoversSelection) {
- this.currentSuggestion = undefined;
- this.hide();
- return;
- }
-
- this.currentSuggestion = suggestionThatCoversSelection;
+ this.currentSuggestion = typeaheadSuggestion;
this.show();
};
@@ -141,12 +130,20 @@ export class TypeaheadPopover extends Popover {
}
};
- protected show = () => {
+ protected show = async () => {
if (!this.currentSuggestion) {
return Promise.resolve();
}
const { node } = this.view.domAtPos(this.currentSuggestion.from);
- return super.show(node as HTMLElement);
+
+ // The node given from `domAtPos` may be a text node
+ if (node && node instanceof HTMLElement) {
+ return super.show(node);
+ }
+
+ console.warn(
+ `[cql]: Attempted to show popover, but domAtPos did not return an element for ${this.currentSuggestion.from}`,
+ );
};
public handleAction: ActionHandler = (action) => {
diff --git a/lib/cql/src/lang/typeahead.spec.ts b/lib/cql/src/lang/typeahead.spec.ts
index bb8892f..d822f79 100644
--- a/lib/cql/src/lang/typeahead.spec.ts
+++ b/lib/cql/src/lang/typeahead.spec.ts
@@ -17,6 +17,7 @@ describe("typeahead", () => {
const getSuggestions = async (
query: string,
+ position: number,
_typeahead: Typeahead = typeahead,
) => {
const result = createParser()(query);
@@ -25,269 +26,176 @@ describe("typeahead", () => {
throw result.error;
}
- return await _typeahead.getSuggestions(result.queryAst);
+ return await _typeahead.getSuggestions(result.queryAst, position);
};
it("should give all options for empty queryFields", async () => {
- expect(await getSuggestions("")).toEqual([]);
-
- expect(await getSuggestions("+:")).toEqual([
- {
- from: 1,
- to: 1,
- position: "chipKey",
- suggestions: [
- new TextSuggestionOption(
- "Tag",
- "tag",
- "Search by content tags, e.g. sport/football",
- ),
- new TextSuggestionOption(
- "Sync",
- "sync",
- "Search synchronous list of tags",
- ),
- new TextSuggestionOption(
- "Section",
- "section",
- "Search by content sections, e.g. section/news",
- ),
- new TextSuggestionOption(
- "From date",
- "from-date",
- "The date to search from",
- ),
- new TextSuggestionOption("ID", "id", "The content ID"),
- ],
- type: "TEXT",
- suffix: ":",
- },
- ]);
+ expect(await getSuggestions("", 0)).toEqual(undefined);
+
+ expect(await getSuggestions("+:", 1)).toEqual({
+ from: 1,
+ to: 2,
+ position: "chipKey",
+ suggestions: [
+ new TextSuggestionOption(
+ "Tag",
+ "tag",
+ "Search by content tags, e.g. sport/football",
+ ),
+ new TextSuggestionOption(
+ "Sync",
+ "sync",
+ "Search synchronous list of tags",
+ ),
+ new TextSuggestionOption(
+ "Section",
+ "section",
+ "Search by content sections, e.g. section/news",
+ ),
+ new TextSuggestionOption(
+ "From date",
+ "from-date",
+ "The date to search from",
+ ),
+ new TextSuggestionOption("ID", "id", "The content ID"),
+ ],
+ type: "TEXT",
+ suffix: ":",
+ });
});
it("should give typeahead suggestions for query meta keys", async () => {
- const suggestions = await getSuggestions("+ta:");
- expect(suggestions).toEqual([
- {
- from: 1,
- to: 2,
- position: "chipKey",
- suggestions: [
- new TextSuggestionOption(
- "Tag",
- "tag",
- "Search by content tags, e.g. sport/football",
- ),
- ],
- type: "TEXT",
- suffix: ":",
- },
- ]);
+ const suggestions = await getSuggestions("+ta:", 1);
+ expect(suggestions).toEqual({
+ from: 1,
+ to: 4,
+ position: "chipKey",
+ suggestions: [
+ new TextSuggestionOption(
+ "Tag",
+ "tag",
+ "Search by content tags, e.g. sport/football",
+ ),
+ ],
+ type: "TEXT",
+ suffix: ":",
+ });
});
- it("should give typeahead suggestions for both query meta keys and values", async () => {
- const suggestions = await getSuggestions("+tag:tags-are-magic");
-
- expect(suggestions).toEqual([
- {
- from: 1,
- to: 3,
- position: "chipKey",
- suggestions: [
- new TextSuggestionOption(
- "Tag",
- "tag",
- "Search by content tags, e.g. sport/football",
- ),
- ],
- type: "TEXT",
- suffix: ":",
- },
- {
- from: 5,
- to: 18,
- position: "chipValue",
- suggestions: testTags,
- type: "TEXT",
- suffix: " ",
- },
- ]);
+ it("should give typeahead suggestions for values", async () => {
+ const suggestions = await getSuggestions("+tag:tags-are-magic", 5);
+
+ expect(suggestions).toEqual({
+ from: 5,
+ to: 19,
+ position: "chipValue",
+ suggestions: testTags,
+ type: "TEXT",
+ suffix: " ",
+ });
});
it("should give typeahead suggestions in a case insensitive way for synchronous query meta keys across value and label", async () => {
- const expectedSuggestions: TypeaheadSuggestion[] = [
- {
- from: 1,
- position: "chipKey",
- suffix: ":",
- suggestions: [
- new TextSuggestionOption(
- "Sync",
- "sync",
- "Search synchronous list of tags",
- ),
- ],
- to: 4,
- type: "TEXT",
- },
- {
- from: 6,
- to: 8,
- position: "chipValue",
- suggestions: [
- new TextSuggestionOption(
- "abc DEF",
- "GHI jkl",
- "A tag with a mix of upper and lowercase strings",
- ),
- ],
- type: "TEXT",
- suffix: " ",
- },
- ];
-
- const suggestionsUppercaseLabelQuery = await getSuggestions("+sync:ABC");
+ const expectedValue: TypeaheadSuggestion = {
+ from: 6,
+ to: 9,
+ position: "chipValue",
+ suggestions: [
+ new TextSuggestionOption(
+ "abc DEF",
+ "GHI jkl",
+ "A tag with a mix of upper and lowercase strings",
+ ),
+ ],
+ type: "TEXT",
+ suffix: " ",
+ };
+ const suggestionsUppercaseLabelQuery = await getSuggestions("+sync:ABC", 6);
- expect(suggestionsUppercaseLabelQuery).toEqual(expectedSuggestions);
+ expect(suggestionsUppercaseLabelQuery).toEqual(expectedValue);
- const suggestionsLowercaseLabelQuery = await getSuggestions("+sync:def");
+ const suggestionsLowercaseLabelQuery = await getSuggestions("+sync:def", 6);
- expect(suggestionsLowercaseLabelQuery).toEqual(expectedSuggestions);
+ expect(suggestionsLowercaseLabelQuery).toEqual(expectedValue);
- const suggestionsUppercaseValueQuery = await getSuggestions("+sync:JKL");
+ const suggestionsUppercaseValueQuery = await getSuggestions("+sync:JKL", 6);
- expect(suggestionsUppercaseValueQuery).toEqual(expectedSuggestions);
+ expect(suggestionsUppercaseValueQuery).toEqual(expectedValue);
- const suggestionsLowercaseValueQuery = await getSuggestions("+sync:ghi");
+ const suggestionsLowercaseValueQuery = await getSuggestions("+sync:ghi", 6);
- expect(suggestionsLowercaseValueQuery).toEqual(expectedSuggestions);
+ expect(suggestionsLowercaseValueQuery).toEqual(expectedValue);
- const suggestionsForNonMatchingQuery = await getSuggestions("+sync:mno");
+ const suggestionsForNonMatchingQuery = await getSuggestions("+sync:mno", 6);
expect(suggestionsLowercaseValueQuery).not.toEqual(
suggestionsForNonMatchingQuery,
);
});
it("should give value suggestions for an empty string", async () => {
- const suggestions = await getSuggestions("+tag:");
- expect(suggestions).toEqual([
- {
- from: 1,
- to: 3,
- position: "chipKey",
- suggestions: [
- new TextSuggestionOption(
- "Tag",
- "tag",
- "Search by content tags, e.g. sport/football",
- ),
- ],
- type: "TEXT",
- suffix: ":",
- },
- {
- from: 5,
- to: 5,
- position: "chipValue",
- suggestions: testTags,
- type: "TEXT",
- suffix: " ",
- },
- ]);
+ const suggestions = await getSuggestions("+tag:", 5);
+ expect(suggestions).toEqual({
+ from: 5,
+ to: 6,
+ position: "chipValue",
+ suggestions: testTags,
+ type: "TEXT",
+ suffix: " ",
+ });
});
it("should give a suggestion of type DATE given e.g. 'from-date'", async () => {
- const suggestions = await getSuggestions("+from-date:");
-
- expect(suggestions).toEqual([
- {
- from: 1,
- to: 9,
- position: "chipKey",
- suggestions: [
- new TextSuggestionOption(
- "From date",
- "from-date",
- "The date to search from",
- ),
- ],
- type: "TEXT",
- suffix: ":",
- },
- {
- from: 11,
- to: 11,
- position: "chipValue",
- suggestions: [
- new DateSuggestionOption("1 day ago", "-1d"),
- new DateSuggestionOption("7 days ago", "-7d"),
- new DateSuggestionOption("14 days ago", "-14d"),
- new DateSuggestionOption("30 days ago", "-30d"),
- new DateSuggestionOption("1 year ago", "-1y"),
- ],
- type: "DATE",
- suffix: " ",
- },
- ]);
+ const suggestions = await getSuggestions("+from-date:", 11);
+
+ expect(suggestions).toEqual({
+ from: 11,
+ to: 12,
+ position: "chipValue",
+ suggestions: [
+ new DateSuggestionOption("1 day ago", "-1d"),
+ new DateSuggestionOption("7 days ago", "-7d"),
+ new DateSuggestionOption("14 days ago", "-14d"),
+ new DateSuggestionOption("30 days ago", "-30d"),
+ new DateSuggestionOption("1 year ago", "-1y"),
+ ],
+ type: "DATE",
+ suffix: " ",
+ });
});
- it("should give suggestions for multiple tags", async () => {
- const suggestions = await getSuggestions("+tag:a +:");
-
- expect(suggestions).toEqual([
- {
- from: 1,
- to: 3,
- position: "chipKey",
- suggestions: [
- new TextSuggestionOption(
- "Tag",
- "tag",
- "Search by content tags, e.g. sport/football",
- ),
- ],
- type: "TEXT",
- suffix: ":",
- },
- {
- from: 5,
- to: 5,
- position: "chipValue",
- suggestions: testTags,
- type: "TEXT",
- suffix: " ",
- },
- {
- from: 8,
- to: 8,
- position: "chipKey",
- suggestions: [
- new TextSuggestionOption(
- "Tag",
- "tag",
- "Search by content tags, e.g. sport/football",
- ),
- new TextSuggestionOption(
- "Sync",
- "sync",
- "Search synchronous list of tags",
- ),
- new TextSuggestionOption(
- "Section",
- "section",
- "Search by content sections, e.g. section/news",
- ),
- new TextSuggestionOption(
- "From date",
- "from-date",
- "The date to search from",
- ),
- new TextSuggestionOption("ID", "id", "The content ID"),
- ],
- type: "TEXT",
- suffix: ":",
- },
- ]);
+ it("should give suggestions for the correct tag where there is more than one", async () => {
+ const suggestions = await getSuggestions("+tag:a +:", 8);
+
+ expect(suggestions).toEqual({
+ from: 8,
+ to: 9,
+ position: "chipKey",
+ suggestions: [
+ new TextSuggestionOption(
+ "Tag",
+ "tag",
+ "Search by content tags, e.g. sport/football",
+ ),
+ new TextSuggestionOption(
+ "Sync",
+ "sync",
+ "Search synchronous list of tags",
+ ),
+ new TextSuggestionOption(
+ "Section",
+ "section",
+ "Search by content sections, e.g. section/news",
+ ),
+ new TextSuggestionOption(
+ "From date",
+ "from-date",
+ "The date to search from",
+ ),
+ new TextSuggestionOption("ID", "id", "The content ID"),
+ ],
+ type: "TEXT",
+ suffix: ":",
+ });
});
it("should suggest keys that start with the search string first", async () => {
@@ -296,8 +204,8 @@ describe("typeahead", () => {
);
expect(
- (await getSuggestions("+g:", typeahead)).flatMap((a) =>
- a.suggestions.map((s) => s.value),
+ (await getSuggestions("+g:", 1, typeahead))?.suggestions.map(
+ (s) => s.value,
),
).toEqual(["gnat", "tag", "stage"]);
});
diff --git a/lib/cql/src/lang/typeahead.ts b/lib/cql/src/lang/typeahead.ts
index 071f92f..99d4270 100644
--- a/lib/cql/src/lang/typeahead.ts
+++ b/lib/cql/src/lang/typeahead.ts
@@ -1,12 +1,12 @@
-import { CqlQuery, CqlField } from "./ast";
-import { Token } from "./token";
+import { ProseMirrorToken } from "../cqlInput/editor/utils";
+import { CqlQuery } from "./ast";
import {
DateSuggestionOption,
TextSuggestionOption,
TypeaheadSuggestion,
SuggestionType,
} from "./types";
-import { getCqlFieldsFromCqlBinary } from "./utils";
+import { getAstNodeAtPos } from "./utils";
type TypeaheadResolver =
| ((str: string, signal?: AbortSignal) => Promise)
@@ -81,16 +81,17 @@ export class Typeahead {
);
}
- public getSuggestions(
+ public async getSuggestions(
program: CqlQuery,
+ position: number,
signal?: AbortSignal,
- ): Promise {
+ ): Promise {
return new Promise((resolve, reject) => {
// Abort existing fetch, if it exists
this.abortController?.abort();
if (!program.content) {
- return resolve([]);
+ return resolve(undefined);
}
const abortController = new AbortController();
@@ -99,41 +100,45 @@ export class Typeahead {
reject(new DOMException("Aborted", "AbortError"));
});
- const eventuallySuggestions = getCqlFieldsFromCqlBinary(
- program.content,
- ).flatMap((queryField) => this.suggestCqlField(queryField, signal));
+ const maybeSuggestionAtPos = getAstNodeAtPos(program.content, position);
- return Promise.all(eventuallySuggestions)
- .then((suggestions) => resolve(suggestions.flat()))
- .catch(reject);
+ if (!maybeSuggestionAtPos) {
+ return resolve(undefined);
+ }
+
+ const { key, value } = maybeSuggestionAtPos;
+
+ resolve(this.suggestCqlField(key, value, signal));
});
}
- private getSuggestionsForKeyToken(keyToken: Token): TypeaheadSuggestion[] {
+ private getSuggestionsForChipKey(
+ keyToken: ProseMirrorToken,
+ ): TypeaheadSuggestion | undefined {
const suggestions = this.suggestFieldKey(keyToken.literal ?? "");
if (!suggestions) {
- return [];
+ return undefined;
}
- return [
- {
- from: keyToken.start,
- to: Math.max(keyToken.start, keyToken.end - 1), // Do not include ':'
- position: "chipKey",
- suggestions,
- type: "TEXT",
- suffix: ":",
- },
- ];
+ return {
+ from: keyToken.from,
+ to: Math.max(keyToken.to, keyToken.to - 1), // Do not include ':'
+ position: "chipKey",
+ suggestions,
+ type: "TEXT",
+ suffix: ":",
+ };
}
private async suggestCqlField(
- q: CqlField,
+ key: ProseMirrorToken,
+ value?: ProseMirrorToken,
signal?: AbortSignal,
- ): Promise {
- const { key, value } = q;
- const keySuggestions = this.getSuggestionsForKeyToken(key);
+ ): Promise {
+ if (!value) {
+ return this.getSuggestionsForChipKey(key);
+ }
const maybeValueSuggestions = this.suggestFieldValue(
key.literal ?? "",
@@ -142,20 +147,19 @@ export class Typeahead {
);
if (!maybeValueSuggestions) {
- return Promise.resolve(keySuggestions);
+ return;
}
- return maybeValueSuggestions.suggestions.then((suggestions) => [
- ...keySuggestions,
- {
- from: value ? value.start : key.end + 1,
- to: value ? value.end : key.end + 1,
- position: "chipValue",
- suggestions,
- type: maybeValueSuggestions.type,
- suffix: " ",
- } as TypeaheadSuggestion,
- ]);
+ const suggestions = await maybeValueSuggestions.suggestions;
+
+ return {
+ from: value ? value.from : key.from,
+ to: value ? value.to : key.to,
+ position: "chipValue",
+ suggestions,
+ type: maybeValueSuggestions.type,
+ suffix: " ",
+ } as TypeaheadSuggestion;
}
private suggestFieldKey(str: string): TextSuggestionOption[] | undefined {
diff --git a/lib/cql/src/lang/utils.ts b/lib/cql/src/lang/utils.ts
index 7fddf4b..798b48e 100644
--- a/lib/cql/src/lang/utils.ts
+++ b/lib/cql/src/lang/utils.ts
@@ -1,4 +1,10 @@
+import {
+ ProseMirrorToken,
+ toProseMirrorToken,
+ toProseMirrorTokens,
+} from "../cqlInput/editor/utils";
import { CqlBinary, CqlExpr, CqlField } from "./ast";
+import { Token, TokenType } from "./token";
const whitespaceR = /\s/;
export const hasWhitespace = (str: string) => !!str.match(whitespaceR);
@@ -56,6 +62,89 @@ export function* getPermutations(
return permutation.slice();
}
+type SuggestionPos =
+ | undefined
+ | { key: ProseMirrorToken; value: undefined }
+ | { key: ProseMirrorToken; value: ProseMirrorToken };
+
+export const getAstNodeAtPos = (
+ queryBinary: CqlBinary,
+ position: number,
+): SuggestionPos => {
+ return (
+ getAstNodeAtPosExpr(queryBinary.left, position) ??
+ (queryBinary.right
+ ? getAstNodeAtPos(queryBinary.right.binary, position)
+ : undefined)
+ );
+};
+
+export const getAstNodeAtPosExpr = (
+ expr: CqlExpr,
+ position: number,
+): SuggestionPos => {
+ switch (expr.content.type) {
+ case "CqlStr": {
+ const key = isWithinRange(expr.content.token, position);
+
+ return key
+ ? {
+ key,
+ value: undefined,
+ }
+ : undefined;
+ }
+ case "CqlBinary":
+ return getAstNodeAtPos(expr.content, position);
+ case "CqlGroup":
+ return getAstNodeAtPos(expr.content.content, position);
+ case "CqlField": {
+ const key = isWithinRange(expr.content.key, position);
+ const value = isWithinRange(expr.content.value, position);
+
+ if (!key && !value) {
+ return undefined;
+ }
+
+ if (key && !value) {
+ return position === key.to
+ ? {
+ key,
+ // Add an empty value here to signal that we are in value position
+ value: toProseMirrorToken(
+ new Token(
+ TokenType.CHIP_VALUE,
+ "",
+ undefined,
+ position,
+ position,
+ ),
+ ),
+ }
+ : { key, value: undefined };
+ }
+
+ return {
+ key: toProseMirrorToken(expr.content.key),
+ value,
+ };
+ }
+ }
+};
+
+const isWithinRange = (
+ token: Token | undefined,
+ position: number,
+): ProseMirrorToken | undefined => {
+ if (!token) {
+ return undefined;
+ }
+
+ const [pmToken] = toProseMirrorTokens([token]);
+ return position >= pmToken.from && position <= pmToken.to
+ ? pmToken
+ : undefined;
+};
export const getCqlFieldsFromCqlBinary = (queryBinary: CqlBinary): CqlField[] =>
getCqlFieldsFromQueryExpr(queryBinary.left).concat(
queryBinary.right
diff --git a/lib/cql/src/page.ts b/lib/cql/src/page.ts
index 23f7f9f..668b7d4 100644
--- a/lib/cql/src/page.ts
+++ b/lib/cql/src/page.ts
@@ -63,7 +63,7 @@ const handleDebugChangeEvent = (e: CustomEvent) => {
debugMappingContainer.innerHTML = `
Original query:
${getOriginalQueryHTML(queryStr)}
- Tokenises to:
+ Tokenises to (ProseMirror positions):
${getDebugTokenHTML(tokens, selection, mapping)}
${
queryAst