|
@ -0,0 +1,334 @@ |
|
|
|
|
|
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) |
|
|
|
|
|
} |
|
|
|
|
|
} |