Merge pull request #704 from zyedidia/multiple-cursors

Multiple cursors
This commit is contained in:
Zachary Yedidia
2017-06-17 17:39:31 -04:00
committed by GitHub
12 changed files with 1035 additions and 733 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -14,90 +14,95 @@ var mouseBindings map[Key][]func(*View, bool, *tcell.EventMouse) bool
var helpBinding string
var mouseBindingActions = map[string]func(*View, bool, *tcell.EventMouse) bool{
"MousePress": (*View).MousePress,
"MousePress": (*View).MousePress,
"MouseMultiCursor": (*View).MouseMultiCursor,
}
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 +501,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 +518,11 @@ func DefaultBindings() map[string]string {
"MouseWheelDown": "ScrollDown",
"MouseLeft": "MousePress",
"MouseMiddle": "PastePrimary",
"Ctrl-MouseLeft": "MouseMultiCursor",
"Alt-n": "SpawnMultiCursor",
"Alt-p": "RemoveMultiCursor",
"Alt-c": "RemoveAllMultiCursors",
"Alt-x": "SkipMultiCursor",
}
}

View File

@@ -28,7 +28,9 @@ 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
curCursor int // the current cursor
// Path to the file on disk
Path string
@@ -169,6 +171,8 @@ func NewBuffer(reader io.Reader, size int64, path string) *Buffer {
file.Close()
}
b.cursors = []*Cursor{&b.Cursor}
return b
}
@@ -305,6 +309,30 @@ func (b *Buffer) Update() {
b.NumLines = len(b.lines)
}
func (b *Buffer) MergeCursors() {
var cursors []*Cursor
for i := 0; i < len(b.cursors); i++ {
c1 := b.cursors[i]
if c1 != nil {
for j := 0; j < len(b.cursors); j++ {
c2 := b.cursors[j]
if i != j && c1.Loc == c2.Loc {
b.cursors[j] = nil
}
}
cursors = append(cursors, c1)
}
}
b.cursors = cursors
}
func (b *Buffer) UpdateCursors() {
for i, c := range b.cursors {
c.Num = i
}
}
// Save saves the buffer to its default path
func (b *Buffer) Save() error {
return b.SaveAs(b.Path)

View File

@@ -21,6 +21,9 @@ type Cursor struct {
// This is used for line and word selection where it is necessary
// to know what the original selection was
OrigSelection [2]Loc
// Which cursor index is this (for multiple cursors)
Num int
}
// Goto puts the cursor at the given cursor's location and gives the current cursor its selection too

View File

@@ -97,30 +97,51 @@ func (eh *EventHandler) ApplyDiff(new string) {
// Insert creates an insert text event and executes it
func (eh *EventHandler) Insert(start Loc, text string) {
e := &TextEvent{
C: eh.buf.Cursor,
C: *eh.buf.cursors[eh.buf.curCursor],
EventType: TextEventInsert,
Deltas: []Delta{Delta{text, start, Loc{0, 0}}},
Time: time.Now(),
}
eh.Execute(e)
e.Deltas[0].End = start.Move(Count(text), eh.buf)
end := e.Deltas[0].End
for _, c := range eh.buf.cursors {
if start.Y != end.Y && c.GreaterThan(start) {
c.Loc.Y += end.Y - start.Y
} else if c.Y == start.Y && c.GreaterEqual(start) {
c.Loc = c.Move(Count(text), eh.buf)
}
c.LastVisualX = c.GetVisualX()
}
}
// Remove creates a remove text event and executes it
func (eh *EventHandler) Remove(start, end Loc) {
e := &TextEvent{
C: eh.buf.Cursor,
C: *eh.buf.cursors[eh.buf.curCursor],
EventType: TextEventRemove,
Deltas: []Delta{Delta{"", start, end}},
Time: time.Now(),
}
eh.Execute(e)
for _, c := range eh.buf.cursors {
if start.Y != end.Y && c.GreaterThan(end) {
c.Loc.Y -= end.Y - start.Y
} else if c.Y == end.Y && c.GreaterEqual(end) {
// TermMessage(start, end)
c.Loc = c.Move(-Diff(start, end, eh.buf), eh.buf)
// c.Loc = c.Move(ToCharPos(start, eh.buf)-ToCharPos(end, eh.buf), eh.buf)
}
c.LastVisualX = c.GetVisualX()
}
}
// MultipleReplace creates an multiple insertions executes them
func (eh *EventHandler) MultipleReplace(deltas []Delta) {
e := &TextEvent{
C: eh.buf.Cursor,
C: *eh.buf.cursors[eh.buf.curCursor],
EventType: TextEventReplace,
Deltas: deltas,
Time: time.Now(),
@@ -195,8 +216,12 @@ func (eh *EventHandler) UndoOneEvent() {
// Set the cursor in the right place
teCursor := t.C
t.C = eh.buf.Cursor
eh.buf.Cursor.Goto(teCursor)
if teCursor.Num >= 0 && teCursor.Num < len(eh.buf.cursors) {
t.C = *eh.buf.cursors[teCursor.Num]
eh.buf.cursors[teCursor.Num].Goto(teCursor)
} else {
teCursor.Num = -1
}
// Push it to the redo stack
eh.RedoStack.Push(t)
@@ -238,8 +263,12 @@ func (eh *EventHandler) RedoOneEvent() {
UndoTextEvent(t, eh.buf)
teCursor := t.C
t.C = eh.buf.Cursor
eh.buf.Cursor.Goto(teCursor)
if teCursor.Num >= 0 && teCursor.Num < len(eh.buf.cursors) {
t.C = *eh.buf.cursors[teCursor.Num]
eh.buf.cursors[teCursor.Num].Goto(teCursor)
} else {
teCursor.Num = -1
}
eh.UndoStack.Push(t)
}

View File

@@ -54,6 +54,28 @@ type Loc struct {
X, Y int
}
func Diff(a, b Loc, buf *Buffer) int {
if a.Y == b.Y {
if a.X > b.X {
return a.X - b.X
}
return b.X - a.X
}
// Make sure a is guaranteed to be less than b
if b.LessThan(a) {
a, b = b, a
}
loc := 0
for i := a.Y + 1; i < b.Y; i++ {
// + 1 for the newline
loc += Count(buf.Line(i)) + 1
}
loc += Count(buf.Line(a.Y)) - a.X + b.X + 1
return loc
}
// LessThan returns true if b is smaller
func (l Loc) LessThan(b Loc) bool {
if l.Y < b.Y {

File diff suppressed because one or more lines are too long

View File

@@ -169,7 +169,7 @@ func (v *View) paste(clip string) {
}
clip = strings.Replace(clip, "\n", "\n"+leadingWS, -1)
v.Buf.Insert(v.Cursor.Loc, clip)
v.Cursor.Loc = v.Cursor.Loc.Move(Count(clip), v.Buf)
// v.Cursor.Loc = v.Cursor.Loc.Move(Count(clip), v.Buf)
v.freshClip = false
messenger.Message("Pasted clipboard")
}
@@ -479,6 +479,11 @@ func (v *View) ExecuteActions(actions []func(*View, bool) bool) bool {
return relocate
}
func (v *View) SetCursor(c *Cursor) {
v.Cursor = c
v.Buf.curCursor = c.Num
}
// HandleEvent handles an event passed by the main loop
func (v *View) HandleEvent(event tcell.Event) {
// This bool determines whether the view is relocated at the end of the function
@@ -499,9 +504,14 @@ 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.SetCursor(c)
relocate = false
isBinding = true
relocate = v.ExecuteActions(actions) || relocate
}
v.SetCursor(&v.Buf.Cursor)
v.Buf.MergeCursors()
break
}
}
@@ -509,24 +519,28 @@ 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.SetCursor(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()))
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.SetCursor(&v.Buf.Cursor)
}
}
case *tcell.EventPaste:
@@ -536,27 +550,34 @@ func (v *View) HandleEvent(event tcell.Event) {
break
}
v.paste(e.Text())
for _, c := range v.Buf.cursors {
v.SetCursor(c)
v.paste(e.Text())
}
v.SetCursor(&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
button := e.Buttons()
for key, actions := range bindings {
if button == key.buttons {
relocate = v.ExecuteActions(actions)
if button == key.buttons && e.Modifiers() == key.modifiers {
for _, c := range v.Buf.cursors {
v.SetCursor(c)
relocate = v.ExecuteActions(actions) || relocate
}
v.SetCursor(&v.Buf.Cursor)
v.Buf.MergeCursors()
}
}
for key, actions := range mouseBindings {
if button == key.buttons {
if button == key.buttons && e.Modifiers() == key.modifiers {
for _, action := range actions {
action(v, true, e)
}
@@ -569,6 +590,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
@@ -596,6 +621,10 @@ func (v *View) HandleEvent(event tcell.Event) {
}
}
func (v *View) mainCursor() bool {
return v.Buf.curCursor == len(v.Buf.cursors)-1
}
// GutterMessage creates a message in this view's gutter
func (v *View) GutterMessage(section string, lineN int, msg string, kind int) {
lineN--
@@ -822,22 +851,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.SetCursor(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.SetCursor(&v.Buf.Cursor)
if v.Buf.Settings["cursorline"].(bool) && tabs[curTab].CurView == v.Num &&
!v.Cursor.HasSelection() && v.Cursor.Y == realLineN {
@@ -848,6 +875,16 @@ func (v *View) DisplayView() {
screen.SetContent(xOffset+char.visualLoc.X, yOffset+char.visualLoc.Y, char.drawChar, nil, lineStyle)
for i, c := range v.Buf.cursors {
v.SetCursor(c)
if tabs[curTab].CurView == v.Num && !v.Cursor.HasSelection() &&
v.Cursor.Y == char.realLoc.Y && v.Cursor.X == char.realLoc.X && (!cursorSet || i != 0) {
ShowMultiCursor(xOffset+char.visualLoc.X, yOffset+char.visualLoc.Y, i)
cursorSet = true
}
}
v.SetCursor(&v.Buf.Cursor)
lastChar = char
}
}
@@ -858,19 +895,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 i, c := range v.Buf.cursors {
v.SetCursor(c)
if tabs[curTab].CurView == v.Num && !v.Cursor.HasSelection() &&
v.Cursor.Y == lastChar.realLoc.Y && v.Cursor.X == lastChar.realLoc.X+1 {
ShowMultiCursor(lastX, yOffset+lastChar.visualLoc.Y, i)
cx, cy = lastX, yOffset+lastChar.visualLoc.Y
}
}
v.SetCursor(&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 i, c := range v.Buf.cursors {
v.SetCursor(c)
if tabs[curTab].CurView == v.Num && !v.Cursor.HasSelection() &&
v.Cursor.Y == realLineN {
ShowMultiCursor(xOffset, yOffset+visualLineN, i)
cx, cy = xOffset, yOffset+visualLineN
}
}
v.SetCursor(&v.Buf.Cursor)
lastX = xOffset
realLoc = Loc{0, realLineN}
visualLoc = Loc{0, visualLineN}
@@ -912,6 +957,18 @@ func (v *View) DisplayView() {
}
}
// ShowMultiCursor will display a cursor at a location
// If i == 0 then the terminal cursor will be used
// Otherwise a fake cursor will be drawn at the position
func ShowMultiCursor(x, y, i int) {
if i == 0 {
screen.ShowCursor(x, y)
} else {
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) {