Skip to content

Commit 9ef365b

Browse files
committed
[feat] bring in forked textinput with IsEmpty optimization
1 parent 64fe577 commit 9ef365b

File tree

9 files changed

+998
-12
lines changed

9 files changed

+998
-12
lines changed

internal/filter/filter.go

+10-5
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,12 @@ package filter
33
import (
44
"fmt"
55
"github.com/charmbracelet/bubbles/v2/cursor"
6-
"github.com/charmbracelet/bubbles/v2/textinput"
76
tea "github.com/charmbracelet/bubbletea/v2"
87
"github.com/charmbracelet/lipgloss/v2"
98
"github.com/robinovitch61/kl/internal/dev"
109
"github.com/robinovitch61/kl/internal/keymap"
1110
"github.com/robinovitch61/kl/internal/style"
11+
"github.com/robinovitch61/kl/internal/textinput"
1212
"regexp"
1313
"strings"
1414
)
@@ -68,7 +68,7 @@ func (m Model) View() string {
6868
m.textinput.TextStyle = m.styles.Inverse
6969
m.textinput.Cursor.Style = lipgloss.NewStyle()
7070
m.textinput.Cursor.TextStyle = lipgloss.NewStyle()
71-
if len(m.textinput.Value()) > 0 {
71+
if !m.textinput.IsEmpty() {
7272
// editing existing filter
7373
if m.isRegex {
7474
m.textinput.Prompt = "regex filter: "
@@ -90,7 +90,7 @@ func (m Model) View() string {
9090
}
9191
}
9292
} else {
93-
if len(m.textinput.Value()) > 0 {
93+
if !m.textinput.IsEmpty() {
9494
// filter applied, not editing
9595
if m.isRegex {
9696
m.textinput.Prompt = "regex filter: "
@@ -119,13 +119,14 @@ func (m Model) View() string {
119119
}
120120

121121
func (m Model) Matches(s string) bool {
122-
if m.Value() == "" {
122+
if m.IsEmpty() {
123123
return true
124124
}
125125
// if invalid regexp, fallback to string matching
126126
if m.isRegex && m.regexp != nil {
127127
return m.regexp.MatchString(s)
128128
} else {
129+
// TODO LEO: reduce number of calls to .Value() here
129130
return strings.Contains(s, m.Value())
130131
}
131132
}
@@ -134,6 +135,10 @@ func (m Model) Value() string {
134135
return m.textinput.Value()
135136
}
136137

138+
func (m Model) IsEmpty() bool {
139+
return m.textinput.IsEmpty()
140+
}
141+
137142
func (m Model) GetContextualMatchIdx() int {
138143
if !m.ShowContext {
139144
return 0
@@ -149,7 +154,7 @@ func (m Model) HasContextualMatches() bool {
149154
}
150155

151156
func (m Model) HasFilterText() bool {
152-
return m.Value() != ""
157+
return !m.IsEmpty()
153158
}
154159

155160
func (m Model) Focused() bool {

internal/filterable_viewport/filterable_viewport.go

+3-3
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,13 @@ package filterable_viewport
22

33
import (
44
"github.com/charmbracelet/bubbles/v2/key"
5-
"github.com/charmbracelet/bubbles/v2/textinput"
65
tea "github.com/charmbracelet/bubbletea/v2"
76
"github.com/charmbracelet/lipgloss/v2"
87
"github.com/robinovitch61/kl/internal/dev"
98
"github.com/robinovitch61/kl/internal/filter"
109
"github.com/robinovitch61/kl/internal/keymap"
1110
"github.com/robinovitch61/kl/internal/style"
11+
"github.com/robinovitch61/kl/internal/textinput"
1212
"github.com/robinovitch61/kl/internal/viewport"
1313
"strings"
1414
)
@@ -271,7 +271,7 @@ func (fv *FilterableViewport[T]) updateVisibleRows() {
271271
dev.Debug("Updating visible rows")
272272
defer dev.Debug("Done updating visible rows")
273273

274-
if fv.Filter.ShowContext && fv.Filter.Value() != "" {
274+
if fv.Filter.ShowContext && !fv.Filter.IsEmpty() {
275275
var entityIndexesMatchingFilter []int
276276
for i := range fv.allRows {
277277
if fv.matchesFilter(fv.allRows[i], fv.Filter) {
@@ -280,7 +280,7 @@ func (fv *FilterableViewport[T]) updateVisibleRows() {
280280
}
281281
fv.Filter.SetIndexesMatchingFilter(entityIndexesMatchingFilter)
282282
fv.viewport.SetContent(fv.allRows)
283-
} else if fv.Filter.Value() != "" {
283+
} else if !fv.Filter.IsEmpty() {
284284
var filtered []T
285285
for i := range fv.allRows {
286286
if fv.matchesFilter(fv.allRows[i], fv.Filter) {

internal/filterable_viewport/filterable_viewport_test.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ func newFilterableViewport() FilterableViewport[TestItem] {
6060
)
6161

6262
matchesFilter := func(item TestItem, f filter.Model) bool {
63-
if f.Value() == "" {
63+
if f.IsEmpty() {
6464
return true
6565
}
6666
if f.IsRegex() {

internal/page/entities.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,7 @@ func (p EntityPage) ContentForFile() []string {
9999
}
100100

101101
func (p EntityPage) HasAppliedFilter() bool {
102-
return p.filterableViewport.Filter.Value() != ""
102+
return !p.filterableViewport.Filter.IsEmpty()
103103
}
104104

105105
func (p EntityPage) ToggleShowContext() GenericPage {

internal/page/log.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@ func (p SingleLogPage) ToggleShowContext() GenericPage {
8888
}
8989

9090
func (p SingleLogPage) HasAppliedFilter() bool {
91-
return p.filterableViewport.Filter.Value() != ""
91+
return !p.filterableViewport.Filter.IsEmpty()
9292
}
9393

9494
func (p SingleLogPage) ContentForClipboard() []string {

internal/page/logs.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,7 @@ func (p LogsPage) ContentForFile() []string {
109109
}
110110

111111
func (p LogsPage) HasAppliedFilter() bool {
112-
return p.filterableViewport.Filter.Value() != ""
112+
return !p.filterableViewport.Filter.IsEmpty()
113113
}
114114

115115
func (p LogsPage) ToggleShowContext() GenericPage {

internal/textinput/README.md

+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
This textinput is forked from https://github.com/charmbracelet/bubbles in order to add some utility functions, e.g.
2+
`IsEmpty`

internal/textinput/sanitizer.go

+100
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
package textinput
2+
3+
import (
4+
"unicode"
5+
"unicode/utf8"
6+
)
7+
8+
// Sanitizer is a helper for bubble widgets that want to process
9+
// Runes from input key messages.
10+
type Sanitizer interface {
11+
// Sanitize removes control characters from runes in a KeyRunes
12+
// message, and optionally replaces newline/carriage return/tabs by a
13+
// specified character.
14+
//
15+
// The rune array is modified in-place if possible. In that case, the
16+
// returned slice is the original slice shortened after the control
17+
// characters have been removed/translated.
18+
Sanitize(runes []rune) []rune
19+
}
20+
21+
// NewSanitizer constructs a rune sanitizer.
22+
func NewSanitizer(opts ...Option) Sanitizer {
23+
s := sanitizer{
24+
replaceNewLine: []rune("\n"),
25+
replaceTab: []rune(" "),
26+
}
27+
for _, o := range opts {
28+
s = o(s)
29+
}
30+
return &s
31+
}
32+
33+
// Option is the type of option that can be passed to Sanitize().
34+
type Option func(sanitizer) sanitizer
35+
36+
// ReplaceTabs replaces tabs by the specified string.
37+
func ReplaceTabs(tabRepl string) Option {
38+
return func(s sanitizer) sanitizer {
39+
s.replaceTab = []rune(tabRepl)
40+
return s
41+
}
42+
}
43+
44+
// ReplaceNewlines replaces newline characters by the specified string.
45+
func ReplaceNewlines(nlRepl string) Option {
46+
return func(s sanitizer) sanitizer {
47+
s.replaceNewLine = []rune(nlRepl)
48+
return s
49+
}
50+
}
51+
52+
func (s *sanitizer) Sanitize(runes []rune) []rune {
53+
// dstrunes are where we are storing the result.
54+
dstrunes := runes[:0:len(runes)]
55+
// copied indicates whether dstrunes is an alias of runes
56+
// or a copy. We need a copy when dst moves past src.
57+
// We use this as an optimization to avoid allocating
58+
// a new rune slice in the common case where the output
59+
// is smaller or equal to the input.
60+
copied := false
61+
62+
for src := 0; src < len(runes); src++ {
63+
r := runes[src]
64+
switch {
65+
case r == utf8.RuneError:
66+
// skip
67+
68+
case r == '\r' || r == '\n':
69+
if len(dstrunes)+len(s.replaceNewLine) > src && !copied {
70+
dst := len(dstrunes)
71+
dstrunes = make([]rune, dst, len(runes)+len(s.replaceNewLine))
72+
copy(dstrunes, runes[:dst])
73+
copied = true
74+
}
75+
dstrunes = append(dstrunes, s.replaceNewLine...)
76+
77+
case r == '\t':
78+
if len(dstrunes)+len(s.replaceTab) > src && !copied {
79+
dst := len(dstrunes)
80+
dstrunes = make([]rune, dst, len(runes)+len(s.replaceTab))
81+
copy(dstrunes, runes[:dst])
82+
copied = true
83+
}
84+
dstrunes = append(dstrunes, s.replaceTab...)
85+
86+
case unicode.IsControl(r):
87+
// Other control characters: skip.
88+
89+
default:
90+
// Keep the character.
91+
dstrunes = append(dstrunes, runes[src])
92+
}
93+
}
94+
return dstrunes
95+
}
96+
97+
type sanitizer struct {
98+
replaceNewLine []rune
99+
replaceTab []rune
100+
}

0 commit comments

Comments
 (0)