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