You can access the code of this chapter in the Kilo-Go github repository in the texteditor branch.
Currently your file structure should look something like this:
Insert ordinary characters
First we want to be able to insert new characters, and if needed to be able to create a new line
File: editor/input.go
func (e *EditorConfig) editorProcessKeypress() {
...
switch b {
...
default:
e.editorInsertChar(byte(b))
}
}
File: editor/row.go
func (e *EditorConfig) editorAppendRow(s string) {
row := EditorRow{
chars: s,
render: make([]byte, len(s)),
}
e.cx = 0
e.editorUpdateRow(&row)
e.rows = append(e.rows, row)
e.numrows++
}
func (e *EditorConfig) editorRowInsertChar(row *EditorRow, at int, c byte) {
row.render = make([]byte, len(row.chars)+1)
row.chars = row.chars[:at] + string(c) + row.chars[at:]
e.editorUpdateRow(row)
}
File: editor/operations.go
package editor
func (e *EditorConfig) editorInsertChar(c byte) {
if e.cy >= len(e.rows) {
e.editorAppendRow("")
}
e.editorRowInsertChar(&e.rows[e.cy], e.cx, c)
e.cx++
}
Prevent inserting special characters
Now that we can insert characters, if we try to insert Backspace or Enter the editor does not understand them and we want to disable them for now
-
Backspacedoesn't have a backslash-escape representation like (\r,\n, etc) but we know it is represented by the byte127 -
Enterkey is represented by the\rescape sequence - For the time being we will not be doing anything when the user press the
Esckey, but we just want to disable it so no side effects occur if the user press it. Also by the way we've wrote theeditorReadKeymethod, we will disable any function keys being pressed
File: utils/constants.go
const (
ESC = 0x1b
ENTER = '\r'
...
)
const (
BACKSPACE = 127
ARROW_LEFT = iota + 1000
...
)
File: utils/input.go
func (e *EditorConfig) editorProcessKeypress() {
b, err := e.editorReadKey()
if err != nil {
utils.SafeExit(e.restoreFunc, err)
}
switch b {
case utils.ENTER:
slog.Info("ENTER")
...
case utils.DEL_KEY, utils.BACKSPACE:
slog.Info("DEL_KEY")
...
case utils.ESC:
// for now we will ignore when the user press the escape key
break
...
}
Save to disk
Now that we can make changes to our files, it is time to be able to save them to disk
File: editor/editor.go
func (e *EditorConfig) EditorLoop() {
...
e.editorSetStatusMessage("HELP: Ctrl-S = save | Ctrl-Q = quit")
...
}
File: editor/input.go
func (e *EditorConfig) editorProcessKeypress() {
...
switch b {
...
case utils.CtrlKey('s'):
e.editorSave()
...
}
}
File: editor/file.go
func (e *EditorConfig) editorSave() {
data := make([]byte, 0)
for _, row := range e.rows {
data = append(data, row.chars...)
data = append(data, '\r', '\n')
}
err := os.WriteFile(e.filename, data, 0644)
if err != nil {
slog.Error("editorSave, error saving file", "error", err)
e.editorSetStatusMessage(fmt.Sprintf("Error saving file: %v", err))
return
}
e.editorSetStatusMessage("File saved")
}
Note: if an error occurs while saving the file, we will not exit the application, we will show the error to the user and log it
Dirty flag
We want to keep track if the file we are showing the user differs from the one saved, so we can alert them if for example they want to quit without saving the file
File: editor/editor.go
type EditorConfig struct {
...
isDirty bool
}
func NewEditor(f func()) *EditorConfig {
...
return &EditorConfig{
...
isDirty: false,
}
}
File: editor/output.go
func (e *EditorConfig) editorDrawStatusBar(abuf *ab.AppendBuffer) {
status := e.filename
if status == "" {
status = "[No Name]"
}
if e.isDirty {
status += " (modified)"
}
...
}
File: editor/row.go
func (e *EditorConfig) editorAppendRow(s string) {
...
e.isDirty = true
}
func (e *EditorConfig) editorRowInsertChar(row *EditorRow, at int, c byte) {
...
e.isDirty = true
}
File: editor/file.go
func (e *EditorConfig) editorOpen(filename string) {
...
e.isDirty = false
}
func (e *EditorConfig) editorSave() {
...
e.isDirty = false
}
Quit confirmation
Now that we control the isDirty flag, we will want to let the user know that they are trying to quit without saving the file
File: utils/constants.go
const (
...
KILO_QUIT_TIMES = 3
)
File: editor/input.go
var quit_times = utils.KILO_QUIT_TIMES
func (e *EditorConfig) editorProcessKeypress() {
...
switch b {
...
case utils.CtrlKey('q'):
if e.isDirty && quit_times > 0 {
e.editorSetStatusMessage(fmt.Sprintf("WARNING! File has unsaved changes. Ctrl-Q %d more times to quit", quit_times))
quit_times--
return
}
utils.SafeExit(e.restoreFunc, nil)
...
}
quit_times = utils.KILO_QUIT_TIMES
}
Simple backspacing
Lets implement the Backspace and Delete keys
File: editor/input.go
func (e *EditorConfig) editorProcessKeypress() {
...
switch b {
...
case utils.DEL_KEY, utils.BACKSPACE:
if b == utils.DEL_KEY {
e.editorMoveCursor(utils.ARROW_RIGHT)
}
e.editorDeleteChar()
...
}
File: editor/operations.go
func (e *EditorConfig) editorDeleteChar() {
if e.cy >= len(e.rows) {
return
}
e.editorRowDeleteChar(&e.rows[e.cy], e.cx)
if e.cx > 0 {
e.cx--
}
}
File: editor/row.go
func (e *EditorConfig) editorRowDeleteChar(row *EditorRow, at int) {
if at < 0 || at >= len(row.chars) {
return
}
row.render = make([]byte, len(row.chars)-1)
row.chars = row.chars[:at] + row.chars[at+1:]
e.editorUpdateRow(row)
e.isDirty = true
}
Backspacing at the start of a line
Currently, editorDelChar doesn't do anything when the cursor is at the beginning of a line. When the user backspaces at the beginning of a line, we want to append the contents of that line to the previous line, and then delete the current line
File: editor/row.go
func (e *EditorConfig) editorDelRow(at int) {
if at < 0 || at >= len(e.rows) {
return
}
e.rows = append(e.rows[:at], e.rows[at+1:]...)
e.numrows--
e.isDirty = true
}
func (e *EditorConfig) editorRowAppendString(row *EditorRow, s string) {
row.chars += s
row.render = make([]byte, len(row.chars))
e.editorUpdateRow(row)
e.isDirty = true
}
File: editor/operations.go
func (e *EditorConfig) editorDeleteChar() {
if e.cy >= len(e.rows) {
return
}
if e.cx == 0 && e.cy == 0 {
return
}
if e.cx > 0 {
e.editorRowDeleteChar(&e.rows[e.cy], e.cx)
e.cx--
} else {
e.cx = len(e.rows[e.cy-1].chars)
e.editorRowAppendString(&e.rows[e.cy-1], e.rows[e.cy].chars)
e.editorDelRow(e.cy)
e.cy--
}
}
The Enter key
The last keyboard input operation we will be going through is the implementation of the Enter key, which allows the user to insert new lines into the text, or split a line into two lines.
File: editor/row.go
func (e *EditorConfig) editorInsertRow(at int, s string) {
e.rows = append(e.rows[:at], append([]EditorRow{{chars: s}}, e.rows[at:]...)...)
e.numrows++
e.isDirty = true
}
File: editor/operations.go
func (e *EditorConfig) editorInsertChar(c byte) {
if e.cy == len(e.rows) {
e.editorInsertRow(e.numrows, "")
}
...
}
func (e *EditorConfig) editorInsertNewline() {
if e.cy == len(e.rows) {
e.editorInsertRow(e.numrows, "")
} else {
row := &e.rows[e.cy]
e.editorInsertRow(e.cy+1, row.chars[e.cx:])
row.chars = row.chars[:e.cx]
row.render = make([]byte, len(row.chars))
e.editorUpdateRow(row)
row = &e.rows[e.cy+1]
row.render = make([]byte, len(row.chars))
e.editorUpdateRow(row)
}
e.cy++
e.cx = 0
}
File: editor/input.go
func (e *EditorConfig) editorProcessKeypress() {
...
switch b {
case utils.ENTER:
e.editorInsertNewline()
...
}
...
}
Prompting the user
We want know to have the ability to prompt the user for input, so when they want to create a new file we can ask them for its name
File: utils/constants.go
const (
...
KILO_DEFAULT_STATUS_MESSAGE = "HELP: Ctrl-S = save | Ctrl-Q = quit"
)
File: utils/ctrl.go
func IsCtrlKey(key int) bool {
return key <= 0x1f || key == BACKSPACE
}
File: editor/editor.go
func (e *EditorConfig) EditorLoop() {
...
e.editorSetStatusMessage(utils.KILO_DEFAULT_STATUS_MESSAGE)
...
}
File: editor/input.go
func (e *EditorConfig) editorProcessKeypress() {
...
switch b {
...
case utils.CtrlKey('c'):
term := e.editorPrompt(": ")
slog.Info("editorProcessKeypress, search term", "term", term)
...
}
...
}
func (e *EditorConfig) editorPrompt(prompt string) string {
var buf string
for {
e.editorSetStatusMessage(prompt + buf)
e.editorRefreshScreen()
b, err := e.editorReadKey()
if err != nil {
utils.SafeExit(e.restoreFunc, err)
}
switch b {
case utils.BACKSPACE, utils.DEL_KEY:
if len(buf) > 0 {
buf = buf[:len(buf)-1]
}
case utils.ESC:
slog.Info("editorPrompt, ESC")
e.editorSetStatusMessage(utils.KILO_DEFAULT_STATUS_MESSAGE)
return ""
case utils.ENTER:
e.editorSetStatusMessage(utils.KILO_DEFAULT_STATUS_MESSAGE)
return buf
default:
if !utils.IsCtrlKey(b) || b < 128 {
buf += string(rune(b))
}
}
}
}
Note: We've added the
Ctrl-Ckey map to test the prompt
Save as
Now that we can prompt things to the user, let's implement the Save as functionality, and it will only work at the moment when we run our editor without arguments
File: editor/file.go
func (e *EditorConfig) editorSave() {
if e.filename == "" {
e.filename = e.editorPrompt("Save as: ")
if e.filename == "" {
e.editorSetStatusMessage("Save aborted")
return
}
}
...
}

Top comments (0)