You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
393 lines
7.5 KiB
393 lines
7.5 KiB
package main
|
|
|
|
import (
|
|
"bufio"
|
|
"fmt"
|
|
"io"
|
|
"log"
|
|
"os"
|
|
"syscall"
|
|
|
|
"github.com/pkg/term/termios"
|
|
)
|
|
|
|
var (
|
|
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)
|
|
KEY_RIGHT_ARROW = (iota + 1000)
|
|
KEY_PAGEUP = (iota + 1000)
|
|
KEY_PAGEDOWN = (iota + 1000)
|
|
KEY_HOME = (iota + 1000)
|
|
KEY_END = (iota + 1000)
|
|
KEY_DELETE = (iota + 1000)
|
|
)
|
|
|
|
func editorKeyPresses() bool {
|
|
char := editorReadKey()
|
|
state.StatusLines[1] = ""
|
|
|
|
if char == ctrl_key('r') {
|
|
uiRefresh()
|
|
} 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)
|
|
}
|
|
} else if char == ctrl_key('k') {
|
|
for i := state.rowOffset + state.Cy; i < state.getCurrentRowLen()-1; i++ {
|
|
editorMoveCaret(KEY_DOWN_ARROW)
|
|
}
|
|
} 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)
|
|
} else {
|
|
editorMoveCaret(char)
|
|
}
|
|
|
|
return true
|
|
}
|
|
|
|
func editorRemoveChar(char rune) {
|
|
cx := state.Cx
|
|
cy := state.Cy
|
|
rc := len(state.rows)
|
|
|
|
lt := ""
|
|
rl := 0
|
|
|
|
if rc > 0 {
|
|
lt = state.rows[cy]
|
|
rl = state.getCurrentRowLen()
|
|
}
|
|
|
|
// file is empty
|
|
if rc == 0 && lt == "" {
|
|
return
|
|
}
|
|
|
|
/*
|
|
1. Backspace at beginning of a line
|
|
2. Backspace in middle of a line
|
|
3. Backspace at end of a line
|
|
4. Backspace when there are 0 characters in the file
|
|
*/
|
|
if char == KEY_BACKSPACE {
|
|
if cx == 0 && cy == 0 {
|
|
return
|
|
}
|
|
// backspace in the middle of a line
|
|
if cx > 0 && cx < rl {
|
|
state.rows[cy] = fmt.Sprintf("%s%s", lt[:cx-1], lt[cx:])
|
|
editorMoveCaret(KEY_LEFT_ARROW)
|
|
return
|
|
}
|
|
|
|
// backspace at start of line
|
|
// join lines together and remove
|
|
if cx == 0 && cy > 0 {
|
|
plt := state.rows[cy-1]
|
|
_, highlightedRowLen := renderLine(plt)
|
|
state.rows[cy-1] = fmt.Sprintf("%s%s", plt, lt)
|
|
|
|
editorMoveCaret(KEY_UP_ARROW)
|
|
for x := 0; x < highlightedRowLen; x++ {
|
|
editorMoveCaret(KEY_RIGHT_ARROW)
|
|
}
|
|
|
|
// if we're at the last row, make sure to only remove the last line
|
|
if cy == rc-1 {
|
|
state.rows = state.rows[:cy]
|
|
} else {
|
|
state.rows = append(state.rows[:cy], state.rows[cy+1:]...)
|
|
}
|
|
|
|
return
|
|
}
|
|
|
|
// backspace at end of line
|
|
if cx == rl {
|
|
state.rows[cy] = lt[:cx-1]
|
|
editorMoveCaret(KEY_LEFT_ARROW)
|
|
return
|
|
}
|
|
|
|
} else if char == KEY_DELETE {
|
|
// in middle of line
|
|
if cx < rl-1 {
|
|
state.rows[state.Cy] = fmt.Sprintf("%s%s", lt[:cx], lt[cx+1:])
|
|
}
|
|
}
|
|
}
|
|
|
|
func editorInsertChar(char rune) {
|
|
cx := state.Cx
|
|
cy := state.Cy
|
|
rc := len(state.rows)
|
|
|
|
lt := ""
|
|
rl := 0
|
|
|
|
if rc > 0 {
|
|
lt = state.rows[cy]
|
|
rl = state.getCurrentRowLen()
|
|
}
|
|
|
|
|
|
/*
|
|
1. Enter at beginning line
|
|
2. Enter at middle of line
|
|
3. Enter at end of line
|
|
*/
|
|
if char == KEY_ENTER {
|
|
// beginning of line
|
|
if cx == 0 {
|
|
if cy == rc {
|
|
state.rows = append(state.rows, lt)
|
|
} else if cy < rc {
|
|
state.rows = append(append(state.rows[:cy], lt), state.rows[cy:]...)
|
|
}
|
|
state.rows[cy] = ""
|
|
|
|
// end of a line
|
|
} else if cx >= rl {
|
|
if cy < rc {
|
|
state.rows = append(append(state.rows[:cy], ""), state.rows[cy:]...)
|
|
state.rows[cy] = lt
|
|
} else if cy == rc {
|
|
state.rows = append(state.rows[:cy], "")
|
|
}
|
|
// mid line
|
|
} else if cx > 0 {
|
|
state.rows = append(append(state.rows[:cy], lt[cx:]), state.rows[cy:]...)
|
|
state.rows[cy] = lt[:cx]
|
|
}
|
|
|
|
state.Cx = 0
|
|
editorMoveCaret(KEY_DOWN_ARROW)
|
|
} else {
|
|
state.StatusLines[1] = fmt.Sprintf("%c %d", char, char)
|
|
state.rows[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)
|
|
|
|
switch char {
|
|
case KEY_UP_ARROW:
|
|
if y != 0 {
|
|
y--
|
|
}
|
|
break
|
|
case KEY_DOWN_ARROW:
|
|
if state.rows != nil && y < len(state.rows)-1 {
|
|
y++
|
|
}
|
|
break
|
|
case KEY_LEFT_ARROW:
|
|
if x > 0 {
|
|
x--
|
|
} else if y != 0 {
|
|
y--
|
|
x = state.getRowLen(y) + 1
|
|
}
|
|
break
|
|
case KEY_RIGHT_ARROW:
|
|
if state.rows != nil && rx < state.getRowLen(y) {
|
|
x++
|
|
} else if y < len(state.rows)-1 {
|
|
y++
|
|
x = 0
|
|
}
|
|
break
|
|
case KEY_HOME:
|
|
x = 0
|
|
break
|
|
case KEY_END:
|
|
x = state.Cols - 1
|
|
break
|
|
default:
|
|
return
|
|
}
|
|
|
|
if char == ctrl_key(KEY_DOWN_ARROW) {
|
|
y = len(state.rows) - 1
|
|
}
|
|
|
|
if char == ctrl_key(KEY_UP_ARROW) {
|
|
y = 0
|
|
}
|
|
|
|
if x > state.getRowLen(y)+1 {
|
|
x = state.getRowLen(y) + 1
|
|
}
|
|
|
|
state.Cx = x
|
|
state.Cy = y
|
|
|
|
editorScroll()
|
|
}
|
|
|
|
func editorScroll() {
|
|
gutterWidth, _ := calcMaxGutterWidth()
|
|
displayWidth := state.Cols - gutterWidth - 2
|
|
|
|
rx := gutterWidth + editorCxoRx(state.rows[state.Cy], state.Cx)
|
|
|
|
if rx >= state.colOffset+displayWidth {
|
|
state.colOffset = rx - displayWidth
|
|
}
|
|
|
|
if rx < state.colOffset {
|
|
state.colOffset = rx
|
|
}
|
|
|
|
if state.Cx >= state.colOffset+displayWidth {
|
|
state.colOffset = state.Cx - displayWidth
|
|
}
|
|
|
|
if state.Cx < state.colOffset {
|
|
state.colOffset = state.Cx
|
|
}
|
|
|
|
if state.Cy >= state.rowOffset+(state.Rows-2) {
|
|
state.rowOffset = state.Cy - (state.Rows - 2)
|
|
}
|
|
|
|
if state.Cy < state.rowOffset {
|
|
state.rowOffset = state.Cy
|
|
}
|
|
}
|
|
|
|
func editorReadKey() rune {
|
|
in := '\000'
|
|
var err error
|
|
var read int
|
|
|
|
for read != 1 {
|
|
in, read, err = stdin.ReadRune()
|
|
if err != nil && err != io.EOF {
|
|
return KEY_ESC
|
|
//log.Fatal("FAILED TO READ:", err)
|
|
}
|
|
}
|
|
|
|
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] == '[' {
|
|
if seq[1] >= '0' && seq[1] <= '9' {
|
|
if seq[2], read, err = stdin.ReadRune(); err != nil || read != 1 {
|
|
return KEY_ESC
|
|
}
|
|
|
|
// handle page up/down and home keys
|
|
if seq[2] == '~' {
|
|
switch seq[1] {
|
|
case '1':
|
|
case '7':
|
|
return KEY_HOME
|
|
case 3:
|
|
return KEY_DELETE
|
|
case '4':
|
|
case '8':
|
|
return KEY_END
|
|
case '5':
|
|
return KEY_PAGEUP
|
|
case '6':
|
|
return KEY_PAGEDOWN
|
|
}
|
|
}
|
|
} else {
|
|
// handle arrow keys and home keys
|
|
switch seq[1] {
|
|
case 'A':
|
|
return KEY_UP_ARROW
|
|
case 'B':
|
|
return KEY_DOWN_ARROW
|
|
case 'C':
|
|
return KEY_RIGHT_ARROW
|
|
case 'D':
|
|
return KEY_LEFT_ARROW
|
|
case 'H':
|
|
return KEY_HOME
|
|
case 'F':
|
|
return KEY_END
|
|
}
|
|
}
|
|
} else if seq[0] == 'O' {
|
|
switch seq[1] {
|
|
case 'H':
|
|
return KEY_HOME
|
|
case 'F':
|
|
return KEY_END
|
|
}
|
|
}
|
|
|
|
return KEY_ESC
|
|
}
|
|
|
|
switch in {
|
|
case '\x7f': // backspace
|
|
return KEY_BACKSPACE
|
|
case '\x0d':
|
|
return KEY_ENTER
|
|
}
|
|
|
|
return in
|
|
}
|
|
|
|
func enableRawMode() {
|
|
if err := termios.Tcgetattr(uintptr(0), &state.orig); err != nil {
|
|
log.Fatal("Tcgetattr, ", err)
|
|
}
|
|
|
|
raw := state.orig
|
|
raw.Iflag &^= syscall.IXON | syscall.ICRNL | syscall.BRKINT | syscall.INPCK | syscall.ISTRIP
|
|
raw.Oflag &^= syscall.OPOST
|
|
raw.Cflag &^= syscall.CS8
|
|
raw.Lflag &^= syscall.ECHO | syscall.ICANON | syscall.ISIG | syscall.IEXTEN
|
|
raw.Cc[syscall.VMIN] = 0
|
|
raw.Cc[syscall.VTIME] = 1
|
|
|
|
if err := termios.Tcsetattr(uintptr(0), termios.TCSAFLUSH, &raw); err != nil {
|
|
log.Fatal("tcsetattr, ", err)
|
|
}
|
|
}
|
|
|
|
func disableRawMode() {
|
|
if err := termios.Tcsetattr(uintptr(0), termios.TCSAFLUSH, &state.orig); err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
}
|
|
|
|
func ctrl_key(key rune) rune {
|
|
return rune(int(key) & 0x1f)
|
|
}
|