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