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