From 41d1aa655363fa6d47b6615fc5704ad0773e43bc Mon Sep 17 00:00:00 2001 From: Timothy Warren Date: Thu, 1 Apr 2021 16:17:13 -0400 Subject: [PATCH] Insert a char --- editor/buffer_test.go | 2 +- editor/constants.go | 2 +- editor/document.go | 2 +- editor/document_test.go | 2 +- editor/draw.go | 28 ++++++++--------- editor/editor.go | 68 ++++++++++++++++++++++++----------------- editor/editor_test.go | 2 +- editor/fn.go | 27 +--------------- editor/fn_test.go | 65 ++------------------------------------- editor/keymap.go | 13 -------- editor/row.go | 39 ++++++++++++++++++++--- editor/row_test.go | 32 +++++++++++++++++++ gilo.go | 2 +- key/key.go | 34 +++++++++++++++++++++ key/key_test.go | 61 ++++++++++++++++++++++++++++++++++++ terminal/ansi.go | 6 ++-- terminal/ansi_test.go | 2 +- terminal/io.go | 2 +- terminal/size.go | 12 ++++---- 19 files changed, 235 insertions(+), 166 deletions(-) create mode 100644 key/key.go create mode 100644 key/key_test.go diff --git a/editor/buffer_test.go b/editor/buffer_test.go index ecaa001..9991756 100644 --- a/editor/buffer_test.go +++ b/editor/buffer_test.go @@ -47,4 +47,4 @@ func TestBufferToString(t *testing.T) { if got != want { t.Errorf("Failed to convert to proper string") } -} \ No newline at end of file +} diff --git a/editor/constants.go b/editor/constants.go index 2819ccc..33694a7 100644 --- a/editor/constants.go +++ b/editor/constants.go @@ -5,4 +5,4 @@ package editor // ---------------------------------------------------------------------------- const KiloVersion = "0.0.1" -const KiloTabStop = 4 \ No newline at end of file +const KiloTabStop = 4 diff --git a/editor/document.go b/editor/document.go index a86aa8f..d796134 100644 --- a/editor/document.go +++ b/editor/document.go @@ -44,4 +44,4 @@ func (d *document) appendRow(s string) { func (d *document) rowCount() int { return len(d.rows) -} \ No newline at end of file +} diff --git a/editor/document_test.go b/editor/document_test.go index e019bc5..e4a3638 100644 --- a/editor/document_test.go +++ b/editor/document_test.go @@ -32,4 +32,4 @@ func TestRowCount(t *testing.T) { if got != want { t.Errorf("Expected %d rows, got %d rows", want, got) } -} \ No newline at end of file +} diff --git a/editor/draw.go b/editor/draw.go index dcc9970..82ec11d 100644 --- a/editor/draw.go +++ b/editor/draw.go @@ -24,7 +24,7 @@ func (e *editor) RefreshScreen() { e.drawStatusBar(ab) e.drawMessageBar(ab) - ab.append(terminal.MoveCursor(e.renderX - e.offset.x, e.cursor.y - e.offset.y)) + ab.append(terminal.MoveCursor(e.renderX-e.offset.x, e.cursor.y-e.offset.y)) ab.append(terminal.ShowCursor) @@ -42,7 +42,7 @@ func (e *editor) scroll() { e.offset.y = e.cursor.y } - if e.cursor.y >= e.offset.y + e.screen.Rows { + if e.cursor.y >= e.offset.y+e.screen.Rows { e.offset.y = e.cursor.y - e.screen.Rows + 1 } @@ -50,13 +50,13 @@ func (e *editor) scroll() { e.offset.x = e.renderX } - if e.renderX >= e.offset.x + e.screen.Cols { + if e.renderX >= e.offset.x+e.screen.Cols { e.offset.x = e.renderX - e.screen.Cols } } func (e *editor) drawRows(ab *buffer) { - for y :=0; y < e.screen.Rows; y++ { + for y := 0; y < e.screen.Rows; y++ { fileRow := y + e.offset.y if fileRow >= e.document.rowCount() { @@ -73,20 +73,19 @@ func (e *editor) drawRows(ab *buffer) { } rowLen := e.document.rows[fileRow].rSize() - e.offset.x - outputRow := truncateString(string(e.document.rows[fileRow].render[e.offset.x:]), rowLen) + outputRow := truncate(string(e.document.rows[fileRow].render[e.offset.x:]), rowLen) ab.append(outputRow) } - ab.append(terminal.ClearLine) - ab.append("\r\n") + ab.appendLn(terminal.ClearLine) } } func (e *editor) drawPlaceholderRow(y int, ab *buffer) { - if e.document.rowCount() == 0 && y == e.screen.Rows / 3 { + if e.document.rowCount() == 0 && y == e.screen.Rows/3 { welcome := fmt.Sprintf("Gilo editor -- version %s", KiloVersion) if len(welcome) > e.screen.Cols { - welcome = truncateString(welcome, e.screen.Cols) + welcome = truncate(welcome, e.screen.Cols) } padding := (e.screen.Cols - len(welcome)) / 2 @@ -108,7 +107,7 @@ func (e *editor) drawPlaceholderRow(y int, ab *buffer) { func (e *editor) drawStatusBar(ab *buffer) { cols := e.screen.Cols - + ab.append(terminal.InvertColor) fileName := "[No Name]" @@ -119,14 +118,14 @@ func (e *editor) drawStatusBar(ab *buffer) { leftStatus := fmt.Sprintf("%.20s - %d lines", fileName, e.document.rowCount()) length := len(leftStatus) if length > cols { - leftStatus = truncateString(leftStatus, cols) + leftStatus = truncate(leftStatus, cols) ab.append(leftStatus) ab.append(terminal.ResetColor) return } - + rightStatus := fmt.Sprintf("%d/%d", e.cursor.y, e.document.rowCount()) rlength := len(rightStatus) statusLength := length + rlength @@ -147,10 +146,9 @@ func (e *editor) drawStatusBar(ab *buffer) { ab.append(leftStatus) // Pad the rest of the status line - padding := strings.Repeat(" ", cols - length) + padding := strings.Repeat(" ", cols-length) ab.append(padding) - ab.append(terminal.ResetColor) } @@ -158,7 +156,7 @@ func (e *editor) drawMessageBar(ab *buffer) { ab.append("\r\n") ab.append(terminal.ClearLine) - msg := truncateString(e.status.message, e.screen.Cols) + msg := truncate(e.status.message, e.screen.Cols) if len(msg) > 0 && time.Since(e.status.created).Seconds() < 5.0 { ab.append(msg) } diff --git a/editor/editor.go b/editor/editor.go index f1664a8..266b2ba 100644 --- a/editor/editor.go +++ b/editor/editor.go @@ -3,6 +3,7 @@ package editor import ( "fmt" "time" + "timshome.page/gilo/key" "timshome.page/gilo/terminal" ) @@ -35,8 +36,8 @@ func New() *editor { // Subtract rows for status bar and message bar/prompt screen.Rows -= 2 - cursor := &point { 0, 0 } - offset := &point { 0, 0 } + cursor := &point{0, 0} + offset := &point{0, 0} document := newDocument() status := &statusMsg{ "", @@ -58,45 +59,47 @@ func (e *editor) Open(filename string) { } func (e *editor) SetStatusMessage(template string, a ...interface{}) { - e.status = &statusMsg { + e.status = &statusMsg{ fmt.Sprintf(template, a...), time.Now(), } } func (e *editor) ProcessKeypress() bool { - var str string - ch, _ := terminal.ReadKey() - if isCtrl(ch) { - switch ch { - case ctrl('q'): - // Clean up on exit - terminal.Write(terminal.ClearScreen + terminal.ResetCursor) - return false + switch ch { + case key.Ctrl('q'): + // Clean up on exit + terminal.Write(terminal.ClearScreen + terminal.ResetCursor) + + return false + + case key.Esc: + // Modifier keys that return ANSI escape sequences + str := parseEscapeSequence() + + switch str { + case keyUp, + keyDown, + keyLeft, + keyRight, + keyPageUp, + keyPageDown, + keyHome, + keyEnd: + + e.moveCursor(str) } - } - if ch == '\x1b' { - str = parseEscapeSequence() - } else { - str = string(ch) - } - - switch str { - case keyUp, keyDown, keyLeft, keyRight, keyPageUp, keyPageDown, keyHome, keyEnd: - e.moveCursor(str) - return true default: - // Do something later - terminal.Write("Code: %v", str) + e.insertChar(ch) } return true } -func (e *editor) moveCursor (key string) { +func (e *editor) moveCursor(key string) { var row *row if e.cursor.y >= e.document.rowCount() { row = nil @@ -121,7 +124,7 @@ func (e *editor) moveCursor (key string) { } // Move from end of current line to beginning of next line - if row != nil && e.cursor.x == row.size() && e.cursor.y < e.document.rowCount() - 1 { + if row != nil && e.cursor.x == row.size() && e.cursor.y < e.document.rowCount()-1 { e.cursor.y += 1 e.cursor.x = 0 } @@ -141,7 +144,7 @@ func (e *editor) moveCursor (key string) { } case keyPageDown: - if e.cursor.y + e.screen.Rows > e.document.rowCount() { + if e.cursor.y+e.screen.Rows > e.document.rowCount() { e.cursor.y += e.screen.Rows } else { e.cursor.y = e.document.rowCount() - 1 @@ -166,8 +169,17 @@ func (e *editor) moveCursor (key string) { } } +func (e *editor) insertChar(ch rune) { + if e.cursor.y == e.document.rowCount() { + e.document.appendRow("") + } + + e.document.rows[e.cursor.y].insertRune(ch, e.cursor.x) + e.cursor.x += 1 +} + // Convert the raw ANSI escape sequences to the type of key input -func parseEscapeSequence () string { +func parseEscapeSequence() string { var runes []rune for i := 0; i < 3; i++ { diff --git a/editor/editor_test.go b/editor/editor_test.go index abd97ed..c31196f 100644 --- a/editor/editor_test.go +++ b/editor/editor_test.go @@ -30,4 +30,4 @@ func TestNew(t *testing.T) { // t.Errorf("Output %v not equal to expected %v for input %q", got, want, test.key) // } // } -//} \ No newline at end of file +//} diff --git a/editor/fn.go b/editor/fn.go index c8a85a6..488feda 100644 --- a/editor/fn.go +++ b/editor/fn.go @@ -4,7 +4,7 @@ package editor import "strings" // Truncate a string to a length -func truncateString(s string, length int) string { +func truncate(s string, length int) string { if length < 1 { return "" } @@ -27,28 +27,3 @@ func truncateString(s string, length int) string { return buf.String() } - -// Is this an ASCII character? -func isAscii(char rune) bool { - return char < 0x80 -} - -// Is this an ASCII ctrl character? -func isCtrl(char rune) bool { - if !isAscii(char) { - return false - } - - return char == 0x7f || char < 0x20 -} - -// Return the input code of a Ctrl-key chord. -func ctrl(char rune) rune { - if !isAscii(char) { - return 0 - } - - ch := char & 0x1f - - return ch -} \ No newline at end of file diff --git a/editor/fn_test.go b/editor/fn_test.go index bf01b6e..fb3bf18 100644 --- a/editor/fn_test.go +++ b/editor/fn_test.go @@ -4,67 +4,9 @@ import ( "testing" ) -type isRune struct { - arg1 rune - expected bool -} - -// (╯°□°)╯︵ ┻━┻ -var isAsciiTest = []isRune { - {'┻', false}, - {'$', true}, - {'︵', false}, - {0x7f, true}, -} - -func TestIsAscii(t *testing.T) { - for _, test := range isAsciiTest { - if output := isAscii(test.arg1); output != test.expected { - t.Errorf("Output %v not equal to expected %v for input %q", output, test.expected, test.arg1) - } - } -} - -var isCtrlTest = []isRune { - {0x78, false}, - {0x7f, true}, - {0x02, true}, - {0x98, false}, - {'a', false}, -} - -func TestIsCtrl(t *testing.T) { - for _, test := range isCtrlTest { - if output := isCtrl(test.arg1); output != test.expected { - t.Errorf("Output %v not equal to expected %v for input %q", output, test.expected, test.arg1) - } - } -} - -type ctrlTest struct { - arg1, expected rune -} - -var ctrlTests = []ctrlTest { - {'A', '\x01'}, - {'B', '\x02'}, - {'Z', '\x1a'}, - {'#', 3}, - {'┻', 0}, - {'😿', 0}, -} - -func TestCtrl(t *testing.T) { - for _, test := range ctrlTests { - if output := ctrl(test.arg1); output != test.expected { - t.Errorf("Output %v not equal to expected %v for input %q", output, test.expected, test.arg1) - } - } -} - func TestTruncateString(t *testing.T) { firstString := "abcdefghijklmnopqrstuvwxyz" - truncated := truncateString(firstString, 13) + truncated := truncate(firstString, 13) got := len(truncated) want := 13 @@ -75,7 +17,7 @@ func TestTruncateString(t *testing.T) { } func TestTruncateStringNegative(t *testing.T) { - got := truncateString("fdlkjf", -5) + got := truncate("fdlkjf", -5) want := "" if got != want { @@ -86,11 +28,10 @@ func TestTruncateStringNegative(t *testing.T) { func TestTruncateShorterString(t *testing.T) { str := "abcdefg" - got := truncateString(str, 13) + got := truncate(str, 13) want := str if got != want { t.Errorf("Truncated value: %q, expected value: %q", got, want) } } - diff --git a/editor/keymap.go b/editor/keymap.go index b7164df..e0fb4d3 100644 --- a/editor/keymap.go +++ b/editor/keymap.go @@ -1,18 +1,5 @@ package editor -// ---------------------------------------------------------------------------- -// !Terminal Input Escape Code Sequences -// ---------------------------------------------------------------------------- - -const( - KeyArrowUp = "A" - KeyArrowDown = "B" - KeyArrowRight = "C" - KeyArrowLeft = "D" - KeyPageUp = "5~" - KeyPageDown = "6~" -) - // ---------------------------------------------------------------------------- // !Constants representing input keys // ---------------------------------------------------------------------------- diff --git a/editor/row.go b/editor/row.go index 3725b70..7185572 100644 --- a/editor/row.go +++ b/editor/row.go @@ -1,6 +1,8 @@ package editor -import "strings" +import ( + "strings" +) type row struct { chars []rune @@ -16,7 +18,7 @@ func newRow(s string) *row { render = append(render, ch) } - return &row {chars, render} + return &row{chars, render} } func (r *row) size() int { @@ -27,6 +29,31 @@ func (r *row) rSize() int { return len(r.render) } +func (r *row) insertRune(ch rune, at int) { + // If insertion index is invalid, just + // append the rune to the end of the array + if at < 0 || at >= r.size() { + r.chars = append(r.chars, ch) + r.update() + + return + } + + var newSlice []rune + + // Split the character array at the insertion point + start := r.chars[0:at] + end := r.chars[at:r.size()] + + // Splice it back together + newSlice = append(newSlice, start...) + newSlice = append(newSlice, ch) + newSlice = append(newSlice, end...) + + r.chars = newSlice + r.update() +} + func (r *row) update() { r.render = r.render[:0] replacement := strings.Repeat(" ", KiloTabStop) @@ -37,7 +64,11 @@ func (r *row) update() { } } -func (r *row) cursorXToRenderX (cursorX int) int { +func (r *row) toString() string { + return string(r.chars) +} + +func (r *row) cursorXToRenderX(cursorX int) int { renderX := 0 i := 0 @@ -50,4 +81,4 @@ func (r *row) cursorXToRenderX (cursorX int) int { } return renderX -} \ No newline at end of file +} diff --git a/editor/row_test.go b/editor/row_test.go index 358eaab..f83e26f 100644 --- a/editor/row_test.go +++ b/editor/row_test.go @@ -38,3 +38,35 @@ func TestRenderSize(t *testing.T) { t.Errorf("Row rsize not equal to number of runes in original string") } } + +type insertRune struct { + initial string + ch rune + at int +} + +var insertRuneTests = []insertRune{ + {"abde", 'c', 2}, + {"bcde", 'a', 0}, + {"abcd", 'e', 4}, + {"abcd", 'e', 17}, + {"abcd", 'e', -2}, +} + +func TestInsertRune(t *testing.T) { + for _, test := range insertRuneTests { + row := newRow(test.initial) + row.insertRune(test.ch, test.at) + + if row.size() != 5 { + t.Errorf("Row size after inserting rune at index [%d] is %d, should be %d", test.at, row.size(), 5) + } + + got := row.toString() + want := "abcde" + + if got != want { + t.Errorf("Row after update is '%s', should be '%s'", got, want) + } + } +} diff --git a/gilo.go b/gilo.go index 9260e88..3ab9f8b 100644 --- a/gilo.go +++ b/gilo.go @@ -29,7 +29,7 @@ func main() { e.Open(os.Args[1]) } - e.SetStatusMessage("HELP: Ctrl-Q = quit"); + e.SetStatusMessage("HELP: Ctrl-Q = quit") // The input loop for { diff --git a/key/key.go b/key/key.go new file mode 100644 index 0000000..a97be46 --- /dev/null +++ b/key/key.go @@ -0,0 +1,34 @@ +package key + +// ---------------------------------------------------------------------------- +// !Terminal Input Escape Code Sequences +// ---------------------------------------------------------------------------- + +const ( + Esc = '\x1b' +) + +// Is this an ASCII character? +func isAscii(char rune) bool { + return char < 0x80 +} + +// Is this an ASCII ctrl character? +func IsCtrl(char rune) bool { + if !isAscii(char) { + return false + } + + return char == 0x7f || char < 0x20 +} + +// Return the input code of a Ctrl-key chord. +func Ctrl(char rune) rune { + if !isAscii(char) { + return 0 + } + + ch := char & 0x1f + + return ch +} diff --git a/key/key_test.go b/key/key_test.go new file mode 100644 index 0000000..0ba333b --- /dev/null +++ b/key/key_test.go @@ -0,0 +1,61 @@ +package key + +import "testing" + +type isRune struct { + arg1 rune + expected bool +} + +// (╯°□°)╯︵ ┻━┻ +var isAsciiTest = []isRune{ + {'┻', false}, + {'$', true}, + {'︵', false}, + {0x7f, true}, +} + +func TestIsAscii(t *testing.T) { + for _, test := range isAsciiTest { + if output := isAscii(test.arg1); output != test.expected { + t.Errorf("Output %v not equal to expected %v for input %q", output, test.expected, test.arg1) + } + } +} + +var isCtrlTest = []isRune{ + {0x78, false}, + {0x7f, true}, + {0x02, true}, + {0x98, false}, + {'a', false}, +} + +func TestIsCtrl(t *testing.T) { + for _, test := range isCtrlTest { + if output := IsCtrl(test.arg1); output != test.expected { + t.Errorf("Output %v not equal to expected %v for input %q", output, test.expected, test.arg1) + } + } +} + +type ctrlTest struct { + arg1, expected rune +} + +var ctrlTests = []ctrlTest{ + {'A', '\x01'}, + {'B', '\x02'}, + {'Z', '\x1a'}, + {'#', 3}, + {'┻', 0}, + {'😿', 0}, +} + +func TestCtrl(t *testing.T) { + for _, test := range ctrlTests { + if output := Ctrl(test.arg1); output != test.expected { + t.Errorf("Output %v not equal to expected %v for input %q", output, test.expected, test.arg1) + } + } +} diff --git a/terminal/ansi.go b/terminal/ansi.go index 8f40b75..87a6b99 100644 --- a/terminal/ansi.go +++ b/terminal/ansi.go @@ -9,7 +9,6 @@ import "fmt" const EscPrefix = "\x1b[" - const ( // Clears the line after the escape sequence ClearLine = EscPrefix + "K" @@ -41,13 +40,12 @@ const ( // ---------------------------------------------------------------------------- // Add the ANSI escape code prefix to the relevant escape code -func Code (s string, a ...interface{}) string { +func Code(s string, a ...interface{}) string { str := fmt.Sprintf(s, a...) return EscPrefix + str } - // Generate the escape sequence to move the terminal cursor to the 0-based coordinate func MoveCursor(x int, y int) string { // Allow 0-based indexing, the terminal code is 1-based @@ -55,4 +53,4 @@ func MoveCursor(x int, y int) string { y += 1 return Code("%d;%dH", y, x) -} \ No newline at end of file +} diff --git a/terminal/ansi_test.go b/terminal/ansi_test.go index cadfb5e..b82f04b 100644 --- a/terminal/ansi_test.go +++ b/terminal/ansi_test.go @@ -20,4 +20,4 @@ func TestMoveCursor(t *testing.T) { if got != want { t.Errorf("Failed to create escape sequence to move cursor") } -} \ No newline at end of file +} diff --git a/terminal/io.go b/terminal/io.go index b329aca..0451ab5 100644 --- a/terminal/io.go +++ b/terminal/io.go @@ -26,4 +26,4 @@ func WriteLn(s string, a ...interface{}) { str := fmt.Sprintf(s, a...) Write("%s\r\n", str) -} \ No newline at end of file +} diff --git a/terminal/size.go b/terminal/size.go index 6364898..9d88a5b 100644 --- a/terminal/size.go +++ b/terminal/size.go @@ -12,14 +12,14 @@ type Screen struct { } // Get the size of the terminal in rows and columns -func Size () *Screen { +func Size() *Screen { cols := 80 rows := 24 // Try the syscall first cols, rows, err := term.GetSize(int(os.Stdin.Fd())) if err == nil { - return &Screen {rows, cols } + return &Screen{rows, cols} } // Figure out the size the hard way @@ -27,10 +27,10 @@ func Size () *Screen { rows, cols = sizeTrick() } - return &Screen{ rows, cols } + return &Screen{rows, cols} } -func sizeTrick () (rows int, cols int) { +func sizeTrick() (rows int, cols int) { // Move cursor to location further than likely screen size // The cursor will move to maximum available position fmt.Print(Code("999C") + Code("999B")) @@ -46,7 +46,7 @@ func sizeTrick () (rows int, cols int) { continue } - if char == 'R' || char == '\x00'{ + if char == 'R' || char == '\x00' { break } @@ -59,4 +59,4 @@ func sizeTrick () (rows int, cols int) { } return rows, cols -} \ No newline at end of file +}