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 }