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 }