From 6829e24c62c0291336502b3390905b57b81abd21 Mon Sep 17 00:00:00 2001 From: Shin'ya Ueoka Date: Thu, 26 Mar 2020 07:17:55 +0900 Subject: Use new completion use-case on current use-case (aliased) --- src/background/completion/BookmarkRepository.ts | 32 +++++++++ src/background/completion/CompletionUseCase.ts | 39 +++++++++++ src/background/completion/HistoryRepository.ts | 32 +++++++++ src/background/completion/filters.ts | 76 ++++++++++++++++++++++ .../repositories/CompletionsRepository.ts | 24 ------- src/background/usecases/CompletionsUseCase.ts | 55 ++++++---------- src/background/usecases/filters.ts | 76 ---------------------- 7 files changed, 199 insertions(+), 135 deletions(-) create mode 100644 src/background/completion/BookmarkRepository.ts create mode 100644 src/background/completion/CompletionUseCase.ts create mode 100644 src/background/completion/HistoryRepository.ts create mode 100644 src/background/completion/filters.ts delete mode 100644 src/background/usecases/filters.ts (limited to 'src/background') diff --git a/src/background/completion/BookmarkRepository.ts b/src/background/completion/BookmarkRepository.ts new file mode 100644 index 0000000..12f5455 --- /dev/null +++ b/src/background/completion/BookmarkRepository.ts @@ -0,0 +1,32 @@ +import { injectable } from "tsyringe"; + +export type BookmarkItem = { + title: string + url: string +} + +const COMPLETION_ITEM_LIMIT = 10; + +@injectable() +export default class BookmarkRepository { + async queryBookmarks(query: string): Promise { + const items = await browser.bookmarks.search({ query }); + return items + .filter(item => item.title && item.title.length > 0) + .filter(item => item.type === 'bookmark' && item.url) + .filter((item) => { + let url = undefined; + try { + url = new URL(item.url!!); + } catch (e) { + return false; + } + return url.protocol !== 'place:'; + }) + .slice(0, COMPLETION_ITEM_LIMIT) + .map(item => ({ + title: item.title!!, + url: item.url!!, + })); + } +} diff --git a/src/background/completion/CompletionUseCase.ts b/src/background/completion/CompletionUseCase.ts new file mode 100644 index 0000000..898efd4 --- /dev/null +++ b/src/background/completion/CompletionUseCase.ts @@ -0,0 +1,39 @@ +import { inject, injectable } from "tsyringe"; +import HistoryRepository from "./HistoryRepository"; +import BookmarkRepository from "./BookmarkRepository"; +import CachedSettingRepository from "../repositories/CachedSettingRepository"; + +export type BookmarkItem = { + title: string + url: string +} + +export type HistoryItem = { + title: string + url: string +} + +@injectable() +export default class CompletionUseCase { + constructor( + private bookmarkRepository: BookmarkRepository, + private historyRepository: HistoryRepository, + @inject("CachedSettingRepository") + private cachedSettingRepository: CachedSettingRepository, + ) { + } + + async requestSearchEngines(query: string): Promise { + const settings = await this.cachedSettingRepository.get(); + return Object.keys(settings.search.engines) + .filter(key => key.startsWith(query)) + } + + requestBookmarks(query: any): Promise { + return this.bookmarkRepository.queryBookmarks(query); + } + + async requestHistory(query: string): Promise { + return this.historyRepository.queryHistories(query); + } +} \ No newline at end of file diff --git a/src/background/completion/HistoryRepository.ts b/src/background/completion/HistoryRepository.ts new file mode 100644 index 0000000..1cfaf1b --- /dev/null +++ b/src/background/completion/HistoryRepository.ts @@ -0,0 +1,32 @@ +import * as filters from "./filters"; +import { injectable } from "tsyringe"; + +export type HistoryItem = { + title: string + url: string +} + +const COMPLETION_ITEM_LIMIT = 10; + +@injectable() +export default class HistoryRepository { + async queryHistories(keywords: string): Promise { + const items = await browser.history.search({ + text: keywords, + startTime: 0, + }); + + return [items] + .map(filters.filterBlankTitle) + .map(filters.filterHttp) + .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/filters.ts b/src/background/completion/filters.ts new file mode 100644 index 0000000..98957a7 --- /dev/null +++ b/src/background/completion/filters.ts @@ -0,0 +1,76 @@ +type Item = browser.history.HistoryItem; + +const filterHttp = (items: Item[]): Item[] => { + const httpsHosts = items.map(x => new URL(x.url as string)) + .filter(x => x.protocol === 'https:') + .map(x => x.host); + const hostsSet = new Set(httpsHosts); + + return items.filter((item: Item) => { + const url = new URL(item.url as string); + return url.protocol === 'https:' || !hostsSet.has(url.host); + }); +}; + +const filterBlankTitle = (items: Item[]): Item[] => { + return items.filter(item => item.title && item.title !== ''); +}; + +const filterByTailingSlash = (items: Item[]): Item[] => { + const urls = items.map(item => new URL(item.url as string)); + const simplePaths = urls + .filter(url => url.hash === '' && url.search === '') + .map(url => url.origin + url.pathname); + const pathsSet = new Set(simplePaths); + + return items.filter((item) => { + const url = new URL(item.url as string); + if (url.hash !== '' || url.search !== '' || + url.pathname.slice(-1) !== '/') { + return true; + } + return !pathsSet.has(url.origin + url.pathname.slice(0, -1)); + }); +}; + +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); + const pathname = url.origin + url.pathname; + if (!hash[pathname]) { + hash[pathname] = item; + } else if ((hash[pathname].url as string).length > + (item.url as string).length) { + hash[pathname] = item; + } + } + const filtered = Object.values(hash); + if (filtered.length < min) { + return items; + } + return filtered; +}; + +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; + if (!hash[origin]) { + hash[origin] = item; + } else if ((hash[origin].url as string).length > + (item.url as string).length) { + hash[origin] = item; + } + } + const filtered = Object.values(hash); + if (filtered.length < min) { + return items; + } + return filtered; +}; + +export { + filterHttp, filterBlankTitle, filterByTailingSlash, + filterByPathname, filterByOrigin +}; diff --git a/src/background/repositories/CompletionsRepository.ts b/src/background/repositories/CompletionsRepository.ts index dfecff0..7d0434c 100644 --- a/src/background/repositories/CompletionsRepository.ts +++ b/src/background/repositories/CompletionsRepository.ts @@ -1,33 +1,9 @@ import { injectable } from 'tsyringe'; type Tab = browser.tabs.Tab; -type BookmarkTreeNode = browser.bookmarks.BookmarkTreeNode; @injectable() export default class CompletionsRepository { - async queryBookmarks(keywords: string): Promise { - const items = await browser.bookmarks.search({ query: keywords }); - return items.filter((item) => { - if (!item.url) { - return false; - } - let url = undefined; - try { - url = new URL(item.url); - } catch (e) { - return false; - } - return item.type === 'bookmark' && url.protocol !== 'place:'; - }); - } - - queryHistories(keywords: string): Promise { - return browser.history.search({ - text: keywords, - startTime: 0, - }); - } - async queryTabs(keywords: string, excludePinned: boolean): Promise { const tabs = await browser.tabs.query({ currentWindow: true }); return tabs.filter((t) => { diff --git a/src/background/usecases/CompletionsUseCase.ts b/src/background/usecases/CompletionsUseCase.ts index 9874644..b75b635 100644 --- a/src/background/usecases/CompletionsUseCase.ts +++ b/src/background/usecases/CompletionsUseCase.ts @@ -2,15 +2,12 @@ import { injectable, inject } from 'tsyringe'; import CompletionGroup from '../domains/CompletionGroup'; import CommandDocs from '../domains/CommandDocs'; import CompletionsRepository from '../repositories/CompletionsRepository'; -import * as filters from './filters'; import CachedSettingRepository from '../repositories/CachedSettingRepository'; import TabPresenter from '../presenters/TabPresenter'; import Properties from '../../shared/settings/Properties'; - -const COMPLETION_ITEM_LIMIT = 10; +import CompletionUseCase from "../completion/CompletionUseCase"; type Tab = browser.tabs.Tab; -type HistoryItem = browser.history.HistoryItem; @injectable() export default class CompletionsUseCase { @@ -18,6 +15,7 @@ export default class CompletionsUseCase { private tabPresenter: TabPresenter, private completionsRepository: CompletionsRepository, @inject("CachedSettingRepository") private cachedSettingRepository: CachedSettingRepository, + private completionUseCase: CompletionUseCase ) { } @@ -179,42 +177,29 @@ export default class CompletionsUseCase { return [{ name: 'Buffers', items }]; } - async querySearchEngineItems(name: string, keywords: string) { - const settings = await this.cachedSettingRepository.get(); - const engines = Object.keys(settings.search.engines) - .filter(key => key.startsWith(keywords)); - return engines.map(key => ({ - caption: key, - content: name + ' ' + key, + async querySearchEngineItems(name: string, query: string) { + const engines = await this.completionUseCase.requestSearchEngines(query); + return engines.map(item => ({ + caption: item, + content: name + ' ' + item, })); } - async queryHistoryItems(name: string, keywords: string) { - let histories = await this.completionsRepository.queryHistories(keywords); - histories = [histories] - .map(filters.filterBlankTitle) - .map(filters.filterHttp) - .map(filters.filterByTailingSlash) - .map(pages => filters.filterByPathname(pages, COMPLETION_ITEM_LIMIT)) - .map(pages => filters.filterByOrigin(pages, COMPLETION_ITEM_LIMIT))[0] - .sort((x: HistoryItem, y: HistoryItem): number => { - return Number(y.visitCount) - Number(x.visitCount); - }) - .slice(0, COMPLETION_ITEM_LIMIT); - return histories.map(page => ({ - caption: page.title, - content: name + ' ' + page.url, - url: page.url + async queryHistoryItems(name: string, query: string) { + const items = await this.completionUseCase.requestHistory(query); + return items.map(item => ({ + caption: item.title, + content: name + ' ' + item.url, + url: item.url })); } - async queryBookmarkItems(name: string, keywords: string) { - const bookmarks = await this.completionsRepository.queryBookmarks(keywords); - return bookmarks.slice(0, COMPLETION_ITEM_LIMIT) - .map(page => ({ - caption: page.title, - content: name + ' ' + page.url, - url: page.url - })); + async queryBookmarkItems(name: string, query: string) { + const items = await this.completionUseCase.requestHistory(query); + return items.map(item => ({ + caption: item.title, + content: name + ' ' + item.url, + url: item.url + })); } } diff --git a/src/background/usecases/filters.ts b/src/background/usecases/filters.ts deleted file mode 100644 index 98957a7..0000000 --- a/src/background/usecases/filters.ts +++ /dev/null @@ -1,76 +0,0 @@ -type Item = browser.history.HistoryItem; - -const filterHttp = (items: Item[]): Item[] => { - const httpsHosts = items.map(x => new URL(x.url as string)) - .filter(x => x.protocol === 'https:') - .map(x => x.host); - const hostsSet = new Set(httpsHosts); - - return items.filter((item: Item) => { - const url = new URL(item.url as string); - return url.protocol === 'https:' || !hostsSet.has(url.host); - }); -}; - -const filterBlankTitle = (items: Item[]): Item[] => { - return items.filter(item => item.title && item.title !== ''); -}; - -const filterByTailingSlash = (items: Item[]): Item[] => { - const urls = items.map(item => new URL(item.url as string)); - const simplePaths = urls - .filter(url => url.hash === '' && url.search === '') - .map(url => url.origin + url.pathname); - const pathsSet = new Set(simplePaths); - - return items.filter((item) => { - const url = new URL(item.url as string); - if (url.hash !== '' || url.search !== '' || - url.pathname.slice(-1) !== '/') { - return true; - } - return !pathsSet.has(url.origin + url.pathname.slice(0, -1)); - }); -}; - -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); - const pathname = url.origin + url.pathname; - if (!hash[pathname]) { - hash[pathname] = item; - } else if ((hash[pathname].url as string).length > - (item.url as string).length) { - hash[pathname] = item; - } - } - const filtered = Object.values(hash); - if (filtered.length < min) { - return items; - } - return filtered; -}; - -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; - if (!hash[origin]) { - hash[origin] = item; - } else if ((hash[origin].url as string).length > - (item.url as string).length) { - hash[origin] = item; - } - } - const filtered = Object.values(hash); - if (filtered.length < min) { - return items; - } - return filtered; -}; - -export { - filterHttp, filterBlankTitle, filterByTailingSlash, - filterByPathname, filterByOrigin -}; -- cgit v1.2.3 From b2a37b8fc3e273dd71e1e3558c58be8002aa3789 Mon Sep 17 00:00:00 2001 From: Shin'ya Ueoka Date: Thu, 26 Mar 2020 22:17:00 +0900 Subject: Query completions on open command by a completion source --- src/background/completion/CompletionUseCase.ts | 21 ++++++++ src/background/controllers/CompletionController.ts | 31 +++++++++++ .../infrastructures/ContentMessageListener.ts | 10 ++++ src/console/actions/console.ts | 61 ++++++++++++++++++++-- src/console/actions/index.ts | 2 + src/console/clients/CompletionClient.ts | 54 +++++++++++++++++++ src/console/components/Console.tsx | 12 ++++- src/console/index.tsx | 4 +- src/console/reducers/index.ts | 4 ++ src/shared/CompletionType.ts | 7 +++ src/shared/messages.ts | 46 +++++++++++++++- 11 files changed, 244 insertions(+), 8 deletions(-) create mode 100644 src/background/controllers/CompletionController.ts create mode 100644 src/console/clients/CompletionClient.ts create mode 100644 src/shared/CompletionType.ts (limited to 'src/background') diff --git a/src/background/completion/CompletionUseCase.ts b/src/background/completion/CompletionUseCase.ts index 898efd4..fd3c279 100644 --- a/src/background/completion/CompletionUseCase.ts +++ b/src/background/completion/CompletionUseCase.ts @@ -2,6 +2,7 @@ import { inject, injectable } from "tsyringe"; import HistoryRepository from "./HistoryRepository"; import BookmarkRepository from "./BookmarkRepository"; import CachedSettingRepository from "../repositories/CachedSettingRepository"; +import CompletionType from "../../shared/CompletionType"; export type BookmarkItem = { title: string @@ -23,6 +24,26 @@ export default class CompletionUseCase { ) { } + async getCompletionTypes(): Promise { + const settings = await this.cachedSettingRepository.get(); + const types: CompletionType[] = []; + for (const c of settings.properties.complete) { + switch (c) { + case 's': + types.push(CompletionType.SearchEngines); + break; + case 'h': + types.push(CompletionType.History); + break; + case 'b': + types.push(CompletionType.Bookmarks); + break; + } + // ignore invalid characters in the complete property + } + return types; + } + async requestSearchEngines(query: string): Promise { const settings = await this.cachedSettingRepository.get(); return Object.keys(settings.search.engines) diff --git a/src/background/controllers/CompletionController.ts b/src/background/controllers/CompletionController.ts new file mode 100644 index 0000000..313f38b --- /dev/null +++ b/src/background/controllers/CompletionController.ts @@ -0,0 +1,31 @@ +import { + ConsoleGetCompletionTypesResponse, + ConsoleRequestBookmarksResponse, ConsoleRequestHistoryResponse, ConsoleRequestSearchEnginesResponse +} from "../../shared/messages"; +import { injectable } from "tsyringe"; +import CompletionUseCase from "../completion/CompletionUseCase"; + +@injectable() +export default class CompletionController { + constructor( + private completionUseCase: CompletionUseCase, + ) { + } + + async getCompletionTypes(): Promise { + return this.completionUseCase.getCompletionTypes(); + } + + async requestSearchEngines(query: string): Promise { + const items = await this.completionUseCase.requestSearchEngines(query); + return items.map(name => ({ title: name })); + } + + async requestBookmarks(query: string): Promise { + return this.completionUseCase.requestBookmarks(query); + } + + async requestHistory(query: string): Promise { + return this.completionUseCase.requestHistory(query); + } +} \ No newline at end of file diff --git a/src/background/infrastructures/ContentMessageListener.ts b/src/background/infrastructures/ContentMessageListener.ts index d063810..62cd49f 100644 --- a/src/background/infrastructures/ContentMessageListener.ts +++ b/src/background/infrastructures/ContentMessageListener.ts @@ -9,6 +9,7 @@ import AddonEnabledController from '../controllers/AddonEnabledController'; import LinkController from '../controllers/LinkController'; import OperationController from '../controllers/OperationController'; import MarkController from '../controllers/MarkController'; +import CompletionController from "../controllers/CompletionController"; @injectable() export default class ContentMessageListener { @@ -17,6 +18,7 @@ export default class ContentMessageListener { constructor( private settingController: SettingController, private commandController: CommandController, + private completionController: CompletionController, private findController: FindController, private addonEnabledController: AddonEnabledController, private linkController: LinkController, @@ -63,6 +65,14 @@ export default class ContentMessageListener { switch (message.type) { case messages.CONSOLE_QUERY_COMPLETIONS: return this.onConsoleQueryCompletions(message.text); + case messages.CONSOLE_GET_COMPLETION_TYPES: + return this.completionController.getCompletionTypes(); + case messages.CONSOLE_REQUEST_SEARCH_ENGINES_MESSAGE: + return this.completionController.requestSearchEngines(message.query); + case messages.CONSOLE_REQUEST_BOOKMARKS: + return this.completionController.requestBookmarks(message.query); + case messages.CONSOLE_REQUEST_HISTORY: + return this.completionController.requestHistory(message.query); case messages.CONSOLE_ENTER_COMMAND: return this.onConsoleEnterCommand(message.text); case messages.SETTINGS_QUERY: diff --git a/src/console/actions/console.ts b/src/console/actions/console.ts index cef04fe..99e58f7 100644 --- a/src/console/actions/console.ts +++ b/src/console/actions/console.ts @@ -1,6 +1,11 @@ import * as messages from '../../shared/messages'; import * as actions from './index'; import { Command } from "../../shared/Command"; +import CompletionClient from "../clients/CompletionClient"; +import CompletionType from "../../shared/CompletionType"; +import Completions from "../Completions"; + +const completionClient = new CompletionClient(); const commandDocs = { [Command.Set]: 'Set a value of the property', @@ -22,10 +27,12 @@ const hide = (): actions.ConsoleAction => { }; }; -const showCommand = (text: string): actions.ConsoleAction => { +const showCommand = async (text: string): Promise => { + const completionTypes = await completionClient.getCompletionTypes(); return { type: actions.CONSOLE_SHOW_COMMAND, - text: text + completionTypes, + text, }; }; @@ -102,6 +109,51 @@ const getCommandCompletions = (text: string): actions.ConsoleAction => { } }; +const getOpenCompletions = async(types: CompletionType[], original: string, command: Command, query: string): Promise => { + const completions: Completions = []; + for (const type of types) { + switch (type) { + case CompletionType.SearchEngines: + completions.push({ + name: 'Search Engines', + items: (await completionClient.requestSearchEngines(query)) + .map(key => ({ + caption: key.title, + content: command + ' ' + key.title, + })) + }); + break; + case CompletionType.History: + completions.push({ + name: 'History', + items: (await completionClient.requestHistory(query)) + .map(item => ({ + caption: item.title, + content: command + ' ' + item.url, + url: item.url + })), + }); + break; + case CompletionType.Bookmarks: + completions.push({ + name: 'Bookmarks', + items: (await completionClient.requestBookmarks(query)) + .map(item => ({ + caption: item.title, + content: command + ' ' + item.url, + url: item.url + })) + }); + break; + } + } + + return { + type: actions.CONSOLE_SET_COMPLETIONS, + completions, + completionSource: original, + }; +}; const getCompletions = async(text: string): Promise => { const completions = await browser.runtime.sendMessage({ @@ -128,6 +180,7 @@ const completionPrev = (): actions.ConsoleAction => { }; export { - hide, showCommand, showFind, showError, showInfo, hideCommand, setConsoleText, - enterCommand, enterFind, getCompletions, getCommandCompletions, completionNext, completionPrev, + hide, showCommand, showFind, showError, showInfo, hideCommand, setConsoleText, enterCommand, enterFind, + getCompletions, getCommandCompletions, getOpenCompletions, + completionNext, completionPrev, }; diff --git a/src/console/actions/index.ts b/src/console/actions/index.ts index d36f8cd..8448e04 100644 --- a/src/console/actions/index.ts +++ b/src/console/actions/index.ts @@ -1,4 +1,5 @@ import Completions from "../Completions"; +import CompletionType from "../../shared/CompletionType"; export const CONSOLE_HIDE = 'console.hide'; export const CONSOLE_SHOW_COMMAND = 'console.show.command'; @@ -18,6 +19,7 @@ interface HideAction { interface ShowCommand { type: typeof CONSOLE_SHOW_COMMAND; text: string; + completionTypes: CompletionType[]; } interface ShowFindAction { diff --git a/src/console/clients/CompletionClient.ts b/src/console/clients/CompletionClient.ts new file mode 100644 index 0000000..d5f9b01 --- /dev/null +++ b/src/console/clients/CompletionClient.ts @@ -0,0 +1,54 @@ +import * as messages from "../../shared/messages"; +import { + ConsoleGetCompletionTypesResponse, + ConsoleRequestBookmarksResponse, + ConsoleRequestHistoryResponse, ConsoleRequestSearchEnginesResponse +} from "../../shared/messages"; +import CompletionType from "../../shared/CompletionType"; + +export type SearchEngines = { + title: string +} + +export type BookmarkItem = { + title: string + url: string +} + +export type HistoryItem = { + title: string + url: string +} + +export default class CompletionClient { + async getCompletionTypes(): Promise { + const resp = await browser.runtime.sendMessage({ + type: messages.CONSOLE_GET_COMPLETION_TYPES, + }) as ConsoleGetCompletionTypesResponse; + return resp; + } + + async requestSearchEngines(query: string): Promise { + const resp = await browser.runtime.sendMessage({ + type: messages.CONSOLE_REQUEST_SEARCH_ENGINES_MESSAGE, + query, + }) as ConsoleRequestSearchEnginesResponse; + return resp; + } + + async requestBookmarks(query: string): Promise { + const resp = await browser.runtime.sendMessage({ + type: messages.CONSOLE_REQUEST_BOOKMARKS, + query, + }) as ConsoleRequestBookmarksResponse; + return resp; + } + + async requestHistory(query: string): Promise { + const resp = await browser.runtime.sendMessage({ + type: messages.CONSOLE_REQUEST_HISTORY, + query, + }) as ConsoleRequestHistoryResponse; + return resp; + } +} diff --git a/src/console/components/Console.tsx b/src/console/components/Console.tsx index 0a102a0..7be073e 100644 --- a/src/console/components/Console.tsx +++ b/src/console/components/Console.tsx @@ -7,6 +7,7 @@ import Message from './console/Message'; import * as consoleActions from '../../console/actions/console'; import { State as AppState } from '../reducers'; import CommandLineParser, { InputPhase } from "../commandline/CommandLineParser"; +import { Command } from "../../shared/Command"; const COMPLETION_MAX_ITEMS = 33; @@ -163,7 +164,16 @@ class Console extends React.Component { if (phase === InputPhase.OnCommand) { return this.props.dispatch(consoleActions.getCommandCompletions(text)); } else { - this.props.dispatch(consoleActions.getCompletions(text)); + const cmd = this.commandLineParser.parse(text); + switch (cmd.command) { + case Command.Open: + case Command.TabOpen: + case Command.WindowOpen: + this.props.dispatch(consoleActions.getOpenCompletions(this.props.completionTypes, text, cmd.command, cmd.args)); + break; + default: + this.props.dispatch(consoleActions.getCompletions(text)); + } } } } diff --git a/src/console/index.tsx b/src/console/index.tsx index 1209ec2..7bee746 100644 --- a/src/console/index.tsx +++ b/src/console/index.tsx @@ -22,11 +22,11 @@ window.addEventListener('load', () => { wrapper); }); -const onMessage = (message: any): any => { +const onMessage = async (message: any): Promise => { const msg = messages.valueOf(message); switch (msg.type) { case messages.CONSOLE_SHOW_COMMAND: - return store.dispatch(consoleActions.showCommand(msg.command)); + return store.dispatch(await consoleActions.showCommand(msg.command)); case messages.CONSOLE_SHOW_FIND: return store.dispatch(consoleActions.showFind()); case messages.CONSOLE_SHOW_ERROR: diff --git a/src/console/reducers/index.ts b/src/console/reducers/index.ts index 677a982..f1508bb 100644 --- a/src/console/reducers/index.ts +++ b/src/console/reducers/index.ts @@ -1,10 +1,12 @@ import * as actions from '../actions'; import Completions from "../Completions"; +import CompletionType from "../../shared/CompletionType"; export interface State { mode: string; messageText: string; consoleText: string; + completionTypes: CompletionType[]; completionSource: string; completions: Completions; select: number; @@ -15,6 +17,7 @@ const defaultState = { mode: '', messageText: '', consoleText: '', + completionTypes: [], completionSource: '', completions: [], select: -1, @@ -69,6 +72,7 @@ export default function reducer( return { ...state, mode: 'command', consoleText: action.text, + completionTypes: action.completionTypes, completions: []}; case actions.CONSOLE_SHOW_FIND: return { ...state, diff --git a/src/shared/CompletionType.ts b/src/shared/CompletionType.ts new file mode 100644 index 0000000..e104455 --- /dev/null +++ b/src/shared/CompletionType.ts @@ -0,0 +1,7 @@ +enum CompletionType { + SearchEngines, + History, + Bookmarks, +} + +export default CompletionType; \ No newline at end of file diff --git a/src/shared/messages.ts b/src/shared/messages.ts index 7f8bd5b..be6a2e1 100644 --- a/src/shared/messages.ts +++ b/src/shared/messages.ts @@ -1,16 +1,21 @@ import * as operations from './operations'; +import CompletionType from "./CompletionType"; export const BACKGROUND_OPERATION = 'background.operation'; export const CONSOLE_UNFOCUS = 'console.unfocus'; export const CONSOLE_ENTER_COMMAND = 'console.enter.command'; export const CONSOLE_ENTER_FIND = 'console.enter.find'; -export const CONSOLE_QUERY_COMPLETIONS = 'console.query.completions'; +export const CONSOLE_QUERY_COMPLETIONS = 'console.query.completions'; // DEPRECATED export const CONSOLE_SHOW_COMMAND = 'console.show.command'; export const CONSOLE_SHOW_ERROR = 'console.show.error'; export const CONSOLE_SHOW_INFO = 'console.show.info'; export const CONSOLE_SHOW_FIND = 'console.show.find'; export const CONSOLE_HIDE = 'console.hide'; +export const CONSOLE_GET_COMPLETION_TYPES = 'console.get.completion.types' +export const CONSOLE_REQUEST_SEARCH_ENGINES_MESSAGE = 'console.qresut.searchEngines'; +export const CONSOLE_REQUEST_BOOKMARKS = 'console.request.bookmarks'; +export const CONSOLE_REQUEST_HISTORY = 'console.request.history'; export const FOLLOW_START = 'follow.start'; export const FOLLOW_REQUEST_COUNT_TARGETS = 'follow.request.count.targets'; @@ -95,6 +100,41 @@ export interface ConsoleHideMessage { type: typeof CONSOLE_HIDE; } +export interface ConsoleGetCompletionTypesMessage { + type: typeof CONSOLE_GET_COMPLETION_TYPES; +} + +export interface ConsoleRequestSearchEnginesMessage { + type: typeof CONSOLE_REQUEST_SEARCH_ENGINES_MESSAGE; + query: string +} + +export interface ConsoleRequestBookmarksMessage { + type: typeof CONSOLE_REQUEST_BOOKMARKS; + query: string; +} + +export interface ConsoleRequestHistoryMessage { + type: typeof CONSOLE_REQUEST_HISTORY; + query: string; +} + +export type ConsoleGetCompletionTypesResponse = CompletionType[]; + +export type ConsoleRequestSearchEnginesResponse = { + title: string; +}[] + +export type ConsoleRequestBookmarksResponse = { + title: string; + url: string; +}[] + +export type ConsoleRequestHistoryResponse = { + title: string; + url: string; +}[] + export interface FollowStartMessage { type: typeof FOLLOW_START; newTab: boolean; @@ -237,6 +277,10 @@ export type Message = ConsoleShowInfoMessage | ConsoleShowFindMessage | ConsoleHideMessage | + ConsoleRequestBookmarksMessage | + ConsoleRequestHistoryMessage | + ConsoleGetCompletionTypesMessage | + ConsoleRequestSearchEnginesMessage | FollowStartMessage | FollowRequestCountTargetsMessage | FollowResponseCountTargetsMessage | -- cgit v1.2.3 From a8d78f1286fb3fe456a786b2c0e534d212835560 Mon Sep 17 00:00:00 2001 From: Shin'ya Ueoka Date: Fri, 27 Mar 2020 07:23:17 +0900 Subject: Separate repository's interface and its implementation --- src/background/completion/BookmarkRepository.ts | 28 +---- src/background/completion/CompletionUseCase.ts | 15 ++- src/background/completion/HistoryRepository.ts | 28 +---- src/background/completion/filters.ts | 76 ------------ .../completion/impl/BookmarkRepositoryImpl.ts | 28 +++++ .../completion/impl/HistoryRepositoryImpl.ts | 28 +++++ src/background/completion/impl/filters.ts | 76 ++++++++++++ src/background/di.ts | 4 + src/console/actions/console.ts | 30 ++--- src/console/actions/index.ts | 20 ++-- .../completion/CompletionUseCase.test.ts | 131 +++++++++++++++++++++ test/background/completion/impl/filters.test.ts | 114 ++++++++++++++++++ test/background/usecases/filters.test.ts | 113 ------------------ test/console/actions/console.test.ts | 9 +- 14 files changed, 422 insertions(+), 278 deletions(-) delete mode 100644 src/background/completion/filters.ts create mode 100644 src/background/completion/impl/BookmarkRepositoryImpl.ts create mode 100644 src/background/completion/impl/HistoryRepositoryImpl.ts create mode 100644 src/background/completion/impl/filters.ts create mode 100644 test/background/completion/CompletionUseCase.test.ts create mode 100644 test/background/completion/impl/filters.test.ts delete mode 100644 test/background/usecases/filters.test.ts (limited to 'src/background') diff --git a/src/background/completion/BookmarkRepository.ts b/src/background/completion/BookmarkRepository.ts index 12f5455..14105c8 100644 --- a/src/background/completion/BookmarkRepository.ts +++ b/src/background/completion/BookmarkRepository.ts @@ -1,32 +1,8 @@ -import { injectable } from "tsyringe"; - export type BookmarkItem = { title: string url: string } -const COMPLETION_ITEM_LIMIT = 10; - -@injectable() -export default class BookmarkRepository { - async queryBookmarks(query: string): Promise { - const items = await browser.bookmarks.search({ query }); - return items - .filter(item => item.title && item.title.length > 0) - .filter(item => item.type === 'bookmark' && item.url) - .filter((item) => { - let url = undefined; - try { - url = new URL(item.url!!); - } catch (e) { - return false; - } - return url.protocol !== 'place:'; - }) - .slice(0, COMPLETION_ITEM_LIMIT) - .map(item => ({ - title: item.title!!, - url: item.url!!, - })); - } +export default interface BookmarkRepository { + queryBookmarks(query: string): Promise; } diff --git a/src/background/completion/CompletionUseCase.ts b/src/background/completion/CompletionUseCase.ts index fd3c279..f7531e7 100644 --- a/src/background/completion/CompletionUseCase.ts +++ b/src/background/completion/CompletionUseCase.ts @@ -1,8 +1,8 @@ import { inject, injectable } from "tsyringe"; -import HistoryRepository from "./HistoryRepository"; -import BookmarkRepository from "./BookmarkRepository"; import CachedSettingRepository from "../repositories/CachedSettingRepository"; import CompletionType from "../../shared/CompletionType"; +import BookmarkRepository from "./BookmarkRepository"; +import HistoryRepository from "./HistoryRepository"; export type BookmarkItem = { title: string @@ -17,10 +17,9 @@ export type HistoryItem = { @injectable() export default class CompletionUseCase { constructor( - private bookmarkRepository: BookmarkRepository, - private historyRepository: HistoryRepository, - @inject("CachedSettingRepository") - private cachedSettingRepository: CachedSettingRepository, + @inject('BookmarkRepository') private bookmarkRepository: BookmarkRepository, + @inject('HistoryRepository') private historyRepository: HistoryRepository, + @inject("CachedSettingRepository") private cachedSettingRepository: CachedSettingRepository, ) { } @@ -50,11 +49,11 @@ export default class CompletionUseCase { .filter(key => key.startsWith(query)) } - requestBookmarks(query: any): Promise { + requestBookmarks(query: string): Promise { return this.bookmarkRepository.queryBookmarks(query); } - async requestHistory(query: string): Promise { + requestHistory(query: string): Promise { return this.historyRepository.queryHistories(query); } } \ No newline at end of file diff --git a/src/background/completion/HistoryRepository.ts b/src/background/completion/HistoryRepository.ts index 1cfaf1b..5eb3a2b 100644 --- a/src/background/completion/HistoryRepository.ts +++ b/src/background/completion/HistoryRepository.ts @@ -1,32 +1,8 @@ -import * as filters from "./filters"; -import { injectable } from "tsyringe"; - export type HistoryItem = { title: string url: string } -const COMPLETION_ITEM_LIMIT = 10; - -@injectable() -export default class HistoryRepository { - async queryHistories(keywords: string): Promise { - const items = await browser.history.search({ - text: keywords, - startTime: 0, - }); - - return [items] - .map(filters.filterBlankTitle) - .map(filters.filterHttp) - .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!!, - })) - } +export default interface HistoryRepository { + queryHistories(keywords: string): Promise; } diff --git a/src/background/completion/filters.ts b/src/background/completion/filters.ts deleted file mode 100644 index 98957a7..0000000 --- a/src/background/completion/filters.ts +++ /dev/null @@ -1,76 +0,0 @@ -type Item = browser.history.HistoryItem; - -const filterHttp = (items: Item[]): Item[] => { - const httpsHosts = items.map(x => new URL(x.url as string)) - .filter(x => x.protocol === 'https:') - .map(x => x.host); - const hostsSet = new Set(httpsHosts); - - return items.filter((item: Item) => { - const url = new URL(item.url as string); - return url.protocol === 'https:' || !hostsSet.has(url.host); - }); -}; - -const filterBlankTitle = (items: Item[]): Item[] => { - return items.filter(item => item.title && item.title !== ''); -}; - -const filterByTailingSlash = (items: Item[]): Item[] => { - const urls = items.map(item => new URL(item.url as string)); - const simplePaths = urls - .filter(url => url.hash === '' && url.search === '') - .map(url => url.origin + url.pathname); - const pathsSet = new Set(simplePaths); - - return items.filter((item) => { - const url = new URL(item.url as string); - if (url.hash !== '' || url.search !== '' || - url.pathname.slice(-1) !== '/') { - return true; - } - return !pathsSet.has(url.origin + url.pathname.slice(0, -1)); - }); -}; - -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); - const pathname = url.origin + url.pathname; - if (!hash[pathname]) { - hash[pathname] = item; - } else if ((hash[pathname].url as string).length > - (item.url as string).length) { - hash[pathname] = item; - } - } - const filtered = Object.values(hash); - if (filtered.length < min) { - return items; - } - return filtered; -}; - -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; - if (!hash[origin]) { - hash[origin] = item; - } else if ((hash[origin].url as string).length > - (item.url as string).length) { - hash[origin] = item; - } - } - const filtered = Object.values(hash); - if (filtered.length < min) { - return items; - } - return filtered; -}; - -export { - filterHttp, filterBlankTitle, filterByTailingSlash, - filterByPathname, filterByOrigin -}; diff --git a/src/background/completion/impl/BookmarkRepositoryImpl.ts b/src/background/completion/impl/BookmarkRepositoryImpl.ts new file mode 100644 index 0000000..58df129 --- /dev/null +++ b/src/background/completion/impl/BookmarkRepositoryImpl.ts @@ -0,0 +1,28 @@ +import { injectable } from "tsyringe"; +import BookmarkRepository, {BookmarkItem} from "../BookmarkRepository"; + +const COMPLETION_ITEM_LIMIT = 10; + +@injectable() +export default class BookmarkRepositoryImpl implements BookmarkRepository { + async queryBookmarks(query: string): Promise { + const items = await browser.bookmarks.search({query}); + return items + .filter(item => item.title && item.title.length > 0) + .filter(item => item.type === 'bookmark' && item.url) + .filter((item) => { + let url = undefined; + try { + url = new URL(item.url!!); + } catch (e) { + return false; + } + return url.protocol !== 'place:'; + }) + .slice(0, COMPLETION_ITEM_LIMIT) + .map(item => ({ + title: item.title!!, + url: item.url!!, + })); + } +} diff --git a/src/background/completion/impl/HistoryRepositoryImpl.ts b/src/background/completion/impl/HistoryRepositoryImpl.ts new file mode 100644 index 0000000..42691aa --- /dev/null +++ b/src/background/completion/impl/HistoryRepositoryImpl.ts @@ -0,0 +1,28 @@ +import { injectable } from "tsyringe"; +import * as filters from "./filters"; +import HistoryRepository, {HistoryItem} from "../HistoryRepository"; + +const COMPLETION_ITEM_LIMIT = 10; + +@injectable() +export default class HistoryRepositoryImpl implements HistoryRepository { + async queryHistories(keywords: string): Promise { + const items = await browser.history.search({ + text: keywords, + startTime: 0, + }); + + return [items] + .map(filters.filterBlankTitle) + .map(filters.filterHttp) + .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/filters.ts b/src/background/completion/impl/filters.ts new file mode 100644 index 0000000..98957a7 --- /dev/null +++ b/src/background/completion/impl/filters.ts @@ -0,0 +1,76 @@ +type Item = browser.history.HistoryItem; + +const filterHttp = (items: Item[]): Item[] => { + const httpsHosts = items.map(x => new URL(x.url as string)) + .filter(x => x.protocol === 'https:') + .map(x => x.host); + const hostsSet = new Set(httpsHosts); + + return items.filter((item: Item) => { + const url = new URL(item.url as string); + return url.protocol === 'https:' || !hostsSet.has(url.host); + }); +}; + +const filterBlankTitle = (items: Item[]): Item[] => { + return items.filter(item => item.title && item.title !== ''); +}; + +const filterByTailingSlash = (items: Item[]): Item[] => { + const urls = items.map(item => new URL(item.url as string)); + const simplePaths = urls + .filter(url => url.hash === '' && url.search === '') + .map(url => url.origin + url.pathname); + const pathsSet = new Set(simplePaths); + + return items.filter((item) => { + const url = new URL(item.url as string); + if (url.hash !== '' || url.search !== '' || + url.pathname.slice(-1) !== '/') { + return true; + } + return !pathsSet.has(url.origin + url.pathname.slice(0, -1)); + }); +}; + +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); + const pathname = url.origin + url.pathname; + if (!hash[pathname]) { + hash[pathname] = item; + } else if ((hash[pathname].url as string).length > + (item.url as string).length) { + hash[pathname] = item; + } + } + const filtered = Object.values(hash); + if (filtered.length < min) { + return items; + } + return filtered; +}; + +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; + if (!hash[origin]) { + hash[origin] = item; + } else if ((hash[origin].url as string).length > + (item.url as string).length) { + hash[origin] = item; + } + } + const filtered = Object.values(hash); + if (filtered.length < min) { + return items; + } + return filtered; +}; + +export { + filterHttp, filterBlankTitle, filterByTailingSlash, + filterByPathname, filterByOrigin +}; diff --git a/src/background/di.ts b/src/background/di.ts index 9fc230c..0b52e0b 100644 --- a/src/background/di.ts +++ b/src/background/di.ts @@ -4,8 +4,12 @@ import { LocalSettingRepository, SyncSettingRepository } from "./repositories/Se import { NotifierImpl } from "./presenters/Notifier"; import { CachedSettingRepositoryImpl } from "./repositories/CachedSettingRepository"; import { container } from 'tsyringe'; +import HistoryRepositoryImpl from "./completion/impl/HistoryRepositoryImpl"; +import BookmarkRepositoryImpl from "./completion/impl/BookmarkRepositoryImpl"; container.register('LocalSettingRepository', { useValue: LocalSettingRepository }); container.register('SyncSettingRepository', { useClass: SyncSettingRepository }); container.register('CachedSettingRepository', { useClass: CachedSettingRepositoryImpl }); container.register('Notifier', { useClass: NotifierImpl }); +container.register('HistoryRepository', { useClass: HistoryRepositoryImpl }); +container.register('BookmarkRepository', { useClass: BookmarkRepositoryImpl }); diff --git a/src/console/actions/console.ts b/src/console/actions/console.ts index 99e58f7..f1db941 100644 --- a/src/console/actions/console.ts +++ b/src/console/actions/console.ts @@ -27,7 +27,7 @@ const hide = (): actions.ConsoleAction => { }; }; -const showCommand = async (text: string): Promise => { +const showCommand = async (text: string): Promise => { const completionTypes = await completionClient.getCompletionTypes(); return { type: actions.CONSOLE_SHOW_COMMAND, @@ -36,27 +36,27 @@ const showCommand = async (text: string): Promise => { }; }; -const showFind = (): actions.ConsoleAction => { +const showFind = (): actions.ShowFindAction => { return { type: actions.CONSOLE_SHOW_FIND, }; }; -const showError = (text: string): actions.ConsoleAction => { +const showError = (text: string): actions.ShowErrorAction => { return { type: actions.CONSOLE_SHOW_ERROR, text: text }; }; -const showInfo = (text: string): actions.ConsoleAction => { +const showInfo = (text: string): actions.ShowInfoAction => { return { type: actions.CONSOLE_SHOW_INFO, text: text }; }; -const hideCommand = (): actions.ConsoleAction => { +const hideCommand = (): actions.HideCommandAction => { window.top.postMessage(JSON.stringify({ type: messages.CONSOLE_UNFOCUS, }), '*'); @@ -65,9 +65,7 @@ const hideCommand = (): actions.ConsoleAction => { }; }; -const enterCommand = async( - text: string, -): Promise => { +const enterCommand = async(text: string): Promise => { await browser.runtime.sendMessage({ type: messages.CONSOLE_ENTER_COMMAND, text, @@ -75,7 +73,7 @@ const enterCommand = async( return hideCommand(); }; -const enterFind = (text?: string): actions.ConsoleAction => { +const enterFind = (text?: string): actions.HideCommandAction => { window.top.postMessage(JSON.stringify({ type: messages.CONSOLE_ENTER_FIND, text, @@ -83,14 +81,14 @@ const enterFind = (text?: string): actions.ConsoleAction => { return hideCommand(); }; -const setConsoleText = (consoleText: string): actions.ConsoleAction => { +const setConsoleText = (consoleText: string): actions.SetConsoleTextAction => { return { type: actions.CONSOLE_SET_CONSOLE_TEXT, consoleText, }; }; -const getCommandCompletions = (text: string): actions.ConsoleAction => { +const getCommandCompletions = (text: string): actions.SetCompletionsAction => { const items = Object.entries(commandDocs) .filter(([name]) => name.startsWith(text.trimLeft())) .map(([name, doc]) => ({ @@ -109,7 +107,9 @@ const getCommandCompletions = (text: string): actions.ConsoleAction => { } }; -const getOpenCompletions = async(types: CompletionType[], original: string, command: Command, query: string): Promise => { +const getOpenCompletions = async( + types: CompletionType[], original: string, command: Command, query: string, +): Promise => { const completions: Completions = []; for (const type of types) { switch (type) { @@ -155,7 +155,7 @@ const getOpenCompletions = async(types: CompletionType[], original: string, comm }; }; -const getCompletions = async(text: string): Promise => { +const getCompletions = async(text: string): Promise => { const completions = await browser.runtime.sendMessage({ type: messages.CONSOLE_QUERY_COMPLETIONS, text, @@ -167,13 +167,13 @@ const getCompletions = async(text: string): Promise => { }; }; -const completionNext = (): actions.ConsoleAction => { +const completionNext = (): actions.CompletionNextAction => { return { type: actions.CONSOLE_COMPLETION_NEXT, }; }; -const completionPrev = (): actions.ConsoleAction => { +const completionPrev = (): actions.CompletionPrevAction => { return { type: actions.CONSOLE_COMPLETION_PREV, }; diff --git a/src/console/actions/index.ts b/src/console/actions/index.ts index 8448e04..e292608 100644 --- a/src/console/actions/index.ts +++ b/src/console/actions/index.ts @@ -12,50 +12,50 @@ export const CONSOLE_COMPLETION_NEXT = 'console.completion.next'; export const CONSOLE_COMPLETION_PREV = 'console.completion.prev'; export const CONSOLE_SHOW_FIND = 'console.show.find'; -interface HideAction { +export interface HideAction { type: typeof CONSOLE_HIDE; } -interface ShowCommand { +export interface ShowCommand { type: typeof CONSOLE_SHOW_COMMAND; text: string; completionTypes: CompletionType[]; } -interface ShowFindAction { +export interface ShowFindAction { type: typeof CONSOLE_SHOW_FIND; } -interface ShowErrorAction { +export interface ShowErrorAction { type: typeof CONSOLE_SHOW_ERROR; text: string; } -interface ShowInfoAction { +export interface ShowInfoAction { type: typeof CONSOLE_SHOW_INFO; text: string; } -interface HideCommandAction { +export interface HideCommandAction { type: typeof CONSOLE_HIDE_COMMAND; } -interface SetConsoleTextAction { +export interface SetConsoleTextAction { type: typeof CONSOLE_SET_CONSOLE_TEXT; consoleText: string; } -interface SetCompletionsAction { +export interface SetCompletionsAction { type: typeof CONSOLE_SET_COMPLETIONS; completions: Completions; completionSource: string; } -interface CompletionNextAction { +export interface CompletionNextAction { type: typeof CONSOLE_COMPLETION_NEXT; } -interface CompletionPrevAction { +export interface CompletionPrevAction { type: typeof CONSOLE_COMPLETION_PREV; } diff --git a/test/background/completion/CompletionUseCase.test.ts b/test/background/completion/CompletionUseCase.test.ts new file mode 100644 index 0000000..0d58e45 --- /dev/null +++ b/test/background/completion/CompletionUseCase.test.ts @@ -0,0 +1,131 @@ +import "reflect-metadata"; +import CompletionType from "../../../src/shared/CompletionType"; +import BookmarkRepository, {BookmarkItem} from "../../../src/background/completion/BookmarkRepository"; +import HistoryRepository, {HistoryItem} from "../../../src/background/completion/HistoryRepository"; +import CompletionUseCase from "../../../src/background/completion/CompletionUseCase"; +import CachedSettingRepository from "../../../src/background/repositories/CachedSettingRepository"; +import Settings, {DefaultSetting} from "../../../src/shared/settings/Settings"; +import { expect } from 'chai'; +import sinon from 'sinon'; +import Properties from "../../../src/shared/settings/Properties"; +import Search from "../../../src/shared/settings/Search"; + +class MockBookmarkRepository implements BookmarkRepository { + queryBookmarks(_query: string): Promise { + throw new Error("not implemented") + } +} + +class MockHistoryRepository implements HistoryRepository { + queryHistories(_keywords: string): Promise { + throw new Error("not implemented") + } +} + +class MockSettingRepository implements CachedSettingRepository { + get(): Promise { + throw new Error("not implemented") + } + + setProperty(_name: string, _value: string | number | boolean): Promise { + throw new Error("not implemented") + } + + update(_value: Settings): Promise { + throw new Error("not implemented") + } +} + +describe('CompletionUseCase', () => { + let bookmarkRepository: MockBookmarkRepository; + let historyRepository: MockHistoryRepository; + let settingRepository: MockSettingRepository; + let sut: CompletionUseCase; + + beforeEach(() => { + bookmarkRepository = new MockBookmarkRepository(); + historyRepository = new MockHistoryRepository(); + settingRepository = new MockSettingRepository(); + sut = new CompletionUseCase(bookmarkRepository, historyRepository, settingRepository) + }); + + describe('#getCompletionTypes', () => { + it("returns completion types from the property", async () => { + sinon.stub(settingRepository, 'get').returns(Promise.resolve(new Settings({ + keymaps: DefaultSetting.keymaps, + search: DefaultSetting.search, + properties: new Properties({ complete: "shb" }), + blacklist: DefaultSetting.blacklist, + }))); + + const items = await sut.getCompletionTypes(); + expect(items).to.deep.equal([ + CompletionType.SearchEngines, + CompletionType.History, + CompletionType.Bookmarks + ]); + }); + }); + + describe('#requestSearchEngines', () => { + it("returns search engines matches by the query", async () => { + sinon.stub(settingRepository, 'get').returns(Promise.resolve(new Settings({ + keymaps: DefaultSetting.keymaps, + search: new Search("google", { + "google": "https://google.com/search?q={}", + "yahoo": "https://search.yahoo.com/search?q={}", + "bing": "https://bing.com/search?q={}", + "google_ja": "https://google.co.jp/search?q={}", + }), + properties: DefaultSetting.properties, + blacklist: DefaultSetting.blacklist, + }))); + + expect(await sut.requestSearchEngines("")).to.deep.equal([ + "google", + "yahoo", + "bing", + "google_ja", + ]); + expect(await sut.requestSearchEngines("go")).to.deep.equal([ + "google", + "google_ja", + ]); + expect(await sut.requestSearchEngines("x")).to.be.empty; + }) + }); + + describe('#requestBookmarks', () => { + it("returns bookmarks from the repository", async() => { + sinon.stub(bookmarkRepository, 'queryBookmarks') + .withArgs("site").returns(Promise.resolve([ + { title: "site1", url: "https://site1.example.com" }, + { title: "site2", url: "https://site2.example.com/" }, + ])) + .withArgs("xyz").returns(Promise.resolve([])); + + expect(await sut.requestBookmarks("site")).to.deep.equal([ + { title: "site1", url: "https://site1.example.com" }, + { title: "site2", url: "https://site2.example.com/" }, + ]); + expect(await sut.requestBookmarks("xyz")).to.be.empty; + }); + }); + + describe('#requestHistory', () => { + it("returns histories from the repository", async() => { + sinon.stub(historyRepository, 'queryHistories') + .withArgs("site").returns(Promise.resolve([ + { title: "site1", url: "https://site1.example.com" }, + { title: "site2", url: "https://site2.example.com/" }, + ])) + .withArgs("xyz").returns(Promise.resolve([])); + + expect(await sut.requestHistory("site")).to.deep.equal([ + { title: "site1", url: "https://site1.example.com" }, + { title: "site2", url: "https://site2.example.com/" }, + ]); + expect(await sut.requestHistory("xyz")).to.be.empty; + }); + }); +}); \ No newline at end of file diff --git a/test/background/completion/impl/filters.test.ts b/test/background/completion/impl/filters.test.ts new file mode 100644 index 0000000..2b15a9b --- /dev/null +++ b/test/background/completion/impl/filters.test.ts @@ -0,0 +1,114 @@ +import * as filters from '../../../../src/background/completion/impl/filters' +import { expect } from 'chai'; + +describe('background/usecases/filters', () => { + describe('filterHttp', () => { + it('filters http URLs duplicates to https hosts', () => { + const pages = [ + { id: '0', url: 'http://i-beam.org/foo' }, + { id: '1', url: 'https://i-beam.org/bar' }, + { id: '2', url: 'http://i-beam.net/hoge' }, + { id: '3', url: 'http://i-beam.net/fuga' }, + ]; + const filtered = filters.filterHttp(pages); + + const urls = filtered.map(x => x.url); + expect(urls).to.deep.equal([ + 'https://i-beam.org/bar', 'http://i-beam.net/hoge', 'http://i-beam.net/fuga' + ]); + }) + }); + + describe('filterBlankTitle', () => { + it('filters blank titles', () => { + const pages = [ + { id: '0', title: 'hello' }, + { id: '1', title: '' }, + { id: '2' }, + ]; + const filtered = filters.filterBlankTitle(pages); + + expect(filtered).to.deep.equal([{ id: '0', title: 'hello' }]); + }); + }); + + describe('filterByTailingSlash', () => { + it('filters duplicated pathname on tailing slash', () => { + const pages = [ + { id: '0', url: 'http://i-beam.org/content' }, + { id: '1', url: 'http://i-beam.org/content/' }, + { id: '2', url: 'http://i-beam.org/search' }, + { id: '3', url: 'http://i-beam.org/search?q=apple_banana_cherry' }, + ]; + const filtered = filters.filterByTailingSlash(pages); + + const urls = filtered.map(x => x.url); + expect(urls).to.deep.equal([ + 'http://i-beam.org/content', + 'http://i-beam.org/search', + 'http://i-beam.org/search?q=apple_banana_cherry', + ]); + }); + }) + + 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' }, + { 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.net/search?q=apple' }, + { 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, 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' }, + ]); + }); + }); + + 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' }, + { 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, 0); + expect(filtered).to.deep.equal([ + { id: '0', url: 'http://i-beam.org/search?q=apple' }, + ]); + }); + }); +}); diff --git a/test/background/usecases/filters.test.ts b/test/background/usecases/filters.test.ts deleted file mode 100644 index 90541ff..0000000 --- a/test/background/usecases/filters.test.ts +++ /dev/null @@ -1,113 +0,0 @@ -import * as filters from 'background/usecases/filters'; - -describe("background/usecases/filters", () => { - describe('filterHttp', () => { - it('filters http URLs duplicates to https hosts', () => { - const pages = [ - { url: 'http://i-beam.org/foo' }, - { url: 'https://i-beam.org/bar' }, - { url: 'http://i-beam.net/hoge' }, - { url: 'http://i-beam.net/fuga' }, - ]; - const filtered = filters.filterHttp(pages); - - const urls = filtered.map(x => x.url); - expect(urls).to.deep.equal([ - 'https://i-beam.org/bar', 'http://i-beam.net/hoge', 'http://i-beam.net/fuga' - ]); - }) - }); - - describe('filterBlankTitle', () => { - it('filters blank titles', () => { - const pages = [ - { title: 'hello' }, - { title: '' }, - {}, - ]; - const filtered = filters.filterBlankTitle(pages); - - expect(filtered).to.deep.equal([{ title: 'hello' }]); - }); - }) - - describe('filterByTailingSlash', () => { - it('filters duplicated pathname on tailing slash', () => { - const pages = [ - { url: 'http://i-beam.org/content' }, - { url: 'http://i-beam.org/content/' }, - { url: 'http://i-beam.org/search' }, - { url: 'http://i-beam.org/search?q=apple_banana_cherry' }, - ]; - const filtered = filters.filterByTailingSlash(pages); - - const urls = filtered.map(x => x.url); - expect(urls).to.deep.equal([ - 'http://i-beam.org/content', - 'http://i-beam.org/search', - 'http://i-beam.org/search?q=apple_banana_cherry', - ]); - }); - }) - - describe('filterByPathname', () => { - it('remains items less than minimam length', () => { - const pages = [ - { url: 'http://i-beam.org/search?q=apple' }, - { url: 'http://i-beam.org/search?q=apple_banana' }, - { url: 'http://i-beam.org/search?q=apple_banana_cherry' }, - { url: 'http://i-beam.org/request?q=apple' }, - { url: 'http://i-beam.org/request?q=apple_banana' }, - { 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 = [ - { url: 'http://i-beam.org/search?q=apple' }, - { url: 'http://i-beam.org/search?q=apple_banana' }, - { url: 'http://i-beam.org/search?q=apple_banana_cherry' }, - { url: 'http://i-beam.net/search?q=apple' }, - { url: 'http://i-beam.net/search?q=apple_banana' }, - { url: 'http://i-beam.net/search?q=apple_banana_cherry' }, - ]; - const filtered = filters.filterByPathname(pages, 0); - expect(filtered).to.deep.equal([ - { url: 'http://i-beam.org/search?q=apple' }, - { url: 'http://i-beam.net/search?q=apple' }, - ]); - }); - }) - - describe('filterByOrigin', () => { - it('remains items less than minimam length', () => { - const pages = [ - { url: 'http://i-beam.org/search?q=apple' }, - { url: 'http://i-beam.org/search?q=apple_banana' }, - { url: 'http://i-beam.org/search?q=apple_banana_cherry' }, - { url: 'http://i-beam.org/request?q=apple' }, - { url: 'http://i-beam.org/request?q=apple_banana' }, - { 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 = [ - { url: 'http://i-beam.org/search?q=apple' }, - { url: 'http://i-beam.org/search?q=apple_banana' }, - { url: 'http://i-beam.org/search?q=apple_banana_cherry' }, - { url: 'http://i-beam.org/request?q=apple' }, - { url: 'http://i-beam.org/request?q=apple_banana' }, - { url: 'http://i-beam.org/request?q=apple_banana_cherry' }, - ]; - const filtered = filters.filterByOrigin(pages, 0); - expect(filtered).to.deep.equal([ - { url: 'http://i-beam.org/search?q=apple' }, - ]); - }); - }) -}); diff --git a/test/console/actions/console.test.ts b/test/console/actions/console.test.ts index 583c878..e6567b2 100644 --- a/test/console/actions/console.test.ts +++ b/test/console/actions/console.test.ts @@ -1,5 +1,6 @@ -import * as actions from 'console/actions'; -import * as consoleActions from 'console/actions/console'; +import * as actions from '../../../src/console/actions'; +import * as consoleActions from '../../../src/console/actions/console'; +import { expect } from 'chai'; describe("console actions", () => { describe('hide', () => { @@ -9,8 +10,8 @@ describe("console actions", () => { }); }); describe("showCommand", () => { - it('create CONSOLE_SHOW_COMMAND action', () => { - const action = consoleActions.showCommand('hello'); + it('create CONSOLE_SHOW_COMMAND action', async () => { + const action = await consoleActions.showCommand('hello'); expect(action.type).to.equal(actions.CONSOLE_SHOW_COMMAND); expect(action.text).to.equal('hello'); }); -- cgit v1.2.3 From ea63c5f78b4c985e9d6dd106afe4f97bfeedbcd0 Mon Sep 17 00:00:00 2001 From: Shin'ya Ueoka Date: Sat, 28 Mar 2020 21:35:06 +0900 Subject: Complete tabs by the completion packages --- src/background/completion/CompletionUseCase.ts | 59 ---------- src/background/completion/OpenCompletionUseCase.ts | 59 ++++++++++ src/background/completion/TabCompletionUseCase.ts | 34 ++++++ src/background/completion/TabItem.ts | 11 ++ src/background/completion/TabRepository.ts | 12 ++ .../completion/impl/TabRepositoryImpl.ts | 27 +++++ src/background/controllers/CompletionController.ts | 15 ++- src/background/di.ts | 4 + .../infrastructures/ContentMessageListener.ts | 2 + src/background/presenters/TabPresenter.ts | 65 +++++++--- src/background/usecases/AddonEnabledUseCase.ts | 4 +- src/background/usecases/CommandUseCase.ts | 4 +- src/background/usecases/CompletionsUseCase.ts | 6 +- src/background/usecases/ConsoleUseCase.ts | 4 +- src/background/usecases/FindUseCase.ts | 4 +- src/background/usecases/LinkUseCase.ts | 4 +- src/background/usecases/MarkUseCase.ts | 4 +- src/background/usecases/NavigateUseCase.ts | 4 +- src/background/usecases/TabSelectUseCase.ts | 4 +- src/background/usecases/TabUseCase.ts | 4 +- src/background/usecases/VersionUseCase.ts | 2 +- src/background/usecases/ZoomUseCase.ts | 4 +- src/console/actions/console.ts | 27 ++++- src/console/clients/CompletionClient.ts | 20 +++- src/console/components/Console.tsx | 15 +++ src/shared/TabFlag.ts | 7 ++ src/shared/messages.ts | 17 +++ .../completion/CompletionUseCase.test.ts | 131 --------------------- .../completion/OpenCompletionUseCase.test.ts | 131 +++++++++++++++++++++ .../completion/TabCompletionUseCase.test.ts | 105 +++++++++++++++++ 30 files changed, 552 insertions(+), 237 deletions(-) delete mode 100644 src/background/completion/CompletionUseCase.ts create mode 100644 src/background/completion/OpenCompletionUseCase.ts create mode 100644 src/background/completion/TabCompletionUseCase.ts create mode 100644 src/background/completion/TabItem.ts create mode 100644 src/background/completion/TabRepository.ts create mode 100644 src/background/completion/impl/TabRepositoryImpl.ts create mode 100644 src/shared/TabFlag.ts delete mode 100644 test/background/completion/CompletionUseCase.test.ts create mode 100644 test/background/completion/OpenCompletionUseCase.test.ts create mode 100644 test/background/completion/TabCompletionUseCase.test.ts (limited to 'src/background') diff --git a/src/background/completion/CompletionUseCase.ts b/src/background/completion/CompletionUseCase.ts deleted file mode 100644 index f7531e7..0000000 --- a/src/background/completion/CompletionUseCase.ts +++ /dev/null @@ -1,59 +0,0 @@ -import { inject, injectable } from "tsyringe"; -import CachedSettingRepository from "../repositories/CachedSettingRepository"; -import CompletionType from "../../shared/CompletionType"; -import BookmarkRepository from "./BookmarkRepository"; -import HistoryRepository from "./HistoryRepository"; - -export type BookmarkItem = { - title: string - url: string -} - -export type HistoryItem = { - title: string - url: string -} - -@injectable() -export default class CompletionUseCase { - constructor( - @inject('BookmarkRepository') private bookmarkRepository: BookmarkRepository, - @inject('HistoryRepository') private historyRepository: HistoryRepository, - @inject("CachedSettingRepository") private cachedSettingRepository: CachedSettingRepository, - ) { - } - - async getCompletionTypes(): Promise { - const settings = await this.cachedSettingRepository.get(); - const types: CompletionType[] = []; - for (const c of settings.properties.complete) { - switch (c) { - case 's': - types.push(CompletionType.SearchEngines); - break; - case 'h': - types.push(CompletionType.History); - break; - case 'b': - types.push(CompletionType.Bookmarks); - break; - } - // ignore invalid characters in the complete property - } - return types; - } - - async requestSearchEngines(query: string): Promise { - const settings = await this.cachedSettingRepository.get(); - return Object.keys(settings.search.engines) - .filter(key => key.startsWith(query)) - } - - requestBookmarks(query: string): Promise { - return this.bookmarkRepository.queryBookmarks(query); - } - - requestHistory(query: string): Promise { - return this.historyRepository.queryHistories(query); - } -} \ No newline at end of file diff --git a/src/background/completion/OpenCompletionUseCase.ts b/src/background/completion/OpenCompletionUseCase.ts new file mode 100644 index 0000000..1b63e7c --- /dev/null +++ b/src/background/completion/OpenCompletionUseCase.ts @@ -0,0 +1,59 @@ +import { inject, injectable } from "tsyringe"; +import CachedSettingRepository from "../repositories/CachedSettingRepository"; +import CompletionType from "../../shared/CompletionType"; +import BookmarkRepository from "./BookmarkRepository"; +import HistoryRepository from "./HistoryRepository"; + +export type BookmarkItem = { + title: string + url: string +} + +export type HistoryItem = { + title: string + url: string +} + +@injectable() +export default class OpenCompletionUseCase { + constructor( + @inject('BookmarkRepository') private bookmarkRepository: BookmarkRepository, + @inject('HistoryRepository') private historyRepository: HistoryRepository, + @inject("CachedSettingRepository") private cachedSettingRepository: CachedSettingRepository, + ) { + } + + async getCompletionTypes(): Promise { + const settings = await this.cachedSettingRepository.get(); + const types: CompletionType[] = []; + for (const c of settings.properties.complete) { + switch (c) { + case 's': + types.push(CompletionType.SearchEngines); + break; + case 'h': + types.push(CompletionType.History); + break; + case 'b': + types.push(CompletionType.Bookmarks); + break; + } + // ignore invalid characters in the complete property + } + return types; + } + + async requestSearchEngines(query: string): Promise { + const settings = await this.cachedSettingRepository.get(); + return Object.keys(settings.search.engines) + .filter(key => key.startsWith(query)) + } + + requestBookmarks(query: string): Promise { + return this.bookmarkRepository.queryBookmarks(query); + } + + requestHistory(query: string): Promise { + return this.historyRepository.queryHistories(query); + } +} \ No newline at end of file diff --git a/src/background/completion/TabCompletionUseCase.ts b/src/background/completion/TabCompletionUseCase.ts new file mode 100644 index 0000000..7e6dce3 --- /dev/null +++ b/src/background/completion/TabCompletionUseCase.ts @@ -0,0 +1,34 @@ +import { inject, injectable } from "tsyringe"; +import TabItem from "./TabItem"; +import TabRepository from "./TabRepository"; +import TabPresenter from "../presenters/TabPresenter"; +import TabFlag from "../../shared/TabFlag"; + +@injectable() +export default class TabCompletionUseCase { + constructor( + @inject('TabRepository') private tabRepository: TabRepository, + @inject('TabPresenter') private tabPresenter: TabPresenter, + ) { + } + + async queryTabs(query: string, excludePinned: boolean): Promise { + const lastTabId = await this.tabPresenter.getLastSelectedId(); + const tabs = await this.tabRepository.queryTabs(query, excludePinned); + return tabs.map(tab => { + let flag = TabFlag.None; + if (tab.active) { + flag = TabFlag.CurrentTab + } else if (tab.id == lastTabId) { + flag = TabFlag.LastTab + } + return { + index: tab.index + 1, + flag: flag, + title: tab.title, + url: tab.url, + faviconUrl : tab.faviconUrl + } + }); + } +} diff --git a/src/background/completion/TabItem.ts b/src/background/completion/TabItem.ts new file mode 100644 index 0000000..630855a --- /dev/null +++ b/src/background/completion/TabItem.ts @@ -0,0 +1,11 @@ +import TabFlag from "../../shared/TabFlag"; + +type TabItem = { + index: number + flag: TabFlag + title: string + url: string + faviconUrl?: string +} + +export default TabItem; \ No newline at end of file diff --git a/src/background/completion/TabRepository.ts b/src/background/completion/TabRepository.ts new file mode 100644 index 0000000..61fac3b --- /dev/null +++ b/src/background/completion/TabRepository.ts @@ -0,0 +1,12 @@ +export type Tab = { + id: number + index: number + active: boolean + title: string + url: string + faviconUrl?: string +} + +export default interface TabRepository { + queryTabs(query: string, excludePinned: boolean): Promise; +} diff --git a/src/background/completion/impl/TabRepositoryImpl.ts b/src/background/completion/impl/TabRepositoryImpl.ts new file mode 100644 index 0000000..6692b27 --- /dev/null +++ b/src/background/completion/impl/TabRepositoryImpl.ts @@ -0,0 +1,27 @@ +import TabRepository, { Tab } from "../TabRepository"; + +const COMPLETION_ITEM_LIMIT = 10; + +export default class TabRepositoryImpl implements TabRepository { + async queryTabs(query: string, excludePinned: boolean): Promise { + const tabs = await browser.tabs.query({ currentWindow: true }); + return tabs + .filter((t) => { + return t.url && t.url.toLowerCase().includes(query.toLowerCase()) || + t.title && t.title.toLowerCase().includes(query.toLowerCase()); + }) + .filter((t) => { + return !(excludePinned && t.pinned); + }) + .filter(item => item.id && item.title && item.url) + .slice(0, COMPLETION_ITEM_LIMIT) + .map(item => ({ + id: item.id!!, + url: item.url!!, + active: item.active, + title: item.title!!, + faviconUrl: item.favIconUrl, + index: item.index, + })) + } +} \ No newline at end of file diff --git a/src/background/controllers/CompletionController.ts b/src/background/controllers/CompletionController.ts index 313f38b..a268d15 100644 --- a/src/background/controllers/CompletionController.ts +++ b/src/background/controllers/CompletionController.ts @@ -1,14 +1,19 @@ import { ConsoleGetCompletionTypesResponse, - ConsoleRequestBookmarksResponse, ConsoleRequestHistoryResponse, ConsoleRequestSearchEnginesResponse + ConsoleRequestBookmarksResponse, + ConsoleRequestHistoryResponse, + ConsoleRequestSearchEnginesResponse, + ConsoleRequesttabsResponse } from "../../shared/messages"; import { injectable } from "tsyringe"; -import CompletionUseCase from "../completion/CompletionUseCase"; +import OpenCompletionUseCase from "../completion/OpenCompletionUseCase"; +import TabCompletionUseCase from "../completion/TabCompletionUseCase"; @injectable() export default class CompletionController { constructor( - private completionUseCase: CompletionUseCase, + private completionUseCase: OpenCompletionUseCase, + private tabCompletionUseCase: TabCompletionUseCase, ) { } @@ -28,4 +33,8 @@ export default class CompletionController { async requestHistory(query: string): Promise { return this.completionUseCase.requestHistory(query); } + + async queryTabs(query: string, excludePinned: boolean): Promise { + return this.tabCompletionUseCase.queryTabs(query, excludePinned); + } } \ No newline at end of file diff --git a/src/background/di.ts b/src/background/di.ts index 0b52e0b..c186262 100644 --- a/src/background/di.ts +++ b/src/background/di.ts @@ -6,6 +6,8 @@ import { CachedSettingRepositoryImpl } from "./repositories/CachedSettingReposit import { container } from 'tsyringe'; import HistoryRepositoryImpl from "./completion/impl/HistoryRepositoryImpl"; import BookmarkRepositoryImpl from "./completion/impl/BookmarkRepositoryImpl"; +import TabRepositoryImpl from "./completion/impl/TabRepositoryImpl"; +import {TabPresenterImpl} from "./presenters/TabPresenter"; container.register('LocalSettingRepository', { useValue: LocalSettingRepository }); container.register('SyncSettingRepository', { useClass: SyncSettingRepository }); @@ -13,3 +15,5 @@ container.register('CachedSettingRepository', { useClass: CachedSettingRepositor container.register('Notifier', { useClass: NotifierImpl }); container.register('HistoryRepository', { useClass: HistoryRepositoryImpl }); container.register('BookmarkRepository', { useClass: BookmarkRepositoryImpl }); +container.register('TabRepository', { useClass: TabRepositoryImpl }); +container.register('TabPresenter', { useClass: TabPresenterImpl }); diff --git a/src/background/infrastructures/ContentMessageListener.ts b/src/background/infrastructures/ContentMessageListener.ts index 62cd49f..13aa763 100644 --- a/src/background/infrastructures/ContentMessageListener.ts +++ b/src/background/infrastructures/ContentMessageListener.ts @@ -73,6 +73,8 @@ export default class ContentMessageListener { return this.completionController.requestBookmarks(message.query); case messages.CONSOLE_REQUEST_HISTORY: return this.completionController.requestHistory(message.query); + case messages.CONSOLE_REQUEST_TABS: + return this.completionController.queryTabs(message.query, message.excludePinned); case messages.CONSOLE_ENTER_COMMAND: return this.onConsoleEnterCommand(message.text); case messages.SETTINGS_QUERY: diff --git a/src/background/presenters/TabPresenter.ts b/src/background/presenters/TabPresenter.ts index 33d8bea..bded5a2 100644 --- a/src/background/presenters/TabPresenter.ts +++ b/src/background/presenters/TabPresenter.ts @@ -1,4 +1,3 @@ -import { injectable } from 'tsyringe'; import MemoryStorage from '../infrastructures/MemoryStorage'; const CURRENT_SELECTED_KEY = 'tabs.current.selected'; @@ -6,8 +5,41 @@ const LAST_SELECTED_KEY = 'tabs.last.selected'; type Tab = browser.tabs.Tab; -@injectable() -export default class TabPresenter { +export default interface TabPresenter { + open(url: string, tabId?: number): Promise; + + create(url: string, opts?: object): Promise; + + getCurrent(): Promise; + + getAll(): Promise; + + getLastSelectedId(): Promise; + + getByKeyword(keyword: string, excludePinned: boolean): Promise; + + select(tabId: number): Promise; + + remove(ids: number[]): Promise; + + reopen(): Promise; + + reload(tabId: number, cache: boolean): Promise; + + setPinned(tabId: number, pinned: boolean): Promise; + + duplicate(id: number): Promise; + + getZoom(tabId: number): Promise; + + setZoom(tabId: number, factor: number): Promise; + + onSelected( + listener: (arg: { tabId: number, windowId: number}) => void, + ): void; +} + +export class TabPresenterImpl implements TabPresenter { open(url: string, tabId?: number): Promise { return browser.tabs.update(tabId, { url }); } @@ -48,15 +80,15 @@ export default class TabPresenter { }); } - select(tabId: number): Promise { - return browser.tabs.update(tabId, { active: true }); + async select(tabId: number): Promise { + await browser.tabs.update(tabId, { active: true }); } - remove(ids: number[]): Promise { - return browser.tabs.remove(ids); + async remove(ids: number[]): Promise { + await browser.tabs.remove(ids); } - async reopen(): Promise { + async reopen(): Promise { const window = await browser.windows.getCurrent(); const sessions = await browser.sessions.getRecentlyClosed(); const session = sessions.find((s) => { @@ -66,19 +98,18 @@ export default class TabPresenter { return; } if (session.tab && session.tab.sessionId) { - return browser.sessions.restore(session.tab.sessionId); - } - if (session.window && session.window.sessionId) { - return browser.sessions.restore(session.window.sessionId); + await browser.sessions.restore(session.tab.sessionId); + } else if (session.window && session.window.sessionId) { + await browser.sessions.restore(session.window.sessionId); } } - reload(tabId: number, cache: boolean): Promise { - return browser.tabs.reload(tabId, { bypassCache: cache }); + async reload(tabId: number, cache: boolean): Promise { + await browser.tabs.reload(tabId, { bypassCache: cache }); } - setPinned(tabId: number, pinned: boolean): Promise { - return browser.tabs.update(tabId, { pinned }); + async setPinned(tabId: number, pinned: boolean): Promise { + await browser.tabs.update(tabId, { pinned }); } duplicate(id: number): Promise { @@ -100,7 +131,7 @@ export default class TabPresenter { } } -const tabPresenter = new TabPresenter(); +const tabPresenter = new TabPresenterImpl(); tabPresenter.onSelected((tab: any) => { const cache = new MemoryStorage(); diff --git a/src/background/usecases/AddonEnabledUseCase.ts b/src/background/usecases/AddonEnabledUseCase.ts index 9abd3dc..51f02e1 100644 --- a/src/background/usecases/AddonEnabledUseCase.ts +++ b/src/background/usecases/AddonEnabledUseCase.ts @@ -1,4 +1,4 @@ -import { injectable } from 'tsyringe'; +import { inject, injectable } from 'tsyringe'; import IndicatorPresenter from '../presenters/IndicatorPresenter'; import TabPresenter from '../presenters/TabPresenter'; import ContentMessageClient from '../infrastructures/ContentMessageClient'; @@ -7,7 +7,7 @@ import ContentMessageClient from '../infrastructures/ContentMessageClient'; export default class AddonEnabledUseCase { constructor( private indicatorPresentor: IndicatorPresenter, - private tabPresenter: TabPresenter, + @inject("TabPresenter") private tabPresenter: TabPresenter, private contentMessageClient: ContentMessageClient, ) { this.indicatorPresentor.onClick((tab) => { diff --git a/src/background/usecases/CommandUseCase.ts b/src/background/usecases/CommandUseCase.ts index 7dba664..d2d707e 100644 --- a/src/background/usecases/CommandUseCase.ts +++ b/src/background/usecases/CommandUseCase.ts @@ -14,7 +14,7 @@ import RepeatUseCase from '../usecases/RepeatUseCase'; @injectable() export default class CommandIndicator { constructor( - private tabPresenter: TabPresenter, + @inject('TabPresenter') private tabPresenter: TabPresenter, private windowPresenter: WindowPresenter, private helpPresenter: HelpPresenter, @inject("CachedSettingRepository") private cachedSettingRepository: CachedSettingRepository, @@ -80,7 +80,7 @@ export default class CommandIndicator { } const current = await this.tabPresenter.getCurrent(); - const tabs = await this.tabPresenter.getByKeyword(keywords); + const tabs = await this.tabPresenter.getByKeyword(keywords, false); if (tabs.length === 0) { throw new RangeError('No matching buffer for ' + keywords); } diff --git a/src/background/usecases/CompletionsUseCase.ts b/src/background/usecases/CompletionsUseCase.ts index b75b635..439c81a 100644 --- a/src/background/usecases/CompletionsUseCase.ts +++ b/src/background/usecases/CompletionsUseCase.ts @@ -5,17 +5,17 @@ import CompletionsRepository from '../repositories/CompletionsRepository'; import CachedSettingRepository from '../repositories/CachedSettingRepository'; import TabPresenter from '../presenters/TabPresenter'; import Properties from '../../shared/settings/Properties'; -import CompletionUseCase from "../completion/CompletionUseCase"; +import OpenCompletionUseCase from "../completion/OpenCompletionUseCase"; type Tab = browser.tabs.Tab; @injectable() export default class CompletionsUseCase { constructor( - private tabPresenter: TabPresenter, + @inject('TabPresenter') private tabPresenter: TabPresenter, private completionsRepository: CompletionsRepository, @inject("CachedSettingRepository") private cachedSettingRepository: CachedSettingRepository, - private completionUseCase: CompletionUseCase + private completionUseCase: OpenCompletionUseCase ) { } diff --git a/src/background/usecases/ConsoleUseCase.ts b/src/background/usecases/ConsoleUseCase.ts index 775a1e0..2de5bc1 100644 --- a/src/background/usecases/ConsoleUseCase.ts +++ b/src/background/usecases/ConsoleUseCase.ts @@ -1,4 +1,4 @@ -import { injectable } from 'tsyringe'; +import { inject, injectable } from 'tsyringe'; import TabPresenter from '../presenters/TabPresenter'; import ConsoleClient from '../infrastructures/ConsoleClient'; @@ -6,7 +6,7 @@ import ConsoleClient from '../infrastructures/ConsoleClient'; export default class ConsoleUseCase { constructor( - private tabPresenter: TabPresenter, + @inject('TabPresenter') private tabPresenter: TabPresenter, private consoleClient: ConsoleClient, ) { } diff --git a/src/background/usecases/FindUseCase.ts b/src/background/usecases/FindUseCase.ts index b8593c6..cb41cd5 100644 --- a/src/background/usecases/FindUseCase.ts +++ b/src/background/usecases/FindUseCase.ts @@ -1,4 +1,4 @@ -import { injectable } from 'tsyringe'; +import { inject, injectable } from 'tsyringe'; import FindRepository from '../repositories/FindRepository'; import TabPresenter from '../presenters/TabPresenter'; import ConsoleClient from '../infrastructures/ConsoleClient'; @@ -6,7 +6,7 @@ import ConsoleClient from '../infrastructures/ConsoleClient'; @injectable() export default class FindUseCase { constructor( - private tabPresenter: TabPresenter, + @inject('TabPresenter') private tabPresenter: TabPresenter, private findRepository: FindRepository, private consoleClient: ConsoleClient, ) { diff --git a/src/background/usecases/LinkUseCase.ts b/src/background/usecases/LinkUseCase.ts index 9c0eab5..be076c7 100644 --- a/src/background/usecases/LinkUseCase.ts +++ b/src/background/usecases/LinkUseCase.ts @@ -1,10 +1,10 @@ -import { injectable } from 'tsyringe'; +import { inject, injectable } from 'tsyringe'; import TabPresenter from '../presenters/TabPresenter'; @injectable() export default class LinkUseCase { constructor( - private tabPresenter: TabPresenter, + @inject('TabPresenter') private tabPresenter: TabPresenter, ) { } diff --git a/src/background/usecases/MarkUseCase.ts b/src/background/usecases/MarkUseCase.ts index eeac40f..2c0bc13 100644 --- a/src/background/usecases/MarkUseCase.ts +++ b/src/background/usecases/MarkUseCase.ts @@ -1,4 +1,4 @@ -import { injectable } from 'tsyringe'; +import { inject, injectable } from 'tsyringe'; import TabPresenter from '../presenters/TabPresenter'; import MarkRepository from '../repositories/MarkRepository'; import ConsoleClient from '../infrastructures/ConsoleClient'; @@ -7,7 +7,7 @@ import ContentMessageClient from '../infrastructures/ContentMessageClient'; @injectable() export default class MarkUseCase { constructor( - private tabPresenter: TabPresenter, + @inject('TabPresenter') private tabPresenter: TabPresenter, private markRepository: MarkRepository, private consoleClient: ConsoleClient, private contentMessageClient: ContentMessageClient, diff --git a/src/background/usecases/NavigateUseCase.ts b/src/background/usecases/NavigateUseCase.ts index 25e7f20..3aa1ed6 100644 --- a/src/background/usecases/NavigateUseCase.ts +++ b/src/background/usecases/NavigateUseCase.ts @@ -1,11 +1,11 @@ -import { injectable } from 'tsyringe'; +import { inject, injectable } from 'tsyringe'; import NavigateClient from '../clients/NavigateClient'; import TabPresenter from '../presenters/TabPresenter'; @injectable() export default class NavigateUseCase { constructor( - private tabPresenter: TabPresenter, + @inject('TabPresenter') private tabPresenter: TabPresenter, private navigateClient: NavigateClient, ) { } diff --git a/src/background/usecases/TabSelectUseCase.ts b/src/background/usecases/TabSelectUseCase.ts index 62098de..271bb6c 100644 --- a/src/background/usecases/TabSelectUseCase.ts +++ b/src/background/usecases/TabSelectUseCase.ts @@ -1,10 +1,10 @@ -import { injectable } from 'tsyringe'; +import { inject, injectable } from 'tsyringe'; import TabPresenter from '../presenters/TabPresenter'; @injectable() export default class TabSelectUseCase { constructor( - private tabPresenter: TabPresenter, + @inject('TabPresenter') private tabPresenter: TabPresenter, ) { } diff --git a/src/background/usecases/TabUseCase.ts b/src/background/usecases/TabUseCase.ts index 66f8573..418dde5 100644 --- a/src/background/usecases/TabUseCase.ts +++ b/src/background/usecases/TabUseCase.ts @@ -1,4 +1,4 @@ -import { injectable } from 'tsyringe'; +import {inject, injectable} from 'tsyringe'; import TabPresenter from '../presenters/TabPresenter'; import WindowPresenter from '../presenters/WindowPresenter'; import BrowserSettingRepository from '../repositories/BrowserSettingRepository'; @@ -6,7 +6,7 @@ import BrowserSettingRepository from '../repositories/BrowserSettingRepository'; @injectable() export default class TabUseCase { constructor( - private tabPresenter: TabPresenter, + @inject('TabPresenter') private tabPresenter: TabPresenter, private windowPresenter: WindowPresenter, private browserSettingRepository: BrowserSettingRepository, ) { diff --git a/src/background/usecases/VersionUseCase.ts b/src/background/usecases/VersionUseCase.ts index 9ea8af9..21a5e2c 100644 --- a/src/background/usecases/VersionUseCase.ts +++ b/src/background/usecases/VersionUseCase.ts @@ -5,7 +5,7 @@ import Notifier from '../presenters/Notifier'; @injectable() export default class VersionUseCase { constructor( - private tabPresenter: TabPresenter, + @inject('TabPresenter') private tabPresenter: TabPresenter, @inject("Notifier") private notifier: Notifier, ) { } diff --git a/src/background/usecases/ZoomUseCase.ts b/src/background/usecases/ZoomUseCase.ts index f598871..ca1368d 100644 --- a/src/background/usecases/ZoomUseCase.ts +++ b/src/background/usecases/ZoomUseCase.ts @@ -1,4 +1,4 @@ -import { injectable } from 'tsyringe'; +import { inject, injectable } from 'tsyringe'; import TabPresenter from '../presenters/TabPresenter'; const ZOOM_SETTINGS: number[] = [ @@ -9,7 +9,7 @@ const ZOOM_SETTINGS: number[] = [ @injectable() export default class ZoomUseCase { constructor( - private tabPresenter: TabPresenter, + @inject('TabPresenter') private tabPresenter: TabPresenter, ) { } diff --git a/src/console/actions/console.ts b/src/console/actions/console.ts index f1db941..603a98d 100644 --- a/src/console/actions/console.ts +++ b/src/console/actions/console.ts @@ -1,9 +1,10 @@ import * as messages from '../../shared/messages'; import * as actions from './index'; -import { Command } from "../../shared/Command"; +import {Command} from "../../shared/Command"; import CompletionClient from "../clients/CompletionClient"; import CompletionType from "../../shared/CompletionType"; import Completions from "../Completions"; +import TabFlag from "../../shared/TabFlag"; const completionClient = new CompletionClient(); @@ -155,6 +156,28 @@ const getOpenCompletions = async( }; }; +const getTabCompletions = async ( + original: string, command: Command, query: string, excludePinned: boolean, +): Promise => { + const items = await completionClient.requestTabs(query, excludePinned); + const completions = [{ + name: 'Buffers', + items: items.map(item => ({ + content: command + ' ' + item.url, + caption: `${item.index}: ${item.flag != TabFlag.None ? item.flag : ' ' } ${item.title}`, + url: item.url, + icon: item.faviconUrl, + })), + }]; + return { + type: actions.CONSOLE_SET_COMPLETIONS, + completions, + completionSource: original, + } +}; + + + const getCompletions = async(text: string): Promise => { const completions = await browser.runtime.sendMessage({ type: messages.CONSOLE_QUERY_COMPLETIONS, @@ -181,6 +204,6 @@ const completionPrev = (): actions.CompletionPrevAction => { export { hide, showCommand, showFind, showError, showInfo, hideCommand, setConsoleText, enterCommand, enterFind, - getCompletions, getCommandCompletions, getOpenCompletions, + getCompletions, getCommandCompletions, getOpenCompletions, getTabCompletions, completionNext, completionPrev, }; diff --git a/src/console/clients/CompletionClient.ts b/src/console/clients/CompletionClient.ts index d5f9b01..19b4d99 100644 --- a/src/console/clients/CompletionClient.ts +++ b/src/console/clients/CompletionClient.ts @@ -2,9 +2,10 @@ import * as messages from "../../shared/messages"; import { ConsoleGetCompletionTypesResponse, ConsoleRequestBookmarksResponse, - ConsoleRequestHistoryResponse, ConsoleRequestSearchEnginesResponse + ConsoleRequestHistoryResponse, ConsoleRequestSearchEnginesResponse, ConsoleRequesttabsResponse } from "../../shared/messages"; import CompletionType from "../../shared/CompletionType"; +import TabFlag from "../../shared/TabFlag"; export type SearchEngines = { title: string @@ -20,6 +21,14 @@ export type HistoryItem = { url: string } +export type TabItem = { + index: number + flag: TabFlag + title: string + url: string + faviconUrl?: string +} + export default class CompletionClient { async getCompletionTypes(): Promise { const resp = await browser.runtime.sendMessage({ @@ -51,4 +60,13 @@ export default class CompletionClient { }) as ConsoleRequestHistoryResponse; return resp; } + + async requestTabs(query: string, excludePinned: boolean): Promise { + const resp = await browser.runtime.sendMessage({ + type: messages.CONSOLE_REQUEST_TABS, + query, + excludePinned, + }) as ConsoleRequesttabsResponse; + return resp; + } } diff --git a/src/console/components/Console.tsx b/src/console/components/Console.tsx index 7be073e..c1709cd 100644 --- a/src/console/components/Console.tsx +++ b/src/console/components/Console.tsx @@ -171,6 +171,21 @@ class Console extends React.Component { case Command.WindowOpen: this.props.dispatch(consoleActions.getOpenCompletions(this.props.completionTypes, text, cmd.command, cmd.args)); break; + case Command.Buffer: + this.props.dispatch(consoleActions.getTabCompletions(text, cmd.command, cmd.args, false)); + break; + case Command.BufferDelete: + this.props.dispatch(consoleActions.getTabCompletions(text, cmd.command, cmd.args, true)); + break; + case Command.BufferDeleteForce: + this.props.dispatch(consoleActions.getTabCompletions(text, cmd.command, cmd.args, false)); + break; + case Command.BuffersDelete: + this.props.dispatch(consoleActions.getTabCompletions(text, cmd.command, cmd.args, true)); + break; + case Command.BuffersDeleteForce: + this.props.dispatch(consoleActions.getTabCompletions(text, cmd.command, cmd.args, false)); + break; default: this.props.dispatch(consoleActions.getCompletions(text)); } diff --git a/src/shared/TabFlag.ts b/src/shared/TabFlag.ts new file mode 100644 index 0000000..b10d5c6 --- /dev/null +++ b/src/shared/TabFlag.ts @@ -0,0 +1,7 @@ +enum TabFlag { + CurrentTab = '%', + LastTab = '#', + None = '', +} + +export default TabFlag \ No newline at end of file diff --git a/src/shared/messages.ts b/src/shared/messages.ts index be6a2e1..3f6b7bf 100644 --- a/src/shared/messages.ts +++ b/src/shared/messages.ts @@ -1,5 +1,6 @@ import * as operations from './operations'; import CompletionType from "./CompletionType"; +import TabFlag from "./TabFlag"; export const BACKGROUND_OPERATION = 'background.operation'; @@ -16,6 +17,7 @@ export const CONSOLE_GET_COMPLETION_TYPES = 'console.get.completion.types' export const CONSOLE_REQUEST_SEARCH_ENGINES_MESSAGE = 'console.qresut.searchEngines'; export const CONSOLE_REQUEST_BOOKMARKS = 'console.request.bookmarks'; export const CONSOLE_REQUEST_HISTORY = 'console.request.history'; +export const CONSOLE_REQUEST_TABS = 'console.request.tabs'; export const FOLLOW_START = 'follow.start'; export const FOLLOW_REQUEST_COUNT_TARGETS = 'follow.request.count.targets'; @@ -119,6 +121,20 @@ export interface ConsoleRequestHistoryMessage { query: string; } +export interface ConsoleRequestTabsMessage { + type: typeof CONSOLE_REQUEST_TABS; + query: string; + excludePinned: boolean; +} + +export type ConsoleRequesttabsResponse = { + index: number + flag: TabFlag + title: string + url: string + faviconUrl?: string +}[] + export type ConsoleGetCompletionTypesResponse = CompletionType[]; export type ConsoleRequestSearchEnginesResponse = { @@ -279,6 +295,7 @@ export type Message = ConsoleHideMessage | ConsoleRequestBookmarksMessage | ConsoleRequestHistoryMessage | + ConsoleRequestTabsMessage | ConsoleGetCompletionTypesMessage | ConsoleRequestSearchEnginesMessage | FollowStartMessage | diff --git a/test/background/completion/CompletionUseCase.test.ts b/test/background/completion/CompletionUseCase.test.ts deleted file mode 100644 index 0d58e45..0000000 --- a/test/background/completion/CompletionUseCase.test.ts +++ /dev/null @@ -1,131 +0,0 @@ -import "reflect-metadata"; -import CompletionType from "../../../src/shared/CompletionType"; -import BookmarkRepository, {BookmarkItem} from "../../../src/background/completion/BookmarkRepository"; -import HistoryRepository, {HistoryItem} from "../../../src/background/completion/HistoryRepository"; -import CompletionUseCase from "../../../src/background/completion/CompletionUseCase"; -import CachedSettingRepository from "../../../src/background/repositories/CachedSettingRepository"; -import Settings, {DefaultSetting} from "../../../src/shared/settings/Settings"; -import { expect } from 'chai'; -import sinon from 'sinon'; -import Properties from "../../../src/shared/settings/Properties"; -import Search from "../../../src/shared/settings/Search"; - -class MockBookmarkRepository implements BookmarkRepository { - queryBookmarks(_query: string): Promise { - throw new Error("not implemented") - } -} - -class MockHistoryRepository implements HistoryRepository { - queryHistories(_keywords: string): Promise { - throw new Error("not implemented") - } -} - -class MockSettingRepository implements CachedSettingRepository { - get(): Promise { - throw new Error("not implemented") - } - - setProperty(_name: string, _value: string | number | boolean): Promise { - throw new Error("not implemented") - } - - update(_value: Settings): Promise { - throw new Error("not implemented") - } -} - -describe('CompletionUseCase', () => { - let bookmarkRepository: MockBookmarkRepository; - let historyRepository: MockHistoryRepository; - let settingRepository: MockSettingRepository; - let sut: CompletionUseCase; - - beforeEach(() => { - bookmarkRepository = new MockBookmarkRepository(); - historyRepository = new MockHistoryRepository(); - settingRepository = new MockSettingRepository(); - sut = new CompletionUseCase(bookmarkRepository, historyRepository, settingRepository) - }); - - describe('#getCompletionTypes', () => { - it("returns completion types from the property", async () => { - sinon.stub(settingRepository, 'get').returns(Promise.resolve(new Settings({ - keymaps: DefaultSetting.keymaps, - search: DefaultSetting.search, - properties: new Properties({ complete: "shb" }), - blacklist: DefaultSetting.blacklist, - }))); - - const items = await sut.getCompletionTypes(); - expect(items).to.deep.equal([ - CompletionType.SearchEngines, - CompletionType.History, - CompletionType.Bookmarks - ]); - }); - }); - - describe('#requestSearchEngines', () => { - it("returns search engines matches by the query", async () => { - sinon.stub(settingRepository, 'get').returns(Promise.resolve(new Settings({ - keymaps: DefaultSetting.keymaps, - search: new Search("google", { - "google": "https://google.com/search?q={}", - "yahoo": "https://search.yahoo.com/search?q={}", - "bing": "https://bing.com/search?q={}", - "google_ja": "https://google.co.jp/search?q={}", - }), - properties: DefaultSetting.properties, - blacklist: DefaultSetting.blacklist, - }))); - - expect(await sut.requestSearchEngines("")).to.deep.equal([ - "google", - "yahoo", - "bing", - "google_ja", - ]); - expect(await sut.requestSearchEngines("go")).to.deep.equal([ - "google", - "google_ja", - ]); - expect(await sut.requestSearchEngines("x")).to.be.empty; - }) - }); - - describe('#requestBookmarks', () => { - it("returns bookmarks from the repository", async() => { - sinon.stub(bookmarkRepository, 'queryBookmarks') - .withArgs("site").returns(Promise.resolve([ - { title: "site1", url: "https://site1.example.com" }, - { title: "site2", url: "https://site2.example.com/" }, - ])) - .withArgs("xyz").returns(Promise.resolve([])); - - expect(await sut.requestBookmarks("site")).to.deep.equal([ - { title: "site1", url: "https://site1.example.com" }, - { title: "site2", url: "https://site2.example.com/" }, - ]); - expect(await sut.requestBookmarks("xyz")).to.be.empty; - }); - }); - - describe('#requestHistory', () => { - it("returns histories from the repository", async() => { - sinon.stub(historyRepository, 'queryHistories') - .withArgs("site").returns(Promise.resolve([ - { title: "site1", url: "https://site1.example.com" }, - { title: "site2", url: "https://site2.example.com/" }, - ])) - .withArgs("xyz").returns(Promise.resolve([])); - - expect(await sut.requestHistory("site")).to.deep.equal([ - { title: "site1", url: "https://site1.example.com" }, - { title: "site2", url: "https://site2.example.com/" }, - ]); - expect(await sut.requestHistory("xyz")).to.be.empty; - }); - }); -}); \ No newline at end of file diff --git a/test/background/completion/OpenCompletionUseCase.test.ts b/test/background/completion/OpenCompletionUseCase.test.ts new file mode 100644 index 0000000..421ce69 --- /dev/null +++ b/test/background/completion/OpenCompletionUseCase.test.ts @@ -0,0 +1,131 @@ +import "reflect-metadata"; +import CompletionType from "../../../src/shared/CompletionType"; +import BookmarkRepository, {BookmarkItem} from "../../../src/background/completion/BookmarkRepository"; +import HistoryRepository, {HistoryItem} from "../../../src/background/completion/HistoryRepository"; +import OpenCompletionUseCase from "../../../src/background/completion/OpenCompletionUseCase"; +import CachedSettingRepository from "../../../src/background/repositories/CachedSettingRepository"; +import Settings, {DefaultSetting} from "../../../src/shared/settings/Settings"; +import { expect } from 'chai'; +import sinon from 'sinon'; +import Properties from "../../../src/shared/settings/Properties"; +import Search from "../../../src/shared/settings/Search"; + +class MockBookmarkRepository implements BookmarkRepository { + queryBookmarks(_query: string): Promise { + throw new Error("not implemented") + } +} + +class MockHistoryRepository implements HistoryRepository { + queryHistories(_keywords: string): Promise { + throw new Error("not implemented") + } +} + +class MockSettingRepository implements CachedSettingRepository { + get(): Promise { + throw new Error("not implemented") + } + + setProperty(_name: string, _value: string | number | boolean): Promise { + throw new Error("not implemented") + } + + update(_value: Settings): Promise { + throw new Error("not implemented") + } +} + +describe('OpenCompletionUseCase', () => { + let bookmarkRepository: MockBookmarkRepository; + let historyRepository: MockHistoryRepository; + let settingRepository: MockSettingRepository; + let sut: OpenCompletionUseCase; + + beforeEach(() => { + bookmarkRepository = new MockBookmarkRepository(); + historyRepository = new MockHistoryRepository(); + settingRepository = new MockSettingRepository(); + sut = new OpenCompletionUseCase(bookmarkRepository, historyRepository, settingRepository) + }); + + describe('#getCompletionTypes', () => { + it("returns completion types from the property", async () => { + sinon.stub(settingRepository, 'get').returns(Promise.resolve(new Settings({ + keymaps: DefaultSetting.keymaps, + search: DefaultSetting.search, + properties: new Properties({ complete: "shb" }), + blacklist: DefaultSetting.blacklist, + }))); + + const items = await sut.getCompletionTypes(); + expect(items).to.deep.equal([ + CompletionType.SearchEngines, + CompletionType.History, + CompletionType.Bookmarks + ]); + }); + }); + + describe('#requestSearchEngines', () => { + it("returns search engines matches by the query", async () => { + sinon.stub(settingRepository, 'get').returns(Promise.resolve(new Settings({ + keymaps: DefaultSetting.keymaps, + search: new Search("google", { + "google": "https://google.com/search?q={}", + "yahoo": "https://search.yahoo.com/search?q={}", + "bing": "https://bing.com/search?q={}", + "google_ja": "https://google.co.jp/search?q={}", + }), + properties: DefaultSetting.properties, + blacklist: DefaultSetting.blacklist, + }))); + + expect(await sut.requestSearchEngines("")).to.deep.equal([ + "google", + "yahoo", + "bing", + "google_ja", + ]); + expect(await sut.requestSearchEngines("go")).to.deep.equal([ + "google", + "google_ja", + ]); + expect(await sut.requestSearchEngines("x")).to.be.empty; + }) + }); + + describe('#requestBookmarks', () => { + it("returns bookmarks from the repository", async() => { + sinon.stub(bookmarkRepository, 'queryBookmarks') + .withArgs("site").returns(Promise.resolve([ + { title: "site1", url: "https://site1.example.com" }, + { title: "site2", url: "https://site2.example.com/" }, + ])) + .withArgs("xyz").returns(Promise.resolve([])); + + expect(await sut.requestBookmarks("site")).to.deep.equal([ + { title: "site1", url: "https://site1.example.com" }, + { title: "site2", url: "https://site2.example.com/" }, + ]); + expect(await sut.requestBookmarks("xyz")).to.be.empty; + }); + }); + + describe('#requestHistory', () => { + it("returns histories from the repository", async() => { + sinon.stub(historyRepository, 'queryHistories') + .withArgs("site").returns(Promise.resolve([ + { title: "site1", url: "https://site1.example.com" }, + { title: "site2", url: "https://site2.example.com/" }, + ])) + .withArgs("xyz").returns(Promise.resolve([])); + + expect(await sut.requestHistory("site")).to.deep.equal([ + { title: "site1", url: "https://site1.example.com" }, + { title: "site2", url: "https://site2.example.com/" }, + ]); + expect(await sut.requestHistory("xyz")).to.be.empty; + }); + }); +}); \ No newline at end of file diff --git a/test/background/completion/TabCompletionUseCase.test.ts b/test/background/completion/TabCompletionUseCase.test.ts new file mode 100644 index 0000000..e2c7c19 --- /dev/null +++ b/test/background/completion/TabCompletionUseCase.test.ts @@ -0,0 +1,105 @@ +import "reflect-metadata" +import TabRepositoryImpl from "../../../src/background/completion/impl/TabRepositoryImpl"; +import {Tab} from "../../../src/background/completion/TabRepository"; +import TabPresenter from "../../../src/background/presenters/TabPresenter"; +import TabCompletionUseCase from "../../../src/background/completion/TabCompletionUseCase"; +import sinon from 'sinon'; +import { expect } from 'chai'; +import TabFlag from "../../../src/shared/TabFlag"; + +class MockTabRepository implements TabRepositoryImpl { + async queryTabs(_query: string, _excludePinned: boolean): Promise { + throw new Error("not implemented") + } +} + +class MockTabPresenter implements TabPresenter { + create(_url: string, _opts?: object): Promise { + throw new Error("not implemented") + } + + duplicate(_id: number): Promise { + throw new Error("not implemented") + } + + getAll(): Promise { + throw new Error("not implemented") + } + + getByKeyword(_keyword: string, _excludePinned: boolean): Promise { + throw new Error("not implemented") + } + + getCurrent(): Promise { + throw new Error("not implemented") + } + + getLastSelectedId(): Promise { + throw new Error("not implemented") + } + + getZoom(_tabId: number): Promise { + throw new Error("not implemented") + } + + onSelected(_listener: (arg: { tabId: number; windowId: number }) => void): void { + throw new Error("not implemented") + } + + open(_url: string, _tabId?: number): Promise { + throw new Error("not implemented") + } + + reload(_tabId: number, _cache: boolean): Promise { + throw new Error("not implemented") + } + + remove(_ids: number[]): Promise { + throw new Error("not implemented") + } + + reopen(): Promise { + throw new Error("not implemented") + } + + select(_tabId: number): Promise { + throw new Error("not implemented") + } + + setPinned(_tabId: number, _pinned: boolean): Promise { + throw new Error("not implemented") + } + + setZoom(_tabId: number, _factor: number): Promise { + throw new Error("not implemented") + } +} + +describe('TabCompletionUseCase', () => { + let tabRepository: MockTabRepository; + let tabPresenter: TabPresenter; + let sut: TabCompletionUseCase; + + beforeEach(() => { + tabRepository = new MockTabRepository(); + tabPresenter = new MockTabPresenter(); + sut = new TabCompletionUseCase(tabRepository, tabPresenter) + }); + + describe('#queryTabs', () => { + it("returns tab items", async () => { + sinon.stub(tabRepository, 'queryTabs').returns(Promise.resolve([ + { id: 10, index: 0, title: 'Google', url: 'https://google.com/', faviconUrl: 'https://google.com/favicon.ico', active: true }, + { id: 11, index: 1, title: 'Yahoo', url: 'https://yahoo.com/', faviconUrl: 'https://yahoo.com/favicon.ico', active: false }, + { id: 12, index: 2, title: 'Bing', url: 'https://bing.com/', active: false }, + ])); + sinon.stub(tabPresenter, 'getLastSelectedId').returns(Promise.resolve(11)); + + expect(await sut.queryTabs("", false)).to.deep.equal([ + { index: 0, title: 'Google', url: 'https://google.com/', faviconUrl: 'https://google.com/favicon.ico', flag: TabFlag.CurrentTab }, + { index: 1, title: 'Yahoo', url: 'https://yahoo.com/', faviconUrl: 'https://yahoo.com/favicon.ico', flag: TabFlag.LastTab }, + { index: 2, title: 'Bing', url: 'https://bing.com/', faviconUrl: undefined, flag: TabFlag.None }, + ]); + }) + }); +}); \ No newline at end of file -- cgit v1.2.3 From 0340c82bc82738a63c8a374930cf39cbed5c7c8c Mon Sep 17 00:00:00 2001 From: Shin'ya Ueoka Date: Sun, 29 Mar 2020 21:10:47 +0900 Subject: Complete tab by an index and a flag --- src/background/completion/TabCompletionUseCase.ts | 25 +++++++++- src/background/completion/TabRepository.ts | 2 + .../completion/impl/TabRepositoryImpl.ts | 32 +++++++++---- .../completion/TabCompletionUseCase.test.ts | 53 +++++++++++++++++++--- 4 files changed, 94 insertions(+), 18 deletions(-) (limited to 'src/background') diff --git a/src/background/completion/TabCompletionUseCase.ts b/src/background/completion/TabCompletionUseCase.ts index 7e6dce3..dec86e9 100644 --- a/src/background/completion/TabCompletionUseCase.ts +++ b/src/background/completion/TabCompletionUseCase.ts @@ -1,6 +1,6 @@ import { inject, injectable } from "tsyringe"; import TabItem from "./TabItem"; -import TabRepository from "./TabRepository"; +import TabRepository, { Tab } from "./TabRepository"; import TabPresenter from "../presenters/TabPresenter"; import TabFlag from "../../shared/TabFlag"; @@ -14,7 +14,28 @@ export default class TabCompletionUseCase { async queryTabs(query: string, excludePinned: boolean): Promise { const lastTabId = await this.tabPresenter.getLastSelectedId(); - const tabs = await this.tabRepository.queryTabs(query, excludePinned); + const allTabs = await this.tabRepository.getAllTabs(excludePinned); + const num = parseInt(query, 10); + let tabs: Tab[] = []; + if (!isNaN(num)) { + const tab = allTabs.find(t => t.index === num - 1); + if (tab) { + tabs = [tab]; + } + } else if (query == '%') { + const tab = allTabs.find(t => t.active); + if (tab) { + tabs = [tab]; + } + } else if (query == '#') { + const tab = allTabs.find(t => t.id === lastTabId); + if (tab) { + tabs = [tab]; + } + } else { + tabs = await this.tabRepository.queryTabs(query, excludePinned); + } + return tabs.map(tab => { let flag = TabFlag.None; if (tab.active) { diff --git a/src/background/completion/TabRepository.ts b/src/background/completion/TabRepository.ts index 61fac3b..fe1b601 100644 --- a/src/background/completion/TabRepository.ts +++ b/src/background/completion/TabRepository.ts @@ -9,4 +9,6 @@ export type Tab = { export default interface TabRepository { queryTabs(query: string, excludePinned: boolean): Promise; + + getAllTabs(excludePinned: boolean): Promise } diff --git a/src/background/completion/impl/TabRepositoryImpl.ts b/src/background/completion/impl/TabRepositoryImpl.ts index 6692b27..adcaba7 100644 --- a/src/background/completion/impl/TabRepositoryImpl.ts +++ b/src/background/completion/impl/TabRepositoryImpl.ts @@ -15,13 +15,27 @@ export default class TabRepositoryImpl implements TabRepository { }) .filter(item => item.id && item.title && item.url) .slice(0, COMPLETION_ITEM_LIMIT) - .map(item => ({ - id: item.id!!, - url: item.url!!, - active: item.active, - title: item.title!!, - faviconUrl: item.favIconUrl, - index: item.index, - })) + .map(TabRepositoryImpl.toEntity); } -} \ No newline at end of file + + async getAllTabs(excludePinned: boolean): Promise { + if (excludePinned) { + return (await browser.tabs.query({ currentWindow: true, pinned: true })) + .map(TabRepositoryImpl.toEntity) + + } + return (await browser.tabs.query({ currentWindow: true })) + .map(TabRepositoryImpl.toEntity) + } + + private static toEntity(tab: browser.tabs.Tab,): Tab { + return { + id: tab.id!!, + url: tab.url!!, + active: tab.active, + title: tab.title!!, + faviconUrl: tab.favIconUrl, + index: tab.index, + } + } +} diff --git a/test/background/completion/TabCompletionUseCase.test.ts b/test/background/completion/TabCompletionUseCase.test.ts index 254bfe6..9df236d 100644 --- a/test/background/completion/TabCompletionUseCase.test.ts +++ b/test/background/completion/TabCompletionUseCase.test.ts @@ -11,6 +11,10 @@ class MockTabRepository implements TabRepositoryImpl { async queryTabs(_query: string, _excludePinned: boolean): Promise { throw new Error("not implemented") } + + async getAllTabs(_excludePinned: boolean): Promise { + throw new Error("not implemented") + } } class MockTabPresenter implements TabPresenter { @@ -83,22 +87,57 @@ describe('TabCompletionUseCase', () => { beforeEach(() => { tabRepository = new MockTabRepository(); tabPresenter = new MockTabPresenter(); - sut = new TabCompletionUseCase(tabRepository, tabPresenter) + sut = new TabCompletionUseCase(tabRepository, tabPresenter); + + sinon.stub(tabPresenter, 'getLastSelectedId').returns(Promise.resolve(12)); + sinon.stub(tabRepository, 'getAllTabs').returns(Promise.resolve([ + { id: 10, index: 0, title: 'Google', url: 'https://google.com/', faviconUrl: 'https://google.com/favicon.ico', active: false }, + { id: 11, index: 1, title: 'Yahoo', url: 'https://yahoo.com/', faviconUrl: 'https://yahoo.com/favicon.ico', active: true }, + { id: 12, index: 2, title: 'Bing', url: 'https://bing.com/', active: false }, + ])); }); describe('#queryTabs', () => { it("returns tab items", async () => { - sinon.stub(tabRepository, 'queryTabs').returns(Promise.resolve([ - { id: 10, index: 0, title: 'Google', url: 'https://google.com/', faviconUrl: 'https://google.com/favicon.ico', active: true }, - { id: 11, index: 1, title: 'Yahoo', url: 'https://yahoo.com/', faviconUrl: 'https://yahoo.com/favicon.ico', active: false }, + sinon.stub(tabRepository, 'queryTabs').withArgs('', false).returns(Promise.resolve([ + { id: 10, index: 0, title: 'Google', url: 'https://google.com/', faviconUrl: 'https://google.com/favicon.ico', active: false }, + { id: 11, index: 1, title: 'Yahoo', url: 'https://yahoo.com/', faviconUrl: 'https://yahoo.com/favicon.ico', active: true }, { id: 12, index: 2, title: 'Bing', url: 'https://bing.com/', active: false }, + ])).withArgs('oo', false).returns(Promise.resolve([ + { id: 10, index: 0, title: 'Google', url: 'https://google.com/', faviconUrl: 'https://google.com/favicon.ico', active: false }, + { id: 11, index: 1, title: 'Yahoo', url: 'https://yahoo.com/', faviconUrl: 'https://yahoo.com/favicon.ico', active: true }, ])); - sinon.stub(tabPresenter, 'getLastSelectedId').returns(Promise.resolve(11)); - expect(await sut.queryTabs("", false)).to.deep.equal([ + expect(await sut.queryTabs('', false)).to.deep.equal([ + { index: 1, title: 'Google', url: 'https://google.com/', faviconUrl: 'https://google.com/favicon.ico', flag: TabFlag.None }, + { index: 2, title: 'Yahoo', url: 'https://yahoo.com/', faviconUrl: 'https://yahoo.com/favicon.ico', flag: TabFlag.CurrentTab }, + { index: 3, title: 'Bing', url: 'https://bing.com/', faviconUrl: undefined, flag: TabFlag.LastTab }, + ]); + + expect(await sut.queryTabs('oo', false)).to.deep.equal([ + { index: 1, title: 'Google', url: 'https://google.com/', faviconUrl: 'https://google.com/favicon.ico', flag: TabFlag.None }, + { index: 2, title: 'Yahoo', url: 'https://yahoo.com/', faviconUrl: 'https://yahoo.com/favicon.ico', flag: TabFlag.CurrentTab }, + ]); + }); + + it("returns a tab by the index", async () => { + expect(await sut.queryTabs('1', false)).to.deep.equal([ { index: 1, title: 'Google', url: 'https://google.com/', faviconUrl: 'https://google.com/favicon.ico', flag: TabFlag.CurrentTab }, + ]); + + expect(await sut.queryTabs('10', false)).to.be.empty; + expect(await sut.queryTabs('-1', false)).to.be.empty; + }); + + it("returns the current tab by % flag", async () => { + expect(await sut.queryTabs('%', false)).to.deep.equal([ { index: 2, title: 'Yahoo', url: 'https://yahoo.com/', faviconUrl: 'https://yahoo.com/favicon.ico', flag: TabFlag.LastTab }, - { index: 3, title: 'Bing', url: 'https://bing.com/', faviconUrl: undefined, flag: TabFlag.None }, + ]); + }); + + it("returns the current tab by # flag", async () => { + expect(await sut.queryTabs('#', false)).to.deep.equal([ + { index: 3, title: 'Bing', url: 'https://bing.com/', faviconUrl: undefined, flag: TabFlag.LastTab }, ]); }) }); -- cgit v1.2.3 From c3be3dde555d1f51f696f4bfbe181e7bad6d3563 Mon Sep 17 00:00:00 2001 From: Shin'ya Ueoka Date: Sun, 29 Mar 2020 21:47:09 +0900 Subject: Complete properties on set command --- .../completion/PropertyCompletionUseCase.ts | 16 ++++++++ src/background/controllers/CompletionController.ts | 7 ++++ .../infrastructures/ContentMessageListener.ts | 2 + src/console/actions/console.ts | 43 +++++++++++++++++++++- src/console/clients/CompletionClient.ts | 14 ++++++- src/console/components/Console.tsx | 3 ++ src/shared/messages.ts | 13 ++++++- .../completion/PropertyCompletionUseCase.test.ts | 15 ++++++++ 8 files changed, 109 insertions(+), 4 deletions(-) create mode 100644 src/background/completion/PropertyCompletionUseCase.ts create mode 100644 test/background/completion/PropertyCompletionUseCase.test.ts (limited to 'src/background') diff --git a/src/background/completion/PropertyCompletionUseCase.ts b/src/background/completion/PropertyCompletionUseCase.ts new file mode 100644 index 0000000..049cfb8 --- /dev/null +++ b/src/background/completion/PropertyCompletionUseCase.ts @@ -0,0 +1,16 @@ +import { injectable } from "tsyringe"; +import Properties from "../../shared/settings/Properties"; + +type Property = { + name: string; + type: 'string' | 'boolean' | 'number'; +} +@injectable() +export default class PropertyCompletionUseCase { + async getProperties(): Promise { + return Properties.defs().map(def => ({ + name: def.name, + type: def.type, + })); + } +} \ No newline at end of file diff --git a/src/background/controllers/CompletionController.ts b/src/background/controllers/CompletionController.ts index a268d15..fb6137c 100644 --- a/src/background/controllers/CompletionController.ts +++ b/src/background/controllers/CompletionController.ts @@ -1,5 +1,6 @@ import { ConsoleGetCompletionTypesResponse, + ConsoleGetPropertiesResponse, ConsoleRequestBookmarksResponse, ConsoleRequestHistoryResponse, ConsoleRequestSearchEnginesResponse, @@ -8,12 +9,14 @@ import { import { injectable } from "tsyringe"; import OpenCompletionUseCase from "../completion/OpenCompletionUseCase"; import TabCompletionUseCase from "../completion/TabCompletionUseCase"; +import PropertyCompletionUseCase from "../completion/PropertyCompletionUseCase"; @injectable() export default class CompletionController { constructor( private completionUseCase: OpenCompletionUseCase, private tabCompletionUseCase: TabCompletionUseCase, + private propertyCompletionUseCase: PropertyCompletionUseCase, ) { } @@ -37,4 +40,8 @@ export default class CompletionController { async queryTabs(query: string, excludePinned: boolean): Promise { return this.tabCompletionUseCase.queryTabs(query, excludePinned); } + + async getProperties(): Promise { + return this.propertyCompletionUseCase.getProperties(); + } } \ No newline at end of file diff --git a/src/background/infrastructures/ContentMessageListener.ts b/src/background/infrastructures/ContentMessageListener.ts index 13aa763..92481da 100644 --- a/src/background/infrastructures/ContentMessageListener.ts +++ b/src/background/infrastructures/ContentMessageListener.ts @@ -75,6 +75,8 @@ export default class ContentMessageListener { return this.completionController.requestHistory(message.query); case messages.CONSOLE_REQUEST_TABS: return this.completionController.queryTabs(message.query, message.excludePinned); + case messages.CONSOLE_GET_PROPERTIES: + return this.completionController.getProperties(); case messages.CONSOLE_ENTER_COMMAND: return this.onConsoleEnterCommand(message.text); case messages.SETTINGS_QUERY: diff --git a/src/console/actions/console.ts b/src/console/actions/console.ts index 89d9c3c..b17754d 100644 --- a/src/console/actions/console.ts +++ b/src/console/actions/console.ts @@ -22,6 +22,12 @@ const commandDocs = { [Command.Help]: 'Open Vim Vixen help in new tab', }; +const propertyDocs: {[key: string]: string} = { + 'hintchars': 'hint characters on follow mode', + 'smoothscroll': 'smooth scroll', + 'complete': 'which are completed at the open page', +}; + const hide = (): actions.ConsoleAction => { return { type: actions.CONSOLE_HIDE, @@ -191,7 +197,40 @@ const getTabCompletions = async ( } }; - +const getPropertyCompletions = async( + original: string, command: Command, query: string, +): Promise => { + const properties = await completionClient.getProperties(); + const items = properties + .map(item => { + const desc = propertyDocs[item.name] || ''; + if (item.type === 'boolean') { + return [{ + caption: item.name, + content: command + ' ' + item.name, + url: 'Enable ' + desc, + }, { + caption: 'no' + item.name, + content: command + ' no' + item.name, + url: 'Disable ' + desc, + }]; + } else { + return [{ + caption: item.name, + content: name + ' ' + item.name, + url: 'Set ' + desc, + }]; + } + }) + .reduce((acc, val) => acc.concat(val), []) + .filter(item => item.caption.startsWith(query)); + const completions: Completions = [{ name: 'Properties', items }]; + return { + type: actions.CONSOLE_SET_COMPLETIONS, + completions, + completionSource: original, + } +}; const getCompletions = async(text: string): Promise => { const completions = await browser.runtime.sendMessage({ @@ -219,6 +258,6 @@ const completionPrev = (): actions.CompletionPrevAction => { export { hide, showCommand, showFind, showError, showInfo, hideCommand, setConsoleText, enterCommand, enterFind, - getCompletions, getCommandCompletions, getOpenCompletions, getTabCompletions, + getCompletions, getCommandCompletions, getOpenCompletions, getTabCompletions, getPropertyCompletions, completionNext, completionPrev, }; diff --git a/src/console/clients/CompletionClient.ts b/src/console/clients/CompletionClient.ts index 19b4d99..56dc665 100644 --- a/src/console/clients/CompletionClient.ts +++ b/src/console/clients/CompletionClient.ts @@ -1,6 +1,6 @@ import * as messages from "../../shared/messages"; import { - ConsoleGetCompletionTypesResponse, + ConsoleGetCompletionTypesResponse, ConsoleGetPropertiesResponse, ConsoleRequestBookmarksResponse, ConsoleRequestHistoryResponse, ConsoleRequestSearchEnginesResponse, ConsoleRequesttabsResponse } from "../../shared/messages"; @@ -29,6 +29,11 @@ export type TabItem = { faviconUrl?: string } +export type Property = { + name: string + type: 'string' | 'boolean' | 'number'; +} + export default class CompletionClient { async getCompletionTypes(): Promise { const resp = await browser.runtime.sendMessage({ @@ -69,4 +74,11 @@ export default class CompletionClient { }) as ConsoleRequesttabsResponse; return resp; } + + async getProperties(): Promise { + const resp = await browser.runtime.sendMessage({ + type: messages.CONSOLE_GET_PROPERTIES, + }) as ConsoleGetPropertiesResponse; + return resp; + } } diff --git a/src/console/components/Console.tsx b/src/console/components/Console.tsx index c1709cd..77f1b09 100644 --- a/src/console/components/Console.tsx +++ b/src/console/components/Console.tsx @@ -186,6 +186,9 @@ class Console extends React.Component { case Command.BuffersDeleteForce: this.props.dispatch(consoleActions.getTabCompletions(text, cmd.command, cmd.args, false)); break; + case Command.Set: + this.props.dispatch(consoleActions.getPropertyCompletions(text, cmd.command, cmd.args)); + break; default: this.props.dispatch(consoleActions.getCompletions(text)); } diff --git a/src/shared/messages.ts b/src/shared/messages.ts index 3f6b7bf..577c34c 100644 --- a/src/shared/messages.ts +++ b/src/shared/messages.ts @@ -13,11 +13,12 @@ export const CONSOLE_SHOW_ERROR = 'console.show.error'; export const CONSOLE_SHOW_INFO = 'console.show.info'; export const CONSOLE_SHOW_FIND = 'console.show.find'; export const CONSOLE_HIDE = 'console.hide'; -export const CONSOLE_GET_COMPLETION_TYPES = 'console.get.completion.types' +export const CONSOLE_GET_COMPLETION_TYPES = 'console.get.completion.types'; export const CONSOLE_REQUEST_SEARCH_ENGINES_MESSAGE = 'console.qresut.searchEngines'; export const CONSOLE_REQUEST_BOOKMARKS = 'console.request.bookmarks'; export const CONSOLE_REQUEST_HISTORY = 'console.request.history'; export const CONSOLE_REQUEST_TABS = 'console.request.tabs'; +export const CONSOLE_GET_PROPERTIES = 'console.get.properties'; export const FOLLOW_START = 'follow.start'; export const FOLLOW_REQUEST_COUNT_TARGETS = 'follow.request.count.targets'; @@ -127,6 +128,10 @@ export interface ConsoleRequestTabsMessage { excludePinned: boolean; } +export interface ConsoleGetPropertiesMessage { + type: typeof CONSOLE_GET_PROPERTIES; +} + export type ConsoleRequesttabsResponse = { index: number flag: TabFlag @@ -151,6 +156,11 @@ export type ConsoleRequestHistoryResponse = { url: string; }[] +export type ConsoleGetPropertiesResponse = { + name: string + type: 'string' | 'boolean' | 'number' +}[] + export interface FollowStartMessage { type: typeof FOLLOW_START; newTab: boolean; @@ -296,6 +306,7 @@ export type Message = ConsoleRequestBookmarksMessage | ConsoleRequestHistoryMessage | ConsoleRequestTabsMessage | + ConsoleGetPropertiesMessage | ConsoleGetCompletionTypesMessage | ConsoleRequestSearchEnginesMessage | FollowStartMessage | diff --git a/test/background/completion/PropertyCompletionUseCase.test.ts b/test/background/completion/PropertyCompletionUseCase.test.ts new file mode 100644 index 0000000..57f5bff --- /dev/null +++ b/test/background/completion/PropertyCompletionUseCase.test.ts @@ -0,0 +1,15 @@ +import 'reflect-metadata'; +import PropertyCompletionUseCase from "../../../src/background/completion/PropertyCompletionUseCase"; +import { expect } from 'chai'; + +describe('PropertyCompletionUseCase', () => { + describe('getProperties', () => { + it('returns property types', async () => { + const sut = new PropertyCompletionUseCase(); + + const properties = await sut.getProperties(); + expect(properties).to.deep.contain({ name: 'smoothscroll', type: 'boolean' }); + expect(properties).to.deep.contain({ name: 'complete', type: 'string' }); + }) + }); +}); \ No newline at end of file -- cgit v1.2.3 From 5176643e64d8f4a6be5fc73f0eb48dc65322e496 Mon Sep 17 00:00:00 2001 From: Shin'ya Ueoka Date: Sun, 29 Mar 2020 21:59:25 +0900 Subject: Clean unused code --- src/background/controllers/CommandController.ts | 38 ---- src/background/domains/CommandDocs.ts | 12 -- src/background/domains/CompletionGroup.ts | 6 - src/background/domains/CompletionItem.ts | 6 - .../infrastructures/ContentMessageListener.ts | 8 - .../repositories/CompletionsRepository.ts | 16 -- src/background/usecases/CompletionsUseCase.ts | 205 --------------------- src/console/actions/console.ts | 14 +- src/console/components/Console.tsx | 8 +- src/shared/messages.ts | 8 - 10 files changed, 2 insertions(+), 319 deletions(-) delete mode 100644 src/background/domains/CommandDocs.ts delete mode 100644 src/background/domains/CompletionGroup.ts delete mode 100644 src/background/domains/CompletionItem.ts delete mode 100644 src/background/repositories/CompletionsRepository.ts delete mode 100644 src/background/usecases/CompletionsUseCase.ts (limited to 'src/background') diff --git a/src/background/controllers/CommandController.ts b/src/background/controllers/CommandController.ts index 7297ef8..16aa6e8 100644 --- a/src/background/controllers/CommandController.ts +++ b/src/background/controllers/CommandController.ts @@ -1,7 +1,5 @@ import { injectable } from 'tsyringe'; -import CompletionsUseCase from '../usecases/CompletionsUseCase'; import CommandUseCase from '../usecases/CommandUseCase'; -import CompletionGroup from '../domains/CompletionGroup'; const trimStart = (str: string): string => { // NOTE String.trimStart is available on Firefox 61 @@ -11,46 +9,10 @@ const trimStart = (str: string): string => { @injectable() export default class CommandController { constructor( - private completionsUseCase: CompletionsUseCase, private commandIndicator: CommandUseCase, ) { } - getCompletions(line: string): Promise { - const trimmed = trimStart(line); - const words = trimmed.split(/ +/); - const name = words[0]; - if (words.length === 1) { - return this.completionsUseCase.queryConsoleCommand(name); - } - const keywords = trimStart(trimmed.slice(name.length)); - switch (words[0]) { - case 'o': - case 'open': - case 't': - case 'tabopen': - case 'w': - case 'winopen': - return this.completionsUseCase.queryOpen(name, keywords); - case 'b': - case 'buffer': - return this.completionsUseCase.queryBuffer(name, keywords); - case 'bd': - case 'bdel': - case 'bdelete': - case 'bdeletes': - return this.completionsUseCase.queryBdelete(name, keywords); - case 'bd!': - case 'bdel!': - case 'bdelete!': - case 'bdeletes!': - return this.completionsUseCase.queryBdeleteForce(name, keywords); - case 'set': - return this.completionsUseCase.querySet(name, keywords); - } - return Promise.resolve([]); - } - // eslint-disable-next-line complexity exec(line: string): Promise { const trimmed = trimStart(line); diff --git a/src/background/domains/CommandDocs.ts b/src/background/domains/CommandDocs.ts deleted file mode 100644 index e926851..0000000 --- a/src/background/domains/CommandDocs.ts +++ /dev/null @@ -1,12 +0,0 @@ -export default { - set: 'Set a value of the property', - open: 'Open a URL or search by keywords in current tab', - tabopen: 'Open a URL or search by keywords in new tab', - winopen: 'Open a URL or search by keywords in new window', - buffer: 'Select tabs by matched keywords', - bdelete: 'Close a certain tab matched by keywords', - bdeletes: 'Close all tabs matched by keywords', - quit: 'Close the current tab', - quitall: 'Close all tabs', - help: 'Open Vim Vixen help in new tab', -} as {[key: string]: string}; diff --git a/src/background/domains/CompletionGroup.ts b/src/background/domains/CompletionGroup.ts deleted file mode 100644 index 8e89bf0..0000000 --- a/src/background/domains/CompletionGroup.ts +++ /dev/null @@ -1,6 +0,0 @@ -import CompletionItem from './CompletionItem'; - -export default interface CompletionGroup { - name: string; - items: CompletionItem[]; -} diff --git a/src/background/domains/CompletionItem.ts b/src/background/domains/CompletionItem.ts deleted file mode 100644 index 96f6ee6..0000000 --- a/src/background/domains/CompletionItem.ts +++ /dev/null @@ -1,6 +0,0 @@ -export default interface CompletionItem { - readonly caption?: string; - readonly content?: string; - readonly url?: string; - readonly icon?: string; -} diff --git a/src/background/infrastructures/ContentMessageListener.ts b/src/background/infrastructures/ContentMessageListener.ts index 92481da..2fbb9cf 100644 --- a/src/background/infrastructures/ContentMessageListener.ts +++ b/src/background/infrastructures/ContentMessageListener.ts @@ -1,7 +1,6 @@ import { injectable } from 'tsyringe'; import * as messages from '../../shared/messages'; import * as operations from '../../shared/operations'; -import CompletionGroup from '../domains/CompletionGroup'; import CommandController from '../controllers/CommandController'; import SettingController from '../controllers/SettingController'; import FindController from '../controllers/FindController'; @@ -63,8 +62,6 @@ export default class ContentMessageListener { message: messages.Message, senderTab: browser.tabs.Tab, ): Promise | any { switch (message.type) { - case messages.CONSOLE_QUERY_COMPLETIONS: - return this.onConsoleQueryCompletions(message.text); case messages.CONSOLE_GET_COMPLETION_TYPES: return this.completionController.getCompletionTypes(); case messages.CONSOLE_REQUEST_SEARCH_ENGINES_MESSAGE: @@ -107,11 +104,6 @@ export default class ContentMessageListener { throw new Error('unsupported message: ' + message.type); } - async onConsoleQueryCompletions(line: string): Promise { - const completions = await this.commandController.getCompletions(line); - return Promise.resolve(completions); - } - onConsoleEnterCommand(text: string): Promise { return this.commandController.exec(text); } diff --git a/src/background/repositories/CompletionsRepository.ts b/src/background/repositories/CompletionsRepository.ts deleted file mode 100644 index 7d0434c..0000000 --- a/src/background/repositories/CompletionsRepository.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { injectable } from 'tsyringe'; - -type Tab = browser.tabs.Tab; - -@injectable() -export default class CompletionsRepository { - async queryTabs(keywords: string, excludePinned: boolean): Promise { - const tabs = await browser.tabs.query({ currentWindow: true }); - return tabs.filter((t) => { - return t.url && t.url.toLowerCase().includes(keywords.toLowerCase()) || - t.title && t.title.toLowerCase().includes(keywords.toLowerCase()); - }).filter((t) => { - return !(excludePinned && t.pinned); - }); - } -} diff --git a/src/background/usecases/CompletionsUseCase.ts b/src/background/usecases/CompletionsUseCase.ts deleted file mode 100644 index 439c81a..0000000 --- a/src/background/usecases/CompletionsUseCase.ts +++ /dev/null @@ -1,205 +0,0 @@ -import { injectable, inject } from 'tsyringe'; -import CompletionGroup from '../domains/CompletionGroup'; -import CommandDocs from '../domains/CommandDocs'; -import CompletionsRepository from '../repositories/CompletionsRepository'; -import CachedSettingRepository from '../repositories/CachedSettingRepository'; -import TabPresenter from '../presenters/TabPresenter'; -import Properties from '../../shared/settings/Properties'; -import OpenCompletionUseCase from "../completion/OpenCompletionUseCase"; - -type Tab = browser.tabs.Tab; - -@injectable() -export default class CompletionsUseCase { - constructor( - @inject('TabPresenter') private tabPresenter: TabPresenter, - private completionsRepository: CompletionsRepository, - @inject("CachedSettingRepository") private cachedSettingRepository: CachedSettingRepository, - private completionUseCase: OpenCompletionUseCase - ) { - } - - queryConsoleCommand(prefix: string): Promise { - const keys = Object.keys(CommandDocs); - const items = keys - .filter(name => name.startsWith(prefix)) - .map(name => ({ - caption: name, - content: name, - url: CommandDocs[name], - })); - - if (items.length === 0) { - return Promise.resolve([]); - } - return Promise.resolve([{ name: 'Console Command', items }]); - } - - async queryOpen(name: string, keywords: string): Promise { - // TODO This logic contains view entities. They should be defined on - // content script - - const settings = await this.cachedSettingRepository.get(); - const groups: CompletionGroup[] = []; - - const complete = settings.properties.complete; - for (const c of complete) { - if (c === 's') { - // eslint-disable-next-line no-await-in-loop - const engines = await this.querySearchEngineItems(name, keywords); - if (engines.length > 0) { - groups.push({ name: 'Search Engines', items: engines }); - } - // browser.history not supported on Android - } else if (c === 'h' && typeof browser.history === 'object') { - // eslint-disable-next-line no-await-in-loop - const histories = await this.queryHistoryItems(name, keywords); - if (histories.length > 0) { - groups.push({ name: 'History', items: histories }); - } - // browser.bookmarks not supported on Android - } else if (c === 'b' && typeof browser.bookmarks === 'object') { - // eslint-disable-next-line no-await-in-loop - const bookmarks = await this.queryBookmarkItems(name, keywords); - if (bookmarks.length > 0) { - groups.push({ name: 'Bookmarks', items: bookmarks }); - } - } - } - return groups; - } - - // eslint-disable-next-line max-statements - async queryBuffer( - name: string, - keywords: string, - ): Promise { - const lastId = await this.tabPresenter.getLastSelectedId(); - const trimmed = keywords.trim(); - let tabs: Tab[] = []; - if (trimmed.length > 0 && !isNaN(Number(trimmed))) { - const all = await this.tabPresenter.getAll(); - const index = parseInt(trimmed, 10) - 1; - if (index >= 0 && index < all.length) { - tabs = [all[index]]; - } - } else if (trimmed === '%') { - const all = await this.tabPresenter.getAll(); - const tab = all.find(t => t.active) as Tab; - tabs = [tab]; - } else if (trimmed === '#') { - if (typeof lastId !== 'undefined' && lastId !== null) { - const all = await this.tabPresenter.getAll(); - const tab = all.find(t => t.id === lastId) as Tab; - tabs = [tab]; - } - } else { - tabs = await this.completionsRepository.queryTabs(keywords, false); - } - const flag = (tab: Tab) => { - if (tab.active) { - return '%'; - } else if (tab.id === lastId) { - return '#'; - } - return ' '; - }; - const items = tabs.map(tab => ({ - caption: tab.index + 1 + ': ' + flag(tab) + ' ' + tab.title, - content: name + ' ' + tab.title, - url: tab.url, - icon: tab.favIconUrl, - })); - if (items.length === 0) { - return Promise.resolve([]); - } - return [{ name: 'Buffers', items }]; - } - - queryBdelete(name: string, keywords: string): Promise { - return this.queryTabs(name, true, keywords); - } - - queryBdeleteForce( - name: string, keywords: string, - ): Promise { - return this.queryTabs(name, false, keywords); - } - - querySet(name: string, keywords: string): Promise { - const items = Properties.defs().map((def) => { - if (def.type === 'boolean') { - return [ - { - caption: def.name, - content: name + ' ' + def.name, - url: 'Enable ' + def.description, - }, { - caption: 'no' + def.name, - content: name + ' no' + def.name, - url: 'Disable ' + def.description - } - ]; - } - return [ - { - caption: def.name, - content: name + ' ' + def.name, - url: 'Set ' + def.description, - } - ]; - }); - let flatten = items.reduce((acc, val) => acc.concat(val), []); - flatten = flatten.filter((item) => { - return item.caption.startsWith(keywords); - }); - if (flatten.length === 0) { - return Promise.resolve([]); - } - return Promise.resolve( - [{ name: 'Properties', items: flatten }], - ); - } - - async queryTabs( - name: string, excludePinned: boolean, args: string, - ): Promise { - const tabs = await this.completionsRepository.queryTabs(args, excludePinned); - const items = tabs.map(tab => ({ - caption: tab.title, - content: name + ' ' + tab.title, - url: tab.url, - icon: tab.favIconUrl - })); - if (items.length === 0) { - return Promise.resolve([]); - } - return [{ name: 'Buffers', items }]; - } - - async querySearchEngineItems(name: string, query: string) { - const engines = await this.completionUseCase.requestSearchEngines(query); - return engines.map(item => ({ - caption: item, - content: name + ' ' + item, - })); - } - - async queryHistoryItems(name: string, query: string) { - const items = await this.completionUseCase.requestHistory(query); - return items.map(item => ({ - caption: item.title, - content: name + ' ' + item.url, - url: item.url - })); - } - - async queryBookmarkItems(name: string, query: string) { - const items = await this.completionUseCase.requestHistory(query); - return items.map(item => ({ - caption: item.title, - content: name + ' ' + item.url, - url: item.url - })); - } -} diff --git a/src/console/actions/console.ts b/src/console/actions/console.ts index b17754d..e44c974 100644 --- a/src/console/actions/console.ts +++ b/src/console/actions/console.ts @@ -232,18 +232,6 @@ const getPropertyCompletions = async( } }; -const getCompletions = async(text: string): Promise => { - const completions = await browser.runtime.sendMessage({ - type: messages.CONSOLE_QUERY_COMPLETIONS, - text, - }); - return { - type: actions.CONSOLE_SET_COMPLETIONS, - completions, - completionSource: text, - }; -}; - const completionNext = (): actions.CompletionNextAction => { return { type: actions.CONSOLE_COMPLETION_NEXT, @@ -258,6 +246,6 @@ const completionPrev = (): actions.CompletionPrevAction => { export { hide, showCommand, showFind, showError, showInfo, hideCommand, setConsoleText, enterCommand, enterFind, - getCompletions, getCommandCompletions, getOpenCompletions, getTabCompletions, getPropertyCompletions, + getCommandCompletions, getOpenCompletions, getTabCompletions, getPropertyCompletions, completionNext, completionPrev, }; diff --git a/src/console/components/Console.tsx b/src/console/components/Console.tsx index 77f1b09..3fe5cee 100644 --- a/src/console/components/Console.tsx +++ b/src/console/components/Console.tsx @@ -175,22 +175,16 @@ class Console extends React.Component { this.props.dispatch(consoleActions.getTabCompletions(text, cmd.command, cmd.args, false)); break; case Command.BufferDelete: - this.props.dispatch(consoleActions.getTabCompletions(text, cmd.command, cmd.args, true)); - break; - case Command.BufferDeleteForce: - this.props.dispatch(consoleActions.getTabCompletions(text, cmd.command, cmd.args, false)); - break; case Command.BuffersDelete: this.props.dispatch(consoleActions.getTabCompletions(text, cmd.command, cmd.args, true)); break; + case Command.BufferDeleteForce: case Command.BuffersDeleteForce: this.props.dispatch(consoleActions.getTabCompletions(text, cmd.command, cmd.args, false)); break; case Command.Set: this.props.dispatch(consoleActions.getPropertyCompletions(text, cmd.command, cmd.args)); break; - default: - this.props.dispatch(consoleActions.getCompletions(text)); } } } diff --git a/src/shared/messages.ts b/src/shared/messages.ts index 577c34c..edb7935 100644 --- a/src/shared/messages.ts +++ b/src/shared/messages.ts @@ -7,7 +7,6 @@ export const BACKGROUND_OPERATION = 'background.operation'; export const CONSOLE_UNFOCUS = 'console.unfocus'; export const CONSOLE_ENTER_COMMAND = 'console.enter.command'; export const CONSOLE_ENTER_FIND = 'console.enter.find'; -export const CONSOLE_QUERY_COMPLETIONS = 'console.query.completions'; // DEPRECATED export const CONSOLE_SHOW_COMMAND = 'console.show.command'; export const CONSOLE_SHOW_ERROR = 'console.show.error'; export const CONSOLE_SHOW_INFO = 'console.show.info'; @@ -75,11 +74,6 @@ export interface ConsoleEnterFindMessage { text?: string; } -export interface ConsoleQueryCompletionsMessage { - type: typeof CONSOLE_QUERY_COMPLETIONS; - text: string; -} - export interface ConsoleShowCommandMessage { type: typeof CONSOLE_SHOW_COMMAND; command: string; @@ -297,7 +291,6 @@ export type Message = ConsoleUnfocusMessage | ConsoleEnterCommandMessage | ConsoleEnterFindMessage | - ConsoleQueryCompletionsMessage | ConsoleShowCommandMessage | ConsoleShowErrorMessage | ConsoleShowInfoMessage | @@ -342,7 +335,6 @@ export const valueOf = (o: any): Message => { case CONSOLE_UNFOCUS: case CONSOLE_ENTER_COMMAND: case CONSOLE_ENTER_FIND: - case CONSOLE_QUERY_COMPLETIONS: case CONSOLE_SHOW_COMMAND: case CONSOLE_SHOW_ERROR: case CONSOLE_SHOW_INFO: -- cgit v1.2.3