mirror of
https://github.com/streetwriters/notesnook.git
synced 2025-12-22 22:49:45 +01:00
web: add command palette (#7314)
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
This commit is contained in:
@@ -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> {
|
||||
|
||||
113
packages/core/src/utils/__tests__/fuzzy.test.ts
Normal file
113
packages/core/src/utils/__tests__/fuzzy.test.ts
Normal 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]]);
|
||||
});
|
||||
});
|
||||
});
|
||||
64
packages/core/src/utils/fuzzy.ts
Normal file
64
packages/core/src/utils/fuzzy.ts
Normal 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]);
|
||||
}
|
||||
@@ -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";
|
||||
|
||||
Reference in New Issue
Block a user