working on backspace, delete and enter key behavior
parent
c89d698b60
commit
cab3304e5b
14
README.md
14
README.md
|
|
@ -1,3 +1,15 @@
|
|||
# Pound
|
||||
|
||||
a text editor - following along with this: https://viewsourcecode.org/snaptoken/kilo/03.rawInputAndOutput.html
|
||||
A text editor - following along with this: https://viewsourcecode.org/snaptoken/kilo/03.rawInputAndOutput.html
|
||||
|
||||
|
||||
## Tips
|
||||
|
||||
Want to add a binding (e.g. "PageUp") but are unsure what the X sequence
|
||||
(e.g. "\x1b[5~") is? Open another terminal (like xterm) without tmux,
|
||||
then run `showkey -a` to get the sequence associated to a key combination.
|
||||
|
||||
|
||||
## License
|
||||
|
||||
GPL3
|
||||
|
|
|
|||
50
file.go
50
file.go
|
|
@ -2,23 +2,59 @@ package main
|
|||
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
)
|
||||
|
||||
func fileOpen(fname string) {
|
||||
f, err := os.Open(fname)
|
||||
|
||||
if err != nil && os.IsNotExist(err) {
|
||||
// create file if it doesn't exist
|
||||
if f, err = os.Create(fname); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
state.rows = []string{""}
|
||||
state.Cy = 0
|
||||
state.Cx = 0
|
||||
} else if err != nil {
|
||||
log.Fatal(err)
|
||||
} else {
|
||||
fread := bufio.NewScanner(f)
|
||||
fread.Split(bufio.ScanLines)
|
||||
state.rows = nil
|
||||
for fread.Scan() {
|
||||
state.rows = append(state.rows, fread.Text())
|
||||
}
|
||||
|
||||
// if len(state.rows) == 0 || (len(state.rows) == 1 && state.rows[0] == "") {
|
||||
// state.rows = []string{" "}
|
||||
// }
|
||||
}
|
||||
|
||||
f.Close()
|
||||
|
||||
state.file = fname
|
||||
}
|
||||
|
||||
func fileSave() {
|
||||
f, err := os.Create(state.file)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
fread := bufio.NewScanner(f)
|
||||
fread.Split(bufio.ScanLines)
|
||||
state.rows = nil
|
||||
|
||||
for fread.Scan() {
|
||||
state.rows = append(state.rows, fread.Text())
|
||||
bytesWritten := 0
|
||||
for _, row := range state.rows {
|
||||
bw, err := fmt.Fprintf(f, "%s\n", row)
|
||||
if err != nil {
|
||||
log.Fatalf("COULD NOT SAVE FILE: %v", err)
|
||||
}
|
||||
bytesWritten += bw
|
||||
}
|
||||
state.file = fname
|
||||
|
||||
state.StatusLine = fmt.Sprintf("'%s' wrote %d bytes.", state.file, bytesWritten)
|
||||
|
||||
}
|
||||
|
|
|
|||
120
input.go
120
input.go
|
|
@ -12,12 +12,14 @@ import (
|
|||
)
|
||||
|
||||
var (
|
||||
stdin = bufio.NewReader(os.Stdin)
|
||||
stdout = bufio.NewWriter(os.Stdout)
|
||||
stdin = bufio.NewReader(os.Stdin)
|
||||
)
|
||||
|
||||
const (
|
||||
KEY_ESC = rune('\x1b')
|
||||
KEY_BACKSPACE = rune('\x08')
|
||||
KEY_ENTER = rune('\x0d')
|
||||
KEY_TAB = rune('\x09')
|
||||
KEY_UP_ARROW = (iota + 1000)
|
||||
KEY_DOWN_ARROW = (iota + 1000)
|
||||
KEY_LEFT_ARROW = (iota + 1000)
|
||||
|
|
@ -31,33 +33,102 @@ const (
|
|||
|
||||
func editorKeyPresses() bool {
|
||||
char := editorReadKey()
|
||||
state.StatusLine = ""
|
||||
|
||||
editorMoveCaret(char)
|
||||
|
||||
if char == ctrl_key('r') {
|
||||
uiRefresh()
|
||||
}
|
||||
|
||||
if char == ctrl_key('w') {
|
||||
} else if char == ctrl_key('s') {
|
||||
fileSave()
|
||||
} else if char == ctrl_key('j') {
|
||||
for i := state.rowOffset + state.Cy; i > 0; i-- {
|
||||
editorMoveCaret(KEY_UP_ARROW)
|
||||
}
|
||||
}
|
||||
if char == ctrl_key('s') {
|
||||
} else if char == ctrl_key('k') {
|
||||
for i := state.rowOffset + state.Cy; i < state.getCurrentRowLen()-1; i++ {
|
||||
editorMoveCaret(KEY_DOWN_ARROW)
|
||||
}
|
||||
}
|
||||
|
||||
if char == ctrl_key('q') || char == 'q' {
|
||||
} else if char == ctrl_key('q') {
|
||||
return false
|
||||
} else if char == KEY_BACKSPACE || char == KEY_DELETE {
|
||||
editorRemoveChar(char)
|
||||
} else if char >= 32 && char <= 126 || char == KEY_ENTER || char == KEY_TAB {
|
||||
editorInsertChar(char)
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func editorRemoveChar(char rune) {
|
||||
lineText := state.rows[state.Cy]
|
||||
rl := state.getCurrentRowLen()
|
||||
|
||||
cx := state.Cx
|
||||
if char == KEY_BACKSPACE {
|
||||
if cx == 0 && state.Cy > 0 {
|
||||
plt := state.rows[state.Cy-1]
|
||||
state.rows[state.Cy-1] = fmt.Sprintf("%s%s", plt, lineText)
|
||||
if state.Cy < len(state.rows)-1 {
|
||||
state.rows = append(state.rows[:state.Cy-1], state.rows[state.Cy:]...)
|
||||
editorMoveCaret(KEY_UP_ARROW)
|
||||
} else {
|
||||
state.rows = state.rows[:state.Cy-1]
|
||||
state.Cy--
|
||||
}
|
||||
state.Cx = 0
|
||||
} else if cx == 0 {
|
||||
// do nothing because there are no characters
|
||||
return
|
||||
} else {
|
||||
state.StatusLine = fmt.Sprintf("D: %d - %s%s", cx, lineText[:cx-1], lineText[cx:])
|
||||
state.rows[state.Cy] = fmt.Sprintf("%s%s", lineText[:cx-1], lineText[cx:])
|
||||
editorMoveCaret(KEY_LEFT_ARROW)
|
||||
}
|
||||
|
||||
} else if char == KEY_DELETE {
|
||||
if cx < rl-1 {
|
||||
state.rows[state.Cy] = fmt.Sprintf("%s%s", lineText[:cx], lineText[cx+1:])
|
||||
}
|
||||
} else {
|
||||
return
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func editorInsertChar(char rune) {
|
||||
lt := ""
|
||||
cx := state.Cx
|
||||
if len(state.rows) > 0 {
|
||||
lt = state.rows[state.Cy]
|
||||
} else {
|
||||
state.rows = []string{fmt.Sprintf("%c", char)}
|
||||
return
|
||||
}
|
||||
|
||||
if char == KEY_ENTER {
|
||||
oldL := lt[:cx]
|
||||
newL := lt[cx:]
|
||||
state.rows[state.Cy] = oldL
|
||||
if state.Cy == len(state.rows)-1 {
|
||||
state.rows[state.Cy] = oldL
|
||||
state.rows = append(state.rows, newL)
|
||||
} else {
|
||||
state.rows = append(append(state.rows[:state.Cy], newL), state.rows[state.Cy:]...)
|
||||
}
|
||||
state.Cx = 0
|
||||
editorMoveCaret(KEY_DOWN_ARROW)
|
||||
} else {
|
||||
state.rows[state.Cy] = fmt.Sprintf("%s%c%s", lt[:cx], char, lt[cx:])
|
||||
editorMoveCaret(KEY_RIGHT_ARROW)
|
||||
}
|
||||
}
|
||||
|
||||
func editorMoveCaret(char rune) {
|
||||
x, y := state.Cx, state.Cy
|
||||
if len(state.rows) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
rx := editorCxoRx(state.rows[y], x)
|
||||
|
||||
|
|
@ -96,7 +167,6 @@ func editorMoveCaret(char rune) {
|
|||
break
|
||||
}
|
||||
|
||||
|
||||
if char == ctrl_key(KEY_DOWN_ARROW) {
|
||||
y = len(state.rows) - 1
|
||||
}
|
||||
|
|
@ -129,13 +199,13 @@ func editorScroll() {
|
|||
state.colOffset = rx
|
||||
}
|
||||
|
||||
// if state.Cx >= state.colOffset+displayWidth {
|
||||
// state.colOffset = state.Cx - displayWidth
|
||||
// }
|
||||
if state.Cx >= state.colOffset+displayWidth {
|
||||
state.colOffset = state.Cx - displayWidth
|
||||
}
|
||||
|
||||
// if state.Cx < state.colOffset {
|
||||
// state.colOffset = state.Cx
|
||||
// }
|
||||
if state.Cx < state.colOffset {
|
||||
state.colOffset = state.Cx
|
||||
}
|
||||
|
||||
if state.Cy >= state.rowOffset+(state.Rows-2) {
|
||||
state.rowOffset = state.Cy - (state.Rows - 2)
|
||||
|
|
@ -154,30 +224,27 @@ func editorReadKey() rune {
|
|||
for read != 1 {
|
||||
in, read, err = stdin.ReadRune()
|
||||
if err != nil && err != io.EOF {
|
||||
log.Fatal("FAILED TO READ:", err)
|
||||
return KEY_ESC
|
||||
//log.Fatal("FAILED TO READ:", err)
|
||||
}
|
||||
}
|
||||
|
||||
// fmt.Printf("%v == %v ? %v", rune(in[0]), KEY_ESC, rune(in[0]) == KEY_ESC)
|
||||
if in == KEY_ESC {
|
||||
seq := make([]rune, 3)
|
||||
if seq[0], read, err = stdin.ReadRune(); err != nil || read != 1 {
|
||||
return KEY_ESC
|
||||
}
|
||||
|
||||
if seq[1], read, err = stdin.ReadRune(); err != nil || read != 1 {
|
||||
return KEY_ESC
|
||||
}
|
||||
|
||||
if seq[0] == '[' {
|
||||
|
||||
// fmt.Printf("%c %c %c", seq[0], seq[1], seq[2])
|
||||
if seq[1] >= '0' && seq[1] <= '9' {
|
||||
if seq[2], read, err = stdin.ReadRune(); err != nil || read != 1 {
|
||||
return KEY_ESC
|
||||
}
|
||||
|
||||
state.StatusLine = fmt.Sprintf("Last Read = '%c''%c''%c' ", seq[0], seq[1], seq[2])
|
||||
|
||||
// handle page up/down and home keys
|
||||
if seq[2] == '~' {
|
||||
switch seq[1] {
|
||||
|
|
@ -224,6 +291,13 @@ func editorReadKey() rune {
|
|||
return KEY_ESC
|
||||
}
|
||||
|
||||
switch in {
|
||||
case '\x7f': // backspace
|
||||
return KEY_BACKSPACE
|
||||
case '\x0d':
|
||||
return KEY_ENTER
|
||||
}
|
||||
|
||||
return in
|
||||
}
|
||||
|
||||
|
|
|
|||
24
main.go
24
main.go
|
|
@ -2,11 +2,14 @@ package main
|
|||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"syscall"
|
||||
)
|
||||
|
||||
var Version = "0.0.0-dev"
|
||||
|
||||
var state editorState
|
||||
|
||||
type editorState struct {
|
||||
|
|
@ -17,6 +20,7 @@ type editorState struct {
|
|||
Rows int
|
||||
Cols int
|
||||
StatusLine string
|
||||
DebugLine string
|
||||
rows []string
|
||||
rowOffset int
|
||||
colOffset int
|
||||
|
|
@ -39,10 +43,9 @@ func (es editorState) getRowLen(lineNumber int) int {
|
|||
}
|
||||
|
||||
func main() {
|
||||
defer cleanup()
|
||||
enableRawMode()
|
||||
initEditor()
|
||||
defer disableRawMode()
|
||||
defer uiClearScreen()
|
||||
if len(os.Args) > 1 {
|
||||
fileOpen(os.Args[1])
|
||||
}
|
||||
|
|
@ -56,11 +59,22 @@ func main() {
|
|||
func initEditor() {
|
||||
var err error
|
||||
if state.Rows, state.Cols, err = getWindowSize(); err != nil {
|
||||
log.Fatal(err)
|
||||
log.Fatalf("COULD NOT GET WINDOW SIZE: %v", err)
|
||||
}
|
||||
|
||||
state.Cx = 5
|
||||
state.Cx = 0
|
||||
state.Buffer = &bytes.Buffer{}
|
||||
state.rows = []string{"Pound Ed -- version 0.0.0"}
|
||||
state.rows = []string{fmt.Sprintf("Pound Ed %s", Version)}
|
||||
state.file = ""
|
||||
}
|
||||
|
||||
func cleanup() {
|
||||
disableRawMode()
|
||||
uiClearScreen()
|
||||
|
||||
return
|
||||
if r := recover(); r != nil {
|
||||
fmt.Fprintf(os.Stderr, "Unrecoverable error:\n\t%+v\n", r)
|
||||
os.Exit(-1)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
4
makefile
4
makefile
|
|
@ -28,8 +28,8 @@ $(LINTBIN):
|
|||
@GO111MODULE=off go get github.com/golangci/golangci-lint/cmd/golangci-lint
|
||||
|
||||
lint: $(LINTBIN)
|
||||
go mod tidy -v
|
||||
@go mod tidy -v
|
||||
$(LINTBIN) run -p bugs -p format -p performance -p unused
|
||||
|
||||
test: lint
|
||||
test:
|
||||
go test -v -cover $(PKGS)
|
||||
|
|
|
|||
57
ui.go
57
ui.go
|
|
@ -16,26 +16,30 @@ import (
|
|||
const (
|
||||
CLEAR = "\x1b[K"
|
||||
TAB_SIZE = 8
|
||||
EMPTY_LINE_GUTTER = " ~ "
|
||||
EMPTY_LINE_GUTTER = " x~ "
|
||||
LINE_GUTTER = " "
|
||||
)
|
||||
|
||||
func uiDrawRows() {
|
||||
buf := state.Buffer
|
||||
_, highlightedRowLen := renderLine(state.rows[state.Cy])
|
||||
rx := editorCxoRx(state.rows[state.Cy], state.Cx)
|
||||
currentRow := ""
|
||||
if len(state.rows) > 0 {
|
||||
currentRow = state.rows[state.Cy]
|
||||
}
|
||||
|
||||
_, highlightedRowLen := renderLine(currentRow)
|
||||
rx := editorCxoRx(currentRow, state.Cx)
|
||||
gutterWidth, gutterFmtStr := calcMaxGutterWidth()
|
||||
|
||||
state.StatusLine = fmt.Sprintf("offset: {x:%d, y:%d}, loc {x:%d:y:%d}",
|
||||
state.colOffset,
|
||||
state.rowOffset,
|
||||
state.Cx,
|
||||
state.Cy)
|
||||
file := state.file
|
||||
if file == "" {
|
||||
file = "NO FILE"
|
||||
}
|
||||
|
||||
statusLine := fmt.Sprintf(
|
||||
"%s\x1b[1;7m '%s' \x1b[m L:%d/%d C:%d/%d | H:%d W: %d | %s",
|
||||
CLEAR,
|
||||
state.file,
|
||||
file,
|
||||
state.Cy+1,
|
||||
len(state.rows),
|
||||
rx+1,
|
||||
|
|
@ -105,11 +109,13 @@ func uiRefresh() {
|
|||
|
||||
gutterWidth, _ := calcMaxGutterWidth()
|
||||
|
||||
|
||||
// x := gutterWidth + (state.Cx - state.colOffset)
|
||||
y := state.Cy - state.rowOffset
|
||||
rx := gutterWidth + editorCxoRx(state.rows[y], state.Cx) - state.colOffset
|
||||
currentRow := ""
|
||||
if len(state.rows) > 0 {
|
||||
currentRow = state.rows[y]
|
||||
}
|
||||
|
||||
rx := gutterWidth + editorCxoRx(currentRow, state.Cx) - state.colOffset
|
||||
|
||||
setCursorPosition(rx, y)
|
||||
|
||||
|
|
@ -119,20 +125,12 @@ func uiRefresh() {
|
|||
buf.Reset()
|
||||
}
|
||||
|
||||
type window struct {
|
||||
Row uint16
|
||||
Col uint16
|
||||
Xpixel uint16
|
||||
Ypixel uint16
|
||||
}
|
||||
|
||||
func uiClearScreen() {
|
||||
fmt.Fprintf(os.Stdout, "\x1b[2J")
|
||||
fmt.Fprintf(os.Stdout, "\x1b[H")
|
||||
}
|
||||
|
||||
func setCursorPosition(x, y int) {
|
||||
|
||||
fmt.Fprintf(state.Buffer, "\x1b[%d;%dH", y+1, x+1)
|
||||
}
|
||||
|
||||
|
|
@ -165,6 +163,13 @@ func getCursorPosition() (int, int, error) {
|
|||
return rows, cols, nil
|
||||
}
|
||||
|
||||
type window struct {
|
||||
Row uint16
|
||||
Col uint16
|
||||
Xpixel uint16
|
||||
Ypixel uint16
|
||||
}
|
||||
|
||||
func getWindowSize() (int, int, error) {
|
||||
w := new(window)
|
||||
_, _, err := syscall.Syscall(syscall.SYS_IOCTL,
|
||||
|
|
@ -184,10 +189,6 @@ func getWindowSize() (int, int, error) {
|
|||
return int(w.Row), int(w.Col), nil
|
||||
}
|
||||
|
||||
func iscntrl(c int) bool {
|
||||
return c > 0x00 && c < 0x1f || c == 0x7f
|
||||
}
|
||||
|
||||
func calcMaxGutterWidth() (int, string) {
|
||||
lineCount := len(state.rows)
|
||||
maxDigits := len([]rune(fmt.Sprintf("%d", lineCount)))
|
||||
|
|
@ -199,7 +200,7 @@ func calcMaxGutterWidth() (int, string) {
|
|||
|
||||
func editorCxoRx(row string, cx int) int {
|
||||
rx := 0
|
||||
for idx, c := range []rune(row) {
|
||||
for idx, c := range row {
|
||||
if idx < cx {
|
||||
if c == '\t' {
|
||||
// rx += (TAB_SIZE - 1) - (rx % TAB_SIZE)
|
||||
|
|
@ -211,15 +212,15 @@ func editorCxoRx(row string, cx int) int {
|
|||
break
|
||||
}
|
||||
|
||||
return rx
|
||||
return rx
|
||||
}
|
||||
|
||||
func renderLine(line string) (string, int) {
|
||||
rl := ""
|
||||
tabCount := 0
|
||||
for _, char := range []rune(line) {
|
||||
for _, char := range line {
|
||||
if char == '\t' {
|
||||
tabCount += 1
|
||||
tabCount++
|
||||
for i := 0; i < TAB_SIZE; i++ {
|
||||
rl += " "
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue