Skip to content

Commit 5459028

Browse files
authored
Merge pull request #3222 from github/koesie10/suggest-box-helper-functions
Add helper functions for suggestion box
2 parents 281f8ee + 7f730d2 commit 5459028

4 files changed

Lines changed: 303 additions & 0 deletions

File tree

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
import { createHighlights } from "../highlight";
2+
3+
describe("createHighlights", () => {
4+
it.each([
5+
{
6+
text: "Argument[foo].Element.Field[@test]",
7+
search: "Argument[foo]",
8+
snippets: [
9+
{ text: "Argument[foo]", highlight: true },
10+
{
11+
text: ".Element.Field[@test]",
12+
highlight: false,
13+
},
14+
],
15+
},
16+
{
17+
text: "Field[@test]",
18+
search: "test",
19+
snippets: [
20+
{ text: "Field[@", highlight: false },
21+
{
22+
text: "test",
23+
highlight: true,
24+
},
25+
{
26+
text: "]",
27+
highlight: false,
28+
},
29+
],
30+
},
31+
{
32+
text: "Field[@test]",
33+
search: "TEST",
34+
snippets: [
35+
{ text: "Field[@", highlight: false },
36+
{
37+
text: "test",
38+
highlight: true,
39+
},
40+
{
41+
text: "]",
42+
highlight: false,
43+
},
44+
],
45+
},
46+
{
47+
text: "Field[@test]",
48+
search: "[@TEST",
49+
snippets: [
50+
{ text: "Field", highlight: false },
51+
{
52+
text: "[@test",
53+
highlight: true,
54+
},
55+
{
56+
text: "]",
57+
highlight: false,
58+
},
59+
],
60+
},
61+
{
62+
text: "Field[@test]",
63+
search: "",
64+
snippets: [{ text: "Field[@test]", highlight: false }],
65+
},
66+
])(
67+
`creates highlights for $text with $search`,
68+
({ text, search, snippets }) => {
69+
expect(createHighlights(text, search)).toEqual(snippets);
70+
},
71+
);
72+
});
Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
import { findMatchingOptions } from "../options";
2+
3+
type TestOption = {
4+
label: string;
5+
value: string;
6+
followup?: TestOption[];
7+
};
8+
9+
const suggestedOptions: TestOption[] = [
10+
{
11+
label: "Argument[self]",
12+
value: "Argument[self]",
13+
},
14+
{
15+
label: "Argument[0]",
16+
value: "Argument[0]",
17+
followup: [
18+
{
19+
label: "Element[0]",
20+
value: "Argument[0].Element[0]",
21+
},
22+
{
23+
label: "Element[1]",
24+
value: "Argument[0].Element[1]",
25+
},
26+
],
27+
},
28+
{
29+
label: "Argument[1]",
30+
value: "Argument[1]",
31+
},
32+
{
33+
label: "Argument[text_rep:]",
34+
value: "Argument[text_rep:]",
35+
},
36+
{
37+
label: "Argument[block]",
38+
value: "Argument[block]",
39+
followup: [
40+
{
41+
label: "Parameter[0]",
42+
value: "Argument[block].Parameter[0]",
43+
followup: [
44+
{
45+
label: "Element[:query]",
46+
value: "Argument[block].Parameter[0].Element[:query]",
47+
},
48+
{
49+
label: "Element[:parameters]",
50+
value: "Argument[block].Parameter[0].Element[:parameters]",
51+
},
52+
],
53+
},
54+
{
55+
label: "Parameter[1]",
56+
value: "Argument[block].Parameter[1]",
57+
followup: [
58+
{
59+
label: "Field[@query]",
60+
value: "Argument[block].Parameter[1].Field[@query]",
61+
},
62+
],
63+
},
64+
],
65+
},
66+
{
67+
label: "ReturnValue",
68+
value: "ReturnValue",
69+
},
70+
];
71+
72+
describe("findMatchingOptions", () => {
73+
it.each([
74+
{
75+
// Argument[block].
76+
tokens: ["Argument[block]", ""],
77+
options: ["Argument[block].Parameter[0]", "Argument[block].Parameter[1]"],
78+
},
79+
{
80+
// Argument[block].Parameter[0]
81+
tokens: ["Argument[block]", "Parameter[0]"],
82+
options: ["Argument[block].Parameter[0]"],
83+
},
84+
{
85+
// Argument[block].Parameter[0].
86+
tokens: ["Argument[block]", "Parameter[0]", ""],
87+
options: [
88+
"Argument[block].Parameter[0].Element[:query]",
89+
"Argument[block].Parameter[0].Element[:parameters]",
90+
],
91+
},
92+
{
93+
// ""
94+
tokens: [""],
95+
options: [
96+
"Argument[self]",
97+
"Argument[0]",
98+
"Argument[1]",
99+
"Argument[text_rep:]",
100+
"Argument[block]",
101+
"ReturnValue",
102+
],
103+
},
104+
{
105+
// ""
106+
tokens: [],
107+
options: [
108+
"Argument[self]",
109+
"Argument[0]",
110+
"Argument[1]",
111+
"Argument[text_rep:]",
112+
"Argument[block]",
113+
"ReturnValue",
114+
],
115+
},
116+
{
117+
// block
118+
tokens: ["block"],
119+
options: ["Argument[block]"],
120+
},
121+
{
122+
// l
123+
tokens: ["l"],
124+
options: ["Argument[self]", "Argument[block]", "ReturnValue"],
125+
},
126+
{
127+
// L
128+
tokens: ["L"],
129+
options: ["Argument[self]", "Argument[block]", "ReturnValue"],
130+
},
131+
])(`creates options for $value`, ({ tokens, options }) => {
132+
expect(
133+
findMatchingOptions(suggestedOptions, tokens).map(
134+
(option) => option.value,
135+
),
136+
).toEqual(options);
137+
});
138+
});
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
type Snippet = {
2+
text: string;
3+
highlight: boolean;
4+
};
5+
6+
/**
7+
* Highlight creates a list of snippets that can be used to render a highlighted
8+
* string. This highlight is case-insensitive.
9+
*
10+
* @param text The text in which to create highlights
11+
* @param search The string that will be highlighted in the text.
12+
* @returns A list of snippets that can be used to render a highlighted string.
13+
*/
14+
export function createHighlights(text: string, search: string): Snippet[] {
15+
if (search === "") {
16+
return [{ text, highlight: false }];
17+
}
18+
19+
const searchLower = search.toLowerCase();
20+
const textLower = text.toLowerCase();
21+
22+
const highlights: Snippet[] = [];
23+
24+
let index = 0;
25+
for (;;) {
26+
const searchIndex = textLower.indexOf(searchLower, index);
27+
if (searchIndex === -1) {
28+
break;
29+
}
30+
31+
highlights.push({
32+
text: text.substring(index, searchIndex),
33+
highlight: false,
34+
});
35+
highlights.push({
36+
text: text.substring(searchIndex, searchIndex + search.length),
37+
highlight: true,
38+
});
39+
40+
index = searchIndex + search.length;
41+
}
42+
43+
highlights.push({
44+
text: text.substring(index),
45+
highlight: false,
46+
});
47+
48+
return highlights.filter((highlight) => highlight.text !== "");
49+
}
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
type Option<T extends Option<T>> = {
2+
label: string;
3+
followup?: T[];
4+
};
5+
6+
function findNestedMatchingOptions<T extends Option<T>>(
7+
parts: string[],
8+
options: T[],
9+
): T[] {
10+
const part = parts[0];
11+
const rest = parts.slice(1);
12+
13+
if (!part) {
14+
return options;
15+
}
16+
17+
const matchingOption = options.find((item) => item.label === part);
18+
if (!matchingOption) {
19+
return [];
20+
}
21+
22+
if (rest.length === 0) {
23+
return matchingOption.followup ?? [];
24+
}
25+
26+
return findNestedMatchingOptions(rest, matchingOption.followup ?? []);
27+
}
28+
29+
export function findMatchingOptions<T extends Option<T>>(
30+
options: T[],
31+
tokens: string[],
32+
): T[] {
33+
if (tokens.length === 0) {
34+
return options;
35+
}
36+
const prefixTokens = tokens.slice(0, tokens.length - 1);
37+
const lastToken = tokens[tokens.length - 1];
38+
39+
const matchingOptions = findNestedMatchingOptions(prefixTokens, options);
40+
41+
return matchingOptions.filter((item) =>
42+
item.label.toLowerCase().includes(lastToken.toLowerCase()),
43+
);
44+
}

0 commit comments

Comments
 (0)