diff --git a/src/cursor.go b/src/cursor.go index 4036d0a8..44b3e451 100644 --- a/src/cursor.go +++ b/src/cursor.go @@ -42,7 +42,11 @@ type Cursor struct { x int y int - curSelection [2]int + // The current selection as a range of character numbers (inclusive) + curSelection [2]int + // 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 origSelection [2]int } @@ -97,43 +101,93 @@ func (c *Cursor) SelectLine() { c.End() c.curSelection[1] = c.Loc() - c.origSelection[0] = c.curSelection[0] - c.origSelection[1] = c.curSelection[1] + c.origSelection = c.curSelection } // AddLineToSelection adds the current line to the selection func (c *Cursor) AddLineToSelection() { loc := c.Loc() + if loc < c.origSelection[0] { + c.Start() + c.curSelection[0] = c.Loc() + c.curSelection[1] = c.origSelection[1] + } + if loc > c.origSelection[1] { + c.End() + c.curSelection[1] = c.Loc() + c.curSelection[0] = c.origSelection[0] + } + + if loc < c.origSelection[1] && loc > c.origSelection[0] { + c.curSelection = c.origSelection + } +} + +// SelectWord selects the word the cursor is currently on +func (c *Cursor) SelectWord() { + if !IsWordChar(string(c.RuneUnder(c.x))) { + return + } + + forward, backward := c.x, c.x + + for backward > 0 && IsWordChar(string(c.RuneUnder(backward-1))) { + backward-- + } + + c.curSelection[0] = ToCharPos(backward, c.y, c.v.buf) + c.origSelection[0] = c.curSelection[0] + + for forward < Count(c.v.buf.lines[c.y])-1 && IsWordChar(string(c.RuneUnder(forward+1))) { + forward++ + } + + c.curSelection[1] = ToCharPos(forward, c.y, c.v.buf) + c.origSelection[1] = c.curSelection[1] +} + +// AddWordToSelection adds the word the cursor is currently on to the selection +func (c *Cursor) AddWordToSelection() { + loc := c.Loc() + if loc > c.origSelection[0] && loc < c.origSelection[1] { c.curSelection = c.origSelection return } if loc < c.origSelection[0] { - c.Start() - c.curSelection[0] = c.Loc() - } else if loc > c.origSelection[1] { - c.End() - c.curSelection[1] = c.Loc() + backward := c.x + + for backward > 0 && IsWordChar(string(c.RuneUnder(backward-1))) { + backward-- + } + + c.curSelection[0] = ToCharPos(backward, c.y, c.v.buf) + c.curSelection[1] = c.origSelection[1] } - if loc < c.curSelection[0] { - c.Start() - c.curSelection[0] = c.Loc() - } else if loc > c.curSelection[1] { - c.End() - c.curSelection[1] = c.Loc() + if loc > c.origSelection[1] { + forward := c.x + + for forward < Count(c.v.buf.lines[c.y])-1 && IsWordChar(string(c.RuneUnder(forward+1))) { + forward++ + } + + c.curSelection[1] = ToCharPos(forward, c.y, c.v.buf) + c.curSelection[0] = c.origSelection[0] } } -// RuneUnder returns the rune under the cursor -func (c *Cursor) RuneUnder() rune { - line := c.v.buf.lines[c.y] - if c.x >= Count(line) { - return ' ' +// RuneUnder returns the rune under the given x position +func (c *Cursor) RuneUnder(x int) rune { + line := []rune(c.v.buf.lines[c.y]) + if x >= len(line) { + x = len(line) - 1 + } else if x < 0 { + x = 0 } - return []rune(line)[c.x] + return line[x] } // Up moves the cursor up one line (if possible) diff --git a/src/util.go b/src/util.go index bb27e4ac..ca2ffb05 100644 --- a/src/util.go +++ b/src/util.go @@ -48,3 +48,15 @@ func Max(a, b int) int { } return b } + +// IsWordChar returns whether or not the string is a 'word character' +// If it is a unicode character, then it does not match +// Word characters are defined as [A-Za-z0-9_] +func IsWordChar(str string) bool { + if len(str) > 1 { + // Unicode + return false + } + c := str[0] + return (c >= '0' && c <= '9') || (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || (c == '_') +} diff --git a/src/util_test.go b/src/util_test.go index e1955aa3..29fc009f 100644 --- a/src/util_test.go +++ b/src/util_test.go @@ -33,3 +33,33 @@ func TestSpaces(t *testing.T) { } } } + +func TestIsWordChar(t *testing.T) { + if IsWordChar("t") == false { + t.Errorf("IsWordChar(t) = false") + } + if IsWordChar("T") == false { + t.Errorf("IsWordChar(T) = false") + } + if IsWordChar("5") == false { + t.Errorf("IsWordChar(5) = false") + } + if IsWordChar("_") == false { + t.Errorf("IsWordChar(_) = false") + } + if IsWordChar("~") == true { + t.Errorf("IsWordChar(~) = true") + } + if IsWordChar(" ") == true { + t.Errorf("IsWordChar( ) = true") + } + if IsWordChar("ß") == true { + t.Errorf("IsWordChar(ß) = true") + } + if IsWordChar(")") == true { + t.Errorf("IsWordChar()) = true") + } + if IsWordChar("\n") == true { + t.Errorf("IsWordChar(\n)) = true") + } +} diff --git a/src/view.go b/src/view.go index 58fe28ba..de3bfc76 100644 --- a/src/view.go +++ b/src/view.go @@ -456,14 +456,19 @@ func (v *View) HandleEvent(event tcell.Event) { if v.doubleClick { // Triple click v.lastClickTime = time.Now() + v.tripleClick = true v.doubleClick = false + v.cursor.SelectLine() } else { // Double click + v.lastClickTime = time.Now() + v.doubleClick = true v.tripleClick = false - v.lastClickTime = time.Now() + + v.cursor.SelectWord() } } else { v.doubleClick = false @@ -478,7 +483,7 @@ func (v *View) HandleEvent(event tcell.Event) { if v.tripleClick { v.cursor.AddLineToSelection() } else if v.doubleClick { - + v.cursor.AddWordToSelection() } else { v.cursor.curSelection[1] = v.cursor.Loc() } diff --git a/todolist.md b/todolist.md index 988207ab..5a6742aa 100644 --- a/todolist.md +++ b/todolist.md @@ -8,10 +8,6 @@ - [ ] Search and replace -- [ ] Better selection - - [ ] Double click selects current word - - [x] Triple click enables line selection - - [ ] More keybindings - [x] Page up and page down - [x] CtrlA for select all @@ -40,6 +36,10 @@ - [x] Help screen which lists keybindings and commands - [x] Opened with Ctrl-h +- [x] Better selection + - [x] Double click selects current word + - [x] Triple click enables line selection + - [x] Options - [x] Colorscheme - [x] tab size