aboutsummaryrefslogtreecommitdiff
path: root/src/console
diff options
context:
space:
mode:
authorShin'ya Ueoka <ueokande@i-beam.org>2020-04-09 10:38:37 +0900
committerGitHub <noreply@github.com>2020-04-09 10:38:37 +0900
commit1656d52d2cefb3846d968c6117484e6aefe7dabe (patch)
treeab58a99b832d2571e2168f2ee0e328bc12d9580e /src/console
parentc6c2da8547891b50aef2f08e5f36d258183831ff (diff)
parent5176643e64d8f4a6be5fc73f0eb48dc65322e496 (diff)
Merge pull request #730 from ueokande/refactor-console-and-completion
Refactor console and completions
Diffstat (limited to 'src/console')
-rw-r--r--src/console/Completions.ts11
-rw-r--r--src/console/actions/console.ts193
-rw-r--r--src/console/actions/index.ts27
-rw-r--r--src/console/clients/CompletionClient.ts84
-rw-r--r--src/console/commandline/CommandLineParser.ts38
-rw-r--r--src/console/commandline/CommandParser.ts52
-rw-r--r--src/console/components/Console.tsx42
-rw-r--r--src/console/index.tsx4
-rw-r--r--src/console/reducers/index.ts7
9 files changed, 419 insertions, 39 deletions
diff --git a/src/console/Completions.ts b/src/console/Completions.ts
new file mode 100644
index 0000000..ec9135f
--- /dev/null
+++ b/src/console/Completions.ts
@@ -0,0 +1,11 @@
+type Completions = {
+ readonly name: string;
+ readonly items: {
+ readonly caption?: string;
+ readonly content?: string;
+ readonly url?: string;
+ readonly icon?: string;
+ }[];
+}[]
+
+export default Completions; \ No newline at end of file
diff --git a/src/console/actions/console.ts b/src/console/actions/console.ts
index f7fa7a2..e44c974 100644
--- a/src/console/actions/console.ts
+++ b/src/console/actions/console.ts
@@ -1,5 +1,32 @@
import * as messages from '../../shared/messages';
import * as actions from './index';
+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',
+};
const hide = (): actions.ConsoleAction => {
return {
@@ -7,34 +34,36 @@ const hide = (): actions.ConsoleAction => {
};
};
-const showCommand = (text: string): actions.ConsoleAction => {
+const showCommand = async (text: string): Promise<actions.ShowCommand> => {
+ const completionTypes = await completionClient.getCompletionTypes();
return {
type: actions.CONSOLE_SHOW_COMMAND,
- text: text
+ completionTypes,
+ text,
};
};
-const showFind = (): actions.ConsoleAction => {
+const showFind = (): actions.ShowFindAction => {
return {
type: actions.CONSOLE_SHOW_FIND,
};
};
-const showError = (text: string): actions.ConsoleAction => {
+const showError = (text: string): actions.ShowErrorAction => {
return {
type: actions.CONSOLE_SHOW_ERROR,
text: text
};
};
-const showInfo = (text: string): actions.ConsoleAction => {
+const showInfo = (text: string): actions.ShowInfoAction => {
return {
type: actions.CONSOLE_SHOW_INFO,
text: text
};
};
-const hideCommand = (): actions.ConsoleAction => {
+const hideCommand = (): actions.HideCommandAction => {
window.top.postMessage(JSON.stringify({
type: messages.CONSOLE_UNFOCUS,
}), '*');
@@ -43,9 +72,7 @@ const hideCommand = (): actions.ConsoleAction => {
};
};
-const enterCommand = async(
- text: string,
-): Promise<actions.ConsoleAction> => {
+const enterCommand = async(text: string): Promise<actions.HideCommandAction> => {
await browser.runtime.sendMessage({
type: messages.CONSOLE_ENTER_COMMAND,
text,
@@ -53,7 +80,7 @@ const enterCommand = async(
return hideCommand();
};
-const enterFind = (text?: string): actions.ConsoleAction => {
+const enterFind = (text?: string): actions.HideCommandAction => {
window.top.postMessage(JSON.stringify({
type: messages.CONSOLE_ENTER_FIND,
text,
@@ -61,38 +88,164 @@ const enterFind = (text?: string): actions.ConsoleAction => {
return hideCommand();
};
-const setConsoleText = (consoleText: string): actions.ConsoleAction => {
+const setConsoleText = (consoleText: string): actions.SetConsoleTextAction => {
return {
type: actions.CONSOLE_SET_CONSOLE_TEXT,
consoleText,
};
};
-const getCompletions = async(text: string): Promise<actions.ConsoleAction> => {
- const completions = await browser.runtime.sendMessage({
- type: messages.CONSOLE_QUERY_COMPLETIONS,
- text,
- });
+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 completionNext = (): actions.ConsoleAction => {
+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: name + ' ' + 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.ConsoleAction => {
+const completionPrev = (): actions.CompletionPrevAction => {
return {
type: actions.CONSOLE_COMPLETION_PREV,
};
};
export {
- hide, showCommand, showFind, showError, showInfo, hideCommand, setConsoleText,
- enterCommand, enterFind, getCompletions, completionNext, completionPrev,
+ hide, showCommand, showFind, showError, showInfo, hideCommand, setConsoleText, enterCommand, enterFind,
+ getCommandCompletions, getOpenCompletions, getTabCompletions, getPropertyCompletions,
+ completionNext, completionPrev,
};
diff --git a/src/console/actions/index.ts b/src/console/actions/index.ts
index 3770496..e292608 100644
--- a/src/console/actions/index.ts
+++ b/src/console/actions/index.ts
@@ -1,4 +1,6 @@
-// console commands
+import Completions from "../Completions";
+import CompletionType from "../../shared/CompletionType";
+
export const CONSOLE_HIDE = 'console.hide';
export const CONSOLE_SHOW_COMMAND = 'console.show.command';
export const CONSOLE_SHOW_ERROR = 'console.show.error';
@@ -10,49 +12,50 @@ export const CONSOLE_COMPLETION_NEXT = 'console.completion.next';
export const CONSOLE_COMPLETION_PREV = 'console.completion.prev';
export const CONSOLE_SHOW_FIND = 'console.show.find';
-interface HideAction {
+export interface HideAction {
type: typeof CONSOLE_HIDE;
}
-interface ShowCommand {
+export interface ShowCommand {
type: typeof CONSOLE_SHOW_COMMAND;
text: string;
+ completionTypes: CompletionType[];
}
-interface ShowFindAction {
+export interface ShowFindAction {
type: typeof CONSOLE_SHOW_FIND;
}
-interface ShowErrorAction {
+export interface ShowErrorAction {
type: typeof CONSOLE_SHOW_ERROR;
text: string;
}
-interface ShowInfoAction {
+export interface ShowInfoAction {
type: typeof CONSOLE_SHOW_INFO;
text: string;
}
-interface HideCommandAction {
+export interface HideCommandAction {
type: typeof CONSOLE_HIDE_COMMAND;
}
-interface SetConsoleTextAction {
+export interface SetConsoleTextAction {
type: typeof CONSOLE_SET_CONSOLE_TEXT;
consoleText: string;
}
-interface SetCompletionsAction {
+export interface SetCompletionsAction {
type: typeof CONSOLE_SET_COMPLETIONS;
- completions: any[];
+ completions: Completions;
completionSource: string;
}
-interface CompletionNextAction {
+export interface CompletionNextAction {
type: typeof CONSOLE_COMPLETION_NEXT;
}
-interface CompletionPrevAction {
+export interface CompletionPrevAction {
type: typeof CONSOLE_COMPLETION_PREV;
}
diff --git a/src/console/clients/CompletionClient.ts b/src/console/clients/CompletionClient.ts
new file mode 100644
index 0000000..56dc665
--- /dev/null
+++ b/src/console/clients/CompletionClient.ts
@@ -0,0 +1,84 @@
+import * as messages from "../../shared/messages";
+import {
+ ConsoleGetCompletionTypesResponse, ConsoleGetPropertiesResponse,
+ ConsoleRequestBookmarksResponse,
+ ConsoleRequestHistoryResponse, ConsoleRequestSearchEnginesResponse, ConsoleRequesttabsResponse
+} from "../../shared/messages";
+import CompletionType from "../../shared/CompletionType";
+import TabFlag from "../../shared/TabFlag";
+
+export type SearchEngines = {
+ title: string
+}
+
+export type BookmarkItem = {
+ title: string
+ url: string
+}
+
+export type HistoryItem = {
+ title: string
+ url: string
+}
+
+export type TabItem = {
+ index: number
+ flag: TabFlag
+ title: string
+ url: string
+ faviconUrl?: string
+}
+
+export type Property = {
+ name: string
+ type: 'string' | 'boolean' | 'number';
+}
+
+export default class CompletionClient {
+ async getCompletionTypes(): Promise<CompletionType[]> {
+ const resp = await browser.runtime.sendMessage({
+ type: messages.CONSOLE_GET_COMPLETION_TYPES,
+ }) as ConsoleGetCompletionTypesResponse;
+ return resp;
+ }
+
+ async requestSearchEngines(query: string): Promise<SearchEngines[]> {
+ const resp = await browser.runtime.sendMessage({
+ type: messages.CONSOLE_REQUEST_SEARCH_ENGINES_MESSAGE,
+ query,
+ }) as ConsoleRequestSearchEnginesResponse;
+ return resp;
+ }
+
+ async requestBookmarks(query: string): Promise<BookmarkItem[]> {
+ const resp = await browser.runtime.sendMessage({
+ type: messages.CONSOLE_REQUEST_BOOKMARKS,
+ query,
+ }) as ConsoleRequestBookmarksResponse;
+ return resp;
+ }
+
+ async requestHistory(query: string): Promise<HistoryItem[]> {
+ const resp = await browser.runtime.sendMessage({
+ type: messages.CONSOLE_REQUEST_HISTORY,
+ query,
+ }) as ConsoleRequestHistoryResponse;
+ return resp;
+ }
+
+ async requestTabs(query: string, excludePinned: boolean): Promise<TabItem[]> {
+ const resp = await browser.runtime.sendMessage({
+ type: messages.CONSOLE_REQUEST_TABS,
+ query,
+ excludePinned,
+ }) as ConsoleRequesttabsResponse;
+ return resp;
+ }
+
+ async getProperties(): Promise<Property[]> {
+ const resp = await browser.runtime.sendMessage({
+ type: messages.CONSOLE_GET_PROPERTIES,
+ }) as ConsoleGetPropertiesResponse;
+ return resp;
+ }
+}
diff --git a/src/console/commandline/CommandLineParser.ts b/src/console/commandline/CommandLineParser.ts
new file mode 100644
index 0000000..a166f49
--- /dev/null
+++ b/src/console/commandline/CommandLineParser.ts
@@ -0,0 +1,38 @@
+import CommandParser from "./CommandParser";
+import { Command } from "../../shared/Command";
+
+export type CommandLine = {
+ readonly command: Command,
+ readonly args: string
+}
+
+export enum InputPhase {
+ OnCommand,
+ OnArgs,
+}
+
+export default class CommandLineParser {
+ private commandParser: CommandParser = new CommandParser();
+
+ inputPhase(line: string): InputPhase {
+ line = line.trimLeft();
+ if (line.length == 0) {
+ return InputPhase.OnCommand
+ }
+ const command = line.split(/\s+/, 1)[0];
+ if (line.length == command.length) {
+ return InputPhase.OnCommand
+ }
+ return InputPhase.OnArgs;
+ }
+
+ parse(line: string): CommandLine {
+ const trimLeft = line.trimLeft();
+ const command = trimLeft.split(/\s+/, 1)[0];
+ const args = trimLeft.slice(command.length).trimLeft();
+ return {
+ command: this.commandParser.parse(command),
+ args: args,
+ }
+ }
+}
diff --git a/src/console/commandline/CommandParser.ts b/src/console/commandline/CommandParser.ts
new file mode 100644
index 0000000..5228c77
--- /dev/null
+++ b/src/console/commandline/CommandParser.ts
@@ -0,0 +1,52 @@
+import { Command } from "../../shared/Command";
+
+export class UnknownCommandError extends Error {
+ constructor(value: string) {
+ super(`unknown command '${value}'`);
+ }
+}
+
+export default class CommandParser {
+ parse(value: string): Command {
+ switch (value) {
+ case 'o':
+ case 'open':
+ return Command.Open;
+ case 't':
+ case 'tabopen':
+ return Command.TabOpen;
+ case 'w':
+ case 'winopen':
+ return Command.WindowOpen;
+ case 'b':
+ case 'buffer':
+ return Command.Buffer;
+ case 'bd':
+ case 'bdel':
+ case 'bdelete':
+ return Command.BufferDelete;
+ case 'bd!':
+ case 'bdel!':
+ case 'bdelete!':
+ return Command.BufferDeleteForce;
+ case 'bdeletes':
+ return Command.BuffersDelete;
+ case 'bdeletes!':
+ return Command.BuffersDeleteForce;
+ case 'addbookmark':
+ return Command.AddBookmark;
+ case 'q':
+ case 'quit':
+ return Command.Quit;
+ case 'qa':
+ case 'quitall':
+ return Command.QuitAll;
+ case 'set':
+ return Command.Set;
+ case 'h':
+ case 'help':
+ return Command.Help;
+ }
+ throw new UnknownCommandError(value);
+ }
+}
diff --git a/src/console/components/Console.tsx b/src/console/components/Console.tsx
index eafe2a7..3fe5cee 100644
--- a/src/console/components/Console.tsx
+++ b/src/console/components/Console.tsx
@@ -6,6 +6,8 @@ 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";
const COMPLETION_MAX_ITEMS = 33;
@@ -18,6 +20,8 @@ type Props = StateProps & DispatchProps;
class Console extends React.Component<Props> {
private input: React.RefObject<Input>;
+ private commandLineParser: CommandLineParser = new CommandLineParser();
+
constructor(props: Props) {
super(props);
@@ -103,16 +107,16 @@ class Console extends React.Component<Props> {
onChange(e: React.ChangeEvent<HTMLInputElement>) {
const text = e.target.value;
this.props.dispatch(consoleActions.setConsoleText(text));
- if (this.props.mode === 'command') {
- this.props.dispatch(consoleActions.getCompletions(text));
+ if (this.props.mode !== 'command') {
+ return
}
+ this.updateCompletions(text)
}
componentDidUpdate(prevProps: Props) {
if (prevProps.mode !== 'command' && this.props.mode === 'command') {
- this.props.dispatch(
- consoleActions.getCompletions(this.props.consoleText));
+ this.updateCompletions(this.props.consoleText);
this.focus();
} else if (prevProps.mode !== 'find' && this.props.mode === 'find') {
this.focus();
@@ -154,6 +158,36 @@ class Console extends React.Component<Props> {
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 });
diff --git a/src/console/index.tsx b/src/console/index.tsx
index 1209ec2..7bee746 100644
--- a/src/console/index.tsx
+++ b/src/console/index.tsx
@@ -22,11 +22,11 @@ window.addEventListener('load', () => {
wrapper);
});
-const onMessage = (message: any): any => {
+const onMessage = async (message: any): Promise<any> => {
const msg = messages.valueOf(message);
switch (msg.type) {
case messages.CONSOLE_SHOW_COMMAND:
- return store.dispatch(consoleActions.showCommand(msg.command));
+ return store.dispatch(await consoleActions.showCommand(msg.command));
case messages.CONSOLE_SHOW_FIND:
return store.dispatch(consoleActions.showFind());
case messages.CONSOLE_SHOW_ERROR:
diff --git a/src/console/reducers/index.ts b/src/console/reducers/index.ts
index 048a24f..f1508bb 100644
--- a/src/console/reducers/index.ts
+++ b/src/console/reducers/index.ts
@@ -1,11 +1,14 @@
import * as actions from '../actions';
+import Completions from "../Completions";
+import CompletionType from "../../shared/CompletionType";
export interface State {
mode: string;
messageText: string;
consoleText: string;
+ completionTypes: CompletionType[];
completionSource: string;
- completions: any[],
+ completions: Completions;
select: number;
viewIndex: number;
}
@@ -14,6 +17,7 @@ const defaultState = {
mode: '',
messageText: '',
consoleText: '',
+ completionTypes: [],
completionSource: '',
completions: [],
select: -1,
@@ -68,6 +72,7 @@ export default function reducer(
return { ...state,
mode: 'command',
consoleText: action.text,
+ completionTypes: action.completionTypes,
completions: []};
case actions.CONSOLE_SHOW_FIND:
return { ...state,