feat: improve textbox editor behaviour
This commit is contained in:
155
tui_test.go
155
tui_test.go
@@ -701,13 +701,16 @@ func TestDashboardEditorUpdatesTitleBodyAndBaseBranch(t *testing.T) {
|
||||
send(tea.KeyMsg{Type: tea.KeyShiftTab})
|
||||
send(tea.KeyMsg{Type: tea.KeyShiftTab})
|
||||
send(tea.KeyMsg{Type: tea.KeyShiftTab})
|
||||
send(runeKey("i"))
|
||||
send(tea.KeyMsg{Type: tea.KeyHome})
|
||||
for range len("main") {
|
||||
send(tea.KeyMsg{Type: tea.KeyDelete})
|
||||
}
|
||||
send(tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune("release")})
|
||||
send(tea.KeyMsg{Type: tea.KeyEsc})
|
||||
|
||||
send(tea.KeyMsg{Type: tea.KeyShiftTab})
|
||||
send(runeKey("i"))
|
||||
send(tea.KeyMsg{Type: tea.KeyHome})
|
||||
for range len("Old title") {
|
||||
send(tea.KeyMsg{Type: tea.KeyDelete})
|
||||
@@ -794,9 +797,95 @@ func TestDashboardDescriptionCanUseStandardEditingMode(t *testing.T) {
|
||||
Permissions: ViewerPermissions{CanUpdatePR: true},
|
||||
}
|
||||
m.startPREdit()
|
||||
editor := m.prEditEditors[prEditBodyField]
|
||||
if editor.Modal || editor.Mode != textEditorInsert || editor.Cursor != len([]rune("body")) {
|
||||
t.Fatalf("standard description editor = %#v", editor)
|
||||
for field, editor := range m.prEditEditors {
|
||||
if editor.Modal || editor.Mode != textEditorInsert ||
|
||||
editor.Cursor != len([]rune(editor.Text)) {
|
||||
t.Fatalf("standard editor field %d = %#v", field, editor)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestVimModeAppliesToEveryTextInput(t *testing.T) {
|
||||
m := NewApp(&recordingPRService{}, "o", "r", false, 50, time.Second)
|
||||
m.loading, m.width, m.height = false, 80, 24
|
||||
m.details = PRDetails{
|
||||
PullRequest: PullRequest{ID: "pr", Title: "Title"},
|
||||
Body: "body", BaseRef: "main",
|
||||
Permissions: ViewerPermissions{CanUpdatePR: true},
|
||||
}
|
||||
m.startPREdit()
|
||||
for field, editor := range m.prEditEditors {
|
||||
if !editor.Modal || editor.Mode != textEditorNormal {
|
||||
t.Fatalf("Vim PR editor field %d = %#v", field, editor)
|
||||
}
|
||||
}
|
||||
|
||||
m.writeMode, m.replyDraft = writeReply, "reply"
|
||||
m.resetInputEditor(&m.replyEditor, m.replyDraft)
|
||||
updated, _ := m.updateWriteInput(runeKey("i"))
|
||||
m = updated.(App)
|
||||
updated, _ = m.updateWriteInput(runeKey("!"))
|
||||
m = updated.(App)
|
||||
updated, _ = m.updateWriteInput(tea.KeyMsg{Type: tea.KeyEsc})
|
||||
m = updated.(App)
|
||||
if m.replyDraft != "!reply" || m.replyEditor.Mode != textEditorNormal {
|
||||
t.Fatalf("Vim reply = %q mode=%s", m.replyDraft, m.replyEditor.Mode)
|
||||
}
|
||||
|
||||
m.writeMode, m.aiMode, m.aiInput = writeNone, aiDiscussion, "ask"
|
||||
m.resetInputEditor(&m.aiInputEditor, m.aiInput)
|
||||
updated, _, handled := m.updateAI(runeKey("A"))
|
||||
m = updated.(App)
|
||||
if !handled || m.aiInputEditor.Mode != textEditorInsert ||
|
||||
m.aiInputEditor.Cursor != len([]rune("ask")) {
|
||||
t.Fatalf("Vim AI append handled=%v editor=%#v", handled, m.aiInputEditor)
|
||||
}
|
||||
|
||||
m.aiMode = aiNone
|
||||
m.screen, m.loading = threadScreen, false
|
||||
m.details.Threads = []ReviewThread{{ID: "thread", Path: "main.go"}}
|
||||
updated, _ = m.Update(runeKey("/"))
|
||||
m = updated.(App)
|
||||
if !m.searching || m.searchEditor.Modal || m.searchEditor.Mode != textEditorInsert {
|
||||
t.Fatalf("insert-only search editor = %#v searching=%v", m.searchEditor, m.searching)
|
||||
}
|
||||
updated, _ = m.Update(runeKey("main"))
|
||||
m = updated.(App)
|
||||
if m.searchQuery != "main" || m.searchEditor.Mode != textEditorInsert {
|
||||
t.Fatalf("Vim search query=%q editor=%#v", m.searchQuery, m.searchEditor)
|
||||
}
|
||||
}
|
||||
|
||||
func TestVimModeUsesGlobalFooterBar(t *testing.T) {
|
||||
m := NewApp(nil, "o", "r", false, 50, time.Second)
|
||||
m.width, m.height = 80, 10
|
||||
m.writeMode, m.replyDraft = writeReply, "reply"
|
||||
m.resetInputEditor(&m.replyEditor, m.replyDraft)
|
||||
|
||||
footer := ansi.Strip(m.renderFooter("ctrl+s review • esc normal/cancel", m.width))
|
||||
if !strings.Contains(footer, "NORMAL") || !strings.Contains(footer, "REPLY") {
|
||||
t.Fatalf("Normal reply footer = %q", footer)
|
||||
}
|
||||
m.replyEditor.Mode = textEditorInsert
|
||||
footer = ansi.Strip(m.renderFooter("ctrl+s review • esc normal/cancel", m.width))
|
||||
if !strings.Contains(footer, "INSERT") || strings.Contains(footer, "NORMAL") {
|
||||
t.Fatalf("Insert reply footer = %q", footer)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDashboardVimModeBarStaysOnBottomRow(t *testing.T) {
|
||||
m := NewApp(&recordingPRService{}, "o", "r", false, 50, time.Second)
|
||||
m.screen, m.loading, m.width, m.height = dashboardScreen, false, 80, 24
|
||||
m.details = PRDetails{
|
||||
PullRequest: PullRequest{ID: "pr", Title: "Title"},
|
||||
Body: "short", BaseRef: "main",
|
||||
Permissions: ViewerPermissions{CanUpdatePR: true},
|
||||
}
|
||||
m.startPREdit()
|
||||
lines := strings.Split(ansi.Strip(m.viewDashboard()), "\n")
|
||||
if len(lines) != m.height || !strings.Contains(lines[len(lines)-1], "NORMAL") ||
|
||||
!strings.Contains(lines[len(lines)-1], "EDIT DESCRIPTION") {
|
||||
t.Fatalf("dashboard mode bar is not on bottom row:\n%s", strings.Join(lines, "\n"))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1046,7 +1135,11 @@ func TestThreadInputsUseHardwareCursor(t *testing.T) {
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
m := NewApp(&recordingPRService{}, "o", "r", false, 50, time.Second)
|
||||
settings := defaultAppSettings()
|
||||
settings.EditorMode = "standard"
|
||||
m := NewAppWithSettings(
|
||||
&recordingPRService{}, "o", "r", false, 50, time.Second, settings,
|
||||
)
|
||||
m.cursorOutput = newTerminalCursorOutput(file)
|
||||
m.screen, m.loading, m.width, m.height = threadScreen, false, 100, 24
|
||||
m.details = PRDetails{
|
||||
@@ -1058,6 +1151,7 @@ func TestThreadInputsUseHardwareCursor(t *testing.T) {
|
||||
}
|
||||
|
||||
m.searching, m.searchQuery = true, "main"
|
||||
m.resetInputEditor(&m.searchEditor, m.searchQuery)
|
||||
_ = m.viewThreads()
|
||||
m.cursorOutput.mu.Lock()
|
||||
searchVisible, searchColumn := m.cursorOutput.visible, m.cursorOutput.column
|
||||
@@ -1068,6 +1162,7 @@ func TestThreadInputsUseHardwareCursor(t *testing.T) {
|
||||
|
||||
m.searching = false
|
||||
m.aiMode, m.writeThreadID, m.aiInput = aiDiscussion, "thread-1", "Question"
|
||||
m.resetInputEditor(&m.aiInputEditor, m.aiInput)
|
||||
m.focus = threadDetailPane
|
||||
m.scroll = m.detailMaxScroll()
|
||||
_ = m.viewThreads()
|
||||
@@ -1140,6 +1235,8 @@ func TestReplyComposerConfirmsAndAddsReturnedComment(t *testing.T) {
|
||||
if m.writeMode != writeReply {
|
||||
t.Fatalf("reply key opened mode %d", m.writeMode)
|
||||
}
|
||||
updated, _ = m.Update(runeKey("i"))
|
||||
m = updated.(App)
|
||||
updated, _ = m.Update(tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune("hello")})
|
||||
m = updated.(App)
|
||||
updated, _ = m.Update(tea.KeyMsg{Type: tea.KeyCtrlS})
|
||||
@@ -1183,6 +1280,13 @@ func TestReplyComposerRendersInlineWithCurrentThread(t *testing.T) {
|
||||
t.Fatalf("inline reply view is missing %q:\n%s", wanted, plain)
|
||||
}
|
||||
}
|
||||
inline := m.inlineReplyLines(80)
|
||||
for _, line := range inline {
|
||||
if strings.Contains(ansi.Strip(line.text), "newline") ||
|
||||
strings.Contains(ansi.Strip(line.text), "review") {
|
||||
t.Fatalf("Vim reply contains redundant inline key help: %#v", inline)
|
||||
}
|
||||
}
|
||||
if m.focus != threadDetailPane {
|
||||
t.Fatal("inline reply did not focus the thread detail pane")
|
||||
}
|
||||
@@ -1873,8 +1977,11 @@ func TestFuzzyFileSearchSupportsSpaceSeparatedTerms(t *testing.T) {
|
||||
t.Fatalf("query matched despite a missing term")
|
||||
}
|
||||
|
||||
m := NewApp(nil, "o", "r", false, 50, 10*time.Second)
|
||||
settings := defaultAppSettings()
|
||||
settings.EditorMode = "standard"
|
||||
m := NewAppWithSettings(nil, "o", "r", false, 50, 10*time.Second, settings)
|
||||
m.screen, m.searching, m.searchQuery = threadScreen, true, "ng"
|
||||
m.resetInputEditor(&m.searchEditor, m.searchQuery)
|
||||
updated, _ := m.Update(tea.KeyMsg{Type: tea.KeySpace})
|
||||
if got := updated.(App).searchQuery; got != "ng " {
|
||||
t.Fatalf("space key produced search query %q", got)
|
||||
@@ -1882,8 +1989,11 @@ func TestFuzzyFileSearchSupportsSpaceSeparatedTerms(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestReplyAndAIDiscussionAcceptSpaceKeyWithoutRunes(t *testing.T) {
|
||||
m := NewApp(nil, "o", "r", false, 50, 10*time.Second)
|
||||
settings := defaultAppSettings()
|
||||
settings.EditorMode = "standard"
|
||||
m := NewAppWithSettings(nil, "o", "r", false, 50, 10*time.Second, settings)
|
||||
m.writeMode, m.replyDraft = writeReply, "reply"
|
||||
m.resetInputEditor(&m.replyEditor, m.replyDraft)
|
||||
updated, _ := m.updateWriteInput(tea.KeyMsg{Type: tea.KeySpace})
|
||||
m = updated.(App)
|
||||
if m.replyDraft != "reply " {
|
||||
@@ -1891,6 +2001,7 @@ func TestReplyAndAIDiscussionAcceptSpaceKeyWithoutRunes(t *testing.T) {
|
||||
}
|
||||
|
||||
m.writeMode, m.aiMode, m.aiInput = writeNone, aiDiscussion, "question"
|
||||
m.resetInputEditor(&m.aiInputEditor, m.aiInput)
|
||||
updated, _, handled := m.updateAI(tea.KeyMsg{Type: tea.KeySpace})
|
||||
m = updated.(App)
|
||||
if !handled || m.aiInput != "question " {
|
||||
@@ -1898,6 +2009,38 @@ func TestReplyAndAIDiscussionAcceptSpaceKeyWithoutRunes(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestReplyAndAIDiscussionArrowKeysMoveInputCursor(t *testing.T) {
|
||||
settings := defaultAppSettings()
|
||||
settings.EditorMode = "standard"
|
||||
m := NewAppWithSettings(nil, "o", "r", false, 50, 10*time.Second, settings)
|
||||
m.width, m.height = 80, 24
|
||||
m.writeMode, m.replyDraft = writeReply, "mistke"
|
||||
m.resetInputEditor(&m.replyEditor, m.replyDraft)
|
||||
for range 2 {
|
||||
updated, _ := m.updateWriteInput(tea.KeyMsg{Type: tea.KeyLeft})
|
||||
m = updated.(App)
|
||||
}
|
||||
updated, _ := m.updateWriteInput(tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune("a")})
|
||||
m = updated.(App)
|
||||
if m.replyDraft != "mistake" || m.replyEditor.Cursor != 5 {
|
||||
t.Fatalf("reply after cursor edit = %q at %d", m.replyDraft, m.replyEditor.Cursor)
|
||||
}
|
||||
|
||||
m.writeMode, m.aiMode, m.aiInput = writeNone, aiDiscussion, "abc\ndef"
|
||||
m.resetInputEditor(&m.aiInputEditor, m.aiInput)
|
||||
m.aiInputEditor.Cursor = 1
|
||||
updated, _, handled := m.updateAI(tea.KeyMsg{Type: tea.KeyDown})
|
||||
m = updated.(App)
|
||||
if !handled || m.aiInputEditor.Cursor != 5 {
|
||||
t.Fatalf("AI down movement handled=%v cursor=%d, want 5", handled, m.aiInputEditor.Cursor)
|
||||
}
|
||||
updated, _, handled = m.updateAI(tea.KeyMsg{Type: tea.KeyRight})
|
||||
m = updated.(App)
|
||||
if !handled || m.aiInputEditor.Cursor != 6 {
|
||||
t.Fatalf("AI right movement handled=%v cursor=%d, want 6", handled, m.aiInputEditor.Cursor)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFileSearchCanJumpOrCancel(t *testing.T) {
|
||||
m := NewApp(nil, "o", "r", false, 50, 10*time.Second)
|
||||
m.screen = threadScreen
|
||||
|
||||
Reference in New Issue
Block a user