From c45ff4dd4f07ffcf266eca8abfe3a62384f37402 Mon Sep 17 00:00:00 2001 From: Zachary Yedidia Date: Mon, 12 Jun 2017 12:10:22 -0400 Subject: [PATCH] Add multiple cursor support This commit creates new keybindings and actions to handle multiple cursors. Here are the defaults: "Alt-n": "SpawnMultiCursor", "Alt-p": "RemoveMultiCursor", "Alt-c": "RemoveAllMultiCursors", "Alt-x": "SkipMultiCursor", --- cmd/micro/actions.go | 109 +++++++++++++++++++++++++- cmd/micro/bindings.go | 173 ++++++++++++++++++++++------------------- cmd/micro/buffer.go | 5 +- cmd/micro/messenger.go | 6 +- cmd/micro/view.go | 128 +++++++++++++++++++----------- 5 files changed, 289 insertions(+), 132 deletions(-) diff --git a/cmd/micro/actions.go b/cmd/micro/actions.go index d8e7991c..3a7050db 100644 --- a/cmd/micro/actions.go +++ b/cmd/micro/actions.go @@ -1829,7 +1829,112 @@ func (v *View) PlayMacro(usePlugin bool) bool { return true } -// None is no action -func None() bool { +func (v *View) SpawnMultiCursor(usePlugin bool) bool { + spawner := v.Buf.cursors[len(v.Buf.cursors)-1] + // You can only spawn a cursor from the main cursor + if v.Cursor == spawner { + if usePlugin && !PreActionCall("SpawnMultiCursor", v) { + return false + } + + if !spawner.HasSelection() { + spawner.SelectWord() + } else { + c := &Cursor{ + buf: v.Buf, + } + + sel := spawner.GetSelection() + + searchStart = ToCharPos(spawner.CurSelection[1], v.Buf) + v.Cursor = c + Search(sel, v, true) + messenger.Message(v.Cursor.Loc) + + for _, cur := range v.Buf.cursors { + if c.Loc == cur.Loc { + return false + } + } + v.Buf.cursors = append(v.Buf.cursors, c) + v.Relocate() + v.Cursor = spawner + } + + if usePlugin { + PostActionCall("SpawnMultiCursor", v) + } + } + + return false +} + +func (v *View) SkipMultiCursor(usePlugin bool) bool { + cursor := v.Buf.cursors[len(v.Buf.cursors)-1] + + if v.Cursor == cursor { + messenger.Message("SKIP") + if usePlugin && !PreActionCall("SkipMultiCursor", v) { + return false + } + sel := cursor.GetSelection() + + searchStart = ToCharPos(cursor.CurSelection[1], v.Buf) + v.Cursor = cursor + Search(sel, v, true) + v.Relocate() + v.Cursor = cursor + + if usePlugin { + PostActionCall("SkipMultiCursor", v) + } + } + return false +} + +func (v *View) RemoveMultiCursor(usePlugin bool) bool { + end := len(v.Buf.cursors) + if end > 1 { + nextOne := v.Buf.cursors[len(v.Buf.cursors)-2] + if v.Cursor == nextOne { + if usePlugin && !PreActionCall("RemoveMultiCursor", v) { + return false + } + + if end > 1 { + v.Buf.cursors[end-1] = nil + v.Buf.cursors = v.Buf.cursors[:end-1] + } + v.Relocate() + + if usePlugin { + return PostActionCall("RemoveMultiCursor", v) + } + return true + } + } else { + v.RemoveAllMultiCursors(usePlugin) + } + return false +} + +func (v *View) RemoveAllMultiCursors(usePlugin bool) bool { + if v.Cursor == &v.Buf.Cursor { + if usePlugin && !PreActionCall("RemoveAllMultiCursors", v) { + return false + } + + for i := 1; i < len(v.Buf.cursors); i++ { + v.Buf.cursors[i] = nil + } + v.Buf.cursors = v.Buf.cursors[:1] + v.Cursor.ResetSelection() + v.Relocate() + + if usePlugin { + return PostActionCall("RemoveAllMultiCursors", v) + } + return true + } return false } diff --git a/cmd/micro/bindings.go b/cmd/micro/bindings.go index f18b2301..cb289d98 100644 --- a/cmd/micro/bindings.go +++ b/cmd/micro/bindings.go @@ -18,86 +18,90 @@ var mouseBindingActions = map[string]func(*View, bool, *tcell.EventMouse) bool{ } var bindingActions = map[string]func(*View, bool) bool{ - "CursorUp": (*View).CursorUp, - "CursorDown": (*View).CursorDown, - "CursorPageUp": (*View).CursorPageUp, - "CursorPageDown": (*View).CursorPageDown, - "CursorLeft": (*View).CursorLeft, - "CursorRight": (*View).CursorRight, - "CursorStart": (*View).CursorStart, - "CursorEnd": (*View).CursorEnd, - "SelectToStart": (*View).SelectToStart, - "SelectToEnd": (*View).SelectToEnd, - "SelectUp": (*View).SelectUp, - "SelectDown": (*View).SelectDown, - "SelectLeft": (*View).SelectLeft, - "SelectRight": (*View).SelectRight, - "WordRight": (*View).WordRight, - "WordLeft": (*View).WordLeft, - "SelectWordRight": (*View).SelectWordRight, - "SelectWordLeft": (*View).SelectWordLeft, - "DeleteWordRight": (*View).DeleteWordRight, - "DeleteWordLeft": (*View).DeleteWordLeft, - "SelectToStartOfLine": (*View).SelectToStartOfLine, - "SelectToEndOfLine": (*View).SelectToEndOfLine, - "InsertNewline": (*View).InsertNewline, - "InsertSpace": (*View).InsertSpace, - "Backspace": (*View).Backspace, - "Delete": (*View).Delete, - "InsertTab": (*View).InsertTab, - "Save": (*View).Save, - "SaveAll": (*View).SaveAll, - "SaveAs": (*View).SaveAs, - "Find": (*View).Find, - "FindNext": (*View).FindNext, - "FindPrevious": (*View).FindPrevious, - "Center": (*View).Center, - "Undo": (*View).Undo, - "Redo": (*View).Redo, - "Copy": (*View).Copy, - "Cut": (*View).Cut, - "CutLine": (*View).CutLine, - "DuplicateLine": (*View).DuplicateLine, - "DeleteLine": (*View).DeleteLine, - "MoveLinesUp": (*View).MoveLinesUp, - "MoveLinesDown": (*View).MoveLinesDown, - "IndentSelection": (*View).IndentSelection, - "OutdentSelection": (*View).OutdentSelection, - "OutdentLine": (*View).OutdentLine, - "Paste": (*View).Paste, - "PastePrimary": (*View).PastePrimary, - "SelectAll": (*View).SelectAll, - "OpenFile": (*View).OpenFile, - "Start": (*View).Start, - "End": (*View).End, - "PageUp": (*View).PageUp, - "PageDown": (*View).PageDown, - "HalfPageUp": (*View).HalfPageUp, - "HalfPageDown": (*View).HalfPageDown, - "StartOfLine": (*View).StartOfLine, - "EndOfLine": (*View).EndOfLine, - "ToggleHelp": (*View).ToggleHelp, - "ToggleRuler": (*View).ToggleRuler, - "JumpLine": (*View).JumpLine, - "ClearStatus": (*View).ClearStatus, - "ShellMode": (*View).ShellMode, - "CommandMode": (*View).CommandMode, - "Escape": (*View).Escape, - "Quit": (*View).Quit, - "QuitAll": (*View).QuitAll, - "AddTab": (*View).AddTab, - "PreviousTab": (*View).PreviousTab, - "NextTab": (*View).NextTab, - "NextSplit": (*View).NextSplit, - "PreviousSplit": (*View).PreviousSplit, - "Unsplit": (*View).Unsplit, - "VSplit": (*View).VSplitBinding, - "HSplit": (*View).HSplitBinding, - "ToggleMacro": (*View).ToggleMacro, - "PlayMacro": (*View).PlayMacro, - "Suspend": (*View).Suspend, - "ScrollUp": (*View).ScrollUpAction, - "ScrollDown": (*View).ScrollDownAction, + "CursorUp": (*View).CursorUp, + "CursorDown": (*View).CursorDown, + "CursorPageUp": (*View).CursorPageUp, + "CursorPageDown": (*View).CursorPageDown, + "CursorLeft": (*View).CursorLeft, + "CursorRight": (*View).CursorRight, + "CursorStart": (*View).CursorStart, + "CursorEnd": (*View).CursorEnd, + "SelectToStart": (*View).SelectToStart, + "SelectToEnd": (*View).SelectToEnd, + "SelectUp": (*View).SelectUp, + "SelectDown": (*View).SelectDown, + "SelectLeft": (*View).SelectLeft, + "SelectRight": (*View).SelectRight, + "WordRight": (*View).WordRight, + "WordLeft": (*View).WordLeft, + "SelectWordRight": (*View).SelectWordRight, + "SelectWordLeft": (*View).SelectWordLeft, + "DeleteWordRight": (*View).DeleteWordRight, + "DeleteWordLeft": (*View).DeleteWordLeft, + "SelectToStartOfLine": (*View).SelectToStartOfLine, + "SelectToEndOfLine": (*View).SelectToEndOfLine, + "InsertNewline": (*View).InsertNewline, + "InsertSpace": (*View).InsertSpace, + "Backspace": (*View).Backspace, + "Delete": (*View).Delete, + "InsertTab": (*View).InsertTab, + "Save": (*View).Save, + "SaveAll": (*View).SaveAll, + "SaveAs": (*View).SaveAs, + "Find": (*View).Find, + "FindNext": (*View).FindNext, + "FindPrevious": (*View).FindPrevious, + "Center": (*View).Center, + "Undo": (*View).Undo, + "Redo": (*View).Redo, + "Copy": (*View).Copy, + "Cut": (*View).Cut, + "CutLine": (*View).CutLine, + "DuplicateLine": (*View).DuplicateLine, + "DeleteLine": (*View).DeleteLine, + "MoveLinesUp": (*View).MoveLinesUp, + "MoveLinesDown": (*View).MoveLinesDown, + "IndentSelection": (*View).IndentSelection, + "OutdentSelection": (*View).OutdentSelection, + "OutdentLine": (*View).OutdentLine, + "Paste": (*View).Paste, + "PastePrimary": (*View).PastePrimary, + "SelectAll": (*View).SelectAll, + "OpenFile": (*View).OpenFile, + "Start": (*View).Start, + "End": (*View).End, + "PageUp": (*View).PageUp, + "PageDown": (*View).PageDown, + "HalfPageUp": (*View).HalfPageUp, + "HalfPageDown": (*View).HalfPageDown, + "StartOfLine": (*View).StartOfLine, + "EndOfLine": (*View).EndOfLine, + "ToggleHelp": (*View).ToggleHelp, + "ToggleRuler": (*View).ToggleRuler, + "JumpLine": (*View).JumpLine, + "ClearStatus": (*View).ClearStatus, + "ShellMode": (*View).ShellMode, + "CommandMode": (*View).CommandMode, + "Escape": (*View).Escape, + "Quit": (*View).Quit, + "QuitAll": (*View).QuitAll, + "AddTab": (*View).AddTab, + "PreviousTab": (*View).PreviousTab, + "NextTab": (*View).NextTab, + "NextSplit": (*View).NextSplit, + "PreviousSplit": (*View).PreviousSplit, + "Unsplit": (*View).Unsplit, + "VSplit": (*View).VSplitBinding, + "HSplit": (*View).HSplitBinding, + "ToggleMacro": (*View).ToggleMacro, + "PlayMacro": (*View).PlayMacro, + "Suspend": (*View).Suspend, + "ScrollUp": (*View).ScrollUpAction, + "ScrollDown": (*View).ScrollDownAction, + "SpawnMultiCursor": (*View).SpawnMultiCursor, + "RemoveMultiCursor": (*View).RemoveMultiCursor, + "RemoveAllMultiCursors": (*View).RemoveAllMultiCursors, + "SkipMultiCursor": (*View).SkipMultiCursor, // This was changed to InsertNewline but I don't want to break backwards compatibility "InsertEnter": (*View).InsertNewline, @@ -496,8 +500,8 @@ func DefaultBindings() map[string]string { "Alt-b": "WordLeft", "Alt-a": "StartOfLine", "Alt-e": "EndOfLine", - "Alt-p": "CursorUp", - "Alt-n": "CursorDown", + // "Alt-p": "CursorUp", + // "Alt-n": "CursorDown", // Integration with file managers "F1": "ToggleHelp", @@ -513,5 +517,10 @@ func DefaultBindings() map[string]string { "MouseWheelDown": "ScrollDown", "MouseLeft": "MousePress", "MouseMiddle": "PastePrimary", + + "Alt-n": "SpawnMultiCursor", + "Alt-p": "RemoveMultiCursor", + "Alt-c": "RemoveAllMultiCursors", + "Alt-x": "SkipMultiCursor", } } diff --git a/cmd/micro/buffer.go b/cmd/micro/buffer.go index bfbcbec4..ab3b18e4 100644 --- a/cmd/micro/buffer.go +++ b/cmd/micro/buffer.go @@ -28,7 +28,8 @@ type Buffer struct { // This stores all the text in the buffer as an array of lines *LineArray - Cursor Cursor + Cursor Cursor + cursors []*Cursor // for multiple cursors // Path to the file on disk Path string @@ -169,6 +170,8 @@ func NewBuffer(reader io.Reader, size int64, path string) *Buffer { file.Close() } + b.cursors = []*Cursor{&b.Cursor} + return b } diff --git a/cmd/micro/messenger.go b/cmd/micro/messenger.go index 3fda5368..6c212910 100644 --- a/cmd/micro/messenger.go +++ b/cmd/micro/messenger.go @@ -153,7 +153,7 @@ func (m *Messenger) YesNoPrompt(prompt string) (bool, bool) { for { m.Clear() m.Display() - screen.ShowCursor(Count(m.message), h-1) + ShowCursor(Count(m.message), h-1) screen.Show() event := <-events @@ -190,7 +190,7 @@ func (m *Messenger) LetterPrompt(prompt string, responses ...rune) (rune, bool) for { m.Clear() m.Display() - screen.ShowCursor(Count(m.message), h-1) + ShowCursor(Count(m.message), h-1) screen.Show() event := <-events @@ -470,7 +470,7 @@ func (m *Messenger) Display() { } if m.hasPrompt { - screen.ShowCursor(Count(m.message)+m.cursorx, h-1) + ShowCursor(Count(m.message)+m.cursorx, h-1) screen.Show() } } diff --git a/cmd/micro/view.go b/cmd/micro/view.go index e3675345..3813f8d9 100644 --- a/cmd/micro/view.go +++ b/cmd/micro/view.go @@ -499,9 +499,13 @@ func (v *View) HandleEvent(event tcell.Event) { } } if e.Modifiers() == key.modifiers { - relocate = false - isBinding = true - relocate = v.ExecuteActions(actions) + for _, c := range v.Buf.cursors { + v.Cursor = c + relocate = false + isBinding = true + relocate = v.ExecuteActions(actions) || relocate + } + v.Cursor = &v.Buf.Cursor break } } @@ -509,24 +513,29 @@ func (v *View) HandleEvent(event tcell.Event) { if !isBinding && e.Key() == tcell.KeyRune { // Check viewtype if readonly don't insert a rune (readonly help and log view etc.) if v.Type.readonly == false { - // Insert a character - if v.Cursor.HasSelection() { - v.Cursor.DeleteSelection() - v.Cursor.ResetSelection() - } - v.Buf.Insert(v.Cursor.Loc, string(e.Rune())) - v.Cursor.Right() + for _, c := range v.Buf.cursors { + v.Cursor = c - for pl := range loadedPlugins { - _, err := Call(pl+".onRune", string(e.Rune()), v) - if err != nil && !strings.HasPrefix(err.Error(), "function does not exist") { - TermMessage(err) + // Insert a character + if v.Cursor.HasSelection() { + v.Cursor.DeleteSelection() + v.Cursor.ResetSelection() + } + v.Buf.Insert(v.Cursor.Loc, string(e.Rune())) + v.Cursor.Right() + + for pl := range loadedPlugins { + _, err := Call(pl+".onRune", string(e.Rune()), v) + if err != nil && !strings.HasPrefix(err.Error(), "function does not exist") { + TermMessage(err) + } + } + + if recordingMacro { + curMacro = append(curMacro, e.Rune()) } } - - if recordingMacro { - curMacro = append(curMacro, e.Rune()) - } + v.Cursor = &v.Buf.Cursor } } case *tcell.EventPaste: @@ -536,14 +545,16 @@ func (v *View) HandleEvent(event tcell.Event) { break } - v.paste(e.Text()) + for _, c := range v.Buf.cursors { + v.Cursor = c + v.paste(e.Text()) + + } + v.Cursor = &v.Buf.Cursor PostActionCall("Paste", v) } case *tcell.EventMouse: - x, y := e.Position() - x -= v.lineNumOffset - v.leftCol + v.x - y += v.Topline - v.y // Don't relocate for mouse events relocate = false @@ -551,7 +562,11 @@ func (v *View) HandleEvent(event tcell.Event) { for key, actions := range bindings { if button == key.buttons { - relocate = v.ExecuteActions(actions) + for _, c := range v.Buf.cursors { + v.Cursor = c + relocate = v.ExecuteActions(actions) || relocate + } + v.Cursor = &v.Buf.Cursor } } @@ -569,6 +584,10 @@ func (v *View) HandleEvent(event tcell.Event) { if !v.mouseReleased { // Mouse was just released + x, y := e.Position() + x -= v.lineNumOffset - v.leftCol + v.x + y += v.Topline - v.y + // Relocating here isn't really necessary because the cursor will // be in the right place from the last mouse event // However, if we are running in a terminal that doesn't support mouse motion @@ -822,22 +841,20 @@ func (v *View) DisplayView() { } charLoc := char.realLoc - if v.Cursor.HasSelection() && - (charLoc.GreaterEqual(v.Cursor.CurSelection[0]) && charLoc.LessThan(v.Cursor.CurSelection[1]) || - charLoc.LessThan(v.Cursor.CurSelection[0]) && charLoc.GreaterEqual(v.Cursor.CurSelection[1])) { - // The current character is selected - lineStyle = defStyle.Reverse(true) + for _, c := range v.Buf.cursors { + v.Cursor = c + if v.Cursor.HasSelection() && + (charLoc.GreaterEqual(v.Cursor.CurSelection[0]) && charLoc.LessThan(v.Cursor.CurSelection[1]) || + charLoc.LessThan(v.Cursor.CurSelection[0]) && charLoc.GreaterEqual(v.Cursor.CurSelection[1])) { + // The current character is selected + lineStyle = defStyle.Reverse(true) - if style, ok := colorscheme["selection"]; ok { - lineStyle = style + if style, ok := colorscheme["selection"]; ok { + lineStyle = style + } } } - - if tabs[curTab].CurView == v.Num && !v.Cursor.HasSelection() && - v.Cursor.Y == char.realLoc.Y && v.Cursor.X == char.realLoc.X && !cursorSet { - screen.ShowCursor(xOffset+char.visualLoc.X, yOffset+char.visualLoc.Y) - cursorSet = true - } + v.Cursor = &v.Buf.Cursor if v.Buf.Settings["cursorline"].(bool) && tabs[curTab].CurView == v.Num && !v.Cursor.HasSelection() && v.Cursor.Y == realLineN { @@ -848,6 +865,16 @@ func (v *View) DisplayView() { screen.SetContent(xOffset+char.visualLoc.X, yOffset+char.visualLoc.Y, char.drawChar, nil, lineStyle) + for _, c := range v.Buf.cursors { + v.Cursor = c + if tabs[curTab].CurView == v.Num && !v.Cursor.HasSelection() && + v.Cursor.Y == char.realLoc.Y && v.Cursor.X == char.realLoc.X && !cursorSet { + ShowCursor(xOffset+char.visualLoc.X, yOffset+char.visualLoc.Y) + // cursorSet = true + } + } + v.Cursor = &v.Buf.Cursor + lastChar = char } } @@ -858,19 +885,27 @@ func (v *View) DisplayView() { var cx, cy int if lastChar != nil { lastX = xOffset + lastChar.visualLoc.X + lastChar.width - if tabs[curTab].CurView == v.Num && !v.Cursor.HasSelection() && - v.Cursor.Y == lastChar.realLoc.Y && v.Cursor.X == lastChar.realLoc.X+1 { - screen.ShowCursor(lastX, yOffset+lastChar.visualLoc.Y) - cx, cy = lastX, yOffset+lastChar.visualLoc.Y + for _, c := range v.Buf.cursors { + v.Cursor = c + if tabs[curTab].CurView == v.Num && !v.Cursor.HasSelection() && + v.Cursor.Y == lastChar.realLoc.Y && v.Cursor.X == lastChar.realLoc.X+1 { + ShowCursor(lastX, yOffset+lastChar.visualLoc.Y) + cx, cy = lastX, yOffset+lastChar.visualLoc.Y + } } + v.Cursor = &v.Buf.Cursor realLoc = Loc{lastChar.realLoc.X + 1, realLineN} visualLoc = Loc{lastX - xOffset, lastChar.visualLoc.Y} } else if len(line) == 0 { - if tabs[curTab].CurView == v.Num && !v.Cursor.HasSelection() && - v.Cursor.Y == realLineN { - screen.ShowCursor(xOffset, yOffset+visualLineN) - cx, cy = xOffset, yOffset+visualLineN + for _, c := range v.Buf.cursors { + v.Cursor = c + if tabs[curTab].CurView == v.Num && !v.Cursor.HasSelection() && + v.Cursor.Y == realLineN { + ShowCursor(xOffset, yOffset+visualLineN) + cx, cy = xOffset, yOffset+visualLineN + } } + v.Cursor = &v.Buf.Cursor lastX = xOffset realLoc = Loc{0, realLineN} visualLoc = Loc{0, visualLineN} @@ -912,6 +947,11 @@ func (v *View) DisplayView() { } } +func ShowCursor(x, y int) { + r, _, _, _ := screen.GetContent(x, y) + screen.SetContent(x, y, r, nil, defStyle.Reverse(true)) +} + // Display renders the view, the cursor, and statusline func (v *View) Display() { if globalSettings["termtitle"].(bool) {