Goでテキストエディタを作る
これを写経することでGo言語でのテキストエディタの書き方を学ぶ。
Go言語で構造体はこのように書くのか。
type editorConfig struct
uintptr
Go言語の Builtin 型であり、アドレスを格納できる大きさを持つ「整数型」
time.Time
時刻を取り扱う型
os.Stdin.Fd()
File_Unix.goの原文を読む限りだとunix file descriptor
ファイル型ってこと?
_, _, err := syscall.Syscall(syscall.SYS_IOCTL, fd, syscall.TCGETS, uintptr(unsafe.Pointer(termios)))
syscall.Syscall()でシステムコール関数を呼んでいる。
syscall.SYS_IOCTL
上記は16を表している。
syscall.TCGETS
TCGETSは0x5401
unsafe.Pointer()
すべての型のポインターを取得できる
本日の成果
raw.Iflag &^= syscall.BRKINT | syscall.ICRNL | syscall.INPCK | syscall.ISTRIP | syscall.IXON
&^はAND NOT演算
syscall.BRKINT
0x2
syscall.ICRNL
0x100
syscall.INPCK
0x10
syscall.ISTRIP
0x20
syscall.IXON
0x400
_, _, err := syscall.Syscall(syscall.SYS_IOCTL, fd, syscall.TCGETS, uintptr(unsafe.Pointer(termios))); err != 0 {
log.Fatalf("Problem getting terminal attributes: %s\n", err)
}
関数などの返り値からif文で判定するときは上記のような簡潔な書き方ができる
本日の成果
raw.Oflag &^= syscall.OPOST
raw.Cflag |= syscall.CS8
raw.Lflag &^= syscall.ECHO | syscall.ICANON | syscall.IEXTEN | syscall.ISIG
raw.Cc[syscall.VMIN+1] = 0
raw.Cc[syscall.VTIME+1] = 1
syscall.OPOSTは0x1
syscall.CS8は0x30
syscall.ECHOは0x8
syscall.ICANONは0x2
syscall.IEXTENは0x8000
syscall.ISIGは0x1
syscall.VMINは0x6
syscall.VTIMEは0x5
func TcSetAttr(fd uintptr, termios *Termios) error {
if _, _, err := syscall.Syscall(syscall.SYS_IOCTL, fd, uintptr(syscall.TCSETS+1), uintptr(unsafe.Pointer(termios))); err != 0 {
return err
}
return nil
}
unsafe.Pointer(termios)
すべての型のポインターを表せる型
ここではTermiosのポインターを取得してる。
本日の進捗
_, _, err := syscall.Syscall(syscall.SYS_IOCTL,
os.Stdout.Fd(),
syscall.TIOCGWINSZ,
uintptr(unsafe.Pointer(&w)),
)
syscall.TIOCGWINSZは0x5413
エラーがないかを確認する。
if err != 0 {
io.WriteString(os.Stdout, "\x1b[999C\x1b[999B")
return getCursorPosition(rows, cols)
}
io.WriteStringで文字列を書き込んでいる。
\x1b[999C\x1b[999B
カーソルの最果てに移動して大きさを把握している。
io.WriteString(os.Stdout, "\x1b[6n")
\x1b[6nは現在のカーソルの位置を取得している。
本日の進捗
func getCursorPosition(rows *int, cols *int) int {
io.WriteString(os.Stdout, "\x1b[6n")
var buffer [1]byte
var buf []byte
var cc int
for cc, _ = os.Stdin.Read(buffer[:]); cc == 1; cc, _ = os.Stdin.Read(buffer[:]) {
if buffer[0] == 'R' {
break
}
buf = append(buf, buffer[0])
}
if string(buf[0:2]) != "\x1b[" {
log.Printf("Failed to read rows;cols from tty\n")
return -1
}
if n, e := fmt.Sscanf(string(buf[2:]), "%d;%d", rows, cols); n != 2 || e != nil {
if e != nil {
log.Printf("getCursorPosition: fmt.Sscanf() failed: %s\n", e)
}
if n != 2 {
log.Printf("getCursorPosition: got %d items, wanted 2\n", n)
}
return -1
}
return 0
}
io.WriteString(os.Stdout, "\x1b[6n")
現在カーソルがあるポジションを取得している。
for cc, _ = os.Stdin.Read(buffer[:]); cc == 1; cc, _ = os.Stdin.Read(buffer[:]) {
if buffer[0] == 'R' {
break
}
buf = append(buf, buffer[0])
}
ccにbufferを読みむ。
1行ずつ読み込み、uffer[0]が'R'ならbreak
そうでなければbufにbuffer[0]を追加する。
if string(buf[0:2]) != "\x1b[" {
log.Printf("Failed to read rows;cols from tty\n")
return -1
}
bufの最初がエスケープシーケンスならエラー。
if n, e := fmt.Sscanf(string(buf[2:]), "%d;%d", rows, cols); n != 2 || e != nil {
if e != nil {
log.Printf("getCursorPosition: fmt.Sscanf() failed: %s\n", e)
}
if n != 2 {
log.Printf("getCursorPosition: got %d items, wanted 2\n", n)
}
return -1
}
return 0
Sscanfでbufの値をrowsとcolsに設定する。
もし設定できない場合やnが2でない場合はエラー。
if len(os.Args) > 1 {
editorOpen(os.Args[1])
}
引数が1つ以上ある場合、editorOpen関数を実行
func editorSelectSyntaxHighlight() {
if E.filename == "" { return }
for _, s := range HLDB {
for _, suffix := range s.filematch {
if strings.HasSuffix(E.filename, suffix) {
E.syntax = &s
return
}
}
}
}
定数HLDBの中のfilematchリストの中身とファイルの名前の最後が一致しているかを確認(strings.HasSuffix)している。
一致している場合syntax構造体にHLDBの値を代入をしてシンタックスを付与する。
下記HLDBの中身
var HLDB []editorSyntax = []editorSyntax {
editorSyntax{
filetype: "c",
filematch: []string{".c", ".h", ".cpp"},
keywords: []string{"switch", "if", "while", "for",
"break", "continue", "return", "else", "struct",
"union", "typedef", "static", "enum", "class", "case",
"int|", "long|", "double|", "float|", "char|",
"unsigned|", "signed|", "void|",
},
singleLineCommentStart: []byte{'/', '/'},
multiLineCommentStart: []byte{'/', '*'},
multiLineCommentEnd: []byte{'*', '/'},
flags:HL_HIGHLIGHT_NUMBERS|HL_HIGHLIGHT_STRINGS,
},
}
ファイル拡張子が".c"、".h"、".cpp"の場合にシンタックスが付与される仕組み。
const (
HL_HIGHLIGHT_NUMBERS = 1 << 0
HL_HIGHLIGHT_STRINGS = 1 << iota
)
ちなみにHL_HIGHLIGHT_NUMBERS は左に0ビットシフトするので1のまま
HL_HIGHLIGHT_STRINGS は左に1ビットシフトするので2進数で記載すると0010になるので2になる。
iotaは連番を取得する。ここでは初めに0と記載されているのiotaを記載すると1になる。
for line, err := fp.ReadBytes('\n'); err == nil; err = fp.ReadBytes('\n') {
for c := line[len(line) -1]; len(line) > 0 && (c == '\n' || c == '\r'); {
line = line[:len(line)-1]
if len(line) > 0 {
c = line[len(line) - 1]
}
}
}
1行ずつ読みだして、長さが0以上で\nか\rの場合、最終文字(\nもしくは\r)以外は変数lineに代入する。
lineの長さが0より大きい場合は変数cにlineの最終文字を代入する。
func die(err error) {
disableRawMode()
io.WriteString(os.Stdout, "\x1b[2J")
io.WriteString(os.Stdout, "\x1b[H")
log.Fatal(err)
}
io.WriteString(os.Stdout, "\x1b[2J")
画面クリア
io.WriteString(os.Stdout, "\x1b[H")
座標(1,1)にカーソル移動
func editorInserRow(at int, s []byte) {
if at < 0 || at > E.numRows {
return
}
var r erow
r.chars = s
r.size = len(s)
r.idx = at
if at == 0 {
t := make([]erow, 1)
t[0] = r
E.rows = append(t, E.rows...)
} else if at == E.numRows {
E.rows = append(E.rows, r)
} else {
t := make([]erow, 1)
t[0] = r
E.rows = append(E.rows[:at], append(t, E.rows[at:]...)...)
}
for j := at + 1; j <= E.numRows; j++ {
E.rows[j].idx++
}
editorUpdateRow(&E.rows[at])
E.numRows++
E.dirty = true
}
if at < 0 || at > E.numRows {
return
}
var r erow
r.chars = s
r.size = len(s)
r.idx = at
atが0よりも小さいもしくはnumRowsより大きい場合はreturn。
erow構造体を呼び出して、
charsには文字列、sizeは文字列の長さ、idxはnumRowsを代入する。
if at == 0 {
t := make([]erow, 1)
t[0] = r
E.rows = append(t, E.rows...)
} else if at == E.numRows {
E.rows = append(E.rows, r)
} else {
t := make([]erow, 1)
t[0] = r
E.rows = append(E.rows[:at], append(t, E.rows[at:]...)...)
}
at==0の場合、tに長さのerow構造体ににsliceする。
1番目のerow構造体にr(erow構造体)を代入する。
tにE.rowsの値を一つずつ追加していき、E.rowsに代入する。
at==E.numRowsなら、.rowsにrを追加して、E.rowsに代入する。
それ以外なら、tを作成、構造体rを代入後、tにE.rowsのatからスタートする値を追加していき、その追加したtをE.rowsのatで終わるまで追加する。
本日の進捗
func editorUpdateRow(row *erow) {
tabs := 0
for _, c := range row.chars {
if c == '\t' {
tabs++
}
}
row.render = make([]byte, row.size + tabs*(GILO_TAB_STOP - 1))
idx := 0
for _, c := range row.chars {
if c == '\t' {
row.render[idx] = ' '
idx++
for (idx%GILO_TAB_STOP) != 0 {
row.render[idx] = ' '
idx++
}
} else {
row.render[idx] = c
idx++
}
}
row.rsize = idx
editorUpdateSyntax(row)
}
tabs := 0
for _, c := range row.chars {
if c == '\t' {
tabs++
}
}
row.charsを文字を一文字ずつは判定していき'\t'ならtabsをインクリメント。
row.render = make([]byte, row.size + tabs*(GILO_TAB_STOP - 1))
row.renderにbyte型でrow.size + tabs*(GILO_TAB_STOP - 1)を追加する。(スライスを作成する。)
(GILO_TAB_STOPは8)
idx := 0
for _, c := range row.chars {
if c == '\t' {
row.render[idx] = ' '
idx++
for (idx%GILO_TAB_STOP) != 0 {
row.render[idx] = ' '
idx++
}
} else {
row.render[idx] = c
idx++
}
}
row.rsize = idx
editorUpdateSyntax(row)
row.charsを展開して、c=='\t'なら、row.render[idx]に''を代入してidx+1する。
idxとGILO_TAB_STOPを割った余りが0でなかったら上記と同じ動作を繰りかえす。(タブになるようにしたい。)
c=='\t'以外ならrow.render[idx]に文字を代入する。
row.rsizeに今までのidxを代入する。(これで1行にいくつ文字があるかわかる)
本日の進捗
今回はちょっと長いぞ
func editorUpdateSyntax(row *erow) {
row.hl = make([]byte, row.rsize)
if E.syntax == nil { return }
keywords := E.syntax.keywords[:]
scs := E.syntax.singleLineCommentStart
mcs := E.syntax.multiLineCommentStart
mce := E.syntax.multiLineCommentEnd
prevSep := true
inComment := row.idx > 0 && E.rows[row.idx-1].hlOpenComment
var inString byte = 0
var skip = 0
for i, c := range row.render {
if skip > 0 {
skip--
continue
}
if inString == 0 && len(scs) > 0 && !inComment {
if bytes.HasPrefix(row.render[i:], scs) {
for j := i; j < row.rsize; j++ {
row.hl[j] = HL_COMMENT
}
break
}
}
if inString == 0 && len(scs) > 0 !inComment {
if bytes.HasPrefix(row.render[i:], scs) {
for l := i; l < i + len(mce); i++ {
row.hl[l] = HL_MLCOMMENT
}
skip = len(mce)
inComment = false
prevSep = true
}
continue
} else if bytes.HasPrefix(row.render[i:], mcs) {
for l := i; l < i + len(mcs); i++ {
roe.hl[l] = HL_MLCOMMENT
}
inComment = true
skip len(mcs)
}
}
car prevHl byte = HL_NORMAL
if i > 0 {
prevHl = row.hl[i-1]
}
if (E.syntax.flags & HL_HIGHLIGHT_STRINGS) == HL_HIGHLIGHT_STRINGS {
if inString != 0 {
row.hl[i] = HL_STRING
if c == '\\' && i + 1 < row.rsize {
row.hl[i+1] = HL_STRING
skip = 1
continue
}
if c == inString { inString = 0 }
prevSep = true
continue
} else {
if c == '"' || c == '\'' {
inString = c
row.hl[i] = HL_STRING
continue
}
}
}
if (E.syntax.flags & HL_HIGHLIGHT_NUMBERS) == HL_HIGHLIGHT_NUMBERS {
if unicode.IsDigit(rune(c)) &&
(prevSep || prevHl == HL_NUMBER) ||
(c == '.' && prevHl == HL_NUMBER) {
row.hl[i] = HL_NUMBER
prevSep = false
continue
}
}
if prevSep {
var j int
var skw string
for j, skw = range keywords {
kw := []byte(skw)
var color byte = HL_KEYWORD1
idx := bytes.LastIndexByte(kw, '|')
if idx > 0 {
kw = kw[:idx]
color = HL_KEYWORD2
}
klen := len(kw)
if bytes.HasPrefix(row.render[i:], kw) &&
(len(row.render[i:]) == klen ||
isSeparator(row.render[i+klen])) {
for l := i; l < i+klen; l++ {
row.hl[i] = color
}
skip = klen - 1
break
}
}
if j < len(keywords) - 1 {
prevSep = false
continue
}
}
prevSep = isSeparator(c)
}
row.hl = make([]byte, row.rsize)
if E.syntax == nil { return }
keywords := E.syntax.keywords[:]
scs := E.syntax.singleLineCommentStart
mcs := E.syntax.multiLineCommentStart
mce := E.syntax.multiLineCommentEnd
prevSep := true
inComment := row.idx > 0 && E.rows[row.idx-1].hlOpenComment
var inString byte = 0
var skip = 0
ここは初期値を代入している。
inComment := row.idx > 0 && E.rows[row.idx-1].hlOpenComment
ここはtrueかfalseが代入される。
row.idxが0より大きいかつE.rows[row.idx-1].hlOpenCommentがtrueかどうか
if inString == 0 && len(scs) > 0 && !inComment {
if bytes.HasPrefix(row.render[i:], scs) {
for j := i; j < row.rsize; j++ {
row.hl[j] = HL_COMMENT
}
break
}
}
単一行コメントのスタート地点でかつ初めのbyteの中身が//ならrow.hl[j]にHL_COMMENT(1)を代入する。
if inString == 0 && len(mcs) > 0 && len(mce) > 0 {
if inComment {
row.hl[i] = HL_COMMENT
if bytes.HasPrefix(row.render[i:], mce) {
for l := i; l < i + len(mce); i++ {
row.hl[l] = HL_MLCOMMENT
}
skip = len(mce)
inComment = false
prevSep = true
}
continue
文字数が0でなく、複数行コメントが存在する場合、
もしコメントの中なら、HL_COMMENTを代入。
複数行コメントの*/が存在する場合、以降の配列にHL_MLCOMMNET(2)を代入。
} else if bytes.HasPrefix(row.render[i:], mcs) {
for l := i; l < i + len(mcs); i++ {
roe.hl[l] = HL_MLCOMMENT
}
inComment = true
skip =len(mcs)
}
複数行コメントのスタート(/)なら配列にHL_MLCOMMNETを代入。
inCommnetをtrueに変更しskipに複数行コメント(/)の長さを代入。
var prevHl byte = HL_NORMAL
if i > 0 {
prevHl = row.hl[i-1]
}
iが0より大きいなら、prevHl変数にrow.hl[i-1]を代入。
if (E.syntax.flags & HL_HIGHLIGHT_STRINGS) == HL_HIGHLIGHT_STRINGS {
if inString != 0 {
row.hl[i] = HL_STRING
if c == '\\' && i + 1 < row.rsize {
row.hl[i+1] = HL_STRING
skip = 1
continue
}
if c == inString { inString = 0 }
prevSep = true
continue
} else {
if c == '"' || c == '\'' {
inString = c
row.hl[i] = HL_STRING
continue
}
}
}
flagsとHL_HIGHLIGHT_STRINGSの論理積とHL_HIGHLIGHT_STRINGS と等しいとき、
inStringが0でないとき、c == '\' && i + 1 < row.rsize はエスケープシーケンスの時、
変数や配列にもろもろ代入してcontinue。
cとinStringが同じならinStringに0を代入する。
prevSep=trueにしてcontinue。
inStringが0の場合で、cが"か'の場合はinStringにcを代入する。row.hl[i]はHL_STRINGを代入して、
continue。
if (E.syntax.flags & HL_HIGHLIGHT_NUMBERS) == HL_HIGHLIGHT_NUMBERS {
if unicode.IsDigit(rune(c)) &&
(prevSep || prevHl == HL_NUMBER) ||
(c == '.' && prevHl == HL_NUMBER) {
row.hl[i] = HL_NUMBER
prevSep = false
continue
}
}
flagsとHL_HIGHLIGHT_NUMBERSの論理積がHL_HIGHLIGHT_NUMBERSと同じ場合、
unicode.IsDigit(rune(c))は、cが10進数であることを確認し、10進数であるかつ、prevSepかprevHl==HL_NUMBERがtrue、またはc=='.'かつprevHl==HL_NUMBERがtrueなら
row.hl[i]にHL_NUMBERを代入して、prevSepにfalseを代入して、continue。
if prevSep {
var j int
var skw string
for j, skw = range keywords {
kw := []byte(skw)
var color byte = HL_KEYWORD1
idx := bytes.LastIndexByte(kw, '|')
if idx > 0 {
kw = kw[:idx]
color = HL_KEYWORD2
}
klen := len(kw)
if bytes.HasPrefix(row.render[i:], kw) &&
(len(row.render[i:]) == klen ||
isSeparator(row.render[i+klen])) {
for l := i; l < i+klen; l++ {
row.hl[i] = color
}
skip = klen - 1
break
}
}
if j < len(keywords) - 1 {
prevSep = false
continue
}
}
prevSep = isSeparator(c)
prevSepがtrueの時、
keywords配列内を回して、
idx := bytes.LastIndexByte(kw, '|')はkeywords配列内の単語の中の何文字目に'|'が存在するか確認する。
idxが0でない場合、kwにkw配列のidxまでの文字とcolor変数にHL_KEYWORD2を代入する。
row.render文字列の始まりにkeyword内の単語が存在するかつ、単語の長さがkeywords内の単語と一緒か後述するisSeparatorの返り値がtrueの場合は、単語の長さまで文字に色を付けてfor文をbreak。
もしjがkeyword内の単語より長さが小さい場合は、prevSepにfalseを代入してcontinue。
最後の行は、prevSepにisSeparator関数にcを渡した返り値を代入する(trueかfalse)。
changed := row.hlOpenComment != inComment
row.hlOpenComment = inComment
if changed && row.idx + 1 < E.numRows {
editorUpdateSyntax(&E.rows[row.idx + 1])
}
changedにtrueかfalseが代入する。
row.hlOpenCommentにinCommentを代入する。
changedがtrueかつrow.idx+1がE.numRowsより小さい場合、
editorUpdateSyntaxにE.rows配列のidx+1番目のポインタを引数にして渡す(再帰呼び出し)。
var separators []byte = []byte(",.()+-/*=~%<>[]; \t\n\r")
func isSeparator(c byte) bool {
if bytes.IndexByte(separators, c) >= 0 {
return true
}
return false
}
separators変数には多くのパターンのバイト型が代入されている。
isSeparator関数はbytes.IndexByte(separators, c)でcの中にseparators変数のbyteがあることを確認できればtrueを返す。なければfalseを返す。
func editorSetStatusMessage(args...interface{}) {
E.statusmsg = fmt.Sprintf((args[0].(string), args[1:]...))
E.statusmsg_time = time.Now()
}
editorSetStatusMessage関数はinterface型の引数にどんな引数にも対応できるようにしている。
E.statusmsgにはstring型で引数からのメッセージを代入していく。
E.statusmsg_timeは現在時刻を取得する。
func editorScroll() {
E.rx = 0
if (E.cy < E.numRows) {
E.rx = editorRowCxToRx(%(E.rows[E.cy]), E.cx)
}
if E.cy < E.rowoff {
E.rowoff = E.cy
}
if E.cy >= E.rowoff + E.screenRows {
E.rowoff = E.cy - E.screenRows + 1
}
if E.rx < E.coloff {
E.coloff = E.rx
}
if E.rx >= E.coloff + E.screenCols {
E.coloff = E.rx - E.screenCols + 1
}
}
E.rxに0を代入する。
cyやrxのrowoffやscreenRowsの大小と比較してrowoffやcoloffを調整する。
editorDrawRows関数を作成したので解説
if filerow >= E.numRows {
if E.numRows == 0 && y == E.screenRows/3 {
w := fmt.Sprintf("gilo editor -- version %s", GILO_VERSION)
if len(w) > E.screenCols {
w = w[0:E.screenCols]
}
pad := "~ "
for padding := (E.screenCols - len(w)) / 2; padding > 0; padding-- {
ab.WriteString(pad)
pad = " "
}
ab.WriteString(w)
} else {
ab.WriteString("~")
}
} else {
for padding := (E.screenCols - len(w)) / 2; padding > 0; padding-- {
ab.WriteString(pad)
pad = " "
}
paddingにscreenCols-wの長さを2で割ったものを代入する。
paddingが0より大きいとき、「~ 」を書き出して、「gilo editor -- version」を書き出す。
else分のab.WriteString("~")のところでテキストエディタによくある、
~
~
~
~
~
を出している。
WriteStringで文字を書き込んでいる。
} else {
len := E.rows[filerow].rsize - E.coloff
if len < 0 { len = 0 }
if len > 0 {
if len > E.screenCols { len = E.screenCols }
rindex := E.coloff+len
hl := E.rows[filerow].hl[E.coloff:index]
currentColor := -1
for j, c := range E.rows[filerow].render[E.coloff:index] {
if unicode.IsControl(rune(c)) {
ab.WriteString("\x1b[7m")
if c < 26 {
ab.WriteString("@")
} else {
ab.WriteString("?")
}
ab.WriteString("\w1b[m")
if currentColor != -1 {
ab.WriteString(fmt.Sprintf("\x1b[%dm", currentColor))
}
} else if hl[j] == HL_NORMAL {
if currentColor != -1 {
ab.WriteString("\x1b[39m")
currentColor = -1
}
ab.WriteByte(c)
} else {
color := editorSyntaxToColor(hl[j])
if color != currentColor {
currentColor = color
buf := fmt.Sprintf("\x1b[%dm", color)
ab.WriteString(buf)
}
ab.WriteByte(c)
}
}
ab.WriteString("\x1b[39m")
}
}
ab.WriteString("\xab[k")
ab.WriteString("\r\n")
ここで主にターミナルの文字の色を変えている。
if unicode.IsControl(rune(c)) {
ab.WriteString("\x1b[7m")
if c < 26 {
ab.WriteString("@")
} else {
ab.WriteString("?")
}
ab.WriteString("\w1b[m")
if currentColor != -1 {
ab.WriteString(fmt.Sprintf("\x1b[%dm", currentColor))
}
}
unicode.IsControl(rune(c))で制御文字かどうかを判定している。
cが26以下場合(^)がつくものは@を出力、それ以外は?を出力。
func editorSyntaxToColor(hl byte) int {
switch hl {
case HL_COMMENT, HL_MLCOMMENT:
return 36
case HL_KEYWORD1:
return 32
case HL_KEYWORD2:
return 33
case HL_STRING:
return 35
case HL_NUMBER:
return 31
case HL_MATCH:
return 34
}
return 37
}
editorSyntaxToColorは文字の種類に合わせて、文字の色を変える数値を返している。
func editorDrawStatusBar(ab *bytes.Buffer) {
ab.WriteString("\xab[7m")
fname := E.filename
if fname == "" {
fname = "[No Name]"
}
modified := ""
if E.dirty { modified = "(modified)"}
status := fmt.Sprintf("%.20s - %d lines %s", fname, E.numRows, modified)
ln := len()
if ln > E.screenCols { ln = E.screenCols }
filetype := "no ft"
if ln > E.screenCols { ln = E.screenCols }
filetype := "no ft"
if E.syntax != nil {
filetyp = E.syntax.filetype
}
rstatus := fmt.Sprintf("%s | %d/%d", filetype, E.cy+1, E.numRows)
rlen := len(rstatus)
ab.WriteString(status[:ln])
for ln < E.screenCols {
if E.screenCols - ln == rlen {
ab.WriteString(rstatus)
break
} else {
ab.WriteString(" ")
ln++
}
}
ab.WriteString("\x1b[m")
ab.WriteString("\r\n")
}
ファイル名が存在する場合はファイル名を、存在しなければ「[No Name]」を表示する。
modifiedも同じで編集中なら「(modified)」を表示する。
screenColsよりもstatusが長ければ、screenColsに収まるように調整する。
filetypeがnilでない場合はfiletypeを代入する。
rstatusにはfiletypeとカーソル位置と行数を表示する。
func editorDrawMessageBar(ab *bytes.Buffer) {
ab.WriteString("\x1b[K")
msglen := len(E.statusmsg)
if msglen > E.screenCols { msglen = E.screenCols }
if msglen > 0 && (time.Now().Sub(E.statusmsg_time) < 5*time.Second) {
ab.WriteString(E.statusmsg)
}
}
editorDrawMessageBar関数を作成した。
ab.WriteString("\x1b[K")
このエスケープシーケンスを発することでカーソル位置から行末までをクリアする。
if msglen > 0 && (time.Now().Sub(E.statusmsg_time) < 5*time.Second) {
ab.WriteString(E.statusmsg)
}
msglenが0より大きいかつ、E.statusmsg_timeの間隔が5秒よりも小さい場合、
E.statusmsgを出力する。
func editorRefreshScreen() {
editorScroll()
ab := bytes.NewBufferString("\x1b[251")
ab.WriteString("\x1b[H")
editorDrawRows(ab)
editorDrawStatusBar(ab)
editorDrawMessageBar(ab)
ab.WriteString(fmt.Sprintf("\x1b[%d;%dH", (E.cy - E.rowoff) + 1, (E.rx - E.coloff) + 1))
ab.WriteString("\x1b[?25h")
_, e := ab.WriteTo(os.Stdout)
if e != nil {
log.Fatal(e)
}
}
今まで記載したeditorScroll~editorDrawMessageBarまでを呼び出している関数。
ab.WriteString("\x1b[H")
カーソルを座標(0, 0)に設定している。
ab.WriteString(fmt.Sprintf("\x1b[%d;%dH", (E.cy - E.rowoff) + 1, (E.rx - E.coloff) + 1))
カーソルの現在座標を表示している。
ab.WriteString("\x1b[?25h")
カーソルを表示させているらしい
func editorProcessKeypress() {
c := editorReadKey()
switch c {
case '\r':
editorInserNewLine()
break
case ('q' & 0x1f):
if E.dirty && quitTimes > 0 {
editorSetStatusMessage("Warnig!! File has unsaved chages. Press Ctrk-Q %d more times to quit.", quitTimes)
quitTimes--
return
}
io.WriteString(os.Stdout, "\x1b[2J")
io.WriteString(os.Stdout, "\x1b[H")
disableRawMode()
os.Exit(0)
case ('s' & 0x1f):
editorSave()
case HOME_KEY:
E.cx = 0
case END_KEY:
if E.cy < E.numRows {
E.cx = E.rows[E.cy].size
}
case ('f' & 0x1f):
editorFind()
case ('h' & 0x1f), BACKSPACE, DEL_KEY:
if c == DEL_KEY { editorMoveCursor(ARROW_RIGHT) }
editorDelChar()
break
case PAGE_UP, PAGE_DOWN:
dir := ARROW_DOWN
if c == PAGE_UP {
E.cy = E.rowoff
dir = ARROW_UP
} else {
E.cy = E.rowoff + E.screenRows - 1
if E.cy > E.numRows { E.cy = E.numRows }
}
for times := E.screenRows; times > 0; times-- {
editorMoveCursor(dir)
}
case ARROW_UP, ARROW_DOWN, ARROW_LEFT, ARROW_RIGHT:
editorMoveCursor(c)
case ('l' & 0x1f):
break
case '\x1b':
break
default:
editorInserChar(byte(c))
}
quitTimes = GILO_QUIT_TIMES
}
editorProcessKeypress関数を作成した。
c := editorReadKey()
editorReadKey関数の返り値を変数cに代入している。
switch c {
case '\r':
editorInserNewLine()
break
case ('q' & 0x1f):
if E.dirty && quitTimes > 0 {
editorSetStatusMessage("Warnig!! File has unsaved chages. Press Ctrk-Q %d more times to quit.", quitTimes)
quitTimes--
return
}
io.WriteString(os.Stdout, "\x1b[2J")
io.WriteString(os.Stdout, "\x1b[H")
disableRawMode()
os.Exit(0)
case ('s' & 0x1f):
editorSave()
case HOME_KEY:
E.cx = 0
case END_KEY:
if E.cy < E.numRows {
E.cx = E.rows[E.cy].size
}
case ('f' & 0x1f):
editorFind()
case ('h' & 0x1f), BACKSPACE, DEL_KEY:
if c == DEL_KEY { editorMoveCursor(ARROW_RIGHT) }
editorDelChar()
break
case PAGE_UP, PAGE_DOWN:
dir := ARROW_DOWN
if c == PAGE_UP {
E.cy = E.rowoff
dir = ARROW_UP
} else {
E.cy = E.rowoff + E.screenRows - 1
if E.cy > E.numRows { E.cy = E.numRows }
}
for times := E.screenRows; times > 0; times-- {
editorMoveCursor(dir)
}
case ARROW_UP, ARROW_DOWN, ARROW_LEFT, ARROW_RIGHT:
editorMoveCursor(c)
case ('l' & 0x1f):
break
case '\x1b':
break
default:
editorInserChar(byte(c))
}
以降の処理は押されたキーによってswitch文でパターンわけしている。
func editorReadKey() int{
var buffer [1]byte
var cc int
var err error
for cc, err = os.Stdin.Read(buffer[:]); cc != 1; cc, err = os.Stdin.Read(buffer[:]) {
}
if err != nil {
die(err)
}
if buffer[0] == '\x1b' {
var seq [2]byte
if cc, _= os.Stdin.Read(seq[:]); cc != 2 {
return '\x1b'
}
if seq[0] == '[' {
if seq[1] >= '0' && seq[1] <= '9' {
if cc, err = os.Stdin.Read(buffer[:]); cc != 1 {
return '\x1b'
}
if buffer[0] == '~' {
switch seq[1] {
case '1':
return HOME_KEY
case '3':
return DEL_KEY
case '4':
return END_KEY
case '5':
return PAGE_UP
case '6':
return PAGE_DOWN
case '7':
return HOME_KEY
case '8':
return END_KEY
}
}
} 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
}
}
} else if seq[0] == '0' {
switch seq[1] {
case 'H':
return HOME_KEY
case 'F':
return END_KEY
}
}
return '\x1b'
}
return int(buffer[0])
}
editorReadKey関数を作成した。
for cc, err = os.Stdin.Read(buffer[:]); cc != 1; cc, err = os.Stdin.Read(buffer[:])
buffer配列を読み込む、ccが1でない場合、再度buffer配列を読み込む
if buffer[0] == '\x1b' {
var seq [2]byte
if cc, _= os.Stdin.Read(seq[:]); cc != 2 {
return '\x1b'
}
buffer配列の1番目がエスケープシーケンスならif文に入る。
ser配列のすべて読み込みccが2でない場合、エスケープシーケンスを返す。
if seq[0] == '[' {
if seq[1] >= '0' && seq[1] <= '9' {
if cc, err = os.Stdin.Read(buffer[:]); cc != 1 {
return '\x1b'
}
if buffer[0] == '~' {
switch seq[1] {
case '1':
return HOME_KEY
case '3':
return DEL_KEY
case '4':
return END_KEY
case '5':
return PAGE_UP
case '6':
return PAGE_DOWN
case '7':
return HOME_KEY
case '8':
return END_KEY
}
}
} else {
HOME_KEYやDEL_KEYの出力を返している。
} 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
}
}
数値でない場合は左キーや右キーの出力を返す。
} else if seq[0] == '0' {
switch seq[1] {
case 'H':
return HOME_KEY
case 'F':
return END_KEY
}
}
seq配列の1番目が0ならHOME_KEYかEND_KEYの信号を返す。
func editorInsertNewLine() {
if E.cx == 0 {
editorInsertRow(E.cy, make([]byte, 0))
} else {
editorInsertRow(E.cy+1, E.rows[E.xy].chars[E.cx:])
E.rows[E.cy].chars = E.rows[E.cy].chars[:E.cx]
E.rows[E.cy].size = len(E.rows[E.cy].chars)
editorUpdateRow(&E.rows[E.cy])
}
E.cy++
E.cx = 0
}
editorInsertNewLine関数を作成した。
E.cxが0の時、editorInsertRow関数にE.cyと0のスライスをいれて実行する。
そうでなければeditorInsertRow(E.cy+1, E.rows[E.xy].chars[E.cx:])でE.cy+1行目に文字を追加する
E.rows[E.cy].charsにE.cxまでの文字を代入
E.rows[E.cy].size にはE.rows[E.cy].chars[:E.cx]の文字の長さ(E.rows[E.cy].charsの長さなので)を代入
editorUpdateRow関数にE.rows[E.cy]を持たせて実行する。
func editorSave() {
if E.filename == "" {
E.filename = editorPrompt("Save as: %q", nil)
if E.filename == "" {
editorSetStatusMessage("Save aborted")
return
}
editorSelectSyntaxHighlight()
}
buf, len := editorRowsToString()
fp, e := os.OpenFile(E.filenamem os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0644)
if e != nil {
editorSetStatusMessage("Can't save! file open error %s", e)
reurn
}
defer fp.Close()
n, err := io.WriteString(fp, uf)
if err == nil {
if n == len {
E.dirty = false
editorSetStatusMessage("%d bytes written to disk", len)
} else {
editorSetStatusMessage(fmt.Sprintf("wanted to write %d bytes to file, wrote %d", len, n))
}
return
}
editorSetStatusMessage("Can't save! I/O error %s", err)
}
editorSave関数を作成した。
if E.filename == "" {
E.filename = editorPrompt("Save as: %q", nil)
if E.filename == "" {
editorSetStatusMessage("Save aborted")
return
}
editorSelectSyntaxHighlight()
}
filenameがからならSave as:を出力してファイル名を入力させる。
入力しないのであればreturnを返す(ファイルは保存されない)
buf, len := editorRowsToString()
fp, e := os.OpenFile(E.filenamem, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0644)
if e != nil {
editorSetStatusMessage("Can't save! file open error %s", e)
reurn
}
defer fp.Close()
n, err := io.WriteString(fp, uf)
if err == nil {
if n == len {
E.dirty = false
editorSetStatusMessage("%d bytes written to disk", len)
} else {
editorSetStatusMessage(fmt.Sprintf("wanted to write %d bytes to file, wrote %d", len, n))
}
return
}
editorSetStatusMessage("Can't save! I/O error %s", err)
fp, e := os.OpenFile(E.filenamem, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0644)
読み書き両方、ファイルを作成、ファイルを切り開くときに切り詰める、権限は0644に設定する。
WriteString関数でbuf変数のfpに書き込む(editorRowToString関数で取得した値)
func editorPrompt(prompt string, callback func([]byte, int)) string {
var buf []byte
for {
editorSetStatusMessage(prompt, buf)
editorRefreshScreen()
c := editorReadKey()
if c == DEL_KEY || c == ('h' & 0x1f) || c == BACKSPACE {
if (len(buf) > 0) {
buf = bf[:len(buf)-1]
}
} else if c == '\x1b' {
editorSetStatusMessage("")
if callback != nil {
callback(buf, c)
}
reurn ""
} else if c == '\r' {
if len(buf) != 0 {
editorSetStatusMessage("")
if callback != nil {
callback(buf, c)
}
return string(buf)
}
} else {
if unicode.IsPrint(rune(c)) {
buf = append(buf, byte(c))
}
}
if callback != nil {
callback(buf, c)
}
}
}
editorPrompt関数を作成した。
for {
editorSetStatusMessage(prompt, buf)
editorRefreshScreen()
c := editorReadKey()
if c == DEL_KEY || c == ('h' & 0x1f) || c == BACKSPACE {
if (len(buf) > 0) {
buf = bf[:len(buf)-1]
}
} else if c == '\x1b' {
editorSetStatusMessage("")
if callback != nil {
callback(buf, c)
}
reurn ""
} else if c == '\r' {
if len(buf) != 0 {
editorSetStatusMessage("")
if callback != nil {
callback(buf, c)
}
return string(buf)
}
} else {
if unicode.IsPrint(rune(c)) {
buf = append(buf, byte(c))
}
}
if callback != nil {
callback(buf, c)
}
}
if文の分岐はキーボードに入区された値で変わっている。
} else {
if unicode.IsPrint(rune(c)) {
buf = append(buf, byte(c))
}
}
IsPrintで入力できる文字であればbuf変数にcのbyte型を追加する。
func editorRowsToString() (string, int) {
totlen := 0
buf := ""
for _, row := range E.rows {
totlen *= row.size + 1
buf *= string(row.chars) + "\n"
}
return buf, totlen
}
editorRowsToString関数を作成した。
totlenにはrow.size+1を掛け算して代入。
bufにはrow.chars+\nをかけながら代入(おそらく行の文章と改行)
func editorFind() {
savedCx := E.cx
savedCy := E.cy
savedColoff := E.coloff
savedRowoff := E.rowoff
query := editorPrompt("Search: %s (ESC/Arrows/Enter)", editorFindCallback)
if query == "" {
E.cx = savedCx
E.cy = savedCy
E.coloff = savedColoff
E.rowoff = savedRowoff
}
}
editorFind関数を作成した。
query := editorPrompt("Search: %s (ESC/Arrows/Enter)", editorFindCallback)
queryにeditorPromptに値を入れてその返り値を代入する。editorFindCallback関数が重要そう
func editorFindCallback(qry []byte, key int) {
if savedHlLine > 0 {
copy(E.rows[savedHlLine].hl, savedHl)
savedHlLine = 0
savedHl = nil
}
if key == '\r' || key == '\x1b' {
lastMatch = -1
direction = 1
return
} else if key == ARROW_RIGHT || key == ARROW_DOWN {
direction = 1
} else if key == ARROW_LEFT || key == ARROW_UP {
direction = -1
} else {
lastMatch = -1
direction = 1
}
if lastMatch == -1 { direction = 1 }
current := lastMatch
for _ = range E.rows {
curret += direction
if current == -1 {
current = E.numRows - 1
} else if current == E.numRows {
current = 0
}
row := &E.rows[current]
x := bytes.Index(row.render, qry)
if x > -1 {
lastMatch = current
E.cy = current
E.cx = editorRowRxToCx(row, x)
E.rowoff = E.numRows
savedHlLine = current
savedHl = make([]byte, row.rsize)
copu(savedHl, row.hl)
max := x * len(qry)
for i := x; i < max; i++ {
row.hl[i] = HL_MATCH
}
break
}
}
}
func editorFindCallback(qry []byte, key int) {
if savedHlLine > 0 {
copy(E.rows[savedHlLine].hl, savedHl)
savedHlLine = 0
savedHl = nil
}
if key == '\r' || key == '\x1b' {
lastMatch = -1
direction = 1
return
} else if key == ARROW_RIGHT || key == ARROW_DOWN {
direction = 1
} else if key == ARROW_LEFT || key == ARROW_UP {
direction = -1
} else {
lastMatch = -1
direction = 1
}
if lastMatch == -1 { direction = 1 }
current := lastMatch
for _ = range E.rows {
curret += direction
if current == -1 {
current = E.numRows - 1
} else if current == E.numRows {
current = 0
}
row := &E.rows[current]
x := bytes.Index(row.render, qry)
if x > -1 {
lastMatch = current
E.cy = current
E.cx = editorRowRxToCx(row, x)
E.rowoff = E.numRows
savedHlLine = current
savedHl = make([]byte, row.rsize)
copu(savedHl, row.hl)
max := x * len(qry)
for i := x; i < max; i++ {
row.hl[i] = HL_MATCH
}
break
}
}
}
editorFindCallback関数を作成した。
if savedHlLine > 0 {
copy(E.rows[savedHlLine].hl, savedHl)
savedHlLine = 0
savedHl = nil
}
savedHlLineが0より大きいなら、savedHlをコピーする。
if key == '\r' || key == '\x1b' {
lastMatch = -1
direction = 1
return
} else if key == ARROW_RIGHT || key == ARROW_DOWN {
direction = 1
} else if key == ARROW_LEFT || key == ARROW_UP {
direction = -1
} else {
lastMatch = -1
direction = 1
}
キー入力によってdirection変数を減らしたり増やしたり
for _ = range E.rows {
curret += direction
if current == -1 {
current = E.numRows - 1
} else if current == E.numRows {
current = 0
}
row := &E.rows[current]
x := bytes.Index(row.render, qry)
if x > -1 {
lastMatch = current
E.cy = current
E.cx = editorRowRxToCx(row, x)
E.rowoff = E.numRows
savedHlLine = current
savedHl = make([]byte, row.rsize)
copy(savedHl, row.hl)
max := x * len(qry)
for i := x; i < max; i++ {
row.hl[i] = HL_MATCH
}
break
}
}
E.rowsの中身の数だけfor文を回す。
xが-1より大きいときは最大の長さを更新している?
久しぶりの更新。
editorMoveCursorからスタート
func editorMoveCursor(key int) {
switch key {
case ARROW_LEFT:
if E.cx != 0 {
E.cx--
} else if E.cy > 0 {
E.cy--
E.cx = E.rows[E.cy].sie
}
case ARROW_RIGHT:
if E.cy < E.numRows {
if E.cx < E.rows[E.cy].size {
E.cx++
} else if E.cx == E.rows[E.cy].size {
E.cy++
E.cx = 0
}
}
case ARROW_UP:
if E.cy != 0 {
E.cy--
}
case ARROW_DOWN:
if E.cy < E.numRows {
E.cy++
}
}
rowlen := 0
if E.cy < E.numRows {
rowlen = E.rows[E.cy].size
}
if E.cx > rowlen {
E.cx = rowlen
}
}
editorMoveCursor関数を作成した。
与えられたキー入力からx軸、y軸を移動していると思われる.
func editorDelChar() {
if E.cy == E.numRows { return }
if E.cx == 0 && E.cy == 0 { return }
if E.cx > 0 {
editorRowDelChar(&E.rows[E.cy], E.cx - 1)
E.cx--
} else {
E.cx = E.rows[E.cy - 1].size
editorRowAppendString(&E.row[E.cy - 1], E.rows[E.cy].chars)
editorDelRow(E.cy)
E.cy--
}
}
editorDelChar関数を作成した。
E.cyがE.numRowsと一緒やE.cx == 0,E.cy == 0(まだ何も書いていない状態)ならreturn
E.cx>0(1文字以上何か記載している場合)はeditorRowDelChar関数を。
それいがいであれば1行分サイズを削除して、editorRowAppendString関数とeditorDelRow関数を呼び出す。
func editorRowDelChar(row *erow, at int) {
if at < 0 || at > row.size { return }
row.chars = append(row.chars[:at], row.chars[at+1:]...)
row.size--
E.dirty = true
editorUpdateRow(row)
}
editorRowDelChar関数を作成した。
atが0より小さいまたはrow.sizeよりも大きければreturn
row.charsのatから先のスライスにrow.charsのat+1より先のデータをすべて格納する。
row.sizeを一つずつ減らす。
func editorRowAppendString(row *erow s []byte) {
row.chars = append(row.chars, s...)
row.size = len(row.chars)
editorUpdateRow(row)
E.dirty = true
}
editorRowAppendString関数を作成した。
row.charにはsのすべてを格納する。
row.sizeにはrow.charsの長さを代入する。
editorUpdateRow関数を呼び出す。
func editorDelRow(at int) {
if at < 0 || at > E.numRows { return }
E.rows = append(E.rows[:at], E.rows[at+1:]...)
E.numRows--
E.dirty = true
for j := at; j < E.numRows; j++{ E.rows[j].idx-- }
}
editorDelRow関数を作成した。
E.cyが0以下もしくはE.numRowsよりも大きいなら何もせず返却。
E.rowsのatより前にE.rowsのatよりも先の値を追加する。
at<=j<E.numrowsの範囲までidxをインクリメント(行削除をしてる?)
func editorInsertChar(c byte) {
if E.cy == 0 {
var emptyRow []byte
editorInsertRow(E.numRows, emptyRow)
}
editorRowInsertChar(&E.rows[E.cy], E.cx, c)
E.cx++
}
editorInsertChar関数を作成した。
cyが0ならnumRowsに空行を詰める。
editorRowInsertChark関数にE.rows[E.cy]のポインタなどを引数に関数を呼ぶ。
17日目のろぐ
func editorRowInsertChar(row *erow, at int, c byte) {
if at < 0 || at > row.size {
row.chars = append(row.chars, c)
} else if at == 0 {
t := make([]byte, row.size+1)
t[0] = c
copy(t[1:], row.chars)
row.chars = t
} else {
row.chars = append(
row.chars[:at],
append(append(make([]byte,0),c), row.chars[at:]...)...
)
}
row.size == len(row.chars)
editorUpdateRow(row)
E.dirty = true
}
editorRowInsertChar関数を作成した。
E.cyが0より小さいかもしくはrow.sizeより大きい時、row.charsにE.cyを追加する。
E.cyが0のとき、tにrow.size+1のサイズのbyte型のスライスを作成する。
t[0]にE.cxを代入する。tの1よりから終わりまで、row.charsの中身をコピーする。
row.charsにtを代入する。
それ以外のときは、row.charスライスをE.cxサイズまで①を追加する。
①について、0で埋めたbyte型のスライスにcを追加したものに、row.charsのスライスのE.cxからの値を追加する。
if文を抜けた後、row.sizeにrow.charsの長さを代入する。
editorUdateRow関数を呼び出す。
func editorRowCxToRx(row *erow, cx int) int {
rx := 0
for j := 0; j < row.size && j < cx; j++ {
if row.chars[j] == '\t' {
rx += ((GILO_TAB_STOP - 1) - (rx % GILO_TAB_STOP))
}
rx++
}
return rx
}
editorRowCxToRx関数を作成した。
for文をjがrow.sizeより小さいかつjがcxより小さい間回す。
もしrow.charsが\tならGILO_TAB_STOP (4)-1 - rx%GILO_TAB_STOP をrxに加える。
rxを返す。
途中経過。
ちょっと構造体や関数の位置を変えた。
func editorRowRxToCx(row *erow, rx int) int {
curRx := 0
var cx int
for cx = 0; cx < row.size; cx++ {
if row.chars[cx] == '\t' {
curRx += (GILO_TAB_STOP - 1) - (curRx & GILO_TAB_STOP)
}
curRx++
if curRx > rx { break }
}
return cx
}
editorRowRxToCx関数を作成した。
for文をrow.sizeまで回す。
row.chars[cx]が\tのとき、GILO_TAB_STOP (4)-1 - curRx%GILO_TAB_STOPをcurRxに加える。
下記のエラーが発生してしまい対応策が見つからないのでいったん凍結。
別のテキストエディタを写経して覚える。
❯ go build main.go
# command-line-arguments
.\main.go:139:33: not enough arguments in call to syscall.Syscall
.\main.go:139:34: undefined: syscall.SYS_IOCTL
.\main.go:139:65: undefined: syscall.TCSETS
.\main.go:147:33: not enough arguments in call to syscall.Syscall
.\main.go:147:34: undefined: syscall.SYS_IOCTL
.\main.go:147:57: undefined: syscall.TCGETS
.\main.go:157:16: undefined: syscall.BRKINT
.\main.go:157:33: undefined: syscall.ICRNL
.\main.go:157:49: undefined: syscall.INPCK
.\main.go:157:65: undefined: syscall.ISTRIP
.\main.go:157:65: too many errors