Skip to content

Commit

Permalink
feat(textinput): store and recover history
Browse files Browse the repository at this point in the history
This adds the history functionality to the textinput model. Specific
actions can push a new value into the history and they can be switched
back to via key presses.

This is done in relation to nobe4/gh-not#190
I was considering implementing that in my project, and realized that it
could just be part of bubbles instead.

Looking forward to getting some feedback on this. Especially the
comments below.
  • Loading branch information
nobe4 committed Oct 3, 2024
1 parent 4382fdf commit 1b2dbdd
Showing 1 changed file with 75 additions and 2 deletions.
77 changes: 75 additions & 2 deletions textinput/textinput.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,8 @@ type KeyMap struct {
AcceptSuggestion key.Binding
NextSuggestion key.Binding
PrevSuggestion key.Binding
NextHistory key.Binding
PrevHistory key.Binding
}

// DefaultKeyMap is the default set of key bindings for navigating and acting
Expand All @@ -78,8 +80,11 @@ var DefaultKeyMap = KeyMap{
LineEnd: key.NewBinding(key.WithKeys("end", "ctrl+e")),
Paste: key.NewBinding(key.WithKeys("ctrl+v")),
AcceptSuggestion: key.NewBinding(key.WithKeys("tab")),
NextSuggestion: key.NewBinding(key.WithKeys("down", "ctrl+n")),
PrevSuggestion: key.NewBinding(key.WithKeys("up", "ctrl+p")),
NextSuggestion: key.NewBinding(key.WithKeys("ctrl+n")),
PrevSuggestion: key.NewBinding(key.WithKeys("ctrl+p")),
// TODO: discuss moving `down` and `up` to this instead of Next/PrevSuggestion
NextHistory: key.NewBinding(key.WithKeys("down")),
PrevHistory: key.NewBinding(key.WithKeys("up")),
}

// Model is the Bubble Tea model for this text input element.
Expand Down Expand Up @@ -152,6 +157,10 @@ type Model struct {
suggestions [][]rune
matchedSuggestions [][]rune
currentSuggestionIndex int

// history of inputs
history [][]rune
historyIndex int
}

// New creates a new model with default settings.
Expand All @@ -170,6 +179,9 @@ func New() Model {
value: nil,
focus: false,
pos: 0,

history: [][]rune{},
historyIndex: 0,
}
}

Expand All @@ -184,6 +196,7 @@ func (m *Model) SetValue(s string) {
// caller. This avoids bugs due to e.g. tab characters and whatnot.
runes := m.san().Sanitize([]rune(s))
err := m.validate(runes)
m.SaveHistory()
m.setValueInternal(runes, err)
}

Expand Down Expand Up @@ -245,12 +258,14 @@ func (m *Model) Focus() tea.Cmd {
// Blur removes the focus state on the model. When the model is blurred it can
// not receive keyboard input and the cursor will be hidden.
func (m *Model) Blur() {
m.SaveHistory()
m.focus = false
m.Cursor.Blur()
}

// Reset sets the input to its default state with no input.
func (m *Model) Reset() {
m.SaveHistory()
m.value = nil
m.SetCursor(0)
}
Expand Down Expand Up @@ -614,6 +629,10 @@ func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) {
m.nextSuggestion()
case key.Matches(msg, m.KeyMap.PrevSuggestion):
m.previousSuggestion()
case key.Matches(msg, m.KeyMap.NextHistory):
m.nextHistory()
case key.Matches(msg, m.KeyMap.PrevHistory):
m.prevHistory()
default:
// Input one or more regular characters.
m.insertRunesFromUserInput(msg.Runes)
Expand All @@ -624,6 +643,7 @@ func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) {
m.updateSuggestions()

case pasteMsg:
m.SaveHistory()
m.insertRunesFromUserInput([]rune(msg))

case pasteErrMsg:
Expand Down Expand Up @@ -899,3 +919,56 @@ func (m Model) validate(v []rune) error {
}
return nil
}

func (m *Model) SaveHistory() {

Check failure on line 923 in textinput/textinput.go

View workflow job for this annotation

GitHub Actions / lint

exported: exported method Model.SaveHistory should have comment or be unexported (revive)
if len(m.value) == 0 {
return
}

if m.EchoMode != EchoNormal {
return
}

// XXX: It seems that doing `append(m.history, m.value)` copies a reference
// to the value, which leads to all history items being similar.
// This is quite surprising given this other test:
// https://go.dev/play/p/MZGpcUzSVch
newRecord := make([]rune, len(m.value))
for i, r := range m.value {

Check failure on line 937 in textinput/textinput.go

View workflow job for this annotation

GitHub Actions / lint

S1001: should use copy(to, from) instead of a loop (gosimple)
newRecord[i] = r
}

m.history = append(m.history, newRecord)
m.historyIndex = len(m.history) - 1
}

func (m Model) History() []string {

Check failure on line 945 in textinput/textinput.go

View workflow job for this annotation

GitHub Actions / lint

exported: exported method Model.History should have comment or be unexported (revive)
// TODO: rename getSuggestions to getStrings
return m.getSuggestions(m.history)
}

func (m *Model) nextHistory() {
if len(m.history) == 0 {
return
}

m.historyIndex = (m.historyIndex + 1)
if m.historyIndex > len(m.history)-1 {
m.historyIndex = 0
}

m.setValueInternal(m.history[m.historyIndex], nil)
}

func (m *Model) prevHistory() {
if len(m.history) == 0 {
return
}

m.historyIndex = (m.historyIndex - 1)
if m.historyIndex < 0 {
m.historyIndex = len(m.history) - 1
}

m.setValueInternal(m.history[m.historyIndex], nil)
}

0 comments on commit 1b2dbdd

Please sign in to comment.