Merge pull request #2665 from masmu/feature/sub-words

Implemented sub-word cursor movement
This commit is contained in:
Jöran Karl
2024-05-22 06:24:19 +02:00
committed by GitHub
6 changed files with 289 additions and 15 deletions

View File

@@ -283,6 +283,22 @@ func (h *BufPane) WordLeft() bool {
return true 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 // SelectUp selects up one line
func (h *BufPane) SelectUp() bool { func (h *BufPane) SelectUp() bool {
if !h.Cursor.HasSelection() { if !h.Cursor.HasSelection() {
@@ -359,6 +375,28 @@ func (h *BufPane) SelectWordLeft() bool {
return true 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 // StartOfText moves the cursor to the start of the text of the line
func (h *BufPane) StartOfText() bool { func (h *BufPane) StartOfText() bool {
h.Cursor.Deselect(true) h.Cursor.Deselect(true)
@@ -622,6 +660,28 @@ func (h *BufPane) DeleteWordLeft() bool {
return true 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 // Delete deletes the next character
func (h *BufPane) Delete() bool { func (h *BufPane) Delete() bool {
if h.Cursor.HasSelection() { if h.Cursor.HasSelection() {
@@ -745,8 +805,8 @@ func (h *BufPane) Autocomplete() bool {
} }
r := h.Cursor.RuneUnder(h.Cursor.X) r := h.Cursor.RuneUnder(h.Cursor.X)
prev := h.Cursor.RuneUnder(h.Cursor.X - 1) prev := h.Cursor.RuneUnder(h.Cursor.X - 1)
if !util.IsAutocomplete(prev) || !util.IsNonAlphaNumeric(r) { if !util.IsAutocomplete(prev) || util.IsWordChar(r) {
// don't autocomplete if cursor is on alpha numeric character (middle of a word) // don't autocomplete if cursor is within a word
return false return false
} }

View File

@@ -746,10 +746,16 @@ var BufKeyActions = map[string]BufKeyAction{
"SelectRight": (*BufPane).SelectRight, "SelectRight": (*BufPane).SelectRight,
"WordRight": (*BufPane).WordRight, "WordRight": (*BufPane).WordRight,
"WordLeft": (*BufPane).WordLeft, "WordLeft": (*BufPane).WordLeft,
"SubWordRight": (*BufPane).SubWordRight,
"SubWordLeft": (*BufPane).SubWordLeft,
"SelectWordRight": (*BufPane).SelectWordRight, "SelectWordRight": (*BufPane).SelectWordRight,
"SelectWordLeft": (*BufPane).SelectWordLeft, "SelectWordLeft": (*BufPane).SelectWordLeft,
"SelectSubWordRight": (*BufPane).SelectSubWordRight,
"SelectSubWordLeft": (*BufPane).SelectSubWordLeft,
"DeleteWordRight": (*BufPane).DeleteWordRight, "DeleteWordRight": (*BufPane).DeleteWordRight,
"DeleteWordLeft": (*BufPane).DeleteWordLeft, "DeleteWordLeft": (*BufPane).DeleteWordLeft,
"DeleteSubWordRight": (*BufPane).DeleteSubWordRight,
"DeleteSubWordLeft": (*BufPane).DeleteSubWordLeft,
"SelectLine": (*BufPane).SelectLine, "SelectLine": (*BufPane).SelectLine,
"SelectToStartOfLine": (*BufPane).SelectToStartOfLine, "SelectToStartOfLine": (*BufPane).SelectToStartOfLine,
"SelectToStartOfText": (*BufPane).SelectToStartOfText, "SelectToStartOfText": (*BufPane).SelectToStartOfText,
@@ -876,10 +882,16 @@ var MultiActions = map[string]bool{
"SelectRight": true, "SelectRight": true,
"WordRight": true, "WordRight": true,
"WordLeft": true, "WordLeft": true,
"SubWordRight": true,
"SubWordLeft": true,
"SelectWordRight": true, "SelectWordRight": true,
"SelectWordLeft": true, "SelectWordLeft": true,
"SelectSubWordRight": true,
"SelectSubWordLeft": true,
"DeleteWordRight": true, "DeleteWordRight": true,
"DeleteWordLeft": true, "DeleteWordLeft": true,
"DeleteSubWordRight": true,
"DeleteSubWordLeft": true,
"SelectLine": true, "SelectLine": true,
"SelectToStartOfLine": true, "SelectToStartOfLine": true,
"SelectToStartOfText": true, "SelectToStartOfText": true,

View File

@@ -73,11 +73,11 @@ func (b *Buffer) GetWord() ([]byte, int) {
return []byte{}, -1 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 return []byte{}, c.X
} }
args := bytes.FieldsFunc(l, util.IsNonAlphaNumeric) args := bytes.FieldsFunc(l, util.IsNonWordChar)
input := args[len(args)-1] input := args[len(args)-1]
return input, c.X - util.CharacterCount(input) return input, c.X - util.CharacterCount(input)
} }
@@ -166,7 +166,7 @@ func BufferComplete(b *Buffer) ([]string, []string) {
var suggestions []string var suggestions []string
for i := c.Y; i >= 0; i-- { for i := c.Y; i >= 0; i-- {
l := b.LineBytes(i) l := b.LineBytes(i)
words := bytes.FieldsFunc(l, util.IsNonAlphaNumeric) words := bytes.FieldsFunc(l, util.IsNonWordChar)
for _, w := range words { for _, w := range words {
if bytes.HasPrefix(w, input) && util.CharacterCount(w) > inputLen { if bytes.HasPrefix(w, input) && util.CharacterCount(w) > inputLen {
strw := string(w) strw := string(w)
@@ -179,7 +179,7 @@ func BufferComplete(b *Buffer) ([]string, []string) {
} }
for i := c.Y + 1; i < b.LinesNum(); i++ { for i := c.Y + 1; i < b.LinesNum(); i++ {
l := b.LineBytes(i) l := b.LineBytes(i)
words := bytes.FieldsFunc(l, util.IsNonAlphaNumeric) words := bytes.FieldsFunc(l, util.IsNonWordChar)
for _, w := range words { for _, w := range words {
if bytes.HasPrefix(w, input) && util.CharacterCount(w) > inputLen { if bytes.HasPrefix(w, input) && util.CharacterCount(w) > inputLen {
strw := string(w) strw := string(w)

View File

@@ -410,6 +410,17 @@ func (c *Cursor) WordRight() {
} }
c.Right() 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() c.Right()
for util.IsWordChar(c.RuneUnder(c.X)) { for util.IsWordChar(c.RuneUnder(c.X)) {
if c.X == util.CharacterCount(c.buf.LineBytes(c.Y)) { if c.X == util.CharacterCount(c.buf.LineBytes(c.Y)) {
@@ -428,6 +439,17 @@ func (c *Cursor) WordLeft() {
} }
c.Left() 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() c.Left()
for util.IsWordChar(c.RuneUnder(c.X)) { for util.IsWordChar(c.RuneUnder(c.X)) {
if c.X == 0 { if c.X == 0 {
@@ -438,6 +460,127 @@ func (c *Cursor) WordLeft() {
c.Right() 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 // RuneUnder returns the rune under the given x position
func (c *Cursor) RuneUnder(x int) rune { func (c *Cursor) RuneUnder(x int) rune {
line := c.buf.LineBytes(c.Y) line := c.buf.LineBytes(c.Y)

View File

@@ -218,10 +218,68 @@ func FSize(f *os.File) int64 {
return fi.Size() return fi.Size()
} }
// IsWordChar returns whether or not the string is a 'word character' // 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 { 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 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 // Spaces returns a string with n spaces
@@ -445,14 +503,9 @@ func Clamp(val, min, max int) int {
return val 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. // IsAutocomplete returns whether a character should begin an autocompletion.
func IsAutocomplete(c rune) bool { func IsAutocomplete(c rune) bool {
return c == '.' || !IsNonAlphaNumeric(c) return c == '.' || IsWordChar(c)
} }
// ParseSpecial replaces escaped ts with '\t'. // ParseSpecial replaces escaped ts with '\t'.

View File

@@ -178,12 +178,18 @@ SelectToStartOfText
SelectToStartOfTextToggle SelectToStartOfTextToggle
WordRight WordRight
WordLeft WordLeft
SubWordRight
SubWordLeft
SelectWordRight SelectWordRight
SelectWordLeft SelectWordLeft
SelectSubWordRight
SelectSubWordLeft
MoveLinesUp MoveLinesUp
MoveLinesDown MoveLinesDown
DeleteWordRight DeleteWordRight
DeleteWordLeft DeleteWordLeft
DeleteSubWordRight
DeleteSubWordLeft
SelectLine SelectLine
SelectToStartOfLine SelectToStartOfLine
SelectToEndOfLine SelectToEndOfLine