1package tui
2
3import (
4 "net/url"
5 "strings"
6
7 "github.com/mattn/go-runewidth"
8)
9
10// Editor is a simple multi-line text editor for the input area.
11//
12// Users can type, paste, move the cursor, and submit. The editor
13// exposes its rendered height and the current cursor row/col for the
14// outer renderer. It does NOT draw itself directly; instead, Render()
15// returns the visible lines.
16type Editor struct {
17 // Lines is the current buffer, one entry per line.
18 Lines []string
19 CursorR int // row index into Lines
20 CursorC int // rune index into Lines[CursorR]
21 Prompt string
22 MaxWidth int
23
24 // History is a ring of previously submitted buffers (newest last).
25 History []string
26 histIdx int // -1 means "editing current buffer"
27 savedDraft string
28}
29
30// NewEditor returns an empty editor with the given prompt.
31func NewEditor(prompt string) *Editor {
32 return &Editor{
33 Lines: []string{""},
34 Prompt: prompt,
35 histIdx: -1,
36 }
37}
38
39// Value returns the buffer as a single string.
40func (e *Editor) Value() string { return strings.Join(e.Lines, "\n") }
41
42// SetValue replaces the buffer and places the cursor at the end.
43func (e *Editor) SetValue(s string) {
44 e.Lines = strings.Split(s, "\n")
45 if len(e.Lines) == 0 {
46 e.Lines = []string{""}
47 }
48 e.CursorR = len(e.Lines) - 1
49 e.CursorC = runeLen(e.Lines[e.CursorR])
50 e.histIdx = -1
51}
52
53// Clear resets the buffer.
54func (e *Editor) Clear() { e.SetValue("") }
55
56// IsEmpty reports whether the buffer has no visible content.
57func (e *Editor) IsEmpty() bool {
58 return len(e.Lines) == 1 && e.Lines[0] == ""
59}
60
61// HandleKey applies k to the editor. It returns (submit, key).
62// If submit is true, the caller should read Value() and call
63// PushHistory + Clear.
64func (e *Editor) HandleKey(k Key) (submit bool) {
65 switch k.Kind {
66 case KeyRune:
67 if k.Alt && (k.Rune == '\r' || k.Rune == '\n') {
68 e.newline()
69 return false
70 }
71 e.insert(string(k.Rune))
72 case KeyEnter:
73 // Shift+Enter would be a separate key in some terminals; we treat
74 // the literal newline byte as Enter. Newline on submit is a decision
75 // for the outer UI via slash commands. Here Enter submits.
76 return true
77 case KeyBackspace:
78 if k.Alt {
79 // Alt+Backspace (Option+Delete on macOS) — delete previous word.
80 e.deleteWord()
81 } else {
82 e.backspace()
83 }
84 case KeyDelete:
85 e.delete()
86 case KeyLeft:
87 if k.Alt {
88 e.moveWordLeft()
89 } else {
90 e.moveLeft()
91 }
92 case KeyRight:
93 if k.Alt {
94 e.moveWordRight()
95 } else {
96 e.moveRight()
97 }
98 case KeyUp:
99 if e.CursorR == 0 {
100 e.historyPrev()
101 } else {
102 e.CursorR--
103 if e.CursorC > runeLen(e.Lines[e.CursorR]) {
104 e.CursorC = runeLen(e.Lines[e.CursorR])
105 }
106 }
107 case KeyDown:
108 if e.CursorR == len(e.Lines)-1 {
109 e.historyNext()
110 } else {
111 e.CursorR++
112 if e.CursorC > runeLen(e.Lines[e.CursorR]) {
113 e.CursorC = runeLen(e.Lines[e.CursorR])
114 }
115 }
116 case KeyHome, KeyCtrlA:
117 e.CursorC = 0
118 case KeyEnd, KeyCtrlE:
119 e.CursorC = runeLen(e.Lines[e.CursorR])
120 case KeyCtrlU:
121 e.Lines[e.CursorR] = substringAfter(e.Lines[e.CursorR], e.CursorC)
122 e.CursorC = 0
123 case KeyCtrlK:
124 e.Lines[e.CursorR] = substringBefore(e.Lines[e.CursorR], e.CursorC)
125 case KeyCtrlW:
126 e.deleteWord()
127 case KeyPaste:
128 // macOS Terminal / iTerm / Ghostty deliver drag-dropped files
129 // as bracketed-paste text. Detect that pattern and wrap the
130 // path(s) in single quotes so the agent sees them as one
131 // argument and any spaces / parens in the filename don't
132 // confuse downstream tool calls.
133 e.insert(quotePastedFilePaths(k.Paste))
134 case KeyEsc:
135 e.Clear()
136 }
137 return false
138}
139
140// quotePastedFilePaths returns paste with any drag-dropped file
141// paths wrapped in single quotes. Heuristics:
142//
143// - one line only; multi-line text is returned unchanged so we
144// never touch a real paste of code.
145// - whitespace-separated tokens are inspected one by one. A token
146// that looks like a filesystem path (absolute, ~/relative, or
147// file:// URL) is normalised and re-quoted; everything else is
148// left exactly as the user dropped it.
149// - terminal-style backslash escapes ("foo\ bar.png") are folded
150// back to literal characters before quoting, since wrapping in
151// single quotes makes them unnecessary.
152// - file:// URLs are URL-decoded and stripped of the scheme so
153// the agent receives a plain filesystem path.
154// - if a path contains a literal single quote, it's escaped using
155// the standard '\” splice.
156//
157// Pastes that don't contain any path-shaped tokens are returned
158// unchanged.
159func quotePastedFilePaths(paste string) string {
160 if paste == "" || strings.ContainsRune(paste, '\n') {
161 return paste
162 }
163 trimmed := strings.TrimSpace(paste)
164 if trimmed == "" {
165 return paste
166 }
167
168 // Split on runs of whitespace, preserving the original separators
169 // so multi-file drops keep their spacing on rebuild. Backslash
170 // before a space is treated as part of the preceding token, since
171 // macOS Terminal escapes spaces in dropped paths that way.
172 tokens := splitPreservingSeparators(paste)
173 changed := false
174 for i, tk := range tokens {
175 if tk.isSpace {
176 continue
177 }
178 if p, ok := normalisePathToken(tk.text); ok {
179 tokens[i].text = singleQuote(p)
180 changed = true
181 }
182 }
183 if !changed {
184 return paste
185 }
186 var out strings.Builder
187 out.Grow(len(paste) + 8)
188 for _, tk := range tokens {
189 out.WriteString(tk.text)
190 }
191 return out.String()
192}
193
194type pasteToken struct {
195 text string
196 isSpace bool
197}
198
199// splitPreservingSeparators splits s into runs of whitespace and
200// runs of non-whitespace, keeping the separator runs intact so a
201// rebuild via concatenation reproduces the original string exactly.
202// A backslash immediately followed by a space ("\ ") is treated as
203// part of the preceding non-whitespace run, since that's how macOS
204// Terminal escapes spaces in drag-dropped file paths.
205func splitPreservingSeparators(s string) []pasteToken {
206 runes := []rune(s)
207 var out []pasteToken
208 var buf strings.Builder
209 inSpace := false
210 flush := func() {
211 if buf.Len() == 0 {
212 return
213 }
214 out = append(out, pasteToken{text: buf.String(), isSpace: inSpace})
215 buf.Reset()
216 }
217 for i := 0; i < len(runes); i++ {
218 r := runes[i]
219 isEscapedSpace := r == '\\' && i+1 < len(runes) && (runes[i+1] == ' ' || runes[i+1] == '\t')
220 rSpace := !isEscapedSpace && (r == ' ' || r == '\t')
221 if buf.Len() == 0 {
222 inSpace = rSpace
223 } else if rSpace != inSpace {
224 flush()
225 inSpace = rSpace
226 }
227 if isEscapedSpace {
228 // Skip the backslash; emit the literal space as part of
229 // this non-whitespace token. normalisePathToken's
230 // unescapeBackslashes is now redundant for this path but
231 // stays in case some other source escapes other chars.
232 buf.WriteRune(runes[i+1])
233 i++
234 continue
235 }
236 buf.WriteRune(r)
237 }
238 flush()
239 return out
240}
241
242// normalisePathToken decides whether tk is a drag-dropped path and,
243// if so, returns the cleaned-up path string.
244func normalisePathToken(tk string) (string, bool) {
245 // Strip pre-existing surrounding quotes; we'll re-quote consistently.
246 if n := len(tk); n >= 2 {
247 if (tk[0] == '\'' && tk[n-1] == '\'') || (tk[0] == '"' && tk[n-1] == '"') {
248 tk = tk[1 : n-1]
249 }
250 }
251
252 // file:// URL form: decode and strip the scheme.
253 if strings.HasPrefix(tk, "file://") {
254 decoded, err := url.PathUnescape(strings.TrimPrefix(tk, "file://"))
255 if err != nil {
256 return "", false
257 }
258 if decoded == "" || decoded[0] != '/' {
259 return "", false
260 }
261 return decoded, true
262 }
263
264 // Looks-like-a-path heuristic: starts with /, ~, ~/. Must contain
265 // at least one path separator after the prefix (otherwise a bare
266 // "/" or "~" gets quoted, which is never what the user meant).
267 if !strings.HasPrefix(tk, "/") && !strings.HasPrefix(tk, "~") {
268 return "", false
269 }
270 if !strings.ContainsAny(tk[1:], "/.") {
271 return "", false
272 }
273 unescaped := unescapeBackslashes(tk)
274 if strings.ContainsAny(unescaped, "|;&$`<>") {
275 return "", false
276 }
277 return unescaped, true
278}
279
280// unescapeBackslashes turns "foo\ bar" into "foo bar". Terminal
281// drag-and-drop on macOS uses backslash escaping by default; quoting
282// makes that unnecessary.
283func unescapeBackslashes(s string) string {
284 if !strings.Contains(s, `\`) {
285 return s
286 }
287 var out strings.Builder
288 out.Grow(len(s))
289 prev := false
290 for _, r := range s {
291 if prev {
292 out.WriteRune(r)
293 prev = false
294 continue
295 }
296 if r == '\\' {
297 prev = true
298 continue
299 }
300 out.WriteRune(r)
301 }
302 if prev {
303 out.WriteRune('\\')
304 }
305 return out.String()
306}
307
308// singleQuote wraps s in single quotes, escaping any embedded single
309// quote using the splice idiom: 'foo'\”bar' for foo'bar.
310func singleQuote(s string) string {
311 return "'" + strings.ReplaceAll(s, "'", "'\\''") + "'"
312}
313
314// Insert places s into the editor at the cursor, splitting on
315// newlines so multi-line pastes preserve their structure.
316func (e *Editor) Insert(s string) { e.insert(s) }
317
318func (e *Editor) insert(s string) {
319 e.histIdx = -1
320 line := e.Lines[e.CursorR]
321 pre := substringBefore(line, e.CursorC)
322 post := substringAfter(line, e.CursorC)
323 // Split pasted text on newlines.
324 parts := strings.Split(s, "\n")
325 if len(parts) == 1 {
326 e.Lines[e.CursorR] = pre + s + post
327 e.CursorC += runeLen(s)
328 return
329 }
330 newLines := make([]string, 0, len(e.Lines)+len(parts)-1)
331 newLines = append(newLines, e.Lines[:e.CursorR]...)
332 newLines = append(newLines, pre+parts[0])
333 for i := 1; i < len(parts)-1; i++ {
334 newLines = append(newLines, parts[i])
335 }
336 last := parts[len(parts)-1]
337 newLines = append(newLines, last+post)
338 newLines = append(newLines, e.Lines[e.CursorR+1:]...)
339 e.Lines = newLines
340 e.CursorR += len(parts) - 1
341 e.CursorC = runeLen(last)
342}
343
344func (e *Editor) newline() {
345 e.histIdx = -1
346 line := e.Lines[e.CursorR]
347 pre := substringBefore(line, e.CursorC)
348 post := substringAfter(line, e.CursorC)
349 e.Lines[e.CursorR] = pre
350 e.Lines = append(e.Lines, "")
351 copy(e.Lines[e.CursorR+2:], e.Lines[e.CursorR+1:])
352 e.Lines[e.CursorR+1] = post
353 e.CursorR++
354 e.CursorC = 0
355}
356
357func (e *Editor) backspace() {
358 e.histIdx = -1
359 if e.CursorC == 0 {
360 if e.CursorR == 0 {
361 return
362 }
363 prev := e.Lines[e.CursorR-1]
364 cur := e.Lines[e.CursorR]
365 e.Lines = append(e.Lines[:e.CursorR], e.Lines[e.CursorR+1:]...)
366 e.CursorR--
367 e.CursorC = runeLen(prev)
368 e.Lines[e.CursorR] = prev + cur
369 return
370 }
371 line := e.Lines[e.CursorR]
372 e.Lines[e.CursorR] = substringBefore(line, e.CursorC-1) + substringAfter(line, e.CursorC)
373 e.CursorC--
374}
375
376func (e *Editor) delete() {
377 e.histIdx = -1
378 line := e.Lines[e.CursorR]
379 if e.CursorC == runeLen(line) {
380 if e.CursorR == len(e.Lines)-1 {
381 return
382 }
383 next := e.Lines[e.CursorR+1]
384 e.Lines = append(e.Lines[:e.CursorR+1], e.Lines[e.CursorR+2:]...)
385 e.Lines[e.CursorR] = line + next
386 return
387 }
388 e.Lines[e.CursorR] = substringBefore(line, e.CursorC) + substringAfter(line, e.CursorC+1)
389}
390
391// moveWordLeft jumps the cursor to the start of the previous word,
392// using the same word-separator rules as deleteWord. If already at the
393// start of the line, wraps to the end of the previous line.
394func (e *Editor) moveWordLeft() {
395 if e.CursorC == 0 {
396 if e.CursorR > 0 {
397 e.CursorR--
398 e.CursorC = runeLen(e.Lines[e.CursorR])
399 }
400 return
401 }
402 r := []rune(e.Lines[e.CursorR])
403 i := e.CursorC
404 for i > 0 && isWordSep(r[i-1]) {
405 i--
406 }
407 for i > 0 && !isWordSep(r[i-1]) {
408 i--
409 }
410 e.CursorC = i
411}
412
413// moveWordRight jumps the cursor to the start of the next word. If
414// already at the end of the line, wraps to the start of the next line.
415func (e *Editor) moveWordRight() {
416 line := e.Lines[e.CursorR]
417 if e.CursorC >= runeLen(line) {
418 if e.CursorR < len(e.Lines)-1 {
419 e.CursorR++
420 e.CursorC = 0
421 }
422 return
423 }
424 r := []rune(line)
425 i := e.CursorC
426 for i < len(r) && !isWordSep(r[i]) {
427 i++
428 }
429 for i < len(r) && isWordSep(r[i]) {
430 i++
431 }
432 e.CursorC = i
433}
434
435func (e *Editor) moveLeft() {
436 if e.CursorC > 0 {
437 e.CursorC--
438 return
439 }
440 if e.CursorR > 0 {
441 e.CursorR--
442 e.CursorC = runeLen(e.Lines[e.CursorR])
443 }
444}
445
446func (e *Editor) moveRight() {
447 if e.CursorC < runeLen(e.Lines[e.CursorR]) {
448 e.CursorC++
449 return
450 }
451 if e.CursorR < len(e.Lines)-1 {
452 e.CursorR++
453 e.CursorC = 0
454 }
455}
456
457func (e *Editor) deleteWord() {
458 e.histIdx = -1
459 line := e.Lines[e.CursorR]
460 if e.CursorC == 0 {
461 e.backspace()
462 return
463 }
464 r := []rune(line)
465 i := e.CursorC
466 // Step 1: walk over any trailing whitespace to the left of the cursor.
467 for i > 0 && isWordSep(r[i-1]) {
468 i--
469 }
470 // Step 2: walk over the word itself (non-separator runes).
471 for i > 0 && !isWordSep(r[i-1]) {
472 i--
473 }
474 e.Lines[e.CursorR] = string(r[:i]) + string(r[e.CursorC:])
475 e.CursorC = i
476}
477
478// isWordSep reports whether r is a word separator. Anything that isn't
479// a letter, digit, or underscore counts as a separator so delete-word
480// feels natural on paths, code, and prose.
481func isWordSep(r rune) bool {
482 switch {
483 case r == '_':
484 return false
485 case r >= '0' && r <= '9':
486 return false
487 case r >= 'a' && r <= 'z':
488 return false
489 case r >= 'A' && r <= 'Z':
490 return false
491 case r >= 0x80:
492 // Treat all non-ASCII as word chars so CJK/äöü don't get chopped.
493 return false
494 }
495 return true
496}
497
498// ---- history ----
499
500// PushHistory saves s to the history ring.
501func (e *Editor) PushHistory(s string) {
502 s = strings.TrimRight(s, "\n")
503 if s == "" {
504 return
505 }
506 if n := len(e.History); n > 0 && e.History[n-1] == s {
507 return
508 }
509 e.History = append(e.History, s)
510 if len(e.History) > 200 {
511 e.History = e.History[len(e.History)-200:]
512 }
513 e.histIdx = -1
514}
515
516func (e *Editor) historyPrev() {
517 if len(e.History) == 0 {
518 return
519 }
520 if e.histIdx == -1 {
521 e.savedDraft = e.Value()
522 e.histIdx = len(e.History) - 1
523 } else if e.histIdx > 0 {
524 e.histIdx--
525 }
526 e.SetValue(e.History[e.histIdx])
527 // SetValue resets histIdx; restore.
528 e.histIdx = e.findInHistory()
529}
530
531func (e *Editor) historyNext() {
532 if e.histIdx == -1 {
533 return
534 }
535 if e.histIdx == len(e.History)-1 {
536 e.histIdx = -1
537 e.SetValue(e.savedDraft)
538 e.histIdx = -1
539 return
540 }
541 e.histIdx++
542 e.SetValue(e.History[e.histIdx])
543 e.histIdx = e.findInHistory()
544}
545
546func (e *Editor) findInHistory() int {
547 v := e.Value()
548 for i, h := range e.History {
549 if h == v {
550 return i
551 }
552 }
553 return -1
554}
555
556// ---- rendering ----
557
558// Render returns the editor's visible lines (wrapped to width).
559// visualRow/visualCol describe where the cursor lands within the returned lines.
560func (e *Editor) Render(width int) (lines []string, visualRow, visualCol int) {
561 promptLen := visibleWidth(e.Prompt)
562 indent := strings.Repeat(" ", promptLen)
563
564 for r, line := range e.Lines {
565 var prefix string
566 if r == 0 {
567 prefix = e.Prompt
568 } else {
569 prefix = indent
570 }
571 wrapped := wrapLine(prefix+line, width, indent)
572 if r == e.CursorR {
573 // Compute where CursorC lands inside the wrapped rows by
574 // walking the wrapped output character-by-character and
575 // tracking which wrapped row + column corresponds to the
576 // cursor's rune index in `line`. This is the only reliable
577 // answer under word-wrap, where a simple (promptLen+col)/width
578 // formula overshoots when wrapLine broke on a space.
579 targetRunes := e.CursorC
580 row, col := locateCursor(wrapped, prefix, line, targetRunes, indent)
581 visualRow = len(lines) + row
582 visualCol = col
583 }
584 lines = append(lines, wrapped...)
585 }
586 return lines, visualRow, visualCol
587}
588
589// locateCursor finds the wrapped row + visible column corresponding to
590// `targetRunes` rune positions into the logical `line`, given that the
591// wrapped output `wrapped` started with `prefix` on its first row and
592// uses `cont` as the continuation indent on subsequent rows.
593func locateCursor(wrapped []string, prefix, line string, targetRunes int, cont string) (int, int) {
594 prefixW := visibleWidth(prefix)
595 contW := visibleWidth(cont)
596 lineRunes := []rune(line)
597 if targetRunes > len(lineRunes) {
598 targetRunes = len(lineRunes)
599 }
600
601 seenRunes := 0
602 for row, w := range wrapped {
603 // strip leading prefix / continuation indent before counting runes
604 body := w
605 var leadW int
606 if row == 0 {
607 if strings.HasPrefix(body, prefix) {
608 body = body[len(prefix):]
609 }
610 leadW = prefixW
611 } else {
612 if strings.HasPrefix(body, cont) {
613 body = body[len(cont):]
614 }
615 leadW = contW
616 }
617 bodyRunes := []rune(body)
618 // Could this wrapped row contain the cursor?
619 if targetRunes <= seenRunes+len(bodyRunes) {
620 // Column inside body.
621 inner := targetRunes - seenRunes
622 if inner < 0 {
623 inner = 0
624 }
625 col := leadW + runewidth.StringWidth(string(bodyRunes[:inner]))
626 return row, col
627 }
628 seenRunes += len(bodyRunes)
629 // Word-wrap may have dropped whitespace at the boundary; skip the
630 // corresponding runes in `line` so counts stay aligned.
631 for seenRunes < len(lineRunes) && lineRunes[seenRunes] == ' ' {
632 seenRunes++
633 if seenRunes >= targetRunes {
634 // Cursor landed in the skipped whitespace — place it at
635 // the start of the next wrapped row.
636 nextRow := row + 1
637 if nextRow >= len(wrapped) {
638 nextRow = row
639 }
640 return nextRow, contW
641 }
642 }
643 }
644 // Fallback: end of the last wrapped row.
645 if len(wrapped) == 0 {
646 return 0, prefixW
647 }
648 last := wrapped[len(wrapped)-1]
649 return len(wrapped) - 1, visibleWidth(last)
650}
651
652// ---- helpers ----
653
654func substringBefore(s string, col int) string {
655 r := []rune(s)
656 if col > len(r) {
657 col = len(r)
658 }
659 return string(r[:col])
660}
661
662func substringAfter(s string, col int) string {
663 r := []rune(s)
664 if col > len(r) {
665 col = len(r)
666 }
667 return string(r[col:])
668}
669
670func runeLen(s string) int { return len([]rune(s)) }
671
672func visualColumn(s string, runeCol int) int {
673 r := []rune(s)
674 if runeCol > len(r) {
675 runeCol = len(r)
676 }
677 return runewidth.StringWidth(string(r[:runeCol]))
678}
679
680func visibleWidth(s string) int {
681 return runewidth.StringWidth(stripANSI(s))
682}
683
684func stripANSI(s string) string {
685 // Minimal ANSI stripper; handles CSI sequences (ESC [ ... final).
686 var out []rune
687 i := 0
688 runes := []rune(s)
689 for i < len(runes) {
690 if runes[i] == 0x1b && i+1 < len(runes) && runes[i+1] == '[' {
691 i += 2
692 for i < len(runes) {
693 c := runes[i]
694 i++
695 if c >= 0x40 && c <= 0x7e {
696 break
697 }
698 }
699 continue
700 }
701 out = append(out, runes[i])
702 i++
703 }
704 return string(out)
705}
706
707func wrapLine(s string, width int, cont string) []string {
708 if width <= 0 {
709 return []string{s}
710 }
711
712 // Tokenize on spaces while preserving their widths. Runs of spaces
713 // stay attached to the preceding word so trailing whitespace doesn't
714 // create empty wrapped lines.
715 type token struct {
716 text string
717 width int
718 space bool // true if this token is whitespace only
719 }
720 var tokens []token
721 {
722 var buf strings.Builder
723 var bufW int
724 inSpace := false
725 flush := func() {
726 if buf.Len() == 0 {
727 return
728 }
729 tokens = append(tokens, token{text: buf.String(), width: bufW, space: inSpace})
730 buf.Reset()
731 bufW = 0
732 }
733 for _, r := range s {
734 rSpace := r == ' ' || r == '\t'
735 if buf.Len() == 0 {
736 inSpace = rSpace
737 } else if rSpace != inSpace {
738 flush()
739 inSpace = rSpace
740 }
741 buf.WriteRune(r)
742 bufW += runewidth.RuneWidth(r)
743 }
744 flush()
745 }
746
747 var out []string
748 var cur strings.Builder
749 curW := 0
750 firstLine := true
751 contW := visibleWidth(cont)
752
753 newLine := func() {
754 out = append(out, cur.String())
755 cur.Reset()
756 curW = 0
757 // After flushing, everything we append next is a CONTINUATION
758 // row, which must start with the cont indent so the editor's
759 // visual cursor alignment stays consistent. The old code only
760 // wrote cont when !firstLine BEFORE toggling firstLine, which
761 // meant the very first wrap never got its indent. That caused
762 // the terminal cursor to land in the wrong column after a
763 // multi-line paste.
764 firstLine = false
765 cur.WriteString(cont)
766 curW = contW
767 }
768
769 for i := 0; i < len(tokens); i++ {
770 tk := tokens[i]
771
772 // Drop leading whitespace after a wrap.
773 if tk.space && curW == contW && !firstLine {
774 continue
775 }
776
777 if curW+tk.width <= width {
778 cur.WriteString(tk.text)
779 curW += tk.width
780 continue
781 }
782
783 // Token overflows.
784 //
785 // If the token on its own is wider than what fits on a
786 // continuation line (width - contW), it'll need to be split
787 // rune-by-rune no matter what. In that case, don't break
788 // first: let the rune-split start right here at the current
789 // column and wrap naturally. Breaking first would strand the
790 // prompt (on firstLine) or the cont indent (on later lines)
791 // on a row by itself, which is the drag-drop-long-path bug.
792 if tk.width > width-contW {
793 // Rune-by-rune starting from the current column. newLine()
794 // inside handles wrapping and re-indents.
795 for _, r := range tk.text {
796 rw := runewidth.RuneWidth(r)
797 if curW+rw > width {
798 newLine()
799 }
800 cur.WriteRune(r)
801 curW += rw
802 }
803 continue
804 }
805
806 // Token fits on a fresh continuation line; break first, then
807 // emit it whole.
808 if cur.Len() > 0 && !(firstLine && curW == 0) {
809 trimmed := strings.TrimRight(cur.String(), " \t")
810 cur.Reset()
811 cur.WriteString(trimmed)
812 curW = visibleWidth(trimmed)
813 newLine()
814 if tk.space {
815 continue
816 }
817 }
818 cur.WriteString(tk.text)
819 curW += tk.width
820 }
821
822 if cur.Len() > 0 || len(out) == 0 {
823 out = append(out, cur.String())
824 }
825 return out
826}
827
828func max(a, b int) int {
829 if a > b {
830 return a
831 }
832 return b
833}