Skip to content

Commit

Permalink
terminal: Go syntax highlighting for listings (go-delve#2294)
Browse files Browse the repository at this point in the history
  • Loading branch information
aarzilli authored and zhaixiaojuan committed Apr 25, 2021
1 parent d9c5ad1 commit c9f5a3c
Show file tree
Hide file tree
Showing 4 changed files with 373 additions and 71 deletions.
28 changes: 26 additions & 2 deletions pkg/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,8 +54,24 @@ type Config struct {
ShowLocationExpr bool `yaml:"show-location-expr"`

// Source list line-number color (3/4 bit color codes as defined
// here: https://en.wikipedia.org/wiki/ANSI_escape_code#Colors)
SourceListLineColor int `yaml:"source-list-line-color"`
// here: https://en.wikipedia.org/wiki/ANSI_escape_code#Colors),
// or a string containing a terminal escape sequence.
SourceListLineColor interface{} `yaml:"source-list-line-color"`

// Source list arrow color, as a terminal escape sequence.
SourceListArrowColor string `yaml:"source-list-arrow-color"`

// Source list keyword color, as a terminal escape sequence.
SourceListKeywordColor string `yaml:"source-list-keyword-color"`

// Source list string color, as a terminal escape sequence.
SourceListStringColor string `yaml:"source-list-string-color"`

// Source list number color, as a terminal escape sequence.
SourceListNumberColor string `yaml:"source-list-number-color"`

// Source list comment color, as a terminal escape sequence.
SourceListCommentColor string `yaml:"source-list-comment-color"`

// number of lines to list above and below cursor when printfile() is
// called (i.e. when execution stops, listCommand is used, etc)
Expand Down Expand Up @@ -215,8 +231,16 @@ func writeDefaultConfig(f *os.File) error {
# Uncomment the following line and set your preferred ANSI foreground color
# for source line numbers in the (list) command (if unset, default is 34,
# dark blue) See https://en.wikipedia.org/wiki/ANSI_escape_code#3/4_bit
# Alternatively a string containing an escape sequence can also be used.
# source-list-line-color: 34
# Uncomment the following lines to change the colors used by syntax highlighting.
# source-list-keyword-color: "\x1b[0m"
# source-list-string-color: "\x1b[92m"
# source-list-number-color: "\x1b[0m"
# source-list-comment-color: "\x1b[95m"
# source-list-arrow-color: "\x1b[93m"
# Uncomment to change the number of lines printed above and below cursor when
# listing source code.
# source-list-line-count: 5
Expand Down
298 changes: 298 additions & 0 deletions pkg/terminal/colorize/colorize.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,298 @@
package colorize

import (
"fmt"
"go/ast"
"go/parser"
"go/token"
"io"
"io/ioutil"
"path/filepath"
"reflect"
"sort"
)

// Style describes the style of a chunk of text.
type Style uint8

const (
NormalStyle Style = iota
KeywordStyle
StringStyle
NumberStyle
CommentStyle
LineNoStyle
ArrowStyle
)

// Print prints to out a syntax highlighted version of the text read from
// reader, between lines startLine and endLine.
func Print(out io.Writer, path string, reader io.Reader, startLine, endLine, arrowLine int, colorEscapes map[Style]string) error {
buf, err := ioutil.ReadAll(reader)
if err != nil {
return err
}

w := &lineWriter{w: out, lineRange: [2]int{startLine, endLine}, arrowLine: arrowLine, colorEscapes: colorEscapes}

if filepath.Ext(path) != ".go" {
w.Write(NormalStyle, buf, true)
return nil
}

var fset token.FileSet
f, err := parser.ParseFile(&fset, path, buf, parser.ParseComments)
if err != nil {
w.Write(NormalStyle, buf, true)
return nil
}

var base int

fset.Iterate(func(file *token.File) bool {
base = file.Base()
return false
})

toks := []colorTok{}

emit := func(tok token.Token, start, end token.Pos) {
if _, ok := tokenToStyle[tok]; !ok {
return
}
start -= token.Pos(base)
if end == token.NoPos {
// end == token.NoPos it's a keyword and we have to find where it ends by looking at the file
for end = start; end < token.Pos(len(buf)); end++ {
if buf[end] < 'a' || buf[end] > 'z' {
break
}
}
} else {
end -= token.Pos(base)
}
if start < 0 || start >= end || end > token.Pos(len(buf)) {
// invalid token?
return
}
toks = append(toks, colorTok{tok, int(start), int(end)})
}

emit(token.PACKAGE, f.Package, token.NoPos)

for _, cgrp := range f.Comments {
for _, cmnt := range cgrp.List {
emit(token.COMMENT, cmnt.Pos(), cmnt.End())
}
}

ast.Inspect(f, func(n ast.Node) bool {
if n == nil {
return true
}

switch n := n.(type) {
case *ast.BasicLit:
emit(n.Kind, n.Pos(), n.End())
return true
case *ast.Ident:
//TODO(aarzilli): builtin functions? basic types?
return true
case *ast.IfStmt:
emit(token.IF, n.If, token.NoPos)
if n.Else != nil {
for elsepos := int(n.Body.End()) - base; elsepos < len(buf)-4; elsepos++ {
if string(buf[elsepos:][:4]) == "else" {
emit(token.ELSE, token.Pos(elsepos+base), token.Pos(elsepos+base+4))
break
}
}
}
return true
}

nval := reflect.ValueOf(n)
if nval.Kind() != reflect.Ptr {
return true
}
nval = nval.Elem()
if nval.Kind() != reflect.Struct {
return true
}

tokposval := nval.FieldByName("TokPos")
tokval := nval.FieldByName("Tok")
if tokposval != (reflect.Value{}) && tokval != (reflect.Value{}) {
emit(tokval.Interface().(token.Token), tokposval.Interface().(token.Pos), token.NoPos)
}

for _, kwname := range []string{"Case", "Begin", "Defer", "Pacakge", "For", "Func", "Go", "Interface", "Map", "Return", "Select", "Struct", "Switch"} {
kwposval := nval.FieldByName(kwname)
if kwposval != (reflect.Value{}) {
kwpos, ok := kwposval.Interface().(token.Pos)
if ok {
emit(token.ILLEGAL, kwpos, token.NoPos)
}
}
}

return true
})

sort.Slice(toks, func(i, j int) bool { return toks[i].start < toks[j].start })

flush := func(start, end int, style Style) {
if start < end {
w.Write(style, buf[start:end], end == len(buf))
}
}

cur := 0
for _, tok := range toks {
flush(cur, tok.start, NormalStyle)
flush(tok.start, tok.end, tokenToStyle[tok.tok])
cur = tok.end
}
if cur != len(buf) {
flush(cur, len(buf), NormalStyle)
}

return nil
}

var tokenToStyle = map[token.Token]Style{
token.ILLEGAL: KeywordStyle,
token.COMMENT: CommentStyle,
token.INT: NumberStyle,
token.FLOAT: NumberStyle,
token.IMAG: NumberStyle,
token.CHAR: StringStyle,
token.STRING: StringStyle,
token.BREAK: KeywordStyle,
token.CASE: KeywordStyle,
token.CHAN: KeywordStyle,
token.CONST: KeywordStyle,
token.CONTINUE: KeywordStyle,
token.DEFAULT: KeywordStyle,
token.DEFER: KeywordStyle,
token.ELSE: KeywordStyle,
token.FALLTHROUGH: KeywordStyle,
token.FOR: KeywordStyle,
token.FUNC: KeywordStyle,
token.GO: KeywordStyle,
token.GOTO: KeywordStyle,
token.IF: KeywordStyle,
token.IMPORT: KeywordStyle,
token.INTERFACE: KeywordStyle,
token.MAP: KeywordStyle,
token.PACKAGE: KeywordStyle,
token.RANGE: KeywordStyle,
token.RETURN: KeywordStyle,
token.SELECT: KeywordStyle,
token.STRUCT: KeywordStyle,
token.SWITCH: KeywordStyle,
token.TYPE: KeywordStyle,
token.VAR: KeywordStyle,
}

type colorTok struct {
tok token.Token // the token type or ILLEGAL for keywords
start, end int // start and end positions of the token
}

type lineWriter struct {
w io.Writer
lineRange [2]int
arrowLine int

curStyle Style
started bool
lineno int

colorEscapes map[Style]string
}

func (w *lineWriter) style(style Style) {
if w.colorEscapes == nil {
return
}
esc := w.colorEscapes[style]
if esc == "" {
esc = w.colorEscapes[NormalStyle]
}
fmt.Fprintf(w.w, "%s", esc)
}

func (w *lineWriter) inrange() bool {
lno := w.lineno
if !w.started {
lno = w.lineno + 1
}
return lno >= w.lineRange[0] && lno < w.lineRange[1]
}

func (w *lineWriter) nl() {
w.lineno++
if !w.inrange() || !w.started {
return
}
w.style(ArrowStyle)
if w.lineno == w.arrowLine {
fmt.Fprintf(w.w, "=>")
} else {
fmt.Fprintf(w.w, " ")
}
w.style(LineNoStyle)
fmt.Fprintf(w.w, "%4d:\t", w.lineno)
w.style(w.curStyle)
}

func (w *lineWriter) writeInternal(style Style, data []byte) {
if !w.inrange() {
return
}

if !w.started {
w.started = true
w.curStyle = style
w.nl()
} else if w.curStyle != style {
w.curStyle = style
w.style(w.curStyle)
}

w.w.Write(data)
}

func (w *lineWriter) Write(style Style, data []byte, last bool) {
cur := 0
for i := range data {
if data[i] == '\n' {
if last && i == len(data)-1 {
w.writeInternal(style, data[cur:i])
if w.curStyle != NormalStyle {
w.style(NormalStyle)
}
if w.inrange() {
w.w.Write([]byte{'\n'})
}
last = false
} else {
w.writeInternal(style, data[cur:i+1])
w.nl()
}
cur = i + 1
}
}
if cur < len(data) {
w.writeInternal(style, data[cur:])
}
if last {
if w.curStyle != NormalStyle {
w.style(NormalStyle)
}
if w.inrange() {
w.w.Write([]byte{'\n'})
}
}
}
Loading

0 comments on commit c9f5a3c

Please sign in to comment.