From 4d043107b80e104c3a029acb405dd69a8371a9a8 Mon Sep 17 00:00:00 2001
From: Shin'ya Ueoka <ueokande@i-beam.org>
Date: Sat, 3 Apr 2021 18:05:20 +0900
Subject: Make Completion as a React.FC

---
 src/console/components/Console.tsx            |   8 --
 src/console/components/console/Completion.tsx | 136 ++++++++++++--------------
 src/console/components/console/Input.tsx      |  60 +++++-------
 src/console/reducers/index.ts                 |   2 -
 4 files changed, 87 insertions(+), 119 deletions(-)

(limited to 'src')

diff --git a/src/console/components/Console.tsx b/src/console/components/Console.tsx
index 18a6632..bb9aee7 100644
--- a/src/console/components/Console.tsx
+++ b/src/console/components/Console.tsx
@@ -28,15 +28,11 @@ interface DispatchProps {
 type Props = StateProps & DispatchProps;
 
 class Console extends React.Component<Props> {
-  private input: React.RefObject<Input>;
-
   private commandLineParser: CommandLineParser = new CommandLineParser();
   private consoleFrameClient = new ConsoleFrameClient();
 
   constructor(props: Props) {
     super(props);
-
-    this.input = React.createRef();
   }
 
   onBlur() {
@@ -167,7 +163,6 @@ class Console extends React.Component<Props> {
                 select={this.props.select}
               />
               <Input
-                ref={this.input}
                 mode={this.props.mode}
                 onBlur={this.onBlur.bind(this)}
                 onKeyDown={this.onKeyDown.bind(this)}
@@ -195,9 +190,6 @@ class Console extends React.Component<Props> {
     this.props.dispatch(consoleActions.setColorScheme());
 
     window.focus();
-    if (this.input.current) {
-      this.input.current.focus();
-    }
   }
 
   private updateCompletions(text: string) {
diff --git a/src/console/components/console/Completion.tsx b/src/console/components/console/Completion.tsx
index 09ae278..ed271aa 100644
--- a/src/console/components/console/Completion.tsx
+++ b/src/console/components/console/Completion.tsx
@@ -19,97 +19,85 @@ interface Props {
   completions: Group[];
 }
 
-interface State {
-  viewOffset: number;
-  select: number;
-}
-
-class Completion extends React.Component<Props, State> {
-  constructor(props: Props) {
-    super(props);
-    this.state = { viewOffset: 0, select: -1 };
-  }
+const Completion: React.FC<Props> = ({ select, size, completions }) => {
+  const [viewOffset, setViewOffset] = React.useState(0);
+  const [prevSelect, setPrevSelect] = React.useState(-1);
 
-  static getDerivedStateFromProps(nextProps: Props, prevState: State) {
-    if (prevState.select === nextProps.select) {
-      return null;
+  React.useEffect(() => {
+    if (select === prevSelect) {
+      return;
     }
 
     const viewSelect = (() => {
       let index = 0;
-      for (let i = 0; i < nextProps.completions.length; ++i) {
+      for (let i = 0; i < completions.length; ++i) {
         ++index;
-        const g = nextProps.completions[i];
-        if (nextProps.select + i + 1 < index + g.items.length) {
-          return nextProps.select + i + 1;
+        const g = completions[i];
+        if (select + i + 1 < index + g.items.length) {
+          return select + i + 1;
         }
         index += g.items.length;
       }
       return -1;
     })();
 
-    let viewOffset = 0;
-    if (nextProps.select < 0) {
-      viewOffset = 0;
-    } else if (prevState.select < nextProps.select) {
-      viewOffset = Math.max(
-        prevState.viewOffset,
-        viewSelect - nextProps.size + 1
-      );
-    } else if (prevState.select > nextProps.select) {
-      viewOffset = Math.min(prevState.viewOffset, viewSelect);
-    }
-    return { viewOffset, select: nextProps.select };
-  }
+    const nextViewOffset = (() => {
+      if (prevSelect < select) {
+        return Math.max(viewOffset, viewSelect - size + 1);
+      } else if (prevSelect > select) {
+        return Math.min(viewOffset, viewSelect);
+      }
+      return 0;
+    })();
+
+    setPrevSelect(select);
+    setViewOffset(nextViewOffset);
+  }, [select]);
 
-  render() {
-    let itemIndex = 0;
-    let viewIndex = 0;
-    const groups: Array<JSX.Element> = [];
-    const viewOffset = this.state.viewOffset;
-    const viewSize = this.props.size;
+  let itemIndex = 0;
+  let viewIndex = 0;
+  const groups: Array<JSX.Element> = [];
 
-    this.props.completions.forEach((group, groupIndex) => {
-      const items = [];
-      const title = (
-        <CompletionTitle
-          id={`title-${groupIndex}`}
-          key={`group-${groupIndex}`}
-          shown={viewOffset <= viewIndex && viewIndex < viewOffset + viewSize}
-          title={group.name}
+  completions.forEach((group, groupIndex) => {
+    const items = [];
+    const title = (
+      <CompletionTitle
+        id={`title-${groupIndex}`}
+        key={`group-${groupIndex}`}
+        shown={viewOffset <= viewIndex && viewIndex < viewOffset + size}
+        title={group.name}
+      />
+    );
+    ++viewIndex;
+    for (const item of group.items) {
+      items.push(
+        <CompletionItem
+          shown={viewOffset <= viewIndex && viewIndex < viewOffset + size}
+          key={`item-${itemIndex}`}
+          icon={item.icon}
+          caption={item.caption}
+          url={item.url}
+          highlight={itemIndex === select}
+          aria-selected={itemIndex === select}
+          role="menuitem"
         />
       );
       ++viewIndex;
-      for (const item of group.items) {
-        items.push(
-          <CompletionItem
-            shown={viewOffset <= viewIndex && viewIndex < viewOffset + viewSize}
-            key={`item-${itemIndex}`}
-            icon={item.icon}
-            caption={item.caption}
-            url={item.url}
-            highlight={itemIndex === this.props.select}
-            aria-selected={itemIndex === this.props.select}
-            role="menuitem"
-          />
-        );
-        ++viewIndex;
-        ++itemIndex;
-      }
-      groups.push(
-        <div
-          key={`group-${groupIndex}`}
-          role="group"
-          aria-describedby={`title-${groupIndex}`}
-        >
-          {title}
-          <ul>{items}</ul>
-        </div>
-      );
-    });
+      ++itemIndex;
+    }
+    groups.push(
+      <div
+        key={`group-${groupIndex}`}
+        role="group"
+        aria-describedby={`title-${groupIndex}`}
+      >
+        {title}
+        <ul>{items}</ul>
+      </div>
+    );
+  });
 
-    return <div role="menu">{groups}</div>;
-  }
-}
+  return <div role="menu">{groups}</div>;
+};
 
 export default Completion;
diff --git a/src/console/components/console/Input.tsx b/src/console/components/console/Input.tsx
index 448b096..bf48d75 100644
--- a/src/console/components/console/Input.tsx
+++ b/src/console/components/console/Input.tsx
@@ -26,42 +26,32 @@ interface Props {
   onChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
 }
 
-class Input extends React.Component<Props> {
-  private input: React.RefObject<HTMLInputElement>;
-
-  constructor(props: Props) {
-    super(props);
-
-    this.input = React.createRef();
-  }
-
-  focus() {
-    if (this.input.current) {
-      this.input.current.focus();
-    }
+const Input: React.FC<Props> = (props) => {
+  const input = React.useRef<HTMLInputElement>(null);
+
+  React.useEffect(() => {
+    input?.current?.focus();
+  }, []);
+
+  let prompt = "";
+  if (props.mode === "command") {
+    prompt = ":";
+  } else if (props.mode === "find") {
+    prompt = "/";
   }
 
-  render() {
-    let prompt = "";
-    if (this.props.mode === "command") {
-      prompt = ":";
-    } else if (this.props.mode === "find") {
-      prompt = "/";
-    }
-
-    return (
-      <Container>
-        <Prompt>{prompt}</Prompt>
-        <InputInner
-          ref={this.input}
-          onBlur={this.props.onBlur}
-          onKeyDown={this.props.onKeyDown}
-          onChange={this.props.onChange}
-          value={this.props.value}
-        />
-      </Container>
-    );
-  }
-}
+  return (
+    <Container>
+      <Prompt>{prompt}</Prompt>
+      <InputInner
+        ref={input}
+        onBlur={props.onBlur}
+        onKeyDown={props.onKeyDown}
+        onChange={props.onChange}
+        value={props.value}
+      />
+    </Container>
+  );
+};
 
 export default Input;
diff --git a/src/console/reducers/index.ts b/src/console/reducers/index.ts
index 752dfd9..dbaf97d 100644
--- a/src/console/reducers/index.ts
+++ b/src/console/reducers/index.ts
@@ -11,7 +11,6 @@ export interface State {
   completionSource: string;
   completions: Completions;
   select: number;
-  viewIndex: number;
   colorscheme: ColorScheme;
 }
 
@@ -23,7 +22,6 @@ const defaultState = {
   completionSource: "",
   completions: [],
   select: -1,
-  viewIndex: 0,
   colorscheme: ColorScheme.System,
 };
 
-- 
cgit v1.2.3


From d0495ce30e9aee853c23d70d512d845d6d131bb0 Mon Sep 17 00:00:00 2001
From: Shin'ya Ueoka <ueokande@i-beam.org>
Date: Sat, 3 Apr 2021 18:05:49 +0900
Subject: Fix props type

---
 src/console/components/console/CompletionItem.tsx  | 6 ++----
 src/console/components/console/CompletionTitle.tsx | 8 ++++----
 2 files changed, 6 insertions(+), 8 deletions(-)

(limited to 'src')

diff --git a/src/console/components/console/CompletionItem.tsx b/src/console/components/console/CompletionItem.tsx
index 5f2f9f6..2451d87 100644
--- a/src/console/components/console/CompletionItem.tsx
+++ b/src/console/components/console/CompletionItem.tsx
@@ -38,7 +38,7 @@ const Description = styled.span`
   overflow: hidden;
 `;
 
-interface Props {
+interface Props extends React.HTMLAttributes<HTMLElement> {
   shown: boolean;
   highlight: boolean;
   caption?: string;
@@ -46,9 +46,7 @@ interface Props {
   icon?: string;
 }
 
-const CompletionItem: React.FC<React.HTMLAttributes<HTMLElement> & Props> = (
-  props
-) => (
+const CompletionItem: React.FC<Props> = (props) => (
   <Container
     icon={props.icon || ""}
     aria-labelledby={`completion-item-${props.caption}`}
diff --git a/src/console/components/console/CompletionTitle.tsx b/src/console/components/console/CompletionTitle.tsx
index ec2fc8b..5594eb3 100644
--- a/src/console/components/console/CompletionTitle.tsx
+++ b/src/console/components/console/CompletionTitle.tsx
@@ -10,13 +10,13 @@ const Li = styled.li<{ shown: boolean }>`
   padding: 0;
 `;
 
-interface Props {
+interface Props extends React.HTMLAttributes<HTMLElement> {
   shown: boolean;
   title: string;
 }
 
-const CompletionTitle: React.FC<React.HTMLAttributes<HTMLElement> & Props> = (
-  props
-) => <Li {...props}>{props.title}</Li>;
+const CompletionTitle: React.FC<Props> = (props) => (
+  <Li {...props}>{props.title}</Li>
+);
 
 export default CompletionTitle;
-- 
cgit v1.2.3


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/components/AppContext.ts |  13 ++
 src/console/components/Console.tsx   | 235 +++++++++++++++++------------------
 src/console/index.tsx                |  79 +++++++-----
 src/console/reducers/index.ts        |   2 +-
 4 files changed, 174 insertions(+), 155 deletions(-)
 create mode 100644 src/console/components/AppContext.ts

(limited to 'src')

diff --git a/src/console/components/AppContext.ts b/src/console/components/AppContext.ts
new file mode 100644
index 0000000..878d00b
--- /dev/null
+++ b/src/console/components/AppContext.ts
@@ -0,0 +1,13 @@
+import React from "react";
+import { State, defaultState } from "../reducers";
+import { ConsoleAction } from "../actions";
+
+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/Console.tsx b/src/console/components/Console.tsx
index bb9aee7..3cccc43 100644
--- a/src/console/components/Console.tsx
+++ b/src/console/components/Console.tsx
@@ -1,10 +1,8 @@
-import { connect } from "react-redux";
 import React from "react";
 import Input from "./console/Input";
 import Completion from "./console/Completion";
 import Message from "./console/Message";
 import * as consoleActions from "../../console/actions/console";
-import { State as AppState } from "../reducers";
 import CommandLineParser, {
   InputPhase,
 } from "../commandline/CommandLineParser";
@@ -14,6 +12,7 @@ import { LightTheme, DarkTheme } from "./Theme";
 import styled from "./Theme";
 import { ThemeProvider } from "styled-components";
 import ConsoleFrameClient from "../clients/ConsoleFrameClient";
+import AppContext from "./AppContext";
 
 const ConsoleWrapper = styled.div`
   border-top: 1px solid gray;
@@ -21,63 +20,54 @@ const ConsoleWrapper = styled.div`
 
 const COMPLETION_MAX_ITEMS = 33;
 
-type StateProps = ReturnType<typeof mapStateToProps>;
-interface DispatchProps {
-  dispatch: (action: any) => void;
-}
-type Props = StateProps & DispatchProps;
+const Console: React.FC = () => {
+  const { state, dispatch } = React.useContext(AppContext);
+  const commandLineParser = new CommandLineParser();
+  const consoleFrameClient = new ConsoleFrameClient();
 
-class Console extends React.Component<Props> {
-  private commandLineParser: CommandLineParser = new CommandLineParser();
-  private consoleFrameClient = new ConsoleFrameClient();
-
-  constructor(props: Props) {
-    super(props);
-  }
-
-  onBlur() {
-    if (this.props.mode === "command" || this.props.mode === "find") {
-      return this.props.dispatch(consoleActions.hideCommand());
+  const onBlur = () => {
+    if (state.mode === "command" || state.mode === "find") {
+      dispatch(consoleActions.hideCommand());
     }
-  }
+  };
 
-  doEnter(e: React.KeyboardEvent<HTMLInputElement>) {
+  const doEnter = (e: React.KeyboardEvent<HTMLInputElement>) => {
     e.stopPropagation();
     e.preventDefault();
 
     const value = (e.target as HTMLInputElement).value;
-    if (this.props.mode === "command") {
-      return this.props.dispatch(consoleActions.enterCommand(value));
-    } else if (this.props.mode === "find") {
-      return this.props.dispatch(
-        consoleActions.enterFind(value === "" ? undefined : value)
-      );
+    if (state.mode === "command") {
+      dispatch(consoleActions.enterCommand(value));
+    } else if (state.mode === "find") {
+      dispatch(consoleActions.enterFind(value === "" ? undefined : value));
     }
-  }
+  };
 
-  selectNext(e: React.KeyboardEvent<HTMLInputElement>) {
-    this.props.dispatch(consoleActions.completionNext());
+  const selectNext = (e: React.KeyboardEvent<HTMLInputElement>) => {
+    dispatch(consoleActions.completionNext());
     e.stopPropagation();
     e.preventDefault();
-  }
+  };
 
-  selectPrev(e: React.KeyboardEvent<HTMLInputElement>) {
-    this.props.dispatch(consoleActions.completionPrev());
+  const selectPrev = (e: React.KeyboardEvent<HTMLInputElement>) => {
+    dispatch(consoleActions.completionPrev());
     e.stopPropagation();
     e.preventDefault();
-  }
+  };
 
-  onKeyDown(e: React.KeyboardEvent<HTMLInputElement>) {
+  const onKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
     switch (e.key) {
       case "Escape":
-        return this.props.dispatch(consoleActions.hideCommand());
+        dispatch(consoleActions.hideCommand());
+        break;
       case "Enter":
-        return this.doEnter(e);
+        doEnter(e);
+        break;
       case "Tab":
         if (e.shiftKey) {
-          this.props.dispatch(consoleActions.completionPrev());
+          dispatch(consoleActions.completionPrev());
         } else {
-          this.props.dispatch(consoleActions.completionNext());
+          dispatch(consoleActions.completionNext());
         }
         e.stopPropagation();
         e.preventDefault();
@@ -85,126 +75,79 @@ class Console extends React.Component<Props> {
       case "[":
         if (e.ctrlKey) {
           e.preventDefault();
-          return this.props.dispatch(consoleActions.hideCommand());
+          dispatch(consoleActions.hideCommand());
         }
         break;
       case "c":
         if (e.ctrlKey) {
           e.preventDefault();
-          return this.props.dispatch(consoleActions.hideCommand());
+          dispatch(consoleActions.hideCommand());
         }
         break;
       case "m":
         if (e.ctrlKey) {
-          return this.doEnter(e);
+          doEnter(e);
         }
         break;
       case "n":
         if (e.ctrlKey) {
-          this.selectNext(e);
+          selectNext(e);
         }
         break;
       case "p":
         if (e.ctrlKey) {
-          this.selectPrev(e);
+          selectPrev(e);
         }
         break;
     }
-  }
+  };
 
-  onChange(e: React.ChangeEvent<HTMLInputElement>) {
+  const onChange = (e: React.ChangeEvent<HTMLInputElement>) => {
     const text = e.target.value;
-    this.props.dispatch(consoleActions.setConsoleText(text));
-    if (this.props.mode !== "command") {
+    dispatch(consoleActions.setConsoleText(text));
+    if (state.mode !== "command") {
       return;
     }
-    this.updateCompletions(text);
-  }
-
-  componentDidUpdate(prevProps: Props) {
-    if (prevProps.mode !== "command" && this.props.mode === "command") {
-      this.updateCompletions(this.props.consoleText);
-      this.focus();
-    } else if (prevProps.mode !== "find" && this.props.mode === "find") {
-      this.focus();
+    updateCompletions(text);
+  };
+
+  const prevState = React.useRef(state);
+  React.useEffect(() => {
+    if (prevState.current.mode !== "command" && state.mode === "command") {
+      updateCompletions(state.consoleText);
+      focus();
+    } else if (prevState.current.mode !== "find" && state.mode === "find") {
+      focus();
     }
 
     const {
       scrollWidth: width,
       scrollHeight: height,
     } = document.getElementById("vimvixen-console")!;
-    this.consoleFrameClient.resize(width, height);
-  }
-
-  render() {
-    let theme = this.props.colorscheme;
-    if (this.props.colorscheme === ColorScheme.System) {
-      if (
-        window.matchMedia &&
-        window.matchMedia("(prefers-color-scheme: dark)").matches
-      ) {
-        theme = ColorScheme.Dark;
-      } else {
-        theme = ColorScheme.Light;
-      }
-    }
+    consoleFrameClient.resize(width, height);
 
-    switch (this.props.mode) {
-      case "command":
-      case "find":
-        return (
-          <ThemeProvider
-            theme={theme === ColorScheme.Dark ? DarkTheme : LightTheme}
-          >
-            <ConsoleWrapper>
-              <Completion
-                size={COMPLETION_MAX_ITEMS}
-                completions={this.props.completions}
-                select={this.props.select}
-              />
-              <Input
-                mode={this.props.mode}
-                onBlur={this.onBlur.bind(this)}
-                onKeyDown={this.onKeyDown.bind(this)}
-                onChange={this.onChange.bind(this)}
-                value={this.props.consoleText}
-              />
-            </ConsoleWrapper>
-          </ThemeProvider>
-        );
-      case "info":
-      case "error":
-        return (
-          <ThemeProvider
-            theme={theme === ColorScheme.Dark ? DarkTheme : LightTheme}
-          >
-            <Message mode={this.props.mode}>{this.props.messageText}</Message>
-          </ThemeProvider>
-        );
-      default:
-        return null;
-    }
-  }
+    prevState.current = state;
+  });
 
-  async focus() {
-    this.props.dispatch(consoleActions.setColorScheme());
+  const focus = () => {
+    dispatch(consoleActions.setColorScheme());
 
     window.focus();
-  }
+  };
 
-  private updateCompletions(text: string) {
-    const phase = this.commandLineParser.inputPhase(text);
+  const updateCompletions = (text: string) => {
+    const phase = commandLineParser.inputPhase(text);
     if (phase === InputPhase.OnCommand) {
-      return this.props.dispatch(consoleActions.getCommandCompletions(text));
+      dispatch(consoleActions.getCommandCompletions(text));
     } else {
-      const cmd = this.commandLineParser.parse(text);
+      const cmd = commandLineParser.parse(text);
       switch (cmd.command) {
         case Command.Open:
         case Command.TabOpen:
         case Command.WindowOpen:
-          this.props.dispatch(
+          dispatch(
             consoleActions.getOpenCompletions(
-              this.props.completionTypes,
+              state.completionTypes,
               text,
               cmd.command,
               cmd.args
@@ -212,32 +155,78 @@ class Console extends React.Component<Props> {
           );
           break;
         case Command.Buffer:
-          this.props.dispatch(
+          dispatch(
             consoleActions.getTabCompletions(text, cmd.command, cmd.args, false)
           );
           break;
         case Command.BufferDelete:
         case Command.BuffersDelete:
-          this.props.dispatch(
+          dispatch(
             consoleActions.getTabCompletions(text, cmd.command, cmd.args, true)
           );
           break;
         case Command.BufferDeleteForce:
         case Command.BuffersDeleteForce:
-          this.props.dispatch(
+          dispatch(
             consoleActions.getTabCompletions(text, cmd.command, cmd.args, false)
           );
           break;
         case Command.Set:
-          this.props.dispatch(
+          dispatch(
             consoleActions.getPropertyCompletions(text, cmd.command, cmd.args)
           );
           break;
       }
     }
+  };
+
+  let theme = state.colorscheme;
+  if (state.colorscheme === ColorScheme.System) {
+    if (
+      window.matchMedia &&
+      window.matchMedia("(prefers-color-scheme: dark)").matches
+    ) {
+      theme = ColorScheme.Dark;
+    } else {
+      theme = ColorScheme.Light;
+    }
   }
-}
 
-const mapStateToProps = (state: AppState) => ({ ...state });
+  switch (state.mode) {
+    case "command":
+    case "find":
+      return (
+        <ThemeProvider
+          theme={theme === ColorScheme.Dark ? DarkTheme : LightTheme}
+        >
+          <ConsoleWrapper>
+            <Completion
+              size={COMPLETION_MAX_ITEMS}
+              completions={state.completions}
+              select={state.select}
+            />
+            <Input
+              mode={state.mode}
+              onBlur={onBlur}
+              onKeyDown={onKeyDown}
+              onChange={onChange}
+              value={state.consoleText}
+            />
+          </ConsoleWrapper>
+        </ThemeProvider>
+      );
+    case "info":
+    case "error":
+      return (
+        <ThemeProvider
+          theme={theme === ColorScheme.Dark ? DarkTheme : LightTheme}
+        >
+          <Message mode={state.mode}>{state.messageText}</Message>
+        </ThemeProvider>
+      );
+    default:
+      return null;
+  }
+};
 
-export default connect(mapStateToProps)(Console);
+export default Console;
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);
+});
diff --git a/src/console/reducers/index.ts b/src/console/reducers/index.ts
index dbaf97d..49d0de1 100644
--- a/src/console/reducers/index.ts
+++ b/src/console/reducers/index.ts
@@ -14,7 +14,7 @@ export interface State {
   colorscheme: ColorScheme;
 }
 
-const defaultState = {
+export const defaultState = {
   mode: "",
   messageText: "",
   consoleText: "",
-- 
cgit v1.2.3


From 20faca581a3b832301c9dca3074b0f61ba163d62 Mon Sep 17 00:00:00 2001
From: Shin'ya Ueoka <ueokande@i-beam.org>
Date: Sun, 4 Apr 2021 14:07:13 +0900
Subject: Extract ttheme as a ColorSchemeProvider

---
 src/console/components/ColorSchemeProvider.tsx     | 76 ++++++++++++++++++++++
 src/console/components/Console.tsx                 | 29 ++-------
 src/console/components/Theme.ts                    | 53 ---------------
 src/console/components/console/CompletionItem.tsx  |  2 +-
 src/console/components/console/CompletionTitle.tsx |  2 +-
 src/console/components/console/Input.tsx           |  2 +-
 src/console/components/console/Message.tsx         |  2 +-
 7 files changed, 85 insertions(+), 81 deletions(-)
 create mode 100644 src/console/components/ColorSchemeProvider.tsx
 delete mode 100644 src/console/components/Theme.ts

(limited to 'src')

diff --git a/src/console/components/ColorSchemeProvider.tsx b/src/console/components/ColorSchemeProvider.tsx
new file mode 100644
index 0000000..bd63571
--- /dev/null
+++ b/src/console/components/ColorSchemeProvider.tsx
@@ -0,0 +1,76 @@
+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 3cccc43..fa91336 100644
--- a/src/console/components/Console.tsx
+++ b/src/console/components/Console.tsx
@@ -7,10 +7,7 @@ import CommandLineParser, {
   InputPhase,
 } from "../commandline/CommandLineParser";
 import { Command } from "../../shared/Command";
-import ColorScheme from "../../shared/ColorScheme";
-import { LightTheme, DarkTheme } from "./Theme";
-import styled from "./Theme";
-import { ThemeProvider } from "styled-components";
+import ColorSchemeProvider, { styled } from "./ColorSchemeProvider";
 import ConsoleFrameClient from "../clients/ConsoleFrameClient";
 import AppContext from "./AppContext";
 
@@ -180,25 +177,11 @@ const Console: React.FC = () => {
     }
   };
 
-  let theme = state.colorscheme;
-  if (state.colorscheme === ColorScheme.System) {
-    if (
-      window.matchMedia &&
-      window.matchMedia("(prefers-color-scheme: dark)").matches
-    ) {
-      theme = ColorScheme.Dark;
-    } else {
-      theme = ColorScheme.Light;
-    }
-  }
-
   switch (state.mode) {
     case "command":
     case "find":
       return (
-        <ThemeProvider
-          theme={theme === ColorScheme.Dark ? DarkTheme : LightTheme}
-        >
+        <ColorSchemeProvider colorscheme={state.colorscheme}>
           <ConsoleWrapper>
             <Completion
               size={COMPLETION_MAX_ITEMS}
@@ -213,16 +196,14 @@ const Console: React.FC = () => {
               value={state.consoleText}
             />
           </ConsoleWrapper>
-        </ThemeProvider>
+        </ColorSchemeProvider>
       );
     case "info":
     case "error":
       return (
-        <ThemeProvider
-          theme={theme === ColorScheme.Dark ? DarkTheme : LightTheme}
-        >
+        <ColorSchemeProvider colorscheme={state.colorscheme}>
           <Message mode={state.mode}>{state.messageText}</Message>
-        </ThemeProvider>
+        </ColorSchemeProvider>
       );
     default:
       return null;
diff --git a/src/console/components/Theme.ts b/src/console/components/Theme.ts
deleted file mode 100644
index dd7baa5..0000000
--- a/src/console/components/Theme.ts
+++ /dev/null
@@ -1,53 +0,0 @@
-import baseStyled, { ThemedStyledInterface } from "styled-components";
-
-type Theme = {
-  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: Theme = {
-  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: Theme = {
-  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",
-};
-
-const styled = baseStyled as ThemedStyledInterface<Theme>;
-
-export default styled;
diff --git a/src/console/components/console/CompletionItem.tsx b/src/console/components/console/CompletionItem.tsx
index 2451d87..7313491 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 "../Theme";
+import { styled } from "../ColorSchemeProvider";
 
 const Container = styled.li<{
   shown: boolean;
diff --git a/src/console/components/console/CompletionTitle.tsx b/src/console/components/console/CompletionTitle.tsx
index 5594eb3..a8e8a54 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 "../Theme";
+import { styled } from "../ColorSchemeProvider";
 
 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 bf48d75..1f56036 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 "../Theme";
+import { styled } from "../ColorSchemeProvider";
 
 const Container = styled.div`
   background-color: ${({ theme }) => theme.commandBackground};
diff --git a/src/console/components/console/Message.tsx b/src/console/components/console/Message.tsx
index 73498fd..8dcdb5a 100644
--- a/src/console/components/console/Message.tsx
+++ b/src/console/components/console/Message.tsx
@@ -1,5 +1,5 @@
 import React from "react";
-import styled from "../Theme";
+import { styled } from "../ColorSchemeProvider";
 
 const Error = styled.p`
   border-top: 1px solid gray;
-- 
cgit v1.2.3


From 2b1079525b86dad45df11e6ad29a89989a6416ab Mon Sep 17 00:00:00 2001
From: Shin'ya Ueoka <ueokande@i-beam.org>
Date: Sun, 4 Apr 2021 16:44:47 +0900
Subject: Separate Message to InfoMessage and ErrorMessage

---
 src/console/components/Console.tsx               | 10 +++++--
 src/console/components/ErrorMessage.tsx          | 15 +++++++++++
 src/console/components/InfoMessage.tsx           | 15 +++++++++++
 src/console/components/console/Message.tsx       | 33 ------------------------
 test/console/components/ErrorMessage.test.tsx    | 17 ++++++++++++
 test/console/components/InfoMessage.test.tsx     | 17 ++++++++++++
 test/console/components/console/Message.test.tsx | 27 -------------------
 7 files changed, 72 insertions(+), 62 deletions(-)
 create mode 100644 src/console/components/ErrorMessage.tsx
 create mode 100644 src/console/components/InfoMessage.tsx
 delete mode 100644 src/console/components/console/Message.tsx
 create mode 100644 test/console/components/ErrorMessage.test.tsx
 create mode 100644 test/console/components/InfoMessage.test.tsx
 delete mode 100644 test/console/components/console/Message.test.tsx

(limited to 'src')

diff --git a/src/console/components/Console.tsx b/src/console/components/Console.tsx
index fa91336..951b627 100644
--- a/src/console/components/Console.tsx
+++ b/src/console/components/Console.tsx
@@ -1,7 +1,8 @@
 import React from "react";
 import Input from "./console/Input";
 import Completion from "./console/Completion";
-import Message from "./console/Message";
+import InfoMessage from "./InfoMessage";
+import ErrorMessage from "./ErrorMessage";
 import * as consoleActions from "../../console/actions/console";
 import CommandLineParser, {
   InputPhase,
@@ -199,10 +200,15 @@ const Console: React.FC = () => {
         </ColorSchemeProvider>
       );
     case "info":
+      return (
+        <ColorSchemeProvider colorscheme={state.colorscheme}>
+          <InfoMessage>{state.messageText}</InfoMessage>
+        </ColorSchemeProvider>
+      );
     case "error":
       return (
         <ColorSchemeProvider colorscheme={state.colorscheme}>
-          <Message mode={state.mode}>{state.messageText}</Message>
+          <ErrorMessage>{state.messageText}</ErrorMessage>
         </ColorSchemeProvider>
       );
     default:
diff --git a/src/console/components/ErrorMessage.tsx b/src/console/components/ErrorMessage.tsx
new file mode 100644
index 0000000..93b049b
--- /dev/null
+++ b/src/console/components/ErrorMessage.tsx
@@ -0,0 +1,15 @@
+import React from "react";
+import { styled } from "./ColorSchemeProvider";
+
+const Wrapper = styled.p`
+  border-top: 1px solid gray;
+  background-color: ${({ theme }) => theme.consoleErrorBackground};
+  color: ${({ theme }) => theme.consoleErrorForeground};
+  font-weight: bold;
+`;
+
+const ErrorMessage: React.FC = ({ children }) => {
+  return <Wrapper role="alert">{children}</Wrapper>;
+};
+
+export default ErrorMessage;
diff --git a/src/console/components/InfoMessage.tsx b/src/console/components/InfoMessage.tsx
new file mode 100644
index 0000000..02ad27d
--- /dev/null
+++ b/src/console/components/InfoMessage.tsx
@@ -0,0 +1,15 @@
+import React from "react";
+import { styled } from "./ColorSchemeProvider";
+
+const Wrapper = styled.p`
+  border-top: 1px solid gray;
+  background-color: ${({ theme }) => theme.consoleInfoBackground};
+  color: ${({ theme }) => theme.consoleInfoForeground};
+  font-weight: normal;
+`;
+
+const InfoMessage: React.FC = ({ children }) => {
+  return <Wrapper role="status">{children}</Wrapper>;
+};
+
+export default InfoMessage;
diff --git a/src/console/components/console/Message.tsx b/src/console/components/console/Message.tsx
deleted file mode 100644
index 8dcdb5a..0000000
--- a/src/console/components/console/Message.tsx
+++ /dev/null
@@ -1,33 +0,0 @@
-import React from "react";
-import { styled } from "../ColorSchemeProvider";
-
-const Error = styled.p`
-  border-top: 1px solid gray;
-  background-color: ${({ theme }) => theme.consoleErrorBackground};
-  color: ${({ theme }) => theme.consoleErrorForeground};
-  font-weight: bold;
-`;
-
-const Info = styled.p`
-  border-top: 1px solid gray;
-  background-color: ${({ theme }) => theme.consoleInfoBackground};
-  color: ${({ theme }) => theme.consoleInfoForeground};
-  font-weight: normal;
-`;
-
-interface Props {
-  mode: string;
-  children: string;
-}
-
-const Message: React.FC<Props> = ({ mode, children }) => {
-  switch (mode) {
-    case "error":
-      return <Error role="alert">{children}</Error>;
-    case "info":
-      return <Info role="status">{children}</Info>;
-  }
-  return null;
-};
-
-export default Message;
diff --git a/test/console/components/ErrorMessage.test.tsx b/test/console/components/ErrorMessage.test.tsx
new file mode 100644
index 0000000..46ec0b0
--- /dev/null
+++ b/test/console/components/ErrorMessage.test.tsx
@@ -0,0 +1,17 @@
+import React from "react";
+import ReactTestRenderer from "react-test-renderer";
+import { expect } from "chai";
+import ErrorMessage from "../../../src/console/components/ErrorMessage";
+
+describe("console/components/console/completion/ErrorMessage", () => {
+  it("renders an error message", () => {
+    const root = ReactTestRenderer.create(
+      <ErrorMessage mode="error">Hello!</ErrorMessage>
+    ).root;
+
+    const p = root.findByType("p");
+
+    expect(p.props["role"]).to.equal("alert");
+    expect(p.children).to.deep.equal(["Hello!"]);
+  });
+});
diff --git a/test/console/components/InfoMessage.test.tsx b/test/console/components/InfoMessage.test.tsx
new file mode 100644
index 0000000..5b678ff
--- /dev/null
+++ b/test/console/components/InfoMessage.test.tsx
@@ -0,0 +1,17 @@
+import React from "react";
+import ReactTestRenderer from "react-test-renderer";
+import { expect } from "chai";
+import InfoMessage from "../../../src/console/components/InfoMessage";
+
+describe("console/components/console/completion/InfoMessage", () => {
+  it("renders an information message", () => {
+    const root = ReactTestRenderer.create(
+      <InfoMessage mode="info">Hello!</InfoMessage>
+    ).root;
+
+    const p = root.findByType("p");
+
+    expect(p.props["role"]).to.equal("status");
+    expect(p.children).to.deep.equal(["Hello!"]);
+  });
+});
diff --git a/test/console/components/console/Message.test.tsx b/test/console/components/console/Message.test.tsx
deleted file mode 100644
index f8f950a..0000000
--- a/test/console/components/console/Message.test.tsx
+++ /dev/null
@@ -1,27 +0,0 @@
-import React from "react";
-import ReactTestRenderer from "react-test-renderer";
-import { expect } from "chai";
-import Message from "../../../../src/console/components/console/Message";
-
-describe("console/components/console/completion/Message", () => {
-  it("renders an information message", () => {
-    const root = ReactTestRenderer.create(<Message mode="info">Hello!</Message>)
-      .root;
-
-    const p = root.findByType("p");
-
-    expect(p.props["role"]).to.equal("status");
-    expect(p.children).to.deep.equal(["Hello!"]);
-  });
-
-  it("renders an error message", () => {
-    const root = ReactTestRenderer.create(
-      <Message mode="error">Hello!</Message>
-    ).root;
-
-    const p = root.findByType("p");
-
-    expect(p.props["role"]).to.equal("alert");
-    expect(p.children).to.deep.equal(["Hello!"]);
-  });
-});
-- 
cgit v1.2.3


From 598b9914c21d609dedfa0a7a8d7d2b774d0bbc6d Mon Sep 17 00:00:00 2001
From: Shin'ya Ueoka <ueokande@i-beam.org>
Date: Sun, 4 Apr 2021 17:44:52 +0900
Subject: Separate FindPrompt

---
 src/console/components/Console.tsx       | 46 +++++++++++-----------
 src/console/components/FindPrompt.tsx    | 67 ++++++++++++++++++++++++++++++++
 src/console/components/console/Input.tsx | 11 +-----
 3 files changed, 92 insertions(+), 32 deletions(-)
 create mode 100644 src/console/components/FindPrompt.tsx

(limited to 'src')

diff --git a/src/console/components/Console.tsx b/src/console/components/Console.tsx
index 951b627..d4c9151 100644
--- a/src/console/components/Console.tsx
+++ b/src/console/components/Console.tsx
@@ -1,6 +1,7 @@
 import React from "react";
 import Input from "./console/Input";
 import Completion from "./console/Completion";
+import FindPrompt from "./FindPrompt";
 import InfoMessage from "./InfoMessage";
 import ErrorMessage from "./ErrorMessage";
 import * as consoleActions from "../../console/actions/console";
@@ -178,11 +179,10 @@ const Console: React.FC = () => {
     }
   };
 
-  switch (state.mode) {
-    case "command":
-    case "find":
-      return (
-        <ColorSchemeProvider colorscheme={state.colorscheme}>
+  const ele = (() => {
+    switch (state.mode) {
+      case "command":
+        return (
           <ConsoleWrapper>
             <Completion
               size={COMPLETION_MAX_ITEMS}
@@ -190,30 +190,30 @@ const Console: React.FC = () => {
               select={state.select}
             />
             <Input
-              mode={state.mode}
+              prompt={":"}
               onBlur={onBlur}
               onKeyDown={onKeyDown}
               onChange={onChange}
               value={state.consoleText}
             />
           </ConsoleWrapper>
-        </ColorSchemeProvider>
-      );
-    case "info":
-      return (
-        <ColorSchemeProvider colorscheme={state.colorscheme}>
-          <InfoMessage>{state.messageText}</InfoMessage>
-        </ColorSchemeProvider>
-      );
-    case "error":
-      return (
-        <ColorSchemeProvider colorscheme={state.colorscheme}>
-          <ErrorMessage>{state.messageText}</ErrorMessage>
-        </ColorSchemeProvider>
-      );
-    default:
-      return null;
-  }
+        );
+      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>
+  );
 };
 
 export default Console;
diff --git a/src/console/components/FindPrompt.tsx b/src/console/components/FindPrompt.tsx
new file mode 100644
index 0000000..c6a0489
--- /dev/null
+++ b/src/console/components/FindPrompt.tsx
@@ -0,0 +1,67 @@
+import React from "react";
+import * as consoleActions from "../../console/actions/console";
+import ConsoleFrameClient from "../clients/ConsoleFrameClient";
+import AppContext from "./AppContext";
+import Input from "./console/Input";
+import styled from "styled-components";
+
+const ConsoleWrapper = styled.div`
+  border-top: 1px solid gray;
+`;
+
+const FindPrompt: React.FC = () => {
+  const { dispatch } = React.useContext(AppContext);
+  const [inputValue, setInputValue] = React.useState("");
+
+  const consoleFrameClient = new ConsoleFrameClient();
+  const onBlur = () => {
+    dispatch(consoleActions.hideCommand());
+  };
+
+  const doEnter = (e: React.KeyboardEvent<HTMLInputElement>) => {
+    e.stopPropagation();
+    e.preventDefault();
+
+    const value = (e.target as HTMLInputElement).value;
+    dispatch(consoleActions.enterFind(value === "" ? undefined : value));
+  };
+
+  const onKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
+    switch (e.key) {
+      case "Escape":
+        dispatch(consoleActions.hideCommand());
+        break;
+      case "Enter":
+        doEnter(e);
+        break;
+    }
+  };
+
+  const onChange = (e: React.ChangeEvent<HTMLInputElement>) => {
+    setInputValue(e.target.value);
+  };
+
+  React.useEffect(() => {
+    window.focus();
+
+    const {
+      scrollWidth: width,
+      scrollHeight: height,
+    } = document.getElementById("vimvixen-console")!;
+    consoleFrameClient.resize(width, height);
+  }, []);
+
+  return (
+    <ConsoleWrapper>
+      <Input
+        prompt={"/"}
+        onBlur={onBlur}
+        onKeyDown={onKeyDown}
+        onChange={onChange}
+        value={inputValue}
+      />
+    </ConsoleWrapper>
+  );
+};
+
+export default FindPrompt;
diff --git a/src/console/components/console/Input.tsx b/src/console/components/console/Input.tsx
index 1f56036..7850f43 100644
--- a/src/console/components/console/Input.tsx
+++ b/src/console/components/console/Input.tsx
@@ -19,7 +19,7 @@ const InputInner = styled.input`
 `;
 
 interface Props {
-  mode: string;
+  prompt: string;
   value: string;
   onBlur: (e: React.FocusEvent<HTMLInputElement>) => void;
   onKeyDown: (e: React.KeyboardEvent<HTMLInputElement>) => void;
@@ -33,16 +33,9 @@ const Input: React.FC<Props> = (props) => {
     input?.current?.focus();
   }, []);
 
-  let prompt = "";
-  if (props.mode === "command") {
-    prompt = ":";
-  } else if (props.mode === "find") {
-    prompt = "/";
-  }
-
   return (
     <Container>
-      <Prompt>{prompt}</Prompt>
+      <Prompt>{props.prompt}</Prompt>
       <InputInner
         ref={input}
         onBlur={props.onBlur}
-- 
cgit v1.2.3


From 39f96db5a3187b4ce2e7df529eaa456ee862dd68 Mon Sep 17 00:00:00 2001
From: Shin'ya Ueoka <ueokande@i-beam.org>
Date: Sun, 4 Apr 2021 18:15:59 +0900
Subject: Separate as CommandPrompt

---
 src/console/components/CommandPrompt.tsx | 182 ++++++++++++++++++++++++++++++
 src/console/components/Console.tsx       | 187 +------------------------------
 src/console/components/FindPrompt.tsx    |   2 -
 3 files changed, 186 insertions(+), 185 deletions(-)
 create mode 100644 src/console/components/CommandPrompt.tsx

(limited to 'src')

diff --git a/src/console/components/CommandPrompt.tsx b/src/console/components/CommandPrompt.tsx
new file mode 100644
index 0000000..d69fae6
--- /dev/null
+++ b/src/console/components/CommandPrompt.tsx
@@ -0,0 +1,182 @@
+import React from "react";
+import * as consoleActions from "../../console/actions/console";
+import AppContext from "./AppContext";
+import CommandLineParser, {
+  InputPhase,
+} from "../commandline/CommandLineParser";
+import Completion from "./console/Completion";
+import ConsoleFrameClient from "../clients/ConsoleFrameClient";
+import Input from "./console//Input";
+import { Command } from "../../shared/Command";
+import styled from "styled-components";
+
+const COMPLETION_MAX_ITEMS = 33;
+
+const ConsoleWrapper = styled.div`
+  border-top: 1px solid gray;
+`;
+
+const CommandPrompt: React.FC = () => {
+  const { state, dispatch } = React.useContext(AppContext);
+  const commandLineParser = new CommandLineParser();
+  const consoleFrameClient = new ConsoleFrameClient();
+
+  const onBlur = () => {
+    if (state.mode === "command" || state.mode === "find") {
+      dispatch(consoleActions.hideCommand());
+    }
+  };
+
+  const doEnter = (e: React.KeyboardEvent<HTMLInputElement>) => {
+    e.stopPropagation();
+    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));
+    }
+  };
+
+  const selectNext = (e: React.KeyboardEvent<HTMLInputElement>) => {
+    dispatch(consoleActions.completionNext());
+    e.stopPropagation();
+    e.preventDefault();
+  };
+
+  const selectPrev = (e: React.KeyboardEvent<HTMLInputElement>) => {
+    dispatch(consoleActions.completionPrev());
+    e.stopPropagation();
+    e.preventDefault();
+  };
+
+  const onKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
+    switch (e.key) {
+      case "Escape":
+        dispatch(consoleActions.hideCommand());
+        break;
+      case "Enter":
+        doEnter(e);
+        break;
+      case "Tab":
+        if (e.shiftKey) {
+          dispatch(consoleActions.completionPrev());
+        } else {
+          dispatch(consoleActions.completionNext());
+        }
+        e.stopPropagation();
+        e.preventDefault();
+        break;
+      case "[":
+        if (e.ctrlKey) {
+          e.preventDefault();
+          dispatch(consoleActions.hideCommand());
+        }
+        break;
+      case "c":
+        if (e.ctrlKey) {
+          e.preventDefault();
+          dispatch(consoleActions.hideCommand());
+        }
+        break;
+      case "m":
+        if (e.ctrlKey) {
+          doEnter(e);
+        }
+        break;
+      case "n":
+        if (e.ctrlKey) {
+          selectNext(e);
+        }
+        break;
+      case "p":
+        if (e.ctrlKey) {
+          selectPrev(e);
+        }
+        break;
+    }
+  };
+
+  const onChange = (e: React.ChangeEvent<HTMLInputElement>) => {
+    const text = e.target.value;
+    dispatch(consoleActions.setConsoleText(text));
+    updateCompletions(text);
+  };
+
+  React.useEffect(() => {
+    updateCompletions(state.consoleText);
+  }, []);
+
+  React.useEffect(() => {
+    const {
+      scrollWidth: width,
+      scrollHeight: height,
+    } = document.getElementById("vimvixen-console")!;
+    consoleFrameClient.resize(width, height);
+  });
+
+  const updateCompletions = (text: string) => {
+    const phase = commandLineParser.inputPhase(text);
+    if (phase === InputPhase.OnCommand) {
+      dispatch(consoleActions.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
+            )
+          );
+          break;
+        case Command.Buffer:
+          dispatch(
+            consoleActions.getTabCompletions(text, cmd.command, cmd.args, false)
+          );
+          break;
+        case Command.BufferDelete:
+        case Command.BuffersDelete:
+          dispatch(
+            consoleActions.getTabCompletions(text, cmd.command, cmd.args, true)
+          );
+          break;
+        case Command.BufferDeleteForce:
+        case Command.BuffersDeleteForce:
+          dispatch(
+            consoleActions.getTabCompletions(text, cmd.command, cmd.args, false)
+          );
+          break;
+        case Command.Set:
+          dispatch(
+            consoleActions.getPropertyCompletions(text, cmd.command, cmd.args)
+          );
+          break;
+      }
+    }
+  };
+
+  return (
+    <ConsoleWrapper>
+      <Completion
+        size={COMPLETION_MAX_ITEMS}
+        completions={state.completions}
+        select={state.select}
+      />
+      <Input
+        prompt={":"}
+        onBlur={onBlur}
+        onKeyDown={onKeyDown}
+        onChange={onChange}
+        value={state.consoleText}
+      />
+    </ConsoleWrapper>
+  );
+};
+
+export default CommandPrompt;
diff --git a/src/console/components/Console.tsx b/src/console/components/Console.tsx
index d4c9151..8a1f73c 100644
--- a/src/console/components/Console.tsx
+++ b/src/console/components/Console.tsx
@@ -1,203 +1,24 @@
 import React from "react";
-import Input from "./console/Input";
-import Completion from "./console/Completion";
 import FindPrompt from "./FindPrompt";
+import CommandPrompt from "./CommandPrompt";
 import InfoMessage from "./InfoMessage";
 import ErrorMessage from "./ErrorMessage";
 import * as consoleActions from "../../console/actions/console";
-import CommandLineParser, {
-  InputPhase,
-} from "../commandline/CommandLineParser";
-import { Command } from "../../shared/Command";
-import ColorSchemeProvider, { styled } from "./ColorSchemeProvider";
-import ConsoleFrameClient from "../clients/ConsoleFrameClient";
+import ColorSchemeProvider from "./ColorSchemeProvider";
 import AppContext from "./AppContext";
 
-const ConsoleWrapper = styled.div`
-  border-top: 1px solid gray;
-`;
-
-const COMPLETION_MAX_ITEMS = 33;
-
 const Console: React.FC = () => {
   const { state, dispatch } = React.useContext(AppContext);
-  const commandLineParser = new CommandLineParser();
-  const consoleFrameClient = new ConsoleFrameClient();
-
-  const onBlur = () => {
-    if (state.mode === "command" || state.mode === "find") {
-      dispatch(consoleActions.hideCommand());
-    }
-  };
-
-  const doEnter = (e: React.KeyboardEvent<HTMLInputElement>) => {
-    e.stopPropagation();
-    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));
-    }
-  };
-
-  const selectNext = (e: React.KeyboardEvent<HTMLInputElement>) => {
-    dispatch(consoleActions.completionNext());
-    e.stopPropagation();
-    e.preventDefault();
-  };
-
-  const selectPrev = (e: React.KeyboardEvent<HTMLInputElement>) => {
-    dispatch(consoleActions.completionPrev());
-    e.stopPropagation();
-    e.preventDefault();
-  };
-
-  const onKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
-    switch (e.key) {
-      case "Escape":
-        dispatch(consoleActions.hideCommand());
-        break;
-      case "Enter":
-        doEnter(e);
-        break;
-      case "Tab":
-        if (e.shiftKey) {
-          dispatch(consoleActions.completionPrev());
-        } else {
-          dispatch(consoleActions.completionNext());
-        }
-        e.stopPropagation();
-        e.preventDefault();
-        break;
-      case "[":
-        if (e.ctrlKey) {
-          e.preventDefault();
-          dispatch(consoleActions.hideCommand());
-        }
-        break;
-      case "c":
-        if (e.ctrlKey) {
-          e.preventDefault();
-          dispatch(consoleActions.hideCommand());
-        }
-        break;
-      case "m":
-        if (e.ctrlKey) {
-          doEnter(e);
-        }
-        break;
-      case "n":
-        if (e.ctrlKey) {
-          selectNext(e);
-        }
-        break;
-      case "p":
-        if (e.ctrlKey) {
-          selectPrev(e);
-        }
-        break;
-    }
-  };
-
-  const onChange = (e: React.ChangeEvent<HTMLInputElement>) => {
-    const text = e.target.value;
-    dispatch(consoleActions.setConsoleText(text));
-    if (state.mode !== "command") {
-      return;
-    }
-    updateCompletions(text);
-  };
 
-  const prevState = React.useRef(state);
   React.useEffect(() => {
-    if (prevState.current.mode !== "command" && state.mode === "command") {
-      updateCompletions(state.consoleText);
-      focus();
-    } else if (prevState.current.mode !== "find" && state.mode === "find") {
-      focus();
-    }
-
-    const {
-      scrollWidth: width,
-      scrollHeight: height,
-    } = document.getElementById("vimvixen-console")!;
-    consoleFrameClient.resize(width, height);
-
-    prevState.current = state;
-  });
-
-  const focus = () => {
     dispatch(consoleActions.setColorScheme());
-
     window.focus();
-  };
-
-  const updateCompletions = (text: string) => {
-    const phase = commandLineParser.inputPhase(text);
-    if (phase === InputPhase.OnCommand) {
-      dispatch(consoleActions.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
-            )
-          );
-          break;
-        case Command.Buffer:
-          dispatch(
-            consoleActions.getTabCompletions(text, cmd.command, cmd.args, false)
-          );
-          break;
-        case Command.BufferDelete:
-        case Command.BuffersDelete:
-          dispatch(
-            consoleActions.getTabCompletions(text, cmd.command, cmd.args, true)
-          );
-          break;
-        case Command.BufferDeleteForce:
-        case Command.BuffersDeleteForce:
-          dispatch(
-            consoleActions.getTabCompletions(text, cmd.command, cmd.args, false)
-          );
-          break;
-        case Command.Set:
-          dispatch(
-            consoleActions.getPropertyCompletions(text, cmd.command, cmd.args)
-          );
-          break;
-      }
-    }
-  };
+  }, []);
 
   const ele = (() => {
     switch (state.mode) {
       case "command":
-        return (
-          <ConsoleWrapper>
-            <Completion
-              size={COMPLETION_MAX_ITEMS}
-              completions={state.completions}
-              select={state.select}
-            />
-            <Input
-              prompt={":"}
-              onBlur={onBlur}
-              onKeyDown={onKeyDown}
-              onChange={onChange}
-              value={state.consoleText}
-            />
-          </ConsoleWrapper>
-        );
+        return <CommandPrompt />;
       case "find":
         return <FindPrompt />;
       case "info":
diff --git a/src/console/components/FindPrompt.tsx b/src/console/components/FindPrompt.tsx
index c6a0489..c79e4d3 100644
--- a/src/console/components/FindPrompt.tsx
+++ b/src/console/components/FindPrompt.tsx
@@ -42,8 +42,6 @@ const FindPrompt: React.FC = () => {
   };
 
   React.useEffect(() => {
-    window.focus();
-
     const {
       scrollWidth: width,
       scrollHeight: height,
-- 
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')

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 9041bae89f54efce14239768e642f99d1f0b35d1 Mon Sep 17 00:00:00 2001
From: Shin'ya Ueoka <ueokande@i-beam.org>
Date: Sun, 4 Apr 2021 23:06:00 +0900
Subject: Make resize to a custom hook

---
 src/console/components/CommandPrompt.tsx | 17 +++--------------
 src/console/components/FindPrompt.tsx    | 13 +++----------
 src/console/hooks/useAutoResize.ts       | 28 ++++++++++++++++++++++++++++
 3 files changed, 34 insertions(+), 24 deletions(-)
 create mode 100644 src/console/hooks/useAutoResize.ts

(limited to 'src')

diff --git a/src/console/components/CommandPrompt.tsx b/src/console/components/CommandPrompt.tsx
index 4e02668..f6f4d8f 100644
--- a/src/console/components/CommandPrompt.tsx
+++ b/src/console/components/CommandPrompt.tsx
@@ -6,12 +6,12 @@ import CommandLineParser, {
   InputPhase,
 } from "../commandline/CommandLineParser";
 import Completion from "./console/Completion";
-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";
+import useAutoResize from "../hooks/useAutoResize";
 
 const COMPLETION_MAX_ITEMS = 33;
 
@@ -26,7 +26,8 @@ const CommandPrompt: React.FC = () => {
     defaultState
   );
   const commandLineParser = new CommandLineParser();
-  const consoleFrameClient = new ConsoleFrameClient();
+
+  useAutoResize();
 
   const onBlur = () => {
     dispatch(consoleActions.hideCommand());
@@ -106,12 +107,6 @@ const CommandPrompt: React.FC = () => {
     Promise.resolve(action).then((a) => {
       if (a) {
         completionDispatch(a);
-
-        const {
-          scrollWidth: width,
-          scrollHeight: height,
-        } = document.getElementById("vimvixen-console")!;
-        consoleFrameClient.resize(width, height);
       }
     });
   };
@@ -127,12 +122,6 @@ const CommandPrompt: React.FC = () => {
       Promise.resolve(completionAction).then((a) => {
         if (a) {
           completionDispatch(a);
-
-          const {
-            scrollWidth: width,
-            scrollHeight: height,
-          } = document.getElementById("vimvixen-console")!;
-          consoleFrameClient.resize(width, height);
         }
       });
     });
diff --git a/src/console/components/FindPrompt.tsx b/src/console/components/FindPrompt.tsx
index c79e4d3..10fa6c3 100644
--- a/src/console/components/FindPrompt.tsx
+++ b/src/console/components/FindPrompt.tsx
@@ -1,9 +1,9 @@
 import React from "react";
 import * as consoleActions from "../../console/actions/console";
-import ConsoleFrameClient from "../clients/ConsoleFrameClient";
 import AppContext from "./AppContext";
 import Input from "./console/Input";
 import styled from "styled-components";
+import useAutoResize from "../hooks/useAutoResize";
 
 const ConsoleWrapper = styled.div`
   border-top: 1px solid gray;
@@ -13,11 +13,12 @@ const FindPrompt: React.FC = () => {
   const { dispatch } = React.useContext(AppContext);
   const [inputValue, setInputValue] = React.useState("");
 
-  const consoleFrameClient = new ConsoleFrameClient();
   const onBlur = () => {
     dispatch(consoleActions.hideCommand());
   };
 
+  useAutoResize();
+
   const doEnter = (e: React.KeyboardEvent<HTMLInputElement>) => {
     e.stopPropagation();
     e.preventDefault();
@@ -41,14 +42,6 @@ const FindPrompt: React.FC = () => {
     setInputValue(e.target.value);
   };
 
-  React.useEffect(() => {
-    const {
-      scrollWidth: width,
-      scrollHeight: height,
-    } = document.getElementById("vimvixen-console")!;
-    consoleFrameClient.resize(width, height);
-  }, []);
-
   return (
     <ConsoleWrapper>
       <Input
diff --git a/src/console/hooks/useAutoResize.ts b/src/console/hooks/useAutoResize.ts
new file mode 100644
index 0000000..4253606
--- /dev/null
+++ b/src/console/hooks/useAutoResize.ts
@@ -0,0 +1,28 @@
+import React from "react";
+import ConsoleFrameClient from "../clients/ConsoleFrameClient";
+
+const useAutoResize = () => {
+  const [prevWidth, setPrevWidth] = React.useState(-1);
+  const [prevHeight, setPrevHeight] = React.useState(-1);
+
+  const consoleFrameClient = React.useMemo(() => {
+    return new ConsoleFrameClient();
+  }, []);
+
+  React.useLayoutEffect(() => {
+    const {
+      scrollWidth: width,
+      scrollHeight: height,
+    } = document.getElementById("vimvixen-console")!;
+    consoleFrameClient.resize(width, height);
+
+    if (width === prevWidth && height === prevHeight) {
+      return;
+    }
+
+    setPrevWidth(width);
+    setPrevHeight(height);
+  });
+};
+
+export default useAutoResize;
-- 
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')

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 618fb497c443662531eb3befe7696a04efe9651d Mon Sep 17 00:00:00 2001
From: Shin'ya Ueoka <ueokande@i-beam.org>
Date: Sun, 11 Apr 2021 18:00:51 +0900
Subject: Replace completion state with Custom Hooks

---
 src/console/actions/completion.ts        | 243 ---------------------------
 src/console/actions/console.ts           |  17 +-
 src/console/completion/actions.ts        |  76 +++++++++
 src/console/completion/context.ts        |   9 +
 src/console/completion/hooks.ts          | 277 +++++++++++++++++++++++++++++++
 src/console/completion/provider.tsx      |  25 +++
 src/console/completion/reducer.ts        |  96 +++++++++++
 src/console/components/CommandPrompt.tsx | 219 +++++++-----------------
 src/console/components/Console.tsx       |   2 +-
 src/console/reducers/completion.ts       |  99 -----------
 src/console/reducers/console.ts          |   3 -
 test/console/actions/completion.test.ts  |  28 ----
 test/console/actions/console.test.ts     |   9 -
 test/console/completion/reducer.test.ts  | 168 +++++++++++++++++++
 test/console/reducers/completion.test.ts | 102 ------------
 test/console/reducers/console.test.ts    |  11 --
 16 files changed, 717 insertions(+), 667 deletions(-)
 delete mode 100644 src/console/actions/completion.ts
 create mode 100644 src/console/completion/actions.ts
 create mode 100644 src/console/completion/context.ts
 create mode 100644 src/console/completion/hooks.ts
 create mode 100644 src/console/completion/provider.tsx
 create mode 100644 src/console/completion/reducer.ts
 delete mode 100644 src/console/reducers/completion.ts
 delete mode 100644 test/console/actions/completion.test.ts
 create mode 100644 test/console/completion/reducer.test.ts
 delete mode 100644 test/console/reducers/completion.test.ts

(limited to 'src')

diff --git a/src/console/actions/completion.ts b/src/console/actions/completion.ts
deleted file mode 100644
index 2f6f82f..0000000
--- a/src/console/actions/completion.ts
+++ /dev/null
@@ -1,243 +0,0 @@
-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 bce2c67..2338067 100644
--- a/src/console/actions/console.ts
+++ b/src/console/actions/console.ts
@@ -4,7 +4,6 @@ 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_HIDE = "console.hide";
 
@@ -35,19 +34,13 @@ export interface HideCommandAction {
   type: typeof CONSOLE_HIDE_COMMAND;
 }
 
-export interface SetConsoleTextAction {
-  type: typeof CONSOLE_SET_CONSOLE_TEXT;
-  consoleText: string;
-}
-
 export type ConsoleAction =
   | HideAction
   | ShowCommand
   | ShowFindAction
   | ShowErrorAction
   | ShowInfoAction
-  | HideCommandAction
-  | SetConsoleTextAction;
+  | HideCommandAction;
 
 const hide = (): ConsoleAction => {
   return {
@@ -113,13 +106,6 @@ const enterFind = (text?: string): HideCommandAction => {
   return hideCommand();
 };
 
-const setConsoleText = (consoleText: string): SetConsoleTextAction => {
-  return {
-    type: CONSOLE_SET_CONSOLE_TEXT,
-    consoleText,
-  };
-};
-
 export {
   hide,
   showCommand,
@@ -127,7 +113,6 @@ export {
   showError,
   showInfo,
   hideCommand,
-  setConsoleText,
   enterCommand,
   enterFind,
 };
diff --git a/src/console/completion/actions.ts b/src/console/completion/actions.ts
new file mode 100644
index 0000000..59d1a04
--- /dev/null
+++ b/src/console/completion/actions.ts
@@ -0,0 +1,76 @@
+import CompletionType from "../../shared/CompletionType";
+import Completions from "../Completions";
+
+export const INIT_COMPLETIONS = "reset.completions";
+export const SET_COMPLETION_SOURCE = "set.completion.source";
+export const SET_COMPLETIONS = "set.completions";
+export const COMPLETION_NEXT = "completion.next";
+export const COMPLETION_PREV = "completion.prev";
+
+export interface InitCompletionAction {
+  type: typeof INIT_COMPLETIONS;
+  completionTypes: CompletionType[];
+}
+
+export interface SetCompletionSourceAction {
+  type: typeof SET_COMPLETION_SOURCE;
+  completionSource: string;
+}
+
+export interface SetCompletionsAction {
+  type: typeof SET_COMPLETIONS;
+  completions: Completions;
+}
+
+export interface CompletionNextAction {
+  type: typeof COMPLETION_NEXT;
+}
+
+export interface CompletionPrevAction {
+  type: typeof COMPLETION_PREV;
+}
+
+export type CompletionAction =
+  | InitCompletionAction
+  | SetCompletionSourceAction
+  | SetCompletionsAction
+  | CompletionNextAction
+  | CompletionPrevAction;
+
+export const initCompletion = (
+  completionTypes: CompletionType[]
+): InitCompletionAction => {
+  return {
+    type: INIT_COMPLETIONS,
+    completionTypes,
+  };
+};
+export const setCompletionSource = (
+  query: string
+): SetCompletionSourceAction => {
+  return {
+    type: SET_COMPLETION_SOURCE,
+    completionSource: query,
+  };
+};
+
+export const setCompletions = (
+  completions: Completions
+): SetCompletionsAction => {
+  return {
+    type: SET_COMPLETIONS,
+    completions,
+  };
+};
+
+export const selectNext = (): CompletionNextAction => {
+  return {
+    type: COMPLETION_NEXT,
+  };
+};
+
+export const selectPrev = (): CompletionPrevAction => {
+  return {
+    type: COMPLETION_PREV,
+  };
+};
diff --git a/src/console/completion/context.ts b/src/console/completion/context.ts
new file mode 100644
index 0000000..2fafcf8
--- /dev/null
+++ b/src/console/completion/context.ts
@@ -0,0 +1,9 @@
+import React from "react";
+import { State, defaultState } from "./reducer";
+import { CompletionAction } from "./actions";
+
+export const CompletionStateContext = React.createContext<State>(defaultState);
+
+export const CompletionDispatchContext = React.createContext<
+  (action: CompletionAction) => void
+>(() => {});
diff --git a/src/console/completion/hooks.ts b/src/console/completion/hooks.ts
new file mode 100644
index 0000000..aac431b
--- /dev/null
+++ b/src/console/completion/hooks.ts
@@ -0,0 +1,277 @@
+import React from "react";
+import * as actions from "./actions";
+import { Command } from "../../shared/Command";
+import TabFlag from "../../shared/TabFlag";
+import { CompletionStateContext, CompletionDispatchContext } from "./context";
+import CompletionClient from "../clients/CompletionClient";
+import CommandLineParser, {
+  CommandLine,
+  InputPhase,
+} from "../commandline/CommandLineParser";
+import { UnknownCommandError } from "../commandline/CommandParser";
+import Completions from "../Completions";
+import CompletionType from "../../shared/CompletionType";
+
+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 completionClient = new CompletionClient();
+
+const getCommandCompletions = async (query: string): Promise<Completions> => {
+  const items = Object.entries(commandDocs)
+    .filter(([name]) => name.startsWith(query))
+    .map(([name, doc]) => ({
+      caption: name,
+      content: name,
+      url: doc,
+    }));
+  return [
+    {
+      name: "Console Command",
+      items,
+    },
+  ];
+};
+
+const getOpenCompletions = async (
+  command: string,
+  query: string,
+  completionTypes: CompletionType[]
+): Promise<Completions> => {
+  const completions: Completions = [];
+  for (const type of completionTypes) {
+    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 completions;
+};
+
+export const getTabCompletions = async (
+  command: string,
+  query: string,
+  excludePinned: boolean
+): Promise<Completions> => {
+  const items = await completionClient.requestTabs(query, excludePinned);
+  if (items.length === 0) {
+    return [];
+  }
+
+  return [
+    {
+      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,
+      })),
+    },
+  ];
+};
+
+export const getPropertyCompletions = async (
+  command: string,
+  query: string
+): Promise<Completions> => {
+  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));
+  return [{ name: "Properties", items }];
+};
+
+export const useCompletions = () => {
+  const state = React.useContext(CompletionStateContext);
+  const dispatch = React.useContext(CompletionDispatchContext);
+  const commandLineParser = React.useMemo(() => new CommandLineParser(), []);
+
+  const updateCompletions = React.useCallback((source: string) => {
+    dispatch(actions.setCompletionSource(source));
+  }, []);
+
+  const initCompletion = React.useCallback((source: string) => {
+    completionClient.getCompletionTypes().then((completionTypes) => {
+      dispatch(actions.initCompletion(completionTypes));
+      dispatch(actions.setCompletionSource(source));
+    });
+  }, []);
+
+  React.useEffect(() => {
+    const text = state.completionSource;
+    const phase = commandLineParser.inputPhase(text);
+    if (phase === InputPhase.OnCommand) {
+      getCommandCompletions(text).then((completions) =>
+        dispatch(actions.setCompletions(completions))
+      );
+    } else {
+      let cmd: CommandLine | null = null;
+      try {
+        cmd = commandLineParser.parse(text);
+      } catch (e) {
+        if (e instanceof UnknownCommandError) {
+          return;
+        }
+      }
+      switch (cmd?.command) {
+        case Command.Open:
+        case Command.TabOpen:
+        case Command.WindowOpen:
+          if (!state.completionTypes) {
+            initCompletion(text);
+            return;
+          }
+
+          getOpenCompletions(
+            cmd.command,
+            cmd.args,
+            state.completionTypes
+          ).then((completions) =>
+            dispatch(actions.setCompletions(completions))
+          );
+          break;
+        case Command.Buffer:
+          getTabCompletions(cmd.command, cmd.args, false).then((completions) =>
+            dispatch(actions.setCompletions(completions))
+          );
+          break;
+        case Command.BufferDelete:
+        case Command.BuffersDelete:
+          getTabCompletions(cmd.command, cmd.args, true).then((completions) =>
+            dispatch(actions.setCompletions(completions))
+          );
+          break;
+        case Command.BufferDeleteForce:
+        case Command.BuffersDeleteForce:
+          getTabCompletions(cmd.command, cmd.args, false).then((completions) =>
+            dispatch(actions.setCompletions(completions))
+          );
+          break;
+        case Command.Set:
+          getPropertyCompletions(cmd.command, cmd.args).then((completions) =>
+            dispatch(actions.setCompletions(completions))
+          );
+          break;
+      }
+    }
+  }, [state.completionSource, state.completionTypes]);
+
+  return {
+    completions: state.completions,
+    updateCompletions,
+    initCompletion,
+  };
+};
+
+export const useSelectCompletion = () => {
+  const state = React.useContext(CompletionStateContext);
+  const dispatch = React.useContext(CompletionDispatchContext);
+  const next = React.useCallback(() => dispatch(actions.selectNext()), [
+    dispatch,
+  ]);
+  const prev = React.useCallback(() => dispatch(actions.selectPrev()), [
+    dispatch,
+  ]);
+  const currentValue = React.useMemo(() => {
+    if (state.select < 0) {
+      return state.completionSource;
+    }
+    const items = state.completions.map((g) => g.items).flat();
+    return items[state.select]?.content || "";
+  }, [state.completionSource, state.select]);
+
+  return {
+    select: state.select,
+    currentValue,
+    selectNext: next,
+    selectPrev: prev,
+  };
+};
diff --git a/src/console/completion/provider.tsx b/src/console/completion/provider.tsx
new file mode 100644
index 0000000..c0de250
--- /dev/null
+++ b/src/console/completion/provider.tsx
@@ -0,0 +1,25 @@
+import reducer, { defaultState } from "./reducer";
+import React from "react";
+import { CompletionDispatchContext, CompletionStateContext } from "./context";
+
+interface Props {
+  initialInputValue: string;
+}
+
+export const CompletionProvider: React.FC<Props> = ({
+  initialInputValue,
+  children,
+}) => {
+  const initialState = {
+    ...defaultState,
+    completionSource: initialInputValue,
+  };
+  const [state, dispatch] = React.useReducer(reducer, initialState);
+  return (
+    <CompletionStateContext.Provider value={state}>
+      <CompletionDispatchContext.Provider value={dispatch}>
+        {children}
+      </CompletionDispatchContext.Provider>
+    </CompletionStateContext.Provider>
+  );
+};
diff --git a/src/console/completion/reducer.ts b/src/console/completion/reducer.ts
new file mode 100644
index 0000000..905451f
--- /dev/null
+++ b/src/console/completion/reducer.ts
@@ -0,0 +1,96 @@
+import Completions from "../Completions";
+import CompletionType from "../../shared/CompletionType";
+import {
+  INIT_COMPLETIONS,
+  SET_COMPLETION_SOURCE,
+  SET_COMPLETIONS,
+  COMPLETION_NEXT,
+  COMPLETION_PREV,
+  CompletionAction,
+} from "./actions";
+
+export interface State {
+  completionTypes?: CompletionType[];
+  completionSource: string;
+  completions: Completions;
+  select: number;
+}
+
+export const defaultState = {
+  completionTypes: undefined,
+  completionSource: "",
+  completions: [],
+  select: -1,
+};
+
+const nextSelection = (state: State): number => {
+  const length = state.completions
+    .map((g) => g.items.length)
+    .reduce((x, y) => x + y, 0);
+  if (length === 0) {
+    return -1;
+  }
+  if (state.select < 0) {
+    return 0;
+  }
+  if (state.select + 1 < length) {
+    return state.select + 1;
+  }
+  return -1;
+};
+
+const prevSelection = (state: State): number => {
+  if (state.completions.length === 0) {
+    return -1;
+  }
+  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;
+};
+
+// eslint-disable-next-line max-lines-per-function
+export default function reducer(
+  state: State = defaultState,
+  action: CompletionAction
+): State {
+  switch (action.type) {
+    case INIT_COMPLETIONS:
+      return {
+        ...state,
+        completionTypes: action.completionTypes,
+        completions: [],
+        select: -1,
+      };
+    case SET_COMPLETION_SOURCE:
+      return {
+        ...state,
+        completionSource: action.completionSource,
+        select: -1,
+      };
+    case SET_COMPLETIONS:
+      return {
+        ...state,
+        completions: action.completions,
+      };
+    case COMPLETION_NEXT: {
+      const select = nextSelection(state);
+      return {
+        ...state,
+        select: select,
+      };
+    }
+    case COMPLETION_PREV: {
+      const select = prevSelection(state);
+      return {
+        ...state,
+        select: select,
+      };
+    }
+    default:
+      return state;
+  }
+}
diff --git a/src/console/components/CommandPrompt.tsx b/src/console/components/CommandPrompt.tsx
index f6f4d8f..24f46ae 100644
--- a/src/console/components/CommandPrompt.tsx
+++ b/src/console/components/CommandPrompt.tsx
@@ -1,17 +1,12 @@
 import React from "react";
 import * as consoleActions from "../actions/console";
-import * as completionActions from "../actions/completion";
 import AppContext from "./AppContext";
-import CommandLineParser, {
-  InputPhase,
-} from "../commandline/CommandLineParser";
 import Completion from "./console/Completion";
 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";
+import { useCompletions, useSelectCompletion } from "../completion/hooks";
 import useAutoResize from "../hooks/useAutoResize";
+import { CompletionProvider } from "../completion/provider";
 
 const COMPLETION_MAX_ITEMS = 33;
 
@@ -19,13 +14,20 @@ const ConsoleWrapper = styled.div`
   border-top: 1px solid gray;
 `;
 
-const CommandPrompt: React.FC = () => {
-  const { state, dispatch } = React.useContext(AppContext);
-  const [completionState, completionDispatch] = React.useReducer(
-    reducer,
-    defaultState
-  );
-  const commandLineParser = new CommandLineParser();
+interface Props {
+  initialInputValue: string;
+}
+
+const CommandPromptInner: React.FC<Props> = ({ initialInputValue }) => {
+  const { dispatch } = React.useContext(AppContext);
+  const [inputValue, setInputValue] = React.useState(initialInputValue);
+  const { completions, updateCompletions } = useCompletions();
+  const {
+    select,
+    currentValue,
+    selectNext,
+    selectPrev,
+  } = useSelectCompletion();
 
   useAutoResize();
 
@@ -33,174 +35,81 @@ const CommandPrompt: React.FC = () => {
     dispatch(consoleActions.hideCommand());
   };
 
-  const doEnter = (e: React.KeyboardEvent<HTMLInputElement>) => {
-    e.stopPropagation();
-    e.preventDefault();
+  const isCancelKey = React.useCallback(
+    (e: React.KeyboardEvent<HTMLInputElement>) =>
+      e.key === "Escape" ||
+      (e.ctrlKey && e.key === "[") ||
+      (e.ctrlKey && e.key === "c"),
+    []
+  );
 
-    const value = (e.target as HTMLInputElement).value;
-    dispatch(consoleActions.enterCommand(value));
-  };
+  const isNextKey = React.useCallback(
+    (e: React.KeyboardEvent<HTMLInputElement>) =>
+      (!e.shiftKey && e.key === "Tab") || (e.ctrlKey && e.key === "n"),
+    []
+  );
 
-  const selectNext = (e: React.KeyboardEvent<HTMLInputElement>) => {
-    completionDispatch(completionActions.completionNext());
-    e.stopPropagation();
-    e.preventDefault();
-  };
+  const isPrevKey = React.useCallback(
+    (e: React.KeyboardEvent<HTMLInputElement>) =>
+      (e.shiftKey && e.key === "Tab") || (e.ctrlKey && e.key === "p"),
+    []
+  );
 
-  const selectPrev = (e: React.KeyboardEvent<HTMLInputElement>) => {
-    completionDispatch(completionActions.completionPrev());
-    e.stopPropagation();
-    e.preventDefault();
-  };
+  const isEnterKey = React.useCallback(
+    (e: React.KeyboardEvent<HTMLInputElement>) =>
+      e.key === "Enter" || (e.ctrlKey && e.key === "m"),
+    []
+  );
 
   const onKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
-    switch (e.key) {
-      case "Escape":
-        dispatch(consoleActions.hideCommand());
-        break;
-      case "Enter":
-        doEnter(e);
-        break;
-      case "Tab":
-        if (e.shiftKey) {
-          completionDispatch(completionActions.completionPrev());
-        } else {
-          completionDispatch(completionActions.completionNext());
-        }
-        e.stopPropagation();
-        e.preventDefault();
-        break;
-      case "[":
-        if (e.ctrlKey) {
-          e.preventDefault();
-          dispatch(consoleActions.hideCommand());
-        }
-        break;
-      case "c":
-        if (e.ctrlKey) {
-          e.preventDefault();
-          dispatch(consoleActions.hideCommand());
-        }
-        break;
-      case "m":
-        if (e.ctrlKey) {
-          doEnter(e);
-        }
-        break;
-      case "n":
-        if (e.ctrlKey) {
-          selectNext(e);
-        }
-        break;
-      case "p":
-        if (e.ctrlKey) {
-          selectPrev(e);
-        }
-        break;
+    if (isCancelKey(e)) {
+      dispatch(consoleActions.hideCommand());
+    } else if (isEnterKey(e)) {
+      const value = (e.target as HTMLInputElement).value;
+      dispatch(consoleActions.enterCommand(value));
+    } else if (isNextKey(e)) {
+      selectNext();
+    } else if (isPrevKey(e)) {
+      selectPrev();
+    } else {
+      return;
     }
+
+    e.stopPropagation();
+    e.preventDefault();
   };
 
   const onChange = (e: React.ChangeEvent<HTMLInputElement>) => {
     const text = e.target.value;
-    dispatch(consoleActions.setConsoleText(text));
-    const action = getCompletionAction(text);
-    Promise.resolve(action).then((a) => {
-      if (a) {
-        completionDispatch(a);
-      }
-    });
+    setInputValue(text);
   };
 
   React.useEffect(() => {
-    completionActions.startCompletion().then((action) => {
-      completionDispatch(action);
-
-      const completionAction = getCompletionAction(
-        state.consoleText,
-        action.completionTypes
-      );
-      Promise.resolve(completionAction).then((a) => {
-        if (a) {
-          completionDispatch(a);
-        }
-      });
-    });
-  }, []);
-
-  const getCompletionAction = (
-    text: string,
-    completionTypes: CompletionType[] | undefined = undefined
-  ) => {
-    const types = completionTypes || completionState.completionTypes;
-    const phase = commandLineParser.inputPhase(text);
-    if (phase === InputPhase.OnCommand) {
-      return completionActions.getCommandCompletions(text);
-    } else {
-      const cmd = commandLineParser.parse(text);
-      switch (cmd.command) {
-        case Command.Open:
-        case Command.TabOpen:
-        case Command.WindowOpen:
-          return completionActions.getOpenCompletions(
-            types,
-            text,
-            cmd.command,
-            cmd.args
-          );
-        case Command.Buffer:
-          return completionActions.getTabCompletions(
-            text,
-            cmd.command,
-            cmd.args,
-            false
-          );
-        case Command.BufferDelete:
-        case Command.BuffersDelete:
-          return completionActions.getTabCompletions(
-            text,
-            cmd.command,
-            cmd.args,
-            true
-          );
-        case Command.BufferDeleteForce:
-        case Command.BuffersDeleteForce:
-          return completionActions.getTabCompletions(
-            text,
-            cmd.command,
-            cmd.args,
-            false
-          );
-        case Command.Set:
-          return completionActions.getPropertyCompletions(
-            text,
-            cmd.command,
-            cmd.args
-          );
-      }
-    }
-    return undefined;
-  };
+    updateCompletions(inputValue);
+  }, [inputValue]);
 
   return (
     <ConsoleWrapper>
       <Completion
         size={COMPLETION_MAX_ITEMS}
-        completions={completionState.completions}
-        select={completionState.select}
+        completions={completions}
+        select={select}
       />
       <Input
         prompt={":"}
         onBlur={onBlur}
         onKeyDown={onKeyDown}
         onChange={onChange}
-        value={
-          completionState.select < 0
-            ? state.consoleText
-            : completedText(completionState)
-        }
+        value={select == -1 ? inputValue : currentValue}
       />
     </ConsoleWrapper>
   );
 };
 
+const CommandPrompt: React.FC<Props> = ({ initialInputValue }) => (
+  <CompletionProvider initialInputValue={initialInputValue}>
+    <CommandPromptInner initialInputValue={initialInputValue} />
+  </CompletionProvider>
+);
+
 export default CommandPrompt;
diff --git a/src/console/components/Console.tsx b/src/console/components/Console.tsx
index f6f4234..508c6eb 100644
--- a/src/console/components/Console.tsx
+++ b/src/console/components/Console.tsx
@@ -16,7 +16,7 @@ const Console: React.FC = () => {
 
   switch (state.mode) {
     case "command":
-      return <CommandPrompt />;
+      return <CommandPrompt initialInputValue={state.consoleText} />;
     case "find":
       return <FindPrompt />;
     case "info":
diff --git a/src/console/reducers/completion.ts b/src/console/reducers/completion.ts
deleted file mode 100644
index 2c7ee55..0000000
--- a/src/console/reducers/completion.ts
+++ /dev/null
@@ -1,99 +0,0 @@
-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
index 37f1efc..babad69 100644
--- a/src/console/reducers/console.ts
+++ b/src/console/reducers/console.ts
@@ -1,7 +1,6 @@
 import {
   CONSOLE_HIDE,
   CONSOLE_HIDE_COMMAND,
-  CONSOLE_SET_CONSOLE_TEXT,
   CONSOLE_SHOW_COMMAND,
   CONSOLE_SHOW_ERROR,
   CONSOLE_SHOW_FIND,
@@ -47,8 +46,6 @@ export default function reducer(
         mode:
           state.mode === "command" || state.mode === "find" ? "" : state.mode,
       };
-    case CONSOLE_SET_CONSOLE_TEXT:
-      return { ...state, consoleText: action.consoleText };
     default:
       return state;
   }
diff --git a/test/console/actions/completion.test.ts b/test/console/actions/completion.test.ts
deleted file mode 100644
index cd6899a..0000000
--- a/test/console/actions/completion.test.ts
+++ /dev/null
@@ -1,28 +0,0 @@
-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 f5f102b..736dd54 100644
--- a/test/console/actions/console.test.ts
+++ b/test/console/actions/console.test.ts
@@ -2,7 +2,6 @@ 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,
@@ -60,12 +59,4 @@ describe("console actions", () => {
       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(CONSOLE_SET_CONSOLE_TEXT);
-      expect(action.consoleText).to.equal("hello world");
-    });
-  });
 });
diff --git a/test/console/completion/reducer.test.ts b/test/console/completion/reducer.test.ts
new file mode 100644
index 0000000..b742872
--- /dev/null
+++ b/test/console/completion/reducer.test.ts
@@ -0,0 +1,168 @@
+import reducer, {
+  defaultState,
+  State,
+} from "../../../src/console/completion/reducer";
+import { expect } from "chai";
+import {
+  initCompletion,
+  selectNext,
+  selectPrev,
+  setCompletions,
+  setCompletionSource,
+} from "../../../src/console/completion/actions";
+import CompletionType from "../../../src/shared/CompletionType";
+
+describe("completion reducer", () => {
+  describe("initCompletion", () => {
+    it("initializes completions", () => {
+      const nextState = reducer(
+        defaultState,
+        initCompletion([CompletionType.Bookmarks, CompletionType.History])
+      );
+
+      expect(nextState.completionTypes).deep.equals([
+        CompletionType.Bookmarks,
+        CompletionType.History,
+      ]);
+    });
+  });
+
+  describe("setCompletionSource", () => {
+    it("sets a completion source", () => {
+      const nextState = reducer(defaultState, setCompletionSource("open "));
+
+      expect(nextState.completionSource).equals("open ");
+    });
+  });
+
+  describe("setCompletions", () => {
+    it("sets completions", () => {
+      const nextState = reducer(
+        defaultState,
+        setCompletions([
+          {
+            name: "Apple",
+            items: [{}, {}],
+          },
+          {
+            name: "Banana",
+            items: [{}],
+          },
+        ])
+      );
+
+      expect(nextState.completions).deep.equals([
+        {
+          name: "Apple",
+          items: [{}, {}],
+        },
+        {
+          name: "Banana",
+          items: [{}],
+        },
+      ]);
+    });
+  });
+
+  describe("selectNext", () => {
+    context("when no completion groups", () => {
+      it("does nothing", () => {
+        const nextState = reducer(defaultState, selectNext());
+        expect(nextState.select).equals(-1);
+      });
+    });
+
+    context("when no completion items", () => {
+      it("does nothing", () => {
+        const state = {
+          ...defaultState,
+          completions: [{ name: "apple", items: [] }],
+        };
+        const nextState = reducer(state, selectNext());
+        expect(nextState.select).equals(-1);
+      });
+    });
+
+    context("when completions exist", () => {
+      it("selects next selection", () => {
+        let state: State = {
+          ...defaultState,
+          select: -1,
+          completions: [
+            {
+              name: "Apple",
+              items: [{}, {}],
+            },
+            {
+              name: "Banana",
+              items: [{}],
+            },
+          ],
+        };
+
+        state = reducer(state, selectNext());
+        expect(state.select).equals(0);
+
+        state = reducer(state, selectNext());
+        expect(state.select).equals(1);
+
+        state = reducer(state, selectNext());
+        expect(state.select).equals(2);
+
+        state = reducer(state, selectNext());
+        expect(state.select).equals(-1);
+      });
+    });
+  });
+
+  describe("selectPrev", () => {
+    context("when no completion groups", () => {
+      it("does nothing", () => {
+        const nextState = reducer(defaultState, selectPrev());
+        expect(nextState.select).equals(-1);
+      });
+
+      context("when no completion items", () => {
+        it("does nothing", () => {
+          const state = {
+            ...defaultState,
+            completions: [{ name: "apple", items: [] }],
+          };
+          const nextState = reducer(state, selectPrev());
+          expect(nextState.select).equals(-1);
+        });
+      });
+    });
+
+    context("when completions exist", () => {
+      it("selects a previous completion", () => {
+        let state: State = {
+          ...defaultState,
+          select: -1,
+          completions: [
+            {
+              name: "Apple",
+              items: [{}, {}],
+            },
+            {
+              name: "Banana",
+              items: [{}],
+            },
+          ],
+        };
+
+        state = reducer(state, selectPrev());
+        expect(state).to.have.property("select", 2);
+
+        state = reducer(state, selectPrev());
+        expect(state).to.have.property("select", 1);
+
+        state = reducer(state, selectPrev());
+        expect(state).to.have.property("select", 0);
+
+        state = reducer(state, selectPrev());
+        expect(state).to.have.property("select", -1);
+      });
+    });
+  });
+});
diff --git a/test/console/reducers/completion.test.ts b/test/console/reducers/completion.test.ts
deleted file mode 100644
index 6c76369..0000000
--- a/test/console/reducers/completion.test.ts
+++ /dev/null
@@ -1,102 +0,0 @@
-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 4d4859d..390dc66 100644
--- a/test/console/reducers/console.test.ts
+++ b/test/console/reducers/console.test.ts
@@ -3,7 +3,6 @@ import { expect } from "chai";
 import {
   CONSOLE_HIDE,
   CONSOLE_HIDE_COMMAND,
-  CONSOLE_SET_CONSOLE_TEXT,
   CONSOLE_SHOW_COMMAND,
   CONSOLE_SHOW_ERROR,
   CONSOLE_SHOW_INFO,
@@ -66,14 +65,4 @@ describe("console reducer", () => {
     state = reducer({ ...initialState, mode: "error" }, action);
     expect(state).to.have.property("mode", "error");
   });
-
-  it("return next state for 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");
-  });
 });
-- 
cgit v1.2.3


From 6767b38c8e57c9f436936dc02ad1c8c4ffd043b2 Mon Sep 17 00:00:00 2001
From: Shin'ya Ueoka <ueokande@i-beam.org>
Date: Sun, 11 Apr 2021 21:07:48 +0900
Subject: Reload colorscheme on mode changed

---
 src/console/components/Console.tsx | 6 ++++--
 1 file changed, 4 insertions(+), 2 deletions(-)

(limited to 'src')

diff --git a/src/console/components/Console.tsx b/src/console/components/Console.tsx
index 508c6eb..c8642c8 100644
--- a/src/console/components/Console.tsx
+++ b/src/console/components/Console.tsx
@@ -11,8 +11,10 @@ const Console: React.FC = () => {
   const refreshColorScheme = useColorSchemeRefresh();
 
   React.useEffect(() => {
-    refreshColorScheme();
-  }, []);
+    if (state.mode !== "") {
+      refreshColorScheme();
+    }
+  }, [state.mode]);
 
   switch (state.mode) {
     case "command":
-- 
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')

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