github.com/elves/elvish@v0.15.0/pkg/cli/codearea.go (about) 1 package cli 2 3 import ( 4 "bytes" 5 "strings" 6 "sync" 7 "unicode" 8 "unicode/utf8" 9 10 "github.com/elves/elvish/pkg/cli/term" 11 "github.com/elves/elvish/pkg/parse" 12 "github.com/elves/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 // A Handler that takes precedence over the default handling of events. 29 OverlayHandler Handler 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.OverlayHandler == nil { 115 spec.OverlayHandler = DummyHandler{} 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 view := getView(w) 150 bb := term.NewBufferBuilder(width) 151 renderView(view, bb) 152 b := bb.Buffer() 153 truncateToHeight(b, height) 154 return b 155 } 156 157 // Handle handles KeyEvent's of non-function keys, as well as PasteSetting 158 // events. 159 func (w *codeArea) Handle(event term.Event) bool { 160 switch event := event.(type) { 161 case term.PasteSetting: 162 return w.handlePasteSetting(bool(event)) 163 case term.KeyEvent: 164 return w.handleKeyEvent(ui.Key(event)) 165 } 166 return false 167 } 168 169 func (w *codeArea) MutateState(f func(*CodeAreaState)) { 170 w.StateMutex.Lock() 171 defer w.StateMutex.Unlock() 172 f(&w.State) 173 } 174 175 func (w *codeArea) CopyState() CodeAreaState { 176 w.StateMutex.RLock() 177 defer w.StateMutex.RUnlock() 178 return w.State 179 } 180 181 func (w *codeArea) resetInserts() { 182 w.inserts = "" 183 w.lastCodeBuffer = CodeBuffer{} 184 } 185 186 func (w *codeArea) handlePasteSetting(start bool) bool { 187 w.resetInserts() 188 if start { 189 w.pasting = true 190 } else { 191 text := w.pasteBuffer.String() 192 if w.QuotePaste() { 193 text = parse.Quote(text) 194 } 195 w.MutateState(func(s *CodeAreaState) { s.Buffer.InsertAtDot(text) }) 196 197 w.pasting = false 198 w.pasteBuffer = bytes.Buffer{} 199 } 200 return true 201 } 202 203 // Tries to expand a simple abbreviation. This function assumes that the state 204 // mutex is already being held. 205 func (w *codeArea) expandSimpleAbbr() { 206 var abbr, full string 207 // Find the longest matching abbreviation. 208 w.Abbreviations(func(a, f string) { 209 if strings.HasSuffix(w.inserts, a) && len(a) > len(abbr) { 210 abbr, full = a, f 211 } 212 }) 213 if len(abbr) > 0 { 214 c := &w.State.Buffer 215 *c = CodeBuffer{ 216 Content: c.Content[:c.Dot-len(abbr)] + full + c.Content[c.Dot:], 217 Dot: c.Dot - len(abbr) + len(full), 218 } 219 w.resetInserts() 220 } 221 } 222 223 // Tries to expand a word abbreviation. This function assumes that the state 224 // mutex is already being held. 225 func (w *codeArea) expandWordAbbr(trigger rune, categorizer func(rune) int) { 226 c := &w.State.Buffer 227 if c.Dot < len(c.Content) { 228 // Word abbreviations are only expanded at the end of the buffer. 229 return 230 } 231 triggerLen := len(string(trigger)) 232 if triggerLen >= len(w.inserts) { 233 // Only the trigger has been inserted, or a simple abbreviation was just 234 // expanded. In either case, there is nothing to expand. 235 return 236 } 237 // The trigger is only used to determine word boundary; when considering 238 // what to expand, we only consider the part that was inserted before it. 239 inserts := w.inserts[:len(w.inserts)-triggerLen] 240 241 var abbr, full string 242 // Find the longest matching abbreviation. 243 w.SmallWordAbbreviations(func(a, f string) { 244 if len(a) <= len(abbr) { 245 // This abbreviation can't be the longest. 246 return 247 } 248 if !strings.HasSuffix(inserts, a) { 249 // This abbreviation was not inserted. 250 return 251 } 252 // Verify the trigger rune creates a word boundary. 253 r, _ := utf8.DecodeLastRuneInString(a) 254 if categorizer(trigger) == categorizer(r) { 255 return 256 } 257 // Verify the rune preceding the abbreviation, if any, creates a word 258 // boundary. 259 if len(c.Content) > len(a)+triggerLen { 260 r1, _ := utf8.DecodeLastRuneInString(c.Content[:len(c.Content)-len(a)-triggerLen]) 261 r2, _ := utf8.DecodeRuneInString(a) 262 if categorizer(r1) == categorizer(r2) { 263 return 264 } 265 } 266 abbr, full = a, f 267 }) 268 if len(abbr) > 0 { 269 *c = CodeBuffer{ 270 Content: c.Content[:c.Dot-len(abbr)-triggerLen] + full + string(trigger), 271 Dot: c.Dot - len(abbr) + len(full), 272 } 273 w.resetInserts() 274 } 275 } 276 277 func (w *codeArea) handleKeyEvent(key ui.Key) bool { 278 isFuncKey := key.Mod != 0 || key.Rune < 0 279 if w.pasting { 280 if isFuncKey { 281 // TODO: Notify the user of the error, or insert the original 282 // character as is. 283 } else { 284 w.pasteBuffer.WriteRune(key.Rune) 285 } 286 return true 287 } 288 289 if w.OverlayHandler.Handle(term.KeyEvent(key)) { 290 return true 291 } 292 293 // We only implement essential keybindings here. Other keybindings can be 294 // added via handler overlays. 295 switch key { 296 case ui.K('\n'): 297 w.resetInserts() 298 w.Submit() 299 return true 300 case ui.K(ui.Backspace), ui.K('H', ui.Ctrl): 301 w.resetInserts() 302 w.MutateState(func(s *CodeAreaState) { 303 c := &s.Buffer 304 // Remove the last rune. 305 _, chop := utf8.DecodeLastRuneInString(c.Content[:c.Dot]) 306 *c = CodeBuffer{ 307 Content: c.Content[:c.Dot-chop] + c.Content[c.Dot:], 308 Dot: c.Dot - chop, 309 } 310 }) 311 return true 312 default: 313 if isFuncKey || !unicode.IsGraphic(key.Rune) { 314 w.resetInserts() 315 return false 316 } 317 w.StateMutex.Lock() 318 defer w.StateMutex.Unlock() 319 if w.lastCodeBuffer != w.State.Buffer { 320 // Something has happened between the last insert and this one; 321 // reset the state. 322 w.resetInserts() 323 } 324 s := string(key.Rune) 325 w.State.Buffer.InsertAtDot(s) 326 w.inserts += s 327 w.lastCodeBuffer = w.State.Buffer 328 w.expandSimpleAbbr() 329 w.expandWordAbbr(key.Rune, CategorizeSmallWord) 330 return true 331 } 332 } 333 334 // IsAlnum determines if the rune is an alphanumeric character. 335 func IsAlnum(r rune) bool { 336 return unicode.IsLetter(r) || unicode.IsNumber(r) 337 } 338 339 // CategorizeSmallWord determines if the rune is whitespace, alphanum, or 340 // something else. 341 func CategorizeSmallWord(r rune) int { 342 switch { 343 case unicode.IsSpace(r): 344 return 0 345 case IsAlnum(r): 346 return 1 347 default: 348 return 2 349 } 350 }