Merge pull request #3222 from github/koesie10/suggest-box-helper-functions
Add helper functions for suggestion box
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