12 "github.com/gdamore/tcell"
13 "github.com/mitchellh/go-homedir"
14 "github.com/zyedidia/clipboard"
17 var bindings map[tcell.Key]func(*View) bool
19 // InitBindings initializes the keybindings for micro
21 bindings = make(map[tcell.Key]func(*View) bool)
23 actions := map[string]func(*View) bool{
25 "CursorDown": CursorDown,
26 "CursorLeft": CursorLeft,
27 "CursorRight": CursorRight,
28 "InsertEnter": InsertEnter,
29 "InsertSpace": InsertSpace,
30 "Backspace": Backspace,
32 "InsertTab": InsertTab,
36 "FindPrevious": FindPrevious,
43 "SelectAll": SelectAll,
45 "Beginning": Beginning,
49 "HalfPageUp": HalfPageUp,
50 "HalfPageDown": HalfPageDown,
51 "StartOfLine": StartOfLine,
52 "EndOfLine": EndOfLine,
53 "ToggleRuler": ToggleRuler,
56 keys := map[string]tcell.Key{
58 "Down": tcell.KeyDown,
59 "Right": tcell.KeyRight,
60 "Left": tcell.KeyLeft,
61 "UpLeft": tcell.KeyUpLeft,
62 "UpRight": tcell.KeyUpRight,
63 "DownLeft": tcell.KeyDownLeft,
64 "DownRight": tcell.KeyDownRight,
65 "Center": tcell.KeyCenter,
66 "PgUp": tcell.KeyPgUp,
67 "PgDn": tcell.KeyPgDn,
68 "Home": tcell.KeyHome,
70 "Insert": tcell.KeyInsert,
71 "Delete": tcell.KeyDelete,
72 "Help": tcell.KeyHelp,
73 "Exit": tcell.KeyExit,
74 "Clear": tcell.KeyClear,
75 "Cancel": tcell.KeyCancel,
76 "Print": tcell.KeyPrint,
77 "Pause": tcell.KeyPause,
78 "Backtab": tcell.KeyBacktab,
143 "CtrlSpace": tcell.KeyCtrlSpace,
144 "CtrlA": tcell.KeyCtrlA,
145 "CtrlB": tcell.KeyCtrlB,
146 "CtrlC": tcell.KeyCtrlC,
147 "CtrlD": tcell.KeyCtrlD,
148 "CtrlE": tcell.KeyCtrlE,
149 "CtrlF": tcell.KeyCtrlF,
150 "CtrlG": tcell.KeyCtrlG,
151 "CtrlH": tcell.KeyCtrlH,
152 "CtrlI": tcell.KeyCtrlI,
153 "CtrlJ": tcell.KeyCtrlJ,
154 "CtrlK": tcell.KeyCtrlK,
155 "CtrlL": tcell.KeyCtrlL,
156 "CtrlM": tcell.KeyCtrlM,
157 "CtrlN": tcell.KeyCtrlN,
158 "CtrlO": tcell.KeyCtrlO,
159 "CtrlP": tcell.KeyCtrlP,
160 "CtrlQ": tcell.KeyCtrlQ,
161 "CtrlR": tcell.KeyCtrlR,
162 "CtrlS": tcell.KeyCtrlS,
163 "CtrlT": tcell.KeyCtrlT,
164 "CtrlU": tcell.KeyCtrlU,
165 "CtrlV": tcell.KeyCtrlV,
166 "CtrlW": tcell.KeyCtrlW,
167 "CtrlX": tcell.KeyCtrlX,
168 "CtrlY": tcell.KeyCtrlY,
169 "CtrlZ": tcell.KeyCtrlZ,
170 "CtrlLeftSq": tcell.KeyCtrlLeftSq,
171 "CtrlBackslash": tcell.KeyCtrlBackslash,
172 "CtrlRightSq": tcell.KeyCtrlRightSq,
173 "CtrlCarat": tcell.KeyCtrlCarat,
174 "CtrlUnderscore": tcell.KeyCtrlUnderscore,
175 "Backspace": tcell.KeyBackspace,
178 "Escape": tcell.KeyEscape,
179 "Enter": tcell.KeyEnter,
180 "Space": tcell.KeySpace,
181 "Backspace2": tcell.KeyBackspace2,
184 var parsed map[string]string
185 defaults := DefaultBindings()
187 filename := configDir + "/bindings.json"
188 if _, e := os.Stat(filename); e == nil {
189 input, err := ioutil.ReadFile(filename)
191 TermMessage("Error reading bindings.json file: " + err.Error())
195 err = json.Unmarshal(input, &parsed)
197 TermMessage("Error reading bindings.json:", err.Error())
201 for k, v := range defaults {
202 bindings[keys[k]] = actions[v]
204 for k, v := range parsed {
205 bindings[keys[k]] = actions[v]
209 // DefaultBindings returns a map containing micro's default keybindings
210 func DefaultBindings() map[string]string {
211 return map[string]string{
213 "Down": "CursorDown",
214 "Right": "CursorRight",
215 "Left": "CursorLeft",
216 "Enter": "InsertEnter",
217 "Space": "InsertSpace",
218 "Backspace": "Backspace",
219 "Backspace2": "Backspace",
225 "CtrlP": "FindPrevious",
232 "CtrlA": "SelectAll",
237 "CtrlU": "HalfPageUp",
238 "CtrlD": "HalfPageDown",
239 "CtrlR": "ToggleRuler",
244 // CursorUp moves the cursor up
245 func CursorUp(v *View) bool {
246 v.cursor.ResetSelection()
251 // CursorDown moves the cursor down
252 func CursorDown(v *View) bool {
253 v.cursor.ResetSelection()
258 // CursorLeft moves the cursor left
259 func CursorLeft(v *View) bool {
260 v.cursor.ResetSelection()
265 // CursorRight moves the cursor right
266 func CursorRight(v *View) bool {
267 v.cursor.ResetSelection()
272 // InsertSpace inserts a space
273 func InsertSpace(v *View) bool {
275 if v.cursor.HasSelection() {
276 v.cursor.DeleteSelection()
277 v.cursor.ResetSelection()
279 v.eh.Insert(v.cursor.Loc(), " ")
284 // InsertEnter inserts a newline plus possible some whitespace if autoindent is on
285 func InsertEnter(v *View) bool {
287 if v.cursor.HasSelection() {
288 v.cursor.DeleteSelection()
289 v.cursor.ResetSelection()
292 v.eh.Insert(v.cursor.Loc(), "\n")
293 ws := GetLeadingWhitespace(v.buf.lines[v.cursor.y])
296 if settings.AutoIndent {
297 v.eh.Insert(v.cursor.Loc(), ws)
298 for i := 0; i < len(ws); i++ {
302 v.cursor.lastVisualX = v.cursor.GetVisualX()
306 // Backspace deletes the previous character
307 func Backspace(v *View) bool {
308 // Delete a character
309 if v.cursor.HasSelection() {
310 v.cursor.DeleteSelection()
311 v.cursor.ResetSelection()
312 } else if v.cursor.Loc() > 0 {
313 // We have to do something a bit hacky here because we want to
314 // delete the line by first moving left and then deleting backwards
315 // but the undo redo would place the cursor in the wrong place
316 // So instead we move left, save the position, move back, delete
317 // and restore the position
319 // If the user is using spaces instead of tabs and they are deleting
320 // whitespace at the start of the line, we should delete as if its a
321 // tab (tabSize number of spaces)
322 lineStart := v.buf.lines[v.cursor.y][:v.cursor.x]
323 if settings.TabsToSpaces && IsSpaces(lineStart) && len(lineStart) != 0 && len(lineStart)%settings.TabSize == 0 {
324 loc := v.cursor.Loc()
325 v.cursor.SetLoc(loc - settings.TabSize)
326 cx, cy := v.cursor.x, v.cursor.y
328 v.eh.Remove(loc-settings.TabSize, loc)
329 v.cursor.x, v.cursor.y = cx, cy
332 cx, cy := v.cursor.x, v.cursor.y
334 loc := v.cursor.Loc()
335 v.eh.Remove(loc-1, loc)
336 v.cursor.x, v.cursor.y = cx, cy
339 v.cursor.lastVisualX = v.cursor.GetVisualX()
343 // Delete deletes the next character
344 func Delete(v *View) bool {
345 if v.cursor.HasSelection() {
346 v.cursor.DeleteSelection()
347 v.cursor.ResetSelection()
349 loc := v.cursor.Loc()
350 if loc < len(v.buf.text) {
351 v.eh.Remove(loc, loc+1)
357 // InsertTab inserts a tab or spaces
358 func InsertTab(v *View) bool {
360 if v.cursor.HasSelection() {
361 v.cursor.DeleteSelection()
362 v.cursor.ResetSelection()
364 if settings.TabsToSpaces {
365 v.eh.Insert(v.cursor.Loc(), Spaces(settings.TabSize))
366 for i := 0; i < settings.TabSize; i++ {
370 v.eh.Insert(v.cursor.Loc(), "\t")
376 // Save the buffer to disk
377 func Save(v *View) bool {
378 // If this is an empty buffer, ask for a filename
379 if v.buf.path == "" {
380 filename, canceled := messenger.Prompt("Filename: ")
382 v.buf.path = filename
383 v.buf.name = filename
390 messenger.Error(err.Error())
392 messenger.Message("Saved " + v.buf.path)
393 switch v.buf.filetype {
401 // GoSave saves the current file (must be a go file) and runs goimports or gofmt
402 // depending on the user's configuration
403 func GoSave(v *View) {
404 if settings.GoImports == true {
405 messenger.Message("Running goimports...")
406 err := goimports(v.buf.path)
410 messenger.Message("Saved " + v.buf.path)
413 } else if settings.GoFmt == true {
414 messenger.Message("Running gofmt...")
415 err := gofmt(v.buf.path)
419 messenger.Message("Saved " + v.buf.path)
428 // Find opens a prompt and searches forward for the input
429 func Find(v *View) bool {
430 if v.cursor.HasSelection() {
431 searchStart = v.cursor.curSelection[1]
433 searchStart = ToCharPos(v.cursor.x, v.cursor.y, v.buf)
439 // FindNext searches forwards for the last used search term
440 func FindNext(v *View) bool {
441 if v.cursor.HasSelection() {
442 searchStart = v.cursor.curSelection[1]
444 searchStart = ToCharPos(v.cursor.x, v.cursor.y, v.buf)
446 messenger.Message("Find: " + lastSearch)
447 Search(lastSearch, v, true)
451 // FindPrevious searches backwards for the last used search term
452 func FindPrevious(v *View) bool {
453 if v.cursor.HasSelection() {
454 searchStart = v.cursor.curSelection[0]
456 searchStart = ToCharPos(v.cursor.x, v.cursor.y, v.buf)
458 messenger.Message("Find: " + lastSearch)
459 Search(lastSearch, v, false)
463 // Undo undoes the last action
464 func Undo(v *View) bool {
469 // Redo redoes the last action
470 func Redo(v *View) bool {
475 // Copy the selection to the system clipboard
476 func Copy(v *View) bool {
477 if v.cursor.HasSelection() {
478 clipboard.WriteAll(v.cursor.GetSelection())
484 // CutLine cuts the current line to the clipboard
485 func CutLine(v *View) bool {
486 v.cursor.SelectLine()
487 if v.freshClip == true {
489 if v.cursor.HasSelection() {
490 if clip, err := clipboard.ReadAll(); err != nil {
493 clipboard.WriteAll(clip + v.cursor.GetSelection())
496 } else if time.Since(v.lastCutTime)/time.Second > 10*time.Second || v.freshClip == false {
500 v.lastCutTime = time.Now()
501 v.cursor.DeleteSelection()
502 v.cursor.ResetSelection()
506 // Cut the selection to the system clipboard
507 func Cut(v *View) bool {
508 if v.cursor.HasSelection() {
509 clipboard.WriteAll(v.cursor.GetSelection())
510 v.cursor.DeleteSelection()
511 v.cursor.ResetSelection()
517 // Paste whatever is in the system clipboard into the buffer
518 // Delete and paste if the user has a selection
519 func Paste(v *View) bool {
520 if v.cursor.HasSelection() {
521 v.cursor.DeleteSelection()
522 v.cursor.ResetSelection()
524 clip, _ := clipboard.ReadAll()
525 v.eh.Insert(v.cursor.Loc(), clip)
526 v.cursor.SetLoc(v.cursor.Loc() + Count(clip))
531 // SelectAll selects the entire buffer
532 func SelectAll(v *View) bool {
533 v.cursor.curSelection[1] = 0
534 v.cursor.curSelection[0] = v.buf.Len()
535 // Put the cursor at the beginning
541 // OpenFile opens a new file in the buffer
542 func OpenFile(v *View) bool {
543 if v.CanClose("Continue? (yes, no, save) ") {
544 filename, canceled := messenger.Prompt("File to open: ")
548 home, _ := homedir.Dir()
549 filename = strings.Replace(filename, "~", home, 1)
550 file, err := ioutil.ReadFile(filename)
553 messenger.Error(err.Error())
556 buf := NewBuffer(string(file), filename)
562 // Beginning moves the viewport to the start of the buffer
563 func Beginning(v *View) bool {
568 // End moves the viewport to the end of the buffer
569 func End(v *View) bool {
570 if v.height > len(v.buf.lines) {
573 v.topline = len(v.buf.lines) - v.height
578 // PageUp scrolls the view up a page
579 func PageUp(v *View) bool {
580 if v.topline > v.height {
588 // PageDown scrolls the view down a page
589 func PageDown(v *View) bool {
590 if len(v.buf.lines)-(v.topline+v.height) > v.height {
591 v.ScrollDown(v.height)
592 } else if len(v.buf.lines) >= v.height {
593 v.topline = len(v.buf.lines) - v.height
598 // HalfPageUp scrolls the view up half a page
599 func HalfPageUp(v *View) bool {
600 if v.topline > v.height/2 {
601 v.ScrollUp(v.height / 2)
608 // HalfPageDown scrolls the view down half a page
609 func HalfPageDown(v *View) bool {
610 if len(v.buf.lines)-(v.topline+v.height) > v.height/2 {
611 v.ScrollDown(v.height / 2)
613 if len(v.buf.lines) >= v.height {
614 v.topline = len(v.buf.lines) - v.height
620 // ToggleRuler turns line numbers off and on
621 func ToggleRuler(v *View) bool {
622 if settings.Ruler == false {
623 settings.Ruler = true
625 settings.Ruler = false
630 // StartOfLine moves the cursor to the start of the line
631 func StartOfLine(v *View) bool {
636 // EndOfLine moves the cursor to the end of the line
637 func EndOfLine(v *View) bool {
647 // gofmt runs gofmt on a file
648 func gofmt(file string) error {
649 cmd := exec.Command("gofmt", "-w", file)
653 return errors.New("Check syntax ") //TODO: highlight or display locations
658 // goimports runs goimports on a file
659 func goimports(file string) error {
660 cmd := exec.Command("goimports", "-w", file)
664 return errors.New("Check syntax ") //TODO: highlight or display locations