aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorShin'ya Ueoka <ueokande@i-beam.org>2021-04-12 13:09:09 +0000
committerGitHub <noreply@github.com>2021-04-12 13:09:09 +0000
commitd80d0f87b82ba4bd74ed9b2bb7354421a28a11b3 (patch)
tree691185ad88418d0f44c236d0913cf5c425b29b23 /src
parentea73c900f66107fd4a5b2f3b05080bcf643c94ea (diff)
parent8a5bba1da639355a25da8c279a9f1cf0a7300a9f (diff)
Merge pull request #1098 from ueokande/replace-redux-with-react-hooks
Refactor state management with React Hooks on Console
Diffstat (limited to 'src')
-rw-r--r--src/console/actions/console.ts302
-rw-r--r--src/console/actions/index.ts80
-rw-r--r--src/console/app/actions.ts82
-rw-r--r--src/console/app/contexts.ts9
-rw-r--r--src/console/app/hooks.ts115
-rw-r--r--src/console/app/provider.tsx14
-rw-r--r--src/console/app/recuer.ts52
-rw-r--r--src/console/colorscheme/contexts.tsx10
-rw-r--r--src/console/colorscheme/hooks.ts15
-rw-r--r--src/console/colorscheme/providers.tsx31
-rw-r--r--src/console/colorscheme/styled.tsx6
-rw-r--r--src/console/colorscheme/theme.ts (renamed from src/console/components/Theme.ts)12
-rw-r--r--src/console/completion/actions.ts76
-rw-r--r--src/console/completion/context.ts9
-rw-r--r--src/console/completion/hooks.ts277
-rw-r--r--src/console/completion/provider.tsx25
-rw-r--r--src/console/completion/reducer.ts96
-rw-r--r--src/console/components/CommandPrompt.tsx116
-rw-r--r--src/console/components/Console.tsx288
-rw-r--r--src/console/components/ErrorMessage.tsx15
-rw-r--r--src/console/components/FindPrompt.tsx58
-rw-r--r--src/console/components/InfoMessage.tsx15
-rw-r--r--src/console/components/console/Completion.tsx136
-rw-r--r--src/console/components/console/CompletionItem.tsx8
-rw-r--r--src/console/components/console/CompletionTitle.tsx10
-rw-r--r--src/console/components/console/Input.tsx61
-rw-r--r--src/console/components/console/Message.tsx33
-rw-r--r--src/console/hooks/useAutoResize.ts28
-rw-r--r--src/console/index.tsx88
-rw-r--r--src/console/reducers/index.ts136
30 files changed, 1238 insertions, 965 deletions
diff --git a/src/console/actions/console.ts b/src/console/actions/console.ts
deleted file mode 100644
index 16d33b3..0000000
--- a/src/console/actions/console.ts
+++ /dev/null
@@ -1,302 +0,0 @@
-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";
-
-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 => {
- return {
- type: actions.CONSOLE_HIDE,
- };
-};
-
-const showCommand = async (text: string): Promise<actions.ShowCommand> => {
- const completionTypes = await completionClient.getCompletionTypes();
- return {
- type: actions.CONSOLE_SHOW_COMMAND,
- completionTypes,
- text,
- };
-};
-
-const showFind = (): actions.ShowFindAction => {
- return {
- type: actions.CONSOLE_SHOW_FIND,
- };
-};
-
-const showError = (text: string): actions.ShowErrorAction => {
- return {
- type: actions.CONSOLE_SHOW_ERROR,
- text: text,
- };
-};
-
-const showInfo = (text: string): actions.ShowInfoAction => {
- return {
- type: actions.CONSOLE_SHOW_INFO,
- text: text,
- };
-};
-
-const hideCommand = (): actions.HideCommandAction => {
- window.top.postMessage(
- JSON.stringify({
- type: messages.CONSOLE_UNFOCUS,
- }),
- "*"
- );
- return {
- type: actions.CONSOLE_HIDE_COMMAND,
- };
-};
-
-const enterCommand = async (
- text: string
-): Promise<actions.HideCommandAction> => {
- await browser.runtime.sendMessage({
- type: messages.CONSOLE_ENTER_COMMAND,
- text,
- });
- return hideCommand();
-};
-
-const enterFind = (text?: string): actions.HideCommandAction => {
- window.top.postMessage(
- JSON.stringify({
- type: messages.CONSOLE_ENTER_FIND,
- text,
- }),
- "*"
- );
- return hideCommand();
-};
-
-const setConsoleText = (consoleText: string): actions.SetConsoleTextAction => {
- return {
- type: actions.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 scheme = await settingClient.getColorScheme();
- return {
- type: actions.CONSOLE_SET_COLORSCHEME,
- colorscheme: scheme,
- };
-};
-
-export {
- hide,
- showCommand,
- showFind,
- showError,
- showInfo,
- hideCommand,
- 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/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/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/components/Theme.ts b/src/console/colorscheme/theme.ts
index dd7baa5..5c17190 100644
--- a/src/console/components/Theme.ts
+++ b/src/console/colorscheme/theme.ts
@@ -1,6 +1,4 @@
-import baseStyled, { ThemedStyledInterface } from "styled-components";
-
-type Theme = {
+export type ThemeProperties = {
completionTitleBackground: string;
completionTitleForeground: string;
completionItemBackground: string;
@@ -16,7 +14,7 @@ type Theme = {
consoleInfoForeground: string;
};
-export const LightTheme: Theme = {
+export const LightTheme: ThemeProperties = {
completionTitleBackground: "lightgray",
completionTitleForeground: "#000000",
completionItemBackground: "#ffffff",
@@ -32,7 +30,7 @@ export const LightTheme: Theme = {
consoleInfoForeground: "#018786",
};
-export const DarkTheme: Theme = {
+export const DarkTheme: ThemeProperties = {
completionTitleBackground: "#052027",
completionTitleForeground: "white",
completionItemBackground: "#2f474f",
@@ -47,7 +45,3 @@ export const DarkTheme: Theme = {
consoleInfoBackground: "#052027",
consoleInfoForeground: "#ffffff",
};
-
-const styled = baseStyled as ThemedStyledInterface<Theme>;
-
-export default styled;
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
new file mode 100644
index 0000000..1b6281b
--- /dev/null
+++ b/src/console/components/CommandPrompt.tsx
@@ -0,0 +1,116 @@
+import React from "react";
+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;
+
+const ConsoleWrapper = styled.div`
+ border-top: 1px solid gray;
+`;
+
+interface Props {
+ initialInputValue: string;
+}
+
+const CommandPromptInner: React.FC<Props> = ({ initialInputValue }) => {
+ const hide = useHide();
+ const [inputValue, setInputValue] = React.useState(initialInputValue);
+ const { completions, updateCompletions } = useCompletions();
+ const {
+ select,
+ currentValue,
+ selectNext,
+ selectPrev,
+ } = useSelectCompletion();
+ const execCommand = useExecCommand();
+
+ useAutoResize();
+
+ const onBlur = () => {
+ hide();
+ };
+
+ const isCancelKey = React.useCallback(
+ (e: React.KeyboardEvent<HTMLInputElement>) =>
+ e.key === "Escape" ||
+ (e.ctrlKey && e.key === "[") ||
+ (e.ctrlKey && e.key === "c"),
+ []
+ );
+
+ const isNextKey = React.useCallback(
+ (e: React.KeyboardEvent<HTMLInputElement>) =>
+ (!e.shiftKey && e.key === "Tab") || (e.ctrlKey && e.key === "n"),
+ []
+ );
+
+ const isPrevKey = React.useCallback(
+ (e: React.KeyboardEvent<HTMLInputElement>) =>
+ (e.shiftKey && e.key === "Tab") || (e.ctrlKey && e.key === "p"),
+ []
+ );
+
+ const isEnterKey = React.useCallback(
+ (e: React.KeyboardEvent<HTMLInputElement>) =>
+ e.key === "Enter" || (e.ctrlKey && e.key === "m"),
+ []
+ );
+
+ const onKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
+ if (isCancelKey(e)) {
+ hide();
+ } else if (isEnterKey(e)) {
+ const value = (e.target as HTMLInputElement).value;
+ execCommand(value);
+ hide();
+ } 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;
+ setInputValue(text);
+ };
+
+ React.useEffect(() => {
+ updateCompletions(inputValue);
+ }, [inputValue]);
+
+ return (
+ <ConsoleWrapper>
+ <Completion
+ size={COMPLETION_MAX_ITEMS}
+ completions={completions}
+ select={select}
+ />
+ <Input
+ prompt={":"}
+ onBlur={onBlur}
+ onKeyDown={onKeyDown}
+ onChange={onChange}
+ 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 18a6632..db18fa0 100644
--- a/src/console/components/Console.tsx
+++ b/src/console/components/Console.tsx
@@ -1,251 +1,39 @@
-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";
-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 ConsoleFrameClient from "../clients/ConsoleFrameClient";
-
-const ConsoleWrapper = styled.div`
- border-top: 1px solid gray;
-`;
-
-const COMPLETION_MAX_ITEMS = 33;
-
-type StateProps = ReturnType<typeof mapStateToProps>;
-interface DispatchProps {
- dispatch: (action: any) => void;
-}
-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() {
- if (this.props.mode === "command" || this.props.mode === "find") {
- return this.props.dispatch(consoleActions.hideCommand());
- }
- }
-
- 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)
- );
- }
- }
-
- selectNext(e: React.KeyboardEvent<HTMLInputElement>) {
- this.props.dispatch(consoleActions.completionNext());
- e.stopPropagation();
- e.preventDefault();
- }
-
- selectPrev(e: React.KeyboardEvent<HTMLInputElement>) {
- this.props.dispatch(consoleActions.completionPrev());
- e.stopPropagation();
- e.preventDefault();
- }
-
- onKeyDown(e: React.KeyboardEvent<HTMLInputElement>) {
- switch (e.key) {
- case "Escape":
- return this.props.dispatch(consoleActions.hideCommand());
- case "Enter":
- return this.doEnter(e);
- case "Tab":
- if (e.shiftKey) {
- this.props.dispatch(consoleActions.completionPrev());
- } else {
- this.props.dispatch(consoleActions.completionNext());
- }
- e.stopPropagation();
- e.preventDefault();
- break;
- case "[":
- if (e.ctrlKey) {
- e.preventDefault();
- return this.props.dispatch(consoleActions.hideCommand());
- }
- break;
- case "c":
- if (e.ctrlKey) {
- e.preventDefault();
- return this.props.dispatch(consoleActions.hideCommand());
- }
- break;
- case "m":
- if (e.ctrlKey) {
- return this.doEnter(e);
- }
- break;
- case "n":
- if (e.ctrlKey) {
- this.selectNext(e);
- }
- break;
- case "p":
- if (e.ctrlKey) {
- this.selectPrev(e);
- }
- break;
- }
- }
-
- onChange(e: React.ChangeEvent<HTMLInputElement>) {
- const text = e.target.value;
- this.props.dispatch(consoleActions.setConsoleText(text));
- if (this.props.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();
- }
-
- 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;
- }
- }
-
- 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
- ref={this.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;
- }
- }
-
- async focus() {
- this.props.dispatch(consoleActions.setColorScheme());
-
- window.focus();
- if (this.input.current) {
- this.input.current.focus();
- }
- }
-
- private updateCompletions(text: string) {
- const phase = this.commandLineParser.inputPhase(text);
- if (phase === InputPhase.OnCommand) {
- return this.props.dispatch(consoleActions.getCommandCompletions(text));
- } else {
- const cmd = this.commandLineParser.parse(text);
- switch (cmd.command) {
- case Command.Open:
- case Command.TabOpen:
- case Command.WindowOpen:
- this.props.dispatch(
- consoleActions.getOpenCompletions(
- this.props.completionTypes,
- text,
- cmd.command,
- cmd.args
- )
- );
- break;
- case Command.Buffer:
- this.props.dispatch(
- consoleActions.getTabCompletions(text, cmd.command, cmd.args, false)
- );
- break;
- case Command.BufferDelete:
- case Command.BuffersDelete:
- this.props.dispatch(
- consoleActions.getTabCompletions(text, cmd.command, cmd.args, true)
- );
- break;
- case Command.BufferDeleteForce:
- case Command.BuffersDeleteForce:
- this.props.dispatch(
- consoleActions.getTabCompletions(text, cmd.command, cmd.args, false)
- );
- break;
- case Command.Set:
- this.props.dispatch(
- consoleActions.getPropertyCompletions(text, cmd.command, cmd.args)
- );
- break;
- }
- }
- }
-}
-
-const mapStateToProps = (state: AppState) => ({ ...state });
-
-export default connect(mapStateToProps)(Console);
+import FindPrompt from "./FindPrompt";
+import CommandPrompt from "./CommandPrompt";
+import InfoMessage from "./InfoMessage";
+import ErrorMessage from "./ErrorMessage";
+import { useColorSchemeRefresh } from "../colorscheme/hooks";
+import {
+ useCommandMode,
+ useErrorMessage,
+ useFindMode,
+ useInfoMessage,
+} from "../app/hooks";
+
+const Console: React.FC = () => {
+ 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 (visibleCommand || visibleFind || visibleInfo || visibleError) {
+ refreshColorScheme();
+ }
+ }, [visibleCommand, visibleFind, visibleInfo, visibleError]);
+
+ 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/ErrorMessage.tsx b/src/console/components/ErrorMessage.tsx
new file mode 100644
index 0000000..f8d5ae7
--- /dev/null
+++ b/src/console/components/ErrorMessage.tsx
@@ -0,0 +1,15 @@
+import React from "react";
+import styled from "../colorscheme/styled";
+
+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/FindPrompt.tsx b/src/console/components/FindPrompt.tsx
new file mode 100644
index 0000000..c437d16
--- /dev/null
+++ b/src/console/components/FindPrompt.tsx
@@ -0,0 +1,58 @@
+import React from "react";
+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 [inputValue, setInputValue] = React.useState("");
+ const hide = useHide();
+ const execFind = useExecFind();
+
+ const onBlur = () => {
+ hide();
+ };
+
+ useAutoResize();
+
+ const doEnter = (e: React.KeyboardEvent<HTMLInputElement>) => {
+ e.stopPropagation();
+ e.preventDefault();
+
+ const value = (e.target as HTMLInputElement).value;
+ execFind(value === "" ? undefined : value);
+ };
+
+ const onKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
+ switch (e.key) {
+ case "Escape":
+ hide();
+ break;
+ case "Enter":
+ doEnter(e);
+ break;
+ }
+ };
+
+ const onChange = (e: React.ChangeEvent<HTMLInputElement>) => {
+ setInputValue(e.target.value);
+ };
+
+ return (
+ <ConsoleWrapper>
+ <Input
+ prompt={"/"}
+ onBlur={onBlur}
+ onKeyDown={onKeyDown}
+ onChange={onChange}
+ value={inputValue}
+ />
+ </ConsoleWrapper>
+ );
+};
+
+export default FindPrompt;
diff --git a/src/console/components/InfoMessage.tsx b/src/console/components/InfoMessage.tsx
new file mode 100644
index 0000000..ccd9bcf
--- /dev/null
+++ b/src/console/components/InfoMessage.tsx
@@ -0,0 +1,15 @@
+import React from "react";
+import styled from "../colorscheme/styled";
+
+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/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/CompletionItem.tsx b/src/console/components/console/CompletionItem.tsx
index 5f2f9f6..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 "../Theme";
+import styled from "../../colorscheme/styled";
const Container = styled.li<{
shown: boolean;
@@ -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..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 "../Theme";
+import styled from "../../colorscheme/styled";
const Li = styled.li<{ shown: boolean }>`
display: ${({ shown }) => (shown ? "display" : "none")};
@@ -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;
diff --git a/src/console/components/console/Input.tsx b/src/console/components/console/Input.tsx
index 448b096..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 "../Theme";
+import styled from "../../colorscheme/styled";
const Container = styled.div`
background-color: ${({ theme }) => theme.commandBackground};
@@ -19,49 +19,32 @@ const InputInner = styled.input`
`;
interface Props {
- mode: string;
+ prompt: string;
value: string;
onBlur: (e: React.FocusEvent<HTMLInputElement>) => void;
onKeyDown: (e: React.KeyboardEvent<HTMLInputElement>) => void;
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();
- }
- }
-
- 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>
- );
- }
-}
+const Input: React.FC<Props> = (props) => {
+ const input = React.useRef<HTMLInputElement>(null);
+
+ React.useEffect(() => {
+ input?.current?.focus();
+ }, []);
+
+ return (
+ <Container>
+ <Prompt>{props.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/components/console/Message.tsx b/src/console/components/console/Message.tsx
deleted file mode 100644
index 73498fd..0000000
--- a/src/console/components/console/Message.tsx
+++ /dev/null
@@ -1,33 +0,0 @@
-import React from "react";
-import styled from "../Theme";
-
-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/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;
diff --git a/src/console/index.tsx b/src/console/index.tsx
index f9313a0..29fa11f 100644
--- a/src/console/index.tsx
+++ b/src/console/index.tsx
@@ -1,42 +1,66 @@
import * as messages from "../shared/messages";
-import reducers from "./reducers";
-import { createStore, applyMiddleware } from "redux";
-import promise from "redux-promise";
-import * as consoleActions from "./actions/console";
-import { Provider } from "react-redux";
import Console from "./components/Console";
import "./index.css";
import React from "react";
import ReactDOM from "react-dom";
+import ColorSchemeProvider from "./colorscheme/providers";
+import { AppProvider } from "./app/provider";
+import {
+ useCommandMode,
+ useFindMode,
+ useInfoMessage,
+ useErrorMessage,
+ useHide,
+} from "./app/hooks";
-const store = createStore(reducers, applyMiddleware(promise));
+const RootComponent: React.FC = () => {
+ const hide = useHide();
+ const { show: showCommand } = useCommandMode();
+ const { show: showFind } = useFindMode();
+ const { show: showError } = useErrorMessage();
+ const { show: showInfo } = useInfoMessage();
-window.addEventListener("DOMContentLoaded", () => {
- const wrapper = document.getElementById("vimvixen-console");
- ReactDOM.render(
- <Provider store={store}>
- <Console></Console>
- </Provider>,
- wrapper
- );
-});
+ React.useEffect(() => {
+ const onMessage = async (message: any): Promise<any> => {
+ const msg = messages.valueOf(message);
+ switch (msg.type) {
+ case messages.CONSOLE_SHOW_COMMAND:
+ showCommand(msg.command);
+ break;
+ case messages.CONSOLE_SHOW_FIND:
+ showFind();
+ break;
+ case messages.CONSOLE_SHOW_ERROR:
+ showError(msg.text);
+ break;
+ case messages.CONSOLE_SHOW_INFO:
+ showInfo(msg.text);
+ break;
+ case messages.CONSOLE_HIDE:
+ hide();
+ break;
+ }
+ };
+
+ browser.runtime.onMessage.addListener(onMessage);
+ const port = browser.runtime.connect(undefined, {
+ name: "vimvixen-console",
+ });
+ port.onMessage.addListener(onMessage);
+ }, []);
-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());
- }
+ return <Console />;
};
-browser.runtime.onMessage.addListener(onMessage);
-const port = browser.runtime.connect(undefined, { name: "vimvixen-console" });
-port.onMessage.addListener(onMessage);
+const App: React.FC = () => (
+ <AppProvider>
+ <ColorSchemeProvider>
+ <RootComponent />
+ </ColorSchemeProvider>
+ </AppProvider>
+);
+
+window.addEventListener("DOMContentLoaded", () => {
+ const wrapper = document.getElementById("vimvixen-console");
+ ReactDOM.render(<App />, wrapper);
+});
diff --git a/src/console/reducers/index.ts b/src/console/reducers/index.ts
deleted file mode 100644
index 752dfd9..0000000
--- a/src/console/reducers/index.ts
+++ /dev/null
@@ -1,136 +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;
- viewIndex: number;
- colorscheme: ColorScheme;
-}
-
-const defaultState = {
- mode: "",
- messageText: "",
- consoleText: "",
- completionTypes: [],
- completionSource: "",
- completions: [],
- select: -1,
- viewIndex: 0,
- 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;
- }
-}