--- /dev/null
+package action
+
+import (
+ "regexp"
+ "runtime"
+ "strings"
+ "time"
+ "unicode/utf8"
+
+ shellquote "github.com/kballard/go-shellquote"
+ "github.com/zyedidia/clipboard"
+ "github.com/zyedidia/micro/internal/buffer"
+ "github.com/zyedidia/micro/internal/config"
+ "github.com/zyedidia/micro/internal/screen"
+ "github.com/zyedidia/micro/internal/shell"
+ "github.com/zyedidia/micro/internal/util"
+ "github.com/zyedidia/tcell"
+)
+
+// ScrollUp is not an action
+func (h *BufPane) ScrollUp(n int) {
+ v := h.GetView()
+ if v.StartLine >= n {
+ v.StartLine -= n
+ h.SetView(v)
+ } else {
+ v.StartLine = 0
+ }
+}
+
+// ScrollDown is not an action
+func (h *BufPane) ScrollDown(n int) {
+ v := h.GetView()
+ if v.StartLine <= h.Buf.LinesNum()-1-n {
+ v.StartLine += n
+ h.SetView(v)
+ }
+}
+
+// MousePress is the event that should happen when a normal click happens
+// This is almost always bound to left click
+func (h *BufPane) MousePress(e *tcell.EventMouse) bool {
+ b := h.Buf
+ mx, my := e.Position()
+ mouseLoc := h.LocFromVisual(buffer.Loc{mx, my})
+ h.Cursor.Loc = mouseLoc
+ if h.mouseReleased {
+ if b.NumCursors() > 1 {
+ b.ClearCursors()
+ h.Relocate()
+ h.Cursor = h.Buf.GetActiveCursor()
+ h.Cursor.Loc = mouseLoc
+ }
+ if time.Since(h.lastClickTime)/time.Millisecond < config.DoubleClickThreshold && (mouseLoc.X == h.lastLoc.X && mouseLoc.Y == h.lastLoc.Y) {
+ if h.doubleClick {
+ // Triple click
+ h.lastClickTime = time.Now()
+
+ h.tripleClick = true
+ h.doubleClick = false
+
+ h.Cursor.SelectLine()
+ h.Cursor.CopySelection("primary")
+ } else {
+ // Double click
+ h.lastClickTime = time.Now()
+
+ h.doubleClick = true
+ h.tripleClick = false
+
+ h.Cursor.SelectWord()
+ h.Cursor.CopySelection("primary")
+ }
+ } else {
+ h.doubleClick = false
+ h.tripleClick = false
+ h.lastClickTime = time.Now()
+
+ h.Cursor.OrigSelection[0] = h.Cursor.Loc
+ h.Cursor.CurSelection[0] = h.Cursor.Loc
+ h.Cursor.CurSelection[1] = h.Cursor.Loc
+ }
+ h.mouseReleased = false
+ } else if !h.mouseReleased {
+ if h.tripleClick {
+ h.Cursor.AddLineToSelection()
+ } else if h.doubleClick {
+ h.Cursor.AddWordToSelection()
+ } else {
+ h.Cursor.SetSelectionEnd(h.Cursor.Loc)
+ }
+ }
+
+ h.Cursor.StoreVisualX()
+ h.lastLoc = mouseLoc
+ return true
+}
+
+// ScrollUpAction scrolls the view up
+func (h *BufPane) ScrollUpAction() bool {
+ h.ScrollUp(util.IntOpt(h.Buf.Settings["scrollspeed"]))
+ return true
+}
+
+// ScrollDownAction scrolls the view up
+func (h *BufPane) ScrollDownAction() bool {
+ h.ScrollDown(util.IntOpt(h.Buf.Settings["scrollspeed"]))
+ return true
+}
+
+// Center centers the view on the cursor
+func (h *BufPane) Center() bool {
+ v := h.GetView()
+ v.StartLine = h.Cursor.Y - v.Height/2
+ if v.StartLine+v.Height > h.Buf.LinesNum() {
+ v.StartLine = h.Buf.LinesNum() - v.Height
+ }
+ if v.StartLine < 0 {
+ v.StartLine = 0
+ }
+ h.SetView(v)
+ h.Relocate()
+ return true
+}
+
+// CursorUp moves the cursor up
+func (h *BufPane) CursorUp() bool {
+ h.Cursor.Deselect(true)
+ h.Cursor.Up()
+ h.Relocate()
+ return true
+}
+
+// CursorDown moves the cursor down
+func (h *BufPane) CursorDown() bool {
+ h.Cursor.Deselect(true)
+ h.Cursor.Down()
+ h.Relocate()
+ return true
+}
+
+// CursorLeft moves the cursor left
+func (h *BufPane) CursorLeft() bool {
+ if h.Cursor.HasSelection() {
+ h.Cursor.Deselect(true)
+ } else {
+ tabstospaces := h.Buf.Settings["tabstospaces"].(bool)
+ tabmovement := h.Buf.Settings["tabmovement"].(bool)
+ if tabstospaces && tabmovement {
+ tabsize := int(h.Buf.Settings["tabsize"].(float64))
+ line := h.Buf.LineBytes(h.Cursor.Y)
+ if h.Cursor.X-tabsize >= 0 && util.IsSpaces(line[h.Cursor.X-tabsize:h.Cursor.X]) && util.IsBytesWhitespace(line[0:h.Cursor.X-tabsize]) {
+ for i := 0; i < tabsize; i++ {
+ h.Cursor.Left()
+ }
+ } else {
+ h.Cursor.Left()
+ }
+ } else {
+ h.Cursor.Left()
+ }
+ }
+ h.Relocate()
+ return true
+}
+
+// CursorRight moves the cursor right
+func (h *BufPane) CursorRight() bool {
+ if h.Cursor.HasSelection() {
+ h.Cursor.Deselect(false)
+ h.Cursor.Loc = h.Cursor.Loc.Move(1, h.Buf)
+ } else {
+ tabstospaces := h.Buf.Settings["tabstospaces"].(bool)
+ tabmovement := h.Buf.Settings["tabmovement"].(bool)
+ if tabstospaces && tabmovement {
+ tabsize := int(h.Buf.Settings["tabsize"].(float64))
+ line := h.Buf.LineBytes(h.Cursor.Y)
+ if h.Cursor.X+tabsize < utf8.RuneCount(line) && util.IsSpaces(line[h.Cursor.X:h.Cursor.X+tabsize]) && util.IsBytesWhitespace(line[0:h.Cursor.X]) {
+ for i := 0; i < tabsize; i++ {
+ h.Cursor.Right()
+ }
+ } else {
+ h.Cursor.Right()
+ }
+ } else {
+ h.Cursor.Right()
+ }
+ }
+
+ h.Relocate()
+ return true
+}
+
+// WordRight moves the cursor one word to the right
+func (h *BufPane) WordRight() bool {
+ h.Cursor.Deselect(false)
+ h.Cursor.WordRight()
+ h.Relocate()
+ return true
+}
+
+// WordLeft moves the cursor one word to the left
+func (h *BufPane) WordLeft() bool {
+ h.Cursor.Deselect(true)
+ h.Cursor.WordLeft()
+ h.Relocate()
+ return true
+}
+
+// SelectUp selects up one line
+func (h *BufPane) SelectUp() bool {
+ if !h.Cursor.HasSelection() {
+ h.Cursor.OrigSelection[0] = h.Cursor.Loc
+ }
+ h.Cursor.Up()
+ h.Cursor.SelectTo(h.Cursor.Loc)
+ h.Relocate()
+ return true
+}
+
+// SelectDown selects down one line
+func (h *BufPane) SelectDown() bool {
+ if !h.Cursor.HasSelection() {
+ h.Cursor.OrigSelection[0] = h.Cursor.Loc
+ }
+ h.Cursor.Down()
+ h.Cursor.SelectTo(h.Cursor.Loc)
+ h.Relocate()
+ return true
+}
+
+// SelectLeft selects the character to the left of the cursor
+func (h *BufPane) SelectLeft() bool {
+ loc := h.Cursor.Loc
+ count := h.Buf.End()
+ if loc.GreaterThan(count) {
+ loc = count
+ }
+ if !h.Cursor.HasSelection() {
+ h.Cursor.OrigSelection[0] = loc
+ }
+ h.Cursor.Left()
+ h.Cursor.SelectTo(h.Cursor.Loc)
+ h.Relocate()
+ return true
+}
+
+// SelectRight selects the character to the right of the cursor
+func (h *BufPane) SelectRight() bool {
+ loc := h.Cursor.Loc
+ count := h.Buf.End()
+ if loc.GreaterThan(count) {
+ loc = count
+ }
+ if !h.Cursor.HasSelection() {
+ h.Cursor.OrigSelection[0] = loc
+ }
+ h.Cursor.Right()
+ h.Cursor.SelectTo(h.Cursor.Loc)
+ h.Relocate()
+ return true
+}
+
+// SelectWordRight selects the word to the right of the cursor
+func (h *BufPane) SelectWordRight() bool {
+ if !h.Cursor.HasSelection() {
+ h.Cursor.OrigSelection[0] = h.Cursor.Loc
+ }
+ h.Cursor.WordRight()
+ h.Cursor.SelectTo(h.Cursor.Loc)
+ h.Relocate()
+ return true
+}
+
+// SelectWordLeft selects the word to the left of the cursor
+func (h *BufPane) SelectWordLeft() bool {
+ if !h.Cursor.HasSelection() {
+ h.Cursor.OrigSelection[0] = h.Cursor.Loc
+ }
+ h.Cursor.WordLeft()
+ h.Cursor.SelectTo(h.Cursor.Loc)
+ h.Relocate()
+ return true
+}
+
+// StartOfLine moves the cursor to the start of the text of the line
+func (h *BufPane) StartOfText() bool {
+ h.Cursor.Deselect(true)
+ h.Cursor.StartOfText()
+ h.Relocate()
+ return true
+}
+
+// StartOfLine moves the cursor to the start of the line
+func (h *BufPane) StartOfLine() bool {
+ h.Cursor.Deselect(true)
+ h.Cursor.Start()
+ h.Relocate()
+ return true
+}
+
+// EndOfLine moves the cursor to the end of the line
+func (h *BufPane) EndOfLine() bool {
+ h.Cursor.Deselect(true)
+ h.Cursor.End()
+ h.Relocate()
+ return true
+}
+
+// SelectLine selects the entire current line
+func (h *BufPane) SelectLine() bool {
+ h.Cursor.SelectLine()
+ h.Relocate()
+ return true
+}
+
+// SelectToStartOfText selects to the start of the text on the current line
+func (h *BufPane) SelectToStartOfText() bool {
+ if !h.Cursor.HasSelection() {
+ h.Cursor.OrigSelection[0] = h.Cursor.Loc
+ }
+ h.Cursor.StartOfText()
+ h.Cursor.SelectTo(h.Cursor.Loc)
+ h.Relocate()
+ return true
+}
+
+// SelectToStartOfLine selects to the start of the current line
+func (h *BufPane) SelectToStartOfLine() bool {
+ if !h.Cursor.HasSelection() {
+ h.Cursor.OrigSelection[0] = h.Cursor.Loc
+ }
+ h.Cursor.Start()
+ h.Cursor.SelectTo(h.Cursor.Loc)
+ h.Relocate()
+ return true
+}
+
+// SelectToEndOfLine selects to the end of the current line
+func (h *BufPane) SelectToEndOfLine() bool {
+ if !h.Cursor.HasSelection() {
+ h.Cursor.OrigSelection[0] = h.Cursor.Loc
+ }
+ h.Cursor.End()
+ h.Cursor.SelectTo(h.Cursor.Loc)
+ h.Relocate()
+ return true
+}
+
+// ParagraphPrevious moves the cursor to the previous empty line, or beginning of the buffer if there's none
+func (h *BufPane) ParagraphPrevious() bool {
+ var line int
+ for line = h.Cursor.Y; line > 0; line-- {
+ if len(h.Buf.LineBytes(line)) == 0 && line != h.Cursor.Y {
+ h.Cursor.X = 0
+ h.Cursor.Y = line
+ break
+ }
+ }
+ // If no empty line found. move cursor to end of buffer
+ if line == 0 {
+ h.Cursor.Loc = h.Buf.Start()
+ }
+ h.Relocate()
+ return true
+}
+
+// ParagraphNext moves the cursor to the next empty line, or end of the buffer if there's none
+func (h *BufPane) ParagraphNext() bool {
+ var line int
+ for line = h.Cursor.Y; line < h.Buf.LinesNum(); line++ {
+ if len(h.Buf.LineBytes(line)) == 0 && line != h.Cursor.Y {
+ h.Cursor.X = 0
+ h.Cursor.Y = line
+ break
+ }
+ }
+ // If no empty line found. move cursor to end of buffer
+ if line == h.Buf.LinesNum() {
+ h.Cursor.Loc = h.Buf.End()
+ }
+ h.Relocate()
+ return true
+}
+
+// Retab changes all tabs to spaces or all spaces to tabs depending
+// on the user's settings
+func (h *BufPane) Retab() bool {
+ h.Buf.Retab()
+ h.Relocate()
+ return true
+}
+
+// CursorStart moves the cursor to the start of the buffer
+func (h *BufPane) CursorStart() bool {
+ h.Cursor.Deselect(true)
+ h.Cursor.X = 0
+ h.Cursor.Y = 0
+ h.Relocate()
+ return true
+}
+
+// CursorEnd moves the cursor to the end of the buffer
+func (h *BufPane) CursorEnd() bool {
+ h.Cursor.Deselect(true)
+ h.Cursor.Loc = h.Buf.End()
+ h.Cursor.StoreVisualX()
+ h.Relocate()
+ return true
+}
+
+// SelectToStart selects the text from the cursor to the start of the buffer
+func (h *BufPane) SelectToStart() bool {
+ if !h.Cursor.HasSelection() {
+ h.Cursor.OrigSelection[0] = h.Cursor.Loc
+ }
+ h.CursorStart()
+ h.Cursor.SelectTo(h.Buf.Start())
+ h.Relocate()
+ return true
+}
+
+// SelectToEnd selects the text from the cursor to the end of the buffer
+func (h *BufPane) SelectToEnd() bool {
+ if !h.Cursor.HasSelection() {
+ h.Cursor.OrigSelection[0] = h.Cursor.Loc
+ }
+ h.CursorEnd()
+ h.Cursor.SelectTo(h.Buf.End())
+ h.Relocate()
+ return true
+}
+
+// InsertNewline inserts a newline plus possible some whitespace if autoindent is on
+func (h *BufPane) InsertNewline() bool {
+ // Insert a newline
+ if h.Cursor.HasSelection() {
+ h.Cursor.DeleteSelection()
+ h.Cursor.ResetSelection()
+ }
+
+ ws := util.GetLeadingWhitespace(h.Buf.LineBytes(h.Cursor.Y))
+ cx := h.Cursor.X
+ h.Buf.Insert(h.Cursor.Loc, "\n")
+ // h.Cursor.Right()
+
+ if h.Buf.Settings["autoindent"].(bool) {
+ if cx < len(ws) {
+ ws = ws[0:cx]
+ }
+ h.Buf.Insert(h.Cursor.Loc, string(ws))
+ // for i := 0; i < len(ws); i++ {
+ // h.Cursor.Right()
+ // }
+
+ // Remove the whitespaces if keepautoindent setting is off
+ if util.IsSpacesOrTabs(h.Buf.LineBytes(h.Cursor.Y-1)) && !h.Buf.Settings["keepautoindent"].(bool) {
+ line := h.Buf.LineBytes(h.Cursor.Y - 1)
+ h.Buf.Remove(buffer.Loc{X: 0, Y: h.Cursor.Y - 1}, buffer.Loc{X: utf8.RuneCount(line), Y: h.Cursor.Y - 1})
+ }
+ }
+ h.Cursor.LastVisualX = h.Cursor.GetVisualX()
+ h.Relocate()
+ return true
+}
+
+// Backspace deletes the previous character
+func (h *BufPane) Backspace() bool {
+ if h.Cursor.HasSelection() {
+ h.Cursor.DeleteSelection()
+ h.Cursor.ResetSelection()
+ } else if h.Cursor.Loc.GreaterThan(h.Buf.Start()) {
+ // We have to do something a bit hacky here because we want to
+ // delete the line by first moving left and then deleting backwards
+ // but the undo redo would place the cursor in the wrong place
+ // So instead we move left, save the position, move back, delete
+ // and restore the position
+
+ // If the user is using spaces instead of tabs and they are deleting
+ // whitespace at the start of the line, we should delete as if it's a
+ // tab (tabSize number of spaces)
+ lineStart := util.SliceStart(h.Buf.LineBytes(h.Cursor.Y), h.Cursor.X)
+ tabSize := int(h.Buf.Settings["tabsize"].(float64))
+ if h.Buf.Settings["tabstospaces"].(bool) && util.IsSpaces(lineStart) && len(lineStart) != 0 && utf8.RuneCount(lineStart)%tabSize == 0 {
+ loc := h.Cursor.Loc
+ h.Buf.Remove(loc.Move(-tabSize, h.Buf), loc)
+ } else {
+ loc := h.Cursor.Loc
+ h.Buf.Remove(loc.Move(-1, h.Buf), loc)
+ }
+ }
+ h.Cursor.LastVisualX = h.Cursor.GetVisualX()
+ h.Relocate()
+ return true
+}
+
+// DeleteWordRight deletes the word to the right of the cursor
+func (h *BufPane) DeleteWordRight() bool {
+ h.SelectWordRight()
+ if h.Cursor.HasSelection() {
+ h.Cursor.DeleteSelection()
+ h.Cursor.ResetSelection()
+ }
+ h.Relocate()
+ return true
+}
+
+// DeleteWordLeft deletes the word to the left of the cursor
+func (h *BufPane) DeleteWordLeft() bool {
+ h.SelectWordLeft()
+ if h.Cursor.HasSelection() {
+ h.Cursor.DeleteSelection()
+ h.Cursor.ResetSelection()
+ }
+ h.Relocate()
+ return true
+}
+
+// Delete deletes the next character
+func (h *BufPane) Delete() bool {
+ if h.Cursor.HasSelection() {
+ h.Cursor.DeleteSelection()
+ h.Cursor.ResetSelection()
+ } else {
+ loc := h.Cursor.Loc
+ if loc.LessThan(h.Buf.End()) {
+ h.Buf.Remove(loc, loc.Move(1, h.Buf))
+ }
+ }
+ h.Relocate()
+ return true
+}
+
+// IndentSelection indents the current selection
+func (h *BufPane) IndentSelection() bool {
+ if h.Cursor.HasSelection() {
+ start := h.Cursor.CurSelection[0]
+ end := h.Cursor.CurSelection[1]
+ if end.Y < start.Y {
+ start, end = end, start
+ h.Cursor.SetSelectionStart(start)
+ h.Cursor.SetSelectionEnd(end)
+ }
+
+ startY := start.Y
+ endY := end.Move(-1, h.Buf).Y
+ endX := end.Move(-1, h.Buf).X
+ tabsize := int(h.Buf.Settings["tabsize"].(float64))
+ indentsize := len(h.Buf.IndentString(tabsize))
+ for y := startY; y <= endY; y++ {
+ if len(h.Buf.LineBytes(y)) > 0 {
+ h.Buf.Insert(buffer.Loc{X: 0, Y: y}, h.Buf.IndentString(tabsize))
+ if y == startY && start.X > 0 {
+ h.Cursor.SetSelectionStart(start.Move(indentsize, h.Buf))
+ }
+ if y == endY {
+ h.Cursor.SetSelectionEnd(buffer.Loc{X: endX + indentsize + 1, Y: endY})
+ }
+ }
+ }
+ h.Buf.RelocateCursors()
+
+ h.Relocate()
+ return true
+ }
+ return false
+}
+
+// OutdentLine moves the current line back one indentation
+func (h *BufPane) OutdentLine() bool {
+ if h.Cursor.HasSelection() {
+ return false
+ }
+
+ for x := 0; x < len(h.Buf.IndentString(util.IntOpt(h.Buf.Settings["tabsize"]))); x++ {
+ if len(util.GetLeadingWhitespace(h.Buf.LineBytes(h.Cursor.Y))) == 0 {
+ break
+ }
+ h.Buf.Remove(buffer.Loc{X: 0, Y: h.Cursor.Y}, buffer.Loc{X: 1, Y: h.Cursor.Y})
+ }
+ h.Buf.RelocateCursors()
+ h.Relocate()
+ return true
+}
+
+// OutdentSelection takes the current selection and moves it back one indent level
+func (h *BufPane) OutdentSelection() bool {
+ if h.Cursor.HasSelection() {
+ start := h.Cursor.CurSelection[0]
+ end := h.Cursor.CurSelection[1]
+ if end.Y < start.Y {
+ start, end = end, start
+ h.Cursor.SetSelectionStart(start)
+ h.Cursor.SetSelectionEnd(end)
+ }
+
+ startY := start.Y
+ endY := end.Move(-1, h.Buf).Y
+ for y := startY; y <= endY; y++ {
+ for x := 0; x < len(h.Buf.IndentString(util.IntOpt(h.Buf.Settings["tabsize"]))); x++ {
+ if len(util.GetLeadingWhitespace(h.Buf.LineBytes(y))) == 0 {
+ break
+ }
+ h.Buf.Remove(buffer.Loc{X: 0, Y: y}, buffer.Loc{X: 1, Y: y})
+ }
+ }
+ h.Buf.RelocateCursors()
+
+ h.Relocate()
+ return true
+ }
+ return false
+}
+
+// Autocomplete cycles the suggestions and performs autocompletion if there are suggestions
+func (h *BufPane) Autocomplete() bool {
+ b := h.Buf
+
+ if h.Cursor.HasSelection() {
+ return false
+ }
+
+ if b.HasSuggestions {
+ b.CycleAutocomplete(true)
+ return true
+ }
+ return b.Autocomplete(buffer.BufferComplete)
+}
+
+// CycleAutocompleteBack cycles back in the autocomplete suggestion list
+func (h *BufPane) CycleAutocompleteBack() bool {
+ if h.Cursor.HasSelection() {
+ return false
+ }
+
+ if h.Buf.HasSuggestions {
+ h.Buf.CycleAutocomplete(false)
+ return true
+ }
+ return false
+}
+
+// InsertTab inserts a tab or spaces
+func (h *BufPane) InsertTab() bool {
+ b := h.Buf
+ indent := b.IndentString(util.IntOpt(b.Settings["tabsize"]))
+ tabBytes := len(indent)
+ bytesUntilIndent := tabBytes - (h.Cursor.GetVisualX() % tabBytes)
+ b.Insert(h.Cursor.Loc, indent[:bytesUntilIndent])
+ h.Relocate()
+ return true
+}
+
+// SaveAll saves all open buffers
+func (h *BufPane) SaveAll() bool {
+ for _, b := range buffer.OpenBuffers {
+ b.Save()
+ }
+ return true
+}
+
+// Save the buffer to disk
+func (h *BufPane) Save() bool {
+ // If this is an empty buffer, ask for a filename
+ if h.Buf.Path == "" {
+ h.SaveAs()
+ } else {
+ noPrompt := h.saveBufToFile(h.Buf.Path, "Save")
+ if noPrompt {
+ return true
+ }
+ }
+
+ return false
+}
+
+// SaveAs saves the buffer to disk with the given name
+func (h *BufPane) SaveAs() bool {
+ InfoBar.Prompt("Filename: ", "", "Save", nil, func(resp string, canceled bool) {
+ if !canceled {
+ // the filename might or might not be quoted, so unquote first then join the strings.
+ args, err := shellquote.Split(resp)
+ if err != nil {
+ InfoBar.Error("Error parsing arguments: ", err)
+ return
+ }
+ if len(args) == 0 {
+ InfoBar.Error("No filename given")
+ return
+ }
+ filename := strings.Join(args, " ")
+ noPrompt := h.saveBufToFile(filename, "SaveAs")
+ if noPrompt {
+ h.completeAction("SaveAs")
+ }
+ }
+ })
+ return false
+}
+
+// This function saves the buffer to `filename` and changes the buffer's path and name
+// to `filename` if the save is successful
+func (h *BufPane) saveBufToFile(filename string, action string) bool {
+ err := h.Buf.SaveAs(filename)
+ if err != nil {
+ if strings.HasSuffix(err.Error(), "permission denied") {
+ InfoBar.YNPrompt("Permission denied. Do you want to save this file using sudo? (y,n)", func(yes, canceled bool) {
+ if yes && !canceled {
+ err = h.Buf.SaveAsWithSudo(filename)
+ if err != nil {
+ InfoBar.Error(err)
+ } else {
+ h.Buf.Path = filename
+ h.Buf.SetName(filename)
+ InfoBar.Message("Saved " + filename)
+ }
+ h.completeAction(action)
+ }
+ })
+ return false
+ } else {
+ InfoBar.Error(err)
+ }
+ } else {
+ h.Buf.Path = filename
+ h.Buf.SetName(filename)
+ InfoBar.Message("Saved " + filename)
+ }
+ return true
+}
+
+// Find opens a prompt and searches forward for the input
+func (h *BufPane) Find() bool {
+ h.searchOrig = h.Cursor.Loc
+ InfoBar.Prompt("Find: ", "", "Find", func(resp string) {
+ // Event callback
+ match, found, _ := h.Buf.FindNext(resp, h.Buf.Start(), h.Buf.End(), h.searchOrig, true, true)
+ if found {
+ h.Cursor.SetSelectionStart(match[0])
+ h.Cursor.SetSelectionEnd(match[1])
+ h.Cursor.OrigSelection[0] = h.Cursor.CurSelection[0]
+ h.Cursor.OrigSelection[1] = h.Cursor.CurSelection[1]
+ h.Cursor.GotoLoc(match[1])
+ } else {
+ h.Cursor.GotoLoc(h.searchOrig)
+ h.Cursor.ResetSelection()
+ }
+ h.Relocate()
+ }, func(resp string, canceled bool) {
+ // Finished callback
+ if !canceled {
+ match, found, err := h.Buf.FindNext(resp, h.Buf.Start(), h.Buf.End(), h.searchOrig, true, true)
+ if err != nil {
+ InfoBar.Error(err)
+ }
+ if found {
+ h.Cursor.SetSelectionStart(match[0])
+ h.Cursor.SetSelectionEnd(match[1])
+ h.Cursor.OrigSelection[0] = h.Cursor.CurSelection[0]
+ h.Cursor.OrigSelection[1] = h.Cursor.CurSelection[1]
+ h.Cursor.GotoLoc(h.Cursor.CurSelection[1])
+ h.lastSearch = resp
+ } else {
+ h.Cursor.ResetSelection()
+ InfoBar.Message("No matches found")
+ }
+ } else {
+ h.Cursor.ResetSelection()
+ }
+ h.Relocate()
+ })
+
+ return true
+}
+
+// FindNext searches forwards for the last used search term
+func (h *BufPane) FindNext() bool {
+ // If the cursor is at the start of a selection and we search we want
+ // to search from the end of the selection in the case that
+ // the selection is a search result in which case we wouldn't move at
+ // at all which would be bad
+ searchLoc := h.Cursor.Loc
+ if h.Cursor.HasSelection() {
+ searchLoc = h.Cursor.CurSelection[1]
+ }
+ match, found, err := h.Buf.FindNext(h.lastSearch, h.Buf.Start(), h.Buf.End(), searchLoc, true, true)
+ if err != nil {
+ InfoBar.Error(err)
+ }
+ if found {
+ h.Cursor.SetSelectionStart(match[0])
+ h.Cursor.SetSelectionEnd(match[1])
+ h.Cursor.OrigSelection[0] = h.Cursor.CurSelection[0]
+ h.Cursor.OrigSelection[1] = h.Cursor.CurSelection[1]
+ h.Cursor.Loc = h.Cursor.CurSelection[1]
+ } else {
+ h.Cursor.ResetSelection()
+ }
+ h.Relocate()
+ return true
+}
+
+// FindPrevious searches backwards for the last used search term
+func (h *BufPane) FindPrevious() bool {
+ // If the cursor is at the end of a selection and we search we want
+ // to search from the beginning of the selection in the case that
+ // the selection is a search result in which case we wouldn't move at
+ // at all which would be bad
+ searchLoc := h.Cursor.Loc
+ if h.Cursor.HasSelection() {
+ searchLoc = h.Cursor.CurSelection[0]
+ }
+ match, found, err := h.Buf.FindNext(h.lastSearch, h.Buf.Start(), h.Buf.End(), searchLoc, false, true)
+ if err != nil {
+ InfoBar.Error(err)
+ }
+ if found {
+ h.Cursor.SetSelectionStart(match[0])
+ h.Cursor.SetSelectionEnd(match[1])
+ h.Cursor.OrigSelection[0] = h.Cursor.CurSelection[0]
+ h.Cursor.OrigSelection[1] = h.Cursor.CurSelection[1]
+ h.Cursor.Loc = h.Cursor.CurSelection[1]
+ } else {
+ h.Cursor.ResetSelection()
+ }
+ h.Relocate()
+ return true
+}
+
+// Undo undoes the last action
+func (h *BufPane) Undo() bool {
+ h.Buf.Undo()
+ InfoBar.Message("Undid action")
+ h.Relocate()
+ return true
+}
+
+// Redo redoes the last action
+func (h *BufPane) Redo() bool {
+ h.Buf.Redo()
+ InfoBar.Message("Redid action")
+ h.Relocate()
+ return true
+}
+
+// Copy the selection to the system clipboard
+func (h *BufPane) Copy() bool {
+ if h.Cursor.HasSelection() {
+ h.Cursor.CopySelection("clipboard")
+ h.freshClip = true
+ if clipboard.Unsupported {
+ InfoBar.Message("Copied selection (install xclip for external clipboard)")
+ } else {
+ InfoBar.Message("Copied selection")
+ }
+ }
+ h.Relocate()
+ return true
+}
+
+// CutLine cuts the current line to the clipboard
+func (h *BufPane) CutLine() bool {
+ h.Cursor.SelectLine()
+ if !h.Cursor.HasSelection() {
+ return false
+ }
+ if h.freshClip == true {
+ if h.Cursor.HasSelection() {
+ if clip, err := clipboard.ReadAll("clipboard"); err != nil {
+ // messenger.Error(err)
+ } else {
+ clipboard.WriteAll(clip+string(h.Cursor.GetSelection()), "clipboard")
+ }
+ }
+ } else if time.Since(h.lastCutTime)/time.Second > 10*time.Second || h.freshClip == false {
+ h.Copy()
+ }
+ h.freshClip = true
+ h.lastCutTime = time.Now()
+ h.Cursor.DeleteSelection()
+ h.Cursor.ResetSelection()
+ InfoBar.Message("Cut line")
+ h.Relocate()
+ return true
+}
+
+// Cut the selection to the system clipboard
+func (h *BufPane) Cut() bool {
+ if h.Cursor.HasSelection() {
+ h.Cursor.CopySelection("clipboard")
+ h.Cursor.DeleteSelection()
+ h.Cursor.ResetSelection()
+ h.freshClip = true
+ InfoBar.Message("Cut selection")
+
+ h.Relocate()
+ return true
+ } else {
+ return h.CutLine()
+ }
+}
+
+// DuplicateLine duplicates the current line or selection
+func (h *BufPane) DuplicateLine() bool {
+ if h.Cursor.HasSelection() {
+ h.Buf.Insert(h.Cursor.CurSelection[1], string(h.Cursor.GetSelection()))
+ } else {
+ h.Cursor.End()
+ h.Buf.Insert(h.Cursor.Loc, "\n"+string(h.Buf.LineBytes(h.Cursor.Y)))
+ // h.Cursor.Right()
+ }
+
+ InfoBar.Message("Duplicated line")
+ h.Relocate()
+ return true
+}
+
+// DeleteLine deletes the current line
+func (h *BufPane) DeleteLine() bool {
+ h.Cursor.SelectLine()
+ if !h.Cursor.HasSelection() {
+ return false
+ }
+ h.Cursor.DeleteSelection()
+ h.Cursor.ResetSelection()
+ InfoBar.Message("Deleted line")
+ h.Relocate()
+ return true
+}
+
+// MoveLinesUp moves up the current line or selected lines if any
+func (h *BufPane) MoveLinesUp() bool {
+ if h.Cursor.HasSelection() {
+ if h.Cursor.CurSelection[0].Y == 0 {
+ InfoBar.Message("Cannot move further up")
+ return false
+ }
+ start := h.Cursor.CurSelection[0].Y
+ end := h.Cursor.CurSelection[1].Y
+ if start > end {
+ end, start = start, end
+ }
+
+ h.Buf.MoveLinesUp(
+ start,
+ end,
+ )
+ h.Cursor.CurSelection[1].Y -= 1
+ } else {
+ if h.Cursor.Loc.Y == 0 {
+ InfoBar.Message("Cannot move further up")
+ return false
+ }
+ h.Buf.MoveLinesUp(
+ h.Cursor.Loc.Y,
+ h.Cursor.Loc.Y+1,
+ )
+ }
+
+ h.Relocate()
+ return true
+}
+
+// MoveLinesDown moves down the current line or selected lines if any
+func (h *BufPane) MoveLinesDown() bool {
+ if h.Cursor.HasSelection() {
+ if h.Cursor.CurSelection[1].Y >= h.Buf.LinesNum() {
+ InfoBar.Message("Cannot move further down")
+ return false
+ }
+ start := h.Cursor.CurSelection[0].Y
+ end := h.Cursor.CurSelection[1].Y
+ if start > end {
+ end, start = start, end
+ }
+
+ h.Buf.MoveLinesDown(
+ start,
+ end,
+ )
+ } else {
+ if h.Cursor.Loc.Y >= h.Buf.LinesNum()-1 {
+ InfoBar.Message("Cannot move further down")
+ return false
+ }
+ h.Buf.MoveLinesDown(
+ h.Cursor.Loc.Y,
+ h.Cursor.Loc.Y+1,
+ )
+ }
+
+ h.Relocate()
+ return true
+}
+
+// Paste whatever is in the system clipboard into the buffer
+// Delete and paste if the user has a selection
+func (h *BufPane) Paste() bool {
+ clip, _ := clipboard.ReadAll("clipboard")
+ h.paste(clip)
+ h.Relocate()
+ return true
+}
+
+// PastePrimary pastes from the primary clipboard (only use on linux)
+func (h *BufPane) PastePrimary() bool {
+ clip, _ := clipboard.ReadAll("primary")
+ h.paste(clip)
+ h.Relocate()
+ return true
+}
+
+func (h *BufPane) paste(clip string) {
+ if h.Buf.Settings["smartpaste"].(bool) {
+ if h.Cursor.X > 0 && len(util.GetLeadingWhitespace([]byte(strings.TrimLeft(clip, "\r\n")))) == 0 {
+ leadingWS := util.GetLeadingWhitespace(h.Buf.LineBytes(h.Cursor.Y))
+ clip = strings.Replace(clip, "\n", "\n"+string(leadingWS), -1)
+ }
+ }
+
+ if h.Cursor.HasSelection() {
+ h.Cursor.DeleteSelection()
+ h.Cursor.ResetSelection()
+ }
+
+ h.Buf.Insert(h.Cursor.Loc, clip)
+ // h.Cursor.Loc = h.Cursor.Loc.Move(Count(clip), h.Buf)
+ h.freshClip = false
+ if clipboard.Unsupported {
+ InfoBar.Message("Pasted clipboard (install xclip for external clipboard)")
+ } else {
+ InfoBar.Message("Pasted clipboard")
+ }
+}
+
+// JumpToMatchingBrace moves the cursor to the matching brace if it is
+// currently on a brace
+func (h *BufPane) JumpToMatchingBrace() bool {
+ for _, bp := range buffer.BracePairs {
+ r := h.Cursor.RuneUnder(h.Cursor.X)
+ rl := h.Cursor.RuneUnder(h.Cursor.X - 1)
+ if r == bp[0] || r == bp[1] || rl == bp[0] || rl == bp[1] {
+ matchingBrace, left := h.Buf.FindMatchingBrace(bp, h.Cursor.Loc)
+ if left {
+ h.Cursor.GotoLoc(matchingBrace)
+ } else {
+ h.Cursor.GotoLoc(matchingBrace.Move(1, h.Buf))
+ }
+ }
+ }
+
+ h.Relocate()
+ return true
+}
+
+// SelectAll selects the entire buffer
+func (h *BufPane) SelectAll() bool {
+ h.Cursor.SetSelectionStart(h.Buf.Start())
+ h.Cursor.SetSelectionEnd(h.Buf.End())
+ // Put the cursor at the beginning
+ h.Cursor.X = 0
+ h.Cursor.Y = 0
+ h.Relocate()
+ return true
+}
+
+// OpenFile opens a new file in the buffer
+func (h *BufPane) OpenFile() bool {
+ InfoBar.Prompt("> ", "open ", "Open", nil, func(resp string, canceled bool) {
+ if !canceled {
+ h.HandleCommand(resp)
+ }
+ })
+ return true
+}
+
+// Start moves the viewport to the start of the buffer
+func (h *BufPane) Start() bool {
+ v := h.GetView()
+ v.StartLine = 0
+ h.SetView(v)
+ return true
+}
+
+// End moves the viewport to the end of the buffer
+func (h *BufPane) End() bool {
+ // TODO: softwrap problems?
+ v := h.GetView()
+ if v.Height > h.Buf.LinesNum() {
+ v.StartLine = 0
+ h.SetView(v)
+ } else {
+ v.StartLine = h.Buf.LinesNum() - v.Height
+ h.SetView(v)
+ }
+ return true
+}
+
+// PageUp scrolls the view up a page
+func (h *BufPane) PageUp() bool {
+ v := h.GetView()
+ if v.StartLine > v.Height {
+ h.ScrollUp(v.Height)
+ } else {
+ v.StartLine = 0
+ }
+ h.SetView(v)
+ return true
+}
+
+// PageDown scrolls the view down a page
+func (h *BufPane) PageDown() bool {
+ v := h.GetView()
+ if h.Buf.LinesNum()-(v.StartLine+v.Height) > v.Height {
+ h.ScrollDown(v.Height)
+ } else if h.Buf.LinesNum() >= v.Height {
+ v.StartLine = h.Buf.LinesNum() - v.Height
+ }
+ return true
+}
+
+// SelectPageUp selects up one page
+func (h *BufPane) SelectPageUp() bool {
+ if !h.Cursor.HasSelection() {
+ h.Cursor.OrigSelection[0] = h.Cursor.Loc
+ }
+ h.Cursor.UpN(h.GetView().Height)
+ h.Cursor.SelectTo(h.Cursor.Loc)
+ h.Relocate()
+ return true
+}
+
+// SelectPageDown selects down one page
+func (h *BufPane) SelectPageDown() bool {
+ if !h.Cursor.HasSelection() {
+ h.Cursor.OrigSelection[0] = h.Cursor.Loc
+ }
+ h.Cursor.DownN(h.GetView().Height)
+ h.Cursor.SelectTo(h.Cursor.Loc)
+ h.Relocate()
+ return true
+}
+
+// CursorPageUp places the cursor a page up
+func (h *BufPane) CursorPageUp() bool {
+ h.Cursor.Deselect(true)
+
+ if h.Cursor.HasSelection() {
+ h.Cursor.Loc = h.Cursor.CurSelection[0]
+ h.Cursor.ResetSelection()
+ h.Cursor.StoreVisualX()
+ }
+ h.Cursor.UpN(h.GetView().Height)
+ h.Relocate()
+ return true
+}
+
+// CursorPageDown places the cursor a page up
+func (h *BufPane) CursorPageDown() bool {
+ h.Cursor.Deselect(false)
+
+ if h.Cursor.HasSelection() {
+ h.Cursor.Loc = h.Cursor.CurSelection[1]
+ h.Cursor.ResetSelection()
+ h.Cursor.StoreVisualX()
+ }
+ h.Cursor.DownN(h.GetView().Height)
+ h.Relocate()
+ return true
+}
+
+// HalfPageUp scrolls the view up half a page
+func (h *BufPane) HalfPageUp() bool {
+ v := h.GetView()
+ if v.StartLine > v.Height/2 {
+ h.ScrollUp(v.Height / 2)
+ } else {
+ v.StartLine = 0
+ }
+ h.SetView(v)
+ return true
+}
+
+// HalfPageDown scrolls the view down half a page
+func (h *BufPane) HalfPageDown() bool {
+ v := h.GetView()
+ if h.Buf.LinesNum()-(v.StartLine+v.Height) > v.Height/2 {
+ h.ScrollDown(v.Height / 2)
+ } else {
+ if h.Buf.LinesNum() >= v.Height {
+ v.StartLine = h.Buf.LinesNum() - v.Height
+ }
+ }
+ h.SetView(v)
+ return true
+}
+
+// ToggleRuler turns line numbers off and on
+func (h *BufPane) ToggleRuler() bool {
+ if !h.Buf.Settings["ruler"].(bool) {
+ h.Buf.Settings["ruler"] = true
+ InfoBar.Message("Enabled ruler")
+ } else {
+ h.Buf.Settings["ruler"] = false
+ InfoBar.Message("Disabled ruler")
+ }
+ return true
+}
+
+// ClearStatus clears the messenger bar
+func (h *BufPane) ClearStatus() bool {
+ InfoBar.Message("")
+ return true
+}
+
+// ToggleHelp toggles the help screen
+func (h *BufPane) ToggleHelp() bool {
+ if h.Buf.Type == buffer.BTHelp {
+ h.Quit()
+ } else {
+ h.openHelp("help")
+ }
+ return true
+}
+
+// ToggleKeyMenu toggles the keymenu option and resizes all tabs
+func (h *BufPane) ToggleKeyMenu() bool {
+ config.GlobalSettings["keymenu"] = !config.GetGlobalOption("keymenu").(bool)
+ Tabs.Resize()
+ return true
+}
+
+// ShellMode opens a terminal to run a shell command
+func (h *BufPane) ShellMode() bool {
+ InfoBar.Prompt("$ ", "", "Shell", nil, func(resp string, canceled bool) {
+ if !canceled {
+ // The true here is for openTerm to make the command interactive
+ shell.RunInteractiveShell(resp, true, false)
+ }
+ })
+
+ return true
+}
+
+// CommandMode lets the user enter a command
+func (h *BufPane) CommandMode() bool {
+ InfoBar.Prompt("> ", "", "Command", nil, func(resp string, canceled bool) {
+ if !canceled {
+ h.HandleCommand(resp)
+ }
+ })
+ return true
+}
+
+// ToggleOverwriteMode lets the user toggle the text overwrite mode
+func (h *BufPane) ToggleOverwriteMode() bool {
+ h.isOverwriteMode = !h.isOverwriteMode
+ return true
+}
+
+// Escape leaves current mode
+func (h *BufPane) Escape() bool {
+ return true
+}
+
+// Quit this will close the current tab or view that is open
+func (h *BufPane) Quit() bool {
+ quit := func() {
+ h.Buf.Close()
+ if len(MainTab().Panes) > 1 {
+ h.Unsplit()
+ } else if len(Tabs.List) > 1 {
+ Tabs.RemoveTab(h.splitID)
+ } else {
+ screen.Screen.Fini()
+ InfoBar.Close()
+ runtime.Goexit()
+ }
+ }
+ if h.Buf.Modified() {
+ // if config.GlobalSettings["autosave"].(float64) > 0 {
+ // autosave on means we automatically save when quitting
+ // h.Save()
+ // quit()
+ // } else {
+ InfoBar.YNPrompt("Save changes to "+h.Buf.GetName()+" before closing? (y,n,esc)", func(yes, canceled bool) {
+ if !canceled && !yes {
+ quit()
+ } else if !canceled && yes {
+ h.Save()
+ quit()
+ }
+ })
+ // }
+ } else {
+ quit()
+ }
+ return true
+}
+
+// QuitAll quits the whole editor; all splits and tabs
+func (h *BufPane) QuitAll() bool {
+ anyModified := false
+ for _, b := range buffer.OpenBuffers {
+ if b.Modified() {
+ anyModified = true
+ break
+ }
+ }
+
+ quit := func() {
+ for _, b := range buffer.OpenBuffers {
+ b.Close()
+ }
+ screen.Screen.Fini()
+ InfoBar.Close()
+ runtime.Goexit()
+ }
+
+ if anyModified {
+ InfoBar.YNPrompt("Quit micro? (all open buffers will be closed without saving)", func(yes, canceled bool) {
+ if !canceled && yes {
+ quit()
+ }
+ })
+ } else {
+ quit()
+ }
+
+ return true
+}
+
+// AddTab adds a new tab with an empty buffer
+func (h *BufPane) AddTab() bool {
+ width, height := screen.Screen.Size()
+ iOffset := config.GetInfoBarOffset()
+ b := buffer.NewBufferFromString("", "", buffer.BTDefault)
+ tp := NewTabFromBuffer(0, 0, width, height-iOffset, b)
+ Tabs.AddTab(tp)
+ Tabs.SetActive(len(Tabs.List) - 1)
+
+ return true
+}
+
+// PreviousTab switches to the previous tab in the tab list
+func (h *BufPane) PreviousTab() bool {
+ a := Tabs.Active()
+ Tabs.SetActive(util.Clamp(a-1, 0, len(Tabs.List)-1))
+
+ return true
+}
+
+// NextTab switches to the next tab in the tab list
+func (h *BufPane) NextTab() bool {
+ a := Tabs.Active()
+ Tabs.SetActive(util.Clamp(a+1, 0, len(Tabs.List)-1))
+ return true
+}
+
+// VSplitAction opens an empty vertical split
+func (h *BufPane) VSplitAction() bool {
+ h.VSplitBuf(buffer.NewBufferFromString("", "", buffer.BTDefault))
+
+ return true
+}
+
+// HSplitAction opens an empty horizontal split
+func (h *BufPane) HSplitAction() bool {
+ h.HSplitBuf(buffer.NewBufferFromString("", "", buffer.BTDefault))
+
+ return true
+}
+
+// Unsplit closes all splits in the current tab except the active one
+func (h *BufPane) Unsplit() bool {
+ n := MainTab().GetNode(h.splitID)
+ n.Unsplit()
+
+ MainTab().RemovePane(MainTab().GetPane(h.splitID))
+ MainTab().Resize()
+ MainTab().SetActive(len(MainTab().Panes) - 1)
+ return true
+}
+
+// NextSplit changes the view to the next split
+func (h *BufPane) NextSplit() bool {
+ a := MainTab().active
+ if a < len(MainTab().Panes)-1 {
+ a++
+ } else {
+ a = 0
+ }
+
+ MainTab().SetActive(a)
+
+ return true
+}
+
+// PreviousSplit changes the view to the previous split
+func (h *BufPane) PreviousSplit() bool {
+ a := MainTab().active
+ if a > 0 {
+ a--
+ } else {
+ a = len(MainTab().Panes) - 1
+ }
+ MainTab().SetActive(a)
+
+ return true
+}
+
+var curmacro []interface{}
+var recording_macro bool
+
+// ToggleMacro toggles recording of a macro
+func (h *BufPane) ToggleMacro() bool {
+ recording_macro = !recording_macro
+ if recording_macro {
+ curmacro = []interface{}{}
+ InfoBar.Message("Recording")
+ } else {
+ InfoBar.Message("Stopped recording")
+ }
+ h.Relocate()
+ return true
+}
+
+// PlayMacro plays back the most recently recorded macro
+func (h *BufPane) PlayMacro() bool {
+ if recording_macro {
+ return false
+ }
+ for _, action := range curmacro {
+ switch t := action.(type) {
+ case rune:
+ h.DoRuneInsert(t)
+ case func(*BufPane) bool:
+ t(h)
+ }
+ }
+ h.Relocate()
+ return true
+}
+
+// SpawnMultiCursor creates a new multiple cursor at the next occurrence of the current selection or current word
+func (h *BufPane) SpawnMultiCursor() bool {
+ spawner := h.Buf.GetCursor(h.Buf.NumCursors() - 1)
+ if !spawner.HasSelection() {
+ spawner.SelectWord()
+ h.multiWord = true
+ h.Relocate()
+ return true
+ }
+
+ sel := spawner.GetSelection()
+ searchStart := spawner.CurSelection[1]
+
+ search := string(sel)
+ search = regexp.QuoteMeta(search)
+ if h.multiWord {
+ search = "\\b" + search + "\\b"
+ }
+ match, found, err := h.Buf.FindNext(search, h.Buf.Start(), h.Buf.End(), searchStart, true, true)
+ if err != nil {
+ InfoBar.Error(err)
+ }
+ if found {
+ c := buffer.NewCursor(h.Buf, buffer.Loc{})
+ c.SetSelectionStart(match[0])
+ c.SetSelectionEnd(match[1])
+ c.OrigSelection[0] = c.CurSelection[0]
+ c.OrigSelection[1] = c.CurSelection[1]
+ c.Loc = c.CurSelection[1]
+
+ h.Buf.AddCursor(c)
+ h.Buf.SetCurCursor(h.Buf.NumCursors() - 1)
+ h.Buf.MergeCursors()
+ } else {
+ InfoBar.Message("No matches found")
+ }
+
+ h.Relocate()
+ return true
+}
+
++// SpawnMultiCursorUp creates additional cursor, at the same X (if possible), one Y less.
++func (h *BufPane) SpawnMultiCursorUp() bool {
++ if h.Cursor.Y == 0 {
++ return false
++ } else {
++ h.Cursor.GotoLoc(buffer.Loc{h.Cursor.X, h.Cursor.Y - 1})
++ h.Cursor.Relocate()
++ }
++
++ c := buffer.NewCursor(h.Buf, buffer.Loc{h.Cursor.X, h.Cursor.Y + 1})
++ h.Buf.AddCursor(c)
++ h.Buf.SetCurCursor(h.Buf.NumCursors() - 1)
++ h.Buf.MergeCursors()
++
++ h.Relocate()
++ return true
++}
++
++// SpawnMultiCursorUp creates additional cursor, at the same X (if possible), one Y more.
++func (h *BufPane) SpawnMultiCursorDown() bool {
++ if h.Cursor.Y+1 == h.Buf.LinesNum() {
++ return false
++ } else {
++ h.Cursor.GotoLoc(buffer.Loc{h.Cursor.X, h.Cursor.Y + 1})
++ h.Cursor.Relocate()
++ }
++
++ c := buffer.NewCursor(h.Buf, buffer.Loc{h.Cursor.X, h.Cursor.Y - 1})
++ h.Buf.AddCursor(c)
++ h.Buf.SetCurCursor(h.Buf.NumCursors() - 1)
++ h.Buf.MergeCursors()
++ h.Relocate()
++ return true
++}
++
+// SpawnMultiCursorSelect adds a cursor at the beginning of each line of a selection
+func (h *BufPane) SpawnMultiCursorSelect() bool {
+ // Avoid cases where multiple cursors already exist, that would create problems
+ if h.Buf.NumCursors() > 1 {
+ return false
+ }
+
+ var startLine int
+ var endLine int
+
+ a, b := h.Cursor.CurSelection[0].Y, h.Cursor.CurSelection[1].Y
+ if a > b {
+ startLine, endLine = b, a
+ } else {
+ startLine, endLine = a, b
+ }
+
+ if h.Cursor.HasSelection() {
+ h.Cursor.ResetSelection()
+ h.Cursor.GotoLoc(buffer.Loc{0, startLine})
+
+ for i := startLine; i <= endLine; i++ {
+ c := buffer.NewCursor(h.Buf, buffer.Loc{0, i})
+ c.StoreVisualX()
+ h.Buf.AddCursor(c)
+ }
+ h.Buf.MergeCursors()
+ } else {
+ return false
+ }
+ InfoBar.Message("Added cursors from selection")
+ return true
+}
+
+// MouseMultiCursor is a mouse action which puts a new cursor at the mouse position
+func (h *BufPane) MouseMultiCursor(e *tcell.EventMouse) bool {
+ b := h.Buf
+ mx, my := e.Position()
+ mouseLoc := h.LocFromVisual(buffer.Loc{X: mx, Y: my})
+ c := buffer.NewCursor(b, mouseLoc)
+ b.AddCursor(c)
+ b.MergeCursors()
+
+ return true
+}
+
+// SkipMultiCursor moves the current multiple cursor to the next available position
+func (h *BufPane) SkipMultiCursor() bool {
+ lastC := h.Buf.GetCursor(h.Buf.NumCursors() - 1)
+ sel := lastC.GetSelection()
+ searchStart := lastC.CurSelection[1]
+
+ search := string(sel)
+ search = regexp.QuoteMeta(search)
+ if h.multiWord {
+ search = "\\b" + search + "\\b"
+ }
+
+ match, found, err := h.Buf.FindNext(search, h.Buf.Start(), h.Buf.End(), searchStart, true, true)
+ if err != nil {
+ InfoBar.Error(err)
+ }
+ if found {
+ lastC.SetSelectionStart(match[0])
+ lastC.SetSelectionEnd(match[1])
+ lastC.OrigSelection[0] = lastC.CurSelection[0]
+ lastC.OrigSelection[1] = lastC.CurSelection[1]
+ lastC.Loc = lastC.CurSelection[1]
+
+ h.Buf.MergeCursors()
+ h.Buf.SetCurCursor(h.Buf.NumCursors() - 1)
+ } else {
+ InfoBar.Message("No matches found")
+ }
+ h.Relocate()
+ return true
+}
+
+// RemoveMultiCursor removes the latest multiple cursor
+func (h *BufPane) RemoveMultiCursor() bool {
+ if h.Buf.NumCursors() > 1 {
+ h.Buf.RemoveCursor(h.Buf.NumCursors() - 1)
+ h.Buf.SetCurCursor(h.Buf.NumCursors() - 1)
+ h.Buf.UpdateCursors()
+ } else {
+ h.multiWord = false
+ }
+ h.Relocate()
+ return true
+}
+
+// RemoveAllMultiCursors removes all cursors except the base cursor
+func (h *BufPane) RemoveAllMultiCursors() bool {
+ h.Buf.ClearCursors()
+ h.multiWord = false
+ h.Relocate()
+ return true
+}
+
+// None is an action that does nothing
+func (h *BufPane) None() bool {
+ return true
+}
--- /dev/null
+package action
+
+import (
+ "strings"
+ "time"
+
+ luar "layeh.com/gopher-luar"
+
+ lua "github.com/yuin/gopher-lua"
+ "github.com/zyedidia/micro/internal/buffer"
+ "github.com/zyedidia/micro/internal/config"
+ "github.com/zyedidia/micro/internal/display"
+ ulua "github.com/zyedidia/micro/internal/lua"
+ "github.com/zyedidia/micro/internal/screen"
+ "github.com/zyedidia/tcell"
+)
+
+type BufKeyAction func(*BufPane) bool
+type BufMouseAction func(*BufPane, *tcell.EventMouse) bool
+
+var BufKeyBindings map[Event]BufKeyAction
+var BufKeyStrings map[Event]string
+var BufMouseBindings map[MouseEvent]BufMouseAction
+
+func init() {
+ BufKeyBindings = make(map[Event]BufKeyAction)
+ BufKeyStrings = make(map[Event]string)
+ BufMouseBindings = make(map[MouseEvent]BufMouseAction)
+}
+
+func LuaAction(fn string) func(*BufPane) bool {
+ luaFn := strings.Split(fn, ".")
+ if len(luaFn) <= 1 {
+ return nil
+ }
+ plName, plFn := luaFn[0], luaFn[1]
+ pl := config.FindPlugin(plName)
+ if pl == nil {
+ return nil
+ }
+ return func(h *BufPane) bool {
+ val, err := pl.Call(plFn, luar.New(ulua.L, h))
+ if err != nil {
+ screen.TermMessage(err)
+ }
+ if v, ok := val.(lua.LBool); !ok {
+ return false
+ } else {
+ return bool(v)
+ }
+ }
+}
+
+// BufMapKey maps a key event to an action
+func BufMapKey(k Event, action string) {
+ BufKeyStrings[k] = action
+ var actionfns []func(*BufPane) bool
+ var names []string
+ var types []byte
+ for i := 0; ; i++ {
+ if action == "" {
+ break
+ }
+
+ // TODO: fix problem when complex bindings have these
+ // characters (escape them?)
+ idx := strings.IndexAny(action, "&|,")
+ a := action
+ if idx >= 0 {
+ a = action[:idx]
+ types = append(types, action[idx])
+ action = action[idx+1:]
+ } else {
+ types = append(types, ' ')
+ action = ""
+ }
+
+ var afn func(*BufPane) bool
+ if strings.HasPrefix(a, "command:") {
+ a = strings.SplitN(a, ":", 2)[1]
+ afn = CommandAction(a)
+ names = append(names, "")
+ } else if strings.HasPrefix(a, "command-edit:") {
+ a = strings.SplitN(a, ":", 2)[1]
+ afn = CommandEditAction(a)
+ names = append(names, "")
+ } else if strings.HasPrefix(a, "lua:") {
+ a = strings.SplitN(a, ":", 2)[1]
+ afn = LuaAction(a)
+ if afn == nil {
+ screen.TermMessage("Lua Error:", a, "does not exist")
+ continue
+ }
+ split := strings.SplitN(a, ".", 2)
+ if len(split) > 1 {
+ a = strings.Title(split[0]) + strings.Title(split[1])
+ } else {
+ a = strings.Title(a)
+ }
+
+ names = append(names, a)
+ } else if f, ok := BufKeyActions[a]; ok {
+ afn = f
+ names = append(names, a)
+ } else {
+ screen.TermMessage("Error:", a, "does not exist")
+ continue
+ }
+ actionfns = append(actionfns, afn)
+ }
+ BufKeyBindings[k] = func(h *BufPane) bool {
+ cursors := h.Buf.GetCursors()
+ success := true
+ for i, a := range actionfns {
+ for j, c := range cursors {
+ h.Buf.SetCurCursor(c.Num)
+ h.Cursor = c
+ if i == 0 || (success && types[i-1] == '&') || (!success && types[i-1] == '|') || (types[i-1] == ',') {
+ success = h.execAction(a, names[i], j)
+ } else {
+ break
+ }
+ }
+ }
+ return true
+ }
+}
+
+// BufMapMouse maps a mouse event to an action
+func BufMapMouse(k MouseEvent, action string) {
+ if f, ok := BufMouseActions[action]; ok {
+ BufMouseBindings[k] = f
+ } else {
+ delete(BufMouseBindings, k)
+ BufMapKey(k, action)
+ }
+}
+
+// The BufPane connects the buffer and the window
+// It provides a cursor (or multiple) and defines a set of actions
+// that can be taken on the buffer
+// The ActionHandler can access the window for necessary info about
+// visual positions for mouse clicks and scrolling
+type BufPane struct {
+ display.BWindow
+
+ Buf *buffer.Buffer
+
+ Cursor *buffer.Cursor // the active cursor
+
+ // Since tcell doesn't differentiate between a mouse release event
+ // and a mouse move event with no keys pressed, we need to keep
+ // track of whether or not the mouse was pressed (or not released) last event to determine
+ // mouse release events
+ mouseReleased bool
+
+ // We need to keep track of insert key press toggle
+ isOverwriteMode bool
+ // This stores when the last click was
+ // This is useful for detecting double and triple clicks
+ lastClickTime time.Time
+ lastLoc buffer.Loc
+
+ // lastCutTime stores when the last ctrl+k was issued.
+ // It is used for clearing the clipboard to replace it with fresh cut lines.
+ lastCutTime time.Time
+
+ // freshClip returns true if the clipboard has never been pasted.
+ freshClip bool
+
+ // Was the last mouse event actually a double click?
+ // Useful for detecting triple clicks -- if a double click is detected
+ // but the last mouse event was actually a double click, it's a triple click
+ doubleClick bool
+ // Same here, just to keep track for mouse move events
+ tripleClick bool
+
+ // Last search stores the last successful search for FindNext and FindPrev
+ lastSearch string
+ // Should the current multiple cursor selection search based on word or
+ // based on selection (false for selection, true for word)
+ multiWord bool
+
+ splitID uint64
+ tab *Tab
+
+ // remember original location of a search in case the search is canceled
+ searchOrig buffer.Loc
+}
+
+func NewBufPane(buf *buffer.Buffer, win display.BWindow, tab *Tab) *BufPane {
+ h := new(BufPane)
+ h.Buf = buf
+ h.BWindow = win
+ h.tab = tab
+
+ h.Cursor = h.Buf.GetActiveCursor()
+ h.mouseReleased = true
+
+ config.RunPluginFn("onBufPaneOpen", luar.New(ulua.L, h))
+
+ return h
+}
+
+func NewBufPaneFromBuf(buf *buffer.Buffer, tab *Tab) *BufPane {
+ w := display.NewBufWindow(0, 0, 0, 0, buf)
+ return NewBufPane(buf, w, tab)
+}
+
+func (h *BufPane) SetTab(t *Tab) {
+ h.tab = t
+}
+
+func (h *BufPane) Tab() *Tab {
+ return h.tab
+}
+
+func (h *BufPane) ResizePane(size int) {
+ n := h.tab.GetNode(h.splitID)
+ n.ResizeSplit(size)
+ h.tab.Resize()
+}
+
+// PluginCB calls all plugin callbacks with a certain name and
+// displays an error if there is one and returns the aggregrate
+// boolean response
+func (h *BufPane) PluginCB(cb string) bool {
+ b, err := config.RunPluginFnBool(cb, luar.New(ulua.L, h))
+ if err != nil {
+ screen.TermMessage(err)
+ }
+ return b
+}
+
+// PluginCBRune is the same as PluginCB but also passes a rune to
+// the plugins
+func (h *BufPane) PluginCBRune(cb string, r rune) bool {
+ b, err := config.RunPluginFnBool(cb, luar.New(ulua.L, h), luar.New(ulua.L, string(r)))
+ if err != nil {
+ screen.TermMessage(err)
+ }
+ return b
+}
+
+func (h *BufPane) OpenBuffer(b *buffer.Buffer) {
+ h.Buf.Close()
+ h.Buf = b
+ h.BWindow.SetBuffer(b)
+ h.Cursor = b.GetActiveCursor()
+ h.Resize(h.GetView().Width, h.GetView().Height)
+ h.Relocate()
+ // Set mouseReleased to true because we assume the mouse is not being pressed when
+ // the editor is opened
+ h.mouseReleased = true
+ // Set isOverwriteMode to false, because we assume we are in the default mode when editor
+ // is opened
+ h.isOverwriteMode = false
+ h.lastClickTime = time.Time{}
+}
+
+func (h *BufPane) ID() uint64 {
+ return h.splitID
+}
+
+func (h *BufPane) SetID(i uint64) {
+ h.splitID = i
+}
+
+func (h *BufPane) Name() string {
+ return h.Buf.GetName()
+}
+
+// HandleEvent executes the tcell event properly
+func (h *BufPane) HandleEvent(event tcell.Event) {
+ if h.Buf.ExternallyModified() {
+ InfoBar.YNPrompt("The file on disk has changed. Reload file? (y,n)", func(yes, canceled bool) {
+ if !yes || canceled {
+ h.Buf.UpdateModTime()
+ } else {
+ h.Buf.ReOpen()
+ }
+ })
+
+ }
+
+ switch e := event.(type) {
+ case *tcell.EventRaw:
+ re := RawEvent{
+ esc: e.EscSeq(),
+ }
+ h.DoKeyEvent(re)
+ case *tcell.EventPaste:
+ h.paste(e.Text())
+ h.Relocate()
+ case *tcell.EventKey:
+ ke := KeyEvent{
+ code: e.Key(),
+ mod: e.Modifiers(),
+ r: e.Rune(),
+ }
+
+ done := h.DoKeyEvent(ke)
+ if !done && e.Key() == tcell.KeyRune {
+ h.DoRuneInsert(e.Rune())
+ }
+ case *tcell.EventMouse:
+ cancel := false
+ switch e.Buttons() {
+ case tcell.Button1:
+ _, my := e.Position()
+ if h.Buf.Settings["statusline"].(bool) && my >= h.GetView().Y+h.GetView().Height-1 {
+ cancel = true
+ }
+ case tcell.ButtonNone:
+ // Mouse event with no click
+ if !h.mouseReleased {
+ // Mouse was just released
+
+ // mx, my := e.Position()
+ // mouseLoc := h.LocFromVisual(buffer.Loc{X: mx, Y: my})
+
+ // we could finish the selection based on the release location as described
+ // below but when the mouse click is within the scroll margin this will
+ // cause a scroll and selection even for a simple mouse click which is
+ // not good
+ // for terminals that don't support mouse motion events, selection via
+ // the mouse won't work but this is ok
+
+ // 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
+ // events, this still allows the user to make selections, except only after they
+ // release the mouse
+
+ // if !h.doubleClick && !h.tripleClick {
+ // h.Cursor.Loc = mouseLoc
+ // h.Cursor.SetSelectionEnd(h.Cursor.Loc)
+ // h.Cursor.CopySelection("primary")
+ // }
+ h.mouseReleased = true
+ }
+ }
+
+ if !cancel {
+ me := MouseEvent{
+ btn: e.Buttons(),
+ mod: e.Modifiers(),
+ }
+ h.DoMouseEvent(me, e)
+ }
+ }
+ h.Buf.MergeCursors()
+
+ if h.IsActive() {
+ // Display any gutter messages for this line
+ c := h.Buf.GetActiveCursor()
+ none := true
+ for _, m := range h.Buf.Messages {
+ if c.Y == m.Start.Y || c.Y == m.End.Y {
+ InfoBar.GutterMessage(m.Msg)
+ none = false
+ break
+ }
+ }
+ if none && InfoBar.HasGutter {
+ InfoBar.ClearGutter()
+ }
+ }
+}
+
+// DoKeyEvent executes a key event by finding the action it is bound
+// to and executing it (possibly multiple times for multiple cursors)
+func (h *BufPane) DoKeyEvent(e Event) bool {
+ if action, ok := BufKeyBindings[e]; ok {
+ return action(h)
+ }
+ return false
+}
+
+func (h *BufPane) execAction(action func(*BufPane) bool, name string, cursor int) bool {
+ if name != "Autocomplete" && name != "CycleAutocompleteBack" {
+ h.Buf.HasSuggestions = false
+ }
+
+ _, isMulti := MultiActions[name]
+ if (!isMulti && cursor == 0) || isMulti {
+ if h.PluginCB("pre" + name) {
+ success := action(h)
+ success = success && h.PluginCB("on"+name)
+
+ if isMulti {
+ if recording_macro {
+ if name != "ToggleMacro" && name != "PlayMacro" {
+ curmacro = append(curmacro, action)
+ }
+ }
+ }
+
+ return success
+ }
+ }
+
+ return false
+}
+
+func (h *BufPane) completeAction(action string) {
+ h.PluginCB("on" + action)
+}
+
+func (h *BufPane) HasKeyEvent(e Event) bool {
+ _, ok := BufKeyBindings[e]
+ return ok
+}
+
+// DoMouseEvent executes a mouse event by finding the action it is bound
+// to and executing it
+func (h *BufPane) DoMouseEvent(e MouseEvent, te *tcell.EventMouse) bool {
+ if action, ok := BufMouseBindings[e]; ok {
+ if action(h, te) {
+ h.Relocate()
+ }
+ return true
+ } else if h.HasKeyEvent(e) {
+ return h.DoKeyEvent(e)
+ }
+ return false
+}
+
+// DoRuneInsert inserts a given rune into the current buffer
+// (possibly multiple times for multiple cursors)
+func (h *BufPane) DoRuneInsert(r rune) {
+ cursors := h.Buf.GetCursors()
+ for _, c := range cursors {
+ // Insert a character
+ h.Buf.SetCurCursor(c.Num)
+ h.Cursor = c
+ if !h.PluginCBRune("preRune", r) {
+ continue
+ }
+ if c.HasSelection() {
+ c.DeleteSelection()
+ c.ResetSelection()
+ }
+
+ if h.isOverwriteMode {
+ next := c.Loc
+ next.X++
+ h.Buf.Replace(c.Loc, next, string(r))
+ } else {
+ h.Buf.Insert(c.Loc, string(r))
+ }
+ if recording_macro {
+ curmacro = append(curmacro, r)
+ }
+ h.PluginCBRune("onRune", r)
+ }
+}
+
+func (h *BufPane) VSplitIndex(buf *buffer.Buffer, right bool) *BufPane {
+ e := NewBufPaneFromBuf(buf, h.tab)
+ e.splitID = MainTab().GetNode(h.splitID).VSplit(right)
+ MainTab().Panes = append(MainTab().Panes, e)
+ MainTab().Resize()
+ MainTab().SetActive(len(MainTab().Panes) - 1)
+ return e
+}
+func (h *BufPane) HSplitIndex(buf *buffer.Buffer, bottom bool) *BufPane {
+ e := NewBufPaneFromBuf(buf, h.tab)
+ e.splitID = MainTab().GetNode(h.splitID).HSplit(bottom)
+ MainTab().Panes = append(MainTab().Panes, e)
+ MainTab().Resize()
+ MainTab().SetActive(len(MainTab().Panes) - 1)
+ return e
+}
+
+func (h *BufPane) VSplitBuf(buf *buffer.Buffer) *BufPane {
+ return h.VSplitIndex(buf, h.Buf.Settings["splitright"].(bool))
+}
+func (h *BufPane) HSplitBuf(buf *buffer.Buffer) *BufPane {
+ return h.HSplitIndex(buf, h.Buf.Settings["splitbottom"].(bool))
+}
+func (h *BufPane) Close() {
+ h.Buf.Close()
+}
+
+func (h *BufPane) SetActive(b bool) {
+ h.BWindow.SetActive(b)
+ if b {
+ // Display any gutter messages for this line
+ c := h.Buf.GetActiveCursor()
+ none := true
+ for _, m := range h.Buf.Messages {
+ if c.Y == m.Start.Y || c.Y == m.End.Y {
+ InfoBar.GutterMessage(m.Msg)
+ none = false
+ break
+ }
+ }
+ if none && InfoBar.HasGutter {
+ InfoBar.ClearGutter()
+ }
+ }
+
+}
+
+// BufKeyActions contains the list of all possible key actions the bufhandler could execute
+var BufKeyActions = map[string]BufKeyAction{
+ "CursorUp": (*BufPane).CursorUp,
+ "CursorDown": (*BufPane).CursorDown,
+ "CursorPageUp": (*BufPane).CursorPageUp,
+ "CursorPageDown": (*BufPane).CursorPageDown,
+ "CursorLeft": (*BufPane).CursorLeft,
+ "CursorRight": (*BufPane).CursorRight,
+ "CursorStart": (*BufPane).CursorStart,
+ "CursorEnd": (*BufPane).CursorEnd,
+ "SelectToStart": (*BufPane).SelectToStart,
+ "SelectToEnd": (*BufPane).SelectToEnd,
+ "SelectUp": (*BufPane).SelectUp,
+ "SelectDown": (*BufPane).SelectDown,
+ "SelectLeft": (*BufPane).SelectLeft,
+ "SelectRight": (*BufPane).SelectRight,
+ "WordRight": (*BufPane).WordRight,
+ "WordLeft": (*BufPane).WordLeft,
+ "SelectWordRight": (*BufPane).SelectWordRight,
+ "SelectWordLeft": (*BufPane).SelectWordLeft,
+ "DeleteWordRight": (*BufPane).DeleteWordRight,
+ "DeleteWordLeft": (*BufPane).DeleteWordLeft,
+ "SelectLine": (*BufPane).SelectLine,
+ "SelectToStartOfLine": (*BufPane).SelectToStartOfLine,
+ "SelectToStartOfText": (*BufPane).SelectToStartOfText,
+ "SelectToEndOfLine": (*BufPane).SelectToEndOfLine,
+ "ParagraphPrevious": (*BufPane).ParagraphPrevious,
+ "ParagraphNext": (*BufPane).ParagraphNext,
+ "InsertNewline": (*BufPane).InsertNewline,
+ "Backspace": (*BufPane).Backspace,
+ "Delete": (*BufPane).Delete,
+ "InsertTab": (*BufPane).InsertTab,
+ "Save": (*BufPane).Save,
+ "SaveAll": (*BufPane).SaveAll,
+ "SaveAs": (*BufPane).SaveAs,
+ "Find": (*BufPane).Find,
+ "FindNext": (*BufPane).FindNext,
+ "FindPrevious": (*BufPane).FindPrevious,
+ "Center": (*BufPane).Center,
+ "Undo": (*BufPane).Undo,
+ "Redo": (*BufPane).Redo,
+ "Copy": (*BufPane).Copy,
+ "Cut": (*BufPane).Cut,
+ "CutLine": (*BufPane).CutLine,
+ "DuplicateLine": (*BufPane).DuplicateLine,
+ "DeleteLine": (*BufPane).DeleteLine,
+ "MoveLinesUp": (*BufPane).MoveLinesUp,
+ "MoveLinesDown": (*BufPane).MoveLinesDown,
+ "IndentSelection": (*BufPane).IndentSelection,
+ "OutdentSelection": (*BufPane).OutdentSelection,
+ "Autocomplete": (*BufPane).Autocomplete,
+ "CycleAutocompleteBack": (*BufPane).CycleAutocompleteBack,
+ "OutdentLine": (*BufPane).OutdentLine,
+ "Paste": (*BufPane).Paste,
+ "PastePrimary": (*BufPane).PastePrimary,
+ "SelectAll": (*BufPane).SelectAll,
+ "OpenFile": (*BufPane).OpenFile,
+ "Start": (*BufPane).Start,
+ "End": (*BufPane).End,
+ "PageUp": (*BufPane).PageUp,
+ "PageDown": (*BufPane).PageDown,
+ "SelectPageUp": (*BufPane).SelectPageUp,
+ "SelectPageDown": (*BufPane).SelectPageDown,
+ "HalfPageUp": (*BufPane).HalfPageUp,
+ "HalfPageDown": (*BufPane).HalfPageDown,
+ "StartOfText": (*BufPane).StartOfText,
+ "StartOfLine": (*BufPane).StartOfLine,
+ "EndOfLine": (*BufPane).EndOfLine,
+ "ToggleHelp": (*BufPane).ToggleHelp,
+ "ToggleKeyMenu": (*BufPane).ToggleKeyMenu,
+ "ToggleRuler": (*BufPane).ToggleRuler,
+ "ClearStatus": (*BufPane).ClearStatus,
+ "ShellMode": (*BufPane).ShellMode,
+ "CommandMode": (*BufPane).CommandMode,
+ "ToggleOverwriteMode": (*BufPane).ToggleOverwriteMode,
+ "Escape": (*BufPane).Escape,
+ "Quit": (*BufPane).Quit,
+ "QuitAll": (*BufPane).QuitAll,
+ "AddTab": (*BufPane).AddTab,
+ "PreviousTab": (*BufPane).PreviousTab,
+ "NextTab": (*BufPane).NextTab,
+ "NextSplit": (*BufPane).NextSplit,
+ "PreviousSplit": (*BufPane).PreviousSplit,
+ "Unsplit": (*BufPane).Unsplit,
+ "VSplit": (*BufPane).VSplitAction,
+ "HSplit": (*BufPane).HSplitAction,
+ "ToggleMacro": (*BufPane).ToggleMacro,
+ "PlayMacro": (*BufPane).PlayMacro,
+ "Suspend": (*BufPane).Suspend,
+ "ScrollUp": (*BufPane).ScrollUpAction,
+ "ScrollDown": (*BufPane).ScrollDownAction,
+ "SpawnMultiCursor": (*BufPane).SpawnMultiCursor,
++ "SpawnMultiCursorUp": (*BufPane).SpawnMultiCursorUp,
++ "SpawnMultiCursorDown": (*BufPane).SpawnMultiCursorDown,
+ "SpawnMultiCursorSelect": (*BufPane).SpawnMultiCursorSelect,
+ "RemoveMultiCursor": (*BufPane).RemoveMultiCursor,
+ "RemoveAllMultiCursors": (*BufPane).RemoveAllMultiCursors,
+ "SkipMultiCursor": (*BufPane).SkipMultiCursor,
+ "JumpToMatchingBrace": (*BufPane).JumpToMatchingBrace,
+ "None": (*BufPane).None,
+
+ // This was changed to InsertNewline but I don't want to break backwards compatibility
+ "InsertEnter": (*BufPane).InsertNewline,
+}
+
+// BufMouseActions contains the list of all possible mouse actions the bufhandler could execute
+var BufMouseActions = map[string]BufMouseAction{
+ "MousePress": (*BufPane).MousePress,
+ "MouseMultiCursor": (*BufPane).MouseMultiCursor,
+}
+
+// MultiActions is a list of actions that should be executed multiple
+// times if there are multiple cursors (one per cursor)
+// Generally actions that modify global editor state like quitting or
+// saving should not be included in this list
+var MultiActions = map[string]bool{
+ "CursorUp": true,
+ "CursorDown": true,
+ "CursorPageUp": true,
+ "CursorPageDown": true,
+ "CursorLeft": true,
+ "CursorRight": true,
+ "CursorStart": true,
+ "CursorEnd": true,
+ "SelectToStart": true,
+ "SelectToEnd": true,
+ "SelectUp": true,
+ "SelectDown": true,
+ "SelectLeft": true,
+ "SelectRight": true,
+ "WordRight": true,
+ "WordLeft": true,
+ "SelectWordRight": true,
+ "SelectWordLeft": true,
+ "DeleteWordRight": true,
+ "DeleteWordLeft": true,
+ "SelectLine": true,
+ "SelectToStartOfLine": true,
+ "SelectToStartOfText": true,
+ "SelectToEndOfLine": true,
+ "ParagraphPrevious": true,
+ "ParagraphNext": true,
+ "InsertNewline": true,
+ "Backspace": true,
+ "Delete": true,
+ "InsertTab": true,
+ "FindNext": true,
+ "FindPrevious": true,
+ "Cut": true,
+ "CutLine": true,
+ "DuplicateLine": true,
+ "DeleteLine": true,
+ "MoveLinesUp": true,
+ "MoveLinesDown": true,
+ "IndentSelection": true,
+ "OutdentSelection": true,
+ "OutdentLine": true,
+ "Paste": true,
+ "PastePrimary": true,
+ "SelectPageUp": true,
+ "SelectPageDown": true,
+ "StartOfLine": true,
+ "StartOfText": true,
+ "EndOfLine": true,
+ "JumpToMatchingBrace": true,
+}