LoginSignup
23
15

More than 5 years have passed since last update.

D言語で簡単なテキストエディタを開発する話

Posted at

はじめに

どうも皆さんこんにちは。
突然ですが、テキストエディタは何を使ってますか?
ふむ。vim?emacs?はたまたその他?
ここでは、宗教戦争をしたいわけじゃないので特定のエディタについての話をするのはやめます。
とにかく、何かしらお気に入りのテキストエディタがあると思います。
さて、では別の質問です。

テキストエディタを作ったことは有りますか?

作ったことがない方は是非、勉強になるので一度作ってみましょう。
それも、1000行程度で。

さて、どうしよう

僕は実際にD言語でテキストエディタを作りました。それがこちらです:GitHub - alphaKAI/dilo
もともとこれはredisなどを開発されたSalvatore Sanfilippo氏のGitHub - antirez/kiloをD言語に移植したものです。

ここでは、diloを実装していきます。
diloは

  • C/C++およびDのシンタックスハイライト
  • 検索機能

などの実装をして(コメントを抜き、私の個人的なコーディングスタイルで書いて)1300行程度です。
また、今試しにシンタックスハイライトと検索機能を削除したところちょうど870行程度となりました。
なので、とりあえずまずはシンタックスハイライトや検索機能を省いたミニマルなバージョンを開発した後に、シンタックスハイライトと検索機能を実装していく方針とします。

それでは実装しよう!!

さて、実装するにあたり簡単な処理の流れを考えましょう。

  1. エディタ内部を初期化する。
  2. 起動時に渡された引数のファイルが存在すればオープンする。
  3. ターミナルのモードをエスケープシーケンスを用いて切り替える(rawモードへ切り替えるなど)。
  4. イベントループに入る。

イベントループで

  • エディターの表示をリフレッシュ(変更を反映する)。
  • キー入力を待つ。

という感じの流れになると思います。
では、順番に作っていきましょう。
また、賛否両論あるとは思いますが、個人的には問題がなければグローバル変数は使うべきだと考えています。特に、diloのようなプログラムでは問題がなければ積極的に使うべきだと考えています。
また、もともとdiloのコメントやエディタでのメッセージは英語で書きましたが、ここではコメントのみ日本語で書くことにします。

あ、あとこの記事を書きながら思ったのはこれはもともとCのコードをDに直したのでメチャクチャC言語臭いです... もう少しD言語っぽく出来る場所がたくさんあるなぁ...って思ったりしてるのですがとりあえず、この記事では目をつぶります...(この記事を書き上げたあとにリファクタリングを行うかもしれません。そしてもしそのリファクタリングを行った後に余裕があれば、この記事にそれを反映させます。)

importと定数, ユーティリティ関数を定義する。

import core.sys.posix.sys.types,
       core.sys.posix.sys.ioctl,
       core.sys.posix.sys.time,
       core.sys.posix.termios,
       core.sys.posix.unistd,
       core.sys.posix.fcntl,
       core.stdc.string,
       core.stdc.stdlib,
       core.stdc.ctype,
       core.stdc.errno;
import core.memory,
       core.vararg;
import std.algorithm,
       std.format,
       std.string,
       std.array,
       std.stdio,
       std.conv,
       std.file,
       std.path;

/* Memory Management Utilities */

static T malloc(T)(size_t size, uint flags = 0u) {
  return cast(T)GC.calloc(size, flags);
}

static T realloc(T)(T ptr, size_t size) {
  return cast(T)GC.realloc(ptr, size);
}

// Constants
version (OSX) {
  // core.sys.posix.termiosではなぜかOSX向けにはTIOCGWINSZが定義されていないので...
  enum TIOCGWINSZ = 0x40087468;
}

enum DILO_VERSION             = "0.0.1";//diloのバージョン
enum USE_SPACE_INSTADE_OF_TAB = true;//タブの代わりにスペースを使うかどうか
enum TAB_SPACE_SIZE           = 2;//タブのサイズ
enum DILO_QUIT_TIMES          = 3;//diloを強制終了する場合にCtrl+Qを連続して何回押すか

//キー入力のキーコード。
enum KEY_ACTION {
  KEY_NULL     = 0,       /* NULL */
  CTRL_C       = 3,       /* Ctrl-c */
  CTRL_D       = 4,       /* Ctrl-d */
  CTRL_F       = 6,       /* Ctrl-f */
  CTRL_H       = 8,       /* Ctrl-h */
  TAB          = 9,       /* Tab */
  CTRL_L       = 12,      /* Ctrl+l */
  ENTER        = 13,      /* Enter */
  CTRL_Q       = 17,      /* Ctrl-q */
  CTRL_S       = 19,      /* Ctrl-s */
  CTRL_U       = 21,      /* Ctrl-u */
  ESC          = 27,      /* Escape */
  BACKSPACE    = 127,    /* Backspace */
  // 以下の定数はソフトコードに過ぎず、実際にターミナルから直接受け取る値ではない。
  ARROW_LEFT   = 1000,
  ARROW_RIGHT,
  ARROW_UP,
  ARROW_DOWN,
  DEL_KEY,
  HOME_KEY,
  END_KEY,
  PAGE_UP,
  PAGE_DOWN
}

とりあえず、main関数を定義しておく。

順番的には最後にmain関数を書いたほうがいいかもしれませんが、この記事ではmain関数の処理ごとにセクションを設け実装を進めていくので(この小節以降の小節がその実装を行う章です。)、先にmain関数を示します。

void main(string[] args) {
  // dilo <ファイル名> という引数のみを受け付ける
  if (args.length != 2) {
    stderr.writeln("Usage: dilo <filename>");
    exit(1);
  }

  initEditor();                // エディタを初期化する。
  editorOpen(args[1]);         // エディタでファイルを開く。
  enableRawMode(STDIN_FILENO); // rawモードを有効化する。
  editorSetStatusMessage("HELP: Ctrl-S = save | Ctrl-Q = quit"); //ステータスメッセージを設定する。

  // イベントループ
  while (1) {
    editorRefreshScreen();               // 変更を反映する。
    editorProcessKeypress(STDIN_FILENO); // キー入力を待つ。
  }
}

エディタの状態を管理する構造体を作る。

// テキストの行を保持する構造体。
struct Erow {
  ulong   idx,    // ファイルの何行目か
          size,   // この行のサイズ。但しnull文字は除く
          rsize;  // レンダリングするサイズ
  char*   chars;  // 行の中身(テキスト本体)
  char*   render; // 実際にレンダリングされる行の中身(タブ文字の為)
}

// エディタの状態を保持する構造体
struct EditorConfig {
  ulong  cx, cy;      // テキスト全体における、カーソルのx座標とy座標
  ulong  rowoff,      // 実際に表示されている行のオフセット
         coloff;      // 実際に表示されている列のオフセット
  int    screenrows,  // 見た目上の行数
         screencols,  // 見た目上の列数
         numrows;     // 行数
  bool   rawmode;     // ターミナルのraw modeが有効かどうか
  Erow*  row;         // テキストの行全体(要するに、ファイル全体)
  bool   dirty;       // ファイルが変更されたが保存されていない場合に真
  string filename;    // ファイル名
  Appender!string statusmsg; // ステータスメッセージ
  time_t          statusmsg_time;
  termios         orig_termios;  // 終了時にリストアするために保持する、もともとのターミナルの状態
}

/* 現在開いてるファイルの状態を保持するグローバル変数。
 * diloでは同時に開けるファイルは1つであり、これをグローバル変数として保持するほうが
 * 行数も減り、可読性も向上する。
 * コードや記事中では以後、Eが保持する状態を環境と呼ぶこともある。
 */
