1package editor
2
3import (
4 "context"
5 "fmt"
6 "io/fs"
7 "log/slog"
8 "math/rand"
9 "net/http"
10 "os"
11 "os/exec"
12 "path/filepath"
13 "runtime"
14 "slices"
15 "strings"
16 "unicode"
17
18 "github.com/charmbracelet/bubbles/v2/key"
19 "github.com/charmbracelet/bubbles/v2/textarea"
20 tea "github.com/charmbracelet/bubbletea/v2"
21 "github.com/charmbracelet/crush/internal/app"
22 "github.com/charmbracelet/crush/internal/config"
23 "github.com/charmbracelet/crush/internal/fsext"
24 "github.com/charmbracelet/crush/internal/message"
25 "github.com/charmbracelet/crush/internal/session"
26 "github.com/charmbracelet/crush/internal/tui/components/chat"
27 "github.com/charmbracelet/crush/internal/tui/components/completions"
28 "github.com/charmbracelet/crush/internal/tui/components/core/layout"
29 "github.com/charmbracelet/crush/internal/tui/components/dialogs"
30 "github.com/charmbracelet/crush/internal/tui/components/dialogs/commands"
31 "github.com/charmbracelet/crush/internal/tui/components/dialogs/filepicker"
32 "github.com/charmbracelet/crush/internal/tui/components/dialogs/quit"
33 "github.com/charmbracelet/crush/internal/tui/styles"
34 "github.com/charmbracelet/crush/internal/tui/util"
35 "github.com/charmbracelet/lipgloss/v2"
36)
37
38type Editor interface {
39 util.Model
40 layout.Sizeable
41 layout.Focusable
42 layout.Help
43 layout.Positional
44
45 SetSession(session session.Session) tea.Cmd
46 IsCompletionsOpen() bool
47 HasAttachments() bool
48 Cursor() *tea.Cursor
49}
50
51type FileCompletionItem struct {
52 Path string // The file path
53}
54
55type editorCmp struct {
56 width int
57 height int
58 x, y int
59 app *app.App
60 session session.Session
61 textarea *textarea.Model
62 attachments []message.Attachment
63 deleteMode bool
64 readyPlaceholder string
65 workingPlaceholder string
66
67 keyMap EditorKeyMap
68
69 // injected file dir lister
70 listDirResolver fsext.DirectoryListerResolver
71
72 // File path completions
73 currentQuery string
74 completionsStartIndex int
75 isCompletionsOpen bool
76
77 previouslyScrollingPromptHistory bool
78 scrollingPromptHistory bool
79 promptHistoryIndex int
80}
81
82var DeleteKeyMaps = DeleteAttachmentKeyMaps{
83 AttachmentDeleteMode: key.NewBinding(
84 key.WithKeys("ctrl+r"),
85 key.WithHelp("ctrl+r+{i}", "delete attachment at index i"),
86 ),
87 Escape: key.NewBinding(
88 key.WithKeys("esc", "alt+esc"),
89 key.WithHelp("esc", "cancel delete mode"),
90 ),
91 DeleteAllAttachments: key.NewBinding(
92 key.WithKeys("r"),
93 key.WithHelp("ctrl+r+r", "delete all attachments"),
94 ),
95}
96
97const (
98 maxAttachments = 5
99)
100
101type OpenEditorMsg struct {
102 Text string
103}
104
105func (m *editorCmp) openEditor(value string) tea.Cmd {
106 editor := os.Getenv("EDITOR")
107 if editor == "" {
108 // Use platform-appropriate default editor
109 if runtime.GOOS == "windows" {
110 editor = "notepad"
111 } else {
112 editor = "nvim"
113 }
114 }
115
116 tmpfile, err := os.CreateTemp("", "msg_*.md")
117 if err != nil {
118 return util.ReportError(err)
119 }
120 defer tmpfile.Close() //nolint:errcheck
121 if _, err := tmpfile.WriteString(value); err != nil {
122 return util.ReportError(err)
123 }
124 c := exec.CommandContext(context.TODO(), editor, tmpfile.Name())
125 c.Stdin = os.Stdin
126 c.Stdout = os.Stdout
127 c.Stderr = os.Stderr
128 return tea.ExecProcess(c, func(err error) tea.Msg {
129 if err != nil {
130 return util.ReportError(err)
131 }
132 content, err := os.ReadFile(tmpfile.Name())
133 if err != nil {
134 return util.ReportError(err)
135 }
136 if len(content) == 0 {
137 return util.ReportWarn("Message is empty")
138 }
139 os.Remove(tmpfile.Name())
140 return OpenEditorMsg{
141 Text: strings.TrimSpace(string(content)),
142 }
143 })
144}
145
146func (m *editorCmp) Init() tea.Cmd {
147 return nil
148}
149
150func (m *editorCmp) send() tea.Cmd {
151 value := m.textarea.Value()
152 value = strings.TrimSpace(value)
153
154 switch value {
155 case "exit", "quit":
156 m.textarea.Reset()
157 return util.CmdHandler(dialogs.OpenDialogMsg{Model: quit.NewQuitDialog()})
158 }
159
160 m.textarea.Reset()
161 attachments := m.attachments
162
163 m.attachments = nil
164 if value == "" {
165 return nil
166 }
167
168 // Change the placeholder when sending a new message.
169 m.randomizePlaceholders()
170
171 return tea.Batch(
172 util.CmdHandler(chat.SendMsg{
173 Text: value,
174 Attachments: attachments,
175 }),
176 )
177}
178
179func (m *editorCmp) repositionCompletions() tea.Msg {
180 x, y := m.completionsPosition()
181 return completions.RepositionCompletionsMsg{X: x, Y: y}
182}
183
184func onCompletionItemSelect(fsys fs.FS, activeModelHasImageSupport func() (bool, string), item FileCompletionItem, insert bool, m *editorCmp) (tea.Model, tea.Cmd) {
185 var cmd tea.Cmd
186 path := item.Path
187 // check if item is an image
188 if isExtOfAllowedImageType(path) {
189 if imagesSupported, modelName := activeModelHasImageSupport(); !imagesSupported {
190 // TODO(tauraamui): consolidate this kind of standard image attachment related warning
191 return m, util.ReportWarn("File attachments are not supported by the current model: " + modelName)
192 }
193 slog.Debug("checking if image is too big", path, 1)
194 tooBig, _ := filepicker.IsFileTooBigWithFS(os.DirFS(filepath.Dir(path)), path, filepicker.MaxAttachmentSize)
195 if tooBig {
196 return m, nil
197 }
198
199 content, err := fs.ReadFile(fsys, path)
200 if err != nil {
201 return m, nil
202 }
203 mimeBufferSize := min(512, len(content))
204 mimeType := http.DetectContentType(content[:mimeBufferSize])
205 fileName := filepath.Base(path)
206 attachment := message.Attachment{FilePath: path, FileName: fileName, MimeType: mimeType, Content: content}
207 cmd = util.CmdHandler(filepicker.FilePickedMsg{
208 Attachment: attachment,
209 })
210 }
211
212 word := m.textarea.Word()
213 // If the selected item is a file, insert its path into the textarea
214 originalValue := m.textarea.Value()
215 newValue := originalValue[:m.completionsStartIndex] // Remove the current query
216 if cmd == nil {
217 newValue += path // insert the file path for non-images
218 }
219 newValue += originalValue[m.completionsStartIndex+len(word):] // Append the rest of the value
220 // XXX: This will always move the cursor to the end of the textarea.
221 m.textarea.SetValue(newValue)
222 m.textarea.MoveToEnd()
223 if !insert {
224 m.isCompletionsOpen = false
225 m.currentQuery = ""
226 m.completionsStartIndex = 0
227 }
228
229 return m, cmd
230}
231
232func isExtOfAllowedImageType(path string) bool {
233 isAllowedType := false
234 // TODO(tauraamui) [17/09/2025]: this needs to be combined with the actual data inference/checking
235 // of the contents that happens when we resolve the "mime" type
236 for _, ext := range filepicker.AllowedTypes {
237 if strings.HasSuffix(path, ext) {
238 isAllowedType = true
239 break
240 }
241 }
242 return isAllowedType
243}
244
245type ResolveAbs func(path string) (string, error)
246
247func onPaste(msg tea.PasteMsg) tea.Msg {
248 return filepicker.OnPaste(filepicker.ResolveFS, string(msg))
249}
250
251func activeModelHasImageSupport() (bool, string) {
252 agentCfg := config.Get().Agents["coder"]
253 model := config.Get().GetModelByType(agentCfg.Model)
254 return model.SupportsImages, model.Name
255}
256
257func (m *editorCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
258 var cmd tea.Cmd
259 var cmds []tea.Cmd
260 switch msg := msg.(type) {
261 case tea.WindowSizeMsg:
262 return m, m.repositionCompletions
263 case filepicker.FilePickedMsg:
264 if len(m.attachments) >= maxAttachments {
265 return m, util.ReportError(fmt.Errorf("cannot add more than %d images", maxAttachments))
266 }
267 m.attachments = append(m.attachments, msg.Attachment)
268 return m, nil
269 case completions.CompletionsOpenedMsg:
270 m.isCompletionsOpen = true
271 case completions.CompletionsClosedMsg:
272 m.isCompletionsOpen = false
273 m.currentQuery = ""
274 m.completionsStartIndex = 0
275 case completions.SelectCompletionMsg:
276 if !m.isCompletionsOpen {
277 return m, nil
278 }
279 if item, ok := msg.Value.(FileCompletionItem); ok {
280 return onCompletionItemSelect(os.DirFS("."), activeModelHasImageSupport, item, msg.Insert, m)
281 }
282 case commands.OpenExternalEditorMsg:
283 if m.app.CoderAgent.IsSessionBusy(m.session.ID) {
284 return m, util.ReportWarn("Agent is working, please wait...")
285 }
286 return m, m.openEditor(m.textarea.Value())
287 case OpenEditorMsg:
288 m.textarea.SetValue(msg.Text)
289 m.textarea.MoveToEnd()
290 case tea.PasteMsg:
291 agentCfg := config.Get().Agents["coder"]
292 model := config.Get().GetModelByType(agentCfg.Model)
293 if !model.SupportsImages {
294 return m, util.ReportWarn("File attachments are not supported by the current model: " + model.Name)
295 }
296 return m, util.CmdHandler(onPaste(msg)) // inject fsys accessible from PWD
297 case commands.ToggleYoloModeMsg:
298 m.setEditorPrompt()
299 return m, nil
300 case tea.KeyPressMsg:
301 cur := m.textarea.Cursor()
302 curIdx := m.textarea.Width()*cur.Y + cur.X
303 switch {
304 // Completions
305 case msg.String() == "/" && !m.isCompletionsOpen &&
306 // only show if beginning of prompt, or if previous char is a space or newline:
307 (len(m.textarea.Value()) == 0 || unicode.IsSpace(rune(m.textarea.Value()[len(m.textarea.Value())-1]))):
308 m.isCompletionsOpen = true
309 m.currentQuery = ""
310 m.completionsStartIndex = curIdx
311
312 cmds = append(cmds, m.startCompletions())
313 case m.isCompletionsOpen && curIdx <= m.completionsStartIndex:
314 cmds = append(cmds, util.CmdHandler(completions.CloseCompletionsMsg{}))
315 }
316 if key.Matches(msg, DeleteKeyMaps.AttachmentDeleteMode) {
317 m.deleteMode = true
318 return m, nil
319 }
320 if key.Matches(msg, DeleteKeyMaps.DeleteAllAttachments) && m.deleteMode {
321 m.deleteMode = false
322 m.attachments = nil
323 return m, nil
324 }
325 rune := msg.Code
326 if m.deleteMode && unicode.IsDigit(rune) {
327 num := int(rune - '0')
328 m.deleteMode = false
329 if num < 10 && len(m.attachments) > num {
330 if num == 0 {
331 m.attachments = m.attachments[num+1:]
332 } else {
333 m.attachments = slices.Delete(m.attachments, num, num+1)
334 }
335 return m, nil
336 }
337 }
338 if key.Matches(msg, m.keyMap.OpenEditor) {
339 if m.app.CoderAgent.IsSessionBusy(m.session.ID) {
340 return m, util.ReportWarn("Agent is working, please wait...")
341 }
342 return m, m.openEditor(m.textarea.Value())
343 }
344 if key.Matches(msg, DeleteKeyMaps.Escape) {
345 m.deleteMode = false
346 return m, nil
347 }
348 if key.Matches(msg, m.keyMap.Newline) {
349 m.textarea.InsertRune('\n')
350 cmds = append(cmds, util.CmdHandler(completions.CloseCompletionsMsg{}))
351 }
352 // History
353 if key.Matches(msg, m.keyMap.Previous) || key.Matches(msg, m.keyMap.Next) {
354 m.textarea.SetValue(m.stepOverHistory(m.getUserMessagesAsText, m.getDirectionFromKey(msg)))
355 }
356 // Handle Enter key
357 if m.textarea.Focused() && key.Matches(msg, m.keyMap.SendMessage) {
358 value := m.textarea.Value()
359 if strings.HasSuffix(value, "\\") {
360 // If the last character is a backslash, remove it and add a newline.
361 m.textarea.SetValue(strings.TrimSuffix(value, "\\"))
362 } else {
363 // Otherwise, send the message
364 return m, m.send()
365 }
366 }
367 }
368
369 m.textarea, cmd = m.textarea.Update(msg)
370 cmds = append(cmds, cmd)
371
372 if m.textarea.Focused() {
373 kp, ok := msg.(tea.KeyPressMsg)
374 if ok {
375 if kp.String() == "space" || m.textarea.Value() == "" {
376 m.isCompletionsOpen = false
377 m.currentQuery = ""
378 m.completionsStartIndex = 0
379 cmds = append(cmds, util.CmdHandler(completions.CloseCompletionsMsg{}))
380 } else {
381 word := m.textarea.Word()
382 if strings.HasPrefix(word, "/") {
383 // XXX: wont' work if editing in the middle of the field.
384 m.completionsStartIndex = strings.LastIndex(m.textarea.Value(), word)
385 m.currentQuery = word[1:]
386 x, y := m.completionsPosition()
387 x -= len(m.currentQuery)
388 m.isCompletionsOpen = true
389 cmds = append(cmds,
390 util.CmdHandler(completions.FilterCompletionsMsg{
391 Query: m.currentQuery,
392 Reopen: m.isCompletionsOpen,
393 X: x,
394 Y: y,
395 }),
396 )
397 } else if m.isCompletionsOpen {
398 m.isCompletionsOpen = false
399 m.currentQuery = ""
400 m.completionsStartIndex = 0
401 cmds = append(cmds, util.CmdHandler(completions.CloseCompletionsMsg{}))
402 }
403 }
404 }
405 }
406
407 return m, tea.Batch(cmds...)
408}
409
410func (m *editorCmp) setEditorPrompt() {
411 if perm := m.app.Permissions; perm != nil {
412 if perm.SkipRequests() {
413 m.textarea.SetPromptFunc(4, yoloPromptFunc)
414 return
415 }
416 }
417 m.textarea.SetPromptFunc(4, normalPromptFunc)
418}
419
420func (m *editorCmp) completionsPosition() (int, int) {
421 cur := m.textarea.Cursor()
422 if cur == nil {
423 return m.x, m.y + 1 // adjust for padding
424 }
425 x := cur.X + m.x
426 y := cur.Y + m.y + 1 // adjust for padding
427 return x, y
428}
429
430func (m *editorCmp) Cursor() *tea.Cursor {
431 cursor := m.textarea.Cursor()
432 if cursor != nil {
433 cursor.X = cursor.X + m.x + 1
434 cursor.Y = cursor.Y + m.y + 1 // adjust for padding
435 }
436 return cursor
437}
438
439var readyPlaceholders = [...]string{
440 "Ready!",
441 "Ready...",
442 "Ready?",
443 "Ready for instructions",
444}
445
446var workingPlaceholders = [...]string{
447 "Working!",
448 "Working...",
449 "Brrrrr...",
450 "Prrrrrrrr...",
451 "Processing...",
452 "Thinking...",
453}
454
455func (m *editorCmp) randomizePlaceholders() {
456 m.workingPlaceholder = workingPlaceholders[rand.Intn(len(workingPlaceholders))]
457 m.readyPlaceholder = readyPlaceholders[rand.Intn(len(readyPlaceholders))]
458}
459
460func (m *editorCmp) View() string {
461 t := styles.CurrentTheme()
462 // Update placeholder
463 if m.app.CoderAgent != nil && m.app.CoderAgent.IsBusy() {
464 m.textarea.Placeholder = m.workingPlaceholder
465 } else {
466 m.textarea.Placeholder = m.readyPlaceholder
467 }
468 if m.app.Permissions.SkipRequests() {
469 m.textarea.Placeholder = "Yolo mode!"
470 }
471 if len(m.attachments) == 0 {
472 content := t.S().Base.Padding(1).Render(
473 m.textarea.View(),
474 )
475 return content
476 }
477 content := t.S().Base.Padding(0, 1, 1, 1).Render(
478 lipgloss.JoinVertical(lipgloss.Top,
479 m.attachmentsContent(),
480 m.textarea.View(),
481 ),
482 )
483 return content
484}
485
486func (m *editorCmp) SetSize(width, height int) tea.Cmd {
487 m.width = width
488 m.height = height
489 m.textarea.SetWidth(width - 2) // adjust for padding
490 m.textarea.SetHeight(height - 2) // adjust for padding
491 return nil
492}
493
494func (m *editorCmp) GetSize() (int, int) {
495 return m.textarea.Width(), m.textarea.Height()
496}
497
498func (m *editorCmp) attachmentsContent() string {
499 var styledAttachments []string
500 t := styles.CurrentTheme()
501 attachmentStyles := t.S().Base.
502 MarginLeft(1).
503 Background(t.FgMuted).
504 Foreground(t.FgBase)
505 for i, attachment := range m.attachments {
506 var filename string
507 if len(attachment.FileName) > 10 {
508 filename = fmt.Sprintf(" %s %s...", styles.DocumentIcon, attachment.FileName[0:7])
509 } else {
510 filename = fmt.Sprintf(" %s %s", styles.DocumentIcon, attachment.FileName)
511 }
512 if m.deleteMode {
513 filename = fmt.Sprintf("%d%s", i, filename)
514 }
515 styledAttachments = append(styledAttachments, attachmentStyles.Render(filename))
516 }
517 content := lipgloss.JoinHorizontal(lipgloss.Left, styledAttachments...)
518 return content
519}
520
521func (m *editorCmp) SetPosition(x, y int) tea.Cmd {
522 m.x = x
523 m.y = y
524 return nil
525}
526
527func (m *editorCmp) startCompletions() func() tea.Msg {
528 return func() tea.Msg {
529 files, _, _ := m.listDirResolver()(".", nil)
530 slices.Sort(files)
531 completionItems := make([]completions.Completion, 0, len(files))
532 for _, file := range files {
533 file = strings.TrimPrefix(file, "./")
534 completionItems = append(completionItems, completions.Completion{
535 Title: file,
536 Value: FileCompletionItem{
537 Path: file,
538 },
539 })
540 }
541
542 x, y := m.completionsPosition()
543 return completions.OpenCompletionsMsg{
544 Completions: completionItems,
545 X: x,
546 Y: y,
547 }
548 }
549}
550
551// Blur implements Container.
552func (c *editorCmp) Blur() tea.Cmd {
553 c.textarea.Blur()
554 return nil
555}
556
557// Focus implements Container.
558func (c *editorCmp) Focus() tea.Cmd {
559 return c.textarea.Focus()
560}
561
562// IsFocused implements Container.
563func (c *editorCmp) IsFocused() bool {
564 return c.textarea.Focused()
565}
566
567// Bindings implements Container.
568func (c *editorCmp) Bindings() []key.Binding {
569 return c.keyMap.KeyBindings()
570}
571
572// TODO: most likely we do not need to have the session here
573// we need to move some functionality to the page level
574func (c *editorCmp) SetSession(session session.Session) tea.Cmd {
575 c.session = session
576 return nil
577}
578
579func (c *editorCmp) IsCompletionsOpen() bool {
580 return c.isCompletionsOpen
581}
582
583func (c *editorCmp) HasAttachments() bool {
584 return len(c.attachments) > 0
585}
586
587func normalPromptFunc(info textarea.PromptInfo) string {
588 t := styles.CurrentTheme()
589 if info.LineNumber == 0 {
590 return " > "
591 }
592 if info.Focused {
593 return t.S().Base.Foreground(t.GreenDark).Render("::: ")
594 }
595 return t.S().Muted.Render("::: ")
596}
597
598func yoloPromptFunc(info textarea.PromptInfo) string {
599 t := styles.CurrentTheme()
600 if info.LineNumber == 0 {
601 if info.Focused {
602 return fmt.Sprintf("%s ", t.YoloIconFocused)
603 } else {
604 return fmt.Sprintf("%s ", t.YoloIconBlurred)
605 }
606 }
607 if info.Focused {
608 return fmt.Sprintf("%s ", t.YoloDotsFocused)
609 }
610 return fmt.Sprintf("%s ", t.YoloDotsBlurred)
611}
612
613func (m *editorCmp) getUserMessagesAsText(ctx context.Context) ([]string, error) {
614 allMessages, err := m.app.Messages.List(ctx, m.session.ID)
615 if err != nil {
616 return nil, err
617 }
618
619 var userMessages []string
620 for _, msg := range allMessages {
621 if msg.Role == message.User {
622 userMessages = append(userMessages, msg.Content().Text)
623 }
624 }
625 return userMessages, nil
626}
627
628type direction int
629
630const (
631 previous = iota
632 next
633)
634
635func (m *editorCmp) getDirectionFromKey(msg tea.KeyPressMsg) func() direction {
636 return func() direction {
637 if key.Matches(msg, m.keyMap.Previous) {
638 return previous
639 }
640 return next
641 }
642}
643
644func (m *editorCmp) stepOverHistory(resolveHistoricMessages func(context.Context) ([]string, error), resolveDirection func() direction) string {
645 // NOTE(tauraamui): the last entry in this list will be the current contents of the input field/box
646 messageHistory, err := resolveHistoricMessages(context.Background())
647 if err != nil {
648 return ""
649 }
650
651 // the list will/should always have at least the current message in the input in the list
652 if len(messageHistory) == 1 {
653 return messageHistory[0]
654 }
655
656 // the first time we invoke scroll we need to start from top of the list
657 if !m.previouslyScrollingPromptHistory {
658 m.promptHistoryIndex = len(messageHistory) - 1
659 m.previouslyScrollingPromptHistory = true
660 }
661
662 switch resolveDirection() {
663 case previous:
664 m.scrollingPromptHistory = true
665 return m.stepBack(messageHistory)
666 case next:
667 return m.stepForward(messageHistory)
668 }
669 return ""
670}
671
672func (m *editorCmp) stepBack(history []string) string {
673 m.promptHistoryIndex -= 1
674 if m.promptHistoryIndex < 0 {
675 m.promptHistoryIndex = 0
676 }
677 return history[m.promptHistoryIndex]
678}
679
680func (m *editorCmp) stepForward(history []string) string {
681 m.promptHistoryIndex += 1
682 maxIndex := len(history) - 1
683 if m.scrollingPromptHistory {
684 maxIndex = len(history) - 2
685 }
686 if m.promptHistoryIndex > maxIndex {
687 m.promptHistoryIndex = maxIndex
688 }
689 return history[m.promptHistoryIndex]
690}
691
692func (m *editorCmp) handleMessageHistory(msg tea.KeyMsg) string {
693 ctx := context.Background()
694 userMessages, err := m.getUserMessagesAsText(ctx)
695 if err != nil {
696 return "" // Do nothing.
697 }
698 userMessages = append(userMessages, "") // Give the user a reset option.
699 if len(userMessages) > 0 {
700 if key.Matches(msg, m.keyMap.Previous) {
701 if m.promptHistoryIndex == 0 {
702 m.promptHistoryIndex = len(userMessages) - 1
703 } else {
704 m.promptHistoryIndex -= 1
705 }
706 }
707 if key.Matches(msg, m.keyMap.Next) {
708 if m.promptHistoryIndex == len(userMessages)-1 {
709 m.promptHistoryIndex = 0
710 } else {
711 m.promptHistoryIndex += 1
712 }
713 }
714 }
715 return userMessages[m.promptHistoryIndex]
716}
717
718func newTextArea() *textarea.Model {
719 t := styles.CurrentTheme()
720 ta := textarea.New()
721 ta.SetStyles(t.S().TextArea)
722 ta.ShowLineNumbers = false
723 ta.CharLimit = -1
724 ta.SetVirtualCursor(false)
725 ta.Focus()
726 return ta
727}
728
729func newEditor(app *app.App, resolveDirLister fsext.DirectoryListerResolver) *editorCmp {
730 e := editorCmp{
731 // TODO: remove the app instance from here
732 app: app,
733 textarea: newTextArea(),
734 keyMap: DefaultEditorKeyMap(),
735 listDirResolver: resolveDirLister,
736 }
737 e.setEditorPrompt()
738
739 e.randomizePlaceholders()
740 e.textarea.Placeholder = e.readyPlaceholder
741
742 return &e
743}
744
745func New(app *app.App) Editor {
746 ls := app.Config().Options.TUI.Completions.Limits
747 return newEditor(app, fsext.ResolveDirectoryLister(ls()))
748}