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