abbrev (略称展開)

ギャップバッファのつづき。
辞書のような機能を追加します。
Luaを使って手抜きします。

エディタと辞書の連携

と言えば真っ先にに日本語入力システムを思い浮かべるかもしれませんが、
今回作るのはいわゆる静的略称展開の機能です。
そもそもマルチバイト文字に (まだ) 対応していませんので。

画面構成

バッファ
前回作ったギャップバッファです。
ミニバッファ
ここに検索したい文字列を入力します。
ステータスバーのようなもの
ここに検索結果などを表示します。

辞書の形式

基本的にはただのテキスト形式で、
キーと値の組を並べます。キーは1行ですが値は複数行でも可とします。
たとえば ``main'' を「空のmain関数の定義」に展開したいなら以下のように書きます。

<main
>int main(int argc, char *argv[])
>{
>  return 0;
>}
>

このテキストデータを jisyo.txt に保存しておきます。

辞書を読み込む (Lua)

文字列の配列の配列を返す関数をつくります。

以下のLuaのコードはすべて im.lua というファイルに保存してください。

-- input method

local lt = string.byte("<")
local gt = string.byte(">")

im.load = function(jisyo)
  assert(type(jisyo) == "string")
  local xs, xss
  xss = {}
  for line in io.lines(jisyo) do
    local c = string.byte(line)
    if c == lt then
      xs = {string.sub(line, 2)}
      table.insert(xss, xs)
    else
      assert(c == gt)
      assert(#xss > 0)
      table.insert(xs, string.sub(line, 2))
    end
  end
  table.sort(xss, function(xs, ys) return xs[1] < ys[1] end)
  return xss
end

im.dict = im.load("jisyo.txt")

辞書を検索する (Lua)

ミニバッファの内容をLuaの文字列に変換するやり方もありますが、ここでは
ミニバッファの内容とLuaの文字列を比較する関数 im.compare を用意します (後述)。

Lua側では im.compare を使って辞書の二分探索をします。
テーブルの参照は O(1) なのに O(log n) の二分探索をする理由は、
キーの最初の何文字かを入力するだけで済むと便利だからです。

im.search = function()
  local left, right = 1, #(im.dict)
  local mid
  while left < right do
    mid = math.floor((left + right) / 2)
    if im.compare(im.dict[mid][1]) > 0 then
      left = mid + 1
    else
      right = mid
    end
  end
  return im.dict[left]
end

キーボード関数 (Lua)

ミニバッファを使っている時だけLuaが呼ばれます。
これを「Luaモード」と呼ぶことにします。
そうでない時はCだけで処理します (そちらのコードは後述)。

文字列を描画したりバッファに挿入したりするためのCの関数を用意して
Luaから呼べるようにしておきます。

im.insert
バッファの現在のカーソル位置に文字列を挿入します。
im.newline
バッファの現在のカーソル位置で改行します。
im.default_keyboard
ミニバッファに文字を挿入・削除したり、 (Tabで) Luaモードをオフにしたりします。
im.height
ステータスバーの行数です。
(つまり1行とは限りません。というか、実は5行です。)
im.display
ステータスバーに文字列を表示します。
im.clear
ステータスバーの表示をクリアします。
im.keyboardfunc
キーボード関数を登録する関数です。
local cr = string.byte("\r")
local lf = string.byte("\n")

im.height = 5

im.keyboard = function(key)
  if key == cr or key == lf then
    local t = im.search()
    im.insert(t[2])
    for i = 3, #t do
      im.newline()
      im.insert(t[i])
    end
  else
    im.default_keyboard(key)
    local t = im.search()
    im.clear()
    for i = 1, math.min(#t, im.height) do
      im.display(1, i, t[i])
    end
  end
end

im.keyboardfunc(im.keyboard)

Cで書く部分

im.h

extern int im_mode;
void im_init(void);
void im_keyboard(int key);

#define key_max 64
extern unsigned char key[key_max];
extern int key_len;

Luaモードのときim_keyboardが呼ばれます。

im.c

こういうのをさらっと書けてしまうのがLuaの長所です。

#include <stdio.h>
#include <stdlib.h>
#include "lua.h"
#include "lualib.h"
#include "lauxlib.h"
#include "pixels.h"
#include "buffer.h"
#include "im.h"

static int im_compare(lua_State *L)
{
  int i, j;
  const unsigned char *p, *q;
  size_t len;

  p = key;
  i = key_len;
  q = luaL_checklstring(L, 1, &len);
  j = len;
  while (1) {
    if (i == 0) {
      lua_pushinteger(L, 0);
      break;
    } else if (j == 0 || *p > *q) {
      lua_pushinteger(L, 1);
      break;
    } else if (*p < *q) {
      lua_pushinteger(L, -1);
      break;
    }
    i--, j--, p++, q++;
  }
  return 1;
}

static int im_insert(lua_State *L)
{
  const char *p;
  size_t len;
  size_t i;

  p = luaL_checklstring(L, 1, &len);
  for (i = 0; i < len; ++i) {
    if (b_insert(*p++) == -1) { break; }
  }
  lua_pushinteger(L, i);
  return 1;
}

static int im_newline(lua_State *L)
{
  int i;

  i = b_newline();
  lua_pushinteger(L, i);
  return 1;
}

#define DEL 0x7f
static int im_default_keyboard(lua_State *L)
{
  int i;

  i = luaL_checkint(L, 1);
  switch (i) {
  case '\t':
    im_mode = 0;
    i = 0;
    break;
  case '\b':
    key_len = key_len == 0 ? 0 : key_len - 1;
    i = 0;
    break;
  case DEL:
    key_len = 0;
    i = 0;
    break;
  default:
    if (key_len == key_max) { i = -1; }
    else { key[key_len++] = i; i = 0; }
    break;
  }
  lua_pushinteger(L, i);
  return 1;
}

static int im_display(lua_State *L)
{
  int x, y;
  size_t len;
  const char *p;

  x = luaL_checkint(L, 1) - 1;
  y = luaL_checkint(L, 2) + BOTTOM;
  p = luaL_checklstring(L, 3, &len);
  draw_string(x, y, p, len);
  return 0;
}

static int im_clear(lua_State *L)
{
  int i, j;

  for (j = (BOTTOM + 1) * 16; j < HEIGHT; ++j) {
    for (i = 0; i < WIDTH; ++i) {
      pixels_(i, j)[red] = 0;
      pixels_(i, j)[green] = 0;
      pixels_(i, j)[blue] = 0;
    }
  }
  return 0;
}

static int im_keyboardfunc(lua_State *L)
{
  luaL_checktype(L, 1, LUA_TFUNCTION);
  lua_pushlightuserdata(L, im_keyboard);
  lua_insert(L, 1);
  lua_settable(L, LUA_REGISTRYINDEX);
  return 0;
}

int im_mode = 0;
unsigned char key[key_max];
int key_len = 0;

static lua_State *L;

void im_init(void)
{
  static luaL_Reg im[] = {
    {"compare", im_compare},
    {"insert", im_insert},
    {"newline", im_newline},
    {"default_keyboard", im_default_keyboard},
    {"display", im_display},
    {"clear", im_clear},
    {"keyboardfunc", im_keyboardfunc},
    {NULL, NULL}};

  if ((L = luaL_newstate()) == NULL) {
    fprintf(stderr, "failed to create lua_State\n");
    exit(EXIT_FAILURE);
  }
  luaL_openlibs(L);

  luaL_register(L, "im", im);
  lua_getfield(L, -1, "keyboardfunc");
  lua_getfield(L, -2, "default_keyboard");
  lua_call(L, 1, 0);

  if (luaL_loadfile(L, "im.lua")) {
    fprintf(stderr, "failed to load im.lua\n");
    exit(EXIT_FAILURE);
  }
  lua_call(L, 0, 0);
}

void im_keyboard(int key)
{
  lua_pushlightuserdata(L, im_keyboard);
  lua_gettable(L, LUA_REGISTRYINDEX);
  lua_pushinteger(L, key);
  lua_call(L, 1, 0);
}

buffer.h, buffer.c, pixels.h

変更なし。

pixels.c

clear()の中でBOTTOMを(BOTTOM + 1)に変更。

void clear(void)
{
  int i, j;

  for (j = 0; j < (BOTTOM + 1) * 16; ++j) {
    for (i = 0; i < WIDTH; ++i) {
      pixels_(i, j)[red] = 0;
      pixels_(i, j)[green] = 0;
      pixels_(i, j)[blue] = 0;
    }
  }
}

main.c

一応 diff gap_buffer abbrev をして確認。

Only in abbrev: im.c
Only in abbrev: im.h
Only in abbrev: im.lua
Only in abbrev: jisyo.txt
diff gap_buffer/main.c abbrev/main.c
3a4
> #include "im.h"
18a20
>   draw_string(0, BOTTOM, key, key_len);
40a43,47
>   if (im_mode) {
>     im_keyboard(key);
>     glutPostRedisplay();
>     return;
>   }
41a49
>   case '\t': im_mode = 1; break;
78a87
>   im_init();
diff gap_buffer/pixels.c abbrev/pixels.c
161c161
<   for (j = 0; j < BOTTOM * 16; ++j) {
---
>   for (j = 0; j < (BOTTOM + 1) * 16; ++j) {
inserted by FC2 system