diff options
author | Shin'ya Ueoka <ueokande@i-beam.org> | 2020-04-09 10:38:37 +0900 |
---|---|---|
committer | GitHub <noreply@github.com> | 2020-04-09 10:38:37 +0900 |
commit | 1656d52d2cefb3846d968c6117484e6aefe7dabe (patch) | |
tree | ab58a99b832d2571e2168f2ee0e328bc12d9580e /src/console | |
parent | c6c2da8547891b50aef2f08e5f36d258183831ff (diff) | |
parent | 5176643e64d8f4a6be5fc73f0eb48dc65322e496 (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.ts | 11 | ||||
-rw-r--r-- | src/console/actions/console.ts | 193 | ||||
-rw-r--r-- | src/console/actions/index.ts | 27 | ||||
-rw-r--r-- | src/console/clients/CompletionClient.ts | 84 | ||||
-rw-r--r-- | src/console/commandline/CommandLineParser.ts | 38 | ||||
-rw-r--r-- | src/console/commandline/CommandParser.ts | 52 | ||||
-rw-r--r-- | src/console/components/Console.tsx | 42 | ||||
-rw-r--r-- | src/console/index.tsx | 4 | ||||
-rw-r--r-- | src/console/reducers/index.ts | 7 |
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, |