From de4e651196502cffa56cedfdaf84d9bb665559e1 Mon Sep 17 00:00:00 2001 From: Shin'ya Ueoka Date: Sun, 4 Apr 2021 11:01:40 +0900 Subject: Replace Console component with a React Hooks --- src/console/index.tsx | 79 +++++++++++++++++++++++++++++++-------------------- 1 file changed, 48 insertions(+), 31 deletions(-) (limited to 'src/console/index.tsx') diff --git a/src/console/index.tsx b/src/console/index.tsx index f9313a0..cf9367b 100644 --- a/src/console/index.tsx +++ b/src/console/index.tsx @@ -1,42 +1,59 @@ import * as messages from "../shared/messages"; -import reducers from "./reducers"; -import { createStore, applyMiddleware } from "redux"; -import promise from "redux-promise"; +import reducers, { defaultState } from "./reducers"; import * as consoleActions from "./actions/console"; -import { Provider } from "react-redux"; import Console from "./components/Console"; +import AppContext from "./components/AppContext"; import "./index.css"; import React from "react"; import ReactDOM from "react-dom"; -const store = createStore(reducers, applyMiddleware(promise)); +const wrapAsync = ( + dispatch: React.Dispatch +): React.Dispatch> => { + return (action: T | Promise) => { + if (action instanceof Promise) { + action.then((a) => dispatch(a)).catch(console.error); + } else { + dispatch(action); + } + }; +}; -window.addEventListener("DOMContentLoaded", () => { - const wrapper = document.getElementById("vimvixen-console"); - ReactDOM.render( - - - , - wrapper - ); -}); +const RootComponent: React.FC = () => { + const [state, dispatch] = React.useReducer(reducers, defaultState); + + React.useEffect(() => { + const onMessage = async (message: any): Promise => { + const msg = messages.valueOf(message); + switch (msg.type) { + case messages.CONSOLE_SHOW_COMMAND: + return dispatch(await consoleActions.showCommand(msg.command)); + case messages.CONSOLE_SHOW_FIND: + return dispatch(consoleActions.showFind()); + case messages.CONSOLE_SHOW_ERROR: + return dispatch(consoleActions.showError(msg.text)); + case messages.CONSOLE_SHOW_INFO: + return dispatch(consoleActions.showInfo(msg.text)); + case messages.CONSOLE_HIDE: + return dispatch(consoleActions.hide()); + } + }; -const onMessage = async (message: any): Promise => { - const msg = messages.valueOf(message); - switch (msg.type) { - case messages.CONSOLE_SHOW_COMMAND: - return store.dispatch(await consoleActions.showCommand(msg.command)); - case messages.CONSOLE_SHOW_FIND: - return store.dispatch(consoleActions.showFind()); - case messages.CONSOLE_SHOW_ERROR: - return store.dispatch(consoleActions.showError(msg.text)); - case messages.CONSOLE_SHOW_INFO: - return store.dispatch(consoleActions.showInfo(msg.text)); - case messages.CONSOLE_HIDE: - return store.dispatch(consoleActions.hide()); - } + browser.runtime.onMessage.addListener(onMessage); + const port = browser.runtime.connect(undefined, { + name: "vimvixen-console", + }); + port.onMessage.addListener(onMessage); + }, []); + + return ( + + + + ); }; -browser.runtime.onMessage.addListener(onMessage); -const port = browser.runtime.connect(undefined, { name: "vimvixen-console" }); -port.onMessage.addListener(onMessage); +window.addEventListener("DOMContentLoaded", () => { + const wrapper = document.getElementById("vimvixen-console"); + ReactDOM.render(, wrapper); +}); -- cgit v1.2.3 From 3a7e55fd292196f600c11fad36425014677a1351 Mon Sep 17 00:00:00 2001 From: Shin'ya Ueoka Date: Sun, 4 Apr 2021 21:34:13 +0900 Subject: Separate Command and Completion reducer --- src/console/actions/completion.ts | 243 +++++++++++++++++++++++++ src/console/actions/console.ts | 299 ++++++++----------------------- src/console/actions/index.ts | 80 --------- src/console/components/AppContext.ts | 4 +- src/console/components/CommandPrompt.tsx | 131 +++++++++----- src/console/components/Console.tsx | 1 - src/console/index.tsx | 2 +- src/console/reducers/completion.ts | 99 ++++++++++ src/console/reducers/console.ts | 64 +++++++ src/console/reducers/index.ts | 134 -------------- test/console/actions/completion.test.ts | 28 +++ test/console/actions/console.test.ts | 38 ++-- test/console/reducers/completion.test.ts | 102 +++++++++++ test/console/reducers/console.test.ts | 127 +++---------- 14 files changed, 734 insertions(+), 618 deletions(-) create mode 100644 src/console/actions/completion.ts delete mode 100644 src/console/actions/index.ts create mode 100644 src/console/reducers/completion.ts create mode 100644 src/console/reducers/console.ts delete mode 100644 src/console/reducers/index.ts create mode 100644 test/console/actions/completion.test.ts create mode 100644 test/console/reducers/completion.test.ts (limited to 'src/console/index.tsx') diff --git a/src/console/actions/completion.ts b/src/console/actions/completion.ts new file mode 100644 index 0000000..2f6f82f --- /dev/null +++ b/src/console/actions/completion.ts @@ -0,0 +1,243 @@ +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(); + +const commandDocs = { + [Command.Set]: "Set a value of the property", + [Command.Open]: "Open a URL or search by keywords in current tab", + [Command.TabOpen]: "Open a URL or search by keywords in new tab", + [Command.WindowOpen]: "Open a URL or search by keywords in new window", + [Command.Buffer]: "Select tabs by matched keywords", + [Command.BufferDelete]: "Close a certain tab matched by keywords", + [Command.BuffersDelete]: "Close all tabs matched by keywords", + [Command.Quit]: "Close the current tab", + [Command.QuitAll]: "Close all tabs", + [Command.AddBookmark]: "Add current page to bookmarks", + [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", + colorscheme: "color scheme of the console", +}; + +export const COMPLETION_START_COMPLETION = "console.start.completion"; +export const COMPLETION_SET_COMPLETIONS = "console.set.completions"; +export const COMPLETION_COMPLETION_NEXT = "completion.completion.next"; +export const COMPLETION_COMPLETION_PREV = "completion.completion.prev"; + +export interface CompletionStartCompletionAction { + type: typeof COMPLETION_START_COMPLETION; + completionTypes: CompletionType[]; +} + +export interface SetCompletionsAction { + type: typeof COMPLETION_SET_COMPLETIONS; + completions: Completions; + completionSource: string; +} + +export interface CompletionNextAction { + type: typeof COMPLETION_COMPLETION_NEXT; +} + +export interface CompletionPrevAction { + type: typeof COMPLETION_COMPLETION_PREV; +} + +export type CompletionAction = + | CompletionStartCompletionAction + | SetCompletionsAction + | CompletionNextAction + | CompletionPrevAction; +const startCompletion = async (): Promise => { + const completionTypes = await completionClient.getCompletionTypes(); + return { + type: COMPLETION_START_COMPLETION, + completionTypes, + }; +}; + +const getCommandCompletions = (text: string): SetCompletionsAction => { + const items = Object.entries(commandDocs) + .filter(([name]) => name.startsWith(text.trimLeft())) + .map(([name, doc]) => ({ + caption: name, + content: name, + url: doc, + })); + const completions = [ + { + name: "Console Command", + items, + }, + ]; + return { + type: COMPLETION_SET_COMPLETIONS, + completions, + completionSource: text, + }; +}; + +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: { + const items = await completionClient.requestSearchEngines(query); + if (items.length === 0) { + break; + } + completions.push({ + name: "Search Engines", + items: items.map((key) => ({ + caption: key.title, + content: command + " " + key.title, + })), + }); + break; + } + case CompletionType.History: { + const items = await completionClient.requestHistory(query); + if (items.length === 0) { + break; + } + completions.push({ + name: "History", + items: items.map((item) => ({ + caption: item.title, + content: command + " " + item.url, + url: item.url, + })), + }); + break; + } + case CompletionType.Bookmarks: { + const items = await completionClient.requestBookmarks(query); + if (items.length === 0) { + break; + } + completions.push({ + name: "Bookmarks", + items: items.map((item) => ({ + caption: item.title, + content: command + " " + item.url, + url: item.url, + })), + }); + break; + } + } + } + + return { + type: COMPLETION_SET_COMPLETIONS, + completions, + completionSource: original, + }; +}; + +const getTabCompletions = async ( + original: string, + command: Command, + query: string, + excludePinned: boolean +): Promise => { + const items = await completionClient.requestTabs(query, excludePinned); + let completions: Completions = []; + if (items.length > 0) { + 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: COMPLETION_SET_COMPLETIONS, + completions, + completionSource: original, + }; +}; + +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: command + " " + 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: COMPLETION_SET_COMPLETIONS, + completions, + completionSource: original, + }; +}; + +const completionNext = (): CompletionNextAction => { + return { + type: COMPLETION_COMPLETION_NEXT, + }; +}; + +const completionPrev = (): CompletionPrevAction => { + return { + type: COMPLETION_COMPLETION_PREV, + }; +}; + +export { + startCompletion, + getCommandCompletions, + getOpenCompletions, + getTabCompletions, + getPropertyCompletions, + completionNext, + completionPrev, +}; diff --git a/src/console/actions/console.ts b/src/console/actions/console.ts index 16d33b3..646cc31 100644 --- a/src/console/actions/console.ts +++ b/src/console/actions/console.ts @@ -1,72 +1,99 @@ import * as messages from "../../shared/messages"; -import * as actions from "./index"; -import { Command } from "../../shared/Command"; -import CompletionClient from "../clients/CompletionClient"; import SettingClient from "../clients/SettingClient"; -import CompletionType from "../../shared/CompletionType"; -import Completions from "../Completions"; -import TabFlag from "../../shared/TabFlag"; +import ColorScheme from "../../shared/ColorScheme"; -const completionClient = new CompletionClient(); const settingClient = new SettingClient(); -const commandDocs = { - [Command.Set]: "Set a value of the property", - [Command.Open]: "Open a URL or search by keywords in current tab", - [Command.TabOpen]: "Open a URL or search by keywords in new tab", - [Command.WindowOpen]: "Open a URL or search by keywords in new window", - [Command.Buffer]: "Select tabs by matched keywords", - [Command.BufferDelete]: "Close a certain tab matched by keywords", - [Command.BuffersDelete]: "Close all tabs matched by keywords", - [Command.Quit]: "Close the current tab", - [Command.QuitAll]: "Close all tabs", - [Command.AddBookmark]: "Add current page to bookmarks", - [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", - colorscheme: "color scheme of the console", -}; - -const hide = (): actions.ConsoleAction => { +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_HIDE_COMMAND = "console.hide.command"; +export const CONSOLE_SET_CONSOLE_TEXT = "console.set.command"; +export const CONSOLE_SHOW_FIND = "console.show.find"; +export const CONSOLE_SET_COLORSCHEME = "completion.set.colorscheme"; +export const CONSOLE_HIDE = "console.hide"; + +export interface HideAction { + type: typeof CONSOLE_HIDE; +} + +export interface ShowCommand { + type: typeof CONSOLE_SHOW_COMMAND; + text: string; +} + +export interface ShowFindAction { + type: typeof CONSOLE_SHOW_FIND; +} + +export interface ShowErrorAction { + type: typeof CONSOLE_SHOW_ERROR; + text: string; +} + +export interface ShowInfoAction { + type: typeof CONSOLE_SHOW_INFO; + text: string; +} + +export interface HideCommandAction { + type: typeof CONSOLE_HIDE_COMMAND; +} + +export interface SetConsoleTextAction { + type: typeof CONSOLE_SET_CONSOLE_TEXT; + consoleText: string; +} + +export interface SetColorSchemeAction { + type: typeof CONSOLE_SET_COLORSCHEME; + colorscheme: ColorScheme; +} + +export type ConsoleAction = + | HideAction + | ShowCommand + | ShowFindAction + | ShowErrorAction + | ShowInfoAction + | HideCommandAction + | SetConsoleTextAction + | SetColorSchemeAction; + +const hide = (): ConsoleAction => { return { - type: actions.CONSOLE_HIDE, + type: CONSOLE_HIDE, }; }; -const showCommand = async (text: string): Promise => { - const completionTypes = await completionClient.getCompletionTypes(); +const showCommand = (text: string): ShowCommand => { return { - type: actions.CONSOLE_SHOW_COMMAND, - completionTypes, + type: CONSOLE_SHOW_COMMAND, text, }; }; -const showFind = (): actions.ShowFindAction => { +const showFind = (): ShowFindAction => { return { - type: actions.CONSOLE_SHOW_FIND, + type: CONSOLE_SHOW_FIND, }; }; -const showError = (text: string): actions.ShowErrorAction => { +const showError = (text: string): ShowErrorAction => { return { - type: actions.CONSOLE_SHOW_ERROR, + type: CONSOLE_SHOW_ERROR, text: text, }; }; -const showInfo = (text: string): actions.ShowInfoAction => { +const showInfo = (text: string): ShowInfoAction => { return { - type: actions.CONSOLE_SHOW_INFO, + type: CONSOLE_SHOW_INFO, text: text, }; }; -const hideCommand = (): actions.HideCommandAction => { +const hideCommand = (): HideCommandAction => { window.top.postMessage( JSON.stringify({ type: messages.CONSOLE_UNFOCUS, @@ -74,13 +101,11 @@ const hideCommand = (): actions.HideCommandAction => { "*" ); return { - type: actions.CONSOLE_HIDE_COMMAND, + type: CONSOLE_HIDE_COMMAND, }; }; -const enterCommand = async ( - text: string -): Promise => { +const enterCommand = async (text: string): Promise => { await browser.runtime.sendMessage({ type: messages.CONSOLE_ENTER_COMMAND, text, @@ -88,7 +113,7 @@ const enterCommand = async ( return hideCommand(); }; -const enterFind = (text?: string): actions.HideCommandAction => { +const enterFind = (text?: string): HideCommandAction => { window.top.postMessage( JSON.stringify({ type: messages.CONSOLE_ENTER_FIND, @@ -99,185 +124,17 @@ const enterFind = (text?: string): actions.HideCommandAction => { return hideCommand(); }; -const setConsoleText = (consoleText: string): actions.SetConsoleTextAction => { +const setConsoleText = (consoleText: string): SetConsoleTextAction => { return { - type: actions.CONSOLE_SET_CONSOLE_TEXT, + type: CONSOLE_SET_CONSOLE_TEXT, consoleText, }; }; -const getCommandCompletions = (text: string): actions.SetCompletionsAction => { - const items = Object.entries(commandDocs) - .filter(([name]) => name.startsWith(text.trimLeft())) - .map(([name, doc]) => ({ - caption: name, - content: name, - url: doc, - })); - const completions = [ - { - name: "Console Command", - items, - }, - ]; - return { - type: actions.CONSOLE_SET_COMPLETIONS, - completions, - completionSource: text, - }; -}; - -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: { - const items = await completionClient.requestSearchEngines(query); - if (items.length === 0) { - break; - } - completions.push({ - name: "Search Engines", - items: items.map((key) => ({ - caption: key.title, - content: command + " " + key.title, - })), - }); - break; - } - case CompletionType.History: { - const items = await completionClient.requestHistory(query); - if (items.length === 0) { - break; - } - completions.push({ - name: "History", - items: items.map((item) => ({ - caption: item.title, - content: command + " " + item.url, - url: item.url, - })), - }); - break; - } - case CompletionType.Bookmarks: { - const items = await completionClient.requestBookmarks(query); - if (items.length === 0) { - break; - } - completions.push({ - name: "Bookmarks", - items: items.map((item) => ({ - caption: item.title, - content: command + " " + item.url, - url: item.url, - })), - }); - break; - } - } - } - - return { - type: actions.CONSOLE_SET_COMPLETIONS, - completions, - completionSource: original, - }; -}; - -const getTabCompletions = async ( - original: string, - command: Command, - query: string, - excludePinned: boolean -): Promise => { - const items = await completionClient.requestTabs(query, excludePinned); - let completions: Completions = []; - if (items.length > 0) { - 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 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: command + " " + 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 completionNext = (): actions.CompletionNextAction => { - return { - type: actions.CONSOLE_COMPLETION_NEXT, - }; -}; - -const completionPrev = (): actions.CompletionPrevAction => { - return { - type: actions.CONSOLE_COMPLETION_PREV, - }; -}; - -const setColorScheme = async (): Promise => { +const setColorScheme = async (): Promise => { const scheme = await settingClient.getColorScheme(); return { - type: actions.CONSOLE_SET_COLORSCHEME, + type: CONSOLE_SET_COLORSCHEME, colorscheme: scheme, }; }; @@ -292,11 +149,5 @@ export { setConsoleText, enterCommand, enterFind, - getCommandCompletions, - getOpenCompletions, - getTabCompletions, - getPropertyCompletions, - completionNext, - completionPrev, setColorScheme, }; diff --git a/src/console/actions/index.ts b/src/console/actions/index.ts deleted file mode 100644 index 6c1c759..0000000 --- a/src/console/actions/index.ts +++ /dev/null @@ -1,80 +0,0 @@ -import Completions from "../Completions"; -import CompletionType from "../../shared/CompletionType"; -import ColorScheme from "../../shared/ColorScheme"; - -export const CONSOLE_HIDE = "console.hide"; -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_HIDE_COMMAND = "console.hide.command"; -export const CONSOLE_SET_CONSOLE_TEXT = "console.set.command"; -export const CONSOLE_SET_COMPLETIONS = "console.set.completions"; -export const CONSOLE_COMPLETION_NEXT = "console.completion.next"; -export const CONSOLE_COMPLETION_PREV = "console.completion.prev"; -export const CONSOLE_SHOW_FIND = "console.show.find"; -export const CONSOLE_SET_COLORSCHEME = "console.set.colorscheme"; - -export interface HideAction { - type: typeof CONSOLE_HIDE; -} - -export interface ShowCommand { - type: typeof CONSOLE_SHOW_COMMAND; - text: string; - completionTypes: CompletionType[]; -} - -export interface ShowFindAction { - type: typeof CONSOLE_SHOW_FIND; -} - -export interface ShowErrorAction { - type: typeof CONSOLE_SHOW_ERROR; - text: string; -} - -export interface ShowInfoAction { - type: typeof CONSOLE_SHOW_INFO; - text: string; -} - -export interface HideCommandAction { - type: typeof CONSOLE_HIDE_COMMAND; -} - -export interface SetConsoleTextAction { - type: typeof CONSOLE_SET_CONSOLE_TEXT; - consoleText: string; -} - -export interface SetCompletionsAction { - type: typeof CONSOLE_SET_COMPLETIONS; - completions: Completions; - completionSource: string; -} - -export interface CompletionNextAction { - type: typeof CONSOLE_COMPLETION_NEXT; -} - -export interface CompletionPrevAction { - type: typeof CONSOLE_COMPLETION_PREV; -} - -export interface SetColorSchemeAction { - type: typeof CONSOLE_SET_COLORSCHEME; - colorscheme: ColorScheme; -} - -export type ConsoleAction = - | HideAction - | ShowCommand - | ShowFindAction - | ShowErrorAction - | ShowInfoAction - | HideCommandAction - | SetConsoleTextAction - | SetCompletionsAction - | CompletionNextAction - | CompletionPrevAction - | SetColorSchemeAction; diff --git a/src/console/components/AppContext.ts b/src/console/components/AppContext.ts index 878d00b..a930e14 100644 --- a/src/console/components/AppContext.ts +++ b/src/console/components/AppContext.ts @@ -1,6 +1,6 @@ import React from "react"; -import { State, defaultState } from "../reducers"; -import { ConsoleAction } from "../actions"; +import { State, defaultState } from "../reducers/console"; +import { ConsoleAction } from "../actions/console"; const AppContext = React.createContext<{ state: State; diff --git a/src/console/components/CommandPrompt.tsx b/src/console/components/CommandPrompt.tsx index d69fae6..4e02668 100644 --- a/src/console/components/CommandPrompt.tsx +++ b/src/console/components/CommandPrompt.tsx @@ -1,5 +1,6 @@ import React from "react"; -import * as consoleActions from "../../console/actions/console"; +import * as consoleActions from "../actions/console"; +import * as completionActions from "../actions/completion"; import AppContext from "./AppContext"; import CommandLineParser, { InputPhase, @@ -9,6 +10,8 @@ import ConsoleFrameClient from "../clients/ConsoleFrameClient"; import Input from "./console//Input"; import { Command } from "../../shared/Command"; import styled from "styled-components"; +import reducer, { defaultState, completedText } from "../reducers/completion"; +import CompletionType from "../../shared/CompletionType"; const COMPLETION_MAX_ITEMS = 33; @@ -18,13 +21,15 @@ const ConsoleWrapper = styled.div` const CommandPrompt: React.FC = () => { const { state, dispatch } = React.useContext(AppContext); + const [completionState, completionDispatch] = React.useReducer( + reducer, + defaultState + ); const commandLineParser = new CommandLineParser(); const consoleFrameClient = new ConsoleFrameClient(); const onBlur = () => { - if (state.mode === "command" || state.mode === "find") { - dispatch(consoleActions.hideCommand()); - } + dispatch(consoleActions.hideCommand()); }; const doEnter = (e: React.KeyboardEvent) => { @@ -32,21 +37,17 @@ const CommandPrompt: React.FC = () => { e.preventDefault(); const value = (e.target as HTMLInputElement).value; - if (state.mode === "command") { - dispatch(consoleActions.enterCommand(value)); - } else if (state.mode === "find") { - dispatch(consoleActions.enterFind(value === "" ? undefined : value)); - } + dispatch(consoleActions.enterCommand(value)); }; const selectNext = (e: React.KeyboardEvent) => { - dispatch(consoleActions.completionNext()); + completionDispatch(completionActions.completionNext()); e.stopPropagation(); e.preventDefault(); }; const selectPrev = (e: React.KeyboardEvent) => { - dispatch(consoleActions.completionPrev()); + completionDispatch(completionActions.completionPrev()); e.stopPropagation(); e.preventDefault(); }; @@ -61,9 +62,9 @@ const CommandPrompt: React.FC = () => { break; case "Tab": if (e.shiftKey) { - dispatch(consoleActions.completionPrev()); + completionDispatch(completionActions.completionPrev()); } else { - dispatch(consoleActions.completionNext()); + completionDispatch(completionActions.completionNext()); } e.stopPropagation(); e.preventDefault(); @@ -101,79 +102,113 @@ const CommandPrompt: React.FC = () => { const onChange = (e: React.ChangeEvent) => { const text = e.target.value; dispatch(consoleActions.setConsoleText(text)); - updateCompletions(text); + const action = getCompletionAction(text); + Promise.resolve(action).then((a) => { + if (a) { + completionDispatch(a); + + const { + scrollWidth: width, + scrollHeight: height, + } = document.getElementById("vimvixen-console")!; + consoleFrameClient.resize(width, height); + } + }); }; React.useEffect(() => { - updateCompletions(state.consoleText); + completionActions.startCompletion().then((action) => { + completionDispatch(action); + + const completionAction = getCompletionAction( + state.consoleText, + action.completionTypes + ); + Promise.resolve(completionAction).then((a) => { + if (a) { + completionDispatch(a); + + const { + scrollWidth: width, + scrollHeight: height, + } = document.getElementById("vimvixen-console")!; + consoleFrameClient.resize(width, height); + } + }); + }); }, []); - React.useEffect(() => { - const { - scrollWidth: width, - scrollHeight: height, - } = document.getElementById("vimvixen-console")!; - consoleFrameClient.resize(width, height); - }); - - const updateCompletions = (text: string) => { + const getCompletionAction = ( + text: string, + completionTypes: CompletionType[] | undefined = undefined + ) => { + const types = completionTypes || completionState.completionTypes; const phase = commandLineParser.inputPhase(text); if (phase === InputPhase.OnCommand) { - dispatch(consoleActions.getCommandCompletions(text)); + return completionActions.getCommandCompletions(text); } else { const cmd = commandLineParser.parse(text); switch (cmd.command) { case Command.Open: case Command.TabOpen: case Command.WindowOpen: - dispatch( - consoleActions.getOpenCompletions( - state.completionTypes, - text, - cmd.command, - cmd.args - ) + return completionActions.getOpenCompletions( + types, + text, + cmd.command, + cmd.args ); - break; case Command.Buffer: - dispatch( - consoleActions.getTabCompletions(text, cmd.command, cmd.args, false) + return completionActions.getTabCompletions( + text, + cmd.command, + cmd.args, + false ); - break; case Command.BufferDelete: case Command.BuffersDelete: - dispatch( - consoleActions.getTabCompletions(text, cmd.command, cmd.args, true) + return completionActions.getTabCompletions( + text, + cmd.command, + cmd.args, + true ); - break; case Command.BufferDeleteForce: case Command.BuffersDeleteForce: - dispatch( - consoleActions.getTabCompletions(text, cmd.command, cmd.args, false) + return completionActions.getTabCompletions( + text, + cmd.command, + cmd.args, + false ); - break; case Command.Set: - dispatch( - consoleActions.getPropertyCompletions(text, cmd.command, cmd.args) + return completionActions.getPropertyCompletions( + text, + cmd.command, + cmd.args ); - break; } } + return undefined; }; return ( ); diff --git a/src/console/components/Console.tsx b/src/console/components/Console.tsx index 8a1f73c..b97ed62 100644 --- a/src/console/components/Console.tsx +++ b/src/console/components/Console.tsx @@ -12,7 +12,6 @@ const Console: React.FC = () => { React.useEffect(() => { dispatch(consoleActions.setColorScheme()); - window.focus(); }, []); const ele = (() => { diff --git a/src/console/index.tsx b/src/console/index.tsx index cf9367b..4a5368b 100644 --- a/src/console/index.tsx +++ b/src/console/index.tsx @@ -1,5 +1,5 @@ import * as messages from "../shared/messages"; -import reducers, { defaultState } from "./reducers"; +import reducers, { defaultState } from "./reducers/console"; import * as consoleActions from "./actions/console"; import Console from "./components/Console"; import AppContext from "./components/AppContext"; diff --git a/src/console/reducers/completion.ts b/src/console/reducers/completion.ts new file mode 100644 index 0000000..2c7ee55 --- /dev/null +++ b/src/console/reducers/completion.ts @@ -0,0 +1,99 @@ +import Completions from "../Completions"; +import CompletionType from "../../shared/CompletionType"; +import { + COMPLETION_COMPLETION_NEXT, + COMPLETION_COMPLETION_PREV, + COMPLETION_SET_COMPLETIONS, + COMPLETION_START_COMPLETION, + CompletionAction, +} from "../actions/completion"; + +export interface State { + completionTypes: CompletionType[]; + completionSource: string; + completions: Completions; + select: number; +} + +export const defaultState = { + completionTypes: [], + completionSource: "", + completions: [], + select: -1, +}; + +const nextSelection = (state: State): number => { + if (state.completions.length === 0) { + return -1; + } + if (state.select < 0) { + return 0; + } + + const length = state.completions + .map((g) => g.items.length) + .reduce((x, y) => x + y); + if (state.select + 1 < length) { + return state.select + 1; + } + return -1; +}; + +const prevSelection = (state: State): number => { + const length = state.completions + .map((g) => g.items.length) + .reduce((x, y) => x + y); + if (state.select < 0) { + return length - 1; + } + return state.select - 1; +}; + +export const completedText = (state: State): string => { + if (state.select < 0) { + return state.completionSource; + } + const items = state.completions + .map((g) => g.items) + .reduce((g1, g2) => g1.concat(g2)); + return items[state.select].content || ""; +}; + +// eslint-disable-next-line max-lines-per-function +export default function reducer( + state: State = defaultState, + action: CompletionAction +): State { + switch (action.type) { + case COMPLETION_START_COMPLETION: + return { + ...state, + completionTypes: action.completionTypes, + completions: [], + select: -1, + }; + case COMPLETION_SET_COMPLETIONS: + return { + ...state, + completions: action.completions, + completionSource: action.completionSource, + select: -1, + }; + case COMPLETION_COMPLETION_NEXT: { + const select = nextSelection(state); + return { + ...state, + select: select, + }; + } + case COMPLETION_COMPLETION_PREV: { + const select = prevSelection(state); + return { + ...state, + select: select, + }; + } + default: + return state; + } +} diff --git a/src/console/reducers/console.ts b/src/console/reducers/console.ts new file mode 100644 index 0000000..3acd0e9 --- /dev/null +++ b/src/console/reducers/console.ts @@ -0,0 +1,64 @@ +import ColorScheme from "../../shared/ColorScheme"; +import { + CONSOLE_HIDE, + CONSOLE_HIDE_COMMAND, + CONSOLE_SET_COLORSCHEME, + CONSOLE_SET_CONSOLE_TEXT, + CONSOLE_SHOW_COMMAND, + CONSOLE_SHOW_ERROR, + CONSOLE_SHOW_FIND, + CONSOLE_SHOW_INFO, + ConsoleAction, +} from "../actions/console"; + +export interface State { + mode: string; + messageText: string; + consoleText: string; + colorscheme: ColorScheme; +} + +export const defaultState = { + mode: "", + messageText: "", + consoleText: "", + colorscheme: ColorScheme.System, +}; + +// eslint-disable-next-line max-lines-per-function +export default function reducer( + state: State = defaultState, + action: ConsoleAction +): State { + switch (action.type) { + case CONSOLE_HIDE: + return { ...state, mode: "" }; + case CONSOLE_SHOW_COMMAND: + return { + ...state, + mode: "command", + consoleText: action.text, + }; + case CONSOLE_SHOW_FIND: + return { ...state, mode: "find", consoleText: "" }; + case CONSOLE_SHOW_ERROR: + return { ...state, mode: "error", messageText: action.text }; + case CONSOLE_SHOW_INFO: + return { ...state, mode: "info", messageText: action.text }; + case CONSOLE_HIDE_COMMAND: + return { + ...state, + mode: + state.mode === "command" || state.mode === "find" ? "" : state.mode, + }; + case CONSOLE_SET_CONSOLE_TEXT: + return { ...state, consoleText: action.consoleText }; + case CONSOLE_SET_COLORSCHEME: + return { + ...state, + colorscheme: action.colorscheme, + }; + default: + return state; + } +} diff --git a/src/console/reducers/index.ts b/src/console/reducers/index.ts deleted file mode 100644 index 49d0de1..0000000 --- a/src/console/reducers/index.ts +++ /dev/null @@ -1,134 +0,0 @@ -import * as actions from "../actions"; -import Completions from "../Completions"; -import CompletionType from "../../shared/CompletionType"; -import ColorScheme from "../../shared/ColorScheme"; - -export interface State { - mode: string; - messageText: string; - consoleText: string; - completionTypes: CompletionType[]; - completionSource: string; - completions: Completions; - select: number; - colorscheme: ColorScheme; -} - -export const defaultState = { - mode: "", - messageText: "", - consoleText: "", - completionTypes: [], - completionSource: "", - completions: [], - select: -1, - colorscheme: ColorScheme.System, -}; - -const nextSelection = (state: State): number => { - if (state.completions.length === 0) { - return -1; - } - if (state.select < 0) { - return 0; - } - - const length = state.completions - .map((g) => g.items.length) - .reduce((x, y) => x + y); - if (state.select + 1 < length) { - return state.select + 1; - } - return -1; -}; - -const prevSelection = (state: State): number => { - const length = state.completions - .map((g) => g.items.length) - .reduce((x, y) => x + y); - if (state.select < 0) { - return length - 1; - } - return state.select - 1; -}; - -const nextConsoleText = (completions: any[], select: number, defaults: any) => { - if (select < 0) { - return defaults; - } - const items = completions - .map((g) => g.items) - .reduce((g1, g2) => g1.concat(g2)); - return items[select].content; -}; - -// eslint-disable-next-line max-lines-per-function -export default function reducer( - state: State = defaultState, - action: actions.ConsoleAction -): State { - switch (action.type) { - case actions.CONSOLE_HIDE: - return { ...state, mode: "" }; - case actions.CONSOLE_SHOW_COMMAND: - return { - ...state, - mode: "command", - consoleText: action.text, - completionTypes: action.completionTypes, - completions: [], - }; - case actions.CONSOLE_SHOW_FIND: - return { ...state, mode: "find", consoleText: "", completions: [] }; - case actions.CONSOLE_SHOW_ERROR: - return { ...state, mode: "error", messageText: action.text }; - case actions.CONSOLE_SHOW_INFO: - return { ...state, mode: "info", messageText: action.text }; - case actions.CONSOLE_HIDE_COMMAND: - return { - ...state, - mode: - state.mode === "command" || state.mode === "find" ? "" : state.mode, - }; - case actions.CONSOLE_SET_CONSOLE_TEXT: - return { ...state, consoleText: action.consoleText }; - case actions.CONSOLE_SET_COMPLETIONS: - return { - ...state, - completions: action.completions, - completionSource: action.completionSource, - select: -1, - }; - case actions.CONSOLE_COMPLETION_NEXT: { - const select = nextSelection(state); - return { - ...state, - select: select, - consoleText: nextConsoleText( - state.completions, - select, - state.completionSource - ), - }; - } - case actions.CONSOLE_COMPLETION_PREV: { - const select = prevSelection(state); - return { - ...state, - select: select, - consoleText: nextConsoleText( - state.completions, - select, - state.completionSource - ), - }; - } - case actions.CONSOLE_SET_COLORSCHEME: - return { - ...state, - colorscheme: action.colorscheme, - }; - default: - return state; - } -} diff --git a/test/console/actions/completion.test.ts b/test/console/actions/completion.test.ts new file mode 100644 index 0000000..cd6899a --- /dev/null +++ b/test/console/actions/completion.test.ts @@ -0,0 +1,28 @@ +import * as completionActions from "../../../src/console/actions/completion"; +import { + COMPLETION_COMPLETION_NEXT, + COMPLETION_COMPLETION_PREV, +} from "../../../src/console/actions/completion"; +import { expect } from "chai"; + +import browserFake from "webextensions-api-fake"; + +describe("completion actions", () => { + beforeEach(() => { + (global as any).browser = browserFake(); + }); + + describe("completionPrev", () => { + it("create COMPLETION_COMPLETION_PREV action", () => { + const action = completionActions.completionPrev(); + expect(action.type).to.equal(COMPLETION_COMPLETION_PREV); + }); + }); + + describe("completionNext", () => { + it("create COMPLETION_COMPLETION_NEXT action", () => { + const action = completionActions.completionNext(); + expect(action.type).to.equal(COMPLETION_COMPLETION_NEXT); + }); + }); +}); diff --git a/test/console/actions/console.test.ts b/test/console/actions/console.test.ts index a03117a..f5f102b 100644 --- a/test/console/actions/console.test.ts +++ b/test/console/actions/console.test.ts @@ -1,5 +1,13 @@ -import * as actions from "../../../src/console/actions"; import * as consoleActions from "../../../src/console/actions/console"; +import { + CONSOLE_HIDE, + CONSOLE_HIDE_COMMAND, + CONSOLE_SET_CONSOLE_TEXT, + CONSOLE_SHOW_COMMAND, + CONSOLE_SHOW_ERROR, + CONSOLE_SHOW_FIND, + CONSOLE_SHOW_INFO, +} from "../../../src/console/actions/console"; import { expect } from "chai"; import browserFake from "webextensions-api-fake"; @@ -12,13 +20,13 @@ describe("console actions", () => { describe("hide", () => { it("create CONSOLE_HIDE action", () => { const action = consoleActions.hide(); - expect(action.type).to.equal(actions.CONSOLE_HIDE); + expect(action.type).to.equal(CONSOLE_HIDE); }); }); describe("showCommand", () => { it("create CONSOLE_SHOW_COMMAND action", async () => { const action = await consoleActions.showCommand("hello"); - expect(action.type).to.equal(actions.CONSOLE_SHOW_COMMAND); + expect(action.type).to.equal(CONSOLE_SHOW_COMMAND); expect(action.text).to.equal("hello"); }); }); @@ -26,14 +34,14 @@ describe("console actions", () => { describe("showFind", () => { it("create CONSOLE_SHOW_FIND action", () => { const action = consoleActions.showFind(); - expect(action.type).to.equal(actions.CONSOLE_SHOW_FIND); + expect(action.type).to.equal(CONSOLE_SHOW_FIND); }); }); describe("showError", () => { it("create CONSOLE_SHOW_ERROR action", () => { const action = consoleActions.showError("an error"); - expect(action.type).to.equal(actions.CONSOLE_SHOW_ERROR); + expect(action.type).to.equal(CONSOLE_SHOW_ERROR); expect(action.text).to.equal("an error"); }); }); @@ -41,7 +49,7 @@ describe("console actions", () => { describe("showInfo", () => { it("create CONSOLE_SHOW_INFO action", () => { const action = consoleActions.showInfo("an info"); - expect(action.type).to.equal(actions.CONSOLE_SHOW_INFO); + expect(action.type).to.equal(CONSOLE_SHOW_INFO); expect(action.text).to.equal("an info"); }); }); @@ -49,29 +57,15 @@ describe("console actions", () => { describe("hideCommand", () => { it("create CONSOLE_HIDE_COMMAND action", () => { const action = consoleActions.hideCommand(); - expect(action.type).to.equal(actions.CONSOLE_HIDE_COMMAND); + expect(action.type).to.equal(CONSOLE_HIDE_COMMAND); }); }); describe("setConsoleText", () => { it("create CONSOLE_SET_CONSOLE_TEXT action", () => { const action = consoleActions.setConsoleText("hello world"); - expect(action.type).to.equal(actions.CONSOLE_SET_CONSOLE_TEXT); + expect(action.type).to.equal(CONSOLE_SET_CONSOLE_TEXT); expect(action.consoleText).to.equal("hello world"); }); }); - - describe("completionPrev", () => { - it("create CONSOLE_COMPLETION_PREV action", () => { - const action = consoleActions.completionPrev(); - expect(action.type).to.equal(actions.CONSOLE_COMPLETION_PREV); - }); - }); - - describe("completionNext", () => { - it("create CONSOLE_COMPLETION_NEXT action", () => { - const action = consoleActions.completionNext(); - expect(action.type).to.equal(actions.CONSOLE_COMPLETION_NEXT); - }); - }); }); diff --git a/test/console/reducers/completion.test.ts b/test/console/reducers/completion.test.ts new file mode 100644 index 0000000..6c76369 --- /dev/null +++ b/test/console/reducers/completion.test.ts @@ -0,0 +1,102 @@ +import reducer, { State } from "../../../src/console/reducers/completion"; +import { expect } from "chai"; +import { + COMPLETION_COMPLETION_NEXT, + COMPLETION_COMPLETION_PREV, + COMPLETION_SET_COMPLETIONS, + CompletionAction, +} from "../../../src/console/actions/completion"; + +describe("completion reducer", () => { + it("return next state for CONSOLE_SET_COMPLETIONS", () => { + const initialState = reducer(undefined, {} as any); + let state: State = { + ...initialState, + select: 0, + completions: [], + }; + const action: CompletionAction = { + type: COMPLETION_SET_COMPLETIONS, + completions: [ + { + name: "Apple", + items: [{}, {}, {}], + }, + { + name: "Banana", + items: [{}, {}, {}], + }, + ], + completionSource: "", + }; + state = reducer(state, action); + expect(state).to.have.property("completions", action.completions); + expect(state).to.have.property("select", -1); + }); + + it("return next state for CONSOLE_COMPLETION_NEXT", () => { + const initialState = reducer(undefined, {} as any); + const action: CompletionAction = { + type: COMPLETION_COMPLETION_NEXT, + }; + let state = { + ...initialState, + select: -1, + completions: [ + { + name: "Apple", + items: [{}, {}], + }, + { + name: "Banana", + items: [{}], + }, + ], + }; + + state = reducer(state, action); + expect(state).to.have.property("select", 0); + + state = reducer(state, action); + expect(state).to.have.property("select", 1); + + state = reducer(state, action); + expect(state).to.have.property("select", 2); + + state = reducer(state, action); + expect(state).to.have.property("select", -1); + }); + + it("return next state for CONSOLE_COMPLETION_PREV", () => { + const initialState = reducer(undefined, {} as any); + const action: CompletionAction = { + type: COMPLETION_COMPLETION_PREV, + }; + let state = { + ...initialState, + select: -1, + completions: [ + { + name: "Apple", + items: [{}, {}], + }, + { + name: "Banana", + items: [{}], + }, + ], + }; + + state = reducer(state, action); + expect(state).to.have.property("select", 2); + + state = reducer(state, action); + expect(state).to.have.property("select", 1); + + state = reducer(state, action); + expect(state).to.have.property("select", 0); + + state = reducer(state, action); + expect(state).to.have.property("select", -1); + }); +}); diff --git a/test/console/reducers/console.test.ts b/test/console/reducers/console.test.ts index 64e8eb3..4d4859d 100644 --- a/test/console/reducers/console.test.ts +++ b/test/console/reducers/console.test.ts @@ -1,8 +1,14 @@ -import * as actions from "../../../src/console/actions"; -import reducer, { State } from "../../../src/console/reducers"; +import reducer from "../../../src/console/reducers/console"; import { expect } from "chai"; -import CompletionType from "../../../src/shared/CompletionType"; -import { ConsoleAction } from "../../../src/console/actions"; +import { + CONSOLE_HIDE, + CONSOLE_HIDE_COMMAND, + CONSOLE_SET_CONSOLE_TEXT, + CONSOLE_SHOW_COMMAND, + CONSOLE_SHOW_ERROR, + CONSOLE_SHOW_INFO, + ConsoleAction, +} from "../../../src/console/actions/console"; describe("console reducer", () => { it("return the initial state", () => { @@ -10,21 +16,18 @@ describe("console reducer", () => { expect(state).to.have.property("mode", ""); expect(state).to.have.property("messageText", ""); expect(state).to.have.property("consoleText", ""); - expect(state).to.have.deep.property("completions", []); - expect(state).to.have.property("select", -1); }); it("return next state for CONSOLE_HIDE", () => { const initialState = reducer(undefined, {} as any); - const action: actions.ConsoleAction = { type: actions.CONSOLE_HIDE }; + const action: ConsoleAction = { type: CONSOLE_HIDE }; const state = reducer({ ...initialState, mode: "error" }, action); expect(state).to.have.property("mode", ""); }); it("return next state for CONSOLE_SHOW_COMMAND", () => { - const action: actions.ConsoleAction = { - type: actions.CONSOLE_SHOW_COMMAND, - completionTypes: [CompletionType.SearchEngines, CompletionType.History], + const action: ConsoleAction = { + type: CONSOLE_SHOW_COMMAND, text: "open ", }; const state = reducer(undefined, action); @@ -33,8 +36,8 @@ describe("console reducer", () => { }); it("return next state for CONSOLE_SHOW_INFO", () => { - const action: actions.ConsoleAction = { - type: actions.CONSOLE_SHOW_INFO, + const action: ConsoleAction = { + type: CONSOLE_SHOW_INFO, text: "an info", }; const state = reducer(undefined, action); @@ -43,8 +46,8 @@ describe("console reducer", () => { }); it("return next state for CONSOLE_SHOW_ERROR", () => { - const action: actions.ConsoleAction = { - type: actions.CONSOLE_SHOW_ERROR, + const action: ConsoleAction = { + type: CONSOLE_SHOW_ERROR, text: "an error", }; const state = reducer(undefined, action); @@ -54,8 +57,8 @@ describe("console reducer", () => { it("return next state for CONSOLE_HIDE_COMMAND", () => { const initialState = reducer(undefined, {} as any); - const action: actions.ConsoleAction = { - type: actions.CONSOLE_HIDE_COMMAND, + const action: ConsoleAction = { + type: CONSOLE_HIDE_COMMAND, }; let state = reducer({ ...initialState, mode: "command" }, action); expect(state).to.have.property("mode", ""); @@ -65,100 +68,12 @@ describe("console reducer", () => { }); it("return next state for CONSOLE_SET_CONSOLE_TEXT", () => { - const action: actions.ConsoleAction = { - type: actions.CONSOLE_SET_CONSOLE_TEXT, + const action: ConsoleAction = { + type: CONSOLE_SET_CONSOLE_TEXT, consoleText: "hello world", }; const state = reducer(undefined, action); expect(state).to.have.property("consoleText", "hello world"); }); - - it("return next state for CONSOLE_SET_COMPLETIONS", () => { - const initialState = reducer(undefined, {} as any); - let state: State = { - ...initialState, - select: 0, - completions: [], - }; - const action: actions.ConsoleAction = { - type: actions.CONSOLE_SET_COMPLETIONS, - completions: [ - { - name: "Apple", - items: [{}, {}, {}], - }, - { - name: "Banana", - items: [{}, {}, {}], - }, - ], - completionSource: "", - }; - state = reducer(state, action); - expect(state).to.have.property("completions", action.completions); - expect(state).to.have.property("select", -1); - }); - - it("return next state for CONSOLE_COMPLETION_NEXT", () => { - const initialState = reducer(undefined, {} as any); - const action: ConsoleAction = { type: actions.CONSOLE_COMPLETION_NEXT }; - let state = { - ...initialState, - select: -1, - completions: [ - { - name: "Apple", - items: [{}, {}], - }, - { - name: "Banana", - items: [{}], - }, - ], - }; - - state = reducer(state, action); - expect(state).to.have.property("select", 0); - - state = reducer(state, action); - expect(state).to.have.property("select", 1); - - state = reducer(state, action); - expect(state).to.have.property("select", 2); - - state = reducer(state, action); - expect(state).to.have.property("select", -1); - }); - - it("return next state for CONSOLE_COMPLETION_PREV", () => { - const initialState = reducer(undefined, {} as any); - const action: ConsoleAction = { type: actions.CONSOLE_COMPLETION_PREV }; - let state = { - ...initialState, - select: -1, - completions: [ - { - name: "Apple", - items: [{}, {}], - }, - { - name: "Banana", - items: [{}], - }, - ], - }; - - state = reducer(state, action); - expect(state).to.have.property("select", 2); - - state = reducer(state, action); - expect(state).to.have.property("select", 1); - - state = reducer(state, action); - expect(state).to.have.property("select", 0); - - state = reducer(state, action); - expect(state).to.have.property("select", -1); - }); }); -- cgit v1.2.3 From 21f863d76fbb5ed752ad529f8fbe33e75460027e Mon Sep 17 00:00:00 2001 From: Shin'ya Ueoka Date: Mon, 5 Apr 2021 23:05:23 +0900 Subject: Replace colorscheme state with React Hooks --- src/console/actions/console.ts | 22 +------ src/console/colorscheme/contexts.tsx | 10 +++ src/console/colorscheme/hooks.ts | 15 +++++ src/console/colorscheme/providers.tsx | 31 +++++++++ src/console/colorscheme/styled.tsx | 6 ++ src/console/colorscheme/theme.ts | 47 +++++++++++++ src/console/components/ColorSchemeProvider.tsx | 76 ---------------------- src/console/components/Console.tsx | 40 +++++------- src/console/components/ErrorMessage.tsx | 2 +- src/console/components/InfoMessage.tsx | 2 +- src/console/components/console/CompletionItem.tsx | 2 +- src/console/components/console/CompletionTitle.tsx | 2 +- src/console/components/console/Input.tsx | 2 +- src/console/index.tsx | 5 +- src/console/reducers/console.ts | 9 --- 15 files changed, 135 insertions(+), 136 deletions(-) create mode 100644 src/console/colorscheme/contexts.tsx create mode 100644 src/console/colorscheme/hooks.ts create mode 100644 src/console/colorscheme/providers.tsx create mode 100644 src/console/colorscheme/styled.tsx create mode 100644 src/console/colorscheme/theme.ts delete mode 100644 src/console/components/ColorSchemeProvider.tsx (limited to 'src/console/index.tsx') diff --git a/src/console/actions/console.ts b/src/console/actions/console.ts index 646cc31..bce2c67 100644 --- a/src/console/actions/console.ts +++ b/src/console/actions/console.ts @@ -1,8 +1,4 @@ import * as messages from "../../shared/messages"; -import SettingClient from "../clients/SettingClient"; -import ColorScheme from "../../shared/ColorScheme"; - -const settingClient = new SettingClient(); export const CONSOLE_SHOW_COMMAND = "console.show.command"; export const CONSOLE_SHOW_ERROR = "console.show.error"; @@ -10,7 +6,6 @@ export const CONSOLE_SHOW_INFO = "console.show.info"; export const CONSOLE_HIDE_COMMAND = "console.hide.command"; export const CONSOLE_SET_CONSOLE_TEXT = "console.set.command"; export const CONSOLE_SHOW_FIND = "console.show.find"; -export const CONSOLE_SET_COLORSCHEME = "completion.set.colorscheme"; export const CONSOLE_HIDE = "console.hide"; export interface HideAction { @@ -45,11 +40,6 @@ export interface SetConsoleTextAction { consoleText: string; } -export interface SetColorSchemeAction { - type: typeof CONSOLE_SET_COLORSCHEME; - colorscheme: ColorScheme; -} - export type ConsoleAction = | HideAction | ShowCommand @@ -57,8 +47,7 @@ export type ConsoleAction = | ShowErrorAction | ShowInfoAction | HideCommandAction - | SetConsoleTextAction - | SetColorSchemeAction; + | SetConsoleTextAction; const hide = (): ConsoleAction => { return { @@ -131,14 +120,6 @@ const setConsoleText = (consoleText: string): SetConsoleTextAction => { }; }; -const setColorScheme = async (): Promise => { - const scheme = await settingClient.getColorScheme(); - return { - type: CONSOLE_SET_COLORSCHEME, - colorscheme: scheme, - }; -}; - export { hide, showCommand, @@ -149,5 +130,4 @@ export { setConsoleText, enterCommand, enterFind, - setColorScheme, }; diff --git a/src/console/colorscheme/contexts.tsx b/src/console/colorscheme/contexts.tsx new file mode 100644 index 0000000..e94454b --- /dev/null +++ b/src/console/colorscheme/contexts.tsx @@ -0,0 +1,10 @@ +import React from "react"; +import ColorScheme from "../../shared/ColorScheme"; + +export const ColorSchemeContext = React.createContext( + ColorScheme.System +); + +export const ColorSchemeUpdateContext = React.createContext< + (colorscheme: ColorScheme) => void +>(() => {}); diff --git a/src/console/colorscheme/hooks.ts b/src/console/colorscheme/hooks.ts new file mode 100644 index 0000000..c9de754 --- /dev/null +++ b/src/console/colorscheme/hooks.ts @@ -0,0 +1,15 @@ +import React from "react"; +import { ColorSchemeUpdateContext } from "./contexts"; +import SettingClient from "../clients/SettingClient"; + +export const useColorSchemeRefresh = () => { + const update = React.useContext(ColorSchemeUpdateContext); + const settingClient = new SettingClient(); + const refresh = React.useCallback(() => { + settingClient.getColorScheme().then((newScheme) => { + update(newScheme); + }); + }, []); + + return refresh; +}; diff --git a/src/console/colorscheme/providers.tsx b/src/console/colorscheme/providers.tsx new file mode 100644 index 0000000..810c8e0 --- /dev/null +++ b/src/console/colorscheme/providers.tsx @@ -0,0 +1,31 @@ +import React from "react"; +import ColorScheme from "../../shared/ColorScheme"; +import { DarkTheme, LightTheme } from "./theme"; +import { ColorSchemeContext, ColorSchemeUpdateContext } from "./contexts"; +import { ThemeProvider } from "styled-components"; + +export const ColorSchemeProvider: React.FC = ({ children }) => { + const [colorscheme, setColorScheme] = React.useState(ColorScheme.System); + const theme = React.useMemo(() => { + if (colorscheme === ColorScheme.System) { + if ( + window.matchMedia && + window.matchMedia("(prefers-color-scheme: dark)").matches + ) { + return DarkTheme; + } + } else if (colorscheme === ColorScheme.Dark) { + return DarkTheme; + } + return LightTheme; + }, [colorscheme]); + + return ( + + + {children} + + + ); +}; +export default ColorSchemeProvider; diff --git a/src/console/colorscheme/styled.tsx b/src/console/colorscheme/styled.tsx new file mode 100644 index 0000000..12e10ec --- /dev/null +++ b/src/console/colorscheme/styled.tsx @@ -0,0 +1,6 @@ +import baseStyled, { ThemedStyledInterface } from "styled-components"; +import { ThemeProperties } from "./theme"; + +const styled = baseStyled as ThemedStyledInterface; + +export default styled; diff --git a/src/console/colorscheme/theme.ts b/src/console/colorscheme/theme.ts new file mode 100644 index 0000000..5c17190 --- /dev/null +++ b/src/console/colorscheme/theme.ts @@ -0,0 +1,47 @@ +export type ThemeProperties = { + completionTitleBackground: string; + completionTitleForeground: string; + completionItemBackground: string; + completionItemForeground: string; + completionItemDescriptionForeground: string; + completionSelectedBackground: string; + completionSelectedForeground: string; + commandBackground: string; + commandForeground: string; + consoleErrorBackground: string; + consoleErrorForeground: string; + consoleInfoBackground: string; + consoleInfoForeground: string; +}; + +export const LightTheme: ThemeProperties = { + completionTitleBackground: "lightgray", + completionTitleForeground: "#000000", + completionItemBackground: "#ffffff", + completionItemForeground: "#000000", + completionItemDescriptionForeground: "#008000", + completionSelectedBackground: "#ffff00", + completionSelectedForeground: "#000000", + commandBackground: "#ffffff", + commandForeground: "#000000", + consoleErrorBackground: "#ff0000", + consoleErrorForeground: "#ffffff", + consoleInfoBackground: "#ffffff", + consoleInfoForeground: "#018786", +}; + +export const DarkTheme: ThemeProperties = { + completionTitleBackground: "#052027", + completionTitleForeground: "white", + completionItemBackground: "#2f474f", + completionItemForeground: "white", + completionItemDescriptionForeground: "#86fab0", + completionSelectedBackground: "#eeff41", + completionSelectedForeground: "#000000", + commandBackground: "#052027", + commandForeground: "white", + consoleErrorBackground: "red", + consoleErrorForeground: "white", + consoleInfoBackground: "#052027", + consoleInfoForeground: "#ffffff", +}; diff --git a/src/console/components/ColorSchemeProvider.tsx b/src/console/components/ColorSchemeProvider.tsx deleted file mode 100644 index bd63571..0000000 --- a/src/console/components/ColorSchemeProvider.tsx +++ /dev/null @@ -1,76 +0,0 @@ -import React from "react"; -import ColorScheme from "../../shared/ColorScheme"; -import { ThemeProvider } from "styled-components"; -import baseStyled, { ThemedStyledInterface } from "styled-components"; - -type ThemeProperties = { - completionTitleBackground: string; - completionTitleForeground: string; - completionItemBackground: string; - completionItemForeground: string; - completionItemDescriptionForeground: string; - completionSelectedBackground: string; - completionSelectedForeground: string; - commandBackground: string; - commandForeground: string; - consoleErrorBackground: string; - consoleErrorForeground: string; - consoleInfoBackground: string; - consoleInfoForeground: string; -}; - -export const LightTheme: ThemeProperties = { - completionTitleBackground: "lightgray", - completionTitleForeground: "#000000", - completionItemBackground: "#ffffff", - completionItemForeground: "#000000", - completionItemDescriptionForeground: "#008000", - completionSelectedBackground: "#ffff00", - completionSelectedForeground: "#000000", - commandBackground: "#ffffff", - commandForeground: "#000000", - consoleErrorBackground: "#ff0000", - consoleErrorForeground: "#ffffff", - consoleInfoBackground: "#ffffff", - consoleInfoForeground: "#018786", -}; - -export const DarkTheme: ThemeProperties = { - completionTitleBackground: "#052027", - completionTitleForeground: "white", - completionItemBackground: "#2f474f", - completionItemForeground: "white", - completionItemDescriptionForeground: "#86fab0", - completionSelectedBackground: "#eeff41", - completionSelectedForeground: "#000000", - commandBackground: "#052027", - commandForeground: "white", - consoleErrorBackground: "red", - consoleErrorForeground: "white", - consoleInfoBackground: "#052027", - consoleInfoForeground: "#ffffff", -}; - -interface Props extends React.HTMLAttributes { - colorscheme: ColorScheme; -} - -const ColorSchemeProvider: React.FC = ({ colorscheme, children }) => { - let theme = LightTheme; - if (colorscheme === ColorScheme.System) { - if ( - window.matchMedia && - window.matchMedia("(prefers-color-scheme: dark)").matches - ) { - theme = DarkTheme; - } - } else if (colorscheme === ColorScheme.Dark) { - theme = DarkTheme; - } - - return {children}; -}; - -export const styled = baseStyled as ThemedStyledInterface; - -export default ColorSchemeProvider; diff --git a/src/console/components/Console.tsx b/src/console/components/Console.tsx index b97ed62..f6f4234 100644 --- a/src/console/components/Console.tsx +++ b/src/console/components/Console.tsx @@ -3,37 +3,29 @@ import FindPrompt from "./FindPrompt"; import CommandPrompt from "./CommandPrompt"; import InfoMessage from "./InfoMessage"; import ErrorMessage from "./ErrorMessage"; -import * as consoleActions from "../../console/actions/console"; -import ColorSchemeProvider from "./ColorSchemeProvider"; import AppContext from "./AppContext"; +import { useColorSchemeRefresh } from "../colorscheme/hooks"; const Console: React.FC = () => { - const { state, dispatch } = React.useContext(AppContext); + const { state } = React.useContext(AppContext); + const refreshColorScheme = useColorSchemeRefresh(); React.useEffect(() => { - dispatch(consoleActions.setColorScheme()); + refreshColorScheme(); }, []); - const ele = (() => { - switch (state.mode) { - case "command": - return ; - case "find": - return ; - case "info": - return {state.messageText}; - case "error": - return {state.messageText}; - default: - return null; - } - })(); - - return ( - - {ele} - - ); + switch (state.mode) { + case "command": + return ; + case "find": + return ; + case "info": + return {state.messageText}; + case "error": + return {state.messageText}; + default: + return null; + } }; export default Console; diff --git a/src/console/components/ErrorMessage.tsx b/src/console/components/ErrorMessage.tsx index 93b049b..f8d5ae7 100644 --- a/src/console/components/ErrorMessage.tsx +++ b/src/console/components/ErrorMessage.tsx @@ -1,5 +1,5 @@ import React from "react"; -import { styled } from "./ColorSchemeProvider"; +import styled from "../colorscheme/styled"; const Wrapper = styled.p` border-top: 1px solid gray; diff --git a/src/console/components/InfoMessage.tsx b/src/console/components/InfoMessage.tsx index 02ad27d..ccd9bcf 100644 --- a/src/console/components/InfoMessage.tsx +++ b/src/console/components/InfoMessage.tsx @@ -1,5 +1,5 @@ import React from "react"; -import { styled } from "./ColorSchemeProvider"; +import styled from "../colorscheme/styled"; const Wrapper = styled.p` border-top: 1px solid gray; diff --git a/src/console/components/console/CompletionItem.tsx b/src/console/components/console/CompletionItem.tsx index 7313491..2de1375 100644 --- a/src/console/components/console/CompletionItem.tsx +++ b/src/console/components/console/CompletionItem.tsx @@ -1,5 +1,5 @@ import React from "react"; -import { styled } from "../ColorSchemeProvider"; +import styled from "../../colorscheme/styled"; const Container = styled.li<{ shown: boolean; diff --git a/src/console/components/console/CompletionTitle.tsx b/src/console/components/console/CompletionTitle.tsx index a8e8a54..4018b3f 100644 --- a/src/console/components/console/CompletionTitle.tsx +++ b/src/console/components/console/CompletionTitle.tsx @@ -1,5 +1,5 @@ import React from "react"; -import { styled } from "../ColorSchemeProvider"; +import styled from "../../colorscheme/styled"; const Li = styled.li<{ shown: boolean }>` display: ${({ shown }) => (shown ? "display" : "none")}; diff --git a/src/console/components/console/Input.tsx b/src/console/components/console/Input.tsx index 7850f43..442bd30 100644 --- a/src/console/components/console/Input.tsx +++ b/src/console/components/console/Input.tsx @@ -1,5 +1,5 @@ import React from "react"; -import { styled } from "../ColorSchemeProvider"; +import styled from "../../colorscheme/styled"; const Container = styled.div` background-color: ${({ theme }) => theme.commandBackground}; diff --git a/src/console/index.tsx b/src/console/index.tsx index 4a5368b..71f2a27 100644 --- a/src/console/index.tsx +++ b/src/console/index.tsx @@ -6,6 +6,7 @@ import AppContext from "./components/AppContext"; import "./index.css"; import React from "react"; import ReactDOM from "react-dom"; +import ColorSchemeProvider from "./colorscheme/providers"; const wrapAsync = ( dispatch: React.Dispatch @@ -48,7 +49,9 @@ const RootComponent: React.FC = () => { return ( - + + + ); }; diff --git a/src/console/reducers/console.ts b/src/console/reducers/console.ts index 3acd0e9..37f1efc 100644 --- a/src/console/reducers/console.ts +++ b/src/console/reducers/console.ts @@ -1,8 +1,6 @@ -import ColorScheme from "../../shared/ColorScheme"; import { CONSOLE_HIDE, CONSOLE_HIDE_COMMAND, - CONSOLE_SET_COLORSCHEME, CONSOLE_SET_CONSOLE_TEXT, CONSOLE_SHOW_COMMAND, CONSOLE_SHOW_ERROR, @@ -15,14 +13,12 @@ export interface State { mode: string; messageText: string; consoleText: string; - colorscheme: ColorScheme; } export const defaultState = { mode: "", messageText: "", consoleText: "", - colorscheme: ColorScheme.System, }; // eslint-disable-next-line max-lines-per-function @@ -53,11 +49,6 @@ export default function reducer( }; case CONSOLE_SET_CONSOLE_TEXT: return { ...state, consoleText: action.consoleText }; - case CONSOLE_SET_COLORSCHEME: - return { - ...state, - colorscheme: action.colorscheme, - }; default: return state; } -- cgit v1.2.3 From 8a5bba1da639355a25da8c279a9f1cf0a7300a9f Mon Sep 17 00:00:00 2001 From: Shin'ya Ueoka Date: Sun, 11 Apr 2021 22:30:41 +0900 Subject: Replace app state with Custom Hooks --- src/console/actions/console.ts | 118 ------------------------------- src/console/app/actions.ts | 82 +++++++++++++++++++++ src/console/app/contexts.ts | 9 +++ src/console/app/hooks.ts | 115 ++++++++++++++++++++++++++++++ src/console/app/provider.tsx | 14 ++++ src/console/app/recuer.ts | 52 ++++++++++++++ src/console/components/AppContext.ts | 13 ---- src/console/components/CommandPrompt.tsx | 13 ++-- src/console/components/Console.tsx | 36 ++++++---- src/console/components/FindPrompt.tsx | 12 ++-- src/console/index.tsx | 62 ++++++++-------- src/console/reducers/console.ts | 52 -------------- test/console/actions/console.test.ts | 62 ---------------- test/console/app/actions.test.ts | 62 ++++++++++++++++ test/console/app/reducer.test.ts | 85 ++++++++++++++++++++++ test/console/reducers/console.test.ts | 68 ------------------ 16 files changed, 486 insertions(+), 369 deletions(-) delete mode 100644 src/console/actions/console.ts create mode 100644 src/console/app/actions.ts create mode 100644 src/console/app/contexts.ts create mode 100644 src/console/app/hooks.ts create mode 100644 src/console/app/provider.tsx create mode 100644 src/console/app/recuer.ts delete mode 100644 src/console/components/AppContext.ts delete mode 100644 src/console/reducers/console.ts delete mode 100644 test/console/actions/console.test.ts create mode 100644 test/console/app/actions.test.ts create mode 100644 test/console/app/reducer.test.ts delete mode 100644 test/console/reducers/console.test.ts (limited to 'src/console/index.tsx') diff --git a/src/console/actions/console.ts b/src/console/actions/console.ts deleted file mode 100644 index 2338067..0000000 --- a/src/console/actions/console.ts +++ /dev/null @@ -1,118 +0,0 @@ -import * as messages from "../../shared/messages"; - -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_HIDE_COMMAND = "console.hide.command"; -export const CONSOLE_SHOW_FIND = "console.show.find"; -export const CONSOLE_HIDE = "console.hide"; - -export interface HideAction { - type: typeof CONSOLE_HIDE; -} - -export interface ShowCommand { - type: typeof CONSOLE_SHOW_COMMAND; - text: string; -} - -export interface ShowFindAction { - type: typeof CONSOLE_SHOW_FIND; -} - -export interface ShowErrorAction { - type: typeof CONSOLE_SHOW_ERROR; - text: string; -} - -export interface ShowInfoAction { - type: typeof CONSOLE_SHOW_INFO; - text: string; -} - -export interface HideCommandAction { - type: typeof CONSOLE_HIDE_COMMAND; -} - -export type ConsoleAction = - | HideAction - | ShowCommand - | ShowFindAction - | ShowErrorAction - | ShowInfoAction - | HideCommandAction; - -const hide = (): ConsoleAction => { - return { - type: CONSOLE_HIDE, - }; -}; - -const showCommand = (text: string): ShowCommand => { - return { - type: CONSOLE_SHOW_COMMAND, - text, - }; -}; - -const showFind = (): ShowFindAction => { - return { - type: CONSOLE_SHOW_FIND, - }; -}; - -const showError = (text: string): ShowErrorAction => { - return { - type: CONSOLE_SHOW_ERROR, - text: text, - }; -}; - -const showInfo = (text: string): ShowInfoAction => { - return { - type: CONSOLE_SHOW_INFO, - text: text, - }; -}; - -const hideCommand = (): HideCommandAction => { - window.top.postMessage( - JSON.stringify({ - type: messages.CONSOLE_UNFOCUS, - }), - "*" - ); - return { - type: CONSOLE_HIDE_COMMAND, - }; -}; - -const enterCommand = async (text: string): Promise => { - await browser.runtime.sendMessage({ - type: messages.CONSOLE_ENTER_COMMAND, - text, - }); - return hideCommand(); -}; - -const enterFind = (text?: string): HideCommandAction => { - window.top.postMessage( - JSON.stringify({ - type: messages.CONSOLE_ENTER_FIND, - text, - }), - "*" - ); - return hideCommand(); -}; - -export { - hide, - showCommand, - showFind, - showError, - showInfo, - hideCommand, - enterCommand, - enterFind, -}; diff --git a/src/console/app/actions.ts b/src/console/app/actions.ts new file mode 100644 index 0000000..5538ae5 --- /dev/null +++ b/src/console/app/actions.ts @@ -0,0 +1,82 @@ +export const SHOW_COMMAND = "show.command"; +export const SHOW_ERROR = "show.error"; +export const SHOW_INFO = "show.info"; +export const HIDE_COMMAND = "hide.command"; +export const SHOW_FIND = "show.find"; +export const HIDE = "hide"; + +export interface HideAction { + type: typeof HIDE; +} + +export interface ShowCommand { + type: typeof SHOW_COMMAND; + text: string; +} + +export interface ShowFindAction { + type: typeof SHOW_FIND; +} + +export interface ShowErrorAction { + type: typeof SHOW_ERROR; + text: string; +} + +export interface ShowInfoAction { + type: typeof SHOW_INFO; + text: string; +} + +export interface HideCommandAction { + type: typeof HIDE_COMMAND; +} + +export type AppAction = + | HideAction + | ShowCommand + | ShowFindAction + | ShowErrorAction + | ShowInfoAction + | HideCommandAction; + +const hide = (): HideAction => { + return { + type: HIDE, + }; +}; + +const showCommand = (text: string): ShowCommand => { + return { + type: SHOW_COMMAND, + text, + }; +}; + +const showFind = (): ShowFindAction => { + return { + type: SHOW_FIND, + }; +}; + +const showError = (text: string): ShowErrorAction => { + return { + type: SHOW_ERROR, + text: text, + }; +}; + +const showInfo = (text: string): ShowInfoAction => { + return { + type: SHOW_INFO, + text: text, + }; +}; + +const hideCommand = (): HideCommandAction => { + return { + type: HIDE_COMMAND, + }; +}; + +export { hide, showCommand, showFind, showError, showInfo, hideCommand }; diff --git a/src/console/app/contexts.ts b/src/console/app/contexts.ts new file mode 100644 index 0000000..7e4f323 --- /dev/null +++ b/src/console/app/contexts.ts @@ -0,0 +1,9 @@ +import React from "react"; +import { State, defaultState } from "./recuer"; +import { AppAction } from "./actions"; + +export const AppStateContext = React.createContext(defaultState); + +export const AppDispatchContext = React.createContext< + (action: AppAction) => void +>(() => {}); diff --git a/src/console/app/hooks.ts b/src/console/app/hooks.ts new file mode 100644 index 0000000..eefdea3 --- /dev/null +++ b/src/console/app/hooks.ts @@ -0,0 +1,115 @@ +import React from "react"; +import * as actions from "./actions"; +import { AppDispatchContext, AppStateContext } from "./contexts"; +import * as messages from "../../shared/messages"; + +export const useHide = () => { + const dispatch = React.useContext(AppDispatchContext); + const hide = React.useCallback(() => { + window.top.postMessage( + JSON.stringify({ + type: messages.CONSOLE_UNFOCUS, + }), + "*" + ); + dispatch(actions.hide()); + }, [dispatch]); + + return hide; +}; + +export const useCommandMode = () => { + const state = React.useContext(AppStateContext); + const dispatch = React.useContext(AppDispatchContext); + + const show = React.useCallback( + (initialInputValue: string) => { + dispatch(actions.showCommand(initialInputValue)); + }, + [dispatch] + ); + + return { + visible: state.mode === "command", + initialInputValue: state.consoleText, + show, + }; +}; + +export const useFindMode = () => { + const state = React.useContext(AppStateContext); + const dispatch = React.useContext(AppDispatchContext); + + const show = React.useCallback(() => { + dispatch(actions.showFind()); + }, [dispatch]); + + return { + visible: state.mode === "find", + show, + }; +}; + +export const useInfoMessage = () => { + const state = React.useContext(AppStateContext); + const dispatch = React.useContext(AppDispatchContext); + + const show = React.useCallback( + (message: string) => { + dispatch(actions.showInfo(message)); + }, + [dispatch] + ); + + return { + visible: state.mode === "info", + message: state.mode === "info" ? state.messageText : "", + show, + }; +}; + +export const useErrorMessage = () => { + const state = React.useContext(AppStateContext); + const dispatch = React.useContext(AppDispatchContext); + + const show = React.useCallback( + (message: string) => { + dispatch(actions.showError(message)); + }, + [dispatch] + ); + + return { + visible: state.mode === "error", + message: state.mode === "error" ? state.messageText : "", + show, + }; +}; + +export const getInitialInputValue = () => { + const state = React.useContext(AppStateContext); + return state.consoleText; +}; + +export const useExecCommand = () => { + const execCommand = React.useCallback((text: string) => { + browser.runtime.sendMessage({ + type: messages.CONSOLE_ENTER_COMMAND, + text, + }); + }, []); + return execCommand; +}; + +export const useExecFind = () => { + const execFind = React.useCallback((text?: string) => { + window.top.postMessage( + JSON.stringify({ + type: messages.CONSOLE_ENTER_FIND, + text, + }), + "*" + ); + }, []); + return execFind; +}; diff --git a/src/console/app/provider.tsx b/src/console/app/provider.tsx new file mode 100644 index 0000000..397f165 --- /dev/null +++ b/src/console/app/provider.tsx @@ -0,0 +1,14 @@ +import React from "react"; +import reducer, { defaultState } from "./recuer"; +import { AppDispatchContext, AppStateContext } from "./contexts"; + +export const AppProvider: React.FC = ({ children }) => { + const [state, dispatch] = React.useReducer(reducer, defaultState); + return ( + + + {children} + + + ); +}; diff --git a/src/console/app/recuer.ts b/src/console/app/recuer.ts new file mode 100644 index 0000000..e3043ee --- /dev/null +++ b/src/console/app/recuer.ts @@ -0,0 +1,52 @@ +import { + HIDE, + HIDE_COMMAND, + SHOW_COMMAND, + SHOW_ERROR, + SHOW_FIND, + SHOW_INFO, + AppAction, +} from "./actions"; + +export interface State { + mode: string; + messageText: string; + consoleText: string; +} + +export const defaultState = { + mode: "", + messageText: "", + consoleText: "", +}; + +// eslint-disable-next-line max-lines-per-function +export default function reducer( + state: State = defaultState, + action: AppAction +): State { + switch (action.type) { + case HIDE: + return { ...state, mode: "" }; + case SHOW_COMMAND: + return { + ...state, + mode: "command", + consoleText: action.text, + }; + case SHOW_FIND: + return { ...state, mode: "find", consoleText: "" }; + case SHOW_ERROR: + return { ...state, mode: "error", messageText: action.text }; + case SHOW_INFO: + return { ...state, mode: "info", messageText: action.text }; + case HIDE_COMMAND: + return { + ...state, + mode: + state.mode === "command" || state.mode === "find" ? "" : state.mode, + }; + default: + return state; + } +} diff --git a/src/console/components/AppContext.ts b/src/console/components/AppContext.ts deleted file mode 100644 index a930e14..0000000 --- a/src/console/components/AppContext.ts +++ /dev/null @@ -1,13 +0,0 @@ -import React from "react"; -import { State, defaultState } from "../reducers/console"; -import { ConsoleAction } from "../actions/console"; - -const AppContext = React.createContext<{ - state: State; - dispatch: React.Dispatch | ConsoleAction>; -}>({ - state: defaultState, - dispatch: () => null, -}); - -export default AppContext; diff --git a/src/console/components/CommandPrompt.tsx b/src/console/components/CommandPrompt.tsx index 24f46ae..1b6281b 100644 --- a/src/console/components/CommandPrompt.tsx +++ b/src/console/components/CommandPrompt.tsx @@ -1,12 +1,11 @@ import React from "react"; -import * as consoleActions from "../actions/console"; -import AppContext from "./AppContext"; import Completion from "./console/Completion"; import Input from "./console//Input"; import styled from "styled-components"; import { useCompletions, useSelectCompletion } from "../completion/hooks"; import useAutoResize from "../hooks/useAutoResize"; import { CompletionProvider } from "../completion/provider"; +import { useExecCommand, useHide } from "../app/hooks"; const COMPLETION_MAX_ITEMS = 33; @@ -19,7 +18,7 @@ interface Props { } const CommandPromptInner: React.FC = ({ initialInputValue }) => { - const { dispatch } = React.useContext(AppContext); + const hide = useHide(); const [inputValue, setInputValue] = React.useState(initialInputValue); const { completions, updateCompletions } = useCompletions(); const { @@ -28,11 +27,12 @@ const CommandPromptInner: React.FC = ({ initialInputValue }) => { selectNext, selectPrev, } = useSelectCompletion(); + const execCommand = useExecCommand(); useAutoResize(); const onBlur = () => { - dispatch(consoleActions.hideCommand()); + hide(); }; const isCancelKey = React.useCallback( @@ -63,10 +63,11 @@ const CommandPromptInner: React.FC = ({ initialInputValue }) => { const onKeyDown = (e: React.KeyboardEvent) => { if (isCancelKey(e)) { - dispatch(consoleActions.hideCommand()); + hide(); } else if (isEnterKey(e)) { const value = (e.target as HTMLInputElement).value; - dispatch(consoleActions.enterCommand(value)); + execCommand(value); + hide(); } else if (isNextKey(e)) { selectNext(); } else if (isPrevKey(e)) { diff --git a/src/console/components/Console.tsx b/src/console/components/Console.tsx index c8642c8..db18fa0 100644 --- a/src/console/components/Console.tsx +++ b/src/console/components/Console.tsx @@ -3,31 +3,37 @@ import FindPrompt from "./FindPrompt"; import CommandPrompt from "./CommandPrompt"; import InfoMessage from "./InfoMessage"; import ErrorMessage from "./ErrorMessage"; -import AppContext from "./AppContext"; import { useColorSchemeRefresh } from "../colorscheme/hooks"; +import { + useCommandMode, + useErrorMessage, + useFindMode, + useInfoMessage, +} from "../app/hooks"; const Console: React.FC = () => { - const { state } = React.useContext(AppContext); const refreshColorScheme = useColorSchemeRefresh(); + const { visible: visibleCommand, initialInputValue } = useCommandMode(); + const { visible: visibleFind } = useFindMode(); + const { visible: visibleInfo, message: infoMessage } = useInfoMessage(); + const { visible: visibleError, message: errorMessage } = useErrorMessage(); React.useEffect(() => { - if (state.mode !== "") { + if (visibleCommand || visibleFind || visibleInfo || visibleError) { refreshColorScheme(); } - }, [state.mode]); + }, [visibleCommand, visibleFind, visibleInfo, visibleError]); - switch (state.mode) { - case "command": - return ; - case "find": - return ; - case "info": - return {state.messageText}; - case "error": - return {state.messageText}; - default: - return null; + if (visibleCommand) { + return ; + } else if (visibleFind) { + return ; + } else if (visibleInfo) { + return {infoMessage}; + } else if (visibleError) { + return {errorMessage}; } + return null; }; export default Console; diff --git a/src/console/components/FindPrompt.tsx b/src/console/components/FindPrompt.tsx index 10fa6c3..c437d16 100644 --- a/src/console/components/FindPrompt.tsx +++ b/src/console/components/FindPrompt.tsx @@ -1,20 +1,20 @@ import React from "react"; -import * as consoleActions from "../../console/actions/console"; -import AppContext from "./AppContext"; import Input from "./console/Input"; import styled from "styled-components"; import useAutoResize from "../hooks/useAutoResize"; +import { useExecFind, useHide } from "../app/hooks"; const ConsoleWrapper = styled.div` border-top: 1px solid gray; `; const FindPrompt: React.FC = () => { - const { dispatch } = React.useContext(AppContext); const [inputValue, setInputValue] = React.useState(""); + const hide = useHide(); + const execFind = useExecFind(); const onBlur = () => { - dispatch(consoleActions.hideCommand()); + hide(); }; useAutoResize(); @@ -24,13 +24,13 @@ const FindPrompt: React.FC = () => { e.preventDefault(); const value = (e.target as HTMLInputElement).value; - dispatch(consoleActions.enterFind(value === "" ? undefined : value)); + execFind(value === "" ? undefined : value); }; const onKeyDown = (e: React.KeyboardEvent) => { switch (e.key) { case "Escape": - dispatch(consoleActions.hideCommand()); + hide(); break; case "Enter": doEnter(e); diff --git a/src/console/index.tsx b/src/console/index.tsx index 71f2a27..29fa11f 100644 --- a/src/console/index.tsx +++ b/src/console/index.tsx @@ -1,42 +1,44 @@ import * as messages from "../shared/messages"; -import reducers, { defaultState } from "./reducers/console"; -import * as consoleActions from "./actions/console"; import Console from "./components/Console"; -import AppContext from "./components/AppContext"; import "./index.css"; import React from "react"; import ReactDOM from "react-dom"; import ColorSchemeProvider from "./colorscheme/providers"; - -const wrapAsync = ( - dispatch: React.Dispatch -): React.Dispatch> => { - return (action: T | Promise) => { - if (action instanceof Promise) { - action.then((a) => dispatch(a)).catch(console.error); - } else { - dispatch(action); - } - }; -}; +import { AppProvider } from "./app/provider"; +import { + useCommandMode, + useFindMode, + useInfoMessage, + useErrorMessage, + useHide, +} from "./app/hooks"; const RootComponent: React.FC = () => { - const [state, dispatch] = React.useReducer(reducers, defaultState); + const hide = useHide(); + const { show: showCommand } = useCommandMode(); + const { show: showFind } = useFindMode(); + const { show: showError } = useErrorMessage(); + const { show: showInfo } = useInfoMessage(); React.useEffect(() => { const onMessage = async (message: any): Promise => { const msg = messages.valueOf(message); switch (msg.type) { case messages.CONSOLE_SHOW_COMMAND: - return dispatch(await consoleActions.showCommand(msg.command)); + showCommand(msg.command); + break; case messages.CONSOLE_SHOW_FIND: - return dispatch(consoleActions.showFind()); + showFind(); + break; case messages.CONSOLE_SHOW_ERROR: - return dispatch(consoleActions.showError(msg.text)); + showError(msg.text); + break; case messages.CONSOLE_SHOW_INFO: - return dispatch(consoleActions.showInfo(msg.text)); + showInfo(msg.text); + break; case messages.CONSOLE_HIDE: - return dispatch(consoleActions.hide()); + hide(); + break; } }; @@ -47,16 +49,18 @@ const RootComponent: React.FC = () => { port.onMessage.addListener(onMessage); }, []); - return ( - - - - - - ); + return ; }; +const App: React.FC = () => ( + + + + + +); + window.addEventListener("DOMContentLoaded", () => { const wrapper = document.getElementById("vimvixen-console"); - ReactDOM.render(, wrapper); + ReactDOM.render(, wrapper); }); diff --git a/src/console/reducers/console.ts b/src/console/reducers/console.ts deleted file mode 100644 index babad69..0000000 --- a/src/console/reducers/console.ts +++ /dev/null @@ -1,52 +0,0 @@ -import { - CONSOLE_HIDE, - CONSOLE_HIDE_COMMAND, - CONSOLE_SHOW_COMMAND, - CONSOLE_SHOW_ERROR, - CONSOLE_SHOW_FIND, - CONSOLE_SHOW_INFO, - ConsoleAction, -} from "../actions/console"; - -export interface State { - mode: string; - messageText: string; - consoleText: string; -} - -export const defaultState = { - mode: "", - messageText: "", - consoleText: "", -}; - -// eslint-disable-next-line max-lines-per-function -export default function reducer( - state: State = defaultState, - action: ConsoleAction -): State { - switch (action.type) { - case CONSOLE_HIDE: - return { ...state, mode: "" }; - case CONSOLE_SHOW_COMMAND: - return { - ...state, - mode: "command", - consoleText: action.text, - }; - case CONSOLE_SHOW_FIND: - return { ...state, mode: "find", consoleText: "" }; - case CONSOLE_SHOW_ERROR: - return { ...state, mode: "error", messageText: action.text }; - case CONSOLE_SHOW_INFO: - return { ...state, mode: "info", messageText: action.text }; - case CONSOLE_HIDE_COMMAND: - return { - ...state, - mode: - state.mode === "command" || state.mode === "find" ? "" : state.mode, - }; - default: - return state; - } -} diff --git a/test/console/actions/console.test.ts b/test/console/actions/console.test.ts deleted file mode 100644 index 736dd54..0000000 --- a/test/console/actions/console.test.ts +++ /dev/null @@ -1,62 +0,0 @@ -import * as consoleActions from "../../../src/console/actions/console"; -import { - CONSOLE_HIDE, - CONSOLE_HIDE_COMMAND, - CONSOLE_SHOW_COMMAND, - CONSOLE_SHOW_ERROR, - CONSOLE_SHOW_FIND, - CONSOLE_SHOW_INFO, -} from "../../../src/console/actions/console"; -import { expect } from "chai"; - -import browserFake from "webextensions-api-fake"; - -describe("console actions", () => { - beforeEach(() => { - (global as any).browser = browserFake(); - }); - - describe("hide", () => { - it("create CONSOLE_HIDE action", () => { - const action = consoleActions.hide(); - expect(action.type).to.equal(CONSOLE_HIDE); - }); - }); - describe("showCommand", () => { - it("create CONSOLE_SHOW_COMMAND action", async () => { - const action = await consoleActions.showCommand("hello"); - expect(action.type).to.equal(CONSOLE_SHOW_COMMAND); - expect(action.text).to.equal("hello"); - }); - }); - - describe("showFind", () => { - it("create CONSOLE_SHOW_FIND action", () => { - const action = consoleActions.showFind(); - expect(action.type).to.equal(CONSOLE_SHOW_FIND); - }); - }); - - describe("showError", () => { - it("create CONSOLE_SHOW_ERROR action", () => { - const action = consoleActions.showError("an error"); - expect(action.type).to.equal(CONSOLE_SHOW_ERROR); - expect(action.text).to.equal("an error"); - }); - }); - - describe("showInfo", () => { - it("create CONSOLE_SHOW_INFO action", () => { - const action = consoleActions.showInfo("an info"); - expect(action.type).to.equal(CONSOLE_SHOW_INFO); - expect(action.text).to.equal("an info"); - }); - }); - - describe("hideCommand", () => { - it("create CONSOLE_HIDE_COMMAND action", () => { - const action = consoleActions.hideCommand(); - expect(action.type).to.equal(CONSOLE_HIDE_COMMAND); - }); - }); -}); diff --git a/test/console/app/actions.test.ts b/test/console/app/actions.test.ts new file mode 100644 index 0000000..2f9dc71 --- /dev/null +++ b/test/console/app/actions.test.ts @@ -0,0 +1,62 @@ +import * as consoleActions from "../../../src/console/app/actions"; +import { + HIDE, + HIDE_COMMAND, + SHOW_COMMAND, + SHOW_ERROR, + SHOW_FIND, + SHOW_INFO, +} from "../../../src/console/app/actions"; +import { expect } from "chai"; + +import browserFake from "webextensions-api-fake"; + +describe("console actions", () => { + beforeEach(() => { + (global as any).browser = browserFake(); + }); + + describe("hide", () => { + it("create CONSOLE_HIDE action", () => { + const action = consoleActions.hide(); + expect(action.type).to.equal(HIDE); + }); + }); + describe("showCommand", () => { + it("create CONSOLE_SHOW_COMMAND action", async () => { + const action = await consoleActions.showCommand("hello"); + expect(action.type).to.equal(SHOW_COMMAND); + expect(action.text).to.equal("hello"); + }); + }); + + describe("showFind", () => { + it("create CONSOLE_SHOW_FIND action", () => { + const action = consoleActions.showFind(); + expect(action.type).to.equal(SHOW_FIND); + }); + }); + + describe("showError", () => { + it("create CONSOLE_SHOW_ERROR action", () => { + const action = consoleActions.showError("an error"); + expect(action.type).to.equal(SHOW_ERROR); + expect(action.text).to.equal("an error"); + }); + }); + + describe("showInfo", () => { + it("create CONSOLE_SHOW_INFO action", () => { + const action = consoleActions.showInfo("an info"); + expect(action.type).to.equal(SHOW_INFO); + expect(action.text).to.equal("an info"); + }); + }); + + describe("hideCommand", () => { + it("create CONSOLE_HIDE_COMMAND action", () => { + const action = consoleActions.hideCommand(); + expect(action.type).to.equal(HIDE_COMMAND); + }); + }); +}); diff --git a/test/console/app/reducer.test.ts b/test/console/app/reducer.test.ts new file mode 100644 index 0000000..4406adc --- /dev/null +++ b/test/console/app/reducer.test.ts @@ -0,0 +1,85 @@ +import { expect } from "chai"; +import reducer, { defaultState, State } from "../../../src/console/app/recuer"; +import { + hide, + hideCommand, + showCommand, + showError, + showFind, + showInfo, +} from "../../../src/console/app/actions"; + +describe("app reducer", () => { + describe("hide", () => { + it("switches to none mode", () => { + const initialState: State = { + ...defaultState, + mode: "info", + }; + const nextState = reducer(initialState, hide()); + + expect(nextState.mode).to.be.empty; + }); + }); + + describe("showCommand", () => { + it("switches to command mode with a message", () => { + const nextState = reducer(defaultState, showCommand("open ")); + + expect(nextState.mode).equals("command"); + expect(nextState.consoleText).equals("open "); + }); + }); + + describe("showFind", () => { + it("switches to find mode with a message", () => { + const nextState = reducer(defaultState, showFind()); + + expect(nextState.mode).equals("find"); + }); + }); + + describe("showError", () => { + it("switches to error message mode with a message", () => { + const nextState = reducer(defaultState, showError("error occurs")); + + expect(nextState.mode).equals("error"); + expect(nextState.messageText).equals("error occurs"); + }); + }); + + describe("showInfo", () => { + it("switches to info message mode with a message", () => { + const nextState = reducer(defaultState, showInfo("what's up")); + + expect(nextState.mode).equals("info"); + expect(nextState.messageText).equals("what's up"); + }); + }); + + describe("hideCommand", () => { + describe("when command mode", () => { + it("switches to none mode", () => { + const initialState: State = { + ...defaultState, + mode: "command", + }; + const nextState = reducer(initialState, hideCommand()); + + expect(nextState.mode).to.be.empty; + }); + }); + + describe("when info message mode", () => { + it("does nothing", () => { + const initialState: State = { + ...defaultState, + mode: "info", + }; + const nextState = reducer(initialState, hideCommand()); + + expect(nextState.mode).equals("info"); + }); + }); + }); +}); diff --git a/test/console/reducers/console.test.ts b/test/console/reducers/console.test.ts deleted file mode 100644 index 390dc66..0000000 --- a/test/console/reducers/console.test.ts +++ /dev/null @@ -1,68 +0,0 @@ -import reducer from "../../../src/console/reducers/console"; -import { expect } from "chai"; -import { - CONSOLE_HIDE, - CONSOLE_HIDE_COMMAND, - CONSOLE_SHOW_COMMAND, - CONSOLE_SHOW_ERROR, - CONSOLE_SHOW_INFO, - ConsoleAction, -} from "../../../src/console/actions/console"; - -describe("console reducer", () => { - it("return the initial state", () => { - const state = reducer(undefined, {} as any); - expect(state).to.have.property("mode", ""); - expect(state).to.have.property("messageText", ""); - expect(state).to.have.property("consoleText", ""); - }); - - it("return next state for CONSOLE_HIDE", () => { - const initialState = reducer(undefined, {} as any); - const action: ConsoleAction = { type: CONSOLE_HIDE }; - const state = reducer({ ...initialState, mode: "error" }, action); - expect(state).to.have.property("mode", ""); - }); - - it("return next state for CONSOLE_SHOW_COMMAND", () => { - const action: ConsoleAction = { - type: CONSOLE_SHOW_COMMAND, - text: "open ", - }; - const state = reducer(undefined, action); - expect(state).to.have.property("mode", "command"); - expect(state).to.have.property("consoleText", "open "); - }); - - it("return next state for CONSOLE_SHOW_INFO", () => { - const action: ConsoleAction = { - type: CONSOLE_SHOW_INFO, - text: "an info", - }; - const state = reducer(undefined, action); - expect(state).to.have.property("mode", "info"); - expect(state).to.have.property("messageText", "an info"); - }); - - it("return next state for CONSOLE_SHOW_ERROR", () => { - const action: ConsoleAction = { - type: CONSOLE_SHOW_ERROR, - text: "an error", - }; - const state = reducer(undefined, action); - expect(state).to.have.property("mode", "error"); - expect(state).to.have.property("messageText", "an error"); - }); - - it("return next state for CONSOLE_HIDE_COMMAND", () => { - const initialState = reducer(undefined, {} as any); - const action: ConsoleAction = { - type: CONSOLE_HIDE_COMMAND, - }; - let state = reducer({ ...initialState, mode: "command" }, action); - expect(state).to.have.property("mode", ""); - - state = reducer({ ...initialState, mode: "error" }, action); - expect(state).to.have.property("mode", "error"); - }); -}); -- cgit v1.2.3