Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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);
},
);
});
Original file line number Diff line number Diff line change
@@ -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 changes: 49 additions & 0 deletions extensions/ql-vscode/src/view/common/SuggestBox/highlight.ts
Original file line number Diff line number Diff line change
@@ -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 changes: 44 additions & 0 deletions extensions/ql-vscode/src/view/common/SuggestBox/options.ts
Original file line number Diff line number Diff line change
@@ -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()),
);
}