github.com/v2fly/tools@v0.100.0/internal/lsp/cmd/semantictokens.go (about)

     1  // Copyright 2020 The Go Authors. All rights reserved.
     2  // Use of this source code is governed by a BSD-style
     3  // license that can be found in the LICENSE file.
     4  
     5  package cmd
     6  
     7  import (
     8  	"bytes"
     9  	"context"
    10  	"flag"
    11  	"fmt"
    12  	"go/parser"
    13  	"go/token"
    14  	"io/ioutil"
    15  	"log"
    16  	"os"
    17  	"runtime"
    18  	"unicode/utf8"
    19  
    20  	"github.com/v2fly/tools/internal/lsp"
    21  	"github.com/v2fly/tools/internal/lsp/protocol"
    22  	"github.com/v2fly/tools/internal/lsp/source"
    23  	"github.com/v2fly/tools/internal/span"
    24  )
    25  
    26  // generate semantic tokens and interpolate them in the file
    27  
    28  // The output is the input file decorated with comments showing the
    29  // syntactic tokens. The comments are stylized:
    30  //   /*<arrow><length>,<token type>,[<modifiers]*/
    31  // For most occurrences, the comment comes just before the token it
    32  // describes, and arrow is a right arrow. If the token is inside a string
    33  // the comment comes just after the string, and the arrow is a left arrow.
    34  // <length> is the length of the token in runes, <token type> is one
    35  // of the supported semantic token types, and <modifiers. is a
    36  // (possibly empty) list of token type modifiers.
    37  
    38  // There are 3 coordinate systems for lines and character offsets in lines
    39  // LSP (what's returned from semanticTokens()):
    40  //    0-based: the first line is line 0, the first character of a line
    41  //      is character 0, and characters are counted as UTF-16 code points
    42  // gopls (and Go error messages):
    43  //    1-based: the first line is line1, the first chararcter of a line
    44  //      is character 0, and characters are counted as bytes
    45  // internal (as used in marks, and lines:=bytes.Split(buf, '\n'))
    46  //    0-based: lines and character positions are 1 less than in
    47  //      the gopls coordinate system
    48  
    49  type semtok struct {
    50  	app *Application
    51  }
    52  
    53  var colmap *protocol.ColumnMapper
    54  
    55  func (c *semtok) Name() string      { return "semtok" }
    56  func (c *semtok) Usage() string     { return "<filename>" }
    57  func (c *semtok) ShortHelp() string { return "show semantic tokens for the specified file" }
    58  func (c *semtok) DetailedHelp(f *flag.FlagSet) {
    59  	for i := 1; ; i++ {
    60  		_, f, l, ok := runtime.Caller(i)
    61  		if !ok {
    62  			break
    63  		}
    64  		log.Printf("%d: %s:%d", i, f, l)
    65  	}
    66  	fmt.Fprint(f.Output(), `
    67  Example: show the semantic tokens for this file:
    68  
    69    $ gopls semtok internal/lsp/cmd/semtok.go
    70  `)
    71  	f.PrintDefaults()
    72  }
    73  
    74  // Run performs the semtok on the files specified by args and prints the
    75  // results to stdout in the format described above.
    76  func (c *semtok) Run(ctx context.Context, args ...string) error {
    77  	if len(args) != 1 {
    78  		return fmt.Errorf("expected one file name, got %d", len(args))
    79  	}
    80  	// perhaps simpler if app had just had a FlagSet member
    81  	origOptions := c.app.options
    82  	c.app.options = func(opts *source.Options) {
    83  		origOptions(opts)
    84  		opts.SemanticTokens = true
    85  	}
    86  	conn, err := c.app.connect(ctx)
    87  	if err != nil {
    88  		return err
    89  	}
    90  	defer conn.terminate(ctx)
    91  	uri := span.URIFromPath(args[0])
    92  	file := conn.AddFile(ctx, uri)
    93  	if file.err != nil {
    94  		return file.err
    95  	}
    96  
    97  	buf, err := ioutil.ReadFile(args[0])
    98  	if err != nil {
    99  		return err
   100  	}
   101  	lines := bytes.Split(buf, []byte{'\n'})
   102  	p := &protocol.SemanticTokensRangeParams{
   103  		TextDocument: protocol.TextDocumentIdentifier{
   104  			URI: protocol.URIFromSpanURI(uri),
   105  		},
   106  		Range: protocol.Range{Start: protocol.Position{Line: 0, Character: 0},
   107  			End: protocol.Position{
   108  				Line:      uint32(len(lines) - 1),
   109  				Character: uint32(len(lines[len(lines)-1]))},
   110  		},
   111  	}
   112  	resp, err := conn.semanticTokens(ctx, p)
   113  	if err != nil {
   114  		return err
   115  	}
   116  	fset := token.NewFileSet()
   117  	f, err := parser.ParseFile(fset, args[0], buf, 0)
   118  	if err != nil {
   119  		log.Printf("parsing %s failed %v", args[0], err)
   120  		return err
   121  	}
   122  	tok := fset.File(f.Pos())
   123  	if tok == nil {
   124  		// can't happen; just parsed this file
   125  		return fmt.Errorf("can't find %s in fset", args[0])
   126  	}
   127  	tc := span.NewContentConverter(args[0], buf)
   128  	colmap = &protocol.ColumnMapper{
   129  		URI:       span.URI(args[0]),
   130  		Content:   buf,
   131  		Converter: tc,
   132  	}
   133  	err = decorate(file.uri.Filename(), resp.Data)
   134  	if err != nil {
   135  		return err
   136  	}
   137  	return nil
   138  }
   139  
   140  type mark struct {
   141  	line, offset int // 1-based, from RangeSpan
   142  	len          int // bytes, not runes
   143  	typ          string
   144  	mods         []string
   145  }
   146  
   147  // prefixes for semantic token comments
   148  const (
   149  	SemanticLeft  = "/*⇐"
   150  	SemanticRight = "/*⇒"
   151  )
   152  
   153  func markLine(m mark, lines [][]byte) {
   154  	l := lines[m.line-1] // mx is 1-based
   155  	length := utf8.RuneCount(l[m.offset-1 : m.offset-1+m.len])
   156  	splitAt := m.offset - 1
   157  	insert := ""
   158  	if m.typ == "namespace" && m.offset-1+m.len < len(l) && l[m.offset-1+m.len] == '"' {
   159  		// it is the last component of an import spec
   160  		// cannot put a comment inside a string
   161  		insert = fmt.Sprintf("%s%d,namespace,[]*/", SemanticLeft, length)
   162  		splitAt = m.offset + m.len
   163  	} else {
   164  		// be careful not to generate //*
   165  		spacer := ""
   166  		if splitAt-1 >= 0 && l[splitAt-1] == '/' {
   167  			spacer = " "
   168  		}
   169  		insert = fmt.Sprintf("%s%s%d,%s,%v*/", spacer, SemanticRight, length, m.typ, m.mods)
   170  	}
   171  	x := append([]byte(insert), l[splitAt:]...)
   172  	l = append(l[:splitAt], x...)
   173  	lines[m.line-1] = l
   174  }
   175  
   176  func decorate(file string, result []uint32) error {
   177  	buf, err := ioutil.ReadFile(file)
   178  	if err != nil {
   179  		return err
   180  	}
   181  	marks := newMarks(result)
   182  	if len(marks) == 0 {
   183  		return nil
   184  	}
   185  	lines := bytes.Split(buf, []byte{'\n'})
   186  	for i := len(marks) - 1; i >= 0; i-- {
   187  		mx := marks[i]
   188  		markLine(mx, lines)
   189  	}
   190  	os.Stdout.Write(bytes.Join(lines, []byte{'\n'}))
   191  	return nil
   192  }
   193  
   194  func newMarks(d []uint32) []mark {
   195  	ans := []mark{}
   196  	// the following two loops could be merged, at the cost
   197  	// of making the logic slightly more complicated to understand
   198  	// first, convert from deltas to absolute, in LSP coordinates
   199  	lspLine := make([]uint32, len(d)/5)
   200  	lspChar := make([]uint32, len(d)/5)
   201  	var line, char uint32
   202  	for i := 0; 5*i < len(d); i++ {
   203  		lspLine[i] = line + d[5*i+0]
   204  		if d[5*i+0] > 0 {
   205  			char = 0
   206  		}
   207  		lspChar[i] = char + d[5*i+1]
   208  		char = lspChar[i]
   209  		line = lspLine[i]
   210  	}
   211  	// second, convert to gopls coordinates
   212  	for i := 0; 5*i < len(d); i++ {
   213  		pr := protocol.Range{
   214  			Start: protocol.Position{
   215  				Line:      lspLine[i],
   216  				Character: lspChar[i],
   217  			},
   218  			End: protocol.Position{
   219  				Line:      lspLine[i],
   220  				Character: lspChar[i] + d[5*i+2],
   221  			},
   222  		}
   223  		spn, err := colmap.RangeSpan(pr)
   224  		if err != nil {
   225  			log.Fatal(err)
   226  		}
   227  		m := mark{
   228  			line:   spn.Start().Line(),
   229  			offset: spn.Start().Column(),
   230  			len:    spn.End().Column() - spn.Start().Column(),
   231  			typ:    lsp.SemType(int(d[5*i+3])),
   232  			mods:   lsp.SemMods(int(d[5*i+4])),
   233  		}
   234  		ans = append(ans, m)
   235  	}
   236  	return ans
   237  }