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 }