Add helper functions for suggestion box
This adds some helper functions that will be used for a suggestion box in the future. There is one helper function for highlighting part of a text case-insensitively. Another helper function will try to find followup options based on a list of tokens. The tests for these functions use access paths as input, but these functions are not intended to be specific to access paths. They can be used for any list of options/string.
This commit is contained in:
@@ -0,0 +1,72 @@
|
||||
import { createHighlights } from "../highlight";
|
||||
|
||||
describe("createHighlights", () => {
|
||||
it.each([
|
||||
{
|
||||
text: "Argument[foo].Element.Field[@test]",
|
||||
search: "Argument[foo]",
|
||||
snippets: [
|
||||
{ text: "Argument[foo]", highlight: true },
|
||||
{
|
||||
text: ".Element.Field[@test]",
|
||||
highlight: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
text: "Field[@test]",
|
||||
search: "test",
|
||||
snippets: [
|
||||
{ text: "Field[@", highlight: false },
|
||||
{
|
||||
text: "test",
|
||||
highlight: true,
|
||||
},
|
||||
{
|
||||
text: "]",
|
||||
highlight: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
text: "Field[@test]",
|
||||
search: "TEST",
|
||||
snippets: [
|
||||
{ text: "Field[@", highlight: false },
|
||||
{
|
||||
text: "test",
|
||||
highlight: true,
|
||||
},
|
||||
{
|
||||
text: "]",
|
||||
highlight: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
text: "Field[@test]",
|
||||
search: "[@TEST",
|
||||
snippets: [
|
||||
{ text: "Field", highlight: false },
|
||||
{
|
||||
text: "[@test",
|
||||
highlight: true,
|
||||
},
|
||||
{
|
||||
text: "]",
|
||||
highlight: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
text: "Field[@test]",
|
||||
search: "",
|
||||
snippets: [{ text: "Field[@test]", highlight: false }],
|
||||
},
|
||||
])(
|
||||
`creates highlights for $text with $search`,
|
||||
({ text, search, snippets }) => {
|
||||
expect(createHighlights(text, search)).toEqual(snippets);
|
||||
},
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,138 @@
|
||||
import { findMatchingOptions } from "../options";
|
||||
|
||||
type TestOption = {
|
||||
label: string;
|
||||
value: string;
|
||||
followup?: TestOption[];
|
||||
};
|
||||
|
||||
const suggestedOptions: TestOption[] = [
|
||||
{
|
||||
label: "Argument[self]",
|
||||
value: "Argument[self]",
|
||||
},
|
||||
{
|
||||
label: "Argument[0]",
|
||||
value: "Argument[0]",
|
||||
followup: [
|
||||
{
|
||||
label: "Element[0]",
|
||||
value: "Argument[0].Element[0]",
|
||||
},
|
||||
{
|
||||
label: "Element[1]",
|
||||
value: "Argument[0].Element[1]",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
label: "Argument[1]",
|
||||
value: "Argument[1]",
|
||||
},
|
||||
{
|
||||
label: "Argument[text_rep:]",
|
||||
value: "Argument[text_rep:]",
|
||||
},
|
||||
{
|
||||
label: "Argument[block]",
|
||||
value: "Argument[block]",
|
||||
followup: [
|
||||
{
|
||||
label: "Parameter[0]",
|
||||
value: "Argument[block].Parameter[0]",
|
||||
followup: [
|
||||
{
|
||||
label: "Element[:query]",
|
||||
value: "Argument[block].Parameter[0].Element[:query]",
|
||||
},
|
||||
{
|
||||
label: "Element[:parameters]",
|
||||
value: "Argument[block].Parameter[0].Element[:parameters]",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
label: "Parameter[1]",
|
||||
value: "Argument[block].Parameter[1]",
|
||||
followup: [
|
||||
{
|
||||
label: "Field[@query]",
|
||||
value: "Argument[block].Parameter[1].Field[@query]",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
label: "ReturnValue",
|
||||
value: "ReturnValue",
|
||||
},
|
||||
];
|
||||
|
||||
describe("findMatchingOptions", () => {
|
||||
it.each([
|
||||
{
|
||||
// Argument[block].
|
||||
tokens: ["Argument[block]", ""],
|
||||
options: ["Argument[block].Parameter[0]", "Argument[block].Parameter[1]"],
|
||||
},
|
||||
{
|
||||
// Argument[block].Parameter[0]
|
||||
tokens: ["Argument[block]", "Parameter[0]"],
|
||||
options: ["Argument[block].Parameter[0]"],
|
||||
},
|
||||
{
|
||||
// Argument[block].Parameter[0].
|
||||
tokens: ["Argument[block]", "Parameter[0]", ""],
|
||||
options: [
|
||||
"Argument[block].Parameter[0].Element[:query]",
|
||||
"Argument[block].Parameter[0].Element[:parameters]",
|
||||
],
|
||||
},
|
||||
{
|
||||
// ""
|
||||
tokens: [""],
|
||||
options: [
|
||||
"Argument[self]",
|
||||
"Argument[0]",
|
||||
"Argument[1]",
|
||||
"Argument[text_rep:]",
|
||||
"Argument[block]",
|
||||
"ReturnValue",
|
||||
],
|
||||
},
|
||||
{
|
||||
// ""
|
||||
tokens: [],
|
||||
options: [
|
||||
"Argument[self]",
|
||||
"Argument[0]",
|
||||
"Argument[1]",
|
||||
"Argument[text_rep:]",
|
||||
"Argument[block]",
|
||||
"ReturnValue",
|
||||
],
|
||||
},
|
||||
{
|
||||
// block
|
||||
tokens: ["block"],
|
||||
options: ["Argument[block]"],
|
||||
},
|
||||
{
|
||||
// l
|
||||
tokens: ["l"],
|
||||
options: ["Argument[self]", "Argument[block]", "ReturnValue"],
|
||||
},
|
||||
{
|
||||
// L
|
||||
tokens: ["L"],
|
||||
options: ["Argument[self]", "Argument[block]", "ReturnValue"],
|
||||
},
|
||||
])(`creates options for $value`, ({ tokens, options }) => {
|
||||
expect(
|
||||
findMatchingOptions(suggestedOptions, tokens).map(
|
||||
(option) => option.value,
|
||||
),
|
||||
).toEqual(options);
|
||||
});
|
||||
});
|
||||
49
extensions/ql-vscode/src/view/common/SuggestBox/highlight.ts
Normal file
49
extensions/ql-vscode/src/view/common/SuggestBox/highlight.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
type Snippet = {
|
||||
text: string;
|
||||
highlight: boolean;
|
||||
};
|
||||
|
||||
/**
|
||||
* Highlight creates a list of snippets that can be used to render a highlighted
|
||||
* string. This highlight is case-insensitive.
|
||||
*
|
||||
* @param text The text in which to create highlights
|
||||
* @param search The string that will be highlighted in the text.
|
||||
* @returns A list of snippets that can be used to render a highlighted string.
|
||||
*/
|
||||
export function createHighlights(text: string, search: string): Snippet[] {
|
||||
if (search === "") {
|
||||
return [{ text, highlight: false }];
|
||||
}
|
||||
|
||||
const searchLower = search.toLowerCase();
|
||||
const textLower = text.toLowerCase();
|
||||
|
||||
const highlights: Snippet[] = [];
|
||||
|
||||
let index = 0;
|
||||
for (;;) {
|
||||
const searchIndex = textLower.indexOf(searchLower, index);
|
||||
if (searchIndex === -1) {
|
||||
break;
|
||||
}
|
||||
|
||||
highlights.push({
|
||||
text: text.substring(index, searchIndex),
|
||||
highlight: false,
|
||||
});
|
||||
highlights.push({
|
||||
text: text.substring(searchIndex, searchIndex + search.length),
|
||||
highlight: true,
|
||||
});
|
||||
|
||||
index = searchIndex + search.length;
|
||||
}
|
||||
|
||||
highlights.push({
|
||||
text: text.substring(index),
|
||||
highlight: false,
|
||||
});
|
||||
|
||||
return highlights.filter((highlight) => highlight.text !== "");
|
||||
}
|
||||
44
extensions/ql-vscode/src/view/common/SuggestBox/options.ts
Normal file
44
extensions/ql-vscode/src/view/common/SuggestBox/options.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
type Option<T extends Option<T>> = {
|
||||
label: string;
|
||||
followup?: T[];
|
||||
};
|
||||
|
||||
function findNestedMatchingOptions<T extends Option<T>>(
|
||||
parts: string[],
|
||||
options: T[],
|
||||
): T[] {
|
||||
const part = parts[0];
|
||||
const rest = parts.slice(1);
|
||||
|
||||
if (!part) {
|
||||
return options;
|
||||
}
|
||||
|
||||
const matchingOption = options.find((item) => item.label === part);
|
||||
if (!matchingOption) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if (rest.length === 0) {
|
||||
return matchingOption.followup ?? [];
|
||||
}
|
||||
|
||||
return findNestedMatchingOptions(rest, matchingOption.followup ?? []);
|
||||
}
|
||||
|
||||
export function findMatchingOptions<T extends Option<T>>(
|
||||
options: T[],
|
||||
tokens: string[],
|
||||
): T[] {
|
||||
if (tokens.length === 0) {
|
||||
return options;
|
||||
}
|
||||
const prefixTokens = tokens.slice(0, tokens.length - 1);
|
||||
const lastToken = tokens[tokens.length - 1];
|
||||
|
||||
const matchingOptions = findNestedMatchingOptions(prefixTokens, options);
|
||||
|
||||
return matchingOptions.filter((item) =>
|
||||
item.label.toLowerCase().includes(lastToken.toLowerCase()),
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user