static EditorConfig E;

エディタを起動時に初期化する。

void initEditor() {
  if (getWindowSize(STDIN_FILENO, STDOUT_FILENO, &E.screenrows, &E.screencols) == -1) {
    perror("Unable to query the screen for size (columns / rows)");
    exit(1);
  }

  E.screenrows -= 2; /* ステータスバーの為の空間を開ける */
}

initEditor内で使われてるgetWindowSizeなどを定義する

 /* エスケープシーケンス(ESC [6n)を使ってカーソルの水平方向の座標を取得し、それを返す。
 * エラー発生すると-1が返され、成功したときは*row, *colsにカーソルの座標を格納し0を返す。 */
int getCursorPosition(int ifd, int ofd, int* rows, int* cols) {
  char[32] buf;
  uint i;

  if (core.sys.posix.unistd.write(ofd, "\x1b[6n".toStringz, 4) != 4) return -1;

  /* レスポンスを読み込む: ESC [ rows; cols R */
  while (i < buf.sizeof - 1) {
    if (read(ifd, buf.ptr + i, 1) != 1) break;
    if (buf[i] == 'R') break;
    i++;
  }

  buf[i] = '\0';

  /* 読み込んだバッファをパースする */
  if (buf[0] != KEY_ACTION.ESC || buf[1] != '[') return -1;
  if (sscanf(buf.ptr + 2, "%d;%d", rows, cols) != 2) return -1;

  return 0;
}

/* 現在のターミナルにおける列番号を得ることを試みる。
 * もし、ioctl()のコールが失敗した場合、ターミナルから情報を得る。
 * 成功時には0を、失敗時には-1を返す。*/
int getWindowSize(int ifd, int ofd, int* rows, int* cols) {
  winsize ws;

  if (ioctl(1, TIOCGWINSZ, &ws) == -1 || ws.ws_col == 0) {
    /* ioctol() が失敗した場合。 ターミナルから情報を得る。*/
    int orig_row, orig_col, retval;

    /* 後でリストアするために初期位置を保存しておく。 */
    retval = getCursorPosition(ifd, ofd, &orig_row, &orig_col);

    if (retval == -1) {
      goto failed;
    }

    /* 右側と下端へ移動し座標を得る。 */
    if (core.sys.posix.unistd.write(ofd, "\x1b[999C\x1b[999B".toStringz, 12) != 12) {
      goto failed;
    }

    retval = getCursorPosition(ifd, ofd, rows, cols);

    if (retval == -1) {
      goto failed;
    }

    /* 座標をリストアする。 */
    auto seq = appender!string;
    formattedWrite(seq, "\x1b[%d;%dH", orig_row, orig_col);

    if (core.sys.posix.unistd.write(ofd, seq.data.toStringz, seq.data.length) == -1) {
      /* 回復不可能 */
    }
    return 0;
  } else {
    *rows = ws.ws_row;
    *cols = ws.ws_col;
    return 0;
  }

failed:
  return -1;
}

起動時の引数で渡されたファイルが存在すればオープンする

int editorOpen(string filename) {
  E.dirty    = false; // ファイルを読み込んでいないのでfalse
  E.filename = filename.dup; // 環境にファイル名を設定する

  // 指定されたファイルが存在しない場合はreturn
  if (!exists(filename)) {
    return 1;
  }

  auto file = File(filename, "r");

  // 行ごとにファイルを読み込む。
  foreach (line; file.byLine) {
    editorInsertRow(E.numrows, line.to!string);
  }

  // editorInsertRowではE.dirtyがtrueに設定されるが、初期化的な読み込みのため当然変更を加えてないのでfalseに再設定する。
  E.dirty = false;

  return 0;
}

エディターにコンテンツを追加するための関数を定義する。

ここでは、上で定義したeditorOpen内でつかったeditorInsertRowやそのような関数を定義する。

/* エディタの行に関する実装 */

/* レンダリングされたバージョンを更新する。 */
void editorUpdateRow(Erow* row) {
  ulong tabs,
        noprint,
        idx;

  /* タブ文字を尊重した直接画面に表示するための行を生成する。ただし、USE_SPACE_INSTADE_OF_TABがfalseである場合はスペースはタブに置換される。加えて、表示不可能な文字は'?'に置換される。 */
  GC.free(row.render);

  static if (!USE_SPACE_INSTADE_OF_TAB) {
    for (ulong j; j < row.size; j++) {
      if (row.chars[j] == KEY_ACTION.TAB) {
        tabs++;
      }
    }
  }

  row.render = malloc!(char*)(row.size + (tabs * 8) + (noprint * 9) + 1, GC.BlkAttr.NO_SCAN | GC.BlkAttr.APPENDABLE);

  for (ulong j; j < row.size; j++) {
    static if (USE_SPACE_INSTADE_OF_TAB) {
      row.render[idx++] = row.chars[j];
    } else {
      with (KEY_ACTION) if (row.chars[j] == TAB) {
        row.render[idx++] = ' ';

        while ((idx + 1) % 8 != 0) {
          row.render[idx++] = ' ';
        }
      } else {
        row.render[idx++] = row.chars[j];
      }
    }
  }

  row.rsize       = idx;
  row.render[idx] = '\0';
}

/* 指定された場所に行を挿入する。必要であればシフトする。 */
void editorInsertRow(ulong at, string s, size_t length = 0) {
  if (s.length > 0 && length == 0) {
    length = s.length;
  }

  if (at > E.numrows) {
    return;
  }

  E.row = realloc(E.row, (Erow).sizeof * (E.numrows + 1));

  if (at != E.numrows) {
    memmove(E.row + at + 1, E.row + at, (E.row[0]).sizeof*(E.numrows-at));    

    for (ulong j = at+1; j <= E.numrows; j++) {
      E.row[j].idx++;
    }
  }

  E.row[at].size  = length;
  E.row[at].chars = malloc!(char*)(length + 1);
  memcpy(E.row[at].chars, s.toStringz, length + 1);
  E.row[at].render = null;
  E.row[at].rsize  = 0;
  E.row[at].idx    = at;
  editorUpdateRow(&E.row[at]);
  E.numrows++;
  E.dirty = true;
}

/* 確保したメモリを開放する。 */
void editorFreeRow(Erow* row) {
  GC.free(row.render);
  GC.free(row.chars);
}

/* 指定された行を削除し、行全体を上にシフトする。 */
void editorDelRow(ulong at) {
  Erow* row;

  if (at >= E.numrows) {
    return;
  }

  row = E.row + at;
  editorFreeRow(row);
  memmove(E.row + at, E.row + at + 1, (E.row[0]).sizeof *(E.numrows - at - 1));

  for (ulong j = at; j < E.numrows-1; j++) {
    E.row[j].idx++;
  }

  E.numrows--;
  E.dirty = true;
}

/* 行全体を改行文字'\n'を挟んで結合してstringにして返す。 */
string editorRowsToString() {
  string buf;

  for (ulong j; j < E.numrows; j++) {
    foreach (s; E.row[j].chars[0..E.row[j].size]) {
      buf ~= s;
    }

    buf ~= "\n";
  }

  return buf;
}

