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/tui/layout"
 11	"github.com/kujtimiihoxha/termai/internal/tui/styles"
 12	"github.com/kujtimiihoxha/termai/internal/tui/util"
 13	"github.com/kujtimiihoxha/vimtea"
 14	"golang.org/x/net/context"
 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	cancelMessage context.CancelFunc
 34}
 35
 36type editorKeyMap struct {
 37	SendMessage    key.Binding
 38	SendMessageI   key.Binding
 39	CancelMessage  key.Binding
 40	InsertMode     key.Binding
 41	NormaMode      key.Binding
 42	VisualMode     key.Binding
 43	VisualLineMode key.Binding
 44}
 45
 46var editorKeyMapValue = editorKeyMap{
 47	SendMessage: key.NewBinding(
 48		key.WithKeys("enter"),
 49		key.WithHelp("enter", "send message normal mode"),
 50	),
 51	SendMessageI: key.NewBinding(
 52		key.WithKeys("ctrl+s"),
 53		key.WithHelp("ctrl+s", "send message insert mode"),
 54	),
 55	CancelMessage: key.NewBinding(
 56		key.WithKeys("ctrl+x"),
 57		key.WithHelp("ctrl+x", "cancel current message"),
 58	),
 59	InsertMode: key.NewBinding(
 60		key.WithKeys("i"),
 61		key.WithHelp("i", "insert mode"),
 62	),
 63	NormaMode: key.NewBinding(
 64		key.WithKeys("esc"),
 65		key.WithHelp("esc", "normal mode"),
 66	),
 67	VisualMode: key.NewBinding(
 68		key.WithKeys("v"),
 69		key.WithHelp("v", "visual mode"),
 70	),
 71	VisualLineMode: key.NewBinding(
 72		key.WithKeys("V"),
 73		key.WithHelp("V", "visual line mode"),
 74	),
 75}
 76
 77func (m *editorCmp) Init() tea.Cmd {
 78	return m.editor.Init()
 79}
 80
 81func (m *editorCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 82	switch msg := msg.(type) {
 83	case vimtea.EditorModeMsg:
 84		m.editorMode = msg.Mode
 85	case SelectedSessionMsg:
 86		if msg.SessionID != m.sessionID {
 87			m.sessionID = msg.SessionID
 88		}
 89	}
 90	if m.IsFocused() {
 91		switch msg := msg.(type) {
 92		case tea.KeyMsg:
 93			switch {
 94			case key.Matches(msg, editorKeyMapValue.SendMessage):
 95				if m.editorMode == vimtea.ModeNormal {
 96					return m, m.Send()
 97				}
 98			case key.Matches(msg, editorKeyMapValue.SendMessageI):
 99				if m.editorMode == vimtea.ModeInsert {
100					return m, m.Send()
101				}
102			case key.Matches(msg, editorKeyMapValue.CancelMessage):
103				return m, m.Cancel()
104			}
105		}
106		u, cmd := m.editor.Update(msg)
107		m.editor = u.(vimtea.Editor)
108		return m, cmd
109	}
110	return m, nil
111}
112
113func (m *editorCmp) Blur() tea.Cmd {
114	m.focused = false
115	return nil
116}
117
118func (m *editorCmp) BorderText() map[layout.BorderPosition]string {
119	title := "New Message"
120	if m.focused {
121		title = lipgloss.NewStyle().Foreground(styles.Primary).Render(title)
122	}
123	return map[layout.BorderPosition]string{
124		layout.BottomLeftBorder: title,
125	}
126}
127
128func (m *editorCmp) Focus() tea.Cmd {
129	m.focused = true
130	return m.editor.Tick()
131}
132
133func (m *editorCmp) GetSize() (int, int) {
134	return m.width, m.height
135}
136
137func (m *editorCmp) IsFocused() bool {
138	return m.focused
139}
140
141func (m *editorCmp) SetSize(width int, height int) {
142	m.width = width
143	m.height = height
144	m.editor.SetSize(width, height)
145}
146
147func (m *editorCmp) Cancel() tea.Cmd {
148	if m.cancelMessage == nil {
149		return util.ReportWarn("No message to cancel")
150	}
151
152	m.cancelMessage()
153	m.cancelMessage = nil
154	return util.ReportWarn("Message cancelled")
155}
156
157func (m *editorCmp) Send() tea.Cmd {
158	if m.cancelMessage != nil {
159		return util.ReportWarn("Assistant is still working on the previous message")
160	}
161
162	messages, err := m.app.Messages.List(context.Background(), m.sessionID)
163	if err != nil {
164		return util.ReportError(err)
165	}
166	if hasUnfinishedMessages(messages) {
167		return util.ReportWarn("Assistant is still working on the previous message")
168	}
169
170	content := strings.Join(m.editor.GetBuffer().Lines(), "\n")
171	if len(content) == 0 {
172		return util.ReportWarn("Message is empty")
173	}
174	ctx, cancel := context.WithCancel(context.Background())
175	m.cancelMessage = cancel
176	go func() {
177		defer cancel()
178		m.app.CoderAgent.Generate(ctx, m.sessionID, content)
179		m.cancelMessage = nil
180	}()
181
182	return m.editor.Reset()
183}
184
185func (m *editorCmp) View() string {
186	return m.editor.View()
187}
188
189func (m *editorCmp) BindingKeys() []key.Binding {
190	return layout.KeyMapToSlice(editorKeyMapValue)
191}
192
193func NewEditorCmp(app *app.App) EditorCmp {
194	editor := vimtea.NewEditor(
195		vimtea.WithFileName("message.md"),
196	)
197	return &editorCmp{
198		app:    app,
199		editor: editor,
200	}
201}