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  }