/* 指定された行の座標に文字を挿入する。 必要であれば挿入された文字より右側全体をずらす。 */
void editorRowInsertChar(Erow* row, ulong at, int c) {
  if (at > row.size) {
    ulong padlen = at-row.size;

    row.chars = realloc(row.chars, row.size + padlen + 2);
    row.chars[row.size..(row.size + padlen)] = ' ';
    row.chars[row.size + padlen + 1] = '\0';
    row.size += padlen+1;
  } else {
    row.chars = realloc(row.chars, row.size + 2);
    memmove(row.chars + at + 1, row.chars + at, row.size - at + 1);

    row.size++;
  }

  row.chars[at] = cast(char)c;
  editorUpdateRow(row);
  E.dirty = true;
}

/* 行末にstring sを付加する。 */
void editorRowAppendString(Erow* row, string s) {
    row.chars = realloc(row.chars, row.size + s.length + 1);
    memcpy(row.chars + row.size, s.toStringz, s.length);    

    row.size           += s.length;
    row.chars[row.size] = '\0';
    editorUpdateRow(row);
    E.dirty = true;
}

/* 特定の行の中の、オフセット'at'で指定された文字を削除する。 */
void editorRowDelChar(Erow* row, ulong at) {
  if (row.size <= at) {
    return;
  }

  memmove(row.chars + at, row.chars + at + 1, row.size - at);
  editorUpdateRow(row);
  row.size--;
  E.dirty = true;
}

/* 現在のプロンプトが存在する場所に文字を追加する。 */
void editorInsertChar(int c) {
  ulong filerow = E.rowoff+E.cy;
  ulong filecol = E.coloff+E.cx;
  Erow* row = (filerow >= E.numrows) ? null : &E.row[filerow];

  /* もし、カーソルが現在位置している場所が編集中のファイルのなかの論理領域に
   * 存在しない場合、十分な空行を必要なだけ追加する。
   */
  if (!row) {
    while (E.numrows <= filerow) {
      editorInsertRow(E.numrows, "");
    }
  }

  row = &E.row[filerow];
  editorRowInsertChar(row, filecol, c);

  if (E.cx == E.screencols-1) {
    E.coloff++;
  } else {
    E.cx++;
  }

  E.dirty = true;
}

/* 改行を挿入することは、行の途中で改行を挿入し、
 * 必要に応じて改行する必要があるため少々複雑になる。
 */
void editorInsertNewline() {
  ulong filerow = E.rowoff+E.cy;
  ulong filecol = E.coloff+E.cx;
  Erow* row     = (filerow >= E.numrows) ? null : &E.row[filerow];

  if (!row) {
    if (filerow == E.numrows) {
      editorInsertRow(filerow, "");
      goto fixcursor;
    }

    return;
  }

  /* カーソルが現在の行のサイズを超えた場合、
   * 概念的にそれが最後の文字を超えたということになる。
   */
  if (filecol >= row.size) {
    filecol = row.size;
  }

  if (filecol == 0) {
    editorInsertRow(filerow, "");
  } else {
    /* 行の途中にカーソルがあるため、そこに改行を挿入することで現在の行を2つに分割する。 */
    editorInsertRow(filerow+1, (row.chars + filecol).to!string, row.size - filecol);

    row                = &E.row[filerow];
    row.chars[filecol] = '\0';
    row.size           = filecol;
    editorUpdateRow(row);
  }

fixcursor:
  if (E.cy == E.screenrows - 1) {
    E.rowoff++;
  } else {
    E.cy++;
  }

  E.cx = 0;
  E.coloff = 0;
}

/* 現在プロンプトがある場所の文字を削除する。 */
void editorDelChar() {
  ulong filerow = E.rowoff + E.cy;
  ulong filecol = E.coloff + E.cx;
  Erow* row     = (filerow >= E.numrows) ? null : &E.row[filerow];

  if (!row || (filecol == 0 && filerow == 0)) {
    return;
  }

  if (filecol == 0) {
    /* columnが0である場合、すなわちカーソルが左端にあってそこでバックスペースが押された場合、
     * 一つ上の行の右に現在の行の内容を移動させる。
     */
    filecol = E.row[filerow - 1].size;
    editorRowAppendString(&E.row[filerow - 1], row.chars[0..row.size].to!string);
    editorDelRow(filerow);
    row = null;

    if (E.cy == 0) {
      E.rowoff--;
    } else {
      E.cy--;
    }

    E.cx = filecol;

    if (E.cx >= E.screencols) {
      ulong shift = (E.screencols - E.cx) + 1;
      E.cx       -= shift;
      E.coloff   += shift;
    }
  } else {
    editorRowDelChar(row, filecol - 1);

    if (E.cx == 0 && E.coloff) {
      E.coloff--;
    } else {
      E.cx--;
    }
  }

  if (row) {
    editorUpdateRow(row);
  }

  E.dirty = true;
}

ターミナルのモードをエスケープシーケンスを用いてrawモードへ切り替える。

/* Low Level terminal handling */

/* Rawモードを無効にする。 */
void disableRawMode(int fd) {
  /* Don't even check the return value as it's too late. */
  if (E.rawmode) {
    tcsetattr(fd, TCSAFLUSH, &E.orig_termios);
    E.rawmode = false;
  }
}

/* rawモードのままになることを避けるために終了時にコールする。 */
extern (C) void editorAtExit() {
  disableRawMode(STDIN_FILENO);
  write("\x1B[2J\x1b[1;1H");
}

/* rawモードを有効にする。 */
int enableRawMode(int fd) {
  termios raw;

  if (E.rawmode) {
    return 0; /* すでに有効になっている。 */
  }

  if (!isatty(STDIN_FILENO)) {
    goto fatal;
  }

  // コールバックを設定する。
  atexit(&editorAtExit);

  if (tcgetattr(fd, &E.orig_termios) == -1) {
    goto fatal;
  }

  raw = E.orig_termios; /* オリジナルのターミナルを変更する。 */
  /* 入力モードを設定する: no break, no CT to NL, no parity check, no strip char, no start/stop output control */
  raw.c_iflag &= ~(BRKINT | ICRNL | INPCK | ISTRIP | IXON);
  /* 出力モード - 後処理を無効にする。 */
  raw.c_oflag &= ~(OPOST);
  /* コントロールモード - 8bit文字を設定する。 */
  raw.c_cflag |= CS8;
  /* ロケールモードの設定 - echoing off, canonical off, no extended function, no signal chars (^Z, ^C) */
  raw.c_lflag &= ~(ECHO | ICANON | IEXTEN | ISIG);
  /* 制御文字の設定。 - 終了条件を設定する: min number of bytes and timer. */
  raw.c_cc[VMIN]  = 0; /* Return each byte. or zero for timeout. */
  raw.c_cc[VTIME] = 1; /* 100ms timeout (unit is tens of second).*/

  /* フラッシュした後にターミナルをrawモードにする。 */
  if (tcsetattr(fd, TCSAFLUSH, &raw) < 0) {
    goto fatal;
  }

  E.rawmode = true;

  return 0;

fatal:
  errno = ENOTTY;
  return -1;
}

ステータスバーにメッセージを設定する。

/* 画面の下端にあるステータスラインの2行目にステータスメッセージを表示する。 */
void editorSetStatusMessage(FMT, A...)(FMT fmt, A a) if (is(FMT == string)) {
  E.statusmsg = appender!string;  
  formattedWrite(E.statusmsg, fmt, a);
  E.statusmsg_time = time(null);
}

メインループに入る。

main関数の以下のループがメインループです。
d
while (1) {
editorRefreshScreen();
editorProcessKeypress(STDIN_FILENO);
}

ここではeditorRefreshScreeneditorProcessKeypress(とそのヘルパ関数)を実装します。

/* Terminal Update */

/* この関数では、外面全体をVT1000の制御文字を使って描画する。
 * 開始位置は、グローバル変数'E'の持つ論理状態によって決まる。
 */
