package main
|
|
|
|
import (
|
|
"bufio"
|
|
"bytes"
|
|
"log"
|
|
"os"
|
|
|
|
"editor/utils"
|
|
"fmt"
|
|
"golang.org/x/crypto/ssh/terminal"
|
|
"regexp"
|
|
"strings"
|
|
)
|
|
|
|
// Editor is the initializing class
|
|
type Editor struct {
|
|
}
|
|
|
|
// Buffer is the temporary view presented to the user
|
|
type Buffer struct {
|
|
Lines []string
|
|
}
|
|
|
|
// Cursor is the object containing cursor point position
|
|
type Cursor struct {
|
|
Row int
|
|
Col int
|
|
}
|
|
|
|
func clamp(max, cur int) int {
|
|
if cur <= max {
|
|
return cur
|
|
}
|
|
return max
|
|
}
|
|
|
|
var filePath string
|
|
var editor Editor
|
|
var buffer Buffer
|
|
var cursor Cursor
|
|
var multiplier int
|
|
|
|
func main() {
|
|
if len(os.Args) == 1 {
|
|
log.Println("a file is required")
|
|
return
|
|
}
|
|
filePath = os.Args[1]
|
|
editor.initialize()
|
|
editor.run()
|
|
}
|
|
|
|
func (e *Editor) initialize() {
|
|
// Load a file
|
|
file, err := os.Open(filePath)
|
|
if err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
defer file.Close()
|
|
|
|
// RAW terminal
|
|
oldState, err := terminal.MakeRaw(0)
|
|
if err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
defer terminal.Restore(0, oldState)
|
|
|
|
var lines []string
|
|
scanner := bufio.NewScanner(file)
|
|
for scanner.Scan() {
|
|
lines = append(lines, scanner.Text())
|
|
}
|
|
if err := scanner.Err(); err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
|
|
multiplier = 1
|
|
buffer = buffer.new(lines)
|
|
cursor = cursor.new()
|
|
}
|
|
|
|
func (e *Editor) run() {
|
|
for {
|
|
e.render()
|
|
e.handleInput()
|
|
}
|
|
}
|
|
|
|
func (e *Editor) render() {
|
|
clearScreen()
|
|
buffer.render()
|
|
|
|
moveCursor(cursor.Row, cursor.Col) // restore cursor
|
|
}
|
|
|
|
func (e *Editor) handleInput() {
|
|
c := utils.Getch()
|
|
// log.Printf("%#v\t%s\n", c, string(c))
|
|
switch {
|
|
case bytes.Equal(c, []byte{0x3}), bytes.Equal(c, []byte{0x11}), bytes.Equal(c, []byte{0x18}): // C-c, C-q, C-x
|
|
// quit
|
|
clearScreen()
|
|
moveCursor(1, 1)
|
|
os.Exit(0)
|
|
return
|
|
case bytes.Equal(c, []byte{0x13}): // C-s
|
|
// save
|
|
buffer.save()
|
|
case bytes.Equal(c, []byte{0x5}): // C-e
|
|
// end of line
|
|
cursorEOL()
|
|
case bytes.Equal(c, []byte{0x1}): // C-a
|
|
// beginning of line
|
|
cursorBOL()
|
|
case bytes.Equal(c, []byte{0x15}): // C-u
|
|
if multiplier == 1 {
|
|
multiplier = 4
|
|
} else {
|
|
multiplier = multiplier + multiplier
|
|
}
|
|
case bytes.Equal(c, []byte{0x1b, 0x5b, 0x41}), bytes.Equal(c, []byte{0x10}): // UP, C-p
|
|
cursorUp(multiplier)
|
|
multiplier = 1
|
|
case bytes.Equal(c, []byte{0x1b, 0x5b, 0x42}), bytes.Equal(c, []byte{0xe}): // DOWN, C-n
|
|
cursorDown(multiplier)
|
|
multiplier = 1
|
|
case bytes.Equal(c, []byte{0x1b, 0x5b, 0x43}), bytes.Equal(c, []byte{0x6}): // RIGHT, C-f
|
|
cursorForward(multiplier)
|
|
multiplier = 1
|
|
case bytes.Equal(c, []byte{0x1b, 0x5b, 0x44}), bytes.Equal(c, []byte{0x2}): // LEFT, C-b
|
|
cursorBackward(multiplier)
|
|
multiplier = 1
|
|
case bytes.Equal(c, []byte{0x1b, 0x62}): // M-b
|
|
cursorBackwardWord()
|
|
multiplier = 1
|
|
case bytes.Equal(c, []byte{0x1b, 0x66}): // M-f
|
|
cursorForwardWord()
|
|
multiplier = 1
|
|
case bytes.Equal(c, []byte{0x7f}): // backspace
|
|
buffer.deleteChar()
|
|
multiplier = 1
|
|
case bytes.Equal(c, []byte{0xb}): // C-k
|
|
buffer.deleteForward()
|
|
multiplier = 1
|
|
default:
|
|
buffer.insertChar(string(c))
|
|
multiplier = 1
|
|
}
|
|
}
|
|
|
|
func (b *Buffer) new(lines []string) Buffer {
|
|
b.Lines = lines
|
|
|
|
return *b
|
|
}
|
|
|
|
func (b *Buffer) fetch(line int) string {
|
|
// fix the index 1 issue
|
|
if line > len(b.Lines) {
|
|
return ""
|
|
}
|
|
return b.Lines[line-1]
|
|
}
|
|
|
|
func (b *Buffer) render() {
|
|
moveCursor(1, 1) // reset cursor for printing buffer
|
|
for _, str := range buffer.Lines {
|
|
// fmt.Printf("%d| %s\r\n", num, str) // with line nums
|
|
fmt.Printf("%s\r\n", str)
|
|
}
|
|
}
|
|
|
|
func (b *Buffer) save() {
|
|
// save file
|
|
file, err := os.OpenFile(filePath, os.O_RDWR|os.O_CREATE, 0755)
|
|
if err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
defer file.Close()
|
|
out := strings.Join(b.Lines, "\n")
|
|
_, err = file.WriteString(out)
|
|
if err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
}
|
|
|
|
func (b *Buffer) insertChar(inp string) {
|
|
if len(b.Lines) < cursor.Row {
|
|
b.Lines = append(b.Lines, inp)
|
|
} else {
|
|
b.Lines[cursor.Row-1] = strings.Join(
|
|
[]string{
|
|
b.Lines[cursor.Row-1][:cursor.Col-1],
|
|
inp,
|
|
b.Lines[cursor.Row-1][cursor.Col-1:],
|
|
}, "")
|
|
}
|
|
cursor.Col++
|
|
moveCursor(cursor.Row, cursor.Col)
|
|
}
|
|
|
|
func (b *Buffer) deleteChar() {
|
|
if cursor.Col == 1 {
|
|
return
|
|
}
|
|
b.Lines[cursor.Row-1] = strings.Join(
|
|
[]string{
|
|
b.Lines[cursor.Row-1][:cursor.Col-2],
|
|
b.Lines[cursor.Row-1][cursor.Col-1:],
|
|
}, "")
|
|
cursor.Col--
|
|
moveCursor(cursor.Row, cursor.Col)
|
|
}
|
|
|
|
func (b *Buffer) deleteForward() {
|
|
b.Lines[cursor.Row-1] = b.Lines[cursor.Row-1][:cursor.Col-1]
|
|
}
|
|
|
|
func (c *Cursor) new() Cursor {
|
|
return Cursor{
|
|
1,
|
|
1,
|
|
}
|
|
}
|
|
|
|
func (c *Cursor) clampCol() {
|
|
line := buffer.fetch(c.Row)
|
|
if c.Col > len(line) {
|
|
c.Col = len(line) + 1
|
|
} else if c.Col < 1 {
|
|
c.Col = 1
|
|
}
|
|
}
|
|
|
|
func (c *Cursor) clampRow() {
|
|
rows := len(buffer.Lines)
|
|
if c.Row > rows+1 {
|
|
c.Row = rows + 1
|
|
} else if c.Row < 1 {
|
|
c.Row = 1
|
|
}
|
|
}
|
|
|
|
//
|
|
//
|
|
//
|
|
|
|
func clearScreen() {
|
|
fmt.Print("[2J")
|
|
}
|
|
func moveCursor(row, col int) {
|
|
fmt.Printf("[%d;%dH", row, col)
|
|
}
|
|
|
|
func cursorEOL() {
|
|
line := buffer.fetch(cursor.Row)
|
|
cursor.Col = len(line) + 1
|
|
moveCursor(cursor.Row, cursor.Col)
|
|
}
|
|
|
|
func cursorBOL() {
|
|
cursor.Col = 1
|
|
moveCursor(cursor.Row, cursor.Col)
|
|
}
|
|
|
|
func cursorUp(multiplier int) {
|
|
for k := 0; k < multiplier; k++ {
|
|
cursor.Row = cursor.Row - 1
|
|
}
|
|
cursor.clampRow()
|
|
cursor.clampCol()
|
|
moveCursor(cursor.Row, cursor.Col)
|
|
}
|
|
|
|
func cursorDown(multiplier int) {
|
|
for k := 0; k < multiplier; k++ {
|
|
cursor.Row = cursor.Row + 1
|
|
}
|
|
cursor.clampRow()
|
|
cursor.clampCol()
|
|
moveCursor(cursor.Row, cursor.Col)
|
|
}
|
|
|
|
func cursorForward(multiplier int) {
|
|
for k := 0; k < multiplier; k++ {
|
|
cursor.Col = cursor.Col + 1
|
|
}
|
|
cursor.clampCol()
|
|
cursor.clampRow()
|
|
moveCursor(cursor.Row, cursor.Col)
|
|
}
|
|
|
|
func cursorForwardWord() {
|
|
// split line into array
|
|
line := buffer.fetch(cursor.Row)
|
|
line = line[cursor.Col-1:]
|
|
|
|
content := []byte(line)
|
|
pattern := regexp.MustCompile(`\W\w`)
|
|
loc := pattern.FindIndex(content)
|
|
if loc != nil {
|
|
cursor.Col = cursor.Col + loc[0] + 1
|
|
|
|
cursor.clampCol()
|
|
cursor.clampRow()
|
|
moveCursor(cursor.Row, cursor.Col)
|
|
}
|
|
}
|
|
|
|
func cursorBackward(multiplier int) {
|
|
for k := 0; k < multiplier; k++ {
|
|
cursor.Col = cursor.Col - 1
|
|
}
|
|
cursor.clampCol()
|
|
cursor.clampRow()
|
|
moveCursor(cursor.Row, cursor.Col)
|
|
}
|
|
|
|
func cursorBackwardWord() {
|
|
line := buffer.fetch(cursor.Row)
|
|
line = strings.TrimRight(line[:cursor.Col-1], " ")
|
|
|
|
content := []byte(line)
|
|
pattern := regexp.MustCompile(`\w+.?$`)
|
|
loc := pattern.FindIndex(content)
|
|
if loc != nil {
|
|
cursor.Col = loc[0] + 1
|
|
|
|
cursor.clampCol()
|
|
cursor.clampRow()
|
|
moveCursor(cursor.Row, cursor.Col)
|
|
}
|
|
}
|