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