void editorRefreshScreen() {
  ulong  y;
  Erow*  r;
  string str;

  str ~= "\x1b[?25l";  /* カーソルを隠す。 */
  str ~= "\x1b[H";     /* カーソルをホームポジションへ移動させる。 */

  for (; y < E.screenrows; y++) {
    ulong filerow = E.rowoff + y;

    if (filerow >= E.numrows) {
      if (E.numrows == 0 && y == E.screenrows / 3) {
        Appender!string welcome = appender!string;

        formattedWrite(welcome, "Dilo editor -- verison %s\x1b[0K\r\n", DILO_VERSION);

        ulong padding = (E.screencols - welcome.data.length) / 2;

        if (padding) {
          str ~= "~";
          padding--;
        }

        while (padding--) {
          str ~= " ";
        }

        str ~= welcome.data;
      } else {
        str ~= "~\x1b[0K\r\n";
      }
      continue;
    }

    r = &E.row[filerow];

    ulong len           = r.rsize - E.coloff;
    int   current_color = -1;

    if (len > 0) {
      if (len > E.screencols) {
        len = E.screencols;
      }

      char*  c  = r.render + E.coloff;

      for (ulong j; j < len; j++) {
        str ~= c[j];
      }
    }

    str ~= "\x1b[39m";
    str ~= "\x1b[0K";
    str ~= "\r\n";
  }

  /* ステータスラインを2行作る。一行目に関する実装: */
  str ~= "\x1b[0K";
  str ~= "\x1b[7m";

  auto status  = appender!string,
       rstatus = appender!string;

  formattedWrite(status, "%.20s - %d lines %s", E.filename, E.numrows, E.dirty ? "(modified)" : "");
  formattedWrite(rstatus, "%d/%d", E.rowoff + E.cy + 1, E.numrows);

  ulong len  = status.data.length,
        rlen = rstatus.data.length;

  if (len > E.screencols) {
    len = E.screencols;
  }

  str ~= status.data;

  while(len < E.screencols) {
    if (E.screencols - len == rlen) {
      str ~= rstatus.data;
      break;
    } else {
      str ~= " ";
      len++;
    }
  }

  str ~= "\x1b[0m\r\n";

  /* 二行目はE.statusmsgとそのメッセージの更新時間に依存する。 */
  str ~= "\x1b[0K";
  ulong msglen = E.statusmsg.data.length;
  if (msglen && time(null) - E.statusmsg_time < 5) {
    str ~= E.statusmsg.data;
  }

  /* カーソルを現在の座標に設定する。タブ文字のせいで表示されているカーソルの
   * 水平方向の座標がE.cxとずれているかもしれないことに注意しなければならない。
   */
  ulong j;
  ulong cx      = 1;
  ulong filerow = E.rowoff + E.cy;
  Erow* row = (filerow >= E.numrows) ? null : &E.row[filerow];

  if (row) {
    for (j = E.coloff; j < (E.cx+E.coloff); j++) {
      with (KEY_ACTION) if (j < row.size && row.chars[j] == TAB) {
        cx += 7 - ((cx) % 8);
      }
      cx++;
    }
  }

  auto buf = appender!string;

  formattedWrite(buf, "\x1b[%d;%dH", E.cy + 1, cx);
  str ~= buf.data;
  str ~= "\x1b[?25h"; /*カーソルを表示する。*/
  core.sys.posix.unistd.write(STDOUT_FILENO, str.toStringz, str.length);
}

/* Editor events handling */
/* 矢印キーが押されたらカーソルの位置を変える。 */
void editorMoveCursor(int key) {
  ulong filerow = E.rowoff + E.cy;
  ulong filecol = E.coloff + E.cx;
  Erow* row     = (filerow >= E.numrows) ? null : &E.row[filerow];
  ulong rowlen;

  with (KEY_ACTION) switch(key) {
    case ARROW_LEFT:
      if (E.cx == 0) {
        if (E.coloff) {
          E.coloff--;
        } else {
          if (filerow > 0) {
            E.cy--;
            E.cx = E.row[filerow - 1].size;

            if (E.cx > E.screencols - 1) {
              E.coloff = E.cx - E.screencols + 1;
              E.cx = E.screencols - 1;
            }
          }
        }
      } else {
        E.cx -= 1;
      }
      break;
    case ARROW_RIGHT:
      if (row && filecol < row.size) {
        if (E.cx == E.screencols - 1) {
          E.coloff++;
        } else {
          E.cx += 1;
        }
      } else if (row && filecol == row.size) {
        E.cx     = 0;
        E.coloff = 0;

        if (E.cy == E.screenrows - 1) {
          E.rowoff++;
        } else {
          E.cy += 1;
        }
      }
      break;
    case ARROW_UP:
      if (E.cy == 0) {
        if (E.rowoff) {
          E.rowoff--;
        }
      } else {
        E.cy -= 1;
      }
      break;
    case ARROW_DOWN:
      if (filerow < E.numrows) {
        if (E.cy == E.screenrows - 1) {
          E.rowoff++;
        } else {
          E.cy += 1;
        }
      }
      break;
    default: break;
  }

  /* 現在の行が十分な文字を持っていない場合にcxを修正する。 */
  filerow = E.rowoff+E.cy;
  filecol = E.coloff+E.cx;
  row     = (filerow >= E.numrows) ? null : &E.row[filerow];
  rowlen  = row ? row.size : 0;

  if (filecol > rowlen) {
    E.cx -= filecol - rowlen;

    if (E.cx < 0) {
      E.coloff += E.cx;
      E.cx      = 0;
    }
  }
}

/* rawモードのターミナルに入力されたキーを受取る、また、エスケープシーケンスを取り扱うことを試みる。 */
int editorReadKey(int fd) {
  long nread;
  char c;
  char[3] seq;

  while ((nread = read(fd, &c, 1)) == 0) {}
  if (nread == -1) exit(1);

  while (true) {
    with (KEY_ACTION) switch (c) {
      case ESC: /* excape sequence */
        /* エスケープだけが押された場合に、ここでタイムアウトする。 */
        if (read(fd, seq.ptr, 1) == 0) {
          return ESC;
        }

        if (read(fd, seq.ptr + 1, 1) == 0) {
          return ESC;
        }

        /* ESC [ sequence */
        if (seq[0] == '[') {
          if (0 <= seq[1] && seq[1] <= '9') {
            /* 拡張エスケープシーケンス。追加のbyteを読み込む。 */
            if (read(fd, seq.ptr + 2, 1) == 0) {
              return ESC;
            }

            if (seq[2] == '~') {
              switch (seq[1]) {
                case '3': return DEL_KEY;
                case '5': return PAGE_UP;
                case '6': return PAGE_DOWN;
                default: break;
              }
            }
          } else {
            switch (seq[1]) {
              case 'A': return ARROW_UP;
              case 'B': return ARROW_DOWN;
              case 'C': return ARROW_RIGHT;
              case 'D': return ARROW_LEFT;
              case 'H': return HOME_KEY;
              case 'F': return END_KEY;
              default: break;
            }
          }
        } else if (seq[0] == 'O') { /* ESC O sequence */
          switch (seq[1]) {
            case 'H': return HOME_KEY;
            case 'F': return END_KEY;
            default: break;
          }
        }
        break;
      default:
        return c;
    }
  }
}

/* ユーザーがターミナルにタイピングした内容を
 * 標準入力から受け取って処理する。 
 */
