github.com/markusbkk/elvish@v0.0.0-20231204143114-91dc52438621/pkg/edit/highlight/highlight.go (about)

     1  // Package highlight provides an Elvish syntax highlighter.
     2  package highlight
     3  
     4  import (
     5  	"time"
     6  
     7  	"github.com/markusbkk/elvish/pkg/diag"
     8  	"github.com/markusbkk/elvish/pkg/parse"
     9  	"github.com/markusbkk/elvish/pkg/ui"
    10  )
    11  
    12  // Config keeps configuration for highlighting code.
    13  type Config struct {
    14  	Check      func(n parse.Tree) error
    15  	HasCommand func(name string) bool
    16  }
    17  
    18  // Information collected about a command region, used for asynchronous
    19  // highlighting.
    20  type cmdRegion struct {
    21  	seg int
    22  	cmd string
    23  }
    24  
    25  // MaxBlockForLate specifies the maximum wait time to block for late results.
    26  // It can be changed for test cases.
    27  var MaxBlockForLate = 10 * time.Millisecond
    28  
    29  // Highlights a piece of Elvish code.
    30  func highlight(code string, cfg Config, lateCb func(ui.Text)) (ui.Text, []error) {
    31  	var errors []error
    32  	var errorRegions []region
    33  
    34  	tree, errParse := parse.Parse(parse.Source{Name: "[tty]", Code: code}, parse.Config{})
    35  	if errParse != nil {
    36  		for _, err := range errParse.(*parse.Error).Entries {
    37  			if err.Context.From != len(code) {
    38  				errors = append(errors, err)
    39  				errorRegions = append(errorRegions,
    40  					region{
    41  						err.Context.From, err.Context.To,
    42  						semanticRegion, errorRegion})
    43  			}
    44  		}
    45  	}
    46  
    47  	if cfg.Check != nil {
    48  		err := cfg.Check(tree)
    49  		if r, ok := err.(diag.Ranger); ok && r.Range().From != len(code) {
    50  			errors = append(errors, err)
    51  			errorRegions = append(errorRegions,
    52  				region{
    53  					r.Range().From, r.Range().To, semanticRegion, errorRegion})
    54  		}
    55  	}
    56  
    57  	var text ui.Text
    58  	regions := getRegionsInner(tree.Root)
    59  	regions = append(regions, errorRegions...)
    60  	regions = fixRegions(regions)
    61  	lastEnd := 0
    62  	var cmdRegions []cmdRegion
    63  
    64  	for _, r := range regions {
    65  		if r.begin > lastEnd {
    66  			// Add inter-region text.
    67  			text = append(text, &ui.Segment{Text: code[lastEnd:r.begin]})
    68  		}
    69  
    70  		regionCode := code[r.begin:r.end]
    71  		var styling ui.Styling
    72  		if r.typ == commandRegion {
    73  			if cfg.HasCommand != nil {
    74  				// Do not highlight now, but collect the index of the region and the
    75  				// segment.
    76  				cmdRegions = append(cmdRegions, cmdRegion{len(text), regionCode})
    77  			} else {
    78  				// Treat all commands as good commands.
    79  				styling = stylingForGoodCommand
    80  			}
    81  		} else {
    82  			styling = stylingFor[r.typ]
    83  		}
    84  		seg := &ui.Segment{Text: regionCode}
    85  		if styling != nil {
    86  			seg = ui.StyleSegment(seg, styling)
    87  		}
    88  
    89  		text = append(text, seg)
    90  		lastEnd = r.end
    91  	}
    92  	if len(code) > lastEnd {
    93  		// Add text after the last region as unstyled.
    94  		text = append(text, &ui.Segment{Text: code[lastEnd:]})
    95  	}
    96  
    97  	if cfg.HasCommand != nil && len(cmdRegions) > 0 {
    98  		// Launch a goroutine to style command regions asynchronously.
    99  		lateCh := make(chan ui.Text)
   100  		go func() {
   101  			newText := text.Clone()
   102  			for _, cmdRegion := range cmdRegions {
   103  				var styling ui.Styling
   104  				if cfg.HasCommand(cmdRegion.cmd) {
   105  					styling = stylingForGoodCommand
   106  				} else {
   107  					styling = stylingForBadCommand
   108  				}
   109  				seg := &newText[cmdRegion.seg]
   110  				*seg = ui.StyleSegment(*seg, styling)
   111  			}
   112  			lateCh <- newText
   113  		}()
   114  		// Block a short while for the late text to arrive, in order to reduce
   115  		// flickering. Otherwise, return the text already computed, and pass the
   116  		// late result to lateCb in another goroutine.
   117  		select {
   118  		case late := <-lateCh:
   119  			return late, errors
   120  		case <-time.After(MaxBlockForLate):
   121  			go func() {
   122  				lateCb(<-lateCh)
   123  			}()
   124  			return text, errors
   125  		}
   126  	}
   127  	return text, errors
   128  }