micro/internal/action/actions.go

1974 lines
48 KiB
Go
Raw Normal View History

2018-08-27 23:53:08 +00:00
package action
2018-08-27 19:53:10 +00:00
import (
"errors"
"fmt"
"io/fs"
2023-01-30 03:23:21 +00:00
"os"
2019-01-16 22:52:30 +00:00
"regexp"
2019-12-22 18:43:29 +00:00
"runtime"
2019-01-02 21:27:27 +00:00
"strings"
2018-12-31 21:36:54 +00:00
"time"
2018-08-27 19:53:10 +00:00
2020-01-02 23:30:51 +00:00
shellquote "github.com/kballard/go-shellquote"
2020-05-04 14:16:15 +00:00
"github.com/zyedidia/micro/v2/internal/buffer"
"github.com/zyedidia/micro/v2/internal/clipboard"
2020-05-04 14:16:15 +00:00
"github.com/zyedidia/micro/v2/internal/config"
"github.com/zyedidia/micro/v2/internal/display"
2020-05-04 14:16:15 +00:00
"github.com/zyedidia/micro/v2/internal/screen"
"github.com/zyedidia/micro/v2/internal/shell"
"github.com/zyedidia/micro/v2/internal/util"
2020-09-05 18:52:35 +00:00
"github.com/zyedidia/tcell/v2"
2018-08-27 19:53:10 +00:00
)
2019-01-01 03:07:01 +00:00
// ScrollUp is not an action
2019-01-19 20:37:59 +00:00
func (h *BufPane) ScrollUp(n int) {
2019-01-11 02:26:58 +00:00
v := h.GetView()
v.StartLine = h.Scroll(v.StartLine, -n)
h.SetView(v)
2019-01-01 03:07:01 +00:00
}
// ScrollDown is not an action
2019-01-19 20:37:59 +00:00
func (h *BufPane) ScrollDown(n int) {
2019-01-11 02:26:58 +00:00
v := h.GetView()
v.StartLine = h.Scroll(v.StartLine, n)
h.SetView(v)
}
2021-08-21 22:04:08 +00:00
// ScrollAdjust can be used to shift the view so that the last line is at the
// bottom if the user has scrolled past the last line.
func (h *BufPane) ScrollAdjust() {
v := h.GetView()
end := h.SLocFromLoc(h.Buf.End())
if h.Diff(v.StartLine, end) < h.BufView().Height-1 {
v.StartLine = h.Scroll(end, -h.BufView().Height+1)
2019-01-01 03:07:01 +00:00
}
h.SetView(v)
2019-01-01 03:07:01 +00:00
}
2018-08-27 19:53:10 +00:00
// MousePress is the event that should happen when a normal click happens
// This is almost always bound to left click
2019-01-19 20:37:59 +00:00
func (h *BufPane) MousePress(e *tcell.EventMouse) bool {
2019-01-02 22:39:50 +00:00
b := h.Buf
mx, my := e.Position()
// ignore click on the status line
if my >= h.BufView().Y+h.BufView().Height {
return false
}
2019-12-24 21:01:08 +00:00
mouseLoc := h.LocFromVisual(buffer.Loc{mx, my})
2019-01-02 22:39:50 +00:00
h.Cursor.Loc = mouseLoc
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()
2019-01-02 22:39:50 +00:00
h.tripleClick = true
h.doubleClick = false
2019-01-02 22:39:50 +00:00
h.Cursor.SelectLine()
h.Cursor.CopySelection(clipboard.PrimaryReg)
2019-01-02 22:39:50 +00:00
} else {
// Double click
2019-01-02 22:39:50 +00:00
h.lastClickTime = time.Now()
h.doubleClick = true
h.tripleClick = false
h.Cursor.SelectWord()
h.Cursor.CopySelection(clipboard.PrimaryReg)
2019-01-02 22:39:50 +00:00
}
} 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
2019-01-02 22:39:50 +00:00
}
2019-08-05 03:23:32 +00:00
h.Cursor.StoreVisualX()
2019-01-02 22:39:50 +00:00
h.lastLoc = mouseLoc
2020-08-12 05:18:15 +00:00
h.Relocate()
return true
2018-08-27 19:53:10 +00:00
}
func (h *BufPane) MouseDrag(e *tcell.EventMouse) bool {
mx, my := e.Position()
// ignore drag on the status line
if my >= h.BufView().Y+h.BufView().Height {
return false
}
h.Cursor.Loc = h.LocFromVisual(buffer.Loc{mx, my})
if h.tripleClick {
h.Cursor.AddLineToSelection()
} else if h.doubleClick {
h.Cursor.AddWordToSelection()
} else {
h.Cursor.SetSelectionEnd(h.Cursor.Loc)
}
h.Cursor.StoreVisualX()
h.Relocate()
return true
}
func (h *BufPane) MouseRelease(e *tcell.EventMouse) bool {
// We could finish the selection based on the release location as in the
// commented out code below, to allow text selections even in a terminal
// that doesn't support mouse motion events. But when the mouse click is
// within the scroll margin, that would cause a scroll and selection
// even for a simple mouse click, which is not good.
// if !h.doubleClick && !h.tripleClick {
// mx, my := e.Position()
// h.Cursor.Loc = h.LocFromVisual(buffer.Loc{mx, my})
// h.Cursor.SetSelectionEnd(h.Cursor.Loc)
// }
if h.Cursor.HasSelection() {
h.Cursor.CopySelection(clipboard.PrimaryReg)
}
return true
}
2018-08-27 19:53:10 +00:00
// ScrollUpAction scrolls the view up
2019-01-19 20:37:59 +00:00
func (h *BufPane) ScrollUpAction() bool {
2019-01-01 04:47:24 +00:00
h.ScrollUp(util.IntOpt(h.Buf.Settings["scrollspeed"]))
return true
2018-08-27 19:53:10 +00:00
}
// ScrollDownAction scrolls the view up
2019-01-19 20:37:59 +00:00
func (h *BufPane) ScrollDownAction() bool {
2019-01-01 04:47:24 +00:00
h.ScrollDown(util.IntOpt(h.Buf.Settings["scrollspeed"]))
return true
2018-08-27 19:53:10 +00:00
}
// Center centers the view on the cursor
2019-01-19 20:37:59 +00:00
func (h *BufPane) Center() bool {
2019-01-11 02:26:58 +00:00
v := h.GetView()
v.StartLine = h.Scroll(h.SLocFromLoc(h.Cursor.Loc), -h.BufView().Height/2)
2019-01-11 02:26:58 +00:00
h.SetView(v)
h.ScrollAdjust()
2018-08-27 19:53:10 +00:00
return true
}
// MoveCursorUp is not an action
func (h *BufPane) MoveCursorUp(n int) {
if !h.Buf.Settings["softwrap"].(bool) {
h.Cursor.UpN(n)
} else {
vloc := h.VLocFromLoc(h.Cursor.Loc)
sloc := h.Scroll(vloc.SLoc, -n)
if sloc == vloc.SLoc {
// we are at the beginning of buffer
h.Cursor.Loc = h.Buf.Start()
h.Cursor.LastVisualX = 0
} else {
vloc.SLoc = sloc
vloc.VisualX = h.Cursor.LastVisualX
h.Cursor.Loc = h.LocFromVLoc(vloc)
}
}
}
// MoveCursorDown is not an action
func (h *BufPane) MoveCursorDown(n int) {
if !h.Buf.Settings["softwrap"].(bool) {
h.Cursor.DownN(n)
} else {
vloc := h.VLocFromLoc(h.Cursor.Loc)
sloc := h.Scroll(vloc.SLoc, n)
if sloc == vloc.SLoc {
// we are at the end of buffer
h.Cursor.Loc = h.Buf.End()
vloc = h.VLocFromLoc(h.Cursor.Loc)
h.Cursor.LastVisualX = vloc.VisualX
} else {
vloc.SLoc = sloc
vloc.VisualX = h.Cursor.LastVisualX
h.Cursor.Loc = h.LocFromVLoc(vloc)
}
}
}
2018-08-27 19:53:10 +00:00
// CursorUp moves the cursor up
2019-01-19 20:37:59 +00:00
func (h *BufPane) CursorUp() bool {
2018-08-28 22:44:52 +00:00
h.Cursor.Deselect(true)
h.MoveCursorUp(1)
h.Relocate()
2018-08-27 19:53:10 +00:00
return true
}
// CursorDown moves the cursor down
2019-01-19 20:37:59 +00:00
func (h *BufPane) CursorDown() bool {
2018-08-28 22:44:52 +00:00
h.Cursor.Deselect(true)
h.MoveCursorDown(1)
h.Relocate()
2018-08-27 19:53:10 +00:00
return true
}
// CursorLeft moves the cursor left
2019-01-19 20:37:59 +00:00
func (h *BufPane) CursorLeft() bool {
2019-06-15 20:03:02 +00:00
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()
2018-08-27 19:53:10 +00:00
return true
}
// CursorRight moves the cursor right
2019-01-19 20:37:59 +00:00
func (h *BufPane) CursorRight() bool {
2019-06-15 20:03:02 +00:00
if h.Cursor.HasSelection() {
h.Cursor.Deselect(false)
h.Cursor.Right()
2019-06-15 20:03:02 +00:00
} 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)
2020-05-20 20:47:08 +00:00
if h.Cursor.X+tabsize < util.CharacterCount(line) && util.IsSpaces(line[h.Cursor.X:h.Cursor.X+tabsize]) && util.IsBytesWhitespace(line[0:h.Cursor.X]) {
2019-06-15 20:03:02 +00:00
for i := 0; i < tabsize; i++ {
h.Cursor.Right()
}
} else {
h.Cursor.Right()
}
} else {
h.Cursor.Right()
}
}
h.Relocate()
2018-08-27 19:53:10 +00:00
return true
}
// WordRight moves the cursor one word to the right
2019-01-19 20:37:59 +00:00
func (h *BufPane) WordRight() bool {
2019-01-05 02:48:19 +00:00
h.Cursor.Deselect(false)
2018-08-28 22:44:52 +00:00
h.Cursor.WordRight()
h.Relocate()
2018-08-27 19:53:10 +00:00
return true
}
// WordLeft moves the cursor one word to the left
2019-01-19 20:37:59 +00:00
func (h *BufPane) WordLeft() bool {
2018-08-28 22:44:52 +00:00
h.Cursor.Deselect(true)
h.Cursor.WordLeft()
h.Relocate()
2018-08-27 19:53:10 +00:00
return true
}
// SelectUp selects up one line
2019-01-19 20:37:59 +00:00
func (h *BufPane) SelectUp() bool {
2018-08-28 22:44:52 +00:00
if !h.Cursor.HasSelection() {
h.Cursor.OrigSelection[0] = h.Cursor.Loc
}
h.MoveCursorUp(1)
2018-08-28 22:44:52 +00:00
h.Cursor.SelectTo(h.Cursor.Loc)
h.Relocate()
2018-08-27 19:53:10 +00:00
return true
}
// SelectDown selects down one line
2019-01-19 20:37:59 +00:00
func (h *BufPane) SelectDown() bool {
2018-08-28 22:44:52 +00:00
if !h.Cursor.HasSelection() {
h.Cursor.OrigSelection[0] = h.Cursor.Loc
}
h.MoveCursorDown(1)
2018-08-28 22:44:52 +00:00
h.Cursor.SelectTo(h.Cursor.Loc)
h.Relocate()
2018-08-27 19:53:10 +00:00
return true
}
// SelectLeft selects the character to the left of the cursor
2019-01-19 20:37:59 +00:00
func (h *BufPane) SelectLeft() bool {
2018-08-28 22:44:52 +00:00
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()
2018-08-27 19:53:10 +00:00
return true
}
// SelectRight selects the character to the right of the cursor
2019-01-19 20:37:59 +00:00
func (h *BufPane) SelectRight() bool {
2018-08-28 22:44:52 +00:00
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()
2018-08-27 19:53:10 +00:00
return true
}
// SelectWordRight selects the word to the right of the cursor
2019-01-19 20:37:59 +00:00
func (h *BufPane) SelectWordRight() bool {
2018-08-28 22:44:52 +00:00
if !h.Cursor.HasSelection() {
h.Cursor.OrigSelection[0] = h.Cursor.Loc
}
h.Cursor.WordRight()
h.Cursor.SelectTo(h.Cursor.Loc)
h.Relocate()
2018-08-27 19:53:10 +00:00
return true
}
// SelectWordLeft selects the word to the left of the cursor
2019-01-19 20:37:59 +00:00
func (h *BufPane) SelectWordLeft() bool {
2018-08-28 22:44:52 +00:00
if !h.Cursor.HasSelection() {
h.Cursor.OrigSelection[0] = h.Cursor.Loc
}
h.Cursor.WordLeft()
h.Cursor.SelectTo(h.Cursor.Loc)
h.Relocate()
2018-08-27 19:53:10 +00:00
return true
}
// StartOfText 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
}
// StartOfTextToggle toggles the cursor between the start of the text of the line
// and the start of the line
func (h *BufPane) StartOfTextToggle() bool {
h.Cursor.Deselect(true)
if h.Cursor.IsStartOfText() {
h.Cursor.Start()
} else {
h.Cursor.StartOfText()
}
h.Relocate()
return true
}
2018-08-27 19:53:10 +00:00
// StartOfLine moves the cursor to the start of the line
2019-01-19 20:37:59 +00:00
func (h *BufPane) StartOfLine() bool {
2018-08-28 22:44:52 +00:00
h.Cursor.Deselect(true)
h.Cursor.Start()
h.Relocate()
2018-08-27 19:53:10 +00:00
return true
}
// EndOfLine moves the cursor to the end of the line
2019-01-19 20:37:59 +00:00
func (h *BufPane) EndOfLine() bool {
2018-08-28 22:44:52 +00:00
h.Cursor.Deselect(true)
h.Cursor.End()
h.Relocate()
2018-08-27 19:53:10 +00:00
return true
}
// SelectLine selects the entire current line
2019-01-19 20:37:59 +00:00
func (h *BufPane) SelectLine() bool {
2018-08-28 22:44:52 +00:00
h.Cursor.SelectLine()
h.Relocate()
2018-08-27 19:53:10 +00:00
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
}
// SelectToStartOfTextToggle toggles the selection between the start of the text
// on the current line and the start of the line
func (h *BufPane) SelectToStartOfTextToggle() bool {
if !h.Cursor.HasSelection() {
h.Cursor.OrigSelection[0] = h.Cursor.Loc
}
if h.Cursor.IsStartOfText() {
h.Cursor.Start()
} else {
h.Cursor.StartOfText()
}
h.Cursor.SelectTo(h.Cursor.Loc)
h.Relocate()
return true
}
2018-08-27 19:53:10 +00:00
// SelectToStartOfLine selects to the start of the current line
2019-01-19 20:37:59 +00:00
func (h *BufPane) SelectToStartOfLine() bool {
2018-08-28 22:44:52 +00:00
if !h.Cursor.HasSelection() {
h.Cursor.OrigSelection[0] = h.Cursor.Loc
}
h.Cursor.Start()
h.Cursor.SelectTo(h.Cursor.Loc)
h.Relocate()
2018-08-27 19:53:10 +00:00
return true
}
// SelectToEndOfLine selects to the end of the current line
2019-01-19 20:37:59 +00:00
func (h *BufPane) SelectToEndOfLine() bool {
2018-08-28 22:44:52 +00:00
if !h.Cursor.HasSelection() {
h.Cursor.OrigSelection[0] = h.Cursor.Loc
}
h.Cursor.End()
h.Cursor.SelectTo(h.Cursor.Loc)
h.Relocate()
2018-08-27 19:53:10 +00:00
return true
}
// ParagraphPrevious moves the cursor to the previous empty line, or beginning of the buffer if there's none
2019-01-19 20:37:59 +00:00
func (h *BufPane) ParagraphPrevious() bool {
2018-08-28 22:44:52 +00:00
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()
2018-08-27 19:53:10 +00:00
return true
}
// ParagraphNext moves the cursor to the next empty line, or end of the buffer if there's none
2019-01-19 20:37:59 +00:00
func (h *BufPane) ParagraphNext() bool {
2018-08-28 22:44:52 +00:00
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()
2018-08-27 19:53:10 +00:00
return true
}
// Retab changes all tabs to spaces or all spaces to tabs depending
// on the user's settings
2019-01-19 20:37:59 +00:00
func (h *BufPane) Retab() bool {
2019-01-15 03:44:06 +00:00
h.Buf.Retab()
h.Relocate()
2018-08-27 19:53:10 +00:00
return true
}
// CursorStart moves the cursor to the start of the buffer
2019-01-19 20:37:59 +00:00
func (h *BufPane) CursorStart() bool {
2018-08-28 22:44:52 +00:00
h.Cursor.Deselect(true)
h.Cursor.X = 0
h.Cursor.Y = 0
h.Cursor.StoreVisualX()
h.Relocate()
2018-08-27 19:53:10 +00:00
return true
}
// CursorEnd moves the cursor to the end of the buffer
2019-01-19 20:37:59 +00:00
func (h *BufPane) CursorEnd() bool {
2018-08-28 22:44:52 +00:00
h.Cursor.Deselect(true)
h.Cursor.Loc = h.Buf.End()
h.Cursor.StoreVisualX()
h.Relocate()
2018-08-27 19:53:10 +00:00
return true
}
// SelectToStart selects the text from the cursor to the start of the buffer
2019-01-19 20:37:59 +00:00
func (h *BufPane) SelectToStart() bool {
2018-08-28 22:44:52 +00:00
if !h.Cursor.HasSelection() {
h.Cursor.OrigSelection[0] = h.Cursor.Loc
}
h.CursorStart()
h.Cursor.SelectTo(h.Buf.Start())
h.Relocate()
2018-08-27 19:53:10 +00:00
return true
}
// SelectToEnd selects the text from the cursor to the end of the buffer
2019-01-19 20:37:59 +00:00
func (h *BufPane) SelectToEnd() bool {
2018-08-28 22:44:52 +00:00
if !h.Cursor.HasSelection() {
h.Cursor.OrigSelection[0] = h.Cursor.Loc
}
h.CursorEnd()
h.Cursor.SelectTo(h.Buf.End())
h.Relocate()
2018-08-27 19:53:10 +00:00
return true
}
// InsertNewline inserts a newline plus possible some whitespace if autoindent is on
2019-01-19 20:37:59 +00:00
func (h *BufPane) InsertNewline() bool {
2019-01-01 04:47:24 +00:00
// 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")
2019-01-01 04:47:24 +00:00
// 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))
2019-01-01 04:47:24 +00:00
// 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)
2020-05-20 20:47:08 +00:00
h.Buf.Remove(buffer.Loc{X: 0, Y: h.Cursor.Y - 1}, buffer.Loc{X: util.CharacterCount(line), Y: h.Cursor.Y - 1})
2019-01-01 04:47:24 +00:00
}
}
h.Cursor.LastVisualX = h.Cursor.GetVisualX()
h.Relocate()
2018-08-27 19:53:10 +00:00
return true
}
// Backspace deletes the previous character
2019-01-19 20:37:59 +00:00
func (h *BufPane) Backspace() bool {
2018-08-29 03:30:39 +00:00
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))
2020-05-20 20:47:08 +00:00
if h.Buf.Settings["tabstospaces"].(bool) && util.IsSpaces(lineStart) && len(lineStart) != 0 && util.CharacterCount(lineStart)%tabSize == 0 {
2018-08-29 03:30:39 +00:00
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()
2018-08-27 19:53:10 +00:00
return true
}
// DeleteWordRight deletes the word to the right of the cursor
2019-01-19 20:37:59 +00:00
func (h *BufPane) DeleteWordRight() bool {
2018-09-03 20:54:56 +00:00
h.SelectWordRight()
if h.Cursor.HasSelection() {
h.Cursor.DeleteSelection()
h.Cursor.ResetSelection()
}
h.Relocate()
2018-08-27 19:53:10 +00:00
return true
}
// DeleteWordLeft deletes the word to the left of the cursor
2019-01-19 20:37:59 +00:00
func (h *BufPane) DeleteWordLeft() bool {
2018-09-03 20:54:56 +00:00
h.SelectWordLeft()
if h.Cursor.HasSelection() {
h.Cursor.DeleteSelection()
h.Cursor.ResetSelection()
}
h.Relocate()
2018-08-27 19:53:10 +00:00
return true
}
// Delete deletes the next character
2019-01-19 20:37:59 +00:00
func (h *BufPane) Delete() bool {
2018-09-03 20:54:56 +00:00
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()
2018-08-27 19:53:10 +00:00
return true
}
// IndentSelection indents the current selection
2019-01-19 20:37:59 +00:00
func (h *BufPane) IndentSelection() bool {
2018-09-03 20:54:56 +00:00
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++ {
2020-01-26 05:40:40 +00:00
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})
}
2018-09-03 20:54:56 +00:00
}
}
2019-01-24 23:25:59 +00:00
h.Buf.RelocateCursors()
2018-09-03 20:54:56 +00:00
h.Relocate()
2018-09-03 20:54:56 +00:00
return true
}
return false
2018-08-27 19:53:10 +00:00
}
// IndentLine moves the current line forward one indentation
func (h *BufPane) IndentLine() bool {
if h.Cursor.HasSelection() {
return false
}
tabsize := int(h.Buf.Settings["tabsize"].(float64))
indentstr := h.Buf.IndentString(tabsize)
h.Buf.Insert(buffer.Loc{X: 0, Y: h.Cursor.Y}, indentstr)
h.Buf.RelocateCursors()
h.Relocate()
return true
}
2018-08-27 19:53:10 +00:00
// OutdentLine moves the current line back one indentation
2019-01-19 20:37:59 +00:00
func (h *BufPane) OutdentLine() bool {
2018-12-31 21:36:54 +00:00
if h.Cursor.HasSelection() {
return false
}
2019-01-01 04:47:24 +00:00
for x := 0; x < len(h.Buf.IndentString(util.IntOpt(h.Buf.Settings["tabsize"]))); x++ {
2018-12-31 21:36:54 +00:00
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})
}
2019-01-24 23:25:59 +00:00
h.Buf.RelocateCursors()
h.Relocate()
2018-08-27 19:53:10 +00:00
return true
}
// OutdentSelection takes the current selection and moves it back one indent level
2019-01-19 20:37:59 +00:00
func (h *BufPane) OutdentSelection() bool {
2018-12-31 21:36:54 +00:00
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++ {
2019-01-01 04:47:24 +00:00
for x := 0; x < len(h.Buf.IndentString(util.IntOpt(h.Buf.Settings["tabsize"]))); x++ {
2018-12-31 21:36:54 +00:00
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})
}
}
2019-01-24 23:25:59 +00:00
h.Buf.RelocateCursors()
2018-12-31 21:36:54 +00:00
h.Relocate()
2018-12-31 21:36:54 +00:00
return true
}
return false
2018-08-27 19:53:10 +00:00
}
// Autocomplete cycles the suggestions and performs autocompletion if there are suggestions
func (h *BufPane) Autocomplete() bool {
2019-06-16 19:56:39 +00:00
b := h.Buf
if h.Cursor.HasSelection() {
return false
}
if h.Cursor.X == 0 {
return false
}
r := h.Cursor.RuneUnder(h.Cursor.X)
prev := h.Cursor.RuneUnder(h.Cursor.X - 1)
if !util.IsAutocomplete(prev) || !util.IsNonAlphaNumeric(r) {
// don't autocomplete if cursor is on alpha numeric character (middle of a word)
return false
}
2019-06-16 19:56:39 +00:00
if b.HasSuggestions {
b.CycleAutocomplete(true)
return true
}
return b.Autocomplete(buffer.BufferComplete)
}
2019-06-16 19:56:39 +00:00
2020-01-04 20:51:15 +00:00
// 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()
2018-08-27 19:53:10 +00:00
return true
}
// SaveAll saves all open buffers
2019-01-19 20:37:59 +00:00
func (h *BufPane) SaveAll() bool {
2019-01-14 21:09:46 +00:00
for _, b := range buffer.OpenBuffers {
b.Save()
}
return true
2018-08-27 19:53:10 +00:00
}
2020-02-09 21:36:15 +00:00
// SaveCB performs a save and does a callback at the very end (after all prompts have been resolved)
func (h *BufPane) SaveCB(action string, callback func()) bool {
2019-01-17 03:15:11 +00:00
// If this is an empty buffer, ask for a filename
if h.Buf.Path == "" {
2020-02-09 21:36:15 +00:00
h.SaveAsCB(action, callback)
2019-01-17 03:15:11 +00:00
} else {
2020-02-09 21:36:15 +00:00
noPrompt := h.saveBufToFile(h.Buf.Path, action, callback)
2020-01-03 22:38:50 +00:00
if noPrompt {
return true
}
2019-01-17 03:15:11 +00:00
}
return false
2018-08-27 19:53:10 +00:00
}
2020-02-09 21:36:15 +00:00
// Save the buffer to disk
func (h *BufPane) Save() bool {
return h.SaveCB("Save", nil)
}
// SaveAsCB performs a save as and does a callback at the very end (after all prompts have been resolved)
// The callback is only called if the save was successful
2020-02-09 21:36:15 +00:00
func (h *BufPane) SaveAsCB(action string, callback func()) bool {
2019-01-17 03:15:11 +00:00
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.
2020-01-02 23:30:51 +00:00
args, err := shellquote.Split(resp)
2019-01-17 03:15:11 +00:00
if err != nil {
InfoBar.Error("Error parsing arguments: ", err)
return
}
if len(args) == 0 {
InfoBar.Error("No filename given")
return
}
filename := strings.Join(args, " ")
fileinfo, err := os.Stat(filename)
if err != nil {
if errors.Is(err, fs.ErrNotExist) || errors.Is(err, fs.ErrPermission) {
noPrompt := h.saveBufToFile(filename, action, callback)
if noPrompt {
h.completeAction(action)
return
}
}
} else {
InfoBar.YNPrompt(
fmt.Sprintf("The file %s already exists in the directory, would you like to overwrite? Y/n", fileinfo.Name()),
func(yes, canceled bool) {
if yes && !canceled {
noPrompt := h.saveBufToFile(filename, action, callback)
if noPrompt {
h.completeAction(action)
}
}
},
)
}
2019-01-17 03:15:11 +00:00
}
})
return false
2018-08-27 19:53:10 +00:00
}
2020-02-09 21:36:15 +00:00
// SaveAs saves the buffer to disk with the given name
func (h *BufPane) SaveAs() bool {
return h.SaveAsCB("SaveAs", nil)
}
2019-01-17 03:15:11 +00:00
// This function saves the buffer to `filename` and changes the buffer's path and name
// to `filename` if the save is successful
// The callback is only called if the save was successful
2020-02-09 21:36:15 +00:00
func (h *BufPane) saveBufToFile(filename string, action string, callback func()) bool {
2019-01-17 03:15:11 +00:00
err := h.Buf.SaveAs(filename)
if err != nil {
if errors.Is(err, fs.ErrPermission) {
saveWithSudo := func() {
err = h.Buf.SaveAsWithSudo(filename)
if err != nil {
InfoBar.Error(err)
} else {
h.Buf.Path = filename
h.Buf.SetName(filename)
InfoBar.Message("Saved " + filename)
if callback != nil {
callback()
}
2020-02-09 21:36:15 +00:00
}
}
if h.Buf.Settings["autosu"].(bool) {
saveWithSudo()
} else {
InfoBar.YNPrompt(
fmt.Sprintf("Permission denied. Do you want to save this file using %s? (y,n)", config.GlobalSettings["sucmd"].(string)),
func(yes, canceled bool) {
if yes && !canceled {
saveWithSudo()
h.completeAction(action)
}
},
)
return false
}
2019-01-17 03:15:11 +00:00
} else {
InfoBar.Error(err)
}
} else {
h.Buf.Path = filename
h.Buf.SetName(filename)
InfoBar.Message("Saved " + filename)
if callback != nil {
callback()
}
2020-02-09 21:36:15 +00:00
}
2020-01-03 22:38:50 +00:00
return true
2019-01-17 03:15:11 +00:00
}
2018-08-27 19:53:10 +00:00
// Find opens a prompt and searches forward for the input
2019-01-19 20:37:59 +00:00
func (h *BufPane) Find() bool {
return h.find(true)
}
// FindLiteral is the same as Find() but does not support regular expressions
func (h *BufPane) FindLiteral() bool {
return h.find(false)
}
2020-06-23 22:47:42 +00:00
// Search searches for a given string/regex in the buffer and selects the next
// match if a match is found
// This function behaves the same way as Find and FindLiteral actions:
// it affects the buffer's LastSearch and LastSearchRegex (saved searches)
// for use with FindNext and FindPrevious, and turns HighlightSearch on or off
// according to hlsearch setting
2020-06-23 22:47:42 +00:00
func (h *BufPane) Search(str string, useRegex bool, searchDown bool) error {
match, found, err := h.Buf.FindNext(str, h.Buf.Start(), h.Buf.End(), h.Cursor.Loc, searchDown, useRegex)
if err != nil {
return 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]
Improve buffer view relocation after jumping to a far-away location (#2628) * Improve buffer view relocation after jumping to a far-away location When the cursor is moved to a location which is far away from the current location (e.g. after a search or a goto line), the buffer view is always relocated in such a way that the cursor is at the bottom or at the top (minus scrollmargin), i.e. as if we just scrolled to this location. It's not like in other editors, and IMHO it's annoying. When we jump to a new location far away, we usually want to see more of its context, so the cursor should be placed closer to the center of the view, not near its edges. This change implements the behavior similar to other editors: - If the distance between the new and the old location is less than one frame (i.e. the view either doesn't change or just slightly "shifts") then the current behavior remains unchanged. - Otherwise the current line is placed at 25% of the window height. * Postpone calling onBufPaneOpen until the initial resize It is currently not possible to find out the geometry of a newly created bufpane in onBufPaneOpen lua callback: bp:GetView() returns {0,0,0,0} instead of the actual window. The reason is that the bufpane view is not properly initialized yet when the bufpane is created and the callback is triggered. It is initialized a bit later, at the initial resize. So postpone calling onBufPaneOpen until after the initial resize. * Improve buffer view relocation when opening a file at a far-away location When a file is opened with the initial cursor location at a given line which is far away from the beginning of the file, the buffer view is relocated so that the cursor is at the bottom (minus scrollmargin) as if we just scrolled to this line, which is annoying since we'd rather like to see more of the context of this initial location. So implement the behavior similar to the earlier commit (which addresses a similar issue about jumping far away after a search or goto): - If the initial cursor location is less than one frame away from the beginning of the buffer, keep the existing behavior i.e. just display the beginning of the buffer. - Otherwise place the cursor location at 25% of the window height.
2022-12-03 03:38:09 +00:00
h.GotoLoc(h.Cursor.CurSelection[1])
h.Buf.LastSearch = str
h.Buf.LastSearchRegex = useRegex
h.Buf.HighlightSearch = h.Buf.Settings["hlsearch"].(bool)
2020-06-23 22:47:42 +00:00
} else {
h.Cursor.ResetSelection()
}
return nil
}
func (h *BufPane) find(useRegex bool) bool {
2019-06-15 18:44:03 +00:00
h.searchOrig = h.Cursor.Loc
prompt := "Find: "
if useRegex {
prompt = "Find (regex): "
}
var eventCallback func(resp string)
2021-01-27 18:49:38 +00:00
if h.Buf.Settings["incsearch"].(bool) {
eventCallback = func(resp string) {
match, found, _ := h.Buf.FindNext(resp, h.Buf.Start(), h.Buf.End(), h.searchOrig, true, useRegex)
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]
Improve buffer view relocation after jumping to a far-away location (#2628) * Improve buffer view relocation after jumping to a far-away location When the cursor is moved to a location which is far away from the current location (e.g. after a search or a goto line), the buffer view is always relocated in such a way that the cursor is at the bottom or at the top (minus scrollmargin), i.e. as if we just scrolled to this location. It's not like in other editors, and IMHO it's annoying. When we jump to a new location far away, we usually want to see more of its context, so the cursor should be placed closer to the center of the view, not near its edges. This change implements the behavior similar to other editors: - If the distance between the new and the old location is less than one frame (i.e. the view either doesn't change or just slightly "shifts") then the current behavior remains unchanged. - Otherwise the current line is placed at 25% of the window height. * Postpone calling onBufPaneOpen until the initial resize It is currently not possible to find out the geometry of a newly created bufpane in onBufPaneOpen lua callback: bp:GetView() returns {0,0,0,0} instead of the actual window. The reason is that the bufpane view is not properly initialized yet when the bufpane is created and the callback is triggered. It is initialized a bit later, at the initial resize. So postpone calling onBufPaneOpen until after the initial resize. * Improve buffer view relocation when opening a file at a far-away location When a file is opened with the initial cursor location at a given line which is far away from the beginning of the file, the buffer view is relocated so that the cursor is at the bottom (minus scrollmargin) as if we just scrolled to this line, which is annoying since we'd rather like to see more of the context of this initial location. So implement the behavior similar to the earlier commit (which addresses a similar issue about jumping far away after a search or goto): - If the initial cursor location is less than one frame away from the beginning of the buffer, keep the existing behavior i.e. just display the beginning of the buffer. - Otherwise place the cursor location at 25% of the window height.
2022-12-03 03:38:09 +00:00
h.GotoLoc(match[1])
} else {
Improve buffer view relocation after jumping to a far-away location (#2628) * Improve buffer view relocation after jumping to a far-away location When the cursor is moved to a location which is far away from the current location (e.g. after a search or a goto line), the buffer view is always relocated in such a way that the cursor is at the bottom or at the top (minus scrollmargin), i.e. as if we just scrolled to this location. It's not like in other editors, and IMHO it's annoying. When we jump to a new location far away, we usually want to see more of its context, so the cursor should be placed closer to the center of the view, not near its edges. This change implements the behavior similar to other editors: - If the distance between the new and the old location is less than one frame (i.e. the view either doesn't change or just slightly "shifts") then the current behavior remains unchanged. - Otherwise the current line is placed at 25% of the window height. * Postpone calling onBufPaneOpen until the initial resize It is currently not possible to find out the geometry of a newly created bufpane in onBufPaneOpen lua callback: bp:GetView() returns {0,0,0,0} instead of the actual window. The reason is that the bufpane view is not properly initialized yet when the bufpane is created and the callback is triggered. It is initialized a bit later, at the initial resize. So postpone calling onBufPaneOpen until after the initial resize. * Improve buffer view relocation when opening a file at a far-away location When a file is opened with the initial cursor location at a given line which is far away from the beginning of the file, the buffer view is relocated so that the cursor is at the bottom (minus scrollmargin) as if we just scrolled to this line, which is annoying since we'd rather like to see more of the context of this initial location. So implement the behavior similar to the earlier commit (which addresses a similar issue about jumping far away after a search or goto): - If the initial cursor location is less than one frame away from the beginning of the buffer, keep the existing behavior i.e. just display the beginning of the buffer. - Otherwise place the cursor location at 25% of the window height.
2022-12-03 03:38:09 +00:00
h.GotoLoc(h.searchOrig)
h.Cursor.ResetSelection()
}
2019-01-03 20:27:43 +00:00
}
}
findCallback := func(resp string, canceled bool) {
2019-01-03 20:35:24 +00:00
// Finished callback
2019-01-03 20:27:43 +00:00
if !canceled {
match, found, err := h.Buf.FindNext(resp, h.Buf.Start(), h.Buf.End(), h.searchOrig, true, useRegex)
2019-01-03 20:27:43 +00:00
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]
Improve buffer view relocation after jumping to a far-away location (#2628) * Improve buffer view relocation after jumping to a far-away location When the cursor is moved to a location which is far away from the current location (e.g. after a search or a goto line), the buffer view is always relocated in such a way that the cursor is at the bottom or at the top (minus scrollmargin), i.e. as if we just scrolled to this location. It's not like in other editors, and IMHO it's annoying. When we jump to a new location far away, we usually want to see more of its context, so the cursor should be placed closer to the center of the view, not near its edges. This change implements the behavior similar to other editors: - If the distance between the new and the old location is less than one frame (i.e. the view either doesn't change or just slightly "shifts") then the current behavior remains unchanged. - Otherwise the current line is placed at 25% of the window height. * Postpone calling onBufPaneOpen until the initial resize It is currently not possible to find out the geometry of a newly created bufpane in onBufPaneOpen lua callback: bp:GetView() returns {0,0,0,0} instead of the actual window. The reason is that the bufpane view is not properly initialized yet when the bufpane is created and the callback is triggered. It is initialized a bit later, at the initial resize. So postpone calling onBufPaneOpen until after the initial resize. * Improve buffer view relocation when opening a file at a far-away location When a file is opened with the initial cursor location at a given line which is far away from the beginning of the file, the buffer view is relocated so that the cursor is at the bottom (minus scrollmargin) as if we just scrolled to this line, which is annoying since we'd rather like to see more of the context of this initial location. So implement the behavior similar to the earlier commit (which addresses a similar issue about jumping far away after a search or goto): - If the initial cursor location is less than one frame away from the beginning of the buffer, keep the existing behavior i.e. just display the beginning of the buffer. - Otherwise place the cursor location at 25% of the window height.
2022-12-03 03:38:09 +00:00
h.GotoLoc(h.Cursor.CurSelection[1])
h.Buf.LastSearch = resp
h.Buf.LastSearchRegex = useRegex
h.Buf.HighlightSearch = h.Buf.Settings["hlsearch"].(bool)
2019-01-03 20:27:43 +00:00
} else {
h.Cursor.ResetSelection()
2019-01-03 20:35:24 +00:00
InfoBar.Message("No matches found")
2019-01-03 20:27:43 +00:00
}
} else {
h.Cursor.ResetSelection()
}
}
pattern := string(h.Cursor.GetSelection())
if useRegex && pattern != "" {
pattern = regexp.QuoteMeta(pattern)
}
if eventCallback != nil && pattern != "" {
eventCallback(pattern)
}
InfoBar.Prompt(prompt, pattern, "Find", eventCallback, findCallback)
if pattern != "" {
InfoBar.SelectAll()
}
return true
2018-08-27 19:53:10 +00:00
}
// ToggleHighlightSearch toggles highlighting all instances of the last used search term
func (h *BufPane) ToggleHighlightSearch() bool {
h.Buf.HighlightSearch = !h.Buf.HighlightSearch
return true
}
// UnhighlightSearch unhighlights all instances of the last used search term
func (h *BufPane) UnhighlightSearch() bool {
h.Buf.HighlightSearch = false
return true
}
2018-08-27 19:53:10 +00:00
// FindNext searches forwards for the last used search term
2019-01-19 20:37:59 +00:00
func (h *BufPane) FindNext() bool {
2019-01-03 20:35:24 +00:00
// 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.Buf.LastSearch, h.Buf.Start(), h.Buf.End(), searchLoc, true, h.Buf.LastSearchRegex)
2019-01-03 20:35:24 +00:00
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]
Improve buffer view relocation after jumping to a far-away location (#2628) * Improve buffer view relocation after jumping to a far-away location When the cursor is moved to a location which is far away from the current location (e.g. after a search or a goto line), the buffer view is always relocated in such a way that the cursor is at the bottom or at the top (minus scrollmargin), i.e. as if we just scrolled to this location. It's not like in other editors, and IMHO it's annoying. When we jump to a new location far away, we usually want to see more of its context, so the cursor should be placed closer to the center of the view, not near its edges. This change implements the behavior similar to other editors: - If the distance between the new and the old location is less than one frame (i.e. the view either doesn't change or just slightly "shifts") then the current behavior remains unchanged. - Otherwise the current line is placed at 25% of the window height. * Postpone calling onBufPaneOpen until the initial resize It is currently not possible to find out the geometry of a newly created bufpane in onBufPaneOpen lua callback: bp:GetView() returns {0,0,0,0} instead of the actual window. The reason is that the bufpane view is not properly initialized yet when the bufpane is created and the callback is triggered. It is initialized a bit later, at the initial resize. So postpone calling onBufPaneOpen until after the initial resize. * Improve buffer view relocation when opening a file at a far-away location When a file is opened with the initial cursor location at a given line which is far away from the beginning of the file, the buffer view is relocated so that the cursor is at the bottom (minus scrollmargin) as if we just scrolled to this line, which is annoying since we'd rather like to see more of the context of this initial location. So implement the behavior similar to the earlier commit (which addresses a similar issue about jumping far away after a search or goto): - If the initial cursor location is less than one frame away from the beginning of the buffer, keep the existing behavior i.e. just display the beginning of the buffer. - Otherwise place the cursor location at 25% of the window height.
2022-12-03 03:38:09 +00:00
h.GotoLoc(h.Cursor.CurSelection[1])
2019-01-03 20:35:24 +00:00
} else {
h.Cursor.ResetSelection()
}
2018-08-27 19:53:10 +00:00
return true
}
// FindPrevious searches backwards for the last used search term
2019-01-19 20:37:59 +00:00
func (h *BufPane) FindPrevious() bool {
2019-01-03 20:35:24 +00:00
// 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.Buf.LastSearch, h.Buf.Start(), h.Buf.End(), searchLoc, false, h.Buf.LastSearchRegex)
2019-01-03 20:35:24 +00:00
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]
Improve buffer view relocation after jumping to a far-away location (#2628) * Improve buffer view relocation after jumping to a far-away location When the cursor is moved to a location which is far away from the current location (e.g. after a search or a goto line), the buffer view is always relocated in such a way that the cursor is at the bottom or at the top (minus scrollmargin), i.e. as if we just scrolled to this location. It's not like in other editors, and IMHO it's annoying. When we jump to a new location far away, we usually want to see more of its context, so the cursor should be placed closer to the center of the view, not near its edges. This change implements the behavior similar to other editors: - If the distance between the new and the old location is less than one frame (i.e. the view either doesn't change or just slightly "shifts") then the current behavior remains unchanged. - Otherwise the current line is placed at 25% of the window height. * Postpone calling onBufPaneOpen until the initial resize It is currently not possible to find out the geometry of a newly created bufpane in onBufPaneOpen lua callback: bp:GetView() returns {0,0,0,0} instead of the actual window. The reason is that the bufpane view is not properly initialized yet when the bufpane is created and the callback is triggered. It is initialized a bit later, at the initial resize. So postpone calling onBufPaneOpen until after the initial resize. * Improve buffer view relocation when opening a file at a far-away location When a file is opened with the initial cursor location at a given line which is far away from the beginning of the file, the buffer view is relocated so that the cursor is at the bottom (minus scrollmargin) as if we just scrolled to this line, which is annoying since we'd rather like to see more of the context of this initial location. So implement the behavior similar to the earlier commit (which addresses a similar issue about jumping far away after a search or goto): - If the initial cursor location is less than one frame away from the beginning of the buffer, keep the existing behavior i.e. just display the beginning of the buffer. - Otherwise place the cursor location at 25% of the window height.
2022-12-03 03:38:09 +00:00
h.GotoLoc(h.Cursor.CurSelection[1])
2019-01-03 20:35:24 +00:00
} else {
h.Cursor.ResetSelection()
}
2018-08-27 19:53:10 +00:00
return true
}
// DiffNext searches forward until the beginning of the next block of diffs
func (h *BufPane) DiffNext() bool {
cur := h.Cursor.Loc.Y
dl, err := h.Buf.FindNextDiffLine(cur, true)
if err != nil {
return false
}
h.GotoLoc(buffer.Loc{0, dl})
return true
}
// DiffPrevious searches forward until the end of the previous block of diffs
func (h *BufPane) DiffPrevious() bool {
cur := h.Cursor.Loc.Y
dl, err := h.Buf.FindNextDiffLine(cur, false)
if err != nil {
return false
}
h.GotoLoc(buffer.Loc{0, dl})
return true
}
2018-08-27 19:53:10 +00:00
// Undo undoes the last action
2019-01-19 20:37:59 +00:00
func (h *BufPane) Undo() bool {
2018-12-31 21:36:54 +00:00
h.Buf.Undo()
2019-01-03 20:59:26 +00:00
InfoBar.Message("Undid action")
h.Relocate()
2018-08-27 19:53:10 +00:00
return true
}
// Redo redoes the last action
2019-01-19 20:37:59 +00:00
func (h *BufPane) Redo() bool {
2018-12-31 21:36:54 +00:00
h.Buf.Redo()
2019-01-03 20:59:26 +00:00
InfoBar.Message("Redid action")
h.Relocate()
2018-08-27 19:53:10 +00:00
return true
}
// Copy the selection to the system clipboard
2019-01-19 20:37:59 +00:00
func (h *BufPane) Copy() bool {
2018-12-31 21:36:54 +00:00
if h.Cursor.HasSelection() {
h.Cursor.CopySelection(clipboard.ClipboardReg)
2018-12-31 21:36:54 +00:00
h.freshClip = true
InfoBar.Message("Copied selection")
2018-12-31 21:36:54 +00:00
}
h.Relocate()
2018-08-27 19:53:10 +00:00
return true
}
2021-08-21 22:04:08 +00:00
// CopyLine copies the current line to the clipboard
func (h *BufPane) CopyLine() bool {
2020-04-30 04:54:02 +00:00
if h.Cursor.HasSelection() {
return false
}
origLoc := h.Cursor.Loc
2021-08-21 22:04:08 +00:00
h.Cursor.SelectLine()
h.Cursor.CopySelection(clipboard.ClipboardReg)
h.freshClip = true
InfoBar.Message("Copied line")
2020-04-30 04:54:02 +00:00
h.Cursor.Deselect(true)
h.Cursor.Loc = origLoc
2020-04-30 04:54:02 +00:00
h.Relocate()
return true
}
2018-08-27 19:53:10 +00:00
// CutLine cuts the current line to the clipboard
2019-01-19 20:37:59 +00:00
func (h *BufPane) CutLine() bool {
2018-12-31 21:36:54 +00:00
h.Cursor.SelectLine()
if !h.Cursor.HasSelection() {
return false
}
if h.freshClip {
2018-12-31 21:36:54 +00:00
if h.Cursor.HasSelection() {
if clip, err := clipboard.Read(clipboard.ClipboardReg); err != nil {
InfoBar.Error(err)
2018-12-31 21:36:54 +00:00
} else {
clipboard.WriteMulti(clip+string(h.Cursor.GetSelection()), clipboard.ClipboardReg, h.Cursor.Num, h.Buf.NumCursors())
2018-12-31 21:36:54 +00:00
}
}
} else if time.Since(h.lastCutTime)/time.Second > 10*time.Second || !h.freshClip {
2018-12-31 21:36:54 +00:00
h.Copy()
}
h.freshClip = true
h.lastCutTime = time.Now()
h.Cursor.DeleteSelection()
h.Cursor.ResetSelection()
2019-01-02 03:36:12 +00:00
InfoBar.Message("Cut line")
h.Relocate()
2018-08-27 19:53:10 +00:00
return true
}
// Cut the selection to the system clipboard
2019-01-19 20:37:59 +00:00
func (h *BufPane) Cut() bool {
2018-12-31 21:36:54 +00:00
if h.Cursor.HasSelection() {
h.Cursor.CopySelection(clipboard.ClipboardReg)
2018-12-31 21:36:54 +00:00
h.Cursor.DeleteSelection()
h.Cursor.ResetSelection()
h.freshClip = true
2019-01-02 03:36:12 +00:00
InfoBar.Message("Cut selection")
2018-12-31 21:36:54 +00:00
h.Relocate()
2018-12-31 21:36:54 +00:00
return true
}
2021-08-21 22:04:08 +00:00
return h.CutLine()
2018-08-27 19:53:10 +00:00
}
// DuplicateLine duplicates the current line or selection
2019-01-19 20:37:59 +00:00
func (h *BufPane) DuplicateLine() bool {
var infoMessage = "Duplicated line"
2018-12-31 21:36:54 +00:00
if h.Cursor.HasSelection() {
infoMessage = "Duplicated selection"
h.Buf.Insert(h.Cursor.CurSelection[1], string(h.Cursor.GetSelection()))
2018-12-31 21:36:54 +00:00
} else {
h.Cursor.End()
h.Buf.Insert(h.Cursor.Loc, "\n"+string(h.Buf.LineBytes(h.Cursor.Y)))
2018-12-31 21:36:54 +00:00
// h.Cursor.Right()
}
InfoBar.Message(infoMessage)
h.Relocate()
2018-08-27 19:53:10 +00:00
return true
}
// DeleteLine deletes the current line
2019-01-19 20:37:59 +00:00
func (h *BufPane) DeleteLine() bool {
2018-12-31 21:36:54 +00:00
h.Cursor.SelectLine()
if !h.Cursor.HasSelection() {
return false
}
h.Cursor.DeleteSelection()
h.Cursor.ResetSelection()
2019-01-02 03:36:12 +00:00
InfoBar.Message("Deleted line")
h.Relocate()
2018-08-27 19:53:10 +00:00
return true
}
// MoveLinesUp moves up the current line or selected lines if any
2019-01-19 20:37:59 +00:00
func (h *BufPane) MoveLinesUp() bool {
2019-01-15 03:38:59 +00:00
if h.Cursor.HasSelection() {
if h.Cursor.CurSelection[0].Y == 0 {
InfoBar.Message("Cannot move further up")
return false
2019-01-15 03:38:59 +00:00
}
start := h.Cursor.CurSelection[0].Y
end := h.Cursor.CurSelection[1].Y
sel := 1
2019-01-15 03:38:59 +00:00
if start > end {
end, start = start, end
sel = 0
}
compensate := false
if h.Cursor.CurSelection[sel].X != 0 {
end++
} else {
compensate = true
2019-01-15 03:38:59 +00:00
}
h.Buf.MoveLinesUp(
start,
end,
)
if compensate {
h.Cursor.CurSelection[sel].Y -= 1
}
2019-01-15 03:38:59 +00:00
} else {
if h.Cursor.Loc.Y == 0 {
InfoBar.Message("Cannot move further up")
return false
2019-01-15 03:38:59 +00:00
}
h.Buf.MoveLinesUp(
h.Cursor.Loc.Y,
h.Cursor.Loc.Y+1,
)
}
h.Relocate()
2018-08-27 19:53:10 +00:00
return true
}
// MoveLinesDown moves down the current line or selected lines if any
2019-01-19 20:37:59 +00:00
func (h *BufPane) MoveLinesDown() bool {
2019-01-15 03:38:59 +00:00
if h.Cursor.HasSelection() {
if h.Cursor.CurSelection[1].Y >= h.Buf.LinesNum() {
InfoBar.Message("Cannot move further down")
return false
2019-01-15 03:38:59 +00:00
}
start := h.Cursor.CurSelection[0].Y
end := h.Cursor.CurSelection[1].Y
sel := 1
2019-01-15 03:38:59 +00:00
if start > end {
end, start = start, end
sel = 0
}
if h.Cursor.CurSelection[sel].X != 0 {
end++
2019-01-15 03:38:59 +00:00
}
h.Buf.MoveLinesDown(
start,
end,
)
} else {
if h.Cursor.Loc.Y >= h.Buf.LinesNum()-1 {
InfoBar.Message("Cannot move further down")
return false
2019-01-15 03:38:59 +00:00
}
h.Buf.MoveLinesDown(
h.Cursor.Loc.Y,
h.Cursor.Loc.Y+1,
)
}
h.Relocate()
2018-08-27 19:53:10 +00:00
return true
}
// Paste whatever is in the system clipboard into the buffer
// Delete and paste if the user has a selection
2019-01-19 20:37:59 +00:00
func (h *BufPane) Paste() bool {
clip, err := clipboard.ReadMulti(clipboard.ClipboardReg, h.Cursor.Num, h.Buf.NumCursors())
if err != nil {
InfoBar.Error(err)
} else {
h.paste(clip)
}
h.Relocate()
2018-08-27 19:53:10 +00:00
return true
}
// PastePrimary pastes from the primary clipboard (only use on linux)
2019-01-19 20:37:59 +00:00
func (h *BufPane) PastePrimary() bool {
clip, err := clipboard.ReadMulti(clipboard.PrimaryReg, h.Cursor.Num, h.Buf.NumCursors())
if err != nil {
InfoBar.Error(err)
} else {
h.paste(clip)
}
h.Relocate()
2018-08-27 19:53:10 +00:00
return true
}
2019-01-19 20:37:59 +00:00
func (h *BufPane) paste(clip string) {
2019-01-02 21:27:27 +00:00
if h.Buf.Settings["smartpaste"].(bool) {
if h.Cursor.X > 0 {
leadingPasteWS := string(util.GetLeadingWhitespace([]byte(clip)))
if leadingPasteWS != " " && strings.Contains(clip, "\n"+leadingPasteWS) {
leadingWS := string(util.GetLeadingWhitespace(h.Buf.LineBytes(h.Cursor.Y)))
clip = strings.TrimPrefix(clip, leadingPasteWS)
clip = strings.ReplaceAll(clip, "\n"+leadingPasteWS, "\n"+leadingWS)
}
2019-01-02 21:27:27 +00:00
}
}
if h.Cursor.HasSelection() {
h.Cursor.DeleteSelection()
h.Cursor.ResetSelection()
}
h.Buf.Insert(h.Cursor.Loc, clip)
2019-01-02 21:27:27 +00:00
// h.Cursor.Loc = h.Cursor.Loc.Move(Count(clip), h.Buf)
h.freshClip = false
InfoBar.Message("Pasted clipboard")
2019-01-02 21:27:27 +00:00
}
2018-08-27 19:53:10 +00:00
// JumpToMatchingBrace moves the cursor to the matching brace if it is
// currently on a brace
2019-01-19 20:37:59 +00:00
func (h *BufPane) JumpToMatchingBrace() bool {
2019-01-15 03:38:59 +00:00
for _, bp := range buffer.BracePairs {
r := h.Cursor.RuneUnder(h.Cursor.X)
2019-08-04 22:11:09 +00:00
rl := h.Cursor.RuneUnder(h.Cursor.X - 1)
if r == bp[0] || r == bp[1] || rl == bp[0] || rl == bp[1] {
matchingBrace, left, found := h.Buf.FindMatchingBrace(bp, h.Cursor.Loc)
if found {
if left {
h.Cursor.GotoLoc(matchingBrace)
} else {
h.Cursor.GotoLoc(matchingBrace.Move(1, h.Buf))
}
h.Relocate()
return true
2019-08-04 22:11:09 +00:00
}
2019-01-15 03:38:59 +00:00
}
}
return false
2018-08-27 19:53:10 +00:00
}
// SelectAll selects the entire buffer
2019-01-19 20:37:59 +00:00
func (h *BufPane) SelectAll() bool {
2018-12-31 21:36:54 +00:00
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()
2018-08-27 19:53:10 +00:00
return true
}
// OpenFile opens a new file in the buffer
2019-01-19 20:37:59 +00:00
func (h *BufPane) OpenFile() bool {
2019-01-03 22:07:28 +00:00
InfoBar.Prompt("> ", "open ", "Open", nil, func(resp string, canceled bool) {
2019-01-01 04:47:24 +00:00
if !canceled {
2019-01-11 19:49:22 +00:00
h.HandleCommand(resp)
2019-01-01 04:47:24 +00:00
}
2019-01-02 21:27:27 +00:00
})
return true
2018-08-27 19:53:10 +00:00
}
// JumpLine asks the user to enter a line number to jump to
func (h *BufPane) JumpLine() bool {
InfoBar.Prompt("> ", "goto ", "Command", nil, func(resp string, canceled bool) {
if !canceled {
h.HandleCommand(resp)
}
})
return true
}
2018-08-27 19:53:10 +00:00
// Start moves the viewport to the start of the buffer
2019-01-19 20:37:59 +00:00
func (h *BufPane) Start() bool {
2019-01-11 02:26:58 +00:00
v := h.GetView()
v.StartLine = display.SLoc{0, 0}
2019-01-11 02:26:58 +00:00
h.SetView(v)
return true
2018-08-27 19:53:10 +00:00
}
// End moves the viewport to the end of the buffer
2019-01-19 20:37:59 +00:00
func (h *BufPane) End() bool {
2019-01-11 02:26:58 +00:00
v := h.GetView()
v.StartLine = h.Scroll(h.SLocFromLoc(h.Buf.End()), -h.BufView().Height+1)
h.SetView(v)
return true
2018-08-27 19:53:10 +00:00
}
// PageUp scrolls the view up a page
2019-01-19 20:37:59 +00:00
func (h *BufPane) PageUp() bool {
h.ScrollUp(h.BufView().Height)
return true
2018-08-27 19:53:10 +00:00
}
// PageDown scrolls the view down a page
2019-01-19 20:37:59 +00:00
func (h *BufPane) PageDown() bool {
h.ScrollDown(h.BufView().Height)
h.ScrollAdjust()
return true
2018-08-27 19:53:10 +00:00
}
// SelectPageUp selects up one page
2019-01-19 20:37:59 +00:00
func (h *BufPane) SelectPageUp() bool {
2019-01-02 21:27:27 +00:00
if !h.Cursor.HasSelection() {
h.Cursor.OrigSelection[0] = h.Cursor.Loc
}
h.MoveCursorUp(h.BufView().Height)
2019-01-02 21:27:27 +00:00
h.Cursor.SelectTo(h.Cursor.Loc)
h.Relocate()
2018-08-27 19:53:10 +00:00
return true
}
// SelectPageDown selects down one page
2019-01-19 20:37:59 +00:00
func (h *BufPane) SelectPageDown() bool {
2019-01-02 21:27:27 +00:00
if !h.Cursor.HasSelection() {
h.Cursor.OrigSelection[0] = h.Cursor.Loc
}
h.MoveCursorDown(h.BufView().Height)
2019-01-02 21:27:27 +00:00
h.Cursor.SelectTo(h.Cursor.Loc)
h.Relocate()
2018-08-27 19:53:10 +00:00
return true
}
// CursorPageUp places the cursor a page up
2019-01-19 20:37:59 +00:00
func (h *BufPane) CursorPageUp() bool {
2019-01-02 21:27:27 +00:00
h.Cursor.Deselect(true)
if h.Cursor.HasSelection() {
h.Cursor.Loc = h.Cursor.CurSelection[0]
h.Cursor.ResetSelection()
h.Cursor.StoreVisualX()
}
h.MoveCursorUp(h.BufView().Height)
h.Relocate()
2018-08-27 19:53:10 +00:00
return true
}
// CursorPageDown places the cursor a page up
2019-01-19 20:37:59 +00:00
func (h *BufPane) CursorPageDown() bool {
2019-01-02 21:27:27 +00:00
h.Cursor.Deselect(false)
if h.Cursor.HasSelection() {
h.Cursor.Loc = h.Cursor.CurSelection[1]
h.Cursor.ResetSelection()
h.Cursor.StoreVisualX()
}
h.MoveCursorDown(h.BufView().Height)
h.Relocate()
2018-08-27 19:53:10 +00:00
return true
}
// HalfPageUp scrolls the view up half a page
2019-01-19 20:37:59 +00:00
func (h *BufPane) HalfPageUp() bool {
h.ScrollUp(h.BufView().Height / 2)
return true
2018-08-27 19:53:10 +00:00
}
// HalfPageDown scrolls the view down half a page
2019-01-19 20:37:59 +00:00
func (h *BufPane) HalfPageDown() bool {
h.ScrollDown(h.BufView().Height / 2)
h.ScrollAdjust()
return true
2018-08-27 19:53:10 +00:00
}
2020-02-08 07:56:24 +00:00
// ToggleDiffGutter turns the diff gutter off and on
func (h *BufPane) ToggleDiffGutter() bool {
if !h.Buf.Settings["diffgutter"].(bool) {
h.Buf.Settings["diffgutter"] = true
h.Buf.UpdateDiff(func(synchronous bool) {
screen.Redraw()
2020-02-08 07:56:24 +00:00
})
InfoBar.Message("Enabled diff gutter")
} else {
h.Buf.Settings["diffgutter"] = false
InfoBar.Message("Disabled diff gutter")
}
return true
}
2018-08-27 19:53:10 +00:00
// ToggleRuler turns line numbers off and on
2019-01-19 20:37:59 +00:00
func (h *BufPane) ToggleRuler() bool {
2018-12-31 21:36:54 +00:00
if !h.Buf.Settings["ruler"].(bool) {
h.Buf.Settings["ruler"] = true
2019-01-02 03:36:12 +00:00
InfoBar.Message("Enabled ruler")
2018-12-31 21:36:54 +00:00
} else {
h.Buf.Settings["ruler"] = false
2019-01-02 03:36:12 +00:00
InfoBar.Message("Disabled ruler")
2018-12-31 21:36:54 +00:00
}
return true
2018-08-27 19:53:10 +00:00
}
// ClearStatus clears the messenger bar
2019-01-19 20:37:59 +00:00
func (h *BufPane) ClearStatus() bool {
2019-01-02 21:27:27 +00:00
InfoBar.Message("")
return true
2018-08-27 19:53:10 +00:00
}
// ToggleHelp toggles the help screen
2019-01-19 20:37:59 +00:00
func (h *BufPane) ToggleHelp() bool {
2019-01-15 22:10:13 +00:00
if h.Buf.Type == buffer.BTHelp {
h.Quit()
} else {
h.openHelp("help")
}
return true
2018-08-27 19:53:10 +00:00
}
// ToggleKeyMenu toggles the keymenu option and resizes all tabs
2019-01-19 20:37:59 +00:00
func (h *BufPane) ToggleKeyMenu() bool {
2019-01-16 23:37:45 +00:00
config.GlobalSettings["keymenu"] = !config.GetGlobalOption("keymenu").(bool)
Tabs.Resize()
return true
2018-08-27 19:53:10 +00:00
}
// ShellMode opens a terminal to run a shell command
2019-01-19 20:37:59 +00:00
func (h *BufPane) ShellMode() bool {
2019-01-10 21:37:05 +00:00
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
2018-08-27 19:53:10 +00:00
}
// CommandMode lets the user enter a command
2019-01-19 20:37:59 +00:00
func (h *BufPane) CommandMode() bool {
2019-01-03 22:07:28 +00:00
InfoBar.Prompt("> ", "", "Command", nil, func(resp string, canceled bool) {
2019-01-02 04:29:25 +00:00
if !canceled {
2019-01-11 19:49:22 +00:00
h.HandleCommand(resp)
2019-01-02 04:29:25 +00:00
}
})
return true
2018-08-27 19:53:10 +00:00
}
// ToggleOverwriteMode lets the user toggle the text overwrite mode
2019-01-19 20:37:59 +00:00
func (h *BufPane) ToggleOverwriteMode() bool {
2019-01-02 21:27:27 +00:00
h.isOverwriteMode = !h.isOverwriteMode
return true
2018-08-27 19:53:10 +00:00
}
// Escape leaves current mode
2019-01-19 20:37:59 +00:00
func (h *BufPane) Escape() bool {
return true
2018-08-27 19:53:10 +00:00
}
// Deselect deselects on the current cursor
func (h *BufPane) Deselect() bool {
h.Cursor.Deselect(true)
return true
}
// ClearInfo clears the infobar
func (h *BufPane) ClearInfo() bool {
InfoBar.Message("")
return true
}
2021-03-02 02:55:49 +00:00
// ForceQuit closes the current tab or view even if there are unsaved changes
// (no prompt)
func (h *BufPane) ForceQuit() bool {
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()
}
return true
}
2018-08-27 19:53:10 +00:00
// Quit this will close the current tab or view that is open
2019-01-19 20:37:59 +00:00
func (h *BufPane) Quit() bool {
2019-01-05 02:48:19 +00:00
if h.Buf.Modified() {
if config.GlobalSettings["autosave"].(float64) > 0 {
// autosave on means we automatically save when quitting
2020-02-09 21:36:15 +00:00
h.SaveCB("Quit", func() {
2021-03-02 02:55:49 +00:00
h.ForceQuit()
2020-02-09 21:36:15 +00:00
})
} else {
InfoBar.YNPrompt("Save changes to "+h.Buf.GetName()+" before closing? (y,n,esc)", func(yes, canceled bool) {
if !canceled && !yes {
2021-03-02 02:55:49 +00:00
h.ForceQuit()
} else if !canceled && yes {
2020-02-09 21:36:15 +00:00
h.SaveCB("Quit", func() {
2021-03-02 02:55:49 +00:00
h.ForceQuit()
2020-02-09 21:36:15 +00:00
})
}
})
}
2019-01-05 02:48:19 +00:00
} else {
2021-03-02 02:55:49 +00:00
h.ForceQuit()
2019-01-05 02:48:19 +00:00
}
return true
2018-08-27 19:53:10 +00:00
}
// QuitAll quits the whole editor; all splits and tabs
2019-01-19 20:37:59 +00:00
func (h *BufPane) QuitAll() bool {
2019-08-05 03:23:32 +00:00
anyModified := false
for _, b := range buffer.OpenBuffers {
if b.Modified() {
anyModified = true
break
}
}
quit := func() {
buffer.CloseOpenBuffers()
2019-08-05 03:23:32 +00:00
screen.Screen.Fini()
InfoBar.Close()
2019-12-22 18:43:29 +00:00
runtime.Goexit()
2019-08-05 03:23:32 +00:00
}
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
2018-08-27 19:53:10 +00:00
}
// AddTab adds a new tab with an empty buffer
2019-01-19 20:37:59 +00:00
func (h *BufPane) AddTab() bool {
2019-01-10 04:44:53 +00:00
width, height := screen.Screen.Size()
2019-01-15 03:16:44 +00:00
iOffset := config.GetInfoBarOffset()
2019-01-10 04:44:53 +00:00
b := buffer.NewBufferFromString("", "", buffer.BTDefault)
2019-01-15 03:16:44 +00:00
tp := NewTabFromBuffer(0, 0, width, height-iOffset, b)
2019-01-10 04:44:53 +00:00
Tabs.AddTab(tp)
Tabs.SetActive(len(Tabs.List) - 1)
return true
2018-08-27 19:53:10 +00:00
}
// PreviousTab switches to the previous tab in the tab list
2019-01-19 20:37:59 +00:00
func (h *BufPane) PreviousTab() bool {
tabsLen := len(Tabs.List)
2020-04-30 04:54:02 +00:00
a := Tabs.Active() + tabsLen
Tabs.SetActive((a - 1) % tabsLen)
2019-01-10 04:44:53 +00:00
return true
2018-08-27 19:53:10 +00:00
}
// NextTab switches to the next tab in the tab list
2019-01-19 20:37:59 +00:00
func (h *BufPane) NextTab() bool {
2019-01-10 04:44:53 +00:00
a := Tabs.Active()
Tabs.SetActive((a + 1) % len(Tabs.List))
return true
2018-08-27 19:53:10 +00:00
}
2019-01-13 23:18:23 +00:00
// VSplitAction opens an empty vertical split
2019-01-19 20:37:59 +00:00
func (h *BufPane) VSplitAction() bool {
2019-01-13 23:18:23 +00:00
h.VSplitBuf(buffer.NewBufferFromString("", "", buffer.BTDefault))
2019-01-10 04:44:53 +00:00
return true
2018-08-27 19:53:10 +00:00
}
2019-01-13 23:18:23 +00:00
// HSplitAction opens an empty horizontal split
2019-01-19 20:37:59 +00:00
func (h *BufPane) HSplitAction() bool {
2019-01-13 23:18:23 +00:00
h.HSplitBuf(buffer.NewBufferFromString("", "", buffer.BTDefault))
2019-01-10 04:44:53 +00:00
return true
2018-08-27 19:53:10 +00:00
}
// Unsplit closes all splits in the current tab except the active one
2019-01-19 20:37:59 +00:00
func (h *BufPane) Unsplit() bool {
2020-02-09 02:06:13 +00:00
tab := h.tab
n := tab.GetNode(h.splitID)
ok := n.Unsplit()
if ok {
tab.RemovePane(tab.GetPane(h.splitID))
tab.Resize()
tab.SetActive(len(tab.Panes) - 1)
2019-01-09 21:55:00 +00:00
2020-02-09 02:06:13 +00:00
return true
}
return false
2018-08-27 19:53:10 +00:00
}
// NextSplit changes the view to the next split
2019-01-19 20:37:59 +00:00
func (h *BufPane) NextSplit() bool {
2020-02-09 02:06:13 +00:00
a := h.tab.active
if a < len(h.tab.Panes)-1 {
a++
} else {
a = 0
}
2020-02-09 02:06:13 +00:00
h.tab.SetActive(a)
2019-01-10 04:44:53 +00:00
return true
2018-08-27 19:53:10 +00:00
}
// PreviousSplit changes the view to the previous split
2019-01-19 20:37:59 +00:00
func (h *BufPane) PreviousSplit() bool {
2020-02-09 02:06:13 +00:00
a := h.tab.active
if a > 0 {
a--
} else {
2020-02-09 02:06:13 +00:00
a = len(h.tab.Panes) - 1
}
2020-02-09 02:06:13 +00:00
h.tab.SetActive(a)
2019-01-10 04:44:53 +00:00
return true
2018-08-27 19:53:10 +00:00
}
2019-08-05 03:23:32 +00:00
var curmacro []interface{}
2021-08-21 22:04:08 +00:00
var recordingMacro bool
2018-08-27 19:53:10 +00:00
// ToggleMacro toggles recording of a macro
2019-01-19 20:37:59 +00:00
func (h *BufPane) ToggleMacro() bool {
2021-08-21 22:04:08 +00:00
recordingMacro = !recordingMacro
if recordingMacro {
2019-08-05 03:23:32 +00:00
curmacro = []interface{}{}
InfoBar.Message("Recording")
} else {
InfoBar.Message("Stopped recording")
}
h.Relocate()
2018-08-27 19:53:10 +00:00
return true
}
// PlayMacro plays back the most recently recorded macro
2019-01-19 20:37:59 +00:00
func (h *BufPane) PlayMacro() bool {
2021-08-21 22:04:08 +00:00
if recordingMacro {
2019-08-05 03:23:32 +00:00
return false
}
for _, action := range curmacro {
switch t := action.(type) {
case rune:
h.DoRuneInsert(t)
case BufKeyAction:
2019-12-25 18:11:38 +00:00
t(h)
2019-08-05 03:23:32 +00:00
}
}
h.Relocate()
2018-08-27 19:53:10 +00:00
return true
}
// SpawnMultiCursor creates a new multiple cursor at the next occurrence of the current selection or current word
2019-01-19 20:37:59 +00:00
func (h *BufPane) SpawnMultiCursor() bool {
2019-01-03 20:59:26 +00:00
spawner := h.Buf.GetCursor(h.Buf.NumCursors() - 1)
if !spawner.HasSelection() {
spawner.SelectWord()
2019-01-05 02:48:19 +00:00
h.multiWord = true
h.Relocate()
2019-01-05 02:48:19 +00:00
return true
}
2019-01-03 20:59:26 +00:00
2019-01-05 02:48:19 +00:00
sel := spawner.GetSelection()
searchStart := spawner.CurSelection[1]
2019-01-03 20:59:26 +00:00
2019-01-05 02:48:19 +00:00
search := string(sel)
2019-01-16 22:52:30 +00:00
search = regexp.QuoteMeta(search)
2019-01-05 02:48:19 +00:00
if h.multiWord {
search = "\\b" + search + "\\b"
}
2019-01-16 22:52:30 +00:00
match, found, err := h.Buf.FindNext(search, h.Buf.Start(), h.Buf.End(), searchStart, true, true)
2019-01-05 02:48:19 +00:00
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)
2019-01-16 22:52:30 +00:00
h.Buf.SetCurCursor(h.Buf.NumCursors() - 1)
2019-01-05 02:48:19 +00:00
h.Buf.MergeCursors()
} else {
InfoBar.Message("No matches found")
2019-01-03 20:59:26 +00:00
}
h.Relocate()
2019-01-05 02:48:19 +00:00
return true
2018-08-27 19:53:10 +00:00
}
Fix various issues with `SpawnMultiCursor{Up,Down}` (#3145) * SpawnMultiCursorUp/Down: change order of adding cursors SpawnMultiCursor{Up,Down} currently works in a tricky way: instead of creating a new cursor above or below, it moves the current "primary" cursor above or below, and then creates a new cursor below or above the new position of the current cursor (i.e. at its previous position), creating an illusion for the user that the current (top-most or bottom-most) cursor is a newly spawned cursor. This trick causes at least the following issues: - When the line above or below, where we spawn a new cursor, is shorter than the current cursor position in the current line, the new cursor is placed at the end of this short line (which is expected), but also the current cursor unexpectedly changes its x position and moves below/above the new cursor. - When removing a cursor in RemoveMultiCursor (default Alt-p key), it non-intuitively removes the cursor which, from the user point of view, is not the last but the last-but-one cursor. Fix these issues by replacing the trick with a straightforward logic: just create the new cursor above or below the last one. Note that this fix has a user-visible side effect: the last cursor is no longer the "primary" one (since it is now the last in the list, not the first), so e.g. when the user clears multicursors via Esc key, the remaining cursor is the first one, not the last one. I assume it's ok. * SpawnMultiCursorUp/Down: move common code to a helper fn * SpawnMultiCursorUp/Down: honor visual width and LastVisualX Make spawning multicursors up/down behave more similarly to cursor movements up/down. This change fixes 2 issues at once: - SpawnMultiCursorUp/Down doesn't take into account the visual width of the text before the cursor, which may be different from its character width (e.g. if it contains tabs). So e.g. if the number of tabs before the cursor in the current line is not the same as in the new line, the new cursor is placed at an unexpected location. - SpawnMultiCursorUp/Down doesn't take into account the cursor's remembered x position (LastVisualX) when e.g. spawning a new cursor in the below line which is short than the current cursor position, and then spawning yet another cursor in the next below line which is longer than this short line. * SpawnMultiCursorUp/Down: honor softwrap When softwrap is enabled and the current line is wrapped, make SpawnMultiCursor{Up,Down} spawn cursor in the next visual line within this wrapped line, similarly to how we handle cursor movements up/down within wrapped lines. * SpawnMultiCursorUp/Down: deselect when spawning cursors To avoid weird user experience (spawned cursors messing with selections of existing cursors).
2024-03-04 21:23:50 +00:00
// SpawnMultiCursorUpN is not an action
func (h *BufPane) SpawnMultiCursorUpN(n int) bool {
lastC := h.Buf.GetCursor(h.Buf.NumCursors() - 1)
var c *buffer.Cursor
if !h.Buf.Settings["softwrap"].(bool) {
if n > 0 && lastC.Y == 0 {
return false
}
if n < 0 && lastC.Y+1 == h.Buf.LinesNum() {
return false
}
h.Buf.DeselectCursors()
c = buffer.NewCursor(h.Buf, buffer.Loc{lastC.X, lastC.Y - n})
c.LastVisualX = lastC.LastVisualX
c.X = c.GetCharPosInLine(h.Buf.LineBytes(c.Y), c.LastVisualX)
c.Relocate()
} else {
vloc := h.VLocFromLoc(lastC.Loc)
sloc := h.Scroll(vloc.SLoc, -n)
if sloc == vloc.SLoc {
return false
}
h.Buf.DeselectCursors()
vloc.SLoc = sloc
vloc.VisualX = lastC.LastVisualX
c = buffer.NewCursor(h.Buf, h.LocFromVLoc(vloc))
c.LastVisualX = lastC.LastVisualX
}
h.Buf.AddCursor(c)
h.Buf.SetCurCursor(h.Buf.NumCursors() - 1)
h.Buf.MergeCursors()
h.Relocate()
return true
}
Fix various issues with `SpawnMultiCursor{Up,Down}` (#3145) * SpawnMultiCursorUp/Down: change order of adding cursors SpawnMultiCursor{Up,Down} currently works in a tricky way: instead of creating a new cursor above or below, it moves the current "primary" cursor above or below, and then creates a new cursor below or above the new position of the current cursor (i.e. at its previous position), creating an illusion for the user that the current (top-most or bottom-most) cursor is a newly spawned cursor. This trick causes at least the following issues: - When the line above or below, where we spawn a new cursor, is shorter than the current cursor position in the current line, the new cursor is placed at the end of this short line (which is expected), but also the current cursor unexpectedly changes its x position and moves below/above the new cursor. - When removing a cursor in RemoveMultiCursor (default Alt-p key), it non-intuitively removes the cursor which, from the user point of view, is not the last but the last-but-one cursor. Fix these issues by replacing the trick with a straightforward logic: just create the new cursor above or below the last one. Note that this fix has a user-visible side effect: the last cursor is no longer the "primary" one (since it is now the last in the list, not the first), so e.g. when the user clears multicursors via Esc key, the remaining cursor is the first one, not the last one. I assume it's ok. * SpawnMultiCursorUp/Down: move common code to a helper fn * SpawnMultiCursorUp/Down: honor visual width and LastVisualX Make spawning multicursors up/down behave more similarly to cursor movements up/down. This change fixes 2 issues at once: - SpawnMultiCursorUp/Down doesn't take into account the visual width of the text before the cursor, which may be different from its character width (e.g. if it contains tabs). So e.g. if the number of tabs before the cursor in the current line is not the same as in the new line, the new cursor is placed at an unexpected location. - SpawnMultiCursorUp/Down doesn't take into account the cursor's remembered x position (LastVisualX) when e.g. spawning a new cursor in the below line which is short than the current cursor position, and then spawning yet another cursor in the next below line which is longer than this short line. * SpawnMultiCursorUp/Down: honor softwrap When softwrap is enabled and the current line is wrapped, make SpawnMultiCursor{Up,Down} spawn cursor in the next visual line within this wrapped line, similarly to how we handle cursor movements up/down within wrapped lines. * SpawnMultiCursorUp/Down: deselect when spawning cursors To avoid weird user experience (spawned cursors messing with selections of existing cursors).
2024-03-04 21:23:50 +00:00
// SpawnMultiCursorUp creates additional cursor, at the same X (if possible), one Y less.
func (h *BufPane) SpawnMultiCursorUp() bool {
return h.SpawnMultiCursorUpN(1)
}
// SpawnMultiCursorDown creates additional cursor, at the same X (if possible), one Y more.
func (h *BufPane) SpawnMultiCursorDown() bool {
Fix various issues with `SpawnMultiCursor{Up,Down}` (#3145) * SpawnMultiCursorUp/Down: change order of adding cursors SpawnMultiCursor{Up,Down} currently works in a tricky way: instead of creating a new cursor above or below, it moves the current "primary" cursor above or below, and then creates a new cursor below or above the new position of the current cursor (i.e. at its previous position), creating an illusion for the user that the current (top-most or bottom-most) cursor is a newly spawned cursor. This trick causes at least the following issues: - When the line above or below, where we spawn a new cursor, is shorter than the current cursor position in the current line, the new cursor is placed at the end of this short line (which is expected), but also the current cursor unexpectedly changes its x position and moves below/above the new cursor. - When removing a cursor in RemoveMultiCursor (default Alt-p key), it non-intuitively removes the cursor which, from the user point of view, is not the last but the last-but-one cursor. Fix these issues by replacing the trick with a straightforward logic: just create the new cursor above or below the last one. Note that this fix has a user-visible side effect: the last cursor is no longer the "primary" one (since it is now the last in the list, not the first), so e.g. when the user clears multicursors via Esc key, the remaining cursor is the first one, not the last one. I assume it's ok. * SpawnMultiCursorUp/Down: move common code to a helper fn * SpawnMultiCursorUp/Down: honor visual width and LastVisualX Make spawning multicursors up/down behave more similarly to cursor movements up/down. This change fixes 2 issues at once: - SpawnMultiCursorUp/Down doesn't take into account the visual width of the text before the cursor, which may be different from its character width (e.g. if it contains tabs). So e.g. if the number of tabs before the cursor in the current line is not the same as in the new line, the new cursor is placed at an unexpected location. - SpawnMultiCursorUp/Down doesn't take into account the cursor's remembered x position (LastVisualX) when e.g. spawning a new cursor in the below line which is short than the current cursor position, and then spawning yet another cursor in the next below line which is longer than this short line. * SpawnMultiCursorUp/Down: honor softwrap When softwrap is enabled and the current line is wrapped, make SpawnMultiCursor{Up,Down} spawn cursor in the next visual line within this wrapped line, similarly to how we handle cursor movements up/down within wrapped lines. * SpawnMultiCursorUp/Down: deselect when spawning cursors To avoid weird user experience (spawned cursors messing with selections of existing cursors).
2024-03-04 21:23:50 +00:00
return h.SpawnMultiCursorUpN(-1)
}
2018-08-27 19:53:10 +00:00
// SpawnMultiCursorSelect adds a cursor at the beginning of each line of a selection
2019-01-19 20:37:59 +00:00
func (h *BufPane) SpawnMultiCursorSelect() bool {
2019-01-03 20:59:26 +00:00
// 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
2018-08-27 19:53:10 +00:00
}
// MouseMultiCursor is a mouse action which puts a new cursor at the mouse position,
// or removes a cursor if it is already there
2019-01-19 20:37:59 +00:00
func (h *BufPane) MouseMultiCursor(e *tcell.EventMouse) bool {
2019-01-03 04:26:40 +00:00
b := h.Buf
mx, my := e.Position()
// ignore click on the status line
if my >= h.BufView().Y+h.BufView().Height {
return false
}
2019-12-24 21:01:08 +00:00
mouseLoc := h.LocFromVisual(buffer.Loc{X: mx, Y: my})
if h.Buf.NumCursors() > 1 {
cursors := h.Buf.GetCursors()
for _, c := range cursors {
if c.Loc == mouseLoc {
h.Buf.RemoveCursor(c.Num)
return true
}
}
}
2019-01-03 04:26:40 +00:00
c := buffer.NewCursor(b, mouseLoc)
b.AddCursor(c)
b.MergeCursors()
return true
2018-08-27 19:53:10 +00:00
}
// SkipMultiCursor moves the current multiple cursor to the next available position
2019-01-19 20:37:59 +00:00
func (h *BufPane) SkipMultiCursor() bool {
2019-01-03 20:59:26 +00:00
lastC := h.Buf.GetCursor(h.Buf.NumCursors() - 1)
sel := lastC.GetSelection()
searchStart := lastC.CurSelection[1]
2019-01-16 22:52:30 +00:00
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)
2019-01-03 20:59:26 +00:00
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()
2019-01-16 22:52:30 +00:00
h.Buf.SetCurCursor(h.Buf.NumCursors() - 1)
2019-01-03 20:59:26 +00:00
} else {
InfoBar.Message("No matches found")
}
h.Relocate()
2019-01-16 22:52:30 +00:00
return true
2018-08-27 19:53:10 +00:00
}
// RemoveMultiCursor removes the latest multiple cursor
2019-01-19 20:37:59 +00:00
func (h *BufPane) RemoveMultiCursor() bool {
2019-01-03 04:26:40 +00:00
if h.Buf.NumCursors() > 1 {
h.Buf.RemoveCursor(h.Buf.NumCursors() - 1)
2019-01-16 22:52:30 +00:00
h.Buf.SetCurCursor(h.Buf.NumCursors() - 1)
2019-01-03 04:26:40 +00:00
h.Buf.UpdateCursors()
2019-01-05 02:48:19 +00:00
} else {
h.multiWord = false
2019-01-03 04:26:40 +00:00
}
h.Relocate()
2019-01-16 22:52:30 +00:00
return true
2018-08-27 19:53:10 +00:00
}
// RemoveAllMultiCursors removes all cursors except the base cursor
2019-01-19 20:37:59 +00:00
func (h *BufPane) RemoveAllMultiCursors() bool {
2019-01-03 04:26:40 +00:00
h.Buf.ClearCursors()
2019-01-05 02:48:19 +00:00
h.multiWord = false
h.Relocate()
2019-01-03 04:26:40 +00:00
return true
2018-08-27 19:53:10 +00:00
}
2019-09-02 18:40:50 +00:00
// None is an action that does nothing
2019-09-02 18:40:50 +00:00
func (h *BufPane) None() bool {
return true
2019-09-02 18:40:50 +00:00
}