editor.go

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