github.com/markusbkk/elvish@v0.0.0-20231204143114-91dc52438621/pkg/cli/tk/codearea.go (about) 1 package tk 2 3 import ( 4 "bytes" 5 "strings" 6 "sync" 7 "unicode" 8 "unicode/utf8" 9 10 "github.com/markusbkk/elvish/pkg/cli/term" 11 "github.com/markusbkk/elvish/pkg/parse" 12 "github.com/markusbkk/elvish/pkg/ui" 13 ) 14 15 // CodeArea is a Widget for displaying and editing code. 16 type 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. 27 type 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. 55 type CodeAreaState struct { 56 Buffer CodeBuffer 57 Pending PendingCode 58 HideRPrompt bool 59 } 60 61 // CodeBuffer represents the buffer of the CodeArea widget. 62 type 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. 71 type 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. 83 func (s *CodeAreaState) ApplyPending() { 84 s.Buffer, _, _ = patchPending(s.Buffer, s.Pending) 85 s.Pending = PendingCode{} 86 } 87 88 func (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 95 type 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. 113 func 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. 142 func (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. 148 func (w *codeArea) Render(width, height int) *term.Buffer { 149 b := w.render(width) 150 truncateToHeight(b, height) 151 return b 152 } 153 154 func (w *codeArea) MaxHeight(width, height int) int { 155 return len(w.render(width).Lines) 156 } 157 158 func (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. 167 func (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 177 func (w *codeArea) MutateState(f func(*CodeAreaState)) { 178 w.StateMutex.Lock() 179 defer w.StateMutex.Unlock() 180 f(&w.State) 181 } 182 183 func (w *codeArea) CopyState() CodeAreaState { 184 w.StateMutex.RLock() 185 defer w.StateMutex.RUnlock() 186 return w.State 187 } 188 189 func (w *codeArea) resetInserts() { 190 w.inserts = "" 191 w.lastCodeBuffer = CodeBuffer{} 192 } 193 194 func (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. 213 func (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. 233 func (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 285 func (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. 343 func 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. 349 func 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 }