golang.org/x/tools/gopls@v0.15.3/internal/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  	"log"
    13  	"os"
    14  	"unicode/utf8"
    15  
    16  	"golang.org/x/tools/gopls/internal/protocol"
    17  	"golang.org/x/tools/gopls/internal/settings"
    18  )
    19  
    20  // generate semantic tokens and interpolate them in the file
    21  
    22  // The output is the input file decorated with comments showing the
    23  // syntactic tokens. The comments are stylized:
    24  //   /*<arrow><length>,<token type>,[<modifiers]*/
    25  // For most occurrences, the comment comes just before the token it
    26  // describes, and arrow is a right arrow. If the token is inside a string
    27  // the comment comes just after the string, and the arrow is a left arrow.
    28  // <length> is the length of the token in runes, <token type> is one
    29  // of the supported semantic token types, and <modifiers. is a
    30  // (possibly empty) list of token type modifiers.
    31  
    32  // There are 3 coordinate systems for lines and character offsets in lines
    33  // LSP (what's returned from semanticTokens()):
    34  //    0-based: the first line is line 0, the first character of a line
    35  //      is character 0, and characters are counted as UTF-16 code points
    36  // gopls (and Go error messages):
    37  //    1-based: the first line is line1, the first character of a line
    38  //      is character 0, and characters are counted as bytes
    39  // internal (as used in marks, and lines:=bytes.Split(buf, '\n'))
    40  //    0-based: lines and character positions are 1 less than in
    41  //      the gopls coordinate system
    42  
    43  type semtok struct {
    44  	app *Application
    45  }
    46  
    47  func (c *semtok) Name() string      { return "semtok" }
    48  func (c *semtok) Parent() string    { return c.app.Name() }
    49  func (c *semtok) Usage() string     { return "<filename>" }
    50  func (c *semtok) ShortHelp() string { return "show semantic tokens for the specified file" }
    51  func (c *semtok) DetailedHelp(f *flag.FlagSet) {
    52  	fmt.Fprint(f.Output(), `
    53  Example: show the semantic tokens for this file:
    54  
    55  	$ gopls semtok internal/cmd/semtok.go
    56  `)
    57  	printFlagDefaults(f)
    58  }
    59  
    60  // Run performs the semtok on the files specified by args and prints the
    61  // results to stdout in the format described above.
    62  func (c *semtok) Run(ctx context.Context, args ...string) error {
    63  	if len(args) != 1 {
    64  		return fmt.Errorf("expected one file name, got %d", len(args))
    65  	}
    66  	// perhaps simpler if app had just had a FlagSet member
    67  	origOptions := c.app.options
    68  	c.app.options = func(opts *settings.Options) {
    69  		origOptions(opts)
    70  		opts.SemanticTokens = true
    71  	}
    72  	conn, err := c.app.connect(ctx, nil)
    73  	if err != nil {
    74  		return err
    75  	}
    76  	defer conn.terminate(ctx)
    77  	uri := protocol.URIFromPath(args[0])
    78  	file, err := conn.openFile(ctx, uri)
    79  	if err != nil {
    80  		return err
    81  	}
    82  
    83  	lines := bytes.Split(file.mapper.Content, []byte{'\n'})
    84  	p := &protocol.SemanticTokensRangeParams{
    85  		TextDocument: protocol.TextDocumentIdentifier{
    86  			URI: uri,
    87  		},
    88  		Range: protocol.Range{Start: protocol.Position{Line: 0, Character: 0},
    89  			End: protocol.Position{
    90  				Line:      uint32(len(lines) - 1),
    91  				Character: uint32(len(lines[len(lines)-1]))},
    92  		},
    93  	}
    94  	resp, err := conn.semanticTokens(ctx, p)
    95  	if err != nil {
    96  		return err
    97  	}
    98  	return decorate(file, resp.Data)
    99  }
   100  
   101  type mark struct {
   102  	line, offset int // 1-based, from RangeSpan
   103  	len          int // bytes, not runes
   104  	typ          string
   105  	mods         []string
   106  }
   107  
   108  // prefixes for semantic token comments
   109  const (
   110  	SemanticLeft  = "/*⇐"
   111  	SemanticRight = "/*⇒"
   112  )
   113  
   114  func markLine(m mark, lines [][]byte) {
   115  	l := lines[m.line-1] // mx is 1-based
   116  	length := utf8.RuneCount(l[m.offset-1 : m.offset-1+m.len])
   117  	splitAt := m.offset - 1
   118  	insert := ""
   119  	if m.typ == "namespace" && m.offset-1+m.len < len(l) && l[m.offset-1+m.len] == '"' {
   120  		// it is the last component of an import spec
   121  		// cannot put a comment inside a string
   122  		insert = fmt.Sprintf("%s%d,namespace,[]*/", SemanticLeft, length)
   123  		splitAt = m.offset + m.len
   124  	} else {
   125  		// be careful not to generate //*
   126  		spacer := ""
   127  		if splitAt-1 >= 0 && l[splitAt-1] == '/' {
   128  			spacer = " "
   129  		}
   130  		insert = fmt.Sprintf("%s%s%d,%s,%v*/", spacer, SemanticRight, length, m.typ, m.mods)
   131  	}
   132  	x := append([]byte(insert), l[splitAt:]...)
   133  	l = append(l[:splitAt], x...)
   134  	lines[m.line-1] = l
   135  }
   136  
   137  func decorate(file *cmdFile, result []uint32) error {
   138  	marks := newMarks(file, result)
   139  	if len(marks) == 0 {
   140  		return nil
   141  	}
   142  	lines := bytes.Split(file.mapper.Content, []byte{'\n'})
   143  	for i := len(marks) - 1; i >= 0; i-- {
   144  		mx := marks[i]
   145  		markLine(mx, lines)
   146  	}
   147  	os.Stdout.Write(bytes.Join(lines, []byte{'\n'}))
   148  	return nil
   149  }
   150  
   151  func newMarks(file *cmdFile, d []uint32) []mark {
   152  	ans := []mark{}
   153  	// the following two loops could be merged, at the cost
   154  	// of making the logic slightly more complicated to understand
   155  	// first, convert from deltas to absolute, in LSP coordinates
   156  	lspLine := make([]uint32, len(d)/5)
   157  	lspChar := make([]uint32, len(d)/5)
   158  	var line, char uint32
   159  	for i := 0; 5*i < len(d); i++ {
   160  		lspLine[i] = line + d[5*i+0]
   161  		if d[5*i+0] > 0 {
   162  			char = 0
   163  		}
   164  		lspChar[i] = char + d[5*i+1]
   165  		char = lspChar[i]
   166  		line = lspLine[i]
   167  	}
   168  	// second, convert to gopls coordinates
   169  	for i := 0; 5*i < len(d); i++ {
   170  		pr := protocol.Range{
   171  			Start: protocol.Position{
   172  				Line:      lspLine[i],
   173  				Character: lspChar[i],
   174  			},
   175  			End: protocol.Position{
   176  				Line:      lspLine[i],
   177  				Character: lspChar[i] + d[5*i+2],
   178  			},
   179  		}
   180  		spn, err := file.rangeSpan(pr)
   181  		if err != nil {
   182  			log.Fatal(err)
   183  		}
   184  		m := mark{
   185  			line:   spn.Start().Line(),
   186  			offset: spn.Start().Column(),
   187  			len:    spn.End().Column() - spn.Start().Column(),
   188  			typ:    protocol.SemType(int(d[5*i+3])),
   189  			mods:   protocol.SemMods(int(d[5*i+4])),
   190  		}
   191  		ans = append(ans, m)
   192  	}
   193  	return ans
   194  }