Skip to content

Commit

Permalink
feat: Add support for negative search terms
Browse files Browse the repository at this point in the history
  • Loading branch information
MohamedBassem committed Dec 31, 2024
1 parent 17af22b commit 4deda9d
Show file tree
Hide file tree
Showing 6 changed files with 282 additions and 69 deletions.
12 changes: 8 additions & 4 deletions apps/web/components/dashboard/search/QueryExplainerTooltip.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,28 +20,32 @@ export default function QueryExplainerTooltip({
case "tagName":
return (
<TableRow>
<TableCell>Tag Name</TableCell>
<TableCell>
{matcher.inverse ? "Doesn't have" : "Has"} Tag
</TableCell>
<TableCell>{matcher.tagName}</TableCell>
</TableRow>
);
case "listName":
return (
<TableRow>
<TableCell>List Name</TableCell>
<TableCell>
{matcher.inverse ? "Is not in" : "Is in "} List
</TableCell>
<TableCell>{matcher.listName}</TableCell>
</TableRow>
);
case "dateAfter":
return (
<TableRow>
<TableCell>Created After</TableCell>
<TableCell>{matcher.inverse ? "Not" : ""} Created After</TableCell>
<TableCell>{matcher.dateAfter.toDateString()}</TableCell>
</TableRow>
);
case "dateBefore":
return (
<TableRow>
<TableCell>Created Before</TableCell>
<TableCell>{matcher.inverse ? "Not" : ""} Created Before</TableCell>
<TableCell>{matcher.dateBefore.toDateString()}</TableCell>
</TableRow>
);
Expand Down
92 changes: 88 additions & 4 deletions packages/shared/searchQueryParser.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ describe("Search Query Parser", () => {
archived: true,
},
});
expect(parseSearchQuery("is:not_archived")).toEqual({
expect(parseSearchQuery("-is:archived")).toEqual({
result: "full",
text: "",
matcher: {
Expand All @@ -28,7 +28,7 @@ describe("Search Query Parser", () => {
favourited: true,
},
});
expect(parseSearchQuery("is:not_fav")).toEqual({
expect(parseSearchQuery("-is:fav")).toEqual({
result: "full",
text: "",
matcher: {
Expand All @@ -45,6 +45,16 @@ describe("Search Query Parser", () => {
matcher: {
type: "url",
url: "https://example.com",
inverse: false,
},
});
expect(parseSearchQuery("-url:https://example.com")).toEqual({
result: "full",
text: "",
matcher: {
type: "url",
url: "https://example.com",
inverse: true,
},
});
expect(parseSearchQuery('url:"https://example.com"')).toEqual({
Expand All @@ -53,6 +63,16 @@ describe("Search Query Parser", () => {
matcher: {
type: "url",
url: "https://example.com",
inverse: false,
},
});
expect(parseSearchQuery('-url:"https://example.com"')).toEqual({
result: "full",
text: "",
matcher: {
type: "url",
url: "https://example.com",
inverse: true,
},
});
expect(parseSearchQuery("#my-tag")).toEqual({
Expand All @@ -61,6 +81,16 @@ describe("Search Query Parser", () => {
matcher: {
type: "tagName",
tagName: "my-tag",
inverse: false,
},
});
expect(parseSearchQuery("-#my-tag")).toEqual({
result: "full",
text: "",
matcher: {
type: "tagName",
tagName: "my-tag",
inverse: true,
},
});
expect(parseSearchQuery('#"my tag"')).toEqual({
Expand All @@ -69,6 +99,16 @@ describe("Search Query Parser", () => {
matcher: {
type: "tagName",
tagName: "my tag",
inverse: false,
},
});
expect(parseSearchQuery('-#"my tag"')).toEqual({
result: "full",
text: "",
matcher: {
type: "tagName",
tagName: "my tag",
inverse: true,
},
});
expect(parseSearchQuery("list:my-list")).toEqual({
Expand All @@ -77,6 +117,16 @@ describe("Search Query Parser", () => {
matcher: {
type: "listName",
listName: "my-list",
inverse: false,
},
});
expect(parseSearchQuery("-list:my-list")).toEqual({
result: "full",
text: "",
matcher: {
type: "listName",
listName: "my-list",
inverse: true,
},
});
expect(parseSearchQuery('list:"my list"')).toEqual({
Expand All @@ -85,6 +135,16 @@ describe("Search Query Parser", () => {
matcher: {
type: "listName",
listName: "my list",
inverse: false,
},
});
expect(parseSearchQuery('-list:"my list"')).toEqual({
result: "full",
text: "",
matcher: {
type: "listName",
listName: "my list",
inverse: true,
},
});
});
Expand All @@ -95,6 +155,16 @@ describe("Search Query Parser", () => {
matcher: {
type: "dateAfter",
dateAfter: new Date("2023-10-12"),
inverse: false,
},
});
expect(parseSearchQuery("-after:2023-10-12")).toEqual({
result: "full",
text: "",
matcher: {
type: "dateAfter",
dateAfter: new Date("2023-10-12"),
inverse: true,
},
});
expect(parseSearchQuery("before:2023-10-12")).toEqual({
Expand All @@ -103,12 +173,22 @@ describe("Search Query Parser", () => {
matcher: {
type: "dateBefore",
dateBefore: new Date("2023-10-12"),
inverse: false,
},
});
expect(parseSearchQuery("-before:2023-10-12")).toEqual({
result: "full",
text: "",
matcher: {
type: "dateBefore",
dateBefore: new Date("2023-10-12"),
inverse: true,
},
});
});

test("complex queries", () => {
expect(parseSearchQuery("is:fav is:archived")).toEqual({
expect(parseSearchQuery("is:fav -is:archived")).toEqual({
result: "full",
text: "",
matcher: {
Expand All @@ -120,7 +200,7 @@ describe("Search Query Parser", () => {
},
{
type: "archived",
archived: true,
archived: false,
},
],
},
Expand All @@ -143,6 +223,7 @@ describe("Search Query Parser", () => {
{
type: "tagName",
tagName: "my-tag",
inverse: false,
},
],
},
Expand Down Expand Up @@ -170,6 +251,7 @@ describe("Search Query Parser", () => {
{
type: "tagName",
tagName: "my-tag",
inverse: false,
},
],
},
Expand Down Expand Up @@ -197,6 +279,7 @@ describe("Search Query Parser", () => {
{
type: "tagName",
tagName: "my-tag",
inverse: false,
},
],
},
Expand Down Expand Up @@ -237,6 +320,7 @@ describe("Search Query Parser", () => {
{
type: "tagName",
tagName: "my-tag",
inverse: false,
},
],
},
Expand Down
79 changes: 39 additions & 40 deletions packages/shared/searchQueryParser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
kmid,
kright,
lrec_sc,
opt,
rule,
seq,
str,
Expand All @@ -28,6 +29,7 @@ enum TokenType {
RParen = "RPAREN",
Space = "SPACE",
Hash = "HASH",
Minus = "MINUS",
}

// Rules are in order of priority
Expand All @@ -43,6 +45,7 @@ const lexerRules: [RegExp, TokenType][] = [
[/^\(/, TokenType.LParen],
[/^\)/, TokenType.RParen],
[/^\s+/, TokenType.Space],
[/^-/, TokenType.Minus],

// This needs to be last as it matches a lot of stuff
[/^[^ )(]+/, TokenType.Ident],
Expand Down Expand Up @@ -109,38 +112,32 @@ const EXP = rule<TokenType, TextAndMatcher>();

MATCHER.setPattern(
alt_sc(
apply(kright(str("is:"), tok(TokenType.Ident)), (toks) => {
switch (toks.text) {
case "fav":
return {
text: "",
matcher: { type: "favourited", favourited: true },
};
case "not_fav":
return {
text: "",
matcher: { type: "favourited", favourited: false },
};
case "archived":
return {
text: "",
matcher: { type: "archived", archived: true },
};
case "not_archived":
return {
text: "",
matcher: { type: "archived", archived: false },
};
default:
// If the token is not known, emit it as pure text
return {
text: `is:${toks.text}`,
matcher: undefined,
};
}
}),
apply(
seq(opt(str("-")), kright(str("is:"), tok(TokenType.Ident))),
([minus, ident]) => {
switch (ident.text) {
case "fav":
return {
text: "",
matcher: { type: "favourited", favourited: !minus },
};
case "archived":
return {
text: "",
matcher: { type: "archived", archived: !minus },
};
default:
// If the token is not known, emit it as pure text
return {
text: `${minus?.text ?? ""}is:${ident.text}`,
matcher: undefined,
};
}
},
),
apply(
seq(
opt(str("-")),
alt(tok(TokenType.Qualifier), tok(TokenType.Hash)),
alt(
apply(tok(TokenType.Ident), (tok) => {
Expand All @@ -151,36 +148,37 @@ MATCHER.setPattern(
}),
),
),
(toks) => {
switch (toks[0].text) {
([minus, qualifier, ident]) => {
switch (qualifier.text) {
case "url:":
return {
text: "",
matcher: { type: "url", url: toks[1] },
matcher: { type: "url", url: ident, inverse: !!minus },
};
case "#":
return {
text: "",
matcher: { type: "tagName", tagName: toks[1] },
matcher: { type: "tagName", tagName: ident, inverse: !!minus },
};
case "list:":
return {
text: "",
matcher: { type: "listName", listName: toks[1] },
matcher: { type: "listName", listName: ident, inverse: !!minus },
};
case "after:":
try {
return {
text: "",
matcher: {
type: "dateAfter",
dateAfter: z.coerce.date().parse(toks[1]),
dateAfter: z.coerce.date().parse(ident),
inverse: !!minus,
},
};
} catch (e) {
return {
// If parsing the date fails, emit it as pure text
text: toks[0].text + toks[1],
text: (minus?.text ?? "") + qualifier.text + ident,
matcher: undefined,
};
}
Expand All @@ -190,20 +188,21 @@ MATCHER.setPattern(
text: "",
matcher: {
type: "dateBefore",
dateBefore: z.coerce.date().parse(toks[1]),
dateBefore: z.coerce.date().parse(ident),
inverse: !!minus,
},
};
} catch (e) {
return {
// If parsing the date fails, emit it as pure text
text: toks[0].text + toks[1],
text: (minus?.text ?? "") + qualifier.text + ident,
matcher: undefined,
};
}
default:
// If the token is not known, emit it as pure text
return {
text: toks[0].text + toks[1],
text: (minus?.text ?? "") + qualifier.text + ident,
matcher: undefined,
};
}
Expand Down
Loading

0 comments on commit 4deda9d

Please sign in to comment.