Skip to content

Commit ad9951d

Browse files
authored
feat: autocomplete orb names
1 parent d1fd058 commit ad9951d

File tree

4 files changed

+175
-3
lines changed

4 files changed

+175
-3
lines changed

pkg/ast/orb.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package ast
33
import (
44
"fmt"
55

6+
sitter "github.com/smacker/go-tree-sitter"
67
"go.lsp.dev/protocol"
78
)
89

@@ -12,6 +13,8 @@ type Orb struct {
1213
Range protocol.Range
1314
NameRange protocol.Range
1415
VersionRange protocol.Range
16+
ValueRange protocol.Range
17+
ValueNode *sitter.Node
1518
}
1619

1720
type OrbURL struct {

pkg/parser/orbs.go

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,8 @@ func (doc *YamlDocument) parseSingleOrb(orbNode *sitter.Node) (*ast.Orb, *LocalO
7272
Range: NodeToRange(orbNode),
7373
NameRange: NodeToRange(orbNameNode),
7474
VersionRange: doc.getOrbVersionRange(orbContent),
75+
ValueNode: orbContent,
76+
ValueRange: NodeToRange(orbContent),
7577
}
7678
return &orb, nil
7779

@@ -89,9 +91,11 @@ func (doc *YamlDocument) parseSingleOrb(orbNode *sitter.Node) (*ast.Orb, *LocalO
8991
Version: "",
9092
IsLocal: true,
9193
},
92-
Name: orbName,
93-
Range: NodeToRange(orbNode),
94-
NameRange: NodeToRange(orbNameNode),
94+
Name: orbName,
95+
Range: NodeToRange(orbNode),
96+
NameRange: NodeToRange(orbNameNode),
97+
ValueNode: orbContent,
98+
ValueRange: NodeToRange(orbContent),
9599
}
96100

97101
return &orb, localOrb

pkg/services/complete/complete.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,8 @@ func (ch *CompletionHandler) GetCompletionItems() {
4747
ch.completeCommands()
4848
} else if utils.PosInRange(ch.Doc.ExecutorsRange, ch.Params.Position) {
4949
ch.completeExecutors()
50+
} else if utils.PosInRange(ch.Doc.OrbsRange, ch.Params.Position) {
51+
ch.completeOrbs()
5052
}
5153

5254
if len(ch.Items) > 0 {

pkg/services/complete/orbs.go

Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
1+
package complete
2+
3+
import (
4+
"fmt"
5+
"net/http"
6+
"regexp"
7+
"strings"
8+
"time"
9+
10+
"github.com/CircleCI-Public/circleci-yaml-language-server/pkg/utils"
11+
sitter "github.com/smacker/go-tree-sitter"
12+
)
13+
14+
var simpleRegistryOrbsCache = make(map[string]*NamespaceOrbResponse)
15+
var fullOrbNameRegex = regexp.MustCompile(`^([a-z0-9\-_]+)\/([a-z0-9\-_]+)@(.*)$`)
16+
var registryAndNameRegex = regexp.MustCompile(`^([a-z0-9\-_]+)\/([a-z0-9\-_]+)$`)
17+
18+
func (ch *CompletionHandler) completeOrbs() {
19+
if ch.DocTag != "original" {
20+
return
21+
}
22+
23+
for _, orb := range ch.Doc.Orbs {
24+
if orb.ValueNode == nil || !utils.PosInRange(orb.ValueRange, ch.Params.Position) || orb.ValueNode.Type() != "flow_node" {
25+
continue
26+
}
27+
28+
child := orb.ValueNode.Child(0)
29+
30+
if child == nil {
31+
continue
32+
}
33+
34+
if child.Type() == "anchor" {
35+
child = child.NextSibling()
36+
}
37+
38+
ch.completeOrbName(child)
39+
40+
return
41+
}
42+
}
43+
44+
func (ch *CompletionHandler) completeOrbName(node *sitter.Node) {
45+
completions, err := getOrbNameCompletions(
46+
ch.Doc.GetNodeText(node),
47+
ch.Doc.Context.Api.HostUrl,
48+
ch.Doc.Context.Api.Token,
49+
)
50+
51+
if err != nil {
52+
return
53+
}
54+
55+
for _, completion := range completions {
56+
ch.addReplaceTextCompletionItem(node, completion)
57+
}
58+
}
59+
60+
func getOrbNameCompletions(name, hostUrl, token string) ([]string, error) {
61+
parts := strings.Split(name, "/")
62+
registry := parts[0]
63+
64+
response, err := fetchOrbsByRegistry(registry, hostUrl, token)
65+
66+
if err != nil {
67+
return nil, err
68+
}
69+
70+
completions := make([]string, len(response.RegistryNamespace.Orbs.Edges))
71+
72+
for i, v := range response.RegistryNamespace.Orbs.Edges {
73+
if len(v.Node.Versions) > 0 {
74+
completions[i] = fmt.Sprintf("%s@%s", v.Node.Name, v.Node.Versions[0].Version)
75+
}
76+
}
77+
78+
return completions, nil
79+
}
80+
81+
func fetchOrbsByRegistry(registry, hostUrl, token string) (*NamespaceOrbResponse, error) {
82+
cached, cacheExists := simpleRegistryOrbsCache[registry]
83+
84+
if cacheExists {
85+
return cached, nil
86+
}
87+
88+
httpClient := &http.Client{
89+
Timeout: 30 * time.Second,
90+
Transport: &http.Transport{
91+
ExpectContinueTimeout: 1 * time.Second,
92+
IdleConnTimeout: 90 * time.Second,
93+
MaxIdleConns: 10,
94+
TLSHandshakeTimeout: 10 * time.Second,
95+
},
96+
}
97+
98+
client := utils.NewClient(
99+
httpClient,
100+
hostUrl,
101+
"graphql-unstable",
102+
token,
103+
false,
104+
)
105+
106+
query := `
107+
query OrbsByRegistry($name: String!) {
108+
registryNamespace(name: $name) {
109+
orbs(first: 1000){
110+
edges {
111+
cursor
112+
node {
113+
id
114+
name
115+
versions(count: 10) {version}
116+
}
117+
}
118+
}
119+
}
120+
}
121+
`
122+
123+
request := utils.NewRequest(query)
124+
request.SetToken(client.Token)
125+
request.Var("name", registry)
126+
127+
var response NamespaceOrbResponse
128+
err := client.Run(request, &response)
129+
130+
if err != nil {
131+
return nil, err
132+
}
133+
134+
simpleRegistryOrbsCache[registry] = &response
135+
136+
return &response, nil
137+
}
138+
139+
type OrbGQLData struct {
140+
ID string
141+
Name string
142+
Versions []struct {
143+
Version string `json:"version"`
144+
Source string `json:"source"`
145+
} `json:"versions"`
146+
}
147+
148+
type NamespaceOrbResponse struct {
149+
RegistryNamespace struct {
150+
ID string
151+
Name string
152+
Orbs struct {
153+
Edges []struct {
154+
Cursor string
155+
Node OrbGQLData
156+
}
157+
TotalCount int
158+
PageInfo struct {
159+
HasNextPage bool
160+
}
161+
}
162+
}
163+
}

0 commit comments

Comments
 (0)