editor.go

  1package repl
  2
  3import (
  4	"strings"
  5
  6	"github.com/charmbracelet/bubbles/key"
  7	tea "github.com/charmbracelet/bubbletea"
  8	"github.com/charmbracelet/lipgloss"
  9	"github.com/kujtimiihoxha/termai/internal/app"
 10	"github.com/kujtimiihoxha/termai/internal/llm/agent"
 11	"github.com/kujtimiihoxha/termai/internal/tui/layout"
 12	"github.com/kujtimiihoxha/termai/internal/tui/styles"
 13	"github.com/kujtimiihoxha/termai/internal/tui/util"
 14	"github.com/kujtimiihoxha/vimtea"
 15)
 16
 17type EditorCmp interface {
 18	tea.Model
 19	layout.Focusable
 20	layout.Sizeable
 21	layout.Bordered
 22	layout.Bindings
 23}
 24
 25type editorCmp struct {
 26	app        *app.App
 27	editor     vimtea.Editor
 28	editorMode vimtea.EditorMode
 29	sessionID  string
 30	focused    bool
 31	width      int
 32	height     int
 33}
 34
 35type editorKeyMap struct {
 36	SendMessage    key.Binding
 37	SendMessageI   key.Binding
 38	InsertMode     key.Binding
 39	NormaMode      key.Binding
 40	VisualMode     key.Binding
 41	VisualLineMode key.Binding
 42}
 43
 44var editorKeyMapValue = editorKeyMap{
 45	SendMessage: key.NewBinding(
 46		key.WithKeys("enter"),
 47		key.WithHelp("enter", "send message normal mode"),
 48	),
 49	SendMessageI: key.NewBinding(
 50		key.WithKeys("ctrl+s"),
 51		key.WithHelp("ctrl+s", "send message insert mode"),
 52	),
 53	InsertMode: key.NewBinding(
 54		key.WithKeys("i"),
 55		key.WithHelp("i", "insert mode"),
 56	),
 57	NormaMode: key.NewBinding(
 58		key.WithKeys("esc"),
 59		key.WithHelp("esc", "normal mode"),
 60	),
 61	VisualMode: key.NewBinding(
 62		key.WithKeys("v"),
 63		key.WithHelp("v", "visual mode"),
 64	),
 65	VisualLineMode: key.NewBinding(
 66		key.WithKeys("V"),
 67		key.WithHelp("V", "visual line mode"),
 68	),
 69}
 70
 71func (m *editorCmp) Init() tea.Cmd {
 72	return m.editor.Init()
 73}
 74
 75func (m *editorCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 76	switch msg := msg.(type) {
 77	case vimtea.EditorModeMsg:
 78		m.editorMode = msg.Mode
 79	case SelectedSessionMsg:
 80		if msg.SessionID != m.sessionID {
 81			m.sessionID = msg.SessionID
 82		}
 83	}
 84	if m.IsFocused() {
 85		switch msg := msg.(type) {
 86		case tea.KeyMsg:
 87			switch {
 88			case key.Matches(msg, editorKeyMapValue.SendMessage):
 89				if m.editorMode == vimtea.ModeNormal {
 90					return m, m.Send()
 91				}
 92			case key.Matches(msg, editorKeyMapValue.SendMessageI):
 93				if m.editorMode == vimtea.ModeInsert {
 94					return m, m.Send()
 95				}
 96			}
 97		}
 98		u, cmd := m.editor.Update(msg)
 99		m.editor = u.(vimtea.Editor)
100		return m, cmd
101	}
102	return m, nil
103}
104
105func (m *editorCmp) Blur() tea.Cmd {
106	m.focused = false
107	return nil
108}
109
110func (m *editorCmp) BorderText() map[layout.BorderPosition]string {
111	title := "New Message"
112	if m.focused {
113		title = lipgloss.NewStyle().Foreground(styles.Primary).Render(title)
114	}
115	return map[layout.BorderPosition]string{
116		layout.BottomLeftBorder: title,
117	}
118}
119
120func (m *editorCmp) Focus() tea.Cmd {
121	m.focused = true
122	return m.editor.Tick()
123}
124
125func (m *editorCmp) GetSize() (int, int) {
126	return m.width, m.height
127}
128
129func (m *editorCmp) IsFocused() bool {
130	return m.focused
131}
132
133func (m *editorCmp) SetSize(width int, height int) {
134	m.width = width
135	m.height = height
136	m.editor.SetSize(width, height)
137}
138
139func (m *editorCmp) Send() tea.Cmd {
140	return func() tea.Msg {
141		messages, _ := m.app.Messages.List(m.sessionID)
142		if hasUnfinishedMessages(messages) {
143			return util.ReportWarn("Assistant is still working on the previous message")
144		}
145		a, _ := agent.NewCoderAgent(m.app)
146
147		content := strings.Join(m.editor.GetBuffer().Lines(), "\n")
148		go a.Generate(m.sessionID, content)
149
150		return m.editor.Reset()
151	}
152}
153
154func (m *editorCmp) View() string {
155	return m.editor.View()
156}
157
158func (m *editorCmp) BindingKeys() []key.Binding {
159	return layout.KeyMapToSlice(editorKeyMapValue)
160}
161
162func NewEditorCmp(app *app.App) EditorCmp {
163	editor := vimtea.NewEditor(
164		vimtea.WithFileName("message.md"),
165	)
166	return &editorCmp{
167		app:    app,
168		editor: editor,
169	}
170}