Emacs 風のキー入力部 (2) 単独 ESC 文字の受け入れ

前のキー入力部は、 ESC f のように、 ESC に文字を組み合わせることを前提にしていました。 そのため、 keymap で ESC にシンボルを指定しても、 ESC を押した後にキー入力待ちをしてしまいました。 これでは、 インクリメンタル・サーチのように ESC を単独で使う処理の入力部として使えません。

keymap = {
  :print => isearch_insert_char,
  "\C-s" => isearch_repeat_char,
  "\C-r" => isearch_reverse_char,
  "\x7f" => isearch_undo,
  "\e" => isearch_exit,
  "\C-g" => isearch_cancel,
}

ところで、 上のように ESC に単独で意味を持たせているとき、 インクリメンタル・サーチの最中に矢印キー類を押したらどうふるまうべきでしょう。 矢印キーを押すと ESC [ C のような文字列を送り込んできます。 これの先頭の ESC をインクリメンタル・サーチの終了を示す ESC と認識されてしまうと、 サーチから抜けた時点の現点に [ Cの 2 文字を挿入することでしょう。 これはキー入力した人の意図したふるまいとは思えません。

前のキー入力部では ESC に文字を組み合わせることを前提にしていたので、 専用の状態を追加して、 矢印キー類の文字列をまとめて読み込んでいました。 今度は、 単独 ESC を許す場合も、 矢印キー類の文字列をまとめて読む必要があります。 そこで、 ESC を読んだらすぐに tty.ready? で端末から入力待ちせずに文字を読めることを調べ、 矢印キー類の始まりをすぐに読めるなら、 それを読む状態へ遷移することにします。 そうでないとき、 もしも keymap に ESC が trie 木の子に関連付けされていないなら、 unget で読み戻してから、 ESC に関連付けている名前を返します。 tty.ready? で端末から直ちに読める文字がないとき、 もしも keymap に ESC が trie 木の子に関連付けされていないなら、 即座に関連付けされた名前を返します。 それ以外は、 前と同じで、 数値引数・負引数・trie 木の子の読み込みに遷移します。

start 状態で ESC を読んだら、 key_get せずに escape 状態へ移ります。 他の選択肢は前と同じです。

#@<start@>=
  def start(keymap, tty, kont)
    c = @string[-1]
    @pos = @string.size - 1
    @name = keymap[c]
    if "\e" == c
      [:escape, kont]
    elsif Hash === @name
      [:key_get, [:keymap_child, kont]]
    elsif :universal_argument == @name && @arg.nil?
      @arg = [4]
      [:key_get, [:universal_argument, [:start, kont]]]
    elsif @name.nil? && keymap.key?(:print) && isprint?(c.ord)
      @name = keymap[:print]
      kont
    else
      kont
    end
  end

escape 状態で直ちに読み取り可能な文字があるときは、 それから矢印キー類の文字列の始まりかどうかの判定をおこないます。 keymap に ESC が trie 木の子に関連付けされているときは、 先読みした文字で子ノードへ移らせます。 そうでないときは unget します。 escape 状態に入る前に、 既に @name へ ESC が関連付けてある名前が入っているので、 unget したら継続摘要で stop 状態に抜けます。 直ちに読めないとき、 keymap に ESC が trie 木の子へ関連付けされているときは、 key_get で一文字読んでから子ノードへ移らせます。 そうでないときは、 @name に関連付けが終わっているので、 stop 状態に抜けます。

#@<escape@>=
  def escape(keymap, tty, kont)
    if tty.ready?
      @string << tty.get
      if '[' == @string[-1]
        [:key_get, [:ansi_ques, kont]]
      elsif 'O' == @string[-1]
        [:key_get, [:ansi_seq, kont]]
      elsif Hash === @name
        [:esc_map_child, kont]
      else
        [:key_unget, kont]
      end
    elsif Hash === @name
      [:key_get, [:esc_map_child, kont]]
    else
      kont
    end
  end

#@<esc_map_child@>

esc_map_child 状態は、 前の escape 状態から矢印キー類の遷移を除いたものです。

#@<esc_map_child@>=
  def esc_map_child(keymap, tty, kont)
    c = @string[-1]
    @name = @name[c]
    if Hash === @name
      [:key_get, [:keymap_child, kont]]
    elsif @name == :negative_argument && @arg.nil?
      @arg = -1
      [:key_get, [:digit_argument, [:start, kont]]]
    elsif @name == :digit_argument && @arg.nil?
      @arg = @digit = c.to_i
      [:key_get, [:digit_argument, [:start, kont]]]
    else
      kont
    end
  end