highlight.go 3.3 KB

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