From 889a841575bd1140b3f24614266cd988bd971655 Mon Sep 17 00:00:00 2001 From: Massimo Mund Date: Tue, 14 May 2024 08:51:13 +0200 Subject: [PATCH 1/3] Replaced IsNonAlphaNumeric() with IsNonWordChar() --- internal/action/actions.go | 4 ++-- internal/buffer/autocomplete.go | 8 ++++---- internal/util/util.go | 18 ++++++++++-------- 3 files changed, 16 insertions(+), 14 deletions(-) diff --git a/internal/action/actions.go b/internal/action/actions.go index 883f8208f..621cb55b1 100644 --- a/internal/action/actions.go +++ b/internal/action/actions.go @@ -745,8 +745,8 @@ func (h *BufPane) Autocomplete() bool { } 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) + if !util.IsAutocomplete(prev) || util.IsWordChar(r) { + // don't autocomplete if cursor is within a word return false } diff --git a/internal/buffer/autocomplete.go b/internal/buffer/autocomplete.go index 7a8c5bee0..8a1c3742a 100644 --- a/internal/buffer/autocomplete.go +++ b/internal/buffer/autocomplete.go @@ -73,11 +73,11 @@ func (b *Buffer) GetWord() ([]byte, int) { return []byte{}, -1 } - if util.IsNonAlphaNumeric(b.RuneAt(c.Loc.Move(-1, b))) { + if util.IsNonWordChar(b.RuneAt(c.Loc.Move(-1, b))) { return []byte{}, c.X } - args := bytes.FieldsFunc(l, util.IsNonAlphaNumeric) + args := bytes.FieldsFunc(l, util.IsNonWordChar) input := args[len(args)-1] return input, c.X - util.CharacterCount(input) } @@ -166,7 +166,7 @@ func BufferComplete(b *Buffer) ([]string, []string) { var suggestions []string for i := c.Y; i >= 0; i-- { l := b.LineBytes(i) - words := bytes.FieldsFunc(l, util.IsNonAlphaNumeric) + words := bytes.FieldsFunc(l, util.IsNonWordChar) for _, w := range words { if bytes.HasPrefix(w, input) && util.CharacterCount(w) > inputLen { strw := string(w) @@ -179,7 +179,7 @@ func BufferComplete(b *Buffer) ([]string, []string) { } for i := c.Y + 1; i < b.LinesNum(); i++ { l := b.LineBytes(i) - words := bytes.FieldsFunc(l, util.IsNonAlphaNumeric) + words := bytes.FieldsFunc(l, util.IsNonWordChar) for _, w := range words { if bytes.HasPrefix(w, input) && util.CharacterCount(w) > inputLen { strw := string(w) diff --git a/internal/util/util.go b/internal/util/util.go index bebd949b6..b7bebcaf2 100644 --- a/internal/util/util.go +++ b/internal/util/util.go @@ -218,12 +218,19 @@ func FSize(f *os.File) int64 { return fi.Size() } -// IsWordChar returns whether or not the string is a 'word character' -// Word characters are defined as numbers, letters, or '_' +// IsWordChar returns whether or not a rune is a 'word character' +// Word characters are defined as numbers, letters or '_' func IsWordChar(r rune) bool { return unicode.IsLetter(r) || unicode.IsNumber(r) || r == '_' } +// IsNonWordChar returns whether or not a rune is not a 'word character' +// Non word characters are defined as all characters not being numbers, letters or '_' +// See IsWordChar() +func IsNonWordChar(r rune) bool { + return !IsWordChar(r) +} + // Spaces returns a string with n spaces func Spaces(n int) string { return strings.Repeat(" ", n) @@ -445,14 +452,9 @@ func Clamp(val, min, max int) int { return val } -// IsNonAlphaNumeric returns if the rune is not a number of letter or underscore. -func IsNonAlphaNumeric(c rune) bool { - return !unicode.IsLetter(c) && !unicode.IsNumber(c) && c != '_' -} - // IsAutocomplete returns whether a character should begin an autocompletion. func IsAutocomplete(c rune) bool { - return c == '.' || !IsNonAlphaNumeric(c) + return c == '.' || IsWordChar(c) } // ParseSpecial replaces escaped ts with '\t'. From 5dbdf8c0e835be2a53610da20af12ba6368fbabc Mon Sep 17 00:00:00 2001 From: Massimo Mund Date: Tue, 14 May 2024 08:59:08 +0200 Subject: [PATCH 2/3] Implemented SubWordRight, SubWordLeft, SelectSubWordRight, SelectSubWordLeft and DeleteSubWordRight, DeleteSubWordLeft --- internal/action/actions.go | 60 ++++++++++++++++++ internal/action/bufpane.go | 12 ++++ internal/buffer/cursor.go | 121 ++++++++++++++++++++++++++++++++++++ internal/util/util.go | 57 ++++++++++++++++- runtime/help/keybindings.md | 6 ++ 5 files changed, 253 insertions(+), 3 deletions(-) diff --git a/internal/action/actions.go b/internal/action/actions.go index 621cb55b1..4f1b7cd61 100644 --- a/internal/action/actions.go +++ b/internal/action/actions.go @@ -283,6 +283,22 @@ func (h *BufPane) WordLeft() bool { return true } +// SubWordRight moves the cursor one sub-word to the right +func (h *BufPane) SubWordRight() bool { + h.Cursor.Deselect(false) + h.Cursor.SubWordRight() + h.Relocate() + return true +} + +// SubWordLeft moves the cursor one sub-word to the left +func (h *BufPane) SubWordLeft() bool { + h.Cursor.Deselect(true) + h.Cursor.SubWordLeft() + h.Relocate() + return true +} + // SelectUp selects up one line func (h *BufPane) SelectUp() bool { if !h.Cursor.HasSelection() { @@ -359,6 +375,28 @@ func (h *BufPane) SelectWordLeft() bool { return true } +// SelectSubWordRight selects the sub-word to the right of the cursor +func (h *BufPane) SelectSubWordRight() bool { + if !h.Cursor.HasSelection() { + h.Cursor.OrigSelection[0] = h.Cursor.Loc + } + h.Cursor.SubWordRight() + h.Cursor.SelectTo(h.Cursor.Loc) + h.Relocate() + return true +} + +// SelectSubWordLeft selects the sub-word to the left of the cursor +func (h *BufPane) SelectSubWordLeft() bool { + if !h.Cursor.HasSelection() { + h.Cursor.OrigSelection[0] = h.Cursor.Loc + } + h.Cursor.SubWordLeft() + h.Cursor.SelectTo(h.Cursor.Loc) + h.Relocate() + return true +} + // StartOfText moves the cursor to the start of the text of the line func (h *BufPane) StartOfText() bool { h.Cursor.Deselect(true) @@ -622,6 +660,28 @@ func (h *BufPane) DeleteWordLeft() bool { return true } +// DeleteSubWordRight deletes the sub-word to the right of the cursor +func (h *BufPane) DeleteSubWordRight() bool { + h.SelectSubWordRight() + if h.Cursor.HasSelection() { + h.Cursor.DeleteSelection() + h.Cursor.ResetSelection() + } + h.Relocate() + return true +} + +// DeleteSubWordLeft deletes the sub-word to the left of the cursor +func (h *BufPane) DeleteSubWordLeft() bool { + h.SelectSubWordLeft() + if h.Cursor.HasSelection() { + h.Cursor.DeleteSelection() + h.Cursor.ResetSelection() + } + h.Relocate() + return true +} + // Delete deletes the next character func (h *BufPane) Delete() bool { if h.Cursor.HasSelection() { diff --git a/internal/action/bufpane.go b/internal/action/bufpane.go index ff83360cc..7b348b79b 100644 --- a/internal/action/bufpane.go +++ b/internal/action/bufpane.go @@ -746,10 +746,16 @@ var BufKeyActions = map[string]BufKeyAction{ "SelectRight": (*BufPane).SelectRight, "WordRight": (*BufPane).WordRight, "WordLeft": (*BufPane).WordLeft, + "SubWordRight": (*BufPane).SubWordRight, + "SubWordLeft": (*BufPane).SubWordLeft, "SelectWordRight": (*BufPane).SelectWordRight, "SelectWordLeft": (*BufPane).SelectWordLeft, + "SelectSubWordRight": (*BufPane).SelectSubWordRight, + "SelectSubWordLeft": (*BufPane).SelectSubWordLeft, "DeleteWordRight": (*BufPane).DeleteWordRight, "DeleteWordLeft": (*BufPane).DeleteWordLeft, + "DeleteSubWordRight": (*BufPane).DeleteSubWordRight, + "DeleteSubWordLeft": (*BufPane).DeleteSubWordLeft, "SelectLine": (*BufPane).SelectLine, "SelectToStartOfLine": (*BufPane).SelectToStartOfLine, "SelectToStartOfText": (*BufPane).SelectToStartOfText, @@ -876,10 +882,16 @@ var MultiActions = map[string]bool{ "SelectRight": true, "WordRight": true, "WordLeft": true, + "SubWordRight": true, + "SubWordLeft": true, "SelectWordRight": true, "SelectWordLeft": true, + "SelectSubWordRight": true, + "SelectSubWordLeft": true, "DeleteWordRight": true, "DeleteWordLeft": true, + "DeleteSubWordRight": true, + "DeleteSubWordLeft": true, "SelectLine": true, "SelectToStartOfLine": true, "SelectToStartOfText": true, diff --git a/internal/buffer/cursor.go b/internal/buffer/cursor.go index bd3ae0687..29ffa8465 100644 --- a/internal/buffer/cursor.go +++ b/internal/buffer/cursor.go @@ -438,6 +438,127 @@ func (c *Cursor) WordLeft() { c.Right() } +// SubWordRight moves the cursor one sub-word to the right +func (c *Cursor) SubWordRight() { + if util.IsWhitespace(c.RuneUnder(c.X)) { + for util.IsWhitespace(c.RuneUnder(c.X)) { + if c.X == util.CharacterCount(c.buf.LineBytes(c.Y)) { + c.Right() + 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)) { + c.Right() + 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)) { + c.Right() + 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() { + 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() + } + } +} + // RuneUnder returns the rune under the given x position func (c *Cursor) RuneUnder(x int) rune { line := c.buf.LineBytes(c.Y) diff --git a/internal/util/util.go b/internal/util/util.go index b7bebcaf2..1cd5d46cc 100644 --- a/internal/util/util.go +++ b/internal/util/util.go @@ -219,18 +219,69 @@ func FSize(f *os.File) int64 { } // IsWordChar returns whether or not a rune is a 'word character' -// Word characters are defined as numbers, letters or '_' +// Word characters are defined as numbers, letters or sub-word delimiters func IsWordChar(r rune) bool { - return unicode.IsLetter(r) || unicode.IsNumber(r) || r == '_' + return IsAlphanumeric(r) || IsSubwordDelimiter(r) } // IsNonWordChar returns whether or not a rune is not a 'word character' -// Non word characters are defined as all characters not being numbers, letters or '_' +// Non word characters are defined as all characters not being numbers, letters or sub-word delimiters // See IsWordChar() func IsNonWordChar(r rune) bool { return !IsWordChar(r) } +// IsUpperWordChar returns whether or not a rune is an 'upper word character' +// Upper word characters are defined as numbers, upper-case letters or sub-word delimiters +func IsUpperWordChar(r rune) bool { + return IsUpperAlphanumeric(r) || IsSubwordDelimiter(r) +} + +// IsLowerWordChar returns whether or not a rune is a 'lower word character' +// Lower word characters are defined as numbers, lower-case letters or sub-word delimiters +func IsLowerWordChar(r rune) bool { + return IsLowerAlphanumeric(r) || IsSubwordDelimiter(r) +} + +// IsSubwordDelimiter returns whether or not a rune is a 'sub-word delimiter character' +// i.e. is considered a part of the word and is used as a delimiter between sub-words of the word. +// For now the only sub-word delimiter character is '_'. +func IsSubwordDelimiter(r rune) bool { + return r == '_' +} + +// IsAlphanumeric returns whether or not a rune is an 'alphanumeric character' +// Alphanumeric characters are defined as numbers or letters +func IsAlphanumeric(r rune) bool { + return unicode.IsLetter(r) || unicode.IsNumber(r) +} + +// IsUpperAlphanumeric returns whether or not a rune is an 'upper alphanumeric character' +// Upper alphanumeric characters are defined as numbers or upper-case letters +func IsUpperAlphanumeric(r rune) bool { + return IsUpperLetter(r) || unicode.IsNumber(r) +} + +// IsLowerAlphanumeric returns whether or not a rune is a 'lower alphanumeric character' +// Lower alphanumeric characters are defined as numbers or lower-case letters +func IsLowerAlphanumeric(r rune) bool { + return IsLowerLetter(r) || unicode.IsNumber(r) +} + +// IsUpperLetter returns whether or not a rune is an 'upper letter character' +// Upper letter characters are defined as upper-case letters +func IsUpperLetter(r rune) bool { + // unicode.IsUpper() returns true for letters only + return unicode.IsUpper(r) +} + +// IsLowerLetter returns whether or not a rune is a 'lower letter character' +// Lower letter characters are defined as lower-case letters +func IsLowerLetter(r rune) bool { + // unicode.IsLower() returns true for letters only + return unicode.IsLower(r) +} + // Spaces returns a string with n spaces func Spaces(n int) string { return strings.Repeat(" ", n) diff --git a/runtime/help/keybindings.md b/runtime/help/keybindings.md index e0c2dd9b8..17f9ab353 100644 --- a/runtime/help/keybindings.md +++ b/runtime/help/keybindings.md @@ -178,12 +178,18 @@ SelectToStartOfText SelectToStartOfTextToggle WordRight WordLeft +SubWordRight +SubWordLeft SelectWordRight SelectWordLeft +SelectSubWordRight +SelectSubWordLeft MoveLinesUp MoveLinesDown DeleteWordRight DeleteWordLeft +DeleteSubWordRight +DeleteSubWordLeft SelectLine SelectToStartOfLine SelectToEndOfLine From 78fcf2fc31347d7f33181f03a32210e5d6988378 Mon Sep 17 00:00:00 2001 From: Massimo Mund Date: Tue, 14 May 2024 09:01:44 +0200 Subject: [PATCH 3/3] Updated WordLeft() and WordRight() behavior to be in line with SubWordLeft() and SubWordRight() --- internal/buffer/cursor.go | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/internal/buffer/cursor.go b/internal/buffer/cursor.go index 29ffa8465..33d67f89d 100644 --- a/internal/buffer/cursor.go +++ b/internal/buffer/cursor.go @@ -410,6 +410,17 @@ func (c *Cursor) WordRight() { } c.Right() } + 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)) { + c.Right() + return + } + c.Right() + } + return + } c.Right() for util.IsWordChar(c.RuneUnder(c.X)) { if c.X == util.CharacterCount(c.buf.LineBytes(c.Y)) { @@ -428,6 +439,17 @@ func (c *Cursor) WordLeft() { } c.Left() } + 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 + } c.Left() for util.IsWordChar(c.RuneUnder(c.X)) { if c.X == 0 {