diff --git a/pkg/config/config.go b/pkg/config/config.go index 34fd64266041ebde9b8ae4ebc04cdd9ef9b96a35..f1a24e46b2c8a42135a69956c05b9b50476260df 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -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) @@ -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 diff --git a/pkg/terminal/colorize/colorize.go b/pkg/terminal/colorize/colorize.go new file mode 100644 index 0000000000000000000000000000000000000000..4083931c8b58f6d19ff430c4f91b596b28f5ecca --- /dev/null +++ b/pkg/terminal/colorize/colorize.go @@ -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'}) + } + } +} diff --git a/pkg/terminal/command.go b/pkg/terminal/command.go index c68dccbef3844e1c9bf0bbc4af6b8a38875636e5..81db7891079cbb37f18d8acf6883d0e744237506 100644 --- a/pkg/terminal/command.go +++ b/pkg/terminal/command.go @@ -24,6 +24,7 @@ import ( "github.com/cosiner/argv" "github.com/go-delve/delve/pkg/locspec" + "github.com/go-delve/delve/pkg/terminal/colorize" "github.com/go-delve/delve/service" "github.com/go-delve/delve/service/api" "github.com/go-delve/delve/service/rpc2" @@ -2247,7 +2248,7 @@ func printcontext(t *Term, state *api.DebuggerState) { if th.File == "" { fmt.Printf("Stopped at: 0x%x\n", state.CurrentThread.PC) - t.Println("=>", "no source available") + _ = colorize.Print(t.stdout, "", bytes.NewReader([]byte("no source available")), 1, 10, 1, nil) return } @@ -2424,6 +2425,13 @@ func printfile(t *Term, filename string, line int, showArrow bool) error { if filename == "" { return nil } + + lineCount := t.conf.GetSourceListLineCount() + arrowLine := 0 + if showArrow { + arrowLine = line + } + file, err := os.Open(t.substitutePath(filename)) if err != nil { return err @@ -2436,38 +2444,7 @@ func printfile(t *Term, filename string, line int, showArrow bool) error { fmt.Println("Warning: listing may not match stale executable") } - lineCount := t.conf.GetSourceListLineCount() - - buf := bufio.NewScanner(file) - l := line - for i := 1; i < l-lineCount; i++ { - if !buf.Scan() { - return nil - } - } - - s := l - lineCount - if s < 1 { - s = 1 - } - - for i := s; i <= l+lineCount; i++ { - if !buf.Scan() { - return nil - } - - var prefix string - if showArrow { - prefix = " " - if i == l { - prefix = "=>" - } - } - - prefix = fmt.Sprintf("%s%4d:\t", prefix, i) - t.Println(prefix, buf.Text()) - } - return nil + return colorize.Print(t.stdout, file.Name(), file, line-lineCount, line+lineCount+1, arrowLine, t.colorEscapes) } // ExitRequestError is returned when the user diff --git a/pkg/terminal/terminal.go b/pkg/terminal/terminal.go index f61cec9dae9a7dd595365da55579a06d91d068b1..e456a78ed4c8073a2b37323da74e4f258b5526e5 100644 --- a/pkg/terminal/terminal.go +++ b/pkg/terminal/terminal.go @@ -14,6 +14,7 @@ import ( "github.com/go-delve/delve/pkg/config" "github.com/go-delve/delve/pkg/locspec" + "github.com/go-delve/delve/pkg/terminal/colorize" "github.com/go-delve/delve/pkg/terminal/starbind" "github.com/go-delve/delve/service" "github.com/go-delve/delve/service/api" @@ -46,15 +47,15 @@ const ( // Term represents the terminal running dlv. type Term struct { - client service.Client - conf *config.Config - prompt string - line *liner.State - cmds *Commands - dumb bool - stdout io.Writer - InitFile string - displays []string + client service.Client + conf *config.Config + prompt string + line *liner.State + cmds *Commands + stdout io.Writer + InitFile string + displays []string + colorEscapes map[colorize.Style]string historyFile *os.File @@ -84,30 +85,41 @@ func New(client service.Client, conf *config.Config) *Term { conf = &config.Config{} } - var w io.Writer - - dumb := strings.ToLower(os.Getenv("TERM")) == "dumb" - if dumb { - w = os.Stdout - } else { - w = getColorableWriter() - } - - if (conf.SourceListLineColor > ansiWhite && - conf.SourceListLineColor < ansiBrBlack) || - conf.SourceListLineColor < ansiBlack || - conf.SourceListLineColor > ansiBrWhite { - conf.SourceListLineColor = ansiBlue - } - t := &Term{ client: client, conf: conf, prompt: "(dlv) ", line: liner.NewLiner(), cmds: cmds, - dumb: dumb, - stdout: w, + stdout: os.Stdout, + } + + if strings.ToLower(os.Getenv("TERM")) != "dumb" { + t.stdout = getColorableWriter() + t.colorEscapes = make(map[colorize.Style]string) + t.colorEscapes[colorize.NormalStyle] = terminalResetEscapeCode + wd := func(s string, defaultCode int) string { + if s == "" { + return fmt.Sprintf(terminalHighlightEscapeCode, defaultCode) + } + return s + } + t.colorEscapes[colorize.KeywordStyle] = conf.SourceListKeywordColor + t.colorEscapes[colorize.StringStyle] = wd(conf.SourceListStringColor, ansiBrGreen) + t.colorEscapes[colorize.NumberStyle] = conf.SourceListNumberColor + t.colorEscapes[colorize.CommentStyle] = wd(conf.SourceListCommentColor, ansiBrMagenta) + t.colorEscapes[colorize.ArrowStyle] = wd(conf.SourceListArrowColor, ansiBrYellow) + switch x := conf.SourceListLineColor.(type) { + case string: + t.colorEscapes[colorize.LineNoStyle] = x + case int: + if (x > ansiWhite && x < ansiBrBlack) || x < ansiBlack || x > ansiBrWhite { + x = ansiBlue + } + t.colorEscapes[colorize.LineNoStyle] = fmt.Sprintf(terminalHighlightEscapeCode, x) + case nil: + t.colorEscapes[colorize.LineNoStyle] = fmt.Sprintf(terminalHighlightEscapeCode, ansiBlue) + } } if client != nil { @@ -273,15 +285,6 @@ func (t *Term) Run() (int, error) { } } -// Println prints a line to the terminal. -func (t *Term) Println(prefix, str string) { - if !t.dumb { - terminalColorEscapeCode := fmt.Sprintf(terminalHighlightEscapeCode, t.conf.SourceListLineColor) - prefix = fmt.Sprintf("%s%s%s", terminalColorEscapeCode, prefix, terminalResetEscapeCode) - } - fmt.Fprintf(t.stdout, "%s%s\n", prefix, str) -} - // Substitutes directory to source file. // // Ensures that only directory is substituted, for example: