Insert a char
All checks were successful
timw4mail/gilo/pipeline/head This commit looks good

This commit is contained in:
Timothy Warren 2021-04-01 16:17:13 -04:00
parent bd9a07feed
commit 41d1aa6553
19 changed files with 235 additions and 166 deletions

View File

@ -47,4 +47,4 @@ func TestBufferToString(t *testing.T) {
if got != want { if got != want {
t.Errorf("Failed to convert to proper string") t.Errorf("Failed to convert to proper string")
} }
} }

View File

@ -5,4 +5,4 @@ package editor
// ---------------------------------------------------------------------------- // ----------------------------------------------------------------------------
const KiloVersion = "0.0.1" const KiloVersion = "0.0.1"
const KiloTabStop = 4 const KiloTabStop = 4

View File

@ -44,4 +44,4 @@ func (d *document) appendRow(s string) {
func (d *document) rowCount() int { func (d *document) rowCount() int {
return len(d.rows) return len(d.rows)
} }

View File

@ -32,4 +32,4 @@ func TestRowCount(t *testing.T) {
if got != want { if got != want {
t.Errorf("Expected %d rows, got %d rows", want, got) t.Errorf("Expected %d rows, got %d rows", want, got)
} }
} }

View File

@ -24,7 +24,7 @@ func (e *editor) RefreshScreen() {
e.drawStatusBar(ab) e.drawStatusBar(ab)
e.drawMessageBar(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) ab.append(terminal.ShowCursor)
@ -42,7 +42,7 @@ func (e *editor) scroll() {
e.offset.y = e.cursor.y 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 e.offset.y = e.cursor.y - e.screen.Rows + 1
} }
@ -50,13 +50,13 @@ func (e *editor) scroll() {
e.offset.x = e.renderX 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 e.offset.x = e.renderX - e.screen.Cols
} }
} }
func (e *editor) drawRows(ab *buffer) { 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 fileRow := y + e.offset.y
if fileRow >= e.document.rowCount() { if fileRow >= e.document.rowCount() {
@ -73,20 +73,19 @@ func (e *editor) drawRows(ab *buffer) {
} }
rowLen := e.document.rows[fileRow].rSize() - e.offset.x 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(outputRow)
} }
ab.append(terminal.ClearLine) ab.appendLn(terminal.ClearLine)
ab.append("\r\n")
} }
} }
func (e *editor) drawPlaceholderRow(y int, ab *buffer) { 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) welcome := fmt.Sprintf("Gilo editor -- version %s", KiloVersion)
if len(welcome) > e.screen.Cols { 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 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) { func (e *editor) drawStatusBar(ab *buffer) {
cols := e.screen.Cols cols := e.screen.Cols
ab.append(terminal.InvertColor) ab.append(terminal.InvertColor)
fileName := "[No Name]" fileName := "[No Name]"
@ -119,14 +118,14 @@ func (e *editor) drawStatusBar(ab *buffer) {
leftStatus := fmt.Sprintf("%.20s - %d lines", fileName, e.document.rowCount()) leftStatus := fmt.Sprintf("%.20s - %d lines", fileName, e.document.rowCount())
length := len(leftStatus) length := len(leftStatus)
if length > cols { if length > cols {
leftStatus = truncateString(leftStatus, cols) leftStatus = truncate(leftStatus, cols)
ab.append(leftStatus) ab.append(leftStatus)
ab.append(terminal.ResetColor) ab.append(terminal.ResetColor)
return return
} }
rightStatus := fmt.Sprintf("%d/%d", e.cursor.y, e.document.rowCount()) rightStatus := fmt.Sprintf("%d/%d", e.cursor.y, e.document.rowCount())
rlength := len(rightStatus) rlength := len(rightStatus)
statusLength := length + rlength statusLength := length + rlength
@ -147,10 +146,9 @@ func (e *editor) drawStatusBar(ab *buffer) {
ab.append(leftStatus) ab.append(leftStatus)
// Pad the rest of the status line // Pad the rest of the status line
padding := strings.Repeat(" ", cols - length) padding := strings.Repeat(" ", cols-length)
ab.append(padding) ab.append(padding)
ab.append(terminal.ResetColor) ab.append(terminal.ResetColor)
} }
@ -158,7 +156,7 @@ func (e *editor) drawMessageBar(ab *buffer) {
ab.append("\r\n") ab.append("\r\n")
ab.append(terminal.ClearLine) 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 { if len(msg) > 0 && time.Since(e.status.created).Seconds() < 5.0 {
ab.append(msg) ab.append(msg)
} }

View File

@ -3,6 +3,7 @@ package editor
import ( import (
"fmt" "fmt"
"time" "time"
"timshome.page/gilo/key"
"timshome.page/gilo/terminal" "timshome.page/gilo/terminal"
) )
@ -35,8 +36,8 @@ func New() *editor {
// Subtract rows for status bar and message bar/prompt // Subtract rows for status bar and message bar/prompt
screen.Rows -= 2 screen.Rows -= 2
cursor := &point { 0, 0 } cursor := &point{0, 0}
offset := &point { 0, 0 } offset := &point{0, 0}
document := newDocument() document := newDocument()
status := &statusMsg{ status := &statusMsg{
"", "",
@ -58,45 +59,47 @@ func (e *editor) Open(filename string) {
} }
func (e *editor) SetStatusMessage(template string, a ...interface{}) { func (e *editor) SetStatusMessage(template string, a ...interface{}) {
e.status = &statusMsg { e.status = &statusMsg{
fmt.Sprintf(template, a...), fmt.Sprintf(template, a...),
time.Now(), time.Now(),
} }
} }
func (e *editor) ProcessKeypress() bool { func (e *editor) ProcessKeypress() bool {
var str string
ch, _ := terminal.ReadKey() 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: default:
// Do something later e.insertChar(ch)
terminal.Write("Code: %v", str)
} }
return true return true
} }
func (e *editor) moveCursor (key string) { func (e *editor) moveCursor(key string) {
var row *row var row *row
if e.cursor.y >= e.document.rowCount() { if e.cursor.y >= e.document.rowCount() {
row = nil row = nil
@ -121,7 +124,7 @@ func (e *editor) moveCursor (key string) {
} }
// Move from end of current line to beginning of next line // 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.y += 1
e.cursor.x = 0 e.cursor.x = 0
} }
@ -141,7 +144,7 @@ func (e *editor) moveCursor (key string) {
} }
case keyPageDown: 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 e.cursor.y += e.screen.Rows
} else { } else {
e.cursor.y = e.document.rowCount() - 1 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 // Convert the raw ANSI escape sequences to the type of key input
func parseEscapeSequence () string { func parseEscapeSequence() string {
var runes []rune var runes []rune
for i := 0; i < 3; i++ { for i := 0; i < 3; i++ {

View File

@ -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) // t.Errorf("Output %v not equal to expected %v for input %q", got, want, test.key)
// } // }
// } // }
//} //}

View File

@ -4,7 +4,7 @@ package editor
import "strings" import "strings"
// Truncate a string to a length // Truncate a string to a length
func truncateString(s string, length int) string { func truncate(s string, length int) string {
if length < 1 { if length < 1 {
return "" return ""
} }
@ -27,28 +27,3 @@ func truncateString(s string, length int) string {
return buf.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
}

View File

@ -4,67 +4,9 @@ import (
"testing" "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) { func TestTruncateString(t *testing.T) {
firstString := "abcdefghijklmnopqrstuvwxyz" firstString := "abcdefghijklmnopqrstuvwxyz"
truncated := truncateString(firstString, 13) truncated := truncate(firstString, 13)
got := len(truncated) got := len(truncated)
want := 13 want := 13
@ -75,7 +17,7 @@ func TestTruncateString(t *testing.T) {
} }
func TestTruncateStringNegative(t *testing.T) { func TestTruncateStringNegative(t *testing.T) {
got := truncateString("fdlkjf", -5) got := truncate("fdlkjf", -5)
want := "" want := ""
if got != want { if got != want {
@ -86,11 +28,10 @@ func TestTruncateStringNegative(t *testing.T) {
func TestTruncateShorterString(t *testing.T) { func TestTruncateShorterString(t *testing.T) {
str := "abcdefg" str := "abcdefg"
got := truncateString(str, 13) got := truncate(str, 13)
want := str want := str
if got != want { if got != want {
t.Errorf("Truncated value: %q, expected value: %q", got, want) t.Errorf("Truncated value: %q, expected value: %q", got, want)
} }
} }

View File

@ -1,18 +1,5 @@
package editor package editor
// ----------------------------------------------------------------------------
// !Terminal Input Escape Code Sequences
// ----------------------------------------------------------------------------
const(
KeyArrowUp = "A"
KeyArrowDown = "B"
KeyArrowRight = "C"
KeyArrowLeft = "D"
KeyPageUp = "5~"
KeyPageDown = "6~"
)
// ---------------------------------------------------------------------------- // ----------------------------------------------------------------------------
// !Constants representing input keys // !Constants representing input keys
// ---------------------------------------------------------------------------- // ----------------------------------------------------------------------------

View File

@ -1,6 +1,8 @@
package editor package editor
import "strings" import (
"strings"
)
type row struct { type row struct {
chars []rune chars []rune
@ -16,7 +18,7 @@ func newRow(s string) *row {
render = append(render, ch) render = append(render, ch)
} }
return &row {chars, render} return &row{chars, render}
} }
func (r *row) size() int { func (r *row) size() int {
@ -27,6 +29,31 @@ func (r *row) rSize() int {
return len(r.render) 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() { func (r *row) update() {
r.render = r.render[:0] r.render = r.render[:0]
replacement := strings.Repeat(" ", KiloTabStop) 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 renderX := 0
i := 0 i := 0
@ -50,4 +81,4 @@ func (r *row) cursorXToRenderX (cursorX int) int {
} }
return renderX return renderX
} }

View File

@ -38,3 +38,35 @@ func TestRenderSize(t *testing.T) {
t.Errorf("Row rsize not equal to number of runes in original string") 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)
}
}
}

View File

@ -29,7 +29,7 @@ func main() {
e.Open(os.Args[1]) e.Open(os.Args[1])
} }
e.SetStatusMessage("HELP: Ctrl-Q = quit"); e.SetStatusMessage("HELP: Ctrl-Q = quit")
// The input loop // The input loop
for { for {

34
key/key.go Normal file
View File

@ -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
}

61
key/key_test.go Normal file
View File

@ -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)
}
}
}

View File

@ -9,7 +9,6 @@ import "fmt"
const EscPrefix = "\x1b[" const EscPrefix = "\x1b["
const ( const (
// Clears the line after the escape sequence // Clears the line after the escape sequence
ClearLine = EscPrefix + "K" ClearLine = EscPrefix + "K"
@ -41,13 +40,12 @@ const (
// ---------------------------------------------------------------------------- // ----------------------------------------------------------------------------
// Add the ANSI escape code prefix to the relevant escape code // 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...) str := fmt.Sprintf(s, a...)
return EscPrefix + str return EscPrefix + str
} }
// Generate the escape sequence to move the terminal cursor to the 0-based coordinate // Generate the escape sequence to move the terminal cursor to the 0-based coordinate
func MoveCursor(x int, y int) string { func MoveCursor(x int, y int) string {
// Allow 0-based indexing, the terminal code is 1-based // Allow 0-based indexing, the terminal code is 1-based
@ -55,4 +53,4 @@ func MoveCursor(x int, y int) string {
y += 1 y += 1
return Code("%d;%dH", y, x) return Code("%d;%dH", y, x)
} }

View File

@ -20,4 +20,4 @@ func TestMoveCursor(t *testing.T) {
if got != want { if got != want {
t.Errorf("Failed to create escape sequence to move cursor") t.Errorf("Failed to create escape sequence to move cursor")
} }
} }

View File

@ -26,4 +26,4 @@ func WriteLn(s string, a ...interface{}) {
str := fmt.Sprintf(s, a...) str := fmt.Sprintf(s, a...)
Write("%s\r\n", str) Write("%s\r\n", str)
} }

View File

@ -12,14 +12,14 @@ type Screen struct {
} }
// Get the size of the terminal in rows and columns // Get the size of the terminal in rows and columns
func Size () *Screen { func Size() *Screen {
cols := 80 cols := 80
rows := 24 rows := 24
// Try the syscall first // Try the syscall first
cols, rows, err := term.GetSize(int(os.Stdin.Fd())) cols, rows, err := term.GetSize(int(os.Stdin.Fd()))
if err == nil { if err == nil {
return &Screen {rows, cols } return &Screen{rows, cols}
} }
// Figure out the size the hard way // Figure out the size the hard way
@ -27,10 +27,10 @@ func Size () *Screen {
rows, cols = sizeTrick() 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 // Move cursor to location further than likely screen size
// The cursor will move to maximum available position // The cursor will move to maximum available position
fmt.Print(Code("999C") + Code("999B")) fmt.Print(Code("999C") + Code("999B"))
@ -46,7 +46,7 @@ func sizeTrick () (rows int, cols int) {
continue continue
} }
if char == 'R' || char == '\x00'{ if char == 'R' || char == '\x00' {
break break
} }
@ -59,4 +59,4 @@ func sizeTrick () (rows int, cols int) {
} }
return rows, cols return rows, cols
} }