You can access the code of this chapter in the Kilo-Go github repository in the movingaround branch
Currently your file structure should look something like this:
A line viewer
First we want to be able to view a single line without reading it from a file, we will do that later
File: editor/editor.go
type EditorRow struct {
chars string
}
type EditorConfig struct {
restoreFunc func()
reader *bufio.Reader
rows, cols int
cx, cy int
numrows int
row EditorRow
}
func NewEditor(f func()) *EditorConfig {
...
return &EditorConfig{
restoreFunc: f,
reader: bufio.NewReader(os.Stdin),
rows: rows,
cols: cols,
cx: 0,
cy: 0,
numrows: 0,
}
}
func (e *EditorConfig) EditorLoop() {
defer utils.SafeExit(e.restoreFunc, nil)
e.editorOpen()
...
}
File: editor/file.go
package editor
func (e *EditorConfig) editorOpen() {
line := "Hello, world!"
e.row.chars = line
e.numrows = 1
}
File: editor/output.go
func (e *EditorConfig) editorDrawRows(abuf *ab.AppendBuffer) {
for y := range e.rows {
if y >= e.numrows {
if y == e.rows/3 {
...
}
} else {
fmt.Fprintf(abuf, "%s", e.row.chars)
}
...
}
}
Reading command line arguments
If we pass a file name through the arguments, we want the application to be able to read its name so we can process it later, if we’ve done everything right, then we will have in our logs the name of the file we want to view
File: editor/editor.go
func (e *EditorConfig) EditorLoop() {
defer utils.SafeExit(e.restoreFunc, nil)
if len(os.Args) > 1 {
slog.Info("Opening file", "file", os.Args[1])
e.editorOpen()
}
...
}
Go provide us with the
os.Argsvariable which holds an array with all the arguments passed in the command line being:
- Argument 0: the name of the program that was executed
- Argument 1: in our case the name of the file we want to view
Opening the file
Now lets print the first line of the file we passed as an argument
File: editor/editor.go
func (e *EditorConfig) EditorLoop() {
...
if len(os.Args) > 1 {
e.editorOpen(os.Args[1])
}
...
}
File: editor/file.go
func (e *EditorConfig) editorOpen(filename string) {
file, err := os.Open(filename)
if err != nil {
fmt.Fprintf(os.Stderr, "editorOpen, error opening file: %vrn", err)
utils.SafeExit(e.restoreFunc, err)
os.Exit(1)
}
defer file.Close()
scanner := bufio.NewScanner(file)
scanner.Scan()
e.row.chars = scanner.Text()
e.numrows = 1
if err := scanner.Err(); err != nil {
fmt.Fprintf(os.Stderr, "editorOpen, error scanning file: %vrn", err)
utils.SafeExit(e.restoreFunc, err)
os.Exit(1)
}
}
Hide the welcome message
Now that we are reading from file, the welcome message may come in the way once we reach there with the text, so we need to hide it when opening a file
File: editor/output.go
func (e *EditorConfig) editorDrawRows(abuf *ab.AppendBuffer) {
for y := range e.rows {
if y >= e.numrows {
if e.numrows == 0 && y == e.rows/3 {
...
}
Refactor rename rows and cols
For better readability we will rename the fields rows and cols to the more meaningful name screenrows and screencols
File: editor/editor.go
type EditorConfig struct {
...
screenrows int
screencols int
...
}
func NewEditor(f func()) *EditorConfig {
...
return &EditorConfig{
...
screenrows: rows,
screencols: cols,
...
}
}
File: editor/input.go
func (e *EditorConfig) editorProcessKeypress() {
...
switch b {
...
case utils.ARROW_DOWN, utils.ARROW_LEFT, utils.ARROW_RIGHT, utils.ARROW_UP:
e.editorMoveCursor(b)
case utils.PAGE_DOWN, utils.PAGE_UP:
times := e.screenrows
...
case utils.END_KEY:
e.cx = e.screencols - 1
}
}
func (e *EditorConfig) editorMoveCursor(key int) {
switch key {
...
case utils.ARROW_DOWN:
if e.cy != e.screenrows-1 {
e.cy++
}
...
case utils.ARROW_RIGHT:
if e.cx != e.screencols-1 {
e.cx++
}
}
}
File: editor/output.go
func (e *EditorConfig) editorDrawRows(abuf *ab.AppendBuffer) {
for y := range e.screenrows {
if y >= e.numrows {
if e.numrows == 0 && y == e.screenrows/3 {
...
welcomeLen := min(len(welcomeMessage), e.screencols)
padding := (e.screencols - welcomeLen) / 2
...
if y < e.screenrows-1 {
fmt.Fprintf(abuf, "rn")
}
}
}
Multiple lines
We are finally ready to read multiple lines from the file, first we have to have a place holder to store all the lines in, so lets change the row field in our struct to be an array of rows
File: editor/editor.go
type EditorConfig struct {
...
rows []EditorRow
}
func NewEditor(f func()) *EditorConfig {
...
return &EditorConfig{
...
rows: make([]EditorRow, 0),
}
}
File: editor/file.go
func (e *EditorConfig) editorOpen(filename string) {
...
for scanner.Scan() {
row := EditorRow{chars: scanner.Text()}
e.rows = append(e.rows, row)
e.numrows++
}
...
}
File: editor/output.go
func (e *EditorConfig) editorDrawRows(abuf *ab.AppendBuffer) {
for y := range e.screenrows {
if y >= e.numrows {
...
} else {
fmt.Fprintf(abuf, "%s", e.rows[y].chars)
}
...
}
}
Vertical Scrolling
What happens if our text goes past the window limits, so lets add the ability to scroll vertically
File: editor/editor.go
type EditorConfig struct {
...
rowoffset int
...
}
func NewEditor(f func()) *EditorConfig {
...
return &EditorConfig{
...
rowoffset: 0,
...
}
}
File: editor/input.go
func (e *EditorConfig) editorMoveCursor(key int) {
switch key {
case utils.ARROW_LEFT:
if e.cx > 0 {
e.cx--
}
case utils.ARROW_DOWN:
if e.cy < e.numrows {
e.cy++
}
...
}
}
File: editor/output.go
func (e *EditorConfig) editorRefreshScreen() {
e.editorScroll()
...
fmt.Fprintf(abuf, "%c[%d;%dH", utils.ESC, (e.cy-e.rowoffset)+1, e.cx+1)
...
}
func (e *EditorConfig) editorDrawRows(abuf *ab.AppendBuffer) {
for y := range e.screenrows {
filerow := y + e.rowoffset
fmt.Fprintf(abuf, "%c[K", utils.ESC)
if filerow >= e.numrows {
...
} else {
fmt.Fprintf(abuf, "%s", e.rows[filerow].chars)
}
...
}
}
func (e *EditorConfig) editorScroll() {
if e.cy < e.rowoffset {
e.rowoffset = e.cy
}
if e.cy >= e.rowoffset+e.screenrows {
e.rowoffset = e.cy - e.screenrows + 1
}
}
Horizontal scrolling
We will implement horizontal scrolling in a similar way as we did the vertical scrolling
File: editor/editor.go
type EditorConfig struct {
...
colloffset int
...
}
func NewEditor(f func()) *EditorConfig {
...
return &EditorConfig{
...
colloffset: 0,
...
}
}
File: editor/input.go
func (e *EditorConfig) editorMoveCursor(key int) {
switch key {
...
case utils.ARROW_RIGHT:
e.cx++
}
}
File: editor/output.go
func (e *EditorConfig) editorRefreshScreen() {
...
fmt.Fprintf(abuf, "%c[%d;%dH", utils.ESC, (e.cy-e.rowoffset)+1, (e.cx-e.colloffset)+1)
...
}
func (e *EditorConfig) editorDrawRows(abuf *ab.AppendBuffer) {
for y := range e.screenrows {
...
if filerow >= e.numrows {
...
} else {
chars := e.rows[filerow].chars
if len(chars) < e.colloffset {
chars = ""
} else {
chars = chars[e.colloffset:]
}
if len(chars) > e.screencols {
chars = chars[e.colloffset:e.screencols]
}
fmt.Fprintf(abuf, "%s", chars)
...
}
}
func (e *EditorConfig) editorScroll() {
...
if e.cx < e.colloffset {
e.colloffset = e.cx
}
if e.cx >= e.colloffset+e.screencols {
e.colloffset = e.cx - e.screencols + 1
}
}
Limit scrolling to the right
Now that we are able to move around the file, we should not be able to move past the last character in a line, making it invalid position. Lets start by not allowing the user to scroll past the end of the current line
File: editor/input.go
func (e *EditorConfig) editorMoveCursor(key int) {
var row *EditorRow = nil
if e.cy < e.numrows {
row = &e.rows[e.cy]
}
switch key {
...
case utils.ARROW_RIGHT:
if row != nil && e.cx < len(row.chars) {
e.cx++
}
}
}
Note: If in your file have some
Tabsour editor still recognizes them asone characterbut displays them like there are several spaces, we will fix that in a later step
Snap cursor to end of line
If you go to the end of a line and move to another which is shorter, the cursor will still be in an invalid position, lets fix that
File: editor/input.go
func (e *EditorConfig) editorMoveCursor(key int) {
...
switch key {
...
}
row = nil
if e.cy < e.numrows {
row = &e.rows[e.cy]
}
if row != nil && e.cx > len(row.chars) {
e.cx = len(row.chars)
}
}
Moving left at the start of a line
Let’s allow the user to press Left Arrow at the beginning of the line to move to the end of the previous line
File: editor/input.go
func (e *EditorConfig) editorMoveCursor(key int) {
...
switch key {
case utils.ARROW_LEFT:
if e.cx != 0 {
e.cx--
} else if e.cy > 0 {
e.cy--
e.cx = len(e.rows[e.cy].chars)
}
...
}
...
}
Moving right at the end of a line
Similarly, lets allow the user to press Right Arrow at the end of a line to go to the beginning of the next line.
File: editor/input.go
func (e *EditorConfig) editorMoveCursor(key int) {
...
switch key {
...
case utils.ARROW_RIGHT:
if row != nil && e.cx < len(row.chars) {
e.cx++
} else if row != nil && e.cx == len(row.chars) {
e.cy++
e.cx = 0
}
}
...
}
Rendering tabs
If you try opening a file that uses a tab character t, you’ll notice that it renders about 8 characters wide, but when you move the cursor it only moves on character per tab. The rendering space is related to the terminal being used and it settings. We want to know the length of each tab, and we also want to control how to render tabs. First we want to create that rendering space
File: editor/editor.go
type EditorRow struct {
chars string
render []byte
}
File: editor/file.go
func (e *EditorConfig) editorOpen(filename string) {
...
for scanner.Scan() {
e.editorAppendRow(scanner.Text())
}
...
}
File: editor/row.go
package editor
func (e *EditorConfig) editorAppendRow(s string) {
row := EditorRow{
chars: s,
render: make([]byte, 0),
}
e.editorUpdateRow(&row)
e.rows = append(e.rows, row)
e.numrows++
}
func (e *EditorConfig) editorUpdateRow(row *EditorRow) {
for j := 0; j < len(row.chars); j++ {
row.render = append(row.render, row.chars[j])
}
}
File: editor/output.go
func (e *EditorConfig) editorDrawRows(abuf *ab.AppendBuffer) {
for y := range e.screenrows {
...
if filerow >= e.numrows {
...
} else {
chars := e.rows[filerow].render
if len(chars) < e.colloffset {
chars = make([]byte, 0)
} else {
chars = chars[e.colloffset:]
}
if len(chars) > e.screencols {
chars = chars[e.colloffset:e.screencols]
}
fmt.Fprintf(abuf, "%s", chars)
}
...
}
}
Read the t character
Let’s start rendering the t character by replacing it with spaces in our render field
File: utils/constants.go
const (
...
KILO_TAB_STOP = 8
)
File: editor/row.go
func (e *EditorConfig) editorUpdateRow(row *EditorRow) {
for j := 0; j < len(row.chars); j++ {
if row.chars[j] == 't' {
for range utils.KILO_TAB_STOP {
row.render = append(row.render, ' ')
}
} else {
row.render = append(row.render, row.chars[j])
}
}
}
Tabs and the cursor
Currently we display the t character as a defined number of spaces in our KILO_TAB_STOP constant, but when moving around it still reads just one character per t, so lets fix it
File: editor/editor.go
type EditorConfig struct {
...
rx int
...
}
func NewEditor(f func()) *EditorConfig {
...
return &EditorConfig{
...
rx: 0,
...
}
}
File: editor/row.go
func editorRowCxToRx(row *EditorRow, cx int) int {
rx := 0
for j := range cx {
if row.chars[j] == 't' {
rx += utils.KILO_TAB_STOP
} else {
rx++
}
}
return rx
}
File: editor/output.go
func (e *EditorConfig) editorRefreshScreen() {
...
fmt.Fprintf(abuf, "%c[%d;%dH", utils.ESC, (e.cy-e.rowoffset)+1, (e.rx-e.colloffset)+1)
...
}
func (e *EditorConfig) editorScroll() {
e.rx = 0
if e.cy < e.numrows {
e.rx = editorRowCxToRx(&e.rows[e.cy], e.cx)
}
...
if e.rx < e.colloffset {
e.colloffset = e.rx
}
if e.rx >= e.colloffset+e.screencols {
e.colloffset = e.rx - e.screencols + 1
}
}
