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  }