]> git.lizzy.rs Git - micro.git/blob - internal/action/bufpane.go
Merge pull request #1437 from serebit/patch-2
[micro.git] / internal / action / bufpane.go
1 package action
2
3 import (
4         "strings"
5         "time"
6
7         luar "layeh.com/gopher-luar"
8
9         lua "github.com/yuin/gopher-lua"
10         "github.com/zyedidia/micro/internal/buffer"
11         "github.com/zyedidia/micro/internal/config"
12         "github.com/zyedidia/micro/internal/display"
13         ulua "github.com/zyedidia/micro/internal/lua"
14         "github.com/zyedidia/micro/internal/screen"
15         "github.com/zyedidia/tcell"
16 )
17
18 type BufKeyAction func(*BufPane) bool
19 type BufMouseAction func(*BufPane, *tcell.EventMouse) bool
20
21 var BufKeyBindings map[Event]BufKeyAction
22 var BufKeyStrings map[Event]string
23 var BufMouseBindings map[MouseEvent]BufMouseAction
24
25 func init() {
26         BufKeyBindings = make(map[Event]BufKeyAction)
27         BufKeyStrings = make(map[Event]string)
28         BufMouseBindings = make(map[MouseEvent]BufMouseAction)
29 }
30
31 func LuaAction(fn string) func(*BufPane) bool {
32         luaFn := strings.Split(fn, ".")
33         if len(luaFn) <= 1 {
34                 return nil
35         }
36         plName, plFn := luaFn[0], luaFn[1]
37         pl := config.FindPlugin(plName)
38         if pl == nil {
39                 return nil
40         }
41         return func(h *BufPane) bool {
42                 val, err := pl.Call(plFn, luar.New(ulua.L, h))
43                 if err != nil {
44                         screen.TermMessage(err)
45                 }
46                 if v, ok := val.(lua.LBool); !ok {
47                         return false
48                 } else {
49                         return bool(v)
50                 }
51         }
52 }
53
54 // BufMapKey maps a key event to an action
55 func BufMapKey(k Event, action string) {
56         BufKeyStrings[k] = action
57         var actionfns []func(*BufPane) bool
58         var names []string
59         var types []byte
60         for i := 0; ; i++ {
61                 if action == "" {
62                         break
63                 }
64
65                 // TODO: fix problem when complex bindings have these
66                 // characters (escape them?)
67                 idx := strings.IndexAny(action, "&|,")
68                 a := action
69                 if idx >= 0 {
70                         a = action[:idx]
71                         types = append(types, action[idx])
72                         action = action[idx+1:]
73                 } else {
74                         types = append(types, ' ')
75                         action = ""
76                 }
77
78                 var afn func(*BufPane) bool
79                 if strings.HasPrefix(a, "command:") {
80                         a = strings.SplitN(a, ":", 2)[1]
81                         afn = CommandAction(a)
82                         names = append(names, "")
83                 } else if strings.HasPrefix(a, "command-edit:") {
84                         a = strings.SplitN(a, ":", 2)[1]
85                         afn = CommandEditAction(a)
86                         names = append(names, "")
87                 } else if strings.HasPrefix(a, "lua:") {
88                         a = strings.SplitN(a, ":", 2)[1]
89                         afn = LuaAction(a)
90                         if afn == nil {
91                                 screen.TermMessage("Lua Error:", a, "does not exist")
92                                 continue
93                         }
94                         names = append(names, "")
95                 } else if f, ok := BufKeyActions[a]; ok {
96                         afn = f
97                         names = append(names, a)
98                 } else {
99                         screen.TermMessage("Error:", a, "does not exist")
100                         continue
101                 }
102                 actionfns = append(actionfns, afn)
103         }
104         BufKeyBindings[k] = func(h *BufPane) bool {
105                 cursors := h.Buf.GetCursors()
106                 success := true
107                 for i, a := range actionfns {
108                         for j, c := range cursors {
109                                 h.Buf.SetCurCursor(c.Num)
110                                 h.Cursor = c
111                                 if i == 0 || (success && types[i-1] == '&') || (!success && types[i-1] == '|') || (types[i-1] == ',') {
112                                         success = h.execAction(a, names[i], j)
113                                 } else {
114                                         break
115                                 }
116                         }
117                 }
118                 return true
119         }
120 }
121
122 // BufMapMouse maps a mouse event to an action
123 func BufMapMouse(k MouseEvent, action string) {
124         if f, ok := BufMouseActions[action]; ok {
125                 BufMouseBindings[k] = f
126         } else {
127                 delete(BufMouseBindings, k)
128                 BufMapKey(k, action)
129         }
130 }
131
132 // The BufPane connects the buffer and the window
133 // It provides a cursor (or multiple) and defines a set of actions
134 // that can be taken on the buffer
135 // The ActionHandler can access the window for necessary info about
136 // visual positions for mouse clicks and scrolling
137 type BufPane struct {
138         display.BWindow
139
140         Buf *buffer.Buffer
141
142         Cursor *buffer.Cursor // the active cursor
143
144         // Since tcell doesn't differentiate between a mouse release event
145         // and a mouse move event with no keys pressed, we need to keep
146         // track of whether or not the mouse was pressed (or not released) last event to determine
147         // mouse release events
148         mouseReleased bool
149
150         // We need to keep track of insert key press toggle
151         isOverwriteMode bool
152         // This stores when the last click was
153         // This is useful for detecting double and triple clicks
154         lastClickTime time.Time
155         lastLoc       buffer.Loc
156
157         // lastCutTime stores when the last ctrl+k was issued.
158         // It is used for clearing the clipboard to replace it with fresh cut lines.
159         lastCutTime time.Time
160
161         // freshClip returns true if the clipboard has never been pasted.
162         freshClip bool
163
164         // Was the last mouse event actually a double click?
165         // Useful for detecting triple clicks -- if a double click is detected
166         // but the last mouse event was actually a double click, it's a triple click
167         doubleClick bool
168         // Same here, just to keep track for mouse move events
169         tripleClick bool
170
171         // Last search stores the last successful search for FindNext and FindPrev
172         lastSearch string
173         // Should the current multiple cursor selection search based on word or
174         // based on selection (false for selection, true for word)
175         multiWord bool
176
177         splitID uint64
178
179         // remember original location of a search in case the search is canceled
180         searchOrig buffer.Loc
181 }
182
183 func NewBufPane(buf *buffer.Buffer, win display.BWindow) *BufPane {
184         h := new(BufPane)
185         h.Buf = buf
186         h.BWindow = win
187
188         h.Cursor = h.Buf.GetActiveCursor()
189         h.mouseReleased = true
190
191         config.RunPluginFn("onBufPaneOpen", luar.New(ulua.L, h))
192
193         return h
194 }
195
196 func NewBufPaneFromBuf(buf *buffer.Buffer) *BufPane {
197         w := display.NewBufWindow(0, 0, 0, 0, buf)
198         return NewBufPane(buf, w)
199 }
200
201 // PluginCB calls all plugin callbacks with a certain name and
202 // displays an error if there is one and returns the aggregrate
203 // boolean response
204 func (h *BufPane) PluginCB(cb string) bool {
205         b, err := config.RunPluginFnBool(cb, luar.New(ulua.L, h))
206         if err != nil {
207                 screen.TermMessage(err)
208         }
209         return b
210 }
211
212 // PluginCBRune is the same as PluginCB but also passes a rune to
213 // the plugins
214 func (h *BufPane) PluginCBRune(cb string, r rune) bool {
215         b, err := config.RunPluginFnBool(cb, luar.New(ulua.L, h), luar.New(ulua.L, string(r)))
216         if err != nil {
217                 screen.TermMessage(err)
218         }
219         return b
220 }
221
222 func (h *BufPane) OpenBuffer(b *buffer.Buffer) {
223         h.Buf.Close()
224         h.Buf = b
225         h.BWindow.SetBuffer(b)
226         h.Cursor = b.GetActiveCursor()
227         h.Resize(h.GetView().Width, h.GetView().Height)
228         v := new(display.View)
229         h.SetView(v)
230         h.Relocate()
231         // Set mouseReleased to true because we assume the mouse is not being pressed when
232         // the editor is opened
233         h.mouseReleased = true
234         // Set isOverwriteMode to false, because we assume we are in the default mode when editor
235         // is opened
236         h.isOverwriteMode = false
237         h.lastClickTime = time.Time{}
238 }
239
240 func (h *BufPane) ID() uint64 {
241         return h.splitID
242 }
243
244 func (h *BufPane) SetID(i uint64) {
245         h.splitID = i
246 }
247
248 func (h *BufPane) Name() string {
249         return h.Buf.GetName()
250 }
251
252 // HandleEvent executes the tcell event properly
253 func (h *BufPane) HandleEvent(event tcell.Event) {
254         if h.Buf.ExternallyModified() {
255                 InfoBar.YNPrompt("The file on disk has changed. Reload file? (y,n)", func(yes, canceled bool) {
256                         if !yes || canceled {
257                                 h.Buf.UpdateModTime()
258                         } else {
259                                 h.Buf.ReOpen()
260                         }
261                 })
262
263         }
264
265         switch e := event.(type) {
266         case *tcell.EventRaw:
267                 re := RawEvent{
268                         esc: e.EscSeq(),
269                 }
270                 h.DoKeyEvent(re)
271         case *tcell.EventPaste:
272                 h.paste(e.Text())
273                 h.Relocate()
274         case *tcell.EventKey:
275                 ke := KeyEvent{
276                         code: e.Key(),
277                         mod:  e.Modifiers(),
278                         r:    e.Rune(),
279                 }
280
281                 done := h.DoKeyEvent(ke)
282                 if !done && e.Key() == tcell.KeyRune {
283                         h.DoRuneInsert(e.Rune())
284                 }
285         case *tcell.EventMouse:
286                 cancel := false
287                 switch e.Buttons() {
288                 case tcell.Button1:
289                         _, my := e.Position()
290                         if h.Buf.Settings["statusline"].(bool) && my >= h.GetView().Y+h.GetView().Height-1 {
291                                 cancel = true
292                         }
293                 case tcell.ButtonNone:
294                         // Mouse event with no click
295                         if !h.mouseReleased {
296                                 // Mouse was just released
297
298                                 // mx, my := e.Position()
299                                 // mouseLoc := h.LocFromVisual(buffer.Loc{X: mx, Y: my})
300
301                                 // we could finish the selection based on the release location as described
302                                 // below but when the mouse click is within the scroll margin this will
303                                 // cause a scroll and selection even for a simple mouse click which is
304                                 // not good
305                                 // for terminals that don't support mouse motion events, selection via
306                                 // the mouse won't work but this is ok
307
308                                 // Relocating here isn't really necessary because the cursor will
309                                 // be in the right place from the last mouse event
310                                 // However, if we are running in a terminal that doesn't support mouse motion
311                                 // events, this still allows the user to make selections, except only after they
312                                 // release the mouse
313
314                                 // if !h.doubleClick && !h.tripleClick {
315                                 //      h.Cursor.Loc = mouseLoc
316                                 //      h.Cursor.SetSelectionEnd(h.Cursor.Loc)
317                                 //      h.Cursor.CopySelection("primary")
318                                 // }
319                                 h.mouseReleased = true
320                         }
321                 }
322
323                 if !cancel {
324                         me := MouseEvent{
325                                 btn: e.Buttons(),
326                                 mod: e.Modifiers(),
327                         }
328                         h.DoMouseEvent(me, e)
329                 }
330         }
331         h.Buf.MergeCursors()
332
333         if h.IsActive() {
334                 // Display any gutter messages for this line
335                 c := h.Buf.GetActiveCursor()
336                 none := true
337                 for _, m := range h.Buf.Messages {
338                         if c.Y == m.Start.Y || c.Y == m.End.Y {
339                                 InfoBar.GutterMessage(m.Msg)
340                                 none = false
341                                 break
342                         }
343                 }
344                 if none && InfoBar.HasGutter {
345                         InfoBar.ClearGutter()
346                 }
347         }
348 }
349
350 // DoKeyEvent executes a key event by finding the action it is bound
351 // to and executing it (possibly multiple times for multiple cursors)
352 func (h *BufPane) DoKeyEvent(e Event) bool {
353         if action, ok := BufKeyBindings[e]; ok {
354                 return action(h)
355         }
356         return false
357 }
358
359 func (h *BufPane) execAction(action func(*BufPane) bool, name string, cursor int) bool {
360         if name != "Autocomplete" {
361                 h.Buf.HasSuggestions = false
362         }
363
364         _, isMulti := MultiActions[name]
365         if (!isMulti && cursor == 0) || isMulti {
366                 if h.PluginCB("pre" + name) {
367                         success := action(h)
368                         success = success && h.PluginCB("on"+name)
369
370                         if isMulti {
371                                 if recording_macro {
372                                         if name != "ToggleMacro" && name != "PlayMacro" {
373                                                 curmacro = append(curmacro, action)
374                                         }
375                                 }
376                         }
377
378                         return success
379                 }
380         }
381
382         return false
383 }
384
385 func (h *BufPane) completeAction(action string) {
386         h.PluginCB("on" + action)
387 }
388
389 func (h *BufPane) HasKeyEvent(e Event) bool {
390         _, ok := BufKeyBindings[e]
391         return ok
392 }
393
394 // DoMouseEvent executes a mouse event by finding the action it is bound
395 // to and executing it
396 func (h *BufPane) DoMouseEvent(e MouseEvent, te *tcell.EventMouse) bool {
397         if action, ok := BufMouseBindings[e]; ok {
398                 if action(h, te) {
399                         h.Relocate()
400                 }
401                 return true
402         } else if h.HasKeyEvent(e) {
403                 return h.DoKeyEvent(e)
404         }
405         return false
406 }
407
408 // DoRuneInsert inserts a given rune into the current buffer
409 // (possibly multiple times for multiple cursors)
410 func (h *BufPane) DoRuneInsert(r rune) {
411         cursors := h.Buf.GetCursors()
412         for _, c := range cursors {
413                 // Insert a character
414                 h.Buf.SetCurCursor(c.Num)
415                 h.Cursor = c
416                 if !h.PluginCBRune("preRune", r) {
417                         continue
418                 }
419                 if c.HasSelection() {
420                         c.DeleteSelection()
421                         c.ResetSelection()
422                 }
423
424                 if h.isOverwriteMode {
425                         next := c.Loc
426                         next.X++
427                         h.Buf.Replace(c.Loc, next, string(r))
428                 } else {
429                         h.Buf.Insert(c.Loc, string(r))
430                 }
431                 if recording_macro {
432                         curmacro = append(curmacro, r)
433                 }
434                 h.PluginCBRune("onRune", r)
435         }
436 }
437
438 func (h *BufPane) VSplitBuf(buf *buffer.Buffer) *BufPane {
439         e := NewBufPaneFromBuf(buf)
440         e.splitID = MainTab().GetNode(h.splitID).VSplit(h.Buf.Settings["splitright"].(bool))
441         MainTab().Panes = append(MainTab().Panes, e)
442         MainTab().Resize()
443         MainTab().SetActive(len(MainTab().Panes) - 1)
444         return e
445 }
446 func (h *BufPane) HSplitBuf(buf *buffer.Buffer) *BufPane {
447         e := NewBufPaneFromBuf(buf)
448         e.splitID = MainTab().GetNode(h.splitID).HSplit(h.Buf.Settings["splitbottom"].(bool))
449         MainTab().Panes = append(MainTab().Panes, e)
450         MainTab().Resize()
451         MainTab().SetActive(len(MainTab().Panes) - 1)
452         return e
453 }
454 func (h *BufPane) Close() {
455         h.Buf.Close()
456 }
457
458 func (h *BufPane) SetActive(b bool) {
459         h.BWindow.SetActive(b)
460         if b {
461                 // Display any gutter messages for this line
462                 c := h.Buf.GetActiveCursor()
463                 none := true
464                 for _, m := range h.Buf.Messages {
465                         if c.Y == m.Start.Y || c.Y == m.End.Y {
466                                 InfoBar.GutterMessage(m.Msg)
467                                 none = false
468                                 break
469                         }
470                 }
471                 if none && InfoBar.HasGutter {
472                         InfoBar.ClearGutter()
473                 }
474         }
475
476 }
477
478 // BufKeyActions contains the list of all possible key actions the bufhandler could execute
479 var BufKeyActions = map[string]BufKeyAction{
480         "CursorUp":               (*BufPane).CursorUp,
481         "CursorDown":             (*BufPane).CursorDown,
482         "CursorPageUp":           (*BufPane).CursorPageUp,
483         "CursorPageDown":         (*BufPane).CursorPageDown,
484         "CursorLeft":             (*BufPane).CursorLeft,
485         "CursorRight":            (*BufPane).CursorRight,
486         "CursorStart":            (*BufPane).CursorStart,
487         "CursorEnd":              (*BufPane).CursorEnd,
488         "SelectToStart":          (*BufPane).SelectToStart,
489         "SelectToEnd":            (*BufPane).SelectToEnd,
490         "SelectUp":               (*BufPane).SelectUp,
491         "SelectDown":             (*BufPane).SelectDown,
492         "SelectLeft":             (*BufPane).SelectLeft,
493         "SelectRight":            (*BufPane).SelectRight,
494         "WordRight":              (*BufPane).WordRight,
495         "WordLeft":               (*BufPane).WordLeft,
496         "SelectWordRight":        (*BufPane).SelectWordRight,
497         "SelectWordLeft":         (*BufPane).SelectWordLeft,
498         "DeleteWordRight":        (*BufPane).DeleteWordRight,
499         "DeleteWordLeft":         (*BufPane).DeleteWordLeft,
500         "SelectLine":             (*BufPane).SelectLine,
501         "SelectToStartOfLine":    (*BufPane).SelectToStartOfLine,
502         "SelectToEndOfLine":      (*BufPane).SelectToEndOfLine,
503         "ParagraphPrevious":      (*BufPane).ParagraphPrevious,
504         "ParagraphNext":          (*BufPane).ParagraphNext,
505         "InsertNewline":          (*BufPane).InsertNewline,
506         "Backspace":              (*BufPane).Backspace,
507         "Delete":                 (*BufPane).Delete,
508         "InsertTab":              (*BufPane).InsertTab,
509         "Save":                   (*BufPane).Save,
510         "SaveAll":                (*BufPane).SaveAll,
511         "SaveAs":                 (*BufPane).SaveAs,
512         "Find":                   (*BufPane).Find,
513         "FindNext":               (*BufPane).FindNext,
514         "FindPrevious":           (*BufPane).FindPrevious,
515         "Center":                 (*BufPane).Center,
516         "Undo":                   (*BufPane).Undo,
517         "Redo":                   (*BufPane).Redo,
518         "Copy":                   (*BufPane).Copy,
519         "Cut":                    (*BufPane).Cut,
520         "CutLine":                (*BufPane).CutLine,
521         "DuplicateLine":          (*BufPane).DuplicateLine,
522         "DeleteLine":             (*BufPane).DeleteLine,
523         "MoveLinesUp":            (*BufPane).MoveLinesUp,
524         "MoveLinesDown":          (*BufPane).MoveLinesDown,
525         "IndentSelection":        (*BufPane).IndentSelection,
526         "OutdentSelection":       (*BufPane).OutdentSelection,
527         "Autocomplete":           (*BufPane).Autocomplete,
528         "OutdentLine":            (*BufPane).OutdentLine,
529         "Paste":                  (*BufPane).Paste,
530         "PastePrimary":           (*BufPane).PastePrimary,
531         "SelectAll":              (*BufPane).SelectAll,
532         "OpenFile":               (*BufPane).OpenFile,
533         "Start":                  (*BufPane).Start,
534         "End":                    (*BufPane).End,
535         "PageUp":                 (*BufPane).PageUp,
536         "PageDown":               (*BufPane).PageDown,
537         "SelectPageUp":           (*BufPane).SelectPageUp,
538         "SelectPageDown":         (*BufPane).SelectPageDown,
539         "HalfPageUp":             (*BufPane).HalfPageUp,
540         "HalfPageDown":           (*BufPane).HalfPageDown,
541         "StartOfLine":            (*BufPane).StartOfLine,
542         "EndOfLine":              (*BufPane).EndOfLine,
543         "ToggleHelp":             (*BufPane).ToggleHelp,
544         "ToggleKeyMenu":          (*BufPane).ToggleKeyMenu,
545         "ToggleRuler":            (*BufPane).ToggleRuler,
546         "ClearStatus":            (*BufPane).ClearStatus,
547         "ShellMode":              (*BufPane).ShellMode,
548         "CommandMode":            (*BufPane).CommandMode,
549         "ToggleOverwriteMode":    (*BufPane).ToggleOverwriteMode,
550         "Escape":                 (*BufPane).Escape,
551         "Quit":                   (*BufPane).Quit,
552         "QuitAll":                (*BufPane).QuitAll,
553         "AddTab":                 (*BufPane).AddTab,
554         "PreviousTab":            (*BufPane).PreviousTab,
555         "NextTab":                (*BufPane).NextTab,
556         "NextSplit":              (*BufPane).NextSplit,
557         "PreviousSplit":          (*BufPane).PreviousSplit,
558         "Unsplit":                (*BufPane).Unsplit,
559         "VSplit":                 (*BufPane).VSplitAction,
560         "HSplit":                 (*BufPane).HSplitAction,
561         "ToggleMacro":            (*BufPane).ToggleMacro,
562         "PlayMacro":              (*BufPane).PlayMacro,
563         "Suspend":                (*BufPane).Suspend,
564         "ScrollUp":               (*BufPane).ScrollUpAction,
565         "ScrollDown":             (*BufPane).ScrollDownAction,
566         "SpawnMultiCursor":       (*BufPane).SpawnMultiCursor,
567         "SpawnMultiCursorSelect": (*BufPane).SpawnMultiCursorSelect,
568         "RemoveMultiCursor":      (*BufPane).RemoveMultiCursor,
569         "RemoveAllMultiCursors":  (*BufPane).RemoveAllMultiCursors,
570         "SkipMultiCursor":        (*BufPane).SkipMultiCursor,
571         "JumpToMatchingBrace":    (*BufPane).JumpToMatchingBrace,
572         "None":                   (*BufPane).None,
573
574         // This was changed to InsertNewline but I don't want to break backwards compatibility
575         "InsertEnter": (*BufPane).InsertNewline,
576 }
577
578 // BufMouseActions contains the list of all possible mouse actions the bufhandler could execute
579 var BufMouseActions = map[string]BufMouseAction{
580         "MousePress":       (*BufPane).MousePress,
581         "MouseMultiCursor": (*BufPane).MouseMultiCursor,
582 }
583
584 // MultiActions is a list of actions that should be executed multiple
585 // times if there are multiple cursors (one per cursor)
586 // Generally actions that modify global editor state like quitting or
587 // saving should not be included in this list
588 var MultiActions = map[string]bool{
589         "CursorUp":            true,
590         "CursorDown":          true,
591         "CursorPageUp":        true,
592         "CursorPageDown":      true,
593         "CursorLeft":          true,
594         "CursorRight":         true,
595         "CursorStart":         true,
596         "CursorEnd":           true,
597         "SelectToStart":       true,
598         "SelectToEnd":         true,
599         "SelectUp":            true,
600         "SelectDown":          true,
601         "SelectLeft":          true,
602         "SelectRight":         true,
603         "WordRight":           true,
604         "WordLeft":            true,
605         "SelectWordRight":     true,
606         "SelectWordLeft":      true,
607         "DeleteWordRight":     true,
608         "DeleteWordLeft":      true,
609         "SelectLine":          true,
610         "SelectToStartOfLine": true,
611         "SelectToEndOfLine":   true,
612         "ParagraphPrevious":   true,
613         "ParagraphNext":       true,
614         "InsertNewline":       true,
615         "Backspace":           true,
616         "Delete":              true,
617         "InsertTab":           true,
618         "FindNext":            true,
619         "FindPrevious":        true,
620         "Cut":                 true,
621         "CutLine":             true,
622         "DuplicateLine":       true,
623         "DeleteLine":          true,
624         "MoveLinesUp":         true,
625         "MoveLinesDown":       true,
626         "IndentSelection":     true,
627         "OutdentSelection":    true,
628         "OutdentLine":         true,
629         "Paste":               true,
630         "PastePrimary":        true,
631         "SelectPageUp":        true,
632         "SelectPageDown":      true,
633         "StartOfLine":         true,
634         "EndOfLine":           true,
635         "JumpToMatchingBrace": true,
636 }