detached c4150ce

tweak(tui): two-threshold paste collapse with line-vs-char shape

Before: any paste with >= 2 newlines (3+ lines) collapsed to
[pasted text #N +L lines]. That fired too eagerly on short
three-line snippets the user wants to see inline, and never
fired on a 2000-character single-line dump that bloats the
editor just as badly.

New rule matches the shape widely used in other TUIs:

  collapse when
    lines > 10         OR
    chars > 1000

Both triggers produce distinct placeholder shapes so you can
see at a glance which dimension tripped it:

  [pasted text #1 +12 lines]   line trigger
  [pasted text #1 1500 chars]  char trigger

When both triggers hit (e.g. 12 lines of 400 chars each) the
line-count shape wins \u2014 "+12 lines" reads more informatively
than a raw character count for multi-line content.

Implementation:

  - pasteCollapseLineThreshold  = 10
  - pasteCollapseCharThreshold  = 1000
  - pasteShouldCollapse checks countLines(s) > 10 || len(s) > 1000
  - formatPastePlaceholder picks the shape; line trigger wins
    on ties
  - pastePlaceholderRE widened with a non-capturing alternation
    (\+\d+ lines?|\d+ chars?) so expansion works for both
    shapes; capture group 1 is still just the id
  - the strings.Contains fast-path in SubmitValue still matches
    both shapes because both start with the same prefix

Tests rewritten around the new thresholds:

  - TestPasteCollapseLineTrigger    11 lines -> +11 lines marker
  - TestPasteCollapseCharTrigger    1500 chars, 1 line -> N chars
  - TestPasteCollapseLinePrecedence 12 lines of 400 chars each ->
                                    line shape wins
  - TestPasteCollapseFallthrough    1 line / 2 lines / 10 lines /
                                    1000 chars / 5 lines under
                                    caps all stay inline
  - TestPasteCollapseSequentialIDs  mixed-shape placeholders
                                    coexist, both expand
  - TestPasteCollapseClearResetsMap unchanged logic, updated body
                                    to trigger the new threshold
commit
c4150ce63030df2b8e1c43dbe44987bbec23425a
author
patriceckhart · · 2026-04-21 15:13 UTC
parent
755ca7c
signature
unsigned
refs
tag: v0.0.79
2 files · +123 -37
M internal/tui/editor.go +43 -10
@@ -172,8 +172,7 @@ func (e *Editor) HandleKey(k Key) (submit bool) {
172172 e.pasteSeq++
173173 id := e.pasteSeq
174174 e.pastes[id] = k.Paste
175- placeholder := fmt.Sprintf("[pasted text #%d +%d lines]", id, countLines(k.Paste))
176- e.insert(placeholder)
175+ e.insert(formatPastePlaceholder(id, k.Paste))
177176 } else {
178177 // macOS Terminal / iTerm / Ghostty deliver drag-dropped
179178 // files as bracketed-paste text. Detect that pattern
@@ -958,13 +957,41 @@ func max(a, b int) int {
958957 return b
959958 }
960959
960+// pasteCollapseLineThreshold and pasteCollapseCharThreshold govern
961+// when a bracketed paste gets collapsed to a [pasted text #N …]
962+// placeholder instead of being inserted inline. Either trigger
963+// alone is enough — a 500-line log dump and a 1200-character
964+// one-line log entry both bloat the editor in ways the user
965+// doesn't want to scroll through while composing a prompt.
966+const (
967+ pasteCollapseLineThreshold = 10
968+ pasteCollapseCharThreshold = 1000
969+)
970+
961971 // pasteShouldCollapse reports whether a pasted chunk is big enough
962972 // to deserve a placeholder token instead of being inserted verbatim.
963-// Two or more newlines (i.e. three-plus lines) is the cutoff: that
964-// covers real multi-line code/log pastes while leaving single-line
965-// file-drop paths and two-line copy-pastes of a sentence alone.
973+// Collapse on > 10 lines OR > 1000 characters, whichever fires
974+// first.
966975 func pasteShouldCollapse(s string) bool {
967- return strings.Count(s, "\n") >= 2
976+ return countLines(s) > pasteCollapseLineThreshold || len(s) > pasteCollapseCharThreshold
977+}
978+
979+// formatPastePlaceholder builds the visible marker for a
980+// collapsed paste. Two shapes:
981+//
982+// [pasted text #N +L lines] — used when the line count is the
983+// trigger (multi-line dumps)
984+// [pasted text #N C chars] — used when the character count is
985+// the trigger (long single-line
986+// or near-single-line pastes)
987+//
988+// Line-triggered takes precedence so a 12-line 4000-char paste
989+// reads as "+12 lines", not "4000 chars".
990+func formatPastePlaceholder(id int, body string) string {
991+ if countLines(body) > pasteCollapseLineThreshold {
992+ return fmt.Sprintf("[pasted text #%d +%d lines]", id, countLines(body))
993+ }
994+ return fmt.Sprintf("[pasted text #%d %d chars]", id, len(body))
968995 }
969996
970997 // countLines returns the number of visual lines in s. A trailing
@@ -982,10 +1009,16 @@ func countLines(s string) int {
9821009 return n
9831010 }
9841011
985-// pastePlaceholderRE matches the "[pasted text #N +L lines]" token
986-// that collapsed pastes leave in the editor. Capture group 1 is
987-// the numeric id used to look up the full body in e.pastes.
988-var pastePlaceholderRE = regexp.MustCompile(`\[pasted text #(\d+) \+\d+ lines?\]`)
1012+// pastePlaceholderRE matches both placeholder shapes the editor
1013+// emits for a collapsed paste:
1014+//
1015+// [pasted text #N +L lines] — multi-line trigger
1016+// [pasted text #N C chars] — long-char trigger
1017+//
1018+// Capture group 1 is the numeric id used to look up the full
1019+// body in e.pastes; the rest of the token is free-form and gets
1020+// discarded during expansion.
1021+var pastePlaceholderRE = regexp.MustCompile(`\[pasted text #(\d+) (?:\+\d+ lines?|\d+ chars?)\]`)
9891022
9901023 // expandPastePlaceholders returns raw with every paste token
9911024 // swapped for the body stored under its id in pastes. Tokens
@@ -172,8 +172,7 @@ func (e *Editor) HandleKey(k Key) (submit bool) {
172 e.pasteSeq++172 e.pasteSeq++
173 id := e.pasteSeq173 id := e.pasteSeq
174 e.pastes[id] = k.Paste174 e.pastes[id] = k.Paste
175- placeholder := fmt.Sprintf("[pasted text #%d +%d lines]", id, countLines(k.Paste))175+ e.insert(formatPastePlaceholder(id, k.Paste))
176- e.insert(placeholder)
177 } else {176 } else {
178 // macOS Terminal / iTerm / Ghostty deliver drag-dropped177 // macOS Terminal / iTerm / Ghostty deliver drag-dropped
179 // files as bracketed-paste text. Detect that pattern178 // files as bracketed-paste text. Detect that pattern
@@ -958,13 +957,41 @@ func max(a, b int) int {
958 return b957 return b
959 }958 }
960 959
960+// pasteCollapseLineThreshold and pasteCollapseCharThreshold govern
961+// when a bracketed paste gets collapsed to a [pasted text #N …]
962+// placeholder instead of being inserted inline. Either trigger
963+// alone is enough — a 500-line log dump and a 1200-character
964+// one-line log entry both bloat the editor in ways the user
965+// doesn't want to scroll through while composing a prompt.
966+const (
967+ pasteCollapseLineThreshold = 10
968+ pasteCollapseCharThreshold = 1000
969+)
970+
961 // pasteShouldCollapse reports whether a pasted chunk is big enough971 // pasteShouldCollapse reports whether a pasted chunk is big enough
962 // to deserve a placeholder token instead of being inserted verbatim.972 // to deserve a placeholder token instead of being inserted verbatim.
963-// Two or more newlines (i.e. three-plus lines) is the cutoff: that973+// Collapse on > 10 lines OR > 1000 characters, whichever fires
964-// covers real multi-line code/log pastes while leaving single-line974+// first.
965-// file-drop paths and two-line copy-pastes of a sentence alone.
966 func pasteShouldCollapse(s string) bool {975 func pasteShouldCollapse(s string) bool {
967- return strings.Count(s, "\n") >= 2976+ return countLines(s) > pasteCollapseLineThreshold || len(s) > pasteCollapseCharThreshold
977+}
978+
979+// formatPastePlaceholder builds the visible marker for a
980+// collapsed paste. Two shapes:
981+//
982+// [pasted text #N +L lines] — used when the line count is the
983+// trigger (multi-line dumps)
984+// [pasted text #N C chars] — used when the character count is
985+// the trigger (long single-line
986+// or near-single-line pastes)
987+//
988+// Line-triggered takes precedence so a 12-line 4000-char paste
989+// reads as "+12 lines", not "4000 chars".
990+func formatPastePlaceholder(id int, body string) string {
991+ if countLines(body) > pasteCollapseLineThreshold {
992+ return fmt.Sprintf("[pasted text #%d +%d lines]", id, countLines(body))
993+ }
994+ return fmt.Sprintf("[pasted text #%d %d chars]", id, len(body))
968 }995 }
969 996
970 // countLines returns the number of visual lines in s. A trailing997 // countLines returns the number of visual lines in s. A trailing
@@ -982,10 +1009,16 @@ func countLines(s string) int {
982 return n1009 return n
983 }1010 }
984 1011
985-// pastePlaceholderRE matches the "[pasted text #N +L lines]" token1012+// pastePlaceholderRE matches both placeholder shapes the editor
986-// that collapsed pastes leave in the editor. Capture group 1 is1013+// emits for a collapsed paste:
987-// the numeric id used to look up the full body in e.pastes.1014+//
988-var pastePlaceholderRE = regexp.MustCompile(`\[pasted text #(\d+) \+\d+ lines?\]`)1015+// [pasted text #N +L lines] — multi-line trigger
1016+// [pasted text #N C chars] — long-char trigger
1017+//
1018+// Capture group 1 is the numeric id used to look up the full
1019+// body in e.pastes; the rest of the token is free-form and gets
1020+// discarded during expansion.
1021+var pastePlaceholderRE = regexp.MustCompile(`\[pasted text #(\d+) (?:\+\d+ lines?|\d+ chars?)\]`)
989 1022
990 // expandPastePlaceholders returns raw with every paste token1023 // expandPastePlaceholders returns raw with every paste token
991 // swapped for the body stored under its id in pastes. Tokens1024 // swapped for the body stored under its id in pastes. Tokens
M internal/tui/paste_collapse_test.go +80 -27
@@ -1,20 +1,50 @@
11 package tui
22
33 import (
4+ "fmt"
45 "strings"
56 "testing"
67 )
78
8-// TestPasteCollapseInsertsPlaceholder verifies that a multi-line
9-// paste gets collapsed to the "[paste #N +L lines]" placeholder
10-// in the editor buffer, leaving the full body behind the scenes.
11-func TestPasteCollapseInsertsPlaceholder(t *testing.T) {
9+// makeLines returns a body with n lines of short text — useful for
10+// exercising the line-count trigger without also tripping the
11+// character-count one.
12+func makeLines(n int) string {
13+ parts := make([]string, n)
14+ for i := range parts {
15+ parts[i] = fmt.Sprintf("line %d", i+1)
16+ }
17+ return strings.Join(parts, "\n")
18+}
19+
20+// TestPasteCollapseLineTrigger verifies that a paste with more than
21+// ten lines gets collapsed to the "+L lines" placeholder shape.
22+// The full body is preserved and expanded back by SubmitValue.
23+func TestPasteCollapseLineTrigger(t *testing.T) {
24+ e := NewEditor("▌ ")
25+ body := makeLines(11)
26+ e.HandleKey(Key{Kind: KeyPaste, Paste: body})
27+
28+ got := e.Value()
29+ want := "[pasted text #1 +11 lines]"
30+ if got != want {
31+ t.Fatalf("editor Value: want %q, got %q", want, got)
32+ }
33+ if e.SubmitValue() != body {
34+ t.Fatalf("SubmitValue didn't expand placeholder: got %q", e.SubmitValue())
35+ }
36+}
37+
38+// TestPasteCollapseCharTrigger verifies that a paste with more than
39+// 1000 characters but few enough lines collapses to the "C chars"
40+// placeholder shape (long single-line / near-single-line dumps).
41+func TestPasteCollapseCharTrigger(t *testing.T) {
1242 e := NewEditor("▌ ")
13- body := "line1\nline2\nline3\nline4"
43+ body := strings.Repeat("a", 1500) // 1 line, 1500 chars
1444 e.HandleKey(Key{Kind: KeyPaste, Paste: body})
1545
1646 got := e.Value()
17- want := "[pasted text #1 +4 lines]"
47+ want := "[pasted text #1 1500 chars]"
1848 if got != want {
1949 t.Fatalf("editor Value: want %q, got %q", want, got)
2050 }
@@ -23,38 +53,61 @@ func TestPasteCollapseInsertsPlaceholder(t *testing.T) {
2353 }
2454 }
2555
26-// TestPasteCollapseSingleLineFallthrough ensures short pastes are
27-// NOT collapsed — single-line drag-drop file paths and short
28-// two-line snippets should appear inline so the user can edit
29-// them in place.
30-func TestPasteCollapseSingleLineFallthrough(t *testing.T) {
56+// TestPasteCollapseLinePrecedence verifies that when a paste trips
57+// both thresholds, the line-count marker wins. "12 lines, 4000
58+// chars" should read as "+12 lines", not "4000 chars".
59+func TestPasteCollapseLinePrecedence(t *testing.T) {
3160 e := NewEditor("▌ ")
32- e.HandleKey(Key{Kind: KeyPaste, Paste: "hello world"})
33- if strings.Contains(e.Value(), "[pasted text #") {
34- t.Errorf("single-line paste should not collapse, got %q", e.Value())
61+ // 12 lines, each 400 chars = 12 lines, ~4800 chars.
62+ parts := make([]string, 12)
63+ for i := range parts {
64+ parts[i] = strings.Repeat("x", 400)
3565 }
66+ body := strings.Join(parts, "\n")
67+ e.HandleKey(Key{Kind: KeyPaste, Paste: body})
3668
37- e2 := NewEditor("▌ ")
38- e2.HandleKey(Key{Kind: KeyPaste, Paste: "line1\nline2"})
39- if strings.Contains(e2.Value(), "[pasted text #") {
40- t.Errorf("two-line paste should not collapse, got %q", e2.Value())
69+ if !strings.HasPrefix(e.Value(), "[pasted text #1 +12 lines") {
70+ t.Errorf("want line-trigger placeholder, got %q", e.Value())
71+ }
72+}
73+
74+// TestPasteCollapseFallthrough ensures short pastes (under both
75+// thresholds) are NOT collapsed — 1-10 lines with < 1000 chars
76+// should appear inline so the user can edit them in place.
77+func TestPasteCollapseFallthrough(t *testing.T) {
78+ cases := map[string]string{
79+ "single line": "hello world",
80+ "two lines": "line1\nline2",
81+ "ten lines (at limit)": makeLines(10),
82+ "exactly 1000 chars": strings.Repeat("a", 1000),
83+ "multiline under caps": "aaa\nbbb\nccc\nddd\neee",
84+ }
85+ for name, body := range cases {
86+ t.Run(name, func(t *testing.T) {
87+ e := NewEditor("▌ ")
88+ e.HandleKey(Key{Kind: KeyPaste, Paste: body})
89+ if strings.Contains(e.Value(), "[pasted text #") {
90+ t.Errorf("%s should NOT collapse, got %q", name, e.Value())
91+ }
92+ })
4193 }
4294 }
4395
44-// TestPasteCollapseSequentialIDs makes sure two separate pastes get
45-// distinct ids and both expand correctly in SubmitValue.
96+// TestPasteCollapseSequentialIDs makes sure two separate large
97+// pastes get distinct ids and both expand correctly in
98+// SubmitValue, even when they use different placeholder shapes.
4699 func TestPasteCollapseSequentialIDs(t *testing.T) {
47100 e := NewEditor("▌ ")
48- a := "aaa\nbbb\nccc"
49- b := "xxx\nyyy\nzzz"
101+ a := makeLines(12) // line trigger
102+ b := strings.Repeat("y", 1500) // char trigger, single line
50103 e.HandleKey(Key{Kind: KeyPaste, Paste: a})
51104 e.Insert(" ")
52105 e.HandleKey(Key{Kind: KeyPaste, Paste: b})
53106
54107 visible := e.Value()
55- if !strings.Contains(visible, "[pasted text #1 +3 lines]") ||
56- !strings.Contains(visible, "[pasted text #2 +3 lines]") {
57- t.Fatalf("expected two placeholders in %q", visible)
108+ if !strings.Contains(visible, "[pasted text #1 +12 lines]") ||
109+ !strings.Contains(visible, "[pasted text #2 1500 chars]") {
110+ t.Fatalf("expected mixed-shape placeholders in %q", visible)
58111 }
59112
60113 full := e.SubmitValue()
@@ -69,11 +122,11 @@ func TestPasteCollapseSequentialIDs(t *testing.T) {
69122 // the wrong body).
70123 func TestPasteCollapseClearResetsMap(t *testing.T) {
71124 e := NewEditor("▌ ")
72- e.HandleKey(Key{Kind: KeyPaste, Paste: "a\nb\nc"})
125+ e.HandleKey(Key{Kind: KeyPaste, Paste: makeLines(11)})
73126 e.Clear()
74127
75128 // A fresh paste must start at id #1 again.
76- e.HandleKey(Key{Kind: KeyPaste, Paste: "d\ne\nf"})
129+ e.HandleKey(Key{Kind: KeyPaste, Paste: makeLines(11)})
77130 if !strings.Contains(e.Value(), "[pasted text #1 ") {
78131 t.Errorf("Clear didn't reset pasteSeq: %q", e.Value())
79132 }
@@ -1,20 +1,50 @@
1 package tui1 package tui
2 2
3 import (3 import (
4+ "fmt"
4 "strings"5 "strings"
5 "testing"6 "testing"
6 )7 )
7 8
8-// TestPasteCollapseInsertsPlaceholder verifies that a multi-line9+// makeLines returns a body with n lines of short text — useful for
9-// paste gets collapsed to the "[paste #N +L lines]" placeholder10+// exercising the line-count trigger without also tripping the
10-// in the editor buffer, leaving the full body behind the scenes.11+// character-count one.
11-func TestPasteCollapseInsertsPlaceholder(t *testing.T) {12+func makeLines(n int) string {
13+ parts := make([]string, n)
14+ for i := range parts {
15+ parts[i] = fmt.Sprintf("line %d", i+1)
16+ }
17+ return strings.Join(parts, "\n")
18+}
19+
20+// TestPasteCollapseLineTrigger verifies that a paste with more than
21+// ten lines gets collapsed to the "+L lines" placeholder shape.
22+// The full body is preserved and expanded back by SubmitValue.
23+func TestPasteCollapseLineTrigger(t *testing.T) {
24+ e := NewEditor("▌ ")
25+ body := makeLines(11)
26+ e.HandleKey(Key{Kind: KeyPaste, Paste: body})
27+
28+ got := e.Value()
29+ want := "[pasted text #1 +11 lines]"
30+ if got != want {
31+ t.Fatalf("editor Value: want %q, got %q", want, got)
32+ }
33+ if e.SubmitValue() != body {
34+ t.Fatalf("SubmitValue didn't expand placeholder: got %q", e.SubmitValue())
35+ }
36+}
37+
38+// TestPasteCollapseCharTrigger verifies that a paste with more than
39+// 1000 characters but few enough lines collapses to the "C chars"
40+// placeholder shape (long single-line / near-single-line dumps).
41+func TestPasteCollapseCharTrigger(t *testing.T) {
12 e := NewEditor("▌ ")42 e := NewEditor("▌ ")
13- body := "line1\nline2\nline3\nline4"43+ body := strings.Repeat("a", 1500) // 1 line, 1500 chars
14 e.HandleKey(Key{Kind: KeyPaste, Paste: body})44 e.HandleKey(Key{Kind: KeyPaste, Paste: body})
15 45
16 got := e.Value()46 got := e.Value()
17- want := "[pasted text #1 +4 lines]"47+ want := "[pasted text #1 1500 chars]"
18 if got != want {48 if got != want {
19 t.Fatalf("editor Value: want %q, got %q", want, got)49 t.Fatalf("editor Value: want %q, got %q", want, got)
20 }50 }
@@ -23,38 +53,61 @@ func TestPasteCollapseInsertsPlaceholder(t *testing.T) {
23 }53 }
24 }54 }
25 55
26-// TestPasteCollapseSingleLineFallthrough ensures short pastes are56+// TestPasteCollapseLinePrecedence verifies that when a paste trips
27-// NOT collapsed — single-line drag-drop file paths and short57+// both thresholds, the line-count marker wins. "12 lines, 4000
28-// two-line snippets should appear inline so the user can edit58+// chars" should read as "+12 lines", not "4000 chars".
29-// them in place.59+func TestPasteCollapseLinePrecedence(t *testing.T) {
30-func TestPasteCollapseSingleLineFallthrough(t *testing.T) {
31 e := NewEditor("▌ ")60 e := NewEditor("▌ ")
32- e.HandleKey(Key{Kind: KeyPaste, Paste: "hello world"})61+ // 12 lines, each 400 chars = 12 lines, ~4800 chars.
33- if strings.Contains(e.Value(), "[pasted text #") {62+ parts := make([]string, 12)
34- t.Errorf("single-line paste should not collapse, got %q", e.Value())63+ for i := range parts {
64+ parts[i] = strings.Repeat("x", 400)
35 }65 }
66+ body := strings.Join(parts, "\n")
67+ e.HandleKey(Key{Kind: KeyPaste, Paste: body})
36 68
37- e2 := NewEditor("▌ ")69+ if !strings.HasPrefix(e.Value(), "[pasted text #1 +12 lines") {
38- e2.HandleKey(Key{Kind: KeyPaste, Paste: "line1\nline2"})70+ t.Errorf("want line-trigger placeholder, got %q", e.Value())
39- if strings.Contains(e2.Value(), "[pasted text #") {71+ }
40- t.Errorf("two-line paste should not collapse, got %q", e2.Value())72+}
73+
74+// TestPasteCollapseFallthrough ensures short pastes (under both
75+// thresholds) are NOT collapsed — 1-10 lines with < 1000 chars
76+// should appear inline so the user can edit them in place.
77+func TestPasteCollapseFallthrough(t *testing.T) {
78+ cases := map[string]string{
79+ "single line": "hello world",
80+ "two lines": "line1\nline2",
81+ "ten lines (at limit)": makeLines(10),
82+ "exactly 1000 chars": strings.Repeat("a", 1000),
83+ "multiline under caps": "aaa\nbbb\nccc\nddd\neee",
84+ }
85+ for name, body := range cases {
86+ t.Run(name, func(t *testing.T) {
87+ e := NewEditor("▌ ")
88+ e.HandleKey(Key{Kind: KeyPaste, Paste: body})
89+ if strings.Contains(e.Value(), "[pasted text #") {
90+ t.Errorf("%s should NOT collapse, got %q", name, e.Value())
91+ }
92+ })
41 }93 }
42 }94 }
43 95
44-// TestPasteCollapseSequentialIDs makes sure two separate pastes get96+// TestPasteCollapseSequentialIDs makes sure two separate large
45-// distinct ids and both expand correctly in SubmitValue.97+// pastes get distinct ids and both expand correctly in
98+// SubmitValue, even when they use different placeholder shapes.
46 func TestPasteCollapseSequentialIDs(t *testing.T) {99 func TestPasteCollapseSequentialIDs(t *testing.T) {
47 e := NewEditor("▌ ")100 e := NewEditor("▌ ")
48- a := "aaa\nbbb\nccc"101+ a := makeLines(12) // line trigger
49- b := "xxx\nyyy\nzzz"102+ b := strings.Repeat("y", 1500) // char trigger, single line
50 e.HandleKey(Key{Kind: KeyPaste, Paste: a})103 e.HandleKey(Key{Kind: KeyPaste, Paste: a})
51 e.Insert(" ")104 e.Insert(" ")
52 e.HandleKey(Key{Kind: KeyPaste, Paste: b})105 e.HandleKey(Key{Kind: KeyPaste, Paste: b})
53 106
54 visible := e.Value()107 visible := e.Value()
55- if !strings.Contains(visible, "[pasted text #1 +3 lines]") ||108+ if !strings.Contains(visible, "[pasted text #1 +12 lines]") ||
56- !strings.Contains(visible, "[pasted text #2 +3 lines]") {109+ !strings.Contains(visible, "[pasted text #2 1500 chars]") {
57- t.Fatalf("expected two placeholders in %q", visible)110+ t.Fatalf("expected mixed-shape placeholders in %q", visible)
58 }111 }
59 112
60 full := e.SubmitValue()113 full := e.SubmitValue()
@@ -69,11 +122,11 @@ func TestPasteCollapseSequentialIDs(t *testing.T) {
69 // the wrong body).122 // the wrong body).
70 func TestPasteCollapseClearResetsMap(t *testing.T) {123 func TestPasteCollapseClearResetsMap(t *testing.T) {
71 e := NewEditor("▌ ")124 e := NewEditor("▌ ")
72- e.HandleKey(Key{Kind: KeyPaste, Paste: "a\nb\nc"})125+ e.HandleKey(Key{Kind: KeyPaste, Paste: makeLines(11)})
73 e.Clear()126 e.Clear()
74 127
75 // A fresh paste must start at id #1 again.128 // A fresh paste must start at id #1 again.
76- e.HandleKey(Key{Kind: KeyPaste, Paste: "d\ne\nf"})129+ e.HandleKey(Key{Kind: KeyPaste, Paste: makeLines(11)})
77 if !strings.Contains(e.Value(), "[pasted text #1 ") {130 if !strings.Contains(e.Value(), "[pasted text #1 ") {
78 t.Errorf("Clear didn't reset pasteSeq: %q", e.Value())131 t.Errorf("Clear didn't reset pasteSeq: %q", e.Value())
79 }132 }

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 1

Tags 476

↑↓ moveenter openesc close