editor.go

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