diff options
| author | Shin'ya Ueoka <ueokande@i-beam.org> | 2021-04-11 18:00:51 +0900 | 
|---|---|---|
| committer | Shin'ya Ueoka <ueokande@i-beam.org> | 2021-04-11 22:34:14 +0900 | 
| commit | 618fb497c443662531eb3befe7696a04efe9651d (patch) | |
| tree | 530af78bf75f03e7ffd71e4ca5ad1cf864584be0 /src/console/completion | |
| parent | 21f863d76fbb5ed752ad529f8fbe33e75460027e (diff) | |
Replace completion state with Custom Hooks
Diffstat (limited to 'src/console/completion')
| -rw-r--r-- | src/console/completion/actions.ts | 76 | ||||
| -rw-r--r-- | src/console/completion/context.ts | 9 | ||||
| -rw-r--r-- | src/console/completion/hooks.ts | 277 | ||||
| -rw-r--r-- | src/console/completion/provider.tsx | 25 | ||||
| -rw-r--r-- | src/console/completion/reducer.ts | 96 | 
5 files changed, 483 insertions, 0 deletions
| 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; +  } +} | 
