src.elv.sh@v0.21.0-dev.0.20240515223629-06979efb9a2a/pkg/edit/highlight/highlight.go (about)

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