void editorProcessKeypress(int fd) {
  /* 本当に終了していいかどうかを確認する必要があるため、
   * ファイルが変更されていた場合、Ctrl-QをDILO_QUITE_TIMES回連打することを要求する。
   */
  static int quit_times = DILO_QUIT_TIMES;
  int c = editorReadKey(fd);

  with (KEY_ACTION) switch (c) {
    case ENTER:         /* Enter */
      editorInsertNewline();
      break;
    case CTRL_C:        /* Ctrl-c */
      /* 編集中のファイルを維持することは単純ではないため、Ctrl-Cを無視する。 */
      break;
    case CTRL_Q:        /* Ctrl-q */
      /* すでにファイルが保存されている場合にのみ終了する。 */
      if (E.dirty && quit_times) {
        editorSetStatusMessage("WARNING!!! File has unsaved changes. "
            "Press Ctrl-Q %s more times to quit.", quit_times.to!string);
        quit_times--;
        return;
      }
      exit(0);
      break;
    case CTRL_S:        /* Ctrl-s */
      editorSave();
      break;
    case BACKSPACE:     /* Backspace */
    case CTRL_H:        /* Ctrl-h */
    case DEL_KEY:
      editorDelChar();
      break;
    case PAGE_UP:
    case PAGE_DOWN:
      if (c == PAGE_UP && E.cy != 0) {
        E.cy = 0;
      } else if (c == PAGE_DOWN && E.cy != E.screenrows - 1) {
        E.cy = E.screenrows - 1;
      }
      ulong times = E.screenrows;
      while(times--) {
        editorMoveCursor(c == PAGE_UP ? ARROW_UP: ARROW_DOWN);
      }
      break;
    case ARROW_UP:
    case ARROW_DOWN:
    case ARROW_LEFT:
    case ARROW_RIGHT:
      editorMoveCursor(c);
      break;
    case CTRL_L: /* Ctrl-l, clear screen */
      /* 副作用があるため何もしない。 */
      break;
    case ESC:
      /* このモードでESCに対して行うことは何もない。*/
     break;
    case TAB:
      static if (USE_SPACE_INSTADE_OF_TAB) {
        for (int j = 0; j < TAB_SPACE_SIZE; j++) {
          editorInsertChar(' ');
        }
      }
      break;
    default:
      editorInsertChar(c); /* 特殊文字でない場合、普通にエディターに挿入する。 */
      break;
  }

  quit_times = DILO_QUIT_TIMES; /* もともとの値をセットする。 */
}

/* 編集中のバッファをファイルに保存する。 */
int editorSave() {
  int    len;
  string buf  = editorRowsToString();
  auto   file = File(E.filename, "w");

  file.write(buf);

  editorSetStatusMessage("%d bytes written on disk", buf.length);

  E.dirty = false;

  return 0;
}

ここまでの内容をdilo.dというファイル名のファイルに保存した後

$ dmd dilo.d

でコンパイルが出来ます。

試しにdilo.dを開いてみましょう。

スクリーンショット 2016-12-01 1.32.54.png

はい!dilo自身でdilo.dを開けましたね!
これで、dilodilo自身で開発することが出来ます!!セルフホスティング!!

とりあえず、ここまでで最低限のテキストエディタを作ることが出来ました。
しかし、これでは快適にコードを書くことが出来ません。
というのも、シンタックスハイライトもなければ、検索もできません。
したがって、初めに検索機能を実装します。(こちらの方が少ない行数で実装できて簡単なので。)

検索機能を実装する。

既存のファイルに対する変更は2つです。一つは実際に検索するコードで、もう一つは検索モードに入るための処理の追加です。
検索するための関数をeditorFindとします。

実際に検索を行う関数は次のコードで実装します。

/* 検索クエリのMAXを仮に256文字と設定する。 */
enum DILO_QUERY_LEN = 256;

void editorFind(int fd) {
  char[DILO_QUERY_LEN+1] query = 0;
  ulong qlen          = 0;
  long  last_match    = -1;  /* 見つかった場合の最後の行。見つからない場合、-1が設定される。 */
  long  find_next     = 0;   /* 1が設定されている場合、現在の行以降を、-1の場合、現在の行以前を検索する。*/

  /* 後からリストアするために現在のカーソルの座標を保存する。 */
  ulong saved_cx     = E.cx,
        saved_cy     = E.cy;
  ulong saved_coloff = E.coloff,
        saved_rowoff = E.rowoff;

  with (KEY_ACTION) while (1) {
    editorSetStatusMessage("Search: %s (Use ESC/Arrows/Enter)", query);
    editorRefreshScreen();

    int c = editorReadKey(fd);

    if (c == DEL_KEY || c == CTRL_H || c == BACKSPACE) {
      if (qlen != 0) {
        query[--qlen] = '\0';
      }

      last_match = -1;
    } else if (c == ESC || c == ENTER) {
      if (c == ESC) {
        E.cx     = saved_cx; E.cy = saved_cy;
        E.coloff = saved_coloff; E.rowoff = saved_rowoff;
      }

      editorSetStatusMessage("");

      return;
    } else if (c == ARROW_RIGHT || c == ARROW_DOWN) {
      find_next = 1;
    } else if (c == ARROW_LEFT || c == ARROW_UP) {
      find_next = -1;
    } else if (isprint(c)) {
      if (qlen < DILO_QUERY_LEN) {
        query[qlen++] = cast(char)c;
        query[qlen]   = '\0';
        last_match    = -1;
      }
    }

    if (last_match == -1) {
      find_next = 1;
    }

    if (find_next) {
      char* match;
      long  match_offset;
      long  current = last_match;

      for (ulong i; i < E.numrows; i++) {
        current += find_next;

        if (current == -1) {
          current = E.numrows - 1;
        } else if (current == E.numrows) {
          current = 0;
        }

        import core.stdc.string;
        match = strstr(E.row[current].render, query.ptr);

        if (match) {
          match_offset = match-E.row[current].render;
          break;
        }
      }

      find_next = 0;

      if (match !is null) {
        Erow* row  = &E.row[current];
        last_match = current;

        E.cy     = 0;
        E.cx     = match_offset;
        E.rowoff = current;
        E.coloff = 0;

        if (E.cx > E.screencols) {
          ulong diff = E.cx - E.screencols;
          E.cx     -= diff;
          E.coloff += diff;
        }
      }
    }
  }
}

また、Ctrl + Fで検索モードに入るために、editorProcessKeypress関数のswitchcase CTRL_Fを追加します。

    /*中略*/
    case CTRL_S:        /* Ctrl-s */
      editorSave();
      break; // これの後に追記する
    case CTRL_F:
      editorFind(fd);
      break;
    case BACKSPACE:     /* Backspace */
    /*以後省略*/

また、ステータスメッセージを変更しましょう。main関数内のeditorSetStatusMessageの引数を書き換えます。

これで検索が可能になります。

はい。これで検索は実装できました。しかし、このままでは不便です。
シンタックスハイライトが無いだけでなく、検索した文字列がハイライトされません。
ですので、今からシンタックスハイライトを実装します。

シンタックスハイライトを実装する。

シンタックスハイライトを実装するために以下の変更を加える必要があります。

  • シンタックスを表す構造体等を定義する。
  • シンタックスハイライト用の辞書を定義する。
  • ErowとEditorConfigにシンタックスに関する情報を持たせる。
  • 既存の幾つかの関数を変更してシンタックスハイライトに対応&リアルタイムに反映させる。

この4ステップを実装することでシンタックスハイライトを実現します。

シンタックスを表す構造体とシンタックスの種類を表す列挙体を定義する。

