diff --git a/complete.go b/complete.go index 122779a..68bd88a 100644 --- a/complete.go +++ b/complete.go @@ -4,6 +4,8 @@ import ( "bytes" "fmt" "io" + + "github.com/chzyer/readline/runes" ) type AutoCompleter interface { @@ -65,7 +67,7 @@ func (o *opCompleter) OnComplete() { buf := o.op.buf rs := buf.Runes() - if o.IsInCompleteMode() && RunesEqual(rs, o.candidateSource) { + if o.IsInCompleteMode() && runes.Equal(rs, o.candidateSource) { o.EnterCompleteSelectMode() o.doSelect() return @@ -176,7 +178,7 @@ func (o *opCompleter) CompleteRefresh() { lineCnt := o.op.buf.CursorLineCount() colWidth := 0 for _, c := range o.candidate { - w := RunesWidth(c) + w := runes.WidthAll(c) if w > colWidth { colWidth = w } diff --git a/complete_helper.go b/complete_helper.go index baf7b90..b153bc5 100644 --- a/complete_helper.go +++ b/complete_helper.go @@ -1,5 +1,7 @@ package readline +import "github.com/chzyer/readline/runes" + type PrefixCompleter struct { Name []rune Children []*PrefixCompleter @@ -25,14 +27,14 @@ func (p *PrefixCompleter) Do(line []rune, pos int) (newLine [][]rune, offset int var lineCompleter *PrefixCompleter for _, child := range p.Children { if len(line) >= len(child.Name) { - if RunesHasPrefix(line, child.Name) { + if runes.HasPrefix(line, child.Name) { newLine = append(newLine, child.Name) offset = len(child.Name) lineCompleter = child goNext = true } } else { - if RunesHasPrefix(child.Name, line) { + if runes.HasPrefix(child.Name, line) { newLine = append(newLine, child.Name[len(line):]) offset = len(line) lineCompleter = child diff --git a/history.go b/history.go index 8923b6f..f94e208 100644 --- a/history.go +++ b/history.go @@ -5,6 +5,8 @@ import ( "container/list" "os" "strings" + + "github.com/chzyer/readline/runes" ) type hisItem struct { @@ -110,7 +112,7 @@ func (o *opHistory) FindHistoryBck(isNewSearch bool, rs []rune, start int) (int, item = item[:start] } } - idx := RunesIndexBck(item, rs) + idx := runes.IndexAllBck(item, rs) if idx < 0 { continue } @@ -135,7 +137,7 @@ func (o *opHistory) FindHistoryFwd(isNewSearch bool, rs []rune, start int) (int, continue } } - idx := RunesIndex(item, rs) + idx := runes.IndexAll(item, rs) if idx < 0 { continue } @@ -187,7 +189,7 @@ func (o *opHistory) NewHistory(current []rune) { prev := back.Prev() if prev != nil { use := o.showItem(o.current.Value.(*hisItem)) - if RunesEqual(use, prev.Value.(*hisItem).Source) { + if runes.Equal(use, prev.Value.(*hisItem).Source) { o.current = o.history.Back() o.current.Value.(*hisItem).Clean() o.historyVer++ diff --git a/runebuf.go b/runebuf.go index 4b0f4aa..69dc378 100644 --- a/runebuf.go +++ b/runebuf.go @@ -3,6 +3,8 @@ package readline import ( "bytes" "io" + + "github.com/chzyer/readline/runes" ) type runeBufferBck struct { @@ -44,11 +46,11 @@ func NewRuneBuffer(w io.Writer, prompt string) *RuneBuffer { } func (r *RuneBuffer) CurrentWidth(x int) int { - return RunesWidth(r.buf[:x]) + return runes.WidthAll(r.buf[:x]) } func (r *RuneBuffer) PromptLen() int { - return RunesWidth(RunesColorFilter(r.prompt)) + return runes.WidthAll(runes.ColorFilter(r.prompt)) } func (r *RuneBuffer) RuneSlice(i int) []rune { @@ -267,7 +269,7 @@ func (r *RuneBuffer) MoveToLineEnd() { } func (r *RuneBuffer) LineCount() int { - return LineCount(RunesWidth(r.buf) + r.PromptLen()) + return LineCount(runes.WidthAll(r.buf) + r.PromptLen()) } func (r *RuneBuffer) MoveTo(ch rune, prevChar, reverse bool) (success bool) { @@ -300,7 +302,7 @@ func (r *RuneBuffer) MoveTo(ch rune, prevChar, reverse bool) (success bool) { } func (r *RuneBuffer) IdxLine() int { - totalWidth := RunesWidth(r.buf[:r.idx]) + r.PromptLen() + totalWidth := runes.WidthAll(r.buf[:r.idx]) + r.PromptLen() w := getWidth() if w == 0 { return 0 @@ -336,7 +338,7 @@ func (r *RuneBuffer) output() []byte { buf.WriteString(string(r.prompt)) buf.Write([]byte(string(r.buf))) if len(r.buf) > r.idx { - buf.Write(bytes.Repeat([]byte{'\b'}, RunesWidth(r.buf[r.idx:]))) + buf.Write(runes.Backspace(r.buf[r.idx:])) } return buf.Bytes() } @@ -350,9 +352,9 @@ func (r *RuneBuffer) Reset() []rune { func (r *RuneBuffer) calWidth(m int) int { if m > 0 { - return RunesWidth(r.buf[r.idx : r.idx+m]) + return runes.WidthAll(r.buf[r.idx : r.idx+m]) } - return RunesWidth(r.buf[r.idx+m : r.idx]) + return runes.WidthAll(r.buf[r.idx+m : r.idx]) } func (r *RuneBuffer) SetStyle(start, end int, style string) { diff --git a/runes/runes.go b/runes/runes.go new file mode 100644 index 0000000..c5a7502 --- /dev/null +++ b/runes/runes.go @@ -0,0 +1,152 @@ +package runes + +import ( + "bytes" + "unicode" +) + +func Equal(a, b []rune) bool { + if len(a) != len(b) { + return false + } + for i := 0; i < len(a); i++ { + if a[i] != b[i] { + return false + } + } + return true +} + +// Search in runes from end to front +func IndexAllBck(r, sub []rune) int { + for i := len(r) - len(sub); i >= 0; i-- { + found := true + for j := 0; j < len(sub); j++ { + if r[i+j] != sub[j] { + found = false + break + } + } + if found { + return i + } + } + return -1 +} + +// Search in runes from front to end +func IndexAll(r, sub []rune) int { + for i := 0; i < len(r); i++ { + found := true + if len(r[i:]) < len(sub) { + return -1 + } + for j := 0; j < len(sub); j++ { + if r[i+j] != sub[j] { + found = false + break + } + } + if found { + return i + } + } + return -1 +} + +func Index(r rune, rs []rune) int { + for i := 0; i < len(rs); i++ { + if rs[i] == r { + return i + } + } + return -1 +} + +func ColorFilter(r []rune) []rune { + newr := make([]rune, 0, len(r)) + for pos := 0; pos < len(r); pos++ { + if r[pos] == '\033' && r[pos+1] == '[' { + idx := Index('m', r[pos+2:]) + if idx == -1 { + continue + } + pos += idx + 2 + continue + } + newr = append(newr, r[pos]) + } + return newr +} + +var zeroWidth = []*unicode.RangeTable{ + unicode.Mn, + unicode.Me, + unicode.Cc, + unicode.Cf, +} + +var doubleWidth = []*unicode.RangeTable{ + unicode.Han, + unicode.Hangul, + unicode.Hiragana, + unicode.Katakana, +} + +func Width(r rune) int { + if unicode.IsOneOf(zeroWidth, r) { + return 0 + } + if unicode.IsOneOf(doubleWidth, r) { + return 2 + } + return 1 +} + +func WidthAll(r []rune) (length int) { + for i := 0; i < len(r); i++ { + length += Width(r[i]) + } + return +} + +func Backspace(r []rune) []byte { + return bytes.Repeat([]byte{'\b'}, WidthAll(r)) +} + +func Copy(r []rune) []rune { + n := make([]rune, len(r)) + copy(n, r) + return n +} + +func HasPrefix(r, prefix []rune) bool { + if len(r) < len(prefix) { + return false + } + return Equal(r[:len(prefix)], prefix) +} + +func Aggregate(candicate [][]rune) (same []rune, size int) { + for i := 0; i < len(candicate[0]); i++ { + for j := 0; j < len(candicate)-1; j++ { + if i >= len(candicate[j]) || i >= len(candicate[j+1]) { + goto aggregate + } + if candicate[j][i] != candicate[j+1][i] { + goto aggregate + } + } + size = i + 1 + } +aggregate: + if size > 0 { + same = Copy(candicate[0][:size]) + for i := 0; i < len(candicate); i++ { + n := Copy(candicate[i]) + copy(n, n[size:]) + candicate[i] = n[:len(n)-size] + } + } + return +} diff --git a/runes/runes_test.go b/runes/runes_test.go new file mode 100644 index 0000000..b7a1412 --- /dev/null +++ b/runes/runes_test.go @@ -0,0 +1,68 @@ +package runes + +import ( + "reflect" + "testing" +) + +type twidth struct { + r []rune + length int +} + +func TestRuneWidth(t *testing.T) { + runes := []twidth{ + {[]rune("☭"), 1}, + {[]rune("a"), 1}, + {[]rune("你"), 2}, + {ColorFilter([]rune("☭\033[13;1m你")), 3}, + } + for _, r := range runes { + if w := WidthAll(r.r); w != r.length { + t.Fatal("result not expect", r.r, r.length, w) + } + } +} + +type tagg struct { + r [][]rune + e [][]rune + length int +} + +func TestAggRunes(t *testing.T) { + runes := []tagg{ + { + [][]rune{[]rune("ab"), []rune("a"), []rune("abc")}, + [][]rune{[]rune("b"), []rune(""), []rune("bc")}, + 1, + }, + { + [][]rune{[]rune("addb"), []rune("ajkajsdf"), []rune("aasdfkc")}, + [][]rune{[]rune("ddb"), []rune("jkajsdf"), []rune("asdfkc")}, + 1, + }, + { + [][]rune{[]rune("ddb"), []rune("ajksdf"), []rune("aasdfkc")}, + [][]rune{[]rune("ddb"), []rune("ajksdf"), []rune("aasdfkc")}, + 0, + }, + { + [][]rune{[]rune("ddb"), []rune("ddajksdf"), []rune("ddaasdfkc")}, + [][]rune{[]rune("b"), []rune("ajksdf"), []rune("aasdfkc")}, + 2, + }, + } + for _, r := range runes { + same, off := Aggregate(r.r) + if off != r.length { + t.Fatal("result not expect", off) + } + if len(same) != off { + t.Fatal("result not expect", same) + } + if !reflect.DeepEqual(r.r, r.e) { + t.Fatal("result not expect") + } + } +} diff --git a/utils.go b/utils.go index 6e83ea2..5f09bfd 100644 --- a/utils.go +++ b/utils.go @@ -3,7 +3,6 @@ package readline import ( "strconv" "syscall" - "unicode" "golang.org/x/crypto/ssh/terminal" ) @@ -72,18 +71,6 @@ func escapeKey(r rune) rune { return r } -func RunesEqual(a, b []rune) bool { - if len(a) != len(b) { - return false - } - for i := 0; i < len(a); i++ { - if a[i] != b[i] { - return false - } - } - return true -} - // calculate how many lines for N character func LineCount(w int) int { screenWidth := getWidth() @@ -94,43 +81,6 @@ func LineCount(w int) int { return r } -// Search in runes from end to front -func RunesIndexBck(r, sub []rune) int { - for i := len(r) - len(sub); i >= 0; i-- { - found := true - for j := 0; j < len(sub); j++ { - if r[i+j] != sub[j] { - found = false - break - } - } - if found { - return i - } - } - return -1 -} - -// Search in runes from front to end -func RunesIndex(r, sub []rune) int { - for i := 0; i < len(r); i++ { - found := true - if len(r[i:]) < len(sub) { - return -1 - } - for j := 0; j < len(sub); j++ { - if r[i+j] != sub[j] { - found = false - break - } - } - if found { - return i - } - } - return -1 -} - func IsWordBreak(i rune) bool { if i >= 'a' && i <= 'z' { return false @@ -141,99 +91,6 @@ func IsWordBreak(i rune) bool { return true } -var zeroWidth = []*unicode.RangeTable{ - unicode.Mn, - unicode.Me, - unicode.Cc, - unicode.Cf, -} - -var doubleWidth = []*unicode.RangeTable{ - unicode.Han, - unicode.Hangul, - unicode.Hiragana, - unicode.Katakana, -} - -func RuneIndex(r rune, rs []rune) int { - for i := 0; i < len(rs); i++ { - if rs[i] == r { - return i - } - } - return -1 -} - -func RunesColorFilter(r []rune) []rune { - newr := make([]rune, 0, len(r)) - for pos := 0; pos < len(r); pos++ { - if r[pos] == '\033' && r[pos+1] == '[' { - idx := RuneIndex('m', r[pos+2:]) - if idx == -1 { - continue - } - pos += idx + 2 - continue - } - newr = append(newr, r[pos]) - } - return newr -} - -func RuneWidth(r rune) int { - if unicode.IsOneOf(zeroWidth, r) { - return 0 - } - if unicode.IsOneOf(doubleWidth, r) { - return 2 - } - return 1 -} - -func RunesWidth(r []rune) (length int) { - for i := 0; i < len(r); i++ { - length += RuneWidth(r[i]) - } - return -} - -func RunesAggregate(candicate [][]rune) (same []rune, size int) { - for i := 0; i < len(candicate[0]); i++ { - for j := 0; j < len(candicate)-1; j++ { - if i >= len(candicate[j]) || i >= len(candicate[j+1]) { - goto aggregate - } - if candicate[j][i] != candicate[j+1][i] { - goto aggregate - } - } - size = i + 1 - } -aggregate: - if size > 0 { - same = RunesCopy(candicate[0][:size]) - for i := 0; i < len(candicate); i++ { - n := RunesCopy(candicate[i]) - copy(n, n[size:]) - candicate[i] = n[:len(n)-size] - } - } - return -} - -func RunesCopy(r []rune) []rune { - n := make([]rune, len(r)) - copy(n, r) - return n -} - -func RunesHasPrefix(r, prefix []rune) bool { - if len(r) < len(prefix) { - return false - } - return RunesEqual(r[:len(prefix)], prefix) -} - func GetInt(s []string, def int) int { if len(s) == 0 { return def diff --git a/utils_test.go b/utils_test.go index 0d8e720..96037df 100644 --- a/utils_test.go +++ b/utils_test.go @@ -1,68 +1 @@ package readline - -import ( - "reflect" - "testing" -) - -type twidth struct { - r []rune - length int -} - -func TestRuneWidth(t *testing.T) { - runes := []twidth{ - {[]rune("☭"), 1}, - {[]rune("a"), 1}, - {[]rune("你"), 2}, - {RunesColorFilter([]rune("☭\033[13;1m你")), 3}, - } - for _, r := range runes { - if w := RunesWidth(r.r); w != r.length { - t.Fatal("result not expect", r.r, r.length, w) - } - } -} - -type tagg struct { - r [][]rune - e [][]rune - length int -} - -func TestAggRunes(t *testing.T) { - runes := []tagg{ - { - [][]rune{[]rune("ab"), []rune("a"), []rune("abc")}, - [][]rune{[]rune("b"), []rune(""), []rune("bc")}, - 1, - }, - { - [][]rune{[]rune("addb"), []rune("ajkajsdf"), []rune("aasdfkc")}, - [][]rune{[]rune("ddb"), []rune("jkajsdf"), []rune("asdfkc")}, - 1, - }, - { - [][]rune{[]rune("ddb"), []rune("ajksdf"), []rune("aasdfkc")}, - [][]rune{[]rune("ddb"), []rune("ajksdf"), []rune("aasdfkc")}, - 0, - }, - { - [][]rune{[]rune("ddb"), []rune("ddajksdf"), []rune("ddaasdfkc")}, - [][]rune{[]rune("b"), []rune("ajksdf"), []rune("aasdfkc")}, - 2, - }, - } - for _, r := range runes { - same, off := RunesAggregate(r.r) - if off != r.length { - t.Fatal("result not expect", off) - } - if len(same) != off { - t.Fatal("result not expect", same) - } - if !reflect.DeepEqual(r.r, r.e) { - t.Fatal("result not expect") - } - } -}