web: add command palette (#7314)

Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
This commit is contained in:
01zulfi
2025-02-19 15:48:44 +05:00
committed by GitHub
parent 53ba5562d3
commit a47967dd53
20 changed files with 2035 additions and 37 deletions

View File

@@ -19,7 +19,14 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
import { match } from "fuzzyjs";
import Database from "./index.js";
import { Item, Note, SortOptions, TrashItem } from "../types.js";
import {
Item,
Note,
Notebook,
Reminder,
SortOptions,
TrashItem
} from "../types.js";
import { DatabaseSchema, RawDatabaseSchema } from "../database/index.js";
import { AnyColumnWithTable, Kysely, sql } from "@streetwriters/kysely";
import { FilteredSelector } from "../database/sql-collection.js";
@@ -43,7 +50,11 @@ type FuzzySearchField<T> = {
export default class Lookup {
constructor(private readonly db: Database) {}
notes(query: string, notes?: FilteredSelector<Note>): SearchResults<Note> {
notes(
query: string,
notes?: FilteredSelector<Note>,
opts?: { titleOnly?: boolean }
): SearchResults<Note> {
return this.toSearchResults(async (limit, sortOptions) => {
const db = this.db.sql() as unknown as Kysely<RawDatabaseSchema>;
const excludedIds = this.db.trash.cache.notes;
@@ -61,21 +72,23 @@ export default class Lookup {
)
.where("title", "match", query)
.select(["id", sql<number>`rank * 10`.as("rank")])
.unionAll((eb) =>
eb
.selectFrom("content_fts")
.$if(!!notes, (eb) =>
eb.where("noteId", "in", notes!.filter.select("id"))
)
.$if(excludedIds.length > 0, (eb) =>
eb.where("id", "not in", excludedIds)
)
.where("data", "match", query)
.select(["noteId as id", "rank"])
.$castTo<{
id: string;
rank: number;
}>()
.$if(!opts?.titleOnly, (eb) =>
eb.unionAll((eb) =>
eb
.selectFrom("content_fts")
.$if(!!notes, (eb) =>
eb.where("noteId", "in", notes!.filter.select("id"))
)
.$if(excludedIds.length > 0, (eb) =>
eb.where("id", "not in", excludedIds)
)
.where("data", "match", query)
.select(["noteId as id", "rank"])
.$castTo<{
id: string;
rank: number;
}>()
)
)
.as("results")
)
@@ -99,12 +112,18 @@ export default class Lookup {
}, notes || this.db.notes.all);
}
notebooks(query: string) {
return this.search(this.db.notebooks.all, query, [
notebooks(query: string, opts: { titleOnly?: boolean } = {}) {
const fields: FuzzySearchField<Notebook>[] = [
{ name: "id", column: "notebooks.id", weight: -100 },
{ name: "title", column: "notebooks.title", weight: 10 },
{ name: "description", column: "notebooks.description" }
]);
{ name: "title", column: "notebooks.title", weight: 10 }
];
if (!opts.titleOnly) {
fields.push({
name: "description",
column: "notebooks.description"
});
}
return this.search(this.db.notebooks.all, query, fields);
}
tags(query: string) {
@@ -114,12 +133,18 @@ export default class Lookup {
]);
}
reminders(query: string) {
return this.search(this.db.reminders.all, query, [
reminders(query: string, opts: { titleOnly?: boolean } = {}) {
const fields: FuzzySearchField<Reminder>[] = [
{ name: "id", column: "reminders.id", weight: -100 },
{ name: "title", column: "reminders.title", weight: 10 },
{ name: "description", column: "reminders.description" }
]);
{ name: "title", column: "reminders.title", weight: 10 }
];
if (!opts.titleOnly) {
fields.push({
name: "description",
column: "reminders.description"
});
}
return this.search(this.db.reminders.all, query, fields);
}
trash(query: string): SearchResults<TrashItem> {

View File

@@ -0,0 +1,113 @@
/*
This file is part of the Notesnook project (https://notesnook.com/)
Copyright (C) 2023 Streetwriters (Private) Limited
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { fuzzy } from "../fuzzy";
import { test, expect, describe } from "vitest";
describe("lookup.fuzzy", () => {
test("should sort items by score when sort", () => {
const items = [
{
title: "system"
},
{
title: "hello"
},
{
title: "items"
}
];
const query = "ems";
expect(fuzzy(query, items, "title")).toStrictEqual([
items[2],
items[0],
items[1]
]);
});
describe("opts.matchOnly", () => {
test("should return all items when matchOnly is false", () => {
const items = [
{
title: "hello"
},
{
title: "world"
}
];
const successQuery = "o";
const failureQuery = "i";
expect(fuzzy(successQuery, items, "title")).toStrictEqual(items);
expect(fuzzy(failureQuery, items, "title")).toStrictEqual(items);
});
test("should return only matching items when matchOnly is true", () => {
const items = [
{
title: "hello"
},
{
title: "world"
}
];
const successQuery = "or";
const failureQuery = "i";
expect(
fuzzy(successQuery, items, "title", { matchOnly: true })
).toStrictEqual([items[1]]);
expect(
fuzzy(failureQuery, items, "title", { matchOnly: true })
).toStrictEqual([]);
});
});
describe("opts.prefix", () => {
test("should prefix matched field with provided value when given", () => {
const items = [
{
title: "hello"
},
{
title: "world"
}
];
const query = "d";
expect(
fuzzy(query, items, "title", {
prefix: "prefix-"
})
).toStrictEqual([items[0], { title: "worlprefix-d" }]);
});
});
describe("opt.suffix", () => {
test("should suffix matched field with provided value when given", () => {
const items = [
{
title: "hello"
},
{
title: "world"
}
];
const query = "llo";
expect(
fuzzy(query, items, "title", {
suffix: "-suffix"
})
).toStrictEqual([{ title: "hello-suffix" }, items[1]]);
});
});
});

View File

@@ -0,0 +1,64 @@
/*
This file is part of the Notesnook project (https://notesnook.com/)
Copyright (C) 2023 Streetwriters (Private) Limited
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { match, surround } from "fuzzyjs";
export function fuzzy<T>(
query: string,
items: T[],
key: keyof T,
opts?: {
prefix?: string;
suffix?: string;
/**
* If true, only items that match the query will be returned
*/
matchOnly?: boolean;
}
): T[] {
if (query === "") return items;
const fuzzied: [T, number][] = [];
for (const item of items) {
const result = match(query, `${item[key]}`);
if (!result.match) {
if (opts?.matchOnly) continue;
fuzzied.push([item, result.score]);
continue;
}
if (opts?.prefix || opts?.suffix) {
fuzzied.push([
{
...item,
[key]: surround(`${item[key]}`, {
result: result,
prefix: opts?.prefix,
suffix: opts?.suffix
})
},
result.score
]);
continue;
}
fuzzied.push([item, result.score]);
}
return fuzzied.sort((a, b) => b[1] - a[1]).map((f) => f[0]);
}

View File

@@ -41,3 +41,4 @@ export * from "./set.js";
export * from "./title-format.js";
export * from "./virtualized-grouping.js";
export * from "./crypto.js";
export * from "./fuzzy";