Skip to content

Commit 5ba1764

Browse files
committed
feat: initial support for cxx collect
1. Since clangd does not support semanticTokens/range method, use semanticTokens/full + filtering to emulate. 2. Since the concept of package and module does not apply to C/C++, treat the whole repo as a single package/module.
1 parent ddca707 commit 5ba1764

File tree

2 files changed

+181
-14
lines changed

2 files changed

+181
-14
lines changed

lang/cxx/spec.go

Lines changed: 127 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,12 @@
1515
package cxx
1616

1717
import (
18+
"fmt"
19+
"path/filepath"
20+
"strings"
21+
1822
lsp "github.com/cloudwego/abcoder/lang/lsp"
23+
"github.com/cloudwego/abcoder/lang/utils"
1924
)
2025

2126
type CxxSpec struct {
@@ -26,56 +31,165 @@ func NewCxxSpec() *CxxSpec {
2631
return &CxxSpec{}
2732
}
2833

34+
// XXX: maybe multi module support for C++?
2935
func (c *CxxSpec) WorkSpace(root string) (map[string]string, error) {
30-
panic("TODO")
36+
c.repo = root
37+
rets := map[string]string{}
38+
absPath, err := filepath.Abs(root)
39+
if err != nil {
40+
return nil, fmt.Errorf("failed to get absolute path: %w", err)
41+
}
42+
rets["current"] = absPath
43+
return rets, nil
3144
}
3245

46+
// returns: mod, path, error
3347
func (c *CxxSpec) NameSpace(path string) (string, string, error) {
34-
panic("TODO")
48+
// external lib: only standard library (system headers), in /usr/
49+
if !strings.HasPrefix(path, c.repo) {
50+
if strings.HasPrefix(path, "/usr") {
51+
// assume it is c system library
52+
return "cstdlib", "cstdlib", nil
53+
}
54+
panic(fmt.Sprintf("external lib: %s\n", path))
55+
}
56+
57+
return "current", "current", nil
58+
3559
}
3660

3761
func (c *CxxSpec) ShouldSkip(path string) bool {
38-
panic("TODO")
62+
if !strings.HasSuffix(path, ".c") {
63+
return true
64+
}
65+
return false
66+
}
67+
68+
func (c *CxxSpec) IsDocToken(tok lsp.Token) bool {
69+
return tok.Type == "comment"
3970
}
4071

4172
func (c *CxxSpec) DeclareTokenOfSymbol(sym lsp.DocumentSymbol) int {
42-
panic("TODO")
73+
for i, t := range sym.Tokens {
74+
if c.IsDocToken(t) {
75+
continue
76+
}
77+
for _, m := range t.Modifiers {
78+
if m == "declaration" {
79+
return i
80+
}
81+
}
82+
}
83+
return -1
4384
}
4485

4586
func (c *CxxSpec) IsEntityToken(tok lsp.Token) bool {
46-
panic("TODO")
87+
return tok.Type == "class" || tok.Type == "function" || tok.Type == "variable"
4788
}
4889

4990
func (c *CxxSpec) IsStdToken(tok lsp.Token) bool {
5091
panic("TODO")
5192
}
5293

5394
func (c *CxxSpec) TokenKind(tok lsp.Token) lsp.SymbolKind {
54-
panic("TODO")
95+
switch tok.Type {
96+
case "class":
97+
return lsp.SKStruct
98+
case "enum":
99+
return lsp.SKEnum
100+
case "enumMember":
101+
return lsp.SKEnumMember
102+
case "function", "macro":
103+
return lsp.SKFunction
104+
// rust spec does not treat parameter as a variable
105+
case "parameter":
106+
return lsp.SKVariable
107+
case "typeParameter":
108+
return lsp.SKTypeParameter
109+
// type: TODO
110+
case "interface", "concept", "method", "modifier", "namespace", "type":
111+
panic(fmt.Sprintf("Unsupported token type: %s at %+v\n", tok.Type, tok.Location))
112+
case "bracket", "comment", "label", "operator", "property", "unknown":
113+
return lsp.SKUnknown
114+
}
115+
panic(fmt.Sprintf("Weird token type: %s at %+v\n", tok.Type, tok.Location))
55116
}
56117

57118
func (c *CxxSpec) IsMainFunction(sym lsp.DocumentSymbol) bool {
58-
panic("TODO")
119+
return sym.Kind == lsp.SKFunction && sym.Name == "main"
59120
}
60121

61122
func (c *CxxSpec) IsEntitySymbol(sym lsp.DocumentSymbol) bool {
62-
panic("TODO")
63-
}
123+
typ := sym.Kind
124+
return typ == lsp.SKFunction || typ == lsp.SKVariable || typ == lsp.SKClass
64125

65-
func (c *CxxSpec) IsPublicSymbol(sym lsp.DocumentSymbol) bool {
66-
panic("TODO")
67126
}
68127

128+
func (c *CxxSpec) IsPublicSymbol(sym lsp.DocumentSymbol) bool {
129+
id := c.DeclareTokenOfSymbol(sym)
130+
if id == -1 {
131+
return false
132+
}
133+
for _, m := range sym.Tokens[id].Modifiers {
134+
if m == "globalScope" {
135+
return true
136+
}
137+
}
138+
return false
139+
}
140+
141+
// TODO(cpp): support C++ OOP
69142
func (c *CxxSpec) HasImplSymbol() bool {
70-
panic("TODO")
143+
return false
71144
}
72145

73146
func (c *CxxSpec) ImplSymbol(sym lsp.DocumentSymbol) (int, int, int) {
74147
panic("TODO")
75148
}
76149

77150
func (c *CxxSpec) FunctionSymbol(sym lsp.DocumentSymbol) (int, []int, []int, []int) {
78-
panic("TODO")
151+
// No receiver and no type params for C
152+
if sym.Kind != lsp.SKFunction {
153+
return -1, nil, nil, nil
154+
}
155+
receiver := -1
156+
typeParams := []int{}
157+
inputParams := []int{}
158+
outputs := []int{}
159+
160+
// general format: RETURNVALUE NAME "(" PARAMS ")" BODY
161+
// --------
162+
// fnNameText
163+
// state machine phase 0 phase 1 phase 2: break
164+
// TODO: attributes may contain parens. also inline structs.
165+
166+
endRelOffset := 0
167+
lines := utils.CountLinesCached(sym.Text)
168+
phase := 0
169+
for i, tok := range sym.Tokens {
170+
switch phase {
171+
case 0:
172+
if tok.Type == "function" {
173+
offset := lsp.RelativePostionWithLines(*lines, sym.Location.Range.Start, tok.Location.Range.Start)
174+
endRelOffset = offset + strings.Index(sym.Text[offset:], ")")
175+
phase = 1
176+
continue
177+
}
178+
if c.IsEntityToken(tok) {
179+
outputs = append(outputs, i)
180+
}
181+
case 1:
182+
offset := lsp.RelativePostionWithLines(*lines, sym.Location.Range.Start, tok.Location.Range.Start)
183+
if offset > endRelOffset {
184+
phase = 2
185+
continue
186+
}
187+
if c.IsEntityToken(tok) {
188+
inputParams = append(inputParams, i)
189+
}
190+
}
191+
}
192+
return receiver, typeParams, inputParams, outputs
79193
}
80194

81195
func (c *CxxSpec) GetUnloadedSymbol(from lsp.Token, define lsp.Location) (string, error) {

lang/lsp/lsp.go

Lines changed: 54 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import (
2424
"sort"
2525
"strings"
2626

27+
"github.com/cloudwego/abcoder/lang/uniast"
2728
"github.com/cloudwego/abcoder/lang/utils"
2829
"github.com/sourcegraph/go-lsp"
2930
)
@@ -284,6 +285,57 @@ func (cli *LSPClient) References(ctx context.Context, id Location) ([]Location,
284285
return resp, nil
285286
}
286287

288+
// TODO(perf): cache results especially for whole file queries.
289+
// TODO(refactor): infer use_full_method from capabilities
290+
func (cli *LSPClient) getSemanticTokensRange(ctx context.Context, req DocumentRange, resp *SemanticTokens, use_full_method bool) error {
291+
if use_full_method {
292+
req1 := struct {
293+
TextDocument lsp.TextDocumentIdentifier `json:"textDocument"`
294+
}{TextDocument: req.TextDocument}
295+
if err := cli.Call(ctx, "textDocument/semanticTokens/full", req1, resp); err != nil {
296+
return err
297+
}
298+
filterSemanticTokensInRange(resp, req.Range)
299+
} else {
300+
if err := cli.Call(ctx, "textDocument/semanticTokens/range", req, resp); err != nil {
301+
return err
302+
}
303+
}
304+
return nil
305+
}
306+
307+
func filterSemanticTokensInRange(resp *SemanticTokens, r Range) {
308+
// LSP starts from 0:0 but the project seems to use 1:1 (see collect PositionOffset)
309+
curPos := Position{
310+
Line: 0,
311+
Character: 0,
312+
}
313+
newData := []uint32{}
314+
includedIs := []int{}
315+
for i := 0; i < len(resp.Data); i += 5 {
316+
deltaLine := int(resp.Data[i])
317+
deltaStart := int(resp.Data[i+1])
318+
if deltaLine != 0 {
319+
curPos.Line += deltaLine
320+
curPos.Character = deltaStart
321+
} else {
322+
curPos.Character += deltaStart
323+
}
324+
if isPositionInRange(curPos, r, true) {
325+
if len(newData) == 0 {
326+
// add range start to initial delta
327+
newData = append(newData, resp.Data[i:i+5]...)
328+
newData[0] = uint32(curPos.Line)
329+
newData[1] = uint32(curPos.Character)
330+
} else {
331+
newData = append(newData, resp.Data[i:i+5]...)
332+
}
333+
includedIs = append(includedIs, i)
334+
}
335+
}
336+
resp.Data = newData
337+
}
338+
287339
func (cli *LSPClient) SemanticTokens(ctx context.Context, id Location) ([]Token, error) {
288340
// open file first
289341
syms, err := cli.DocumentSymbols(ctx, id.URI)
@@ -304,7 +356,8 @@ func (cli *LSPClient) SemanticTokens(ctx context.Context, id Location) ([]Token,
304356
}
305357

306358
var resp SemanticTokens
307-
if err := cli.Call(ctx, "textDocument/semanticTokens/range", req, &resp); err != nil {
359+
if err := cli.getSemanticTokensRange(ctx, req, &resp, cli.Language == uniast.Cxx); err != nil {
360+
308361
return nil, err
309362
}
310363

0 commit comments

Comments
 (0)