From c4afd7237b7720acbf642fc4c6eb529420295dcd Mon Sep 17 00:00:00 2001 From: Shin'ya Ueoka Date: Mon, 23 Jul 2018 21:26:47 +0900 Subject: [PATCH] [wip] implement command usecases --- src/background/controllers/command.js | 89 ++++++++++++++ src/background/controllers/completions.js | 43 ------- .../content-message-listener.js | 13 +- src/background/presenters/bookmark.js | 0 src/background/presenters/console.js | 16 +++ src/background/presenters/tab.js | 45 ++++++- src/background/presenters/window.js | 5 + src/background/repositories/bookmark.js | 13 ++ src/background/usecases/command.js | 114 ++++++++++++++++++ src/background/usecases/completions.js | 2 +- 10 files changed, 291 insertions(+), 49 deletions(-) create mode 100644 src/background/controllers/command.js delete mode 100644 src/background/controllers/completions.js create mode 100644 src/background/presenters/bookmark.js create mode 100644 src/background/presenters/console.js create mode 100644 src/background/presenters/window.js create mode 100644 src/background/repositories/bookmark.js create mode 100644 src/background/usecases/command.js diff --git a/src/background/controllers/command.js b/src/background/controllers/command.js new file mode 100644 index 0000000..41057e0 --- /dev/null +++ b/src/background/controllers/command.js @@ -0,0 +1,89 @@ +import CompletionsInteractor from '../usecases/completions'; +import CommandInteractor from '../usecases/command'; +import Completions from '../domains/completions'; + +export default class CommandController { + constructor() { + this.completionsInteractor = new CompletionsInteractor(); + this.commandIndicator = new CommandInteractor(); + } + + getCompletions(line) { + let trimmed = line.trimStart(); + let words = trimmed.split(/ +/); + let name = words[0]; + if (words.length === 1) { + return this.completionsInteractor.queryConsoleCommand(name); + } + let keywords = trimmed.slice(name.length).trimStart(); + switch (words[0]) { + case 'o': + case 'open': + case 't': + case 'tabopen': + case 'w': + case 'winopen': + return this.completionsInteractor.queryOpen(name, keywords); + case 'b': + case 'buffer': + return this.completionsInteractor.queryBuffer(name, keywords); + case 'bd': + case 'bdel': + case 'bdelete': + case 'bdeletes': + return this.completionsInteractor.queryBdelete(name, keywords); + case 'bd!': + case 'bdel!': + case 'bdelete!': + case 'bdeletes!': + return this.completionsInteractor.queryBdeleteForce(name, keywords); + case 'set': + return this.completionsInteractor.querySet(name, keywords); + } + return Promise.resolve(Completions.empty()); + } + + // eslint-disable-next-line complexity + exec(line) { + let trimmed = line.trimStart(); + let words = trimmed.split(/ +/); + let name = words[0]; + let keywords = trimmed.slice(name.length).trimStart(); + switch (words[0]) { + case 'o': + case 'open': + return this.commandIndicator.open(keywords); + case 't': + case 'tabopen': + return this.commandIndicator.tabopen(keywords); + case 'w': + case 'winopen': + return this.commandIndicator.winopen(keywords); + case 'b': + case 'buffer': + return this.commandIndicator.buffer(keywords); + case 'bd': + case 'bdel': + case 'bdelete': + return this.commandIndicator.bdelete(false, keywords); + case 'bd!': + case 'bdel!': + case 'bdelete!': + return this.commandIndicator.bdelete(true, keywords); + case 'bdeletes': + return this.commandIndicator.bdeletes(false, keywords); + case 'bdeletes!': + return this.commandIndicator.bdeletes(true, keywords); + case 'addbookmark': + return this.commandIndicator.addbookmark(keywords); + case 'q': + case 'quit': + return this.commandIndicator.quit(); + case 'qa': + case 'quitall': + return this.commandIndicator.quitAll(); + case 'set': + return this.commandIndicator.set(keywords); + } + } +} diff --git a/src/background/controllers/completions.js b/src/background/controllers/completions.js deleted file mode 100644 index f8eade9..0000000 --- a/src/background/controllers/completions.js +++ /dev/null @@ -1,43 +0,0 @@ -import CompletionsInteractor from '../usecases/completions'; -import Completions from '../domains/completions'; - -export default class ContentMessageController { - constructor() { - this.completionsInteractor = new CompletionsInteractor(); - } - - getCompletions(line) { - let trimmed = line.trimStart(); - let words = trimmed.split(/ +/); - let name = words[0]; - if (words.length === 1) { - return this.completionsInteractor.queryConsoleCommand(name); - } - let keywords = trimmed.slice(name.length).trimStart(); - switch (words[0]) { - case 'o': - case 'open': - case 't': - case 'tabopen': - case 'w': - case 'winopen': - return this.completionsInteractor.queryOpen(name, keywords); - case 'b': - case 'buffer': - return this.completionsInteractor.queryBuffer(name, keywords); - case 'bd': - case 'bdel': - case 'bdelete': - case 'bdeletes': - return this.completionsInteractor.queryBdelete(name, keywords); - case 'bd!': - case 'bdel!': - case 'bdelete!': - case 'bdeletes!': - return this.completionsInteractor.queryBdeleteForce(name, keywords); - case 'set': - return this.completionsInteractor.querySet(name, keywords); - } - return Promise.resolve(Completions.empty()); - } -} diff --git a/src/background/infrastructures/content-message-listener.js b/src/background/infrastructures/content-message-listener.js index f16804f..2e84fcc 100644 --- a/src/background/infrastructures/content-message-listener.js +++ b/src/background/infrastructures/content-message-listener.js @@ -1,5 +1,5 @@ import messages from '../../shared/messages'; -import CompletionsController from '../controllers/completions'; +import CommandController from '../controllers/command'; import SettingController from '../controllers/setting'; import FindController from '../controllers/find'; import AddonEnabledController from '../controllers/addon-enabled'; @@ -8,7 +8,7 @@ import LinkController from '../controllers/link'; export default class ContentMessageListener { constructor() { this.settingController = new SettingController(); - this.completionsController = new CompletionsController(); + this.commandController = new CommandController(); this.findController = new FindController(); this.addonEnabledController = new AddonEnabledController(); this.linkController = new LinkController(); @@ -31,6 +31,8 @@ export default class ContentMessageListener { switch (message.type) { case messages.CONSOLE_QUERY_COMPLETIONS: return this.onConsoleQueryCompletions(message.text); + case messages.CONSOLE_ENTER_COMMAND: + return this.onConsoleEnterCommand(message.text); case messages.SETTINGS_QUERY: return this.onSettingsQuery(); case messages.SETTINGS_RELOAD: @@ -48,10 +50,15 @@ export default class ContentMessageListener { } async onConsoleQueryCompletions(line) { - let completions = await this.completionsController.getCompletions(line); + let completions = await this.commandController.getCompletions(line); return Promise.resolve(completions.serialize()); } + onConsoleEnterCommand(text) { + return this.commandController.exec(text); + } + + onSettingsQuery() { return this.settingController.getSetting(); } diff --git a/src/background/presenters/bookmark.js b/src/background/presenters/bookmark.js new file mode 100644 index 0000000..e69de29 diff --git a/src/background/presenters/console.js b/src/background/presenters/console.js new file mode 100644 index 0000000..f7d3777 --- /dev/null +++ b/src/background/presenters/console.js @@ -0,0 +1,16 @@ +import messages from '../../shared/messages'; + +export default class ConsolePresenter { + showInfo(tabId, message) { + return browser.tabs.sendMessage(tabId, { + type: messages.CONSOLE_SHOW_INFO, + text: message, + }); + } + showError(tabId, message) { + return browser.tabs.sendMessage(tabId, { + type: messages.CONSOLE_SHOW_ERROR, + text: message, + }); + } +} diff --git a/src/background/presenters/tab.js b/src/background/presenters/tab.js index 66a207f..be6955a 100644 --- a/src/background/presenters/tab.js +++ b/src/background/presenters/tab.js @@ -3,8 +3,49 @@ export default class TabPresenter { return browser.tabs.update(tabId, { url }); } - create(url, { openerTabId, active }) { - return browser.tabs.create({ url, openerTabId, active }); + create(url, opts) { + return browser.tabs.create({ url, ...opts }); + } + + async getCurrent() { + let tabs = await browser.tabs.query({ + active: true, currentWindow: true + }); + return tabs[0]; + } + + getAll() { + return browser.tabs.query({ currentWindow: true }); + } + + async getByKeyword(keyword, excludePinned = false) { + let tabs = await browser.tabs.query({ currentWindow: true }); + return tabs.filter((t) => { + return t.url.toLowerCase().includes(keyword.toLowerCase()) || + t.title && t.title.toLowerCase().includes(keyword.toLowerCase()); + }).filter((t) => { + return !(excludePinned && t.pinned); + }); + } + + select(tabId) { + return browser.tabs.update(tabId, { active: true }); + } + + async selectAt(index) { + let tabs = await browser.tabs.query({ currentWindow: true }); + if (tabs.length < 2) { + return; + } + if (index < 0 || tabs.length <= index) { + throw new RangeError(`tab ${index + 1} does not exist`); + } + let id = tabs[index].id; + return browser.tabs.update(id, { active: true }); + } + + remove(ids) { + return browser.tabs.remove(ids); } async createAdjacent(url, { openerTabId, active }) { diff --git a/src/background/presenters/window.js b/src/background/presenters/window.js new file mode 100644 index 0000000..a82c4a2 --- /dev/null +++ b/src/background/presenters/window.js @@ -0,0 +1,5 @@ +export default class WindowPresenter { + create(url) { + return browser.windows.create({ url }); + } +} diff --git a/src/background/repositories/bookmark.js b/src/background/repositories/bookmark.js new file mode 100644 index 0000000..99f7ec4 --- /dev/null +++ b/src/background/repositories/bookmark.js @@ -0,0 +1,13 @@ +export default class BookmarkRepository { + async create(title, url) { + let item = await browser.bookmarks.create({ + type: 'bookmark', + title, + url, + }); + if (!item) { + throw new Error('Could not create a bookmark'); + } + return item; + } +} diff --git a/src/background/usecases/command.js b/src/background/usecases/command.js new file mode 100644 index 0000000..1d4744e --- /dev/null +++ b/src/background/usecases/command.js @@ -0,0 +1,114 @@ +import TabPresenter from '../presenters/tab'; +import WindowPresenter from '../presenters/window'; +import SettingRepository from '../repositories/setting'; +import BookmarkRepository from '../repositories/bookmark'; +import ConsolePresenter from '../presenters/console'; + +export default class CommandIndicator { + constructor() { + this.tabPresenter = new TabPresenter(); + this.windowPresenter = new WindowPresenter(); + this.settingRepository = new SettingRepository(); + this.bookmarkRepository = new BookmarkRepository(); + this.consolePresenter = new ConsolePresenter(); + } + + async open(keywords) { + let url = await this.urlOrSearch(keywords); + return this.tabPresenter.open(url); + } + + async tabopen(keywords) { + let url = await this.urlOrSearch(keywords); + return this.tabPresenter.create(url); + } + + async winopen(keywords) { + let url = await this.urlOrSearch(keywords); + return this.windowPresenter.create(url); + } + + async buffer(keywords) { + if (keywords.length === 0) { + return; + } + if (!isNaN(keywords)) { + let index = parseInt(keywords, 10) - 1; + return tabs.selectAt(index); + } + + let current = await this.tabPresenter.getCurrent(); + let tabs = await this.tabPresenter.getByKeyword(keywords); + if (tabs.length === 0) { + throw new RangeError('No matching buffer for ' + keywords); + } + for (let tab of tabs) { + if (tab.index > current.index) { + return this.tabPresenter.select(tab.id); + } + } + return this.tabPresenter.select(tabs[0].id); + } + + async bdelete(force, keywords) { + let excludePinned = !force; + let tabs = await this.tabPresenter.getByKeyword(keywords, excludePinned); + if (tabs.length === 0) { + throw new Error('No matching buffer for ' + keywords); + } else if (tabs.length > 1) { + throw new Error('More than one match for ' + keywords); + } + return this.tabPresenter.remove([tabs[0].id]); + } + + async bdeletes(force, keywords) { + let excludePinned = !force; + let tabs = await this.tabPresenter.getByKeyword(keywords, excludePinned); + let ids = tabs.map(tab => tab.id); + return this.tabPresenter.remove(ids); + } + + async quit() { + let tab = await this.tabPresenter.getCurrent(); + return this.tabPresenter.remove([tab.id]); + } + + async quitall() { + let tabs = await this.tabPresenter.getAll(); + let ids = tabs.map(tab => tab.id); + this.tabPresenter.tabPresenter.remove(ids); + } + + async addbookmark(title) { + let tab = await this.tabPresenter.getCurrent(); + let item = await this.bookmarkRepository.create(title, tab.url); + let message = 'Saved current page: ' + item.url; + return this.consolePresenter.showInfo(tab.id, message); + } + + set(keywords) { + // TODO implement set command + } + + async urlOrSearch(keywords) { + try { + return new URL(keywords).href; + } catch (e) { + if (keywords.includes('.') && !keywords.includes(' ')) { + return 'http://' + keywords; + } + let settings = await this.settingRepository.get(); + let engines = settings.search.engines; + + let template = engines[settings.search.default]; + let query = keywords; + + let first = keywords.trimStart().split(' ')[0]; + if (Object.keys(engines).includes(first)) { + template = engines[first]; + query = keywords.trimStart().slice(first.length).trimStart(); + } + return template.replace('{}', encodeURIComponent(query)); + } + } +} diff --git a/src/background/usecases/completions.js b/src/background/usecases/completions.js index 18174bd..ee519e1 100644 --- a/src/background/usecases/completions.js +++ b/src/background/usecases/completions.js @@ -50,7 +50,7 @@ export default class CompletionsInteractor { } queryBuffer(name, keywords) { - return this.queryTabs(name, true, keywords); + return this.queryTabs(name, false, keywords); } queryBdelete(name, keywords) {