/* シンタックスの種類を表す列挙体 */
enum HL {
  NORMAL,
  NONPRINT,
  COMMENT,   
  MLCOMMENT, 
  KEYWORD1,
  KEYWORD2,
  STRING,
  NUMBER,
  MATCH 
}

/* シンタックスを表す構造体 */
struct EditorSyntax {
  string   syntaxName;
  string[] filematch,
           keywords;
  string   singleline_comment_start,
           multiline_comment_start,
           multiline_comment_end;
}

シンタックスハイライト用の辞書を定義する

/* C/C++用のシンタックスハイライト(私はC/C++のキーワードをあまり知らないので不十分だと思いますが...)。 */
/* 2種類のハイライトが可能で、キーワードの右端に|を付加すると色を変えることが可能。 */
enum string[] C_HL_extensions = [".c", ".cpp"];
enum string[] C_HL_keywords   = [
  "switch", "if", "while", "for", "break", "continue", "return", "else",
  "struct", "union", "typedef", "static", "enum", "class",
  "int|","long|","double|","float|","char|","unsigned|","signed|",
  "void|"
];

/* D用のシンタックスハイライト。 */
enum string[] D_HL_extensions = [".d"];
enum string[] D_HL_keywords   = [
  "switch", "if", "while", "for", "break", "continue", "return", "else",
  "struct", "union", "class", "static", "enum", "alias", "mixin", "template",
  "final", "do", "foreach", "scope", "in", "body", "out", "const", "immutable",
  "delegate", "function", "version", "is", "typeof", "typeid", "with", "import",
  "byte|", "ubyte|", "short|", "ushort|", "int|", "uint|", "long|", "ulong|",
  "string|", "wstring|", "dstring|", "char|", "wchar|", "dchar|", "float|", "double|", "real|", "void|"
];

/* シンタックスハイライトに関する情報を保持する辞書。
 * 上のようなキーワード列を定義し、以下に同様のデータを追加することで
 * 他の言語のシンタックスハイライトにも対応させることが可能。 
 */
enum static EditorSyntax[] HLDB = [
  {
    "C/C++",
    C_HL_extensions,
    C_HL_keywords,
    "//",
    "/*",
    "*/"
  },
  {
    "D",
    D_HL_extensions,
    D_HL_keywords,
    "//",
    "/*",
    "*/"
  }
];

/* シンタックスハイライトの種類を保持する。 */
enum HLDB_ENTRIES = HLDB.length;

ErowとEditorConfigを拡張して、シンタックスハイライトに関する情報を保持させる。

変更分だけを書いてもわかりにくいので再度定義します。

// テキストの行を保持する構造体。
struct Erow {
  ulong   idx,    // ファイルの何行目か
          size,   // この行のサイズ。但しnull文字は除く
          rsize;  // レンダリングするサイズ
  char*   chars;  // 行の中身(テキスト本体)
  char*   render; // 実際にレンダリングされる行の中身(タブ文字の為)
  ubyte*  hl;     // renderの文字ごとのシンタックスハイライトの種類を保持する。
  bool    hl_oc;  // この行がコメントの開始トークンを持っているかどうか。
}

// エディタの状態を保持する構造体
struct EditorConfig {
  ulong  cx, cy;      // テキスト全体における、カーソルのx座標とy座標
  ulong  rowoff,      // 実際に表示されている行のオフセット
         coloff;      // 実際に表示されている列のオフセット
  int    screenrows,  // 見た目上の行数
         screencols,  // 見た目上の列数
         numrows;     // 行数
  bool   rawmode;     // ターミナルのraw modeが有効かどうか
  Erow*  row;         // テキストの行全体(要するに、ファイル全体)
  bool   dirty;       // ファイルが変更されたが保存されていない場合に真
  string filename;    // ファイル名
  Appender!string statusmsg; // ステータスメッセージ
  time_t          statusmsg_time;
  EditorSyntax*   syntax;        // 現在のシンタックスハイライト、またはnull
  termios         orig_termios;  // 終了時にリストアするために保持する、もともとのターミナルの状態
}

シンタックスハイライトを行う関数を実装と既存の関数を改変してシンタックスハイライトに対応&リアルタイムに反映させる。

今から加える変更は

  • シンタックスハイライトを行う関数editorUpdateSyntaxとそのヘルパ関数を定義する。
  • シンタックスハイライトに対応させるために、editorUpdateRoweditorInsertRow,editorFreeRowに多少の追記を行う。
  • シンタックスハイライトにリアルタイムに反映させるために、editorRefreshScreenを大幅に拡張する。

順番に行いましょう。

シンタックスハイライトを行うためのeditorUpdateSyntax関数とそのヘルパ関数を実装する。

/* row.hlの全てのバイトにシンタックスハイライトのtype(HL.*)を設定する。
 * これは、rowの持つ全ての文字に対して設定を行うということである。
 */
/* コメントの中や文字列の中にいる場合のフラグを行を超えて保持する。 */
bool prev_sep,
     in_string,  /* "" か '' の中にいるかどうか。 */
     in_comment; /* 複数行のコメントの中にいるかどうか。 */

void editorUpdateSyntax(Erow* row) {
  row.hl = realloc(row.hl, row.rsize);
  memset(row.hl, HL.NORMAL, row.rsize);

  if (E.syntax is null) {
    return; /* シンタックスハイライトのルールが設定されていない。全てはHL.NORMALとなる。 */
  }

  ulong    i;
  char*    p;
  string[] keywords = E.syntax.keywords.dup;
  string   scs      = E.syntax.singleline_comment_start,
           mcs      = E.syntax.multiline_comment_start,
           mce      = E.syntax.multiline_comment_end;

  /* 最初のスペースではない文字へのポインタ。 */
  p = row.render;

  while (*p && isspace(*p)) {
    p++;
    i++;
  }

  prev_sep = true;  /* 'i'が単語の開始地点であることを知らせる。*/

  char string_char;   /* 文字列の中にいる場合、それが""か''かどうかを表す。*/

  /* もし前の行がコメントアウトの開始トークンを持っていた場合、コメントアウトを開始する。 */
  if (row.idx > 0 && editorRowHasOpenComment(&E.row[row.idx - 1])) {
    in_comment = true;
  }

  while (*p) {
    if (!in_string) {
      /* 単行のコメントを処理する。 */
      if (prev_sep && *p == scs[0] && *(p + 1) == scs[1]) {
        memset(row.hl+i, HL.COMMENT, row.rsize-i);
        return;
      }

      /* 複数行のコメントを処理する。 */
      if (in_comment) {
        row.hl[i] = HL.MLCOMMENT;

        if (*p == mce[0] && *(p + 1) == mce[1]) {
          row.hl[i + 1] = HL.MLCOMMENT;
          in_comment    = false;
          prev_sep      = true;
          p += 2;
          i += 2;
          continue;
        } else {
          prev_sep = false;
          p++;
          i++;
          continue;
        }
      } else if (*p == mcs[0] && *(p + 1) == mcs[1]) {
        row.hl[i]     = HL.MLCOMMENT;
        row.hl[i + 1] = HL.MLCOMMENT;
        in_comment    = true;
        prev_sep      = false;
        p += 2;
        i += 2;
        continue;
      }
    }

    if (!in_comment) {
      /* "" と '' を処理する。 */
      if (in_string) {
        row.hl[i] = HL.STRING;

        if (*p == '\\') {
          row.hl[i + 1] = HL.STRING;
          prev_sep      = false;
          p += 2;
          i += 2;
          continue;
        }

        if (*p == string_char) {
          in_string = false;
        }

        p++;
        i++;

        continue;
      } else {
        if (*p == '"' || *p == '\'') {
          string_char = *p;
          in_string   = true;
          row.hl[i]   = HL.STRING;
          prev_sep    = false;
          p++;
          i++;
          continue;
        }
      }
    }

    /* 表示不可文字を処理する。 */
    /* また、現在の実装では、マルチバイト文字は表示できないので今後それを修正する必要がある。 */
    if (!isprint(*p)) {
      row.hl[i] = HL.NONPRINT;
      p++;
      i++;
      prev_sep = false;
      continue;
    }

    /* 数字を処理する。 */
    if ((isdigit(*p) && (prev_sep || row.hl[i - 1] == HL.NUMBER)) ||
        (*p == '.' && i > 0 && row.hl[i - 1] == HL.NUMBER) || // floating point support
        (*p == 'x' && i > 0 && row.hl[i - 1] == HL.NUMBER) || // hex number support
        (*p == '-' && i < row.rsize && (((p+1) !is null) && isdigit(*(p+1)))) // negative number support
       ) {
      row.hl[i] = HL.NUMBER;
      p++;
      i++;
      prev_sep = false;
      continue;
    }

    /* キーワードを処理する。 */
    if (prev_sep) {
      ulong j;

      for (; j < keywords.length; j++) {
        ulong klen = keywords[j].length;
        bool  kw2  = keywords[j][klen - 1] == '|';

        if (kw2) {
          klen--;
        }

        if (!memcmp(p, keywords[j].ptr, klen) &&
            is_separator(*(p + klen))) {
          /* キーワード */
          memset(row.hl + i, kw2 ? HL.KEYWORD2 : HL.KEYWORD1, klen);
          p += klen;
          i += klen;
          break;
        }
      }

      if (j < keywords.length) {
        prev_sep = false;
        continue;
      }
    }

    /* 普通の文字。 */
    prev_sep = is_separator(*p);
    p++;
    i++;
  }

  /* コメントアウトが行われている場合、次の行にそれを伝える。
   * この処理は、後続のすべての行に影響をあたえるかもしれない。
   */
  bool oc = editorRowHasOpenComment(row);

  if (row.hl_oc != oc && (row.idx + 1) < E.numrows) {
    editorUpdateSyntax(&E.row[(row.idx + 1)]);
  }

  row.hl_oc = oc;
}

