LED is a lightweight text editor written using Go.
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.

334 lines
6.3 KiB

5 years ago
  1. package main
  2. import (
  3. "bufio"
  4. "bytes"
  5. "log"
  6. "os"
  7. "editor/utils"
  8. "fmt"
  9. "golang.org/x/crypto/ssh/terminal"
  10. "regexp"
  11. "strings"
  12. )
  13. // Editor is the initializing class
  14. type Editor struct {
  15. }
  16. // Buffer is the temporary view presented to the user
  17. type Buffer struct {
  18. Lines []string
  19. }
  20. // Cursor is the object containing cursor point position
  21. type Cursor struct {
  22. Row int
  23. Col int
  24. }
  25. func clamp(max, cur int) int {
  26. if cur <= max {
  27. return cur
  28. }
  29. return max
  30. }
  31. var filePath string
  32. var editor Editor
  33. var buffer Buffer
  34. var cursor Cursor
  35. var multiplier int
  36. func main() {
  37. if len(os.Args) == 1 {
  38. log.Println("a file is required")
  39. return
  40. }
  41. filePath = os.Args[1]
  42. editor.initialize()
  43. editor.run()
  44. }
  45. func (e *Editor) initialize() {
  46. // Load a file
  47. file, err := os.Open(filePath)
  48. if err != nil {
  49. log.Fatal(err)
  50. }
  51. defer file.Close()
  52. // RAW terminal
  53. oldState, err := terminal.MakeRaw(0)
  54. if err != nil {
  55. log.Fatal(err)
  56. }
  57. defer terminal.Restore(0, oldState)
  58. var lines []string
  59. scanner := bufio.NewScanner(file)
  60. for scanner.Scan() {
  61. lines = append(lines, scanner.Text())
  62. }
  63. if err := scanner.Err(); err != nil {
  64. log.Fatal(err)
  65. }
  66. multiplier = 1
  67. buffer = buffer.new(lines)
  68. cursor = cursor.new()
  69. }
  70. func (e *Editor) run() {
  71. for {
  72. e.render()
  73. e.handleInput()
  74. }
  75. }
  76. func (e *Editor) render() {
  77. clearScreen()
  78. buffer.render()
  79. moveCursor(cursor.Row, cursor.Col) // restore cursor
  80. }
  81. func (e *Editor) handleInput() {
  82. c := utils.Getch()
  83. // log.Printf("%#v\t%s\n", c, string(c))
  84. switch {
  85. case bytes.Equal(c, []byte{0x3}), bytes.Equal(c, []byte{0x11}), bytes.Equal(c, []byte{0x18}): // C-c, C-q, C-x
  86. // quit
  87. clearScreen()
  88. moveCursor(1, 1)
  89. os.Exit(0)
  90. return
  91. case bytes.Equal(c, []byte{0x13}): // C-s
  92. // save
  93. buffer.save()
  94. case bytes.Equal(c, []byte{0x5}): // C-e
  95. // end of line
  96. cursorEOL()
  97. case bytes.Equal(c, []byte{0x1}): // C-a
  98. // beginning of line
  99. cursorBOL()
  100. case bytes.Equal(c, []byte{0x15}): // C-u
  101. if multiplier == 1 {
  102. multiplier = 4
  103. } else {
  104. multiplier = multiplier + multiplier
  105. }
  106. case bytes.Equal(c, []byte{0x1b, 0x5b, 0x41}), bytes.Equal(c, []byte{0x10}): // UP, C-p
  107. cursorUp(multiplier)
  108. multiplier = 1
  109. case bytes.Equal(c, []byte{0x1b, 0x5b, 0x42}), bytes.Equal(c, []byte{0xe}): // DOWN, C-n
  110. cursorDown(multiplier)
  111. multiplier = 1
  112. case bytes.Equal(c, []byte{0x1b, 0x5b, 0x43}), bytes.Equal(c, []byte{0x6}): // RIGHT, C-f
  113. cursorForward(multiplier)
  114. multiplier = 1
  115. case bytes.Equal(c, []byte{0x1b, 0x5b, 0x44}), bytes.Equal(c, []byte{0x2}): // LEFT, C-b
  116. cursorBackward(multiplier)
  117. multiplier = 1
  118. case bytes.Equal(c, []byte{0x1b, 0x62}): // M-b
  119. cursorBackwardWord()
  120. multiplier = 1
  121. case bytes.Equal(c, []byte{0x1b, 0x66}): // M-f
  122. cursorForwardWord()
  123. multiplier = 1
  124. case bytes.Equal(c, []byte{0x7f}): // backspace
  125. buffer.deleteChar()
  126. multiplier = 1
  127. case bytes.Equal(c, []byte{0xb}): // C-k
  128. buffer.deleteForward()
  129. multiplier = 1
  130. default:
  131. buffer.insertChar(string(c))
  132. multiplier = 1
  133. }
  134. }
  135. func (b *Buffer) new(lines []string) Buffer {
  136. b.Lines = lines
  137. return *b
  138. }
  139. func (b *Buffer) fetch(line int) string {
  140. // fix the index 1 issue
  141. if line > len(b.Lines) {
  142. return ""
  143. }
  144. return b.Lines[line-1]
  145. }
  146. func (b *Buffer) render() {
  147. moveCursor(1, 1) // reset cursor for printing buffer
  148. for _, str := range buffer.Lines {
  149. // fmt.Printf("%d| %s\r\n", num, str) // with line nums
  150. fmt.Printf("%s\r\n", str)
  151. }
  152. }
  153. func (b *Buffer) save() {
  154. // save file
  155. file, err := os.OpenFile(filePath, os.O_RDWR|os.O_CREATE, 0755)
  156. if err != nil {
  157. log.Fatal(err)
  158. }
  159. defer file.Close()
  160. out := strings.Join(b.Lines, "\n")
  161. _, err = file.WriteString(out)
  162. if err != nil {
  163. log.Fatal(err)
  164. }
  165. }
  166. func (b *Buffer) insertChar(inp string) {
  167. if len(b.Lines) < cursor.Row {
  168. b.Lines = append(b.Lines, inp)
  169. } else {
  170. b.Lines[cursor.Row-1] = strings.Join(
  171. []string{
  172. b.Lines[cursor.Row-1][:cursor.Col-1],
  173. inp,
  174. b.Lines[cursor.Row-1][cursor.Col-1:],
  175. }, "")
  176. }
  177. cursor.Col++
  178. moveCursor(cursor.Row, cursor.Col)
  179. }
  180. func (b *Buffer) deleteChar() {
  181. if cursor.Col == 1 {
  182. return
  183. }
  184. b.Lines[cursor.Row-1] = strings.Join(
  185. []string{
  186. b.Lines[cursor.Row-1][:cursor.Col-2],
  187. b.Lines[cursor.Row-1][cursor.Col-1:],
  188. }, "")
  189. cursor.Col--
  190. moveCursor(cursor.Row, cursor.Col)
  191. }
  192. func (b *Buffer) deleteForward() {
  193. b.Lines[cursor.Row-1] = b.Lines[cursor.Row-1][:cursor.Col-1]
  194. }
  195. func (c *Cursor) new() Cursor {
  196. return Cursor{
  197. 1,
  198. 1,
  199. }
  200. }
  201. func (c *Cursor) clampCol() {
  202. line := buffer.fetch(c.Row)
  203. if c.Col > len(line) {
  204. c.Col = len(line) + 1
  205. } else if c.Col < 1 {
  206. c.Col = 1
  207. }
  208. }
  209. func (c *Cursor) clampRow() {
  210. rows := len(buffer.Lines)
  211. if c.Row > rows+1 {
  212. c.Row = rows + 1
  213. } else if c.Row < 1 {
  214. c.Row = 1
  215. }
  216. }
  217. //
  218. //
  219. //
  220. func clearScreen() {
  221. fmt.Print("")
  222. }
  223. func moveCursor(row, col int) {
  224. fmt.Printf("[%d;%dH", row, col)
  225. }
  226. func cursorEOL() {
  227. line := buffer.fetch(cursor.Row)
  228. cursor.Col = len(line) + 1
  229. moveCursor(cursor.Row, cursor.Col)
  230. }
  231. func cursorBOL() {
  232. cursor.Col = 1
  233. moveCursor(cursor.Row, cursor.Col)
  234. }
  235. func cursorUp(multiplier int) {
  236. for k := 0; k < multiplier; k++ {
  237. cursor.Row = cursor.Row - 1
  238. }
  239. cursor.clampRow()
  240. cursor.clampCol()
  241. moveCursor(cursor.Row, cursor.Col)
  242. }
  243. func cursorDown(multiplier int) {
  244. for k := 0; k < multiplier; k++ {
  245. cursor.Row = cursor.Row + 1
  246. }
  247. cursor.clampRow()
  248. cursor.clampCol()
  249. moveCursor(cursor.Row, cursor.Col)
  250. }
  251. func cursorForward(multiplier int) {
  252. for k := 0; k < multiplier; k++ {
  253. cursor.Col = cursor.Col + 1
  254. }
  255. cursor.clampCol()
  256. cursor.clampRow()
  257. moveCursor(cursor.Row, cursor.Col)
  258. }
  259. func cursorForwardWord() {
  260. // split line into array
  261. line := buffer.fetch(cursor.Row)
  262. line = line[cursor.Col-1:]
  263. content := []byte(line)
  264. pattern := regexp.MustCompile(`\W\w`)
  265. loc := pattern.FindIndex(content)
  266. if loc != nil {
  267. cursor.Col = cursor.Col + loc[0] + 1
  268. cursor.clampCol()
  269. cursor.clampRow()
  270. moveCursor(cursor.Row, cursor.Col)
  271. }
  272. }
  273. func cursorBackward(multiplier int) {
  274. for k := 0; k < multiplier; k++ {
  275. cursor.Col = cursor.Col - 1
  276. }
  277. cursor.clampCol()
  278. cursor.clampRow()
  279. moveCursor(cursor.Row, cursor.Col)
  280. }
  281. func cursorBackwardWord() {
  282. line := buffer.fetch(cursor.Row)
  283. line = strings.TrimRight(line[:cursor.Col-1], " ")
  284. content := []byte(line)
  285. pattern := regexp.MustCompile(`\w+.?$`)
  286. loc := pattern.FindIndex(content)
  287. if loc != nil {
  288. cursor.Col = loc[0] + 1
  289. cursor.clampCol()
  290. cursor.clampRow()
  291. moveCursor(cursor.Row, cursor.Col)
  292. }
  293. }