Merge pull request #3222 from github/koesie10/suggest-box-helper-functions

Add helper functions for suggestion box
This commit is contained in:
Koen Vlaswinkel
2024-01-16 09:35:12 +01:00
committed by GitHub
4 changed files with 303 additions and 0 deletions

View File

@@ -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);
},
);
});

View File

@@ -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);
});
});

View 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 !== "");
}

View 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()),
);
}