/* シンタックスハイライトの種類を受け取って対応するターミナルのカラーコードを返す。 */
int editorSyntaxToColor(HL hl) {
  switch (hl) with (HL) {
    case COMMENT:
    case MLCOMMENT: return 36; /* cyan */
    case KEYWORD1:  return 33; /* yellow */
    case KEYWORD2:  return 32; /* green */
    case STRING:    return 35; /* magenta */
    case NUMBER:    return 31; /* red */
    case MATCH:     return 34; /* blue */
    default:        return 37; /* white */
  }
}

/* ファイル名からシンタックスハイライトの種類を決定し、E.syntaxに設定する。 */
void editorSelectSyntaxHighlight(string filename) {
  for (size_t idx; idx < HLDB_ENTRIES; idx++) {
    EditorSyntax* hl = &HLDB[idx];

    foreach (patn; hl.filematch) {
      if ((patn.length == filename.length && patn == filename) ||
          (filename.extension == patn)) {
        E.syntax = hl;
        return;
      }
    }
  }
}

/* 与えられた文字がセパレーターかどうか。 */
bool is_separator(int c) {
  return c == '\0' || c == ' ' || "{},.()+-/*=~%[];".canFind(c);
}

/* 指定された行に複数行コメントの開始トークンが存在するかどうかを返す。
 * ただし、トークンが存在してもその行の中に終了トークンが存在する場合は偽を返す。 */
bool editorRowHasOpenComment(Erow* row) {
  if (E.syntax is null) {
    return false;
  }

  char mcs1 = E.syntax.multiline_comment_start[0],
       mcs2 = E.syntax.multiline_comment_start[1];

  if (row.hl && row.rsize && row.hl[row.rsize - 1] == HL.MLCOMMENT &&
      (row.rsize < 2 || (row.render[row.rsize - 2] != mcs2 ||
                         row.render[row.rsize - 1] != mcs1))) {
    return true;
  }

  return false;
}

シンタックスハイライトに対応させるために、editorUpdateRoweditorInsertRow, editorFreeRowに多少の追記を行う。

editorUpdateRowへの追記は単純です。
関数の末行に

  /* 行のシンタックスハイライトの属性を更新する。 */
  editorUpdateSyntax(row);

editorInsertRowへの追記も単純です。

  E.row[at].hl     = null;
  E.row[at].hl_oc  = false;


D
memcpy(E.row[at].chars, s.toStringz, length + 1);
E.row[at].hl = null;
E.row[at].hl_oc = false;
E.row[at].render = null;

のようにmemcpyE.row[at].render = null;の間に挿入するだけです。

また、editorFreeRowに対する挿入も単純で関数の末尾に
D
GC.free(row.hl);

を追記するだけです。

シンタックスハイライトにリアルタイムに反映させるために、editorRefreshScreenを大幅に拡張する。

さて、いよいよ大詰めです。

あまりにも変更点が多いので、ここではまるごとeditorRefreshScreen関数を示します。

void editorRefreshScreen() {
  ulong  y;
  Erow*  r;
  string str;

  str ~= "\x1b[?25l";  /* カーソルを隠す。 */
  str ~= "\x1b[H";     /* カーソルをホームポジションへ移動させる。 */

  for (; y < E.screenrows; y++) {
    ulong filerow = E.rowoff + y;

    if (filerow >= E.numrows) {
      if (E.numrows == 0 && y == E.screenrows / 3) {
        Appender!string welcome = appender!string;

        formattedWrite(welcome, "Dilo editor -- verison %s\x1b[0K\r\n", DILO_VERSION);

        ulong padding = (E.screencols - welcome.data.length) / 2;

        if (padding) {
          str ~= "~";
          padding--;
        }

        while (padding--) {
          str ~= " ";
        }

        str ~= welcome.data;
      } else {
        str ~= "~\x1b[0K\r\n";
      }
      continue;
    }

    r = &E.row[filerow];

    ulong len           = r.rsize - E.coloff;
    int   current_color = -1;

    if (len > 0) {
      if (len > E.screencols) {
        len = E.screencols;
      }

      char*  c  = r.render + E.coloff;
      ubyte* hl = r.hl + E.coloff;

      for (ulong j; j < len; j++) {
        if (hl[j] == HL.NONPRINT) {
          char sym;
          str ~= "\x1b[7m";

          if (c[j] <= 26) {
            sym = cast(char)('@'+c[j]);
          } else {
            sym = '?';
          }

          str ~= sym;
          str ~= "\x1b[0m";
        } else if (hl[j] == HL.NORMAL) {
          if (current_color != -1) {
            str ~= "\x1b[39m";
            current_color = -1;
          }

          str ~= c[j];
        } else {
          int color = editorSyntaxToColor(cast(HL)hl[j]);

          if (color != current_color) {
            auto buf = appender!string;
            formattedWrite(buf, "\x1b[%dm", color);
            current_color = color;
            str ~= buf.data;
          }

          str ~= c[j];
        }
      }
    }

    str ~= "\x1b[39m";
    str ~= "\x1b[0K";
    str ~= "\r\n";
  }

  /* ステータスラインを2行作る。一行目に関する実装: */
  str ~= "\x1b[0K";
  str ~= "\x1b[7m";

  auto status  = appender!string,
       rstatus = appender!string;

  string filetype = "plain text";

  if (E.syntax !is null) {
    filetype = E.syntax.syntaxName;
  }

  formattedWrite(status, "%.20s [filetype: %s]- %d lines %s", E.filename, filetype, E.numrows, E.dirty ? "(modified)" : "");
  formattedWrite(rstatus, "%d/%d", E.rowoff + E.cy + 1, E.numrows);

  ulong len  = status.data.length,
        rlen = rstatus.data.length;

  if (len > E.screencols) {
    len = E.screencols;
  }

  str ~= status.data;

  while(len < E.screencols) {
    if (E.screencols - len == rlen) {
      str ~= rstatus.data;
      break;
    } else {
      str ~= " ";
      len++;
    }
  }

  str ~= "\x1b[0m\r\n";

  /* 二行目はE.statusmsgとそのメッセージの更新時間に依存する。 */
  ulong msglen = E.statusmsg.data.length;
  if (msglen && time(null) - E.statusmsg_time < 5) {
    str ~= E.statusmsg.data;
  }

  /* カーソルを現在の座標に設定する。タブ文字のせいで表示されているカーソルの
   * 水平方向の座標がE.cxとずれているかもしれないことに注意しなければならない。
   */
  ulong j;
  ulong cx      = 1;
  ulong filerow = E.rowoff + E.cy;
  Erow* row = (filerow >= E.numrows) ? null : &E.row[filerow];

  if (row) {
    for (j = E.coloff; j < (E.cx+E.coloff); j++) {
      with (KEY_ACTION) if (j < row.size && row.chars[j] == TAB) {
        cx += 7 - ((cx) % 8);
      }
      cx++;
    }
  }

  auto buf = appender!string;

  formattedWrite(buf, "\x1b[%d;%dH", E.cy + 1, cx);
  str ~= buf.data;
  str ~= "\x1b[?25h"; /* カーソルを表示する。 */
  core.sys.posix.unistd.write(STDOUT_FILENO, str.toStringz, str.length);
}

