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("") } 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) } }