From de4e651196502cffa56cedfdaf84d9bb665559e1 Mon Sep 17 00:00:00 2001
From: Shin'ya Ueoka <ueokande@i-beam.org>
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 = <T extends unknown>(
+  dispatch: React.Dispatch<T>
+): React.Dispatch<T | Promise<T>> => {
+  return (action: T | Promise<T>) => {
+    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(
-    <Provider store={store}>
-      <Console></Console>
-    </Provider>,
-    wrapper
-  );
-});
+const RootComponent: React.FC = () => {
+  const [state, dispatch] = React.useReducer(reducers, defaultState);
+
+  React.useEffect(() => {
+    const onMessage = async (message: any): Promise<any> => {
+      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<any> => {
-  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 (
+    <AppContext.Provider value={{ state, dispatch: wrapAsync(dispatch) }}>
+      <Console />
+    </AppContext.Provider>
+  );
 };
 
-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(<RootComponent />, wrapper);
+});
-- 
cgit v1.2.3


From 3a7e55fd292196f600c11fad36425014677a1351 Mon Sep 17 00:00:00 2001
From: Shin'ya Ueoka <ueokande@i-beam.org>
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<CompletionStartCompletionAction> => {
+  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<SetCompletionsAction> => {
+  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<SetCompletionsAction> => {
+  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<SetCompletionsAction> => {
+  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<actions.ShowCommand> => {
-  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<actions.HideCommandAction> => {
+const enterCommand = async (text: string): Promise<HideCommandAction> => {
   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<actions.SetCompletionsAction> => {
-  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<actions.SetCompletionsAction> => {
-  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<actions.SetCompletionsAction> => {
-  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<actions.SetColorSchemeAction> => {
+const setColorScheme = async (): Promise<SetColorSchemeAction> => {
   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<HTMLInputElement>) => {
@@ -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<HTMLInputElement>) => {
-    dispatch(consoleActions.completionNext());
+    completionDispatch(completionActions.completionNext());
     e.stopPropagation();
     e.preventDefault();
   };
 
   const selectPrev = (e: React.KeyboardEvent<HTMLInputElement>) => {
-    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<HTMLInputElement>) => {
     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 (
     <ConsoleWrapper>
       <Completion
         size={COMPLETION_MAX_ITEMS}
-        completions={state.completions}
-        select={state.select}
+        completions={completionState.completions}
+        select={completionState.select}
       />
       <Input
         prompt={":"}
         onBlur={onBlur}
         onKeyDown={onKeyDown}
         onChange={onChange}
-        value={state.consoleText}
+        value={
+          completionState.select < 0
+            ? state.consoleText
+            : completedText(completionState)
+        }
       />
     </ConsoleWrapper>
   );
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 <ueokande@i-beam.org>
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<SetColorSchemeAction> => {
-  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>(
+  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 (
+    <ColorSchemeContext.Provider value={colorscheme}>
+      <ColorSchemeUpdateContext.Provider value={setColorScheme}>
+        <ThemeProvider theme={theme}>{children}</ThemeProvider>
+      </ColorSchemeUpdateContext.Provider>
+    </ColorSchemeContext.Provider>
+  );
+};
+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<ThemeProperties>;
+
+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<HTMLElement> {
-  colorscheme: ColorScheme;
-}
-
-const ColorSchemeProvider: React.FC<Props> = ({ 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 <ThemeProvider theme={theme}>{children}</ThemeProvider>;
-};
-
-export const styled = baseStyled as ThemedStyledInterface<ThemeProperties>;
-
-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 <CommandPrompt />;
-      case "find":
-        return <FindPrompt />;
-      case "info":
-        return <InfoMessage>{state.messageText}</InfoMessage>;
-      case "error":
-        return <ErrorMessage>{state.messageText}</ErrorMessage>;
-      default:
-        return null;
-    }
-  })();
-
-  return (
-    <ColorSchemeProvider colorscheme={state.colorscheme}>
-      {ele}
-    </ColorSchemeProvider>
-  );
+  switch (state.mode) {
+    case "command":
+      return <CommandPrompt />;
+    case "find":
+      return <FindPrompt />;
+    case "info":
+      return <InfoMessage>{state.messageText}</InfoMessage>;
+    case "error":
+      return <ErrorMessage>{state.messageText}</ErrorMessage>;
+    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 = <T extends unknown>(
   dispatch: React.Dispatch<T>
@@ -48,7 +49,9 @@ const RootComponent: React.FC = () => {
 
   return (
     <AppContext.Provider value={{ state, dispatch: wrapAsync(dispatch) }}>
-      <Console />
+      <ColorSchemeProvider>
+        <Console />
+      </ColorSchemeProvider>
     </AppContext.Provider>
   );
 };
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 <ueokande@i-beam.org>
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<HideCommandAction> => {
-  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<State>(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 (
+    <AppStateContext.Provider value={state}>
+      <AppDispatchContext.Provider value={dispatch}>
+        {children}
+      </AppDispatchContext.Provider>
+    </AppStateContext.Provider>
+  );
+};
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<Promise<ConsoleAction> | 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<Props> = ({ 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<Props> = ({ 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<Props> = ({ initialInputValue }) => {
 
   const onKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
     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 <CommandPrompt initialInputValue={state.consoleText} />;
-    case "find":
-      return <FindPrompt />;
-    case "info":
-      return <InfoMessage>{state.messageText}</InfoMessage>;
-    case "error":
-      return <ErrorMessage>{state.messageText}</ErrorMessage>;
-    default:
-      return null;
+  if (visibleCommand) {
+    return <CommandPrompt initialInputValue={initialInputValue} />;
+  } else if (visibleFind) {
+    return <FindPrompt />;
+  } else if (visibleInfo) {
+    return <InfoMessage>{infoMessage}</InfoMessage>;
+  } else if (visibleError) {
+    return <ErrorMessage>{errorMessage}</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<HTMLInputElement>) => {
     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 = <T extends unknown>(
-  dispatch: React.Dispatch<T>
-): React.Dispatch<T | Promise<T>> => {
-  return (action: T | Promise<T>) => {
-    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<any> => {
       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 (
-    <AppContext.Provider value={{ state, dispatch: wrapAsync(dispatch) }}>
-      <ColorSchemeProvider>
-        <Console />
-      </ColorSchemeProvider>
-    </AppContext.Provider>
-  );
+  return <Console />;
 };
 
+const App: React.FC = () => (
+  <AppProvider>
+    <ColorSchemeProvider>
+      <RootComponent />
+    </ColorSchemeProvider>
+  </AppProvider>
+);
+
 window.addEventListener("DOMContentLoaded", () => {
   const wrapper = document.getElementById("vimvixen-console");
-  ReactDOM.render(<RootComponent />, wrapper);
+  ReactDOM.render(<App />, 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

