feat(editor): extend path tab-completion to /btw and /swarm editors
The shell-style path tab-completion added in the previous commit only ran against the main interactive editor; /btw and /swarm have their own *tui.Editor instances inside their dialogs and routed Tab through editor.HandleKey untouched, so '~/Dev'+Tab inserted nothing in those modes. Refactored the completion into a free helper: tryPathTabCompleteEditor(ed *tui.Editor, cwd string) bool The Interactive method is now a thin wrapper that adds frame invalidation. btwDialog gained a cwd field (set by Open) and calls the helper before forwarding to editor.HandleKey. swarmDialog already tracked d.cwd; both its prompt-editor and spawn-editor paths now run the helper before HandleKey, gated to skip when the @-picker popup is active so Tab there still selects the highlighted chip. Net effect: ~/Dev+Tab, ./int+Tab, cmd/+Tab, etc. now work the same way in the main composer, in /btw side chats, and in /swarm spawn and follow-up prompts.
- commit
- ad93054ac46e7c329d06afb01f74f6eae3a7f36d
- author
- patriceckhart · · 2026-05-19 16:50 UTC
- parent
- 8096aeb
- signature
- unsigned
- refs
- tag: v0.1.84
3 files · +67 -15
M internal/agent/modes/btw_dialog.go +18 -1
| @@ -57,6 +57,13 @@ type btwDialog struct { | |||
| 57 | 57 | ||
| 58 | 58 | // Theme cached so render() doesn't need it threaded through. | |
| 59 | 59 | theme tui.Theme | |
| 60 | + | ||
| 61 | + | // cwd is the working directory used to resolve relative paths | |
| 62 | + | // when the user presses Tab on a path-like token in the side- | |
| 63 | + | // chat editor. Set by Open() from the host's cwd so the same | |
| 64 | + | // path-completion that works in the main editor also works | |
| 65 | + | // here. | |
| 66 | + | cwd string | |
| 60 | 67 | } | |
| 61 | 68 | ||
| 62 | 69 | func newBtwDialog() *btwDialog { | |
| @@ -93,7 +100,7 @@ func (d *btwDialog) Loading() bool { | |||
| 93 | 100 | // auto-submitted (so /btw <text> starts a conversation right away). | |
| 94 | 101 | // invalidate, if non-nil, is called after each state change so the | |
| 95 | 102 | // host redraw loop can pick up the update without polling. | |
| 96 | - | func (d *btwDialog) Open(th tui.Theme, agent *core.Agent, system, model, seed string, invalidate func()) { | |
| 103 | + | func (d *btwDialog) Open(th tui.Theme, agent *core.Agent, system, model, cwd, seed string, invalidate func()) { | |
| 97 | 104 | d.mu.Lock() | |
| 98 | 105 | d.active = true | |
| 99 | 106 | d.theme = th | |
| @@ -105,6 +112,7 @@ func (d *btwDialog) Open(th tui.Theme, agent *core.Agent, system, model, seed st | |||
| 105 | 112 | d.frozenMsgs = agent.Messages() | |
| 106 | 113 | d.client = agent.Client | |
| 107 | 114 | d.model = model | |
| 115 | + | d.cwd = cwd | |
| 108 | 116 | d.mu.Unlock() | |
| 109 | 117 | ||
| 110 | 118 | if seed = strings.TrimSpace(seed); seed != "" { | |
| @@ -157,10 +165,19 @@ func (d *btwDialog) HandleKey(k tui.Key, invalidate func()) (closed bool) { | |||
| 157 | 165 | d.mu.Lock() | |
| 158 | 166 | editor := d.editor | |
| 159 | 167 | loading := d.loading | |
| 168 | + | cwd := d.cwd | |
| 160 | 169 | d.mu.Unlock() | |
| 161 | 170 | if editor == nil { | |
| 162 | 171 | return false | |
| 163 | 172 | } | |
| 173 | + | // Tab-complete a path-like token before the editor sees the key, | |
| 174 | + | // matching the main editor's behaviour. | |
| 175 | + | if k.Kind == tui.KeyTab { | |
| 176 | + | if tryPathTabCompleteEditor(editor, cwd) { | |
| 177 | + | invalidate() | |
| 178 | + | return false | |
| 179 | + | } | |
| 180 | + | } | |
| 164 | 181 | // Don't accept new submissions while a call is in flight; arrow | |
| 165 | 182 | // keys / scrolling still flow through to the editor for caret | |
| 166 | 183 | // movement and history. | |
| @@ -57,6 +57,13 @@ type btwDialog struct { | |||||
| 57 | 57 | ||||
| 58 | // Theme cached so render() doesn't need it threaded through. | 58 | // Theme cached so render() doesn't need it threaded through. | ||
| 59 | theme tui.Theme | 59 | theme tui.Theme | ||
| 60 | + | ||||
| 61 | + | // cwd is the working directory used to resolve relative paths | |||
| 62 | + | // when the user presses Tab on a path-like token in the side- | |||
| 63 | + | // chat editor. Set by Open() from the host's cwd so the same | |||
| 64 | + | // path-completion that works in the main editor also works | |||
| 65 | + | // here. | |||
| 66 | + | cwd string | |||
| 60 | } | 67 | } | ||
| 61 | 68 | ||||
| 62 | func newBtwDialog() *btwDialog { | 69 | func newBtwDialog() *btwDialog { | ||
| @@ -93,7 +100,7 @@ func (d *btwDialog) Loading() bool { | |||||
| 93 | // auto-submitted (so /btw <text> starts a conversation right away). | 100 | // auto-submitted (so /btw <text> starts a conversation right away). | ||
| 94 | // invalidate, if non-nil, is called after each state change so the | 101 | // invalidate, if non-nil, is called after each state change so the | ||
| 95 | // host redraw loop can pick up the update without polling. | 102 | // host redraw loop can pick up the update without polling. | ||
| 96 | - | func (d *btwDialog) Open(th tui.Theme, agent *core.Agent, system, model, seed string, invalidate func()) { | 103 | + | func (d *btwDialog) Open(th tui.Theme, agent *core.Agent, system, model, cwd, seed string, invalidate func()) { |
| 97 | d.mu.Lock() | 104 | d.mu.Lock() | ||
| 98 | d.active = true | 105 | d.active = true | ||
| 99 | d.theme = th | 106 | d.theme = th | ||
| @@ -105,6 +112,7 @@ func (d *btwDialog) Open(th tui.Theme, agent *core.Agent, system, model, seed st | |||||
| 105 | d.frozenMsgs = agent.Messages() | 112 | d.frozenMsgs = agent.Messages() | ||
| 106 | d.client = agent.Client | 113 | d.client = agent.Client | ||
| 107 | d.model = model | 114 | d.model = model | ||
| 115 | + | d.cwd = cwd | |||
| 108 | d.mu.Unlock() | 116 | d.mu.Unlock() | ||
| 109 | 117 | ||||
| 110 | if seed = strings.TrimSpace(seed); seed != "" { | 118 | if seed = strings.TrimSpace(seed); seed != "" { | ||
| @@ -157,10 +165,19 @@ func (d *btwDialog) HandleKey(k tui.Key, invalidate func()) (closed bool) { | |||||
| 157 | d.mu.Lock() | 165 | d.mu.Lock() | ||
| 158 | editor := d.editor | 166 | editor := d.editor | ||
| 159 | loading := d.loading | 167 | loading := d.loading | ||
| 168 | + | cwd := d.cwd | |||
| 160 | d.mu.Unlock() | 169 | d.mu.Unlock() | ||
| 161 | if editor == nil { | 170 | if editor == nil { | ||
| 162 | return false | 171 | return false | ||
| 163 | } | 172 | } | ||
| 173 | + | // Tab-complete a path-like token before the editor sees the key, | |||
| 174 | + | // matching the main editor's behaviour. | |||
| 175 | + | if k.Kind == tui.KeyTab { | |||
| 176 | + | if tryPathTabCompleteEditor(editor, cwd) { | |||
| 177 | + | invalidate() | |||
| 178 | + | return false | |||
| 179 | + | } | |||
| 180 | + | } | |||
| 164 | // Don't accept new submissions while a call is in flight; arrow | 181 | // Don't accept new submissions while a call is in flight; arrow | ||
| 165 | // keys / scrolling still flow through to the editor for caret | 182 | // keys / scrolling still flow through to the editor for caret | ||
| 166 | // movement and history. | 183 | // movement and history. | ||
M internal/agent/modes/interactive.go +31 -14
| @@ -2412,27 +2412,34 @@ func buildStudyPrompt(arg, cwd string) string { | |||
| 2412 | 2412 | return "Read and understand everything in the directory " + display + "." | |
| 2413 | 2413 | } | |
| 2414 | 2414 | ||
| 2415 | - | // tryPathTabComplete looks at the editor's current value, finds the | |
| 2416 | - | // path-like token immediately before the cursor (in this codebase the | |
| 2417 | - | // cursor is always at the end of the buffer after a keystroke, so | |
| 2418 | - | // "before the cursor" is the trailing non-whitespace run), and rewrites | |
| 2419 | - | // it to its shell-style completion against the filesystem. | |
| 2415 | + | // tryPathTabCompleteEditor looks at ed's current value, finds the | |
| 2416 | + | // path-like token immediately before the cursor (the cursor is always | |
| 2417 | + | // at the end of the buffer after a keystroke, so "before the cursor" | |
| 2418 | + | // is the trailing non-whitespace run), and rewrites it to its shell- | |
| 2419 | + | // style completion against the filesystem. | |
| 2420 | 2420 | // | |
| 2421 | 2421 | // Returns true when it consumed the Tab keystroke (token recognised, | |
| 2422 | 2422 | // completion attempted — even if no candidates matched, the keystroke | |
| 2423 | 2423 | // is still consumed so it doesn't insert a literal tab character). | |
| 2424 | - | // Returns false when the token doesn't look like a path; the caller | |
| 2425 | - | // then lets Tab fall through to its normal no-op. | |
| 2424 | + | // Returns false when the token doesn't look like a path; callers then | |
| 2425 | + | // let Tab fall through to its normal no-op. | |
| 2426 | 2426 | // | |
| 2427 | 2427 | // Recognised path shapes: | |
| 2428 | 2428 | // - ~ or ~/foo expanded via os.UserHomeDir() | |
| 2429 | 2429 | // - /abs/path or /abs/path/foo absolute | |
| 2430 | - | // - ./foo, ../foo, foo/bar relative to i.cfg.CWD | |
| 2430 | + | // - ./foo, ../foo, foo/bar relative to cwd | |
| 2431 | 2431 | // | |
| 2432 | 2432 | // A bare word like "hello" is not treated as a path so plain text | |
| 2433 | 2433 | // keeps Tab as a literal no-op. | |
| 2434 | - | func (i *Interactive) tryPathTabComplete() bool { | |
| 2435 | - | val := i.ed.Value() | |
| 2434 | + | // | |
| 2435 | + | // Free function (not a method) so the same logic runs against the | |
| 2436 | + | // editor instances owned by btwDialog and swarmDialog without each | |
| 2437 | + | // dialog needing its own copy. | |
| 2438 | + | func tryPathTabCompleteEditor(ed *tui.Editor, cwd string) bool { | |
| 2439 | + | if ed == nil { | |
| 2440 | + | return false | |
| 2441 | + | } | |
| 2442 | + | val := ed.Value() | |
| 2436 | 2443 | // Find the trailing run of non-whitespace. | |
| 2437 | 2444 | start := len(val) | |
| 2438 | 2445 | for start > 0 { | |
| @@ -2451,7 +2458,7 @@ func (i *Interactive) tryPathTabComplete() bool { | |||
| 2451 | 2458 | } | |
| 2452 | 2459 | ||
| 2453 | 2460 | // Resolve the absolute parent directory + base prefix to match. | |
| 2454 | - | parentAbs, basePrefix, displayParent, ok := resolvePathTabToken(token, i.cfg.CWD) | |
| 2461 | + | parentAbs, basePrefix, displayParent, ok := resolvePathTabToken(token, cwd) | |
| 2455 | 2462 | if !ok { | |
| 2456 | 2463 | return true | |
| 2457 | 2464 | } | |
| @@ -2498,11 +2505,21 @@ func (i *Interactive) tryPathTabComplete() bool { | |||
| 2498 | 2505 | newToken += "/" | |
| 2499 | 2506 | } | |
| 2500 | 2507 | ||
| 2501 | - | i.ed.SetValue(val[:start] + newToken) | |
| 2502 | - | i.invalidate() | |
| 2508 | + | ed.SetValue(val[:start] + newToken) | |
| 2503 | 2509 | return true | |
| 2504 | 2510 | } | |
| 2505 | 2511 | ||
| 2512 | + | // tryPathTabComplete is the Interactive-bound convenience wrapper. | |
| 2513 | + | // It calls the free helper against the main editor and invalidates | |
| 2514 | + | // the frame on a successful rewrite. | |
| 2515 | + | func (i *Interactive) tryPathTabComplete() bool { | |
| 2516 | + | if tryPathTabCompleteEditor(i.ed, i.cfg.CWD) { | |
| 2517 | + | i.invalidate() | |
| 2518 | + | return true | |
| 2519 | + | } | |
| 2520 | + | return false | |
| 2521 | + | } | |
| 2522 | + | ||
| 2506 | 2523 | // looksLikePathToken reports whether tok is shaped like a filesystem | |
| 2507 | 2524 | // path. Paths must either start with ~, /, ./, ../ or contain a /. | |
| 2508 | 2525 | // Plain words are excluded so Tab on "hello" stays a no-op. | |
| @@ -2990,7 +3007,7 @@ func (i *Interactive) openBtwDialog(args []string) { | |||
| 2990 | 3007 | return | |
| 2991 | 3008 | } | |
| 2992 | 3009 | seed := strings.TrimSpace(strings.Join(args, " ")) | |
| 2993 | - | i.btwDialog.Open(i.cfg.Theme, i.agent, i.agent.System, i.cfg.Model, seed, i.invalidate) | |
| 3010 | + | i.btwDialog.Open(i.cfg.Theme, i.agent, i.agent.System, i.cfg.Model, i.cfg.CWD, seed, i.invalidate) | |
| 2994 | 3011 | i.invalidate() | |
| 2995 | 3012 | } | |
| 2996 | 3013 | ||
| @@ -2412,27 +2412,34 @@ func buildStudyPrompt(arg, cwd string) string { | |||||
| 2412 | return "Read and understand everything in the directory " + display + "." | 2412 | return "Read and understand everything in the directory " + display + "." | ||
| 2413 | } | 2413 | } | ||
| 2414 | 2414 | ||||
| 2415 | - | // tryPathTabComplete looks at the editor's current value, finds the | 2415 | + | // tryPathTabCompleteEditor looks at ed's current value, finds the |
| 2416 | - | // path-like token immediately before the cursor (in this codebase the | 2416 | + | // path-like token immediately before the cursor (the cursor is always |
| 2417 | - | // cursor is always at the end of the buffer after a keystroke, so | 2417 | + | // at the end of the buffer after a keystroke, so "before the cursor" |
| 2418 | - | // "before the cursor" is the trailing non-whitespace run), and rewrites | 2418 | + | // is the trailing non-whitespace run), and rewrites it to its shell- |
| 2419 | - | // it to its shell-style completion against the filesystem. | 2419 | + | // style completion against the filesystem. |
| 2420 | // | 2420 | // | ||
| 2421 | // Returns true when it consumed the Tab keystroke (token recognised, | 2421 | // Returns true when it consumed the Tab keystroke (token recognised, | ||
| 2422 | // completion attempted — even if no candidates matched, the keystroke | 2422 | // completion attempted — even if no candidates matched, the keystroke | ||
| 2423 | // is still consumed so it doesn't insert a literal tab character). | 2423 | // is still consumed so it doesn't insert a literal tab character). | ||
| 2424 | - | // Returns false when the token doesn't look like a path; the caller | 2424 | + | // Returns false when the token doesn't look like a path; callers then |
| 2425 | - | // then lets Tab fall through to its normal no-op. | 2425 | + | // let Tab fall through to its normal no-op. |
| 2426 | // | 2426 | // | ||
| 2427 | // Recognised path shapes: | 2427 | // Recognised path shapes: | ||
| 2428 | // - ~ or ~/foo expanded via os.UserHomeDir() | 2428 | // - ~ or ~/foo expanded via os.UserHomeDir() | ||
| 2429 | // - /abs/path or /abs/path/foo absolute | 2429 | // - /abs/path or /abs/path/foo absolute | ||
| 2430 | - | // - ./foo, ../foo, foo/bar relative to i.cfg.CWD | 2430 | + | // - ./foo, ../foo, foo/bar relative to cwd |
| 2431 | // | 2431 | // | ||
| 2432 | // A bare word like "hello" is not treated as a path so plain text | 2432 | // A bare word like "hello" is not treated as a path so plain text | ||
| 2433 | // keeps Tab as a literal no-op. | 2433 | // keeps Tab as a literal no-op. | ||
| 2434 | - | func (i *Interactive) tryPathTabComplete() bool { | 2434 | + | // |
| 2435 | - | val := i.ed.Value() | 2435 | + | // Free function (not a method) so the same logic runs against the |
| 2436 | + | // editor instances owned by btwDialog and swarmDialog without each | |||
| 2437 | + | // dialog needing its own copy. | |||
| 2438 | + | func tryPathTabCompleteEditor(ed *tui.Editor, cwd string) bool { | |||
| 2439 | + | if ed == nil { | |||
| 2440 | + | return false | |||
| 2441 | + | } | |||
| 2442 | + | val := ed.Value() | |||
| 2436 | // Find the trailing run of non-whitespace. | 2443 | // Find the trailing run of non-whitespace. | ||
| 2437 | start := len(val) | 2444 | start := len(val) | ||
| 2438 | for start > 0 { | 2445 | for start > 0 { | ||
| @@ -2451,7 +2458,7 @@ func (i *Interactive) tryPathTabComplete() bool { | |||||
| 2451 | } | 2458 | } | ||
| 2452 | 2459 | ||||
| 2453 | // Resolve the absolute parent directory + base prefix to match. | 2460 | // Resolve the absolute parent directory + base prefix to match. | ||
| 2454 | - | parentAbs, basePrefix, displayParent, ok := resolvePathTabToken(token, i.cfg.CWD) | 2461 | + | parentAbs, basePrefix, displayParent, ok := resolvePathTabToken(token, cwd) |
| 2455 | if !ok { | 2462 | if !ok { | ||
| 2456 | return true | 2463 | return true | ||
| 2457 | } | 2464 | } | ||
| @@ -2498,11 +2505,21 @@ func (i *Interactive) tryPathTabComplete() bool { | |||||
| 2498 | newToken += "/" | 2505 | newToken += "/" | ||
| 2499 | } | 2506 | } | ||
| 2500 | 2507 | ||||
| 2501 | - | i.ed.SetValue(val[:start] + newToken) | 2508 | + | ed.SetValue(val[:start] + newToken) |
| 2502 | - | i.invalidate() | |||
| 2503 | return true | 2509 | return true | ||
| 2504 | } | 2510 | } | ||
| 2505 | 2511 | ||||
| 2512 | + | // tryPathTabComplete is the Interactive-bound convenience wrapper. | |||
| 2513 | + | // It calls the free helper against the main editor and invalidates | |||
| 2514 | + | // the frame on a successful rewrite. | |||
| 2515 | + | func (i *Interactive) tryPathTabComplete() bool { | |||
| 2516 | + | if tryPathTabCompleteEditor(i.ed, i.cfg.CWD) { | |||
| 2517 | + | i.invalidate() | |||
| 2518 | + | return true | |||
| 2519 | + | } | |||
| 2520 | + | return false | |||
| 2521 | + | } | |||
| 2522 | + | ||||
| 2506 | // looksLikePathToken reports whether tok is shaped like a filesystem | 2523 | // looksLikePathToken reports whether tok is shaped like a filesystem | ||
| 2507 | // path. Paths must either start with ~, /, ./, ../ or contain a /. | 2524 | // path. Paths must either start with ~, /, ./, ../ or contain a /. | ||
| 2508 | // Plain words are excluded so Tab on "hello" stays a no-op. | 2525 | // Plain words are excluded so Tab on "hello" stays a no-op. | ||
| @@ -2990,7 +3007,7 @@ func (i *Interactive) openBtwDialog(args []string) { | |||||
| 2990 | return | 3007 | return | ||
| 2991 | } | 3008 | } | ||
| 2992 | seed := strings.TrimSpace(strings.Join(args, " ")) | 3009 | seed := strings.TrimSpace(strings.Join(args, " ")) | ||
| 2993 | - | i.btwDialog.Open(i.cfg.Theme, i.agent, i.agent.System, i.cfg.Model, seed, i.invalidate) | 3010 | + | i.btwDialog.Open(i.cfg.Theme, i.agent, i.agent.System, i.cfg.Model, i.cfg.CWD, seed, i.invalidate) |
| 2994 | i.invalidate() | 3011 | i.invalidate() | ||
| 2995 | } | 3012 | } | ||
| 2996 | 3013 | ||||
M internal/agent/modes/swarm_dialog.go +18 -0
| @@ -766,6 +766,15 @@ func (d *swarmDialog) handlePromptKey(k tui.Key) (closed bool, msg, errMsg strin | |||
| 766 | 766 | return false, "", "" | |
| 767 | 767 | } | |
| 768 | 768 | ||
| 769 | + | // Tab-complete a path-like token before the editor sees the key, | |
| 770 | + | // matching the main editor's behaviour. Skipped above by the | |
| 771 | + | // @-picker branch when its popup is active. | |
| 772 | + | if k.Kind == tui.KeyTab { | |
| 773 | + | if tryPathTabCompleteEditor(ed, d.cwd) { | |
| 774 | + | return false, "", "" | |
| 775 | + | } | |
| 776 | + | } | |
| 777 | + | ||
| 769 | 778 | if submitted := ed.HandleKey(k); submitted { | |
| 770 | 779 | text := strings.TrimRight(ed.SubmitValue(), "\n") | |
| 771 | 780 | text = expandFileChips(text, d.cwd) | |
| @@ -886,6 +895,15 @@ func (d *swarmDialog) handleSpawnKey(k tui.Key) (closed bool, msg, errMsg string | |||
| 886 | 895 | return false, "", "" | |
| 887 | 896 | } | |
| 888 | 897 | ||
| 898 | + | // Tab-complete a path-like token before the editor sees the key, | |
| 899 | + | // matching the main editor's behaviour. Skipped above by the | |
| 900 | + | // @-picker branch when its popup is active. | |
| 901 | + | if k.Kind == tui.KeyTab { | |
| 902 | + | if tryPathTabCompleteEditor(ed, d.cwd) { | |
| 903 | + | return false, "", "" | |
| 904 | + | } | |
| 905 | + | } | |
| 906 | + | ||
| 889 | 907 | if submitted := ed.HandleKey(k); submitted { | |
| 890 | 908 | raw := strings.TrimRight(ed.SubmitValue(), "\n") | |
| 891 | 909 | ||
| @@ -766,6 +766,15 @@ func (d *swarmDialog) handlePromptKey(k tui.Key) (closed bool, msg, errMsg strin | |||||
| 766 | return false, "", "" | 766 | return false, "", "" | ||
| 767 | } | 767 | } | ||
| 768 | 768 | ||||
| 769 | + | // Tab-complete a path-like token before the editor sees the key, | |||
| 770 | + | // matching the main editor's behaviour. Skipped above by the | |||
| 771 | + | // @-picker branch when its popup is active. | |||
| 772 | + | if k.Kind == tui.KeyTab { | |||
| 773 | + | if tryPathTabCompleteEditor(ed, d.cwd) { | |||
| 774 | + | return false, "", "" | |||
| 775 | + | } | |||
| 776 | + | } | |||
| 777 | + | ||||
| 769 | if submitted := ed.HandleKey(k); submitted { | 778 | if submitted := ed.HandleKey(k); submitted { | ||
| 770 | text := strings.TrimRight(ed.SubmitValue(), "\n") | 779 | text := strings.TrimRight(ed.SubmitValue(), "\n") | ||
| 771 | text = expandFileChips(text, d.cwd) | 780 | text = expandFileChips(text, d.cwd) | ||
| @@ -886,6 +895,15 @@ func (d *swarmDialog) handleSpawnKey(k tui.Key) (closed bool, msg, errMsg string | |||||
| 886 | return false, "", "" | 895 | return false, "", "" | ||
| 887 | } | 896 | } | ||
| 888 | 897 | ||||
| 898 | + | // Tab-complete a path-like token before the editor sees the key, | |||
| 899 | + | // matching the main editor's behaviour. Skipped above by the | |||
| 900 | + | // @-picker branch when its popup is active. | |||
| 901 | + | if k.Kind == tui.KeyTab { | |||
| 902 | + | if tryPathTabCompleteEditor(ed, d.cwd) { | |||
| 903 | + | return false, "", "" | |||
| 904 | + | } | |||
| 905 | + | } | |||
| 906 | + | ||||
| 889 | if submitted := ed.HandleKey(k); submitted { | 907 | if submitted := ed.HandleKey(k); submitted { | ||
| 890 | raw := strings.TrimRight(ed.SubmitValue(), "\n") | 908 | raw := strings.TrimRight(ed.SubmitValue(), "\n") | ||
| 891 | 909 | ||||