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 }