はい!これでシンタックスハイライトも実装できました!!

さて、後もう一息です。
せっかくシンタックスハイライトを実装したので検索結果にもシンタックスハイライトを適用させましょう。
ちょっとだけ追記する箇所が多いので今回もeditorFindをまるごと貼ります。

void editorFind(int fd) {
  char[DILO_QUERY_LEN+1] query = 0;
  ulong qlen          = 0;
  long  last_match    = -1;  /* 見つかった場合の最後の行。見つからない場合、-1が設定される。 */
  long  find_next     = 0;   /* 1が設定されている場合、現在の行以降を、-1の場合、現在の行以前を検索する。*/
  long  saved_hl_line = -1;  
  char* saved_hl      = null;

  enum FIND_RESTORE_HL = q{
    do {
      if (saved_hl) { 
        memcpy(E.row[saved_hl_line].hl, saved_hl, E.row[saved_hl_line].rsize);        
        saved_hl = null; 
      } 
    } while (0);
  };

  /* 後からリストアするために現在のカーソルの座標を保存する。 */  
  ulong saved_cx     = E.cx,
        saved_cy     = E.cy;
  ulong saved_coloff = E.coloff,
        saved_rowoff = E.rowoff;

  with (KEY_ACTION) while (1) {
    editorSetStatusMessage("Search: %s (Use ESC/Arrows/Enter)", query);
    editorRefreshScreen();

    int c = editorReadKey(fd);

    if (c == DEL_KEY || c == CTRL_H || c == BACKSPACE) {
      if (qlen != 0) {
        query[--qlen] = '\0';
      }

      last_match = -1;
    } else if (c == ESC || c == ENTER) {
      if (c == ESC) {
        E.cx     = saved_cx; E.cy = saved_cy;
        E.coloff = saved_coloff; E.rowoff = saved_rowoff;
      }

      mixin(FIND_RESTORE_HL);
      editorSetStatusMessage("");

      return;
    } else if (c == ARROW_RIGHT || c == ARROW_DOWN) {
      find_next = 1;
    } else if (c == ARROW_LEFT || c == ARROW_UP) {
      find_next = -1;
    } else if (isprint(c)) {
      if (qlen < DILO_QUERY_LEN) {
        query[qlen++] = cast(char)c;
        query[qlen]   = '\0';
        last_match    = -1;
      }
    }

    if (last_match == -1) {
      find_next = 1;
    }

    if (find_next) {
      char* match;
      long  match_offset;
      long  current = last_match;

      for (ulong i; i < E.numrows; i++) {
        current += find_next;

        if (current == -1) {
          current = E.numrows - 1;
        } else if (current == E.numrows) {
          current = 0;
        }

        import core.stdc.string;
        match = strstr(E.row[current].render, query.ptr);

        if (match) {
          match_offset = match-E.row[current].render;
          break;
        }
      }

      find_next = 0;

      mixin(FIND_RESTORE_HL);

      if (match !is null) {
        Erow* row  = &E.row[current];
        last_match = current;

        if (row.hl) {
          saved_hl_line = current;
          saved_hl      = malloc!(char*)(row.rsize);
          memcpy(saved_hl, row.hl, row.rsize);
          memset(row.hl + match_offset, HL.MATCH, qlen);
        }

        E.cy     = 0;
        E.cx     = match_offset;
        E.rowoff = current;
        E.coloff = 0;


        if (E.cx > E.screencols) {
          ulong diff = E.cx - E.screencols;
          E.cx     -= diff;
          E.coloff += diff;
        }
      }
    }
  }
}

はい。これで検索時にマッチした文字に色が付きます。
現在の実装では、マッチした文字間の移動などはまだ実装していないので今度時間があったら実装しようと思います...

まとめ

とりあえずこのような1.4k行以下のコードでそれなりに遊べるテキストエディタがD言語でも書くことが出来ます。
もともと、私は普段余りソースコードにコメントは書かないのですが
記事を書くにあたってソースコードをババン!とはってこーすればできます!!!みたいな記事にはしたくなかったのでコメントを割りと書いたつもりです。
とはいえ、結局なんだかババン!と張っただけの下手くそな記事になってしまった気がします...
もともとの、kiloのソースコードを読んで僕はこれを書いたわけですが(大部分がほぼ1対1の移植ですが...)、もともとのコードはとても読みやすく
それをほぼ1対1に移植したdiloのコードも個人的には読みやすいと思いますしトリッキーな場所もないように思えるので特別に解説すべき場所というのが思い浮かびませんでした...
もしも不明な点とかが有りましたらコメントなどで指摘してくださればその都度解説します。

恐らく、この記事の紹介順にdilo.dを作っていった場合、あまりきれいなソースコードにはならないような気もします....
ですので、完全なソースコードはdiloのリポジトリにあるdilo.dを参照してください。
GitHub - alphaKAI/dilo

また、今後の展望として現在D言語で開発中のLisp処理系ChickenClispを組み込んでEmacsのようなものを作ってみたら面白いかも?などと思っています笑
今のところ、D言語で開発中のシェル(もどきですが)dshには実際にChickenClispを組み込み、シェルスクリプトや設定記述言語としてChickenClispを使えるようにしてみました。
このような感じで自分の、言わば生活環境を、自分で構築していくことはさながら無人島のサバイバル生活のような感じでとても楽しいですし、勉強になります。
みなさんもエディタ, 言語, シェルを自作してみてはいかがでしょうか!?

なお、多分(と言うか確実に)日時は過ぎてしまうと思いますが、12/4にも私は記事を書く予定ですが、そこでは言語を実装する話をしようと思っています。そう、ChickenClispの実装です!

とりあえず、今回はこのへんで。
お疲れ様でした。

23
15
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
23
15