2018-08-27 19:53:10 +00:00
|
|
|
package buffer
|
2016-03-17 21:27:57 +00:00
|
|
|
|
2017-06-20 09:53:59 +00:00
|
|
|
import (
|
2020-07-05 00:00:39 +00:00
|
|
|
"github.com/zyedidia/micro/v2/internal/clipboard"
|
2020-05-04 14:16:15 +00:00
|
|
|
"github.com/zyedidia/micro/v2/internal/util"
|
2017-06-20 09:53:59 +00:00
|
|
|
)
|
2016-09-05 01:19:14 +00:00
|
|
|
|
2018-08-26 03:06:44 +00:00
|
|
|
// InBounds returns whether the given location is a valid character position in the given buffer
|
|
|
|
|
func InBounds(pos Loc, buf *Buffer) bool {
|
2020-05-20 20:47:08 +00:00
|
|
|
if pos.Y < 0 || pos.Y >= len(buf.lines) || pos.X < 0 || pos.X > util.CharacterCount(buf.LineBytes(pos.Y)) {
|
2018-08-26 03:06:44 +00:00
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return true
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// The Cursor struct stores the location of the cursor in the buffer
|
|
|
|
|
// as well as the selection
|
2016-03-17 21:27:57 +00:00
|
|
|
type Cursor struct {
|
2018-08-30 00:53:40 +00:00
|
|
|
buf *Buffer
|
2016-06-07 15:43:28 +00:00
|
|
|
Loc
|
2016-03-17 21:27:57 +00:00
|
|
|
|
Overhaul LastVisualX and GetVisualX() usage
Restore the original meaning of LastVisualX before commit 6d13710d934d
("Implement moving cursor up/down within a wrapped line"): last visual x
location of the cursor in a logical line in the buffer, not in a visual
line on the screen (in other words, taking tabs and wide characters into
account, but not taking softwrap into account). And add a separate
LastWrappedVisualX field, similar to LastVisualX but taking softwrap
into account as well.
This allows tracking last x position at the same time for both cases
when we care about softwrap and when we don't care about it. This can be
useful, for example, for implementing cursor up/down movement actions
that always move by logical lines, not by visual lines, even if softwrap
is enabled (in addition to our default CursorUp and CursorDown actions
that move by visual lines).
Also this fixes a minor bug: in InsertTab(), when `tabstospaces` is
enabled and we insert a tab, the amount of inserted spaces depends on
the visual line wrapping (i.e. on the window width), which is probably
not a good idea.
2024-10-13 21:12:04 +00:00
|
|
|
// Last visual x position of the cursor. Used in cursor up/down movements
|
|
|
|
|
// for remembering the original x position when moving to a line that is
|
|
|
|
|
// shorter than current x position.
|
2016-05-28 21:29:49 +00:00
|
|
|
LastVisualX int
|
Overhaul LastVisualX and GetVisualX() usage
Restore the original meaning of LastVisualX before commit 6d13710d934d
("Implement moving cursor up/down within a wrapped line"): last visual x
location of the cursor in a logical line in the buffer, not in a visual
line on the screen (in other words, taking tabs and wide characters into
account, but not taking softwrap into account). And add a separate
LastWrappedVisualX field, similar to LastVisualX but taking softwrap
into account as well.
This allows tracking last x position at the same time for both cases
when we care about softwrap and when we don't care about it. This can be
useful, for example, for implementing cursor up/down movement actions
that always move by logical lines, not by visual lines, even if softwrap
is enabled (in addition to our default CursorUp and CursorDown actions
that move by visual lines).
Also this fixes a minor bug: in InsertTab(), when `tabstospaces` is
enabled and we insert a tab, the amount of inserted spaces depends on
the visual line wrapping (i.e. on the window width), which is probably
not a good idea.
2024-10-13 21:12:04 +00:00
|
|
|
// Similar to LastVisualX but takes softwrapping into account, i.e. last
|
|
|
|
|
// visual x position in a visual (wrapped) line on the screen, which may be
|
|
|
|
|
// different from the line in the buffer.
|
|
|
|
|
LastWrappedVisualX int
|
2016-04-04 18:09:24 +00:00
|
|
|
|
2016-03-28 12:43:08 +00:00
|
|
|
// The current selection as a range of character numbers (inclusive)
|
2016-06-07 15:43:28 +00:00
|
|
|
CurSelection [2]Loc
|
2016-03-28 12:43:08 +00:00
|
|
|
// The original selection as a range of character numbers
|
|
|
|
|
// This is used for line and word selection where it is necessary
|
|
|
|
|
// to know what the original selection was
|
2016-06-07 15:43:28 +00:00
|
|
|
OrigSelection [2]Loc
|
2017-06-17 02:19:33 +00:00
|
|
|
|
2020-10-20 20:52:49 +00:00
|
|
|
// The line number where a new trailing whitespace has been added
|
|
|
|
|
// or -1 if there is no new trailing whitespace at this cursor.
|
|
|
|
|
// This is used for checking if a trailing whitespace should be highlighted
|
|
|
|
|
NewTrailingWsY int
|
|
|
|
|
|
2017-06-17 02:19:33 +00:00
|
|
|
// Which cursor index is this (for multiple cursors)
|
|
|
|
|
Num int
|
2016-05-28 21:29:49 +00:00
|
|
|
}
|
|
|
|
|
|
2018-08-30 00:53:40 +00:00
|
|
|
func NewCursor(b *Buffer, l Loc) *Cursor {
|
|
|
|
|
c := &Cursor{
|
|
|
|
|
buf: b,
|
|
|
|
|
Loc: l,
|
2020-10-20 20:52:49 +00:00
|
|
|
|
|
|
|
|
NewTrailingWsY: -1,
|
2018-08-30 00:53:40 +00:00
|
|
|
}
|
|
|
|
|
c.StoreVisualX()
|
|
|
|
|
return c
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (c *Cursor) SetBuf(b *Buffer) {
|
|
|
|
|
c.buf = b
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (c *Cursor) Buf() *Buffer {
|
|
|
|
|
return c.buf
|
|
|
|
|
}
|
|
|
|
|
|
2017-11-24 18:35:11 +00:00
|
|
|
// Goto puts the cursor at the given cursor's location and gives
|
|
|
|
|
// the current cursor its selection too
|
2016-05-29 15:02:56 +00:00
|
|
|
func (c *Cursor) Goto(b Cursor) {
|
2024-10-13 21:14:37 +00:00
|
|
|
c.X, c.Y = b.X, b.Y
|
2016-05-29 15:02:56 +00:00
|
|
|
c.OrigSelection, c.CurSelection = b.OrigSelection, b.CurSelection
|
2024-10-13 21:14:37 +00:00
|
|
|
c.StoreVisualX()
|
2016-05-29 15:02:56 +00:00
|
|
|
}
|
|
|
|
|
|
2018-01-07 21:17:22 +00:00
|
|
|
// GotoLoc puts the cursor at the given cursor's location and gives
|
|
|
|
|
// the current cursor its selection too
|
|
|
|
|
func (c *Cursor) GotoLoc(l Loc) {
|
|
|
|
|
c.X, c.Y = l.X, l.Y
|
2018-08-27 21:55:28 +00:00
|
|
|
c.StoreVisualX()
|
2018-01-07 21:17:22 +00:00
|
|
|
}
|
|
|
|
|
|
2018-08-26 03:06:44 +00:00
|
|
|
// GetVisualX returns the x value of the cursor in visual spaces
|
Overhaul LastVisualX and GetVisualX() usage
Restore the original meaning of LastVisualX before commit 6d13710d934d
("Implement moving cursor up/down within a wrapped line"): last visual x
location of the cursor in a logical line in the buffer, not in a visual
line on the screen (in other words, taking tabs and wide characters into
account, but not taking softwrap into account). And add a separate
LastWrappedVisualX field, similar to LastVisualX but taking softwrap
into account as well.
This allows tracking last x position at the same time for both cases
when we care about softwrap and when we don't care about it. This can be
useful, for example, for implementing cursor up/down movement actions
that always move by logical lines, not by visual lines, even if softwrap
is enabled (in addition to our default CursorUp and CursorDown actions
that move by visual lines).
Also this fixes a minor bug: in InsertTab(), when `tabstospaces` is
enabled and we insert a tab, the amount of inserted spaces depends on
the visual line wrapping (i.e. on the window width), which is probably
not a good idea.
2024-10-13 21:12:04 +00:00
|
|
|
func (c *Cursor) GetVisualX(wrap bool) int {
|
|
|
|
|
if wrap && c.buf.GetVisualX != nil {
|
2021-03-06 22:43:36 +00:00
|
|
|
return c.buf.GetVisualX(c.Loc)
|
|
|
|
|
}
|
|
|
|
|
|
2018-08-26 03:06:44 +00:00
|
|
|
if c.X <= 0 {
|
|
|
|
|
c.X = 0
|
|
|
|
|
return 0
|
|
|
|
|
}
|
|
|
|
|
|
2018-08-30 00:53:40 +00:00
|
|
|
bytes := c.buf.LineBytes(c.Y)
|
|
|
|
|
tabsize := int(c.buf.Settings["tabsize"].(float64))
|
2018-08-26 03:06:44 +00:00
|
|
|
|
2018-08-27 19:53:10 +00:00
|
|
|
return util.StringWidth(bytes, c.X, tabsize)
|
2018-08-26 03:06:44 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// GetCharPosInLine gets the char position of a visual x y
|
|
|
|
|
// coordinate (this is necessary because tabs are 1 char but
|
|
|
|
|
// 4 visual spaces)
|
|
|
|
|
func (c *Cursor) GetCharPosInLine(b []byte, visualPos int) int {
|
2018-08-30 00:53:40 +00:00
|
|
|
tabsize := int(c.buf.Settings["tabsize"].(float64))
|
2019-01-03 01:07:48 +00:00
|
|
|
return util.GetCharPosInLine(b, visualPos, tabsize)
|
2018-08-26 03:06:44 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Start moves the cursor to the start of the line it is on
|
|
|
|
|
func (c *Cursor) Start() {
|
|
|
|
|
c.X = 0
|
2024-10-13 15:46:34 +00:00
|
|
|
c.StoreVisualX()
|
2018-08-26 03:06:44 +00:00
|
|
|
}
|
|
|
|
|
|
2018-08-28 22:44:52 +00:00
|
|
|
// StartOfText moves the cursor to the first non-whitespace rune of
|
|
|
|
|
// the line it is on
|
|
|
|
|
func (c *Cursor) StartOfText() {
|
|
|
|
|
c.Start()
|
|
|
|
|
for util.IsWhitespace(c.RuneUnder(c.X)) {
|
2020-05-20 20:47:08 +00:00
|
|
|
if c.X == util.CharacterCount(c.buf.LineBytes(c.Y)) {
|
2018-08-28 22:44:52 +00:00
|
|
|
break
|
|
|
|
|
}
|
|
|
|
|
c.Right()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2020-04-10 21:21:02 +00:00
|
|
|
// IsStartOfText returns whether the cursor is at the first
|
|
|
|
|
// non-whitespace rune of the line it is on
|
|
|
|
|
func (c *Cursor) IsStartOfText() bool {
|
|
|
|
|
x := 0
|
|
|
|
|
for util.IsWhitespace(c.RuneUnder(x)) {
|
2020-05-20 20:47:08 +00:00
|
|
|
if x == util.CharacterCount(c.buf.LineBytes(c.Y)) {
|
2020-04-10 21:21:02 +00:00
|
|
|
break
|
|
|
|
|
}
|
|
|
|
|
x++
|
|
|
|
|
}
|
|
|
|
|
return c.X == x
|
|
|
|
|
}
|
|
|
|
|
|
2018-08-26 03:06:44 +00:00
|
|
|
// End moves the cursor to the end of the line it is on
|
|
|
|
|
func (c *Cursor) End() {
|
2020-05-20 20:47:08 +00:00
|
|
|
c.X = util.CharacterCount(c.buf.LineBytes(c.Y))
|
2024-10-13 15:46:34 +00:00
|
|
|
c.StoreVisualX()
|
2018-08-26 03:06:44 +00:00
|
|
|
}
|
|
|
|
|
|
2017-11-24 18:35:11 +00:00
|
|
|
// CopySelection copies the user's selection to either "primary"
|
|
|
|
|
// or "clipboard"
|
2020-07-05 00:00:39 +00:00
|
|
|
func (c *Cursor) CopySelection(target clipboard.Register) {
|
2017-01-09 23:28:45 +00:00
|
|
|
if c.HasSelection() {
|
2020-07-05 00:00:39 +00:00
|
|
|
if target != clipboard.PrimaryReg || c.buf.Settings["useprimary"].(bool) {
|
2020-07-05 05:12:35 +00:00
|
|
|
clipboard.WriteMulti(string(c.GetSelection()), target, c.Num, c.buf.NumCursors())
|
2017-02-08 00:21:25 +00:00
|
|
|
}
|
2017-01-09 23:28:45 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2016-03-19 00:40:00 +00:00
|
|
|
// ResetSelection resets the user's selection
|
|
|
|
|
func (c *Cursor) ResetSelection() {
|
2018-08-30 00:53:40 +00:00
|
|
|
c.CurSelection[0] = c.buf.Start()
|
|
|
|
|
c.CurSelection[1] = c.buf.Start()
|
2016-09-05 01:19:14 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// SetSelectionStart sets the start of the selection
|
|
|
|
|
func (c *Cursor) SetSelectionStart(pos Loc) {
|
2016-09-05 01:28:40 +00:00
|
|
|
c.CurSelection[0] = pos
|
2016-09-05 01:19:14 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// SetSelectionEnd sets the end of the selection
|
|
|
|
|
func (c *Cursor) SetSelectionEnd(pos Loc) {
|
2016-09-05 01:28:40 +00:00
|
|
|
c.CurSelection[1] = pos
|
2016-03-17 21:27:57 +00:00
|
|
|
}
|
|
|
|
|
|
2016-03-19 00:40:00 +00:00
|
|
|
// HasSelection returns whether or not the user has selected anything
|
|
|
|
|
func (c *Cursor) HasSelection() bool {
|
2016-05-28 21:29:49 +00:00
|
|
|
return c.CurSelection[0] != c.CurSelection[1]
|
2016-03-17 21:27:57 +00:00
|
|
|
}
|
|
|
|
|
|
2016-03-19 22:38:28 +00:00
|
|
|
// DeleteSelection deletes the currently selected text
|
|
|
|
|
func (c *Cursor) DeleteSelection() {
|
2016-06-07 15:43:28 +00:00
|
|
|
if c.CurSelection[0].GreaterThan(c.CurSelection[1]) {
|
2018-08-30 00:53:40 +00:00
|
|
|
c.buf.Remove(c.CurSelection[1], c.CurSelection[0])
|
2016-06-07 15:43:28 +00:00
|
|
|
c.Loc = c.CurSelection[1]
|
2016-09-05 12:36:30 +00:00
|
|
|
} else if !c.HasSelection() {
|
2016-04-24 01:29:09 +00:00
|
|
|
return
|
2016-03-19 00:01:05 +00:00
|
|
|
} else {
|
2018-08-30 00:53:40 +00:00
|
|
|
c.buf.Remove(c.CurSelection[0], c.CurSelection[1])
|
2016-06-07 15:43:28 +00:00
|
|
|
c.Loc = c.CurSelection[0]
|
2016-03-19 00:01:05 +00:00
|
|
|
}
|
2016-03-17 21:27:57 +00:00
|
|
|
}
|
|
|
|
|
|
2018-08-27 21:55:28 +00:00
|
|
|
// Deselect closes the cursor's current selection
|
|
|
|
|
// Start indicates whether the cursor should be placed
|
|
|
|
|
// at the start or end of the selection
|
|
|
|
|
func (c *Cursor) Deselect(start bool) {
|
|
|
|
|
if c.HasSelection() {
|
|
|
|
|
if start {
|
|
|
|
|
c.Loc = c.CurSelection[0]
|
|
|
|
|
} else {
|
2024-11-30 14:25:14 +00:00
|
|
|
c.Loc = c.CurSelection[1]
|
2018-08-27 21:55:28 +00:00
|
|
|
}
|
|
|
|
|
c.ResetSelection()
|
|
|
|
|
c.StoreVisualX()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2016-03-19 22:38:28 +00:00
|
|
|
// GetSelection returns the cursor's selection
|
2018-08-26 03:06:44 +00:00
|
|
|
func (c *Cursor) GetSelection() []byte {
|
2018-08-30 00:53:40 +00:00
|
|
|
if InBounds(c.CurSelection[0], c.buf) && InBounds(c.CurSelection[1], c.buf) {
|
2016-11-12 01:12:21 +00:00
|
|
|
if c.CurSelection[0].GreaterThan(c.CurSelection[1]) {
|
2018-08-30 00:53:40 +00:00
|
|
|
return c.buf.Substr(c.CurSelection[1], c.CurSelection[0])
|
2016-11-12 01:12:21 +00:00
|
|
|
}
|
2018-08-30 00:53:40 +00:00
|
|
|
return c.buf.Substr(c.CurSelection[0], c.CurSelection[1])
|
2016-03-19 22:38:28 +00:00
|
|
|
}
|
2018-08-26 03:06:44 +00:00
|
|
|
return []byte{}
|
2016-03-19 22:38:28 +00:00
|
|
|
}
|
|
|
|
|
|
2016-03-26 20:32:19 +00:00
|
|
|
// SelectLine selects the current line
|
|
|
|
|
func (c *Cursor) SelectLine() {
|
|
|
|
|
c.Start()
|
2016-09-05 01:19:14 +00:00
|
|
|
c.SetSelectionStart(c.Loc)
|
2016-03-26 20:32:19 +00:00
|
|
|
c.End()
|
2018-08-30 00:53:40 +00:00
|
|
|
if len(c.buf.lines)-1 > c.Y {
|
|
|
|
|
c.SetSelectionEnd(c.Loc.Move(1, c.buf))
|
2016-04-23 18:02:20 +00:00
|
|
|
} else {
|
2016-09-05 01:19:14 +00:00
|
|
|
c.SetSelectionEnd(c.Loc)
|
2016-04-23 18:02:20 +00:00
|
|
|
}
|
2016-03-27 20:35:54 +00:00
|
|
|
|
2016-05-28 21:29:49 +00:00
|
|
|
c.OrigSelection = c.CurSelection
|
2016-03-26 20:32:19 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// AddLineToSelection adds the current line to the selection
|
|
|
|
|
func (c *Cursor) AddLineToSelection() {
|
2016-06-07 15:43:28 +00:00
|
|
|
if c.Loc.LessThan(c.OrigSelection[0]) {
|
2016-03-28 12:43:08 +00:00
|
|
|
c.Start()
|
2016-09-05 01:19:14 +00:00
|
|
|
c.SetSelectionStart(c.Loc)
|
|
|
|
|
c.SetSelectionEnd(c.OrigSelection[1])
|
2016-03-28 12:43:08 +00:00
|
|
|
}
|
2016-06-07 15:43:28 +00:00
|
|
|
if c.Loc.GreaterThan(c.OrigSelection[1]) {
|
2016-03-28 12:43:08 +00:00
|
|
|
c.End()
|
2018-08-30 00:53:40 +00:00
|
|
|
c.SetSelectionEnd(c.Loc.Move(1, c.buf))
|
2016-09-05 01:19:14 +00:00
|
|
|
c.SetSelectionStart(c.OrigSelection[0])
|
2016-03-28 12:43:08 +00:00
|
|
|
}
|
|
|
|
|
|
2016-06-07 15:43:28 +00:00
|
|
|
if c.Loc.LessThan(c.OrigSelection[1]) && c.Loc.GreaterThan(c.OrigSelection[0]) {
|
2016-05-28 21:29:49 +00:00
|
|
|
c.CurSelection = c.OrigSelection
|
2016-03-28 12:43:08 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2016-05-31 22:02:42 +00:00
|
|
|
// UpN moves the cursor up N lines (if possible)
|
|
|
|
|
func (c *Cursor) UpN(amount int) {
|
|
|
|
|
proposedY := c.Y - amount
|
|
|
|
|
if proposedY < 0 {
|
2017-09-07 20:44:05 +00:00
|
|
|
proposedY = 0
|
2018-08-30 00:53:40 +00:00
|
|
|
} else if proposedY >= len(c.buf.lines) {
|
|
|
|
|
proposedY = len(c.buf.lines) - 1
|
2016-05-31 22:02:42 +00:00
|
|
|
}
|
2016-03-17 21:27:57 +00:00
|
|
|
|
2018-08-30 00:53:40 +00:00
|
|
|
bytes := c.buf.LineBytes(proposedY)
|
2018-08-26 03:06:44 +00:00
|
|
|
c.X = c.GetCharPosInLine(bytes, c.LastVisualX)
|
|
|
|
|
|
2020-05-20 20:47:08 +00:00
|
|
|
if c.X > util.CharacterCount(bytes) || (amount < 0 && proposedY == c.Y) {
|
|
|
|
|
c.X = util.CharacterCount(bytes)
|
2020-06-03 04:27:24 +00:00
|
|
|
c.StoreVisualX()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if c.X < 0 || (amount > 0 && proposedY == c.Y) {
|
|
|
|
|
c.X = 0
|
|
|
|
|
c.StoreVisualX()
|
2016-03-17 21:27:57 +00:00
|
|
|
}
|
2017-08-09 15:58:37 +00:00
|
|
|
|
|
|
|
|
c.Y = proposedY
|
2016-03-17 21:27:57 +00:00
|
|
|
}
|
2016-03-19 00:40:00 +00:00
|
|
|
|
2016-05-31 22:02:42 +00:00
|
|
|
// DownN moves the cursor down N lines (if possible)
|
|
|
|
|
func (c *Cursor) DownN(amount int) {
|
|
|
|
|
c.UpN(-amount)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Up moves the cursor up one line (if possible)
|
|
|
|
|
func (c *Cursor) Up() {
|
|
|
|
|
c.UpN(1)
|
|
|
|
|
}
|
|
|
|
|
|
2016-03-19 00:40:00 +00:00
|
|
|
// Down moves the cursor down one line (if possible)
|
|
|
|
|
func (c *Cursor) Down() {
|
2016-05-31 22:02:42 +00:00
|
|
|
c.DownN(1)
|
2016-03-17 21:27:57 +00:00
|
|
|
}
|
2016-03-19 00:40:00 +00:00
|
|
|
|
2017-11-24 18:35:11 +00:00
|
|
|
// Left moves the cursor left one cell (if possible) or to
|
|
|
|
|
// the previous line if it is at the beginning
|
2016-03-19 00:40:00 +00:00
|
|
|
func (c *Cursor) Left() {
|
2018-08-30 00:53:40 +00:00
|
|
|
if c.Loc == c.buf.Start() {
|
2016-03-19 00:40:00 +00:00
|
|
|
return
|
|
|
|
|
}
|
2016-05-28 21:29:49 +00:00
|
|
|
if c.X > 0 {
|
|
|
|
|
c.X--
|
2016-03-17 21:27:57 +00:00
|
|
|
} else {
|
2016-03-19 00:40:00 +00:00
|
|
|
c.Up()
|
|
|
|
|
c.End()
|
2016-03-17 21:27:57 +00:00
|
|
|
}
|
2018-08-27 21:55:28 +00:00
|
|
|
c.StoreVisualX()
|
2016-03-17 21:27:57 +00:00
|
|
|
}
|
2016-03-19 00:40:00 +00:00
|
|
|
|
2017-11-24 18:35:11 +00:00
|
|
|
// Right moves the cursor right one cell (if possible) or
|
|
|
|
|
// to the next line if it is at the end
|
2016-03-19 00:40:00 +00:00
|
|
|
func (c *Cursor) Right() {
|
2018-08-30 00:53:40 +00:00
|
|
|
if c.Loc == c.buf.End() {
|
2016-03-19 00:40:00 +00:00
|
|
|
return
|
|
|
|
|
}
|
2020-05-20 20:47:08 +00:00
|
|
|
if c.X < util.CharacterCount(c.buf.LineBytes(c.Y)) {
|
2016-05-28 21:29:49 +00:00
|
|
|
c.X++
|
2016-03-17 21:27:57 +00:00
|
|
|
} else {
|
2016-03-19 00:40:00 +00:00
|
|
|
c.Down()
|
|
|
|
|
c.Start()
|
2016-03-17 21:27:57 +00:00
|
|
|
}
|
2018-08-27 21:55:28 +00:00
|
|
|
c.StoreVisualX()
|
2016-03-17 21:27:57 +00:00
|
|
|
}
|
|
|
|
|
|
2017-11-24 18:35:11 +00:00
|
|
|
// Relocate makes sure that the cursor is inside the bounds
|
|
|
|
|
// of the buffer If it isn't, it moves it to be within the
|
|
|
|
|
// buffer's lines
|
2016-04-24 21:26:42 +00:00
|
|
|
func (c *Cursor) Relocate() {
|
2016-05-28 21:29:49 +00:00
|
|
|
if c.Y < 0 {
|
|
|
|
|
c.Y = 0
|
2018-08-30 00:53:40 +00:00
|
|
|
} else if c.Y >= len(c.buf.lines) {
|
|
|
|
|
c.Y = len(c.buf.lines) - 1
|
2016-04-24 21:26:42 +00:00
|
|
|
}
|
|
|
|
|
|
2016-05-28 21:29:49 +00:00
|
|
|
if c.X < 0 {
|
|
|
|
|
c.X = 0
|
2020-05-20 20:47:08 +00:00
|
|
|
} else if c.X > util.CharacterCount(c.buf.LineBytes(c.Y)) {
|
|
|
|
|
c.X = util.CharacterCount(c.buf.LineBytes(c.Y))
|
2016-03-17 21:27:57 +00:00
|
|
|
}
|
|
|
|
|
}
|
2018-08-27 21:55:28 +00:00
|
|
|
|
2018-08-28 22:44:52 +00:00
|
|
|
// SelectWord selects the word the cursor is currently on
|
|
|
|
|
func (c *Cursor) SelectWord() {
|
2018-08-30 00:53:40 +00:00
|
|
|
if len(c.buf.LineBytes(c.Y)) == 0 {
|
2018-08-28 22:44:52 +00:00
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if !util.IsWordChar(c.RuneUnder(c.X)) {
|
|
|
|
|
c.SetSelectionStart(c.Loc)
|
2018-08-30 00:53:40 +00:00
|
|
|
c.SetSelectionEnd(c.Loc.Move(1, c.buf))
|
2018-08-28 22:44:52 +00:00
|
|
|
c.OrigSelection = c.CurSelection
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
forward, backward := c.X, c.X
|
|
|
|
|
|
|
|
|
|
for backward > 0 && util.IsWordChar(c.RuneUnder(backward-1)) {
|
|
|
|
|
backward--
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
c.SetSelectionStart(Loc{backward, c.Y})
|
|
|
|
|
c.OrigSelection[0] = c.CurSelection[0]
|
|
|
|
|
|
2020-05-20 20:47:08 +00:00
|
|
|
lineLen := util.CharacterCount(c.buf.LineBytes(c.Y)) - 1
|
2018-08-28 22:44:52 +00:00
|
|
|
for forward < lineLen && util.IsWordChar(c.RuneUnder(forward+1)) {
|
|
|
|
|
forward++
|
|
|
|
|
}
|
|
|
|
|
|
2018-08-30 00:53:40 +00:00
|
|
|
c.SetSelectionEnd(Loc{forward, c.Y}.Move(1, c.buf))
|
2018-08-28 22:44:52 +00:00
|
|
|
c.OrigSelection[1] = c.CurSelection[1]
|
|
|
|
|
c.Loc = c.CurSelection[1]
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// AddWordToSelection adds the word the cursor is currently on
|
|
|
|
|
// to the selection
|
|
|
|
|
func (c *Cursor) AddWordToSelection() {
|
|
|
|
|
if c.Loc.GreaterThan(c.OrigSelection[0]) && c.Loc.LessThan(c.OrigSelection[1]) {
|
|
|
|
|
c.CurSelection = c.OrigSelection
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if c.Loc.LessThan(c.OrigSelection[0]) {
|
|
|
|
|
backward := c.X
|
|
|
|
|
|
|
|
|
|
for backward > 0 && util.IsWordChar(c.RuneUnder(backward-1)) {
|
|
|
|
|
backward--
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
c.SetSelectionStart(Loc{backward, c.Y})
|
|
|
|
|
c.SetSelectionEnd(c.OrigSelection[1])
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if c.Loc.GreaterThan(c.OrigSelection[1]) {
|
|
|
|
|
forward := c.X
|
|
|
|
|
|
2020-05-20 20:47:08 +00:00
|
|
|
lineLen := util.CharacterCount(c.buf.LineBytes(c.Y)) - 1
|
2018-08-28 22:44:52 +00:00
|
|
|
for forward < lineLen && util.IsWordChar(c.RuneUnder(forward+1)) {
|
|
|
|
|
forward++
|
|
|
|
|
}
|
|
|
|
|
|
2018-08-30 00:53:40 +00:00
|
|
|
c.SetSelectionEnd(Loc{forward, c.Y}.Move(1, c.buf))
|
2018-08-28 22:44:52 +00:00
|
|
|
c.SetSelectionStart(c.OrigSelection[0])
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
c.Loc = c.CurSelection[1]
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// SelectTo selects from the current cursor location to the given
|
|
|
|
|
// location
|
|
|
|
|
func (c *Cursor) SelectTo(loc Loc) {
|
|
|
|
|
if loc.GreaterThan(c.OrigSelection[0]) {
|
|
|
|
|
c.SetSelectionStart(c.OrigSelection[0])
|
|
|
|
|
c.SetSelectionEnd(loc)
|
|
|
|
|
} else {
|
|
|
|
|
c.SetSelectionStart(loc)
|
|
|
|
|
c.SetSelectionEnd(c.OrigSelection[0])
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// WordRight moves the cursor one word to the right
|
|
|
|
|
func (c *Cursor) WordRight() {
|
2024-06-04 19:10:09 +00:00
|
|
|
if c.X == util.CharacterCount(c.buf.LineBytes(c.Y)) {
|
|
|
|
|
c.Right()
|
|
|
|
|
return
|
|
|
|
|
}
|
2018-08-28 22:44:52 +00:00
|
|
|
for util.IsWhitespace(c.RuneUnder(c.X)) {
|
2020-05-20 20:47:08 +00:00
|
|
|
if c.X == util.CharacterCount(c.buf.LineBytes(c.Y)) {
|
2018-08-28 22:44:52 +00:00
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
c.Right()
|
|
|
|
|
}
|
2024-05-14 07:01:44 +00:00
|
|
|
if util.IsNonWordChar(c.RuneUnder(c.X)) && !util.IsWhitespace(c.RuneUnder(c.X)) &&
|
|
|
|
|
util.IsNonWordChar(c.RuneUnder(c.X+1)) {
|
|
|
|
|
for util.IsNonWordChar(c.RuneUnder(c.X)) && !util.IsWhitespace(c.RuneUnder(c.X)) {
|
|
|
|
|
if c.X == util.CharacterCount(c.buf.LineBytes(c.Y)) {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
c.Right()
|
|
|
|
|
}
|
|
|
|
|
return
|
|
|
|
|
}
|
2018-08-28 22:44:52 +00:00
|
|
|
c.Right()
|
|
|
|
|
for util.IsWordChar(c.RuneUnder(c.X)) {
|
2020-05-20 20:47:08 +00:00
|
|
|
if c.X == util.CharacterCount(c.buf.LineBytes(c.Y)) {
|
2018-08-28 22:44:52 +00:00
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
c.Right()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// WordLeft moves the cursor one word to the left
|
|
|
|
|
func (c *Cursor) WordLeft() {
|
2024-06-04 19:10:09 +00:00
|
|
|
if c.X == 0 {
|
|
|
|
|
c.Left()
|
|
|
|
|
return
|
|
|
|
|
}
|
2018-08-28 22:44:52 +00:00
|
|
|
c.Left()
|
|
|
|
|
for util.IsWhitespace(c.RuneUnder(c.X)) {
|
|
|
|
|
if c.X == 0 {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
c.Left()
|
|
|
|
|
}
|
2024-05-14 07:01:44 +00:00
|
|
|
if util.IsNonWordChar(c.RuneUnder(c.X)) && !util.IsWhitespace(c.RuneUnder(c.X)) &&
|
|
|
|
|
util.IsNonWordChar(c.RuneUnder(c.X-1)) {
|
|
|
|
|
for util.IsNonWordChar(c.RuneUnder(c.X)) && !util.IsWhitespace(c.RuneUnder(c.X)) {
|
|
|
|
|
if c.X == 0 {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
c.Left()
|
|
|
|
|
}
|
|
|
|
|
c.Right()
|
|
|
|
|
return
|
|
|
|
|
}
|
2018-08-28 22:44:52 +00:00
|
|
|
c.Left()
|
|
|
|
|
for util.IsWordChar(c.RuneUnder(c.X)) {
|
|
|
|
|
if c.X == 0 {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
c.Left()
|
|
|
|
|
}
|
|
|
|
|
c.Right()
|
|
|
|
|
}
|
|
|
|
|
|
2024-05-14 06:59:08 +00:00
|
|
|
// SubWordRight moves the cursor one sub-word to the right
|
|
|
|
|
func (c *Cursor) SubWordRight() {
|
2024-06-04 19:10:09 +00:00
|
|
|
if c.X == util.CharacterCount(c.buf.LineBytes(c.Y)) {
|
|
|
|
|
c.Right()
|
|
|
|
|
return
|
|
|
|
|
}
|
2024-05-14 06:59:08 +00:00
|
|
|
if util.IsWhitespace(c.RuneUnder(c.X)) {
|
|
|
|
|
for util.IsWhitespace(c.RuneUnder(c.X)) {
|
|
|
|
|
if c.X == util.CharacterCount(c.buf.LineBytes(c.Y)) {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
c.Right()
|
|
|
|
|
}
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
if util.IsNonWordChar(c.RuneUnder(c.X)) && !util.IsWhitespace(c.RuneUnder(c.X)) {
|
|
|
|
|
for util.IsNonWordChar(c.RuneUnder(c.X)) && !util.IsWhitespace(c.RuneUnder(c.X)) {
|
|
|
|
|
if c.X == util.CharacterCount(c.buf.LineBytes(c.Y)) {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
c.Right()
|
|
|
|
|
}
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
if util.IsSubwordDelimiter(c.RuneUnder(c.X)) {
|
|
|
|
|
for util.IsSubwordDelimiter(c.RuneUnder(c.X)) {
|
|
|
|
|
if c.X == util.CharacterCount(c.buf.LineBytes(c.Y)) {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
c.Right()
|
|
|
|
|
}
|
|
|
|
|
if util.IsWhitespace(c.RuneUnder(c.X)) {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if c.X == util.CharacterCount(c.buf.LineBytes(c.Y)) {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
if util.IsUpperLetter(c.RuneUnder(c.X)) &&
|
|
|
|
|
util.IsUpperLetter(c.RuneUnder(c.X+1)) {
|
|
|
|
|
for util.IsUpperAlphanumeric(c.RuneUnder(c.X)) {
|
|
|
|
|
if c.X == util.CharacterCount(c.buf.LineBytes(c.Y)) {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
c.Right()
|
|
|
|
|
}
|
|
|
|
|
if util.IsLowerAlphanumeric(c.RuneUnder(c.X)) {
|
|
|
|
|
c.Left()
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
c.Right()
|
|
|
|
|
for util.IsLowerAlphanumeric(c.RuneUnder(c.X)) {
|
|
|
|
|
if c.X == util.CharacterCount(c.buf.LineBytes(c.Y)) {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
c.Right()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// SubWordLeft moves the cursor one sub-word to the left
|
|
|
|
|
func (c *Cursor) SubWordLeft() {
|
2024-06-04 19:10:09 +00:00
|
|
|
if c.X == 0 {
|
|
|
|
|
c.Left()
|
|
|
|
|
return
|
|
|
|
|
}
|
2024-05-14 06:59:08 +00:00
|
|
|
c.Left()
|
|
|
|
|
if util.IsWhitespace(c.RuneUnder(c.X)) {
|
|
|
|
|
for util.IsWhitespace(c.RuneUnder(c.X)) {
|
|
|
|
|
if c.X == 0 {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
c.Left()
|
|
|
|
|
}
|
|
|
|
|
c.Right()
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
if util.IsNonWordChar(c.RuneUnder(c.X)) && !util.IsWhitespace(c.RuneUnder(c.X)) {
|
|
|
|
|
for util.IsNonWordChar(c.RuneUnder(c.X)) && !util.IsWhitespace(c.RuneUnder(c.X)) {
|
|
|
|
|
if c.X == 0 {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
c.Left()
|
|
|
|
|
}
|
|
|
|
|
c.Right()
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
if util.IsSubwordDelimiter(c.RuneUnder(c.X)) {
|
|
|
|
|
for util.IsSubwordDelimiter(c.RuneUnder(c.X)) {
|
|
|
|
|
if c.X == 0 {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
c.Left()
|
|
|
|
|
}
|
|
|
|
|
if util.IsWhitespace(c.RuneUnder(c.X)) {
|
|
|
|
|
c.Right()
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if c.X == 0 {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
if util.IsUpperLetter(c.RuneUnder(c.X)) &&
|
|
|
|
|
util.IsUpperLetter(c.RuneUnder(c.X-1)) {
|
|
|
|
|
for util.IsUpperAlphanumeric(c.RuneUnder(c.X)) {
|
|
|
|
|
if c.X == 0 {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
c.Left()
|
|
|
|
|
}
|
|
|
|
|
if !util.IsUpperAlphanumeric(c.RuneUnder(c.X)) {
|
|
|
|
|
c.Right()
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
for util.IsLowerAlphanumeric(c.RuneUnder(c.X)) {
|
|
|
|
|
if c.X == 0 {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
c.Left()
|
|
|
|
|
}
|
|
|
|
|
if !util.IsAlphanumeric(c.RuneUnder(c.X)) {
|
|
|
|
|
c.Right()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2018-08-28 22:44:52 +00:00
|
|
|
// RuneUnder returns the rune under the given x position
|
|
|
|
|
func (c *Cursor) RuneUnder(x int) rune {
|
2018-08-30 00:53:40 +00:00
|
|
|
line := c.buf.LineBytes(c.Y)
|
2020-05-20 20:47:08 +00:00
|
|
|
if len(line) == 0 || x >= util.CharacterCount(line) {
|
2018-08-28 22:44:52 +00:00
|
|
|
return '\n'
|
|
|
|
|
} else if x < 0 {
|
|
|
|
|
x = 0
|
|
|
|
|
}
|
|
|
|
|
i := 0
|
|
|
|
|
for len(line) > 0 {
|
2020-05-20 20:43:12 +00:00
|
|
|
r, _, size := util.DecodeCharacter(line)
|
2018-08-28 22:44:52 +00:00
|
|
|
line = line[size:]
|
|
|
|
|
|
|
|
|
|
if i == x {
|
|
|
|
|
return r
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
i++
|
|
|
|
|
}
|
|
|
|
|
return '\n'
|
|
|
|
|
}
|
|
|
|
|
|
2018-08-27 21:55:28 +00:00
|
|
|
func (c *Cursor) StoreVisualX() {
|
Overhaul LastVisualX and GetVisualX() usage
Restore the original meaning of LastVisualX before commit 6d13710d934d
("Implement moving cursor up/down within a wrapped line"): last visual x
location of the cursor in a logical line in the buffer, not in a visual
line on the screen (in other words, taking tabs and wide characters into
account, but not taking softwrap into account). And add a separate
LastWrappedVisualX field, similar to LastVisualX but taking softwrap
into account as well.
This allows tracking last x position at the same time for both cases
when we care about softwrap and when we don't care about it. This can be
useful, for example, for implementing cursor up/down movement actions
that always move by logical lines, not by visual lines, even if softwrap
is enabled (in addition to our default CursorUp and CursorDown actions
that move by visual lines).
Also this fixes a minor bug: in InsertTab(), when `tabstospaces` is
enabled and we insert a tab, the amount of inserted spaces depends on
the visual line wrapping (i.e. on the window width), which is probably
not a good idea.
2024-10-13 21:12:04 +00:00
|
|
|
c.LastVisualX = c.GetVisualX(false)
|
|
|
|
|
c.LastWrappedVisualX = c.GetVisualX(true)
|
2018-08-27 21:55:28 +00:00
|
|
|
}
|