DEV Community

Andres Court
Andres Court

Posted on

Create a Text Editor in Go - Search

You can access the code of this chapter in the Kilo-Go github repository in the search branch.

Currently your file structure should look something like this:

We have now a functional text editor, we could even continue writing the code for this project in it

Search

Let's use editorPrompt to implement a minimal search feature. When the user types a search query and presses Enter, we'll loop through the text, and if a row contains the query string, we'll move the cursor there

File: utils/constants.go

const (
    ...
    KILO_DEFAULT_STATUS_MESSAGE = "HELP: Ctrl-S = save | Ctrl-Q = quit | Ctrl-F = find"
)
Enter fullscreen mode Exit fullscreen mode

File: utils/find.go

package editor

import (
    "strings"
)

func (e *EditorConfig) editorFind() {
    query := e.editorPrompt("Search: ")
    if query == "" {
        return
    }

    for i := range e.numrows {
        row := &e.rows[i]
        if strings.Contains(row.chars, query) {
            e.cy = i
            e.cx = strings.Index(row.chars, query)
            return
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

File: editor/input.go

func (e *EditorConfig) editorProcessKeypress() {
    ...
    switch b {
    ...
    case utils.CtrlKey('f'):
        e.editorFind()
        e.editorSetStatusMessage(utils.KILO_DEFAULT_STATUS_MESSAGE)
    ...
    }
    ...
}
Enter fullscreen mode Exit fullscreen mode

Refactor: Have editorPrompt to take a callback

In order for us to be able to implement an Incremental Search, so we can execute some code when we need

File: utils/constants

type Callback func(query string, key int)
Enter fullscreen mode Exit fullscreen mode

File: editor/input.go

func (e *EditorConfig) editorPrompt(prompt string, callback utils.Callback) string {
    ...
        ...
        switch b {
        ...
        case utils.ENTER:
            e.editorSetStatusMessage(utils.KILO_DEFAULT_STATUS_MESSAGE)
            if callback != nil {
                slog.Info("editorPrompt, calling callback")
            }
            return buf
        ...
}
Enter fullscreen mode Exit fullscreen mode

File: editor/find.go

func (e *EditorConfig) editorFind() {
    query := e.editorPrompt("Search: ", nil)
    ...
}
Enter fullscreen mode Exit fullscreen mode

File: editor/file.go

func (e *EditorConfig) editorSave() {
    if e.filename == "" {
        e.filename = e.editorPrompt("Save as: ", nil)
        if e.filename == "" {
            e.editorSetStatusMessage("Save aborted")
            return
        }
    }
    ...
}
Enter fullscreen mode Exit fullscreen mode

Incremental search

Now let's move the actual searching to a new function that will be called by our callback function

File: editor/find.go

func (e *EditorConfig) editorFind() {
    query := e.editorPrompt("Search: ", e.editorFindCallback)
    if query == "" {
        return
    }

}

func (e *EditorConfig) editorFindCallback(query string, key int) {
    if key == utils.ENTER || key == utils.ESC {
        return
    }

    for i := range e.numrows {
        row := &e.rows[i]
        if strings.Contains(row.chars, query) {
            e.cy = i
            e.cx = strings.Index(row.chars, query)
            return
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

File: editor/input.go

func (e *EditorConfig) editorPrompt(prompt string, callback utils.Callback) string {
    ...
    for {
        ...
        switch b {
        ...
        case utils.ESC:
            slog.Info("editorPrompt, ESC")
            e.editorSetStatusMessage(utils.KILO_DEFAULT_STATUS_MESSAGE)
            if callback != nil {
                callback(buf, b)
            }
            return ""
        case utils.ENTER:
            e.editorSetStatusMessage(utils.KILO_DEFAULT_STATUS_MESSAGE)
            if callback != nil {
                callback(buf, b)
            }
            return buf
        ...
        }

        if callback != nil {
            callback(buf, b)
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Restoring cursor position when canceling query

At the moment if the user cancels the query will be left at whatever position the incremental search left them, we want to make it so it returns to where ever they were

File: editor/find.go

func (e *EditorConfig) editorFind() {
    cx := e.cx
    cy := e.cy
    colloffset := e.colloffset
    rowoffset := e.rowoffset

    query := e.editorPrompt("Search: ", e.editorFindCallback)
    if query == "" {
        e.cx = cx
        e.cy = cy
        e.colloffset = colloffset
        e.rowoffset = rowoffset
    }
}
Enter fullscreen mode Exit fullscreen mode

Search forward and backward

At the moment we can only find the first match it can find, let's add a feature to advance to the next or previous match.

  • The Up Arrow and Left Arrow keys will go to the previous match
  • The Down Arrow and Right Arrow keys will go to the next match

File: editor/input.go

func (e *EditorConfig) editorPrompt(prompt string, callback utils.Callback) string {
    ...
    for {
        ...
        switch b {
        ...
        default:
            if !utils.IsCtrlKey(b) && b < 128 {
                buf += string(rune(b))
            }
        }
        ...
    }
}
Enter fullscreen mode Exit fullscreen mode

File: editor/find.go

// lastMatch is the index of the row that the last match was on
// or -1 if there was no match
var lastMatch int = -1

// direction will store the direction of the search:
// 1 for forward, -1 for backward
var direction int = 1

func (e *EditorConfig) editorFindCallback(query string, key int) {
    if key == utils.ENTER || key == utils.ESC {
        lastMatch = -1
        direction = 1
        return
    }

    switch key {
    case utils.ARROW_DOWN, utils.ARROW_RIGHT:
        direction = 1
    case utils.ARROW_UP, utils.ARROW_LEFT:
        direction = -1
    default:
        lastMatch = -1
        direction = 1
    }

    if lastMatch == -1 {
        direction = 1
    }
    current := lastMatch
    for range e.numrows {
        current += direction
        switch current {
        case -1:
            current = e.numrows - 1
        case e.numrows:
            current = 0
        }

        row := &e.rows[current]
        if strings.Contains(row.chars, query) {
            lastMatch = current
            e.cy = current
            e.cx = strings.Index(row.chars, query)
            e.rowoffset = e.numrows
            return
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Top comments (0)