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