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"
)
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
}
}
}
File: editor/input.go
func (e *EditorConfig) editorProcessKeypress() {
...
switch b {
...
case utils.CtrlKey('f'):
e.editorFind()
e.editorSetStatusMessage(utils.KILO_DEFAULT_STATUS_MESSAGE)
...
}
...
}
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)
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
...
}
File: editor/find.go
func (e *EditorConfig) editorFind() {
query := e.editorPrompt("Search: ", nil)
...
}
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
}
}
...
}
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
}
}
}
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)
}
}
}
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
}
}
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 ArrowandLeft Arrowkeys will go to the previous match - The
Down ArrowandRight Arrowkeys 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))
}
}
...
}
}
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
}
}
}

Top comments (0)