From e7b3760d883dcba31b04fc078bdac94e56efd316 Mon Sep 17 00:00:00 2001 From: Gabriel Nagy Date: Thu, 29 Feb 2024 12:43:34 +0200 Subject: [PATCH] feat(textinput): do not block input on validation This PR builds upon the excellent work in #167 and #114 and makes a breaking change to the validation API. Currently, validation will completely block text input if the Validate function returns an error. This is now changed so the function no longer blocks input if this is the case, thus handing this responsibility to the clients. This is helpful for cases where the user is requested to type an existing system path, and the Validate function keeps asserting the existence of the path. With the current implementation such a validation is not possible. For example: > / Err: nil > /t Err: /t: No such file or directory > /tm Err: /tm: No such file or directory > /tmp Err: nil --- textinput/textinput.go | 38 +++++++++++++++++++------------------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/textinput/textinput.go b/textinput/textinput.go index 9d612936..816a648f 100644 --- a/textinput/textinput.go +++ b/textinput/textinput.go @@ -181,19 +181,14 @@ func (m *Model) SetValue(s string) { // Clean up any special characters in the input provided by the // caller. This avoids bugs due to e.g. tab characters and whatnot. runes := m.san().Sanitize([]rune(s)) - m.setValueInternal(runes) + err := m.validateIfDefined(string(runes)) + m.setValueInternal(runes, err) } -func (m *Model) setValueInternal(runes []rune) { - if m.Validate != nil { - if err := m.Validate(string(runes)); err != nil { - m.Err = err - return - } - } +func (m *Model) setValueInternal(runes []rune, err error) { + m.Err = err empty := len(m.value) == 0 - m.Err = nil if m.CharLimit > 0 && len(runes) > m.CharLimit { m.value = runes[:m.CharLimit] @@ -307,8 +302,6 @@ func (m *Model) insertRunesFromUserInput(v []rune) { tail := make([]rune, len(tailSrc)) copy(tail, tailSrc) - oldPos := m.pos - // Insert pasted runes for _, r := range paste { head = append(head, r) @@ -323,11 +316,8 @@ func (m *Model) insertRunesFromUserInput(v []rune) { // Put it all back together value := append(head, tail...) - m.setValueInternal(value) - - if m.Err != nil { - m.pos = oldPos - } + inputErr := m.validateIfDefined(string(value)) + m.setValueInternal(value, inputErr) } // If a max width is defined, perform some logic to treat the visible area @@ -378,6 +368,7 @@ func (m *Model) handleOverflow() { // deleteBeforeCursor deletes all text before the cursor. func (m *Model) deleteBeforeCursor() { m.value = m.value[m.pos:] + m.Err = m.validateIfDefined(string(m.value)) m.offset = 0 m.SetCursor(0) } @@ -387,6 +378,7 @@ func (m *Model) deleteBeforeCursor() { // masked input. func (m *Model) deleteAfterCursor() { m.value = m.value[:m.pos] + m.Err = m.validateIfDefined(string(m.value)) m.SetCursor(len(m.value)) } @@ -432,6 +424,7 @@ func (m *Model) deleteWordBackward() { } else { m.value = append(m.value[:m.pos], m.value[oldPos:]...) } + m.Err = m.validateIfDefined(string(m.value)) } // deleteWordForward deletes the word right to the cursor. If input is masked @@ -471,6 +464,7 @@ func (m *Model) deleteWordForward() { } else { m.value = append(m.value[:oldPos], m.value[m.pos:]...) } + m.Err = m.validateIfDefined(string(m.value)) m.SetCursor(oldPos) } @@ -575,12 +569,12 @@ func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) { case tea.KeyMsg: switch { case key.Matches(msg, m.KeyMap.DeleteWordBackward): - m.Err = nil m.deleteWordBackward() case key.Matches(msg, m.KeyMap.DeleteCharacterBackward): m.Err = nil if len(m.value) > 0 { m.value = append(m.value[:max(0, m.pos-1)], m.value[m.pos:]...) + m.Err = m.validateIfDefined(string(m.value)) if m.pos > 0 { m.SetCursor(m.pos - 1) } @@ -597,13 +591,12 @@ func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) { if m.pos < len(m.value) { m.SetCursor(m.pos + 1) } - case key.Matches(msg, m.KeyMap.DeleteWordBackward): - m.deleteWordBackward() case key.Matches(msg, m.KeyMap.LineStart): m.CursorStart() case key.Matches(msg, m.KeyMap.DeleteCharacterForward): if len(m.value) > 0 && m.pos < len(m.value) { m.value = append(m.value[:m.pos], m.value[m.pos+1:]...) + m.Err = m.validateIfDefined(string(m.value)) } case key.Matches(msg, m.KeyMap.LineEnd): m.CursorEnd() @@ -884,3 +877,10 @@ func (m *Model) previousSuggestion() { m.currentSuggestionIndex = len(m.matchedSuggestions) - 1 } } + +func (m Model) validateIfDefined(v string) error { + if m.Validate != nil { + return m.Validate(v) + } + return nil +}