diff --git a/src/background/completion/TabCompletionUseCase.ts b/src/background/completion/TabCompletionUseCase.ts index d0d3bb7f..6816faed 100644 --- a/src/background/completion/TabCompletionUseCase.ts +++ b/src/background/completion/TabCompletionUseCase.ts @@ -11,16 +11,21 @@ export default class TabCompletionUseCase { @inject("TabPresenter") private tabPresenter: TabPresenter ) {} - async queryTabs(query: string, excludePinned: boolean): Promise { + async queryTabs(query: string, excludePinned: boolean, onlyCurrentWin: boolean): Promise { + const multiIndex = (t: Tab) => t.index + 1 + parseFloat('0.' + t.windowId + '1') const lastTabId = await this.tabPresenter.getLastSelectedId(); - const allTabs = await this.tabRepository.getAllTabs(excludePinned); - const num = parseInt(query, 10); + const allTabs = await this.tabRepository.getAllTabs(excludePinned, onlyCurrentWin); + const num = parseFloat(query); let tabs: Tab[] = []; if (!isNaN(num)) { - const tab = allTabs.find((t) => t.index === num - 1); - if (tab) { - tabs = [tab]; + let tab = allTabs.find((t) => t.index === num - 1); + if (!tab) { + tab = allTabs.find((t) => multiIndex(t) === num); } + if(tab) { + tabs = [tab] + } + } else if (query == "%") { const tab = allTabs.find((t) => t.active); if (tab) { @@ -32,8 +37,10 @@ export default class TabCompletionUseCase { tabs = [tab]; } } else { - tabs = await this.tabRepository.queryTabs(query, excludePinned); + tabs = await this.tabRepository.queryTabs(query, excludePinned, onlyCurrentWin); } + const winSet = tabs.reduce( (wins, tab) => wins.add(tab.windowId), new Set()); + const multiWin = winSet.size > 1 return tabs.map((tab) => { let flag = TabFlag.None; @@ -43,7 +50,7 @@ export default class TabCompletionUseCase { flag = TabFlag.LastTab; } return { - index: tab.index + 1, + index: tab.index + 1 + (multiWin ? parseFloat('0.' + tab.windowId + '1') : 0), flag: flag, title: tab.title, url: tab.url, diff --git a/src/background/completion/TabRepository.ts b/src/background/completion/TabRepository.ts index 57dbf21b..0e61d877 100644 --- a/src/background/completion/TabRepository.ts +++ b/src/background/completion/TabRepository.ts @@ -5,10 +5,11 @@ export type Tab = { title: string; url: string; faviconUrl?: string; + windowId: number; }; export default interface TabRepository { - queryTabs(query: string, excludePinned: boolean): Promise; + queryTabs(query: string, excludePinned: boolean, onlyCurrentWin: boolean): Promise; - getAllTabs(excludePinned: boolean): Promise; + getAllTabs(excludePinned: boolean, onlyCurrentWin: boolean): Promise; } diff --git a/src/background/completion/impl/TabRepositoryImpl.ts b/src/background/completion/impl/TabRepositoryImpl.ts index 5e33e5af..6aa61dc1 100644 --- a/src/background/completion/impl/TabRepositoryImpl.ts +++ b/src/background/completion/impl/TabRepositoryImpl.ts @@ -1,8 +1,11 @@ import TabRepository, { Tab } from "../TabRepository"; export default class TabRepositoryImpl implements TabRepository { - async queryTabs(query: string, excludePinned: boolean): Promise { - const tabs = await browser.tabs.query({ currentWindow: true }); + async queryTabs(query: string, excludePinned: boolean, onlyCurrentWin: boolean): Promise { + const tabs = [ + ...await browser.tabs.query({ currentWindow: true }), + ...(onlyCurrentWin ? [] : await browser.tabs.query({ currentWindow: false })) + ] return tabs .filter((t) => { return ( @@ -17,13 +20,19 @@ export default class TabRepositoryImpl implements TabRepository { .map(TabRepositoryImpl.toEntity); } - async getAllTabs(excludePinned: boolean): Promise { + async getAllTabs(excludePinned: boolean, onlyCurrentWin: boolean): Promise { if (excludePinned) { return ( - await browser.tabs.query({ currentWindow: true, pinned: true }) + [ + ...await browser.tabs.query({ currentWindow: true, pinned: true }), + ...(onlyCurrentWin ? [] : await browser.tabs.query({ currentWindow: false, pinned: true })) + ] ).map(TabRepositoryImpl.toEntity); } - return (await browser.tabs.query({ currentWindow: true })).map( + return [ + ...await browser.tabs.query({ currentWindow: true }), + ...(onlyCurrentWin ? [] : await browser.tabs.query({ currentWindow: false })) + ].map( TabRepositoryImpl.toEntity ); } @@ -36,6 +45,7 @@ export default class TabRepositoryImpl implements TabRepository { title: tab.title!!, faviconUrl: tab.favIconUrl, index: tab.index, + windowId: tab.windowId, }; } } diff --git a/src/background/controllers/CompletionController.ts b/src/background/controllers/CompletionController.ts index b00a0cb9..7afda7cd 100644 --- a/src/background/controllers/CompletionController.ts +++ b/src/background/controllers/CompletionController.ts @@ -42,9 +42,10 @@ export default class CompletionController { async queryTabs( query: string, - excludePinned: boolean + excludePinned: boolean, + onlyCurrentWin: boolean ): Promise { - return this.tabCompletionUseCase.queryTabs(query, excludePinned); + return this.tabCompletionUseCase.queryTabs(query, excludePinned, onlyCurrentWin); } async getProperties(): Promise { diff --git a/src/background/infrastructures/ContentMessageListener.ts b/src/background/infrastructures/ContentMessageListener.ts index 6978d359..0ce5444f 100644 --- a/src/background/infrastructures/ContentMessageListener.ts +++ b/src/background/infrastructures/ContentMessageListener.ts @@ -74,7 +74,8 @@ export default class ContentMessageListener { case messages.CONSOLE_REQUEST_TABS: return this.completionController.queryTabs( message.query, - message.excludePinned + message.excludePinned, + message.onlyCurrentWin ); case messages.CONSOLE_GET_PROPERTIES: return this.completionController.getProperties(); diff --git a/src/background/presenters/TabPresenter.ts b/src/background/presenters/TabPresenter.ts index 09cfa23c..c85c9874 100644 --- a/src/background/presenters/TabPresenter.ts +++ b/src/background/presenters/TabPresenter.ts @@ -12,11 +12,11 @@ export default interface TabPresenter { getCurrent(): Promise; - getAll(): Promise; + getAll(onlyCurrentWin?: boolean): Promise; getLastSelectedId(): Promise; - getByKeyword(keyword: string, excludePinned: boolean): Promise; + getByKeyword(keyword: string, excludePinned: boolean, onlyCurrentWin?: boolean): Promise; select(tabId: number): Promise; @@ -56,8 +56,11 @@ export class TabPresenterImpl implements TabPresenter { return tabs[0]; } - getAll(): Promise { - return browser.tabs.query({ currentWindow: true }); + getAll(onlyCurrentWin: boolean = true): Promise { + return browser.tabs.query({ currentWindow: true }) + .then( res => + (onlyCurrentWin ? res : browser.tabs.query({ currentWindow: false }) + .then( res2 => [...res, ...res2]))) } async getLastSelectedId(): Promise { @@ -69,8 +72,11 @@ export class TabPresenterImpl implements TabPresenter { return tabId; } - async getByKeyword(keyword: string, excludePinned = false): Promise { - const tabs = await browser.tabs.query({ currentWindow: true }); + async getByKeyword(keyword: string, excludePinned = false, onlyCurrentWin: boolean = true): Promise { + const tabs = [ + ...await browser.tabs.query({ currentWindow: true }), + ...(onlyCurrentWin ? [] : await browser.tabs.query({ currentWindow: false })) + ]; return tabs .filter((t) => { return ( @@ -85,6 +91,10 @@ export class TabPresenterImpl implements TabPresenter { async select(tabId: number): Promise { await browser.tabs.update(tabId, { active: true }); + const windowId = (await browser.tabs.get(tabId)).windowId; + if((await browser.windows.getCurrent()).id !== windowId) { + await browser.windows.update(windowId, {focused: true}) + } } async remove(ids: number[]): Promise { diff --git a/src/background/repositories/CachedSettingRepository.ts b/src/background/repositories/CachedSettingRepository.ts index b4cdd1c0..7c4dd062 100644 --- a/src/background/repositories/CachedSettingRepository.ts +++ b/src/background/repositories/CachedSettingRepository.ts @@ -69,6 +69,10 @@ export class CachedSettingRepositoryImpl implements CachedSettingRepository { } break; } + case "searchOnlyCurrentWin": + current.properties.searchOnlyCurrentWin = newValue as boolean; + break; + } await this.update(current); } diff --git a/src/background/usecases/CommandUseCase.ts b/src/background/usecases/CommandUseCase.ts index 811ec773..9ac3ea7b 100644 --- a/src/background/usecases/CommandUseCase.ts +++ b/src/background/usecases/CommandUseCase.ts @@ -60,8 +60,10 @@ export default class CommandIndicator { return; } + const settings = await this.cachedSettingRepository.get(); + const onlyCurrentWin = settings.properties.searchOnlyCurrentWin; if (!isNaN(Number(keywords))) { - const tabs = await this.tabPresenter.getAll(); + const tabs = await this.tabPresenter.getAll(onlyCurrentWin); const index = parseInt(keywords, 10) - 1; if (index < 0 || tabs.length <= index) { throw new RangeError(`tab ${index + 1} does not exist`); @@ -80,7 +82,7 @@ export default class CommandIndicator { } const current = await this.tabPresenter.getCurrent(); - const tabs = await this.tabPresenter.getByKeyword(keywords, false); + const tabs = await this.tabPresenter.getByKeyword(keywords, false, onlyCurrentWin); if (tabs.length === 0) { throw new RangeError("No matching buffer for " + keywords); } diff --git a/src/console/actions/console.ts b/src/console/actions/console.ts index 16d33b30..ae811b2d 100644 --- a/src/console/actions/console.ts +++ b/src/console/actions/console.ts @@ -29,6 +29,7 @@ const propertyDocs: { [key: string]: string } = { smoothscroll: "smooth scroll", complete: "which are completed at the open page", colorscheme: "color scheme of the console", + searchOnlyCurrentWin: "buffer switch and tab completion only in current browser window" }; const hide = (): actions.ConsoleAction => { @@ -194,9 +195,10 @@ const getTabCompletions = async ( original: string, command: Command, query: string, - excludePinned: boolean + excludePinned: boolean, ): Promise => { - const items = await completionClient.requestTabs(query, excludePinned); + const onlyCurrentWin = await settingClient.shouldSearchOnlyCurrentWin(); + const items = await completionClient.requestTabs(query, excludePinned, onlyCurrentWin); let completions: Completions = []; if (items.length > 0) { completions = [ diff --git a/src/console/clients/CompletionClient.ts b/src/console/clients/CompletionClient.ts index a80918bc..1dd61ee0 100644 --- a/src/console/clients/CompletionClient.ts +++ b/src/console/clients/CompletionClient.ts @@ -69,11 +69,12 @@ export default class CompletionClient { return resp; } - async requestTabs(query: string, excludePinned: boolean): Promise { + async requestTabs(query: string, excludePinned: boolean, onlyCurrentWin: boolean): Promise { const resp = (await browser.runtime.sendMessage({ type: messages.CONSOLE_REQUEST_TABS, query, excludePinned, + onlyCurrentWin })) as ConsoleRequestTabsResponse; return resp; } diff --git a/src/console/clients/SettingClient.ts b/src/console/clients/SettingClient.ts index f75517a2..be7cedf2 100644 --- a/src/console/clients/SettingClient.ts +++ b/src/console/clients/SettingClient.ts @@ -1,13 +1,23 @@ import Settings from "../../shared/settings/Settings"; import * as messages from "../../shared/messages"; import ColorScheme from "../../shared/ColorScheme"; +import Properties from "../../shared/settings/Properties"; export default class SettingClient { - async getColorScheme(): Promise { + + private async getProperties(): Promise { const json = await browser.runtime.sendMessage({ type: messages.SETTINGS_QUERY, }); const settings = Settings.fromJSON(json); - return settings.properties.colorscheme; + return settings.properties; + } + + async getColorScheme(): Promise { + return (await this.getProperties()).colorscheme; + } + + async shouldSearchOnlyCurrentWin(): Promise { + return (await this.getProperties()).searchOnlyCurrentWin; } } diff --git a/src/shared/messages.ts b/src/shared/messages.ts index 60d4c9ed..c31a8915 100644 --- a/src/shared/messages.ts +++ b/src/shared/messages.ts @@ -121,6 +121,7 @@ export interface ConsoleRequestTabsMessage { type: typeof CONSOLE_REQUEST_TABS; query: string; excludePinned: boolean; + onlyCurrentWin: boolean; } export interface ConsoleGetPropertiesMessage { diff --git a/src/shared/settings/Properties.ts b/src/shared/settings/Properties.ts index 7540c8a1..8202c5d0 100644 --- a/src/shared/settings/Properties.ts +++ b/src/shared/settings/Properties.ts @@ -5,6 +5,7 @@ export type PropertiesJSON = { smoothscroll?: boolean; complete?: string; colorscheme?: ColorScheme; + searchOnlyCurrentWin?: boolean; }; export type PropertyTypes = { @@ -12,9 +13,10 @@ export type PropertyTypes = { smoothscroll: string; complete: string; colorscheme: string; + searchOnlyCurrentWin: string; }; -type PropertyName = "hintchars" | "smoothscroll" | "complete" | "colorscheme"; +type PropertyName = "hintchars" | "smoothscroll" | "complete" | "colorscheme" | "searchOnlyCurrentWin"; type PropertyDef = { name: PropertyName; @@ -43,6 +45,11 @@ const defs: PropertyDef[] = [ defaultValue: ColorScheme.System, type: "string", }, + { + name: "searchOnlyCurrentWin", + defaultValue: true, + type: "boolean", + }, ]; const defaultValues = { @@ -50,6 +57,7 @@ const defaultValues = { smoothscroll: false, complete: "sbh", colorscheme: ColorScheme.System, + searchOnlyCurrentWin: false, }; export default class Properties { @@ -61,21 +69,26 @@ export default class Properties { public colorscheme: ColorScheme; + public searchOnlyCurrentWin: boolean; + constructor({ hintchars, smoothscroll, complete, colorscheme, + searchOnlyCurrentWin, }: { hintchars?: string; smoothscroll?: boolean; complete?: string; colorscheme?: ColorScheme; + searchOnlyCurrentWin?: boolean; } = {}) { this.hintchars = hintchars || defaultValues.hintchars; this.smoothscroll = smoothscroll || defaultValues.smoothscroll; this.complete = complete || defaultValues.complete; this.colorscheme = colorscheme || defaultValues.colorscheme; + this.searchOnlyCurrentWin = searchOnlyCurrentWin || defaultValues.searchOnlyCurrentWin; } static fromJSON(json: PropertiesJSON): Properties { @@ -88,6 +101,7 @@ export default class Properties { smoothscroll: "boolean", complete: "string", colorscheme: "string", + searchOnlyCurrentWin: "boolean", }; } @@ -105,6 +119,7 @@ export default class Properties { smoothscroll: this.smoothscroll, complete: this.complete, colorscheme: this.colorscheme, + searchOnlyCurrentWin: this.searchOnlyCurrentWin, }; } } diff --git a/src/shared/settings/Settings.ts b/src/shared/settings/Settings.ts index 6f178ea9..919c49c9 100644 --- a/src/shared/settings/Settings.ts +++ b/src/shared/settings/Settings.ts @@ -157,7 +157,8 @@ export const DefaultSettingJSONText = `{ "hintchars": "abcdefghijklmnopqrstuvwxyz", "smoothscroll": false, "complete": "sbh", - "colorscheme": "system" + "colorscheme": "system", + "searchOnlyCurrentWin": false }, "blacklist": [ ] diff --git a/src/shared/settings/schema.json b/src/shared/settings/schema.json index be0f2ec6..ae3717a2 100644 --- a/src/shared/settings/schema.json +++ b/src/shared/settings/schema.json @@ -52,6 +52,9 @@ "colorscheme": { "type": "string", "enum": ["system", "light", "dark"] + }, + "searchOnlyCurrentWin": { + "type": "boolean" } } }, diff --git a/src/shared/settings/validate.js b/src/shared/settings/validate.js index 30f78885..4b5ded39 100644 --- a/src/shared/settings/validate.js +++ b/src/shared/settings/validate.js @@ -313,6 +313,26 @@ var validate = (function() { } var valid2 = errors === errs_2; } + if (valid2) { + if (data1.searchOnlyCurrentWin === undefined) { + valid2 = true; + } else { + var errs_2 = errors; + if (typeof data1.searchOnlyCurrentWin !== "boolean") { + validate.errors = [{ + keyword: 'type', + dataPath: (dataPath || '') + '.properties.searchOnlyCurrentWin', + schemaPath: '#/properties/properties/properties/searchOnlyCurrentWin/type', + params: { + type: 'boolean' + }, + message: 'should be boolean' + }]; + return false; + } + var valid2 = errors === errs_2; + } + } } } } @@ -580,6 +600,9 @@ validate.schema = { "colorscheme": { "type": "string", "enum": ["system", "light", "dark"] + }, + "searchOnlyCurrentWin": { + "type": "boolean" } } },