aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--src/background/completion/impl/BookmarkRepositoryImpl.ts25
-rw-r--r--src/background/completion/impl/HistoryRepositoryImpl.ts62
-rw-r--r--src/background/completion/impl/PrefetchAndCache.ts108
-rw-r--r--src/background/completion/impl/filters.ts16
-rw-r--r--src/console/completion/hooks.ts170
-rw-r--r--test/background/completion/impl/PrefetchAndCache.test.ts100
-rw-r--r--test/background/completion/impl/filters.test.ts30
7 files changed, 166 insertions, 345 deletions
diff --git a/src/background/completion/impl/BookmarkRepositoryImpl.ts b/src/background/completion/impl/BookmarkRepositoryImpl.ts
index ed6c5a6..0c95cf7 100644
--- a/src/background/completion/impl/BookmarkRepositoryImpl.ts
+++ b/src/background/completion/impl/BookmarkRepositoryImpl.ts
@@ -1,21 +1,9 @@
import BookmarkRepository, { BookmarkItem } from "../BookmarkRepository";
-import { HistoryItem } from "../HistoryRepository";
-import PrefetchAndCache from "./PrefetchAndCache";
const COMPLETION_ITEM_LIMIT = 10;
export default class CachedBookmarkRepository implements BookmarkRepository {
- private bookmarkCache: PrefetchAndCache<BookmarkItem>;
-
- constructor() {
- this.bookmarkCache = new PrefetchAndCache(this.getter, this.filter, 10);
- }
-
- queryBookmarks(query: string): Promise<BookmarkItem[]> {
- return this.bookmarkCache.get(query);
- }
-
- private async getter(query: string): Promise<BookmarkItem[]> {
+ async queryBookmarks(query: string): Promise<BookmarkItem[]> {
const items = await browser.bookmarks.search({ query });
return items
.filter((item) => item.title && item.title.length > 0)
@@ -35,15 +23,4 @@ export default class CachedBookmarkRepository implements BookmarkRepository {
url: item.url!,
}));
}
-
- private filter(items: HistoryItem[], query: string) {
- return items.filter((item) => {
- return query.split(" ").every((keyword) => {
- return (
- item.title.toLowerCase().includes(keyword.toLowerCase()) ||
- item.url!.includes(keyword)
- );
- });
- });
- }
}
diff --git a/src/background/completion/impl/HistoryRepositoryImpl.ts b/src/background/completion/impl/HistoryRepositoryImpl.ts
index 3bf064e..789b393 100644
--- a/src/background/completion/impl/HistoryRepositoryImpl.ts
+++ b/src/background/completion/impl/HistoryRepositoryImpl.ts
@@ -1,49 +1,10 @@
import * as filters from "./filters";
import HistoryRepository, { HistoryItem } from "../HistoryRepository";
-import PrefetchAndCache from "./PrefetchAndCache";
const COMPLETION_ITEM_LIMIT = 10;
-export default class CachedHistoryRepository implements HistoryRepository {
- private historyCache: PrefetchAndCache<browser.history.HistoryItem>;
-
- constructor() {
- this.historyCache = new PrefetchAndCache(this.getter, this.filter, 10);
- }
-
+export default class HistoryRepositoryImpl implements HistoryRepository {
async queryHistories(keywords: string): Promise<HistoryItem[]> {
- const items = await this.historyCache.get(keywords);
-
- const filterOrKeep = <T>(
- source: T[],
- filter: (items: T[]) => T[],
- min: number
- ): T[] => {
- const filtered = filter(source);
- if (filtered.length < min) {
- return source;
- }
- return filtered;
- };
-
- return [items]
- .map((items) =>
- filterOrKeep(items, filters.filterByPathname, COMPLETION_ITEM_LIMIT)
- )
- .map((items) =>
- filterOrKeep(items, filters.filterByOrigin, COMPLETION_ITEM_LIMIT)
- )[0]
- .sort((x, y) => Number(y.visitCount) - Number(x.visitCount))
- .slice(0, COMPLETION_ITEM_LIMIT)
- .map((item) => ({
- title: item.title!,
- url: item.url!,
- }));
- }
-
- private async getter(
- keywords: string
- ): Promise<browser.history.HistoryItem[]> {
const items = await browser.history.search({
text: keywords,
startTime: 0,
@@ -52,17 +13,14 @@ export default class CachedHistoryRepository implements HistoryRepository {
return [items]
.map(filters.filterBlankTitle)
.map(filters.filterHttp)
- .map(filters.filterByTailingSlash)[0];
- }
-
- private filter(items: browser.history.HistoryItem[], query: string) {
- return items.filter((item) => {
- return query.split(" ").every((keyword) => {
- return (
- item.title!.toLowerCase().includes(keyword.toLowerCase()) ||
- item.url!.includes(keyword)
- );
- });
- });
+ .map(filters.filterByTailingSlash)
+ .map((pages) => filters.filterByPathname(pages, COMPLETION_ITEM_LIMIT))
+ .map((pages) => filters.filterByOrigin(pages, COMPLETION_ITEM_LIMIT))[0]
+ .sort((x, y) => Number(y.visitCount) - Number(x.visitCount))
+ .slice(0, COMPLETION_ITEM_LIMIT)
+ .map((item) => ({
+ title: item.title!,
+ url: item.url!,
+ }));
}
}
diff --git a/src/background/completion/impl/PrefetchAndCache.ts b/src/background/completion/impl/PrefetchAndCache.ts
deleted file mode 100644
index d2889b0..0000000
--- a/src/background/completion/impl/PrefetchAndCache.ts
+++ /dev/null
@@ -1,108 +0,0 @@
-type Getter<T> = (query: string) => Promise<T[]>;
-type Filter<T> = (src: T[], query: string) => T[];
-
-const WHITESPACE = /\s/;
-
-// `shortKey` returns a shorten key to pre-fetch completions and store in the
-// cache. The shorten key is generated by the following rules:
-//
-// 1. If the query contains a space in the middle: i.e. the query consists of
-// multiple words, the method removes the last word from the query, and
-// returns joined remaining words with space.
-//
-// 2. If the query is a single word and it's an URL, the method returns a new
-// URL excluding search query with the upper path of the original URL.
-//
-// 3. If the query is a single word and it's not an URL, the method returns a
-// word with the half-length of the original query.
-//
-// Examples:
-//
-// shortKey("hello world good bye")
-// => "hello world good"
-//
-// shortKey("https://example.com/path/to/resource?q=hello")
-// => "https://example.com/path/to/"
-//
-// shortKey("the-query-with-super-long-word")
-// => "the-query-with-"
-//
-export const shortKey = (query: string): string => {
- if (WHITESPACE.test(query)) {
- return query
- .split(WHITESPACE)
- .filter((word) => word.length > 0)
- .slice(0, -1)
- .join(" ");
- }
- let url;
- try {
- url = new URL(query);
- } catch (e) {
- return query.slice(0, query.length / 2);
- }
-
- if (url.origin === query) {
- // may be on typing or removing URLs such as "such as https://goog"
- return query.slice(0, query.length / 2);
- }
- if (url.pathname.endsWith("/")) {
- // remove parameters and move to upper path
- return new URL("..", url).href;
- }
- // remove parameters
- return new URL(".", url).href;
-};
-
-export default class PrefetchAndCache<T> {
- private shortKey: string | undefined;
-
- private shortKeyCache: T[] = [];
-
- constructor(
- private getter: Getter<T>,
- private filter: Filter<T>,
- private prefetchThrethold: number = 1
- ) {}
-
- async get(query: string): Promise<T[]> {
- query = query.trim();
- if (query.length < this.prefetchThrethold) {
- this.shortKey = undefined;
- return this.getter(query);
- }
-
- if (this.needToRefresh(query)) {
- this.shortKey = shortKey(query);
- this.shortKeyCache = await this.getter(this.shortKey);
- }
- return this.filter(this.shortKeyCache, query);
- }
-
- private needToRefresh(query: string): boolean {
- if (!this.shortKey) {
- // no cache
- return true;
- }
-
- if (query.length < this.shortKey.length) {
- // query: "hello"
- // cache: "hello_world"
- return true;
- }
-
- if (!query.startsWith(this.shortKey)) {
- // queyr: "hello_w"
- // shorten: "hello_morning"
- return true;
- }
-
- if (query.slice(this.shortKey.length).includes(" ")) {
- // queyr: "hello x"
- // shorten: "hello"
- return true;
- }
-
- return false;
- }
-}
diff --git a/src/background/completion/impl/filters.ts b/src/background/completion/impl/filters.ts
index 523491d..3d1cfb4 100644
--- a/src/background/completion/impl/filters.ts
+++ b/src/background/completion/impl/filters.ts
@@ -37,7 +37,7 @@ const filterByTailingSlash = (items: Item[]): Item[] => {
});
};
-const filterByPathname = (items: Item[]): Item[] => {
+const filterByPathname = (items: Item[], min: number): Item[] => {
const hash: { [key: string]: Item } = {};
for (const item of items) {
const url = new URL(item.url as string);
@@ -50,10 +50,14 @@ const filterByPathname = (items: Item[]): Item[] => {
hash[pathname] = item;
}
}
- return Object.values(hash);
+ const filtered = Object.values(hash);
+ if (filtered.length < min) {
+ return items;
+ }
+ return filtered;
};
-const filterByOrigin = (items: Item[]): Item[] => {
+const filterByOrigin = (items: Item[], min: number): Item[] => {
const hash: { [key: string]: Item } = {};
for (const item of items) {
const origin = new URL(item.url as string).origin;
@@ -65,7 +69,11 @@ const filterByOrigin = (items: Item[]): Item[] => {
hash[origin] = item;
}
}
- return Object.values(hash);
+ const filtered = Object.values(hash);
+ if (filtered.length < min) {
+ return items;
+ }
+ return filtered;
};
export {
diff --git a/src/console/completion/hooks.ts b/src/console/completion/hooks.ts
index aac431b..c3940c7 100644
--- a/src/console/completion/hooks.ts
+++ b/src/console/completion/hooks.ts
@@ -35,6 +35,41 @@ const propertyDocs: { [key: string]: string } = {
const completionClient = new CompletionClient();
+const useDelayedCallback = <T extends unknown, U extends unknown>(
+ callback: (arg1: T, arg2: U) => void,
+ timeout: number
+) => {
+ const [timer, setTimer] = React.useState<
+ ReturnType<typeof setTimeout> | undefined
+ >();
+ const [enabled, setEnabled] = React.useState(false);
+
+ const enableDelay = React.useCallback(() => {
+ setEnabled(true);
+ }, [setEnabled]);
+
+ const delayedCallback = React.useCallback(
+ (arg1: T, arg2: U) => {
+ if (enabled) {
+ if (typeof timer !== "undefined") {
+ clearTimeout(timer);
+ }
+ const id = setTimeout(() => {
+ callback(arg1, arg2);
+ clearTimeout(timer!);
+ setTimer(undefined);
+ }, timeout);
+ setTimer(id);
+ } else {
+ callback(arg1, arg2);
+ }
+ },
+ [enabled, timer]
+ );
+
+ return { enableDelay, delayedCallback };
+};
+
const getCommandCompletions = async (query: string): Promise<Completions> => {
const items = Object.entries(commandDocs)
.filter(([name]) => name.startsWith(query))
@@ -185,63 +220,88 @@ export const useCompletions = () => {
});
}, []);
- React.useEffect(() => {
- const text = state.completionSource;
- const phase = commandLineParser.inputPhase(text);
- if (phase === InputPhase.OnCommand) {
- getCommandCompletions(text).then((completions) =>
- dispatch(actions.setCompletions(completions))
- );
- } else {
- let cmd: CommandLine | null = null;
- try {
- cmd = commandLineParser.parse(text);
- } catch (e) {
- if (e instanceof UnknownCommandError) {
- return;
- }
- }
- switch (cmd?.command) {
- case Command.Open:
- case Command.TabOpen:
- case Command.WindowOpen:
- if (!state.completionTypes) {
- initCompletion(text);
- return;
- }
-
- getOpenCompletions(
- cmd.command,
- cmd.args,
- state.completionTypes
- ).then((completions) =>
+ const { delayedCallback: queryCompletions, enableDelay } = useDelayedCallback(
+ React.useCallback(
+ (text: string, completionTypes?: CompletionType[]) => {
+ const phase = commandLineParser.inputPhase(text);
+ if (phase === InputPhase.OnCommand) {
+ getCommandCompletions(text).then((completions) =>
dispatch(actions.setCompletions(completions))
);
- break;
- case Command.Buffer:
- getTabCompletions(cmd.command, cmd.args, false).then((completions) =>
- dispatch(actions.setCompletions(completions))
- );
- break;
- case Command.BufferDelete:
- case Command.BuffersDelete:
- getTabCompletions(cmd.command, cmd.args, true).then((completions) =>
- dispatch(actions.setCompletions(completions))
- );
- break;
- case Command.BufferDeleteForce:
- case Command.BuffersDeleteForce:
- getTabCompletions(cmd.command, cmd.args, false).then((completions) =>
- dispatch(actions.setCompletions(completions))
- );
- break;
- case Command.Set:
- getPropertyCompletions(cmd.command, cmd.args).then((completions) =>
- dispatch(actions.setCompletions(completions))
- );
- break;
- }
- }
+ } else {
+ let cmd: CommandLine | null = null;
+ try {
+ cmd = commandLineParser.parse(text);
+ } catch (e) {
+ if (e instanceof UnknownCommandError) {
+ return;
+ }
+ }
+ switch (cmd?.command) {
+ case Command.Open:
+ case Command.TabOpen:
+ case Command.WindowOpen:
+ if (!completionTypes) {
+ initCompletion(text);
+ return;
+ }
+
+ getOpenCompletions(
+ cmd.command,
+ cmd.args,
+ completionTypes
+ ).then((completions) =>
+ dispatch(actions.setCompletions(completions))
+ );
+ break;
+ case Command.Buffer:
+ getTabCompletions(
+ cmd.command,
+ cmd.args,
+ false
+ ).then((completions) =>
+ dispatch(actions.setCompletions(completions))
+ );
+ break;
+ case Command.BufferDelete:
+ case Command.BuffersDelete:
+ getTabCompletions(
+ cmd.command,
+ cmd.args,
+ true
+ ).then((completions) =>
+ dispatch(actions.setCompletions(completions))
+ );
+ break;
+ case Command.BufferDeleteForce:
+ case Command.BuffersDeleteForce:
+ getTabCompletions(
+ cmd.command,
+ cmd.args,
+ false
+ ).then((completions) =>
+ dispatch(actions.setCompletions(completions))
+ );
+ break;
+ case Command.Set:
+ getPropertyCompletions(
+ cmd.command,
+ cmd.args
+ ).then((completions) =>
+ dispatch(actions.setCompletions(completions))
+ );
+ break;
+ }
+ enableDelay();
+ }
+ },
+ [dispatch]
+ ),
+ 100
+ );
+
+ React.useEffect(() => {
+ queryCompletions(state.completionSource, state.completionTypes);
}, [state.completionSource, state.completionTypes]);
return {
diff --git a/test/background/completion/impl/PrefetchAndCache.test.ts b/test/background/completion/impl/PrefetchAndCache.test.ts
deleted file mode 100644
index b24dfa9..0000000
--- a/test/background/completion/impl/PrefetchAndCache.test.ts
+++ /dev/null
@@ -1,100 +0,0 @@
-import PrefetchAndCache, {
- shortKey,
-} from "../../../../src/background/completion/impl/PrefetchAndCache";
-import { expect } from "chai";
-
-class MockRepository {
- public history: string[] = [];
-
- constructor(private items: string[]) {}
-
- get(query: string): Promise<string[]> {
- this.history.push(query);
- if (query.length === 0) {
- return Promise.resolve(this.items);
- } else {
- return Promise.resolve(this.items.filter((item) => item.includes(query)));
- }
- }
-}
-const filter = (items: string[], query: string) =>
- query.length === 0 ? items : items.filter((item) => item.includes(query));
-
-describe("shortKey", () => {
- it("returns query excluding the last word", () => {
- const query = "hello\t world good bye";
- const shorten = shortKey(query);
- expect(shorten).to.equal("hello world good");
- });
-
- it("returns half-length of the query", () => {
- const query = "the-query-with-super-long-word";
- const shorten = shortKey(query);
- expect(shorten).to.equal("the-query-with-");
- });
-
- it("returns shorten URL", () => {
- let query = "https://example.com/path/to/resource?q=hello";
- let shorten = shortKey(query);
- expect(shorten).to.equal("https://example.com/path/to/");
-
- query = "https://example.com/path/to/resource/#id1";
- shorten = shortKey(query);
- expect(shorten).to.equal("https://example.com/path/to/");
-
- query = "https://www.google.c";
- shorten = shortKey(query);
- expect(shorten).to.equal("https://ww");
- });
-});
-
-describe("PrefetchAndCache", () => {
- describe("get", () => {
- it("returns cached request", async () => {
- const repo = new MockRepository([
- "apple",
- "apple pie",
- "apple juice",
- "banana",
- "banana pudding",
- "cherry",
- ]);
- const sut = new PrefetchAndCache(repo.get.bind(repo), filter);
-
- expect(await sut.get("apple pie")).deep.equal(["apple pie"]);
- expect(await sut.get("apple ")).deep.equal([
- "apple",
- "apple pie",
- "apple juice",
- ]);
- expect(await sut.get("apple")).deep.equal([
- "apple",
- "apple pie",
- "apple juice",
- ]);
- expect(await sut.get("appl")).deep.equal([
- "apple",
- "apple pie",
- "apple juice",
- ]);
- expect(repo.history).to.deep.equal(["apple", "ap"]);
-
- expect(await sut.get("banana")).deep.equal(["banana", "banana pudding"]);
- expect(repo.history).to.deep.equal(["apple", "ap", "ban"]);
- expect(await sut.get("banana p")).deep.equal(["banana pudding"]);
- expect(repo.history).to.deep.equal(["apple", "ap", "ban", "banana"]);
- expect(await sut.get("ba")).deep.equal(["banana", "banana pudding"]);
- expect(repo.history).to.deep.equal(["apple", "ap", "ban", "banana", "b"]);
-
- expect(await sut.get("")).to.have.lengthOf(6);
- expect(repo.history).to.deep.equal([
- "apple",
- "ap",
- "ban",
- "banana",
- "b",
- "",
- ]);
- });
- });
-});
diff --git a/test/background/completion/impl/filters.test.ts b/test/background/completion/impl/filters.test.ts
index 70c2663..b160944 100644
--- a/test/background/completion/impl/filters.test.ts
+++ b/test/background/completion/impl/filters.test.ts
@@ -54,6 +54,19 @@ describe("background/usecases/filters", () => {
});
describe("filterByPathname", () => {
+ it("remains items less than minimam length", () => {
+ const pages = [
+ { id: "0", url: "http://i-beam.org/search?q=apple" },
+ { id: "1", url: "http://i-beam.org/search?q=apple_banana" },
+ { id: "2", url: "http://i-beam.org/search?q=apple_banana_cherry" },
+ { id: "3", url: "http://i-beam.org/request?q=apple" },
+ { id: "4", url: "http://i-beam.org/request?q=apple_banana" },
+ { id: "5", url: "http://i-beam.org/request?q=apple_banana_cherry" },
+ ];
+ const filtered = filters.filterByPathname(pages, 10);
+ expect(filtered).to.have.lengthOf(6);
+ });
+
it("filters by length of pathname", () => {
const pages = [
{ id: "0", url: "http://i-beam.org/search?q=apple" },
@@ -63,7 +76,7 @@ describe("background/usecases/filters", () => {
{ id: "4", url: "http://i-beam.net/search?q=apple_banana" },
{ id: "5", url: "http://i-beam.net/search?q=apple_banana_cherry" },
];
- const filtered = filters.filterByPathname(pages);
+ const filtered = filters.filterByPathname(pages, 0);
expect(filtered).to.deep.equal([
{ id: "0", url: "http://i-beam.org/search?q=apple" },
{ id: "3", url: "http://i-beam.net/search?q=apple" },
@@ -72,6 +85,19 @@ describe("background/usecases/filters", () => {
});
describe("filterByOrigin", () => {
+ it("remains items less than minimam length", () => {
+ const pages = [
+ { id: "0", url: "http://i-beam.org/search?q=apple" },
+ { id: "1", url: "http://i-beam.org/search?q=apple_banana" },
+ { id: "2", url: "http://i-beam.org/search?q=apple_banana_cherry" },
+ { id: "3", url: "http://i-beam.org/request?q=apple" },
+ { id: "4", url: "http://i-beam.org/request?q=apple_banana" },
+ { id: "5", url: "http://i-beam.org/request?q=apple_banana_cherry" },
+ ];
+ const filtered = filters.filterByOrigin(pages, 10);
+ expect(filtered).to.have.lengthOf(6);
+ });
+
it("filters by length of pathname", () => {
const pages = [
{ id: "0", url: "http://i-beam.org/search?q=apple" },
@@ -81,7 +107,7 @@ describe("background/usecases/filters", () => {
{ id: "4", url: "http://i-beam.org/request?q=apple_banana" },
{ id: "5", url: "http://i-beam.org/request?q=apple_banana_cherry" },
];
- const filtered = filters.filterByOrigin(pages);
+ const filtered = filters.filterByOrigin(pages, 0);
expect(filtered).to.deep.equal([
{ id: "0", url: "http://i-beam.org/search?q=apple" },
]);