1package tk
2
3import (
4	"bytes"
5	"strings"
6	"sync"
7	"unicode"
8	"unicode/utf8"
9
10	"src.elv.sh/pkg/cli/term"
11	"src.elv.sh/pkg/parse"
12	"src.elv.sh/pkg/ui"
13)
14
15// CodeArea is a Widget for displaying and editing code.
16type CodeArea interface {
17	Widget
18	// CopyState returns a copy of the state.
19	CopyState() CodeAreaState
20	// MutateState calls the given the function while locking StateMutex.
21	MutateState(f func(*CodeAreaState))
22	// Submit triggers the OnSubmit callback.
23	Submit()
24}
25
26// CodeAreaSpec specifies the configuration and initial state for CodeArea.
27type CodeAreaSpec struct {
28	// Key bindings.
29	Bindings Bindings
30	// A function that highlights the given code and returns any errors it has
31	// found when highlighting. If this function is not given, the Widget does
32	// not highlight the code nor show any errors.
33	Highlighter func(code string) (ui.Text, []error)
34	// Prompt callback.
35	Prompt func() ui.Text
36	// Right-prompt callback.
37	RPrompt func() ui.Text
38	// A function that calls the callback with string pairs for abbreviations
39	// and their expansions. If this function is not given, the Widget does not
40	// expand any abbreviations.
41	Abbreviations          func(f func(abbr, full string))
42	SmallWordAbbreviations func(f func(abbr, full string))
43	// A function that returns whether pasted texts (from bracketed pastes)
44	// should be quoted. If this function is not given, the Widget defaults to
45	// not quoting pasted texts.
46	QuotePaste func() bool
47	// A function that is called on the submit event.
48	OnSubmit func()
49
50	// State. When used in New, this field specifies the initial state.
51	State CodeAreaState
52}
53
54// CodeAreaState keeps the mutable state of the CodeArea widget.
55type CodeAreaState struct {
56	Buffer      CodeBuffer
57	Pending     PendingCode
58	HideRPrompt bool
59}
60
61// CodeBuffer represents the buffer of the CodeArea widget.
62type CodeBuffer struct {
63	// Content of the buffer.
64	Content string
65	// Position of the dot (more commonly known as the cursor), as a byte index
66	// into Content.
67	Dot int
68}
69
70// PendingCode represents pending code, such as during completion.
71type PendingCode struct {
72	// Beginning index of the text area that the pending code replaces, as a
73	// byte index into RawState.Code.
74	From int
75	// End index of the text area that the pending code replaces, as a byte
76	// index into RawState.Code.
77	To int
78	// The content of the pending code.
79	Content string
80}
81
82// ApplyPending applies pending code to the code buffer, and resets pending code.
83func (s *CodeAreaState) ApplyPending() {
84	s.Buffer, _, _ = patchPending(s.Buffer, s.Pending)
85	s.Pending = PendingCode{}
86}
87
88func (c *CodeBuffer) InsertAtDot(text string) {
89	*c = CodeBuffer{
90		Content: c.Content[:c.Dot] + text + c.Content[c.Dot:],
91		Dot:     c.Dot + len(text),
92	}
93}
94
95type codeArea struct {
96	// Mutex for synchronizing access to State.
97	StateMutex sync.RWMutex
98	// Configuration and state.
99	CodeAreaSpec
100
101	// Consecutively inserted text. Used for expanding abbreviations.
102	inserts string
103	// Value of State.CodeBuffer when handleKeyEvent was last called. Used for
104	// detecting whether insertion has been interrupted.
105	lastCodeBuffer CodeBuffer
106	// Whether the widget is in the middle of bracketed pasting.
107	pasting bool
108	// Buffer for keeping Pasted text during bracketed pasting.
109	pasteBuffer bytes.Buffer
110}
111
112// NewCodeArea creates a new CodeArea from the given spec.
113func NewCodeArea(spec CodeAreaSpec) CodeArea {
114	if spec.Bindings == nil {
115		spec.Bindings = DummyBindings{}
116	}
117	if spec.Highlighter == nil {
118		spec.Highlighter = func(s string) (ui.Text, []error) { return ui.T(s), nil }
119	}
120	if spec.Prompt == nil {
121		spec.Prompt = func() ui.Text { return nil }
122	}
123	if spec.RPrompt == nil {
124		spec.RPrompt = func() ui.Text { return nil }
125	}
126	if spec.Abbreviations == nil {
127		spec.Abbreviations = func(func(a, f string)) {}
128	}
129	if spec.SmallWordAbbreviations == nil {
130		spec.SmallWordAbbreviations = func(func(a, f string)) {}
131	}
132	if spec.QuotePaste == nil {
133		spec.QuotePaste = func() bool { return false }
134	}
135	if spec.OnSubmit == nil {
136		spec.OnSubmit = func() {}
137	}
138	return &codeArea{CodeAreaSpec: spec}
139}
140
141// Submit emits a submit event with the current code content.
142func (w *codeArea) Submit() {
143	w.OnSubmit()
144}
145
146// Render renders the code area, including the prompt and rprompt, highlighted
147// code, the cursor, and compilation errors in the code content.
148func (w *codeArea) Render(width, height int) *term.Buffer {
149	b := w.render(width)
150	truncateToHeight(b, height)
151	return b
152}
153
154func (w *codeArea) MaxHeight(width, height int) int {
155	return len(w.render(width).Lines)
156}
157
158func (w *codeArea) render(width int) *term.Buffer {
159	view := getView(w)
160	bb := term.NewBufferBuilder(width)
161	renderView(view, bb)
162	return bb.Buffer()
163}
164
165// Handle handles KeyEvent's of non-function keys, as well as PasteSetting
166// events.
167func (w *codeArea) Handle(event term.Event) bool {
168	switch event := event.(type) {
169	case term.PasteSetting:
170		return w.handlePasteSetting(bool(event))
171	case term.KeyEvent:
172		return w.handleKeyEvent(ui.Key(event))
173	}
174	return false
175}
176
177func (w *codeArea) MutateState(f func(*CodeAreaState)) {
178	w.StateMutex.Lock()
179	defer w.StateMutex.Unlock()
180	f(&w.State)
181}
182
183func (w *codeArea) CopyState() CodeAreaState {
184	w.StateMutex.RLock()
185	defer w.StateMutex.RUnlock()
186	return w.State
187}
188
189func (w *codeArea) resetInserts() {
190	w.inserts = ""
191	w.lastCodeBuffer = CodeBuffer{}
192}
193
194func (w *codeArea) handlePasteSetting(start bool) bool {
195	w.resetInserts()
196	if start {
197		w.pasting = true
198	} else {
199		text := w.pasteBuffer.String()
200		if w.QuotePaste() {
201			text = parse.Quote(text)
202		}
203		w.MutateState(func(s *CodeAreaState) { s.Buffer.InsertAtDot(text) })
204
205		w.pasting = false
206		w.pasteBuffer = bytes.Buffer{}
207	}
208	return true
209}
210
211// Tries to expand a simple abbreviation. This function assumes that the state
212// mutex is already being held.
213func (w *codeArea) expandSimpleAbbr() {
214	var abbr, full string
215	// Find the longest matching abbreviation.
216	w.Abbreviations(func(a, f string) {
217		if strings.HasSuffix(w.inserts, a) && len(a) > len(abbr) {
218			abbr, full = a, f
219		}
220	})
221	if len(abbr) > 0 {
222		c := &w.State.Buffer
223		*c = CodeBuffer{
224			Content: c.Content[:c.Dot-len(abbr)] + full + c.Content[c.Dot:],
225			Dot:     c.Dot - len(abbr) + len(full),
226		}
227		w.resetInserts()
228	}
229}
230
231// Tries to expand a word abbreviation. This function assumes that the state
232// mutex is already being held.
233func (w *codeArea) expandWordAbbr(trigger rune, categorizer func(rune) int) {
234	c := &w.State.Buffer
235	if c.Dot < len(c.Content) {
236		// Word abbreviations are only expanded at the end of the buffer.
237		return
238	}
239	triggerLen := len(string(trigger))
240	if triggerLen >= len(w.inserts) {
241		// Only the trigger has been inserted, or a simple abbreviation was just
242		// expanded. In either case, there is nothing to expand.
243		return
244	}
245	// The trigger is only used to determine word boundary; when considering
246	// what to expand, we only consider the part that was inserted before it.
247	inserts := w.inserts[:len(w.inserts)-triggerLen]
248
249	var abbr, full string
250	// Find the longest matching abbreviation.
251	w.SmallWordAbbreviations(func(a, f string) {
252		if len(a) <= len(abbr) {
253			// This abbreviation can't be the longest.
254			return
255		}
256		if !strings.HasSuffix(inserts, a) {
257			// This abbreviation was not inserted.
258			return
259		}
260		// Verify the trigger rune creates a word boundary.
261		r, _ := utf8.DecodeLastRuneInString(a)
262		if categorizer(trigger) == categorizer(r) {
263			return
264		}
265		// Verify the rune preceding the abbreviation, if any, creates a word
266		// boundary.
267		if len(c.Content) > len(a)+triggerLen {
268			r1, _ := utf8.DecodeLastRuneInString(c.Content[:len(c.Content)-len(a)-triggerLen])
269			r2, _ := utf8.DecodeRuneInString(a)
270			if categorizer(r1) == categorizer(r2) {
271				return
272			}
273		}
274		abbr, full = a, f
275	})
276	if len(abbr) > 0 {
277		*c = CodeBuffer{
278			Content: c.Content[:c.Dot-len(abbr)-triggerLen] + full + string(trigger),
279			Dot:     c.Dot - len(abbr) + len(full),
280		}
281		w.resetInserts()
282	}
283}
284
285func (w *codeArea) handleKeyEvent(key ui.Key) bool {
286	isFuncKey := key.Mod != 0 || key.Rune < 0
287	if w.pasting {
288		if isFuncKey {
289			// TODO: Notify the user of the error, or insert the original
290			// character as is.
291		} else {
292			w.pasteBuffer.WriteRune(key.Rune)
293		}
294		return true
295	}
296
297	if w.Bindings.Handle(w, term.KeyEvent(key)) {
298		return true
299	}
300
301	// We only implement essential keybindings here. Other keybindings can be
302	// added via handler overlays.
303	switch key {
304	case ui.K('\n'):
305		w.resetInserts()
306		w.Submit()
307		return true
308	case ui.K(ui.Backspace), ui.K('H', ui.Ctrl):
309		w.resetInserts()
310		w.MutateState(func(s *CodeAreaState) {
311			c := &s.Buffer
312			// Remove the last rune.
313			_, chop := utf8.DecodeLastRuneInString(c.Content[:c.Dot])
314			*c = CodeBuffer{
315				Content: c.Content[:c.Dot-chop] + c.Content[c.Dot:],
316				Dot:     c.Dot - chop,
317			}
318		})
319		return true
320	default:
321		if isFuncKey || !unicode.IsGraphic(key.Rune) {
322			w.resetInserts()
323			return false
324		}
325		w.StateMutex.Lock()
326		defer w.StateMutex.Unlock()
327		if w.lastCodeBuffer != w.State.Buffer {
328			// Something has happened between the last insert and this one;
329			// reset the state.
330			w.resetInserts()
331		}
332		s := string(key.Rune)
333		w.State.Buffer.InsertAtDot(s)
334		w.inserts += s
335		w.lastCodeBuffer = w.State.Buffer
336		w.expandSimpleAbbr()
337		w.expandWordAbbr(key.Rune, CategorizeSmallWord)
338		return true
339	}
340}
341
342// IsAlnum determines if the rune is an alphanumeric character.
343func IsAlnum(r rune) bool {
344	return unicode.IsLetter(r) || unicode.IsNumber(r)
345}
346
347// CategorizeSmallWord determines if the rune is whitespace, alphanum, or
348// something else.
349func CategorizeSmallWord(r rune) int {
350	switch {
351	case unicode.IsSpace(r):
352		return 0
353	case IsAlnum(r):
354		return 1
355	default:
356		return 2
357	}
358}
359