未验证 提交 8e91d3b0 编写于 作者: A Alessandro Arzilli 提交者: GitHub

terminal: Go syntax highlighting for listings (#2294)

Fixes #1273
上级 c40774d3
......@@ -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
......
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'})
}
}
}
......@@ -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
......
......@@ -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:
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册