regions.go 6.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249
  1. package highlight
  2. import (
  3. "sort"
  4. "strings"
  5. "src.elv.sh/pkg/parse"
  6. "src.elv.sh/pkg/parse/cmpd"
  7. )
  8. var sourceText = parse.SourceText
  9. // Represents a region to be highlighted.
  10. type region struct {
  11. Begin int
  12. End int
  13. // Regions can be lexical or semantic. Lexical regions always correspond to
  14. // a leaf node in the parse tree, either a parse.Primary node or a parse.Sep
  15. // node. Semantic regions may span several leaves and override all lexical
  16. // regions in it.
  17. Kind regionKind
  18. // In lexical regions for Primary nodes, this field corresponds to the Type
  19. // field of the node (e.g. "bareword", "single-quoted"). In lexical regions
  20. // for Sep nodes, this field is simply the source text itself (e.g. "(",
  21. // "|"), except for comments, which have Type == "comment".
  22. //
  23. // In semantic regions, this field takes a value from a fixed list (see
  24. // below).
  25. Type string
  26. }
  27. type regionKind int
  28. // Region kinds.
  29. const (
  30. lexicalRegion regionKind = iota
  31. semanticRegion
  32. )
  33. // Lexical region types.
  34. const (
  35. barewordRegion = "bareword"
  36. singleQuotedRegion = "single-quoted"
  37. doubleQuotedRegion = "double-quoted"
  38. variableRegion = "variable" // Could also be semantic.
  39. wildcardRegion = "wildcard"
  40. tildeRegion = "tilde"
  41. // A comment region. Note that this is the only type of Sep leaf node that
  42. // is not identified by its text.
  43. commentRegion = "comment"
  44. )
  45. // Semantic region types.
  46. const (
  47. // A region when a string literal (bareword, single-quoted or double-quoted)
  48. // appears as a command.
  49. commandRegion = "command"
  50. // A region for keywords in special forms, like "else" in an "if" form.
  51. keywordRegion = "keyword"
  52. // A region of parse or compilation error.
  53. errorRegion = "error"
  54. )
  55. func getRegions(n parse.Node) []region {
  56. regions := getRegionsInner(n)
  57. regions = fixRegions(regions)
  58. return regions
  59. }
  60. func getRegionsInner(n parse.Node) []region {
  61. var regions []region
  62. emitRegions(n, func(n parse.Node, kind regionKind, typ string) {
  63. regions = append(regions, region{n.Range().From, n.Range().To, kind, typ})
  64. })
  65. return regions
  66. }
  67. func fixRegions(regions []region) []region {
  68. // Sort regions by the begin position, putting semantic regions before
  69. // lexical regions.
  70. sort.Slice(regions, func(i, j int) bool {
  71. if regions[i].Begin < regions[j].Begin {
  72. return true
  73. }
  74. if regions[i].Begin == regions[j].Begin {
  75. return regions[i].Kind == semanticRegion && regions[j].Kind == lexicalRegion
  76. }
  77. return false
  78. })
  79. // Remove overlapping regions, preferring the ones that appear earlier.
  80. var newRegions []region
  81. lastEnd := 0
  82. for _, r := range regions {
  83. if r.Begin < lastEnd {
  84. continue
  85. }
  86. newRegions = append(newRegions, r)
  87. lastEnd = r.End
  88. }
  89. return newRegions
  90. }
  91. func emitRegions(n parse.Node, f func(parse.Node, regionKind, string)) {
  92. switch n := n.(type) {
  93. case *parse.Form:
  94. emitRegionsInForm(n, f)
  95. case *parse.Primary:
  96. emitRegionsInPrimary(n, f)
  97. case *parse.Sep:
  98. emitRegionsInSep(n, f)
  99. }
  100. for _, child := range parse.Children(n) {
  101. emitRegions(child, f)
  102. }
  103. }
  104. func emitRegionsInForm(n *parse.Form, f func(parse.Node, regionKind, string)) {
  105. // Left hands of temporary assignments.
  106. for _, an := range n.Assignments {
  107. if an.Left != nil && an.Left.Head != nil {
  108. f(an.Left.Head, semanticRegion, variableRegion)
  109. }
  110. }
  111. if n.Head == nil {
  112. return
  113. }
  114. // Special forms.
  115. // TODO: This only highlights bareword special commands, however currently
  116. // quoted special commands are also possible (e.g `"if" $true { }` is
  117. // accepted).
  118. head := sourceText(n.Head)
  119. switch head {
  120. case "var", "set", "tmp":
  121. emitRegionsInAssign(n, f)
  122. case "if":
  123. emitRegionsInIf(n, f)
  124. case "for":
  125. emitRegionsInFor(n, f)
  126. case "try":
  127. emitRegionsInTry(n, f)
  128. }
  129. if isBarewordCompound(n.Head) {
  130. f(n.Head, semanticRegion, commandRegion)
  131. }
  132. }
  133. func emitRegionsInAssign(n *parse.Form, f func(parse.Node, regionKind, string)) {
  134. // Highlight all LHS, and = as a keyword.
  135. for _, arg := range n.Args {
  136. if parse.SourceText(arg) == "=" {
  137. f(arg, semanticRegion, keywordRegion)
  138. break
  139. }
  140. emitVariableRegion(arg, f)
  141. }
  142. }
  143. func emitVariableRegion(n *parse.Compound, f func(parse.Node, regionKind, string)) {
  144. // Only handle valid LHS here. Invalid LHS will result in a compile error
  145. // and highlighted as an error accordingly.
  146. if n != nil && len(n.Indexings) == 1 && n.Indexings[0].Head != nil {
  147. f(n.Indexings[0].Head, semanticRegion, variableRegion)
  148. }
  149. }
  150. func isBarewordCompound(n *parse.Compound) bool {
  151. return len(n.Indexings) == 1 && len(n.Indexings[0].Indices) == 0 && n.Indexings[0].Head.Type == parse.Bareword
  152. }
  153. func emitRegionsInIf(n *parse.Form, f func(parse.Node, regionKind, string)) {
  154. // Highlight all "elif" and "else".
  155. for i := 2; i < len(n.Args); i += 2 {
  156. arg := n.Args[i]
  157. if s := sourceText(arg); s == "elif" || s == "else" {
  158. f(arg, semanticRegion, keywordRegion)
  159. }
  160. }
  161. }
  162. func emitRegionsInFor(n *parse.Form, f func(parse.Node, regionKind, string)) {
  163. // Highlight the iterating variable.
  164. if 0 < len(n.Args) && len(n.Args[0].Indexings) > 0 {
  165. f(n.Args[0].Indexings[0].Head, semanticRegion, variableRegion)
  166. }
  167. // Highlight "else".
  168. if 3 < len(n.Args) && sourceText(n.Args[3]) == "else" {
  169. f(n.Args[3], semanticRegion, keywordRegion)
  170. }
  171. }
  172. func emitRegionsInTry(n *parse.Form, f func(parse.Node, regionKind, string)) {
  173. // Highlight "except", the exception variable after it, "else" and
  174. // "finally".
  175. i := 1
  176. matchKW := func(text string) bool {
  177. if i < len(n.Args) && sourceText(n.Args[i]) == text {
  178. f(n.Args[i], semanticRegion, keywordRegion)
  179. return true
  180. }
  181. return false
  182. }
  183. if matchKW("except") {
  184. if i+1 < len(n.Args) && isStringLiteral(n.Args[i+1]) {
  185. f(n.Args[i+1], semanticRegion, variableRegion)
  186. i += 3
  187. } else {
  188. i += 2
  189. }
  190. }
  191. if matchKW("else") {
  192. i += 2
  193. }
  194. matchKW("finally")
  195. }
  196. func isStringLiteral(n *parse.Compound) bool {
  197. _, ok := cmpd.StringLiteral(n)
  198. return ok
  199. }
  200. func emitRegionsInPrimary(n *parse.Primary, f func(parse.Node, regionKind, string)) {
  201. switch n.Type {
  202. case parse.Bareword:
  203. f(n, lexicalRegion, barewordRegion)
  204. case parse.SingleQuoted:
  205. f(n, lexicalRegion, singleQuotedRegion)
  206. case parse.DoubleQuoted:
  207. f(n, lexicalRegion, doubleQuotedRegion)
  208. case parse.Variable:
  209. f(n, lexicalRegion, variableRegion)
  210. case parse.Wildcard:
  211. f(n, lexicalRegion, wildcardRegion)
  212. case parse.Tilde:
  213. f(n, lexicalRegion, tildeRegion)
  214. }
  215. }
  216. func emitRegionsInSep(n *parse.Sep, f func(parse.Node, regionKind, string)) {
  217. text := sourceText(n)
  218. trimmed := strings.TrimLeftFunc(text, parse.IsWhitespace)
  219. switch {
  220. case trimmed == "":
  221. // Don't do anything; whitespaces do not get highlighted.
  222. case strings.HasPrefix(trimmed, "#"):
  223. f(n, lexicalRegion, commentRegion)
  224. default:
  225. f(n, lexicalRegion, text)
  226. }
  227. }