Skip to content

Commit 93c65a7

Browse files
committed
provider for sourcegraph references endpoint
1 parent d5a4ab6 commit 93c65a7

File tree

10 files changed

+592
-0
lines changed

10 files changed

+592
-0
lines changed

pnpm-lock.yaml

Lines changed: 19 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
# Sourcegraph refs context provider for OpenCtx
2+
3+
This is a context provider for [OpenCtx](https://openctx.org) that fetches Sourcegraph references for use as context.
4+
5+
## Usage
6+
7+
Add the following to your settings in any OpenCtx client:
8+
9+
```json
10+
"openctx.providers": {
11+
// ...other providers...
12+
"https://openctx.org/npm/@openctx/provider-sourcegraph-refs": {
13+
"sourcegraphEndpoint": "https://sourcegraph.sourcegraph.com",
14+
"sourcegraphToken": "$YOUR_TOKEN",
15+
"repositoryNames": ["github.com/sourcegraph/sourcegraph"]
16+
}
17+
},
18+
```
Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
import { escapeRegExp } from 'lodash'
2+
import { BLOB_QUERY, type BlobInfo, BlobResponseSchema } from './graphql_blobs.js'
3+
import {
4+
FUZZY_SYMBOLS_QUERY,
5+
FuzzySymbolsResponseSchema,
6+
type SymbolInfo,
7+
transformToSymbols,
8+
} from './graphql_symbols.js'
9+
import {
10+
USAGES_FOR_SYMBOL_QUERY,
11+
type Usage,
12+
UsagesForSymbolResponseSchema,
13+
transformToUsages,
14+
} from './graphql_usages.js'
15+
16+
interface APIResponse<T> {
17+
data?: T
18+
errors?: { message: string; path?: string[] }[]
19+
}
20+
21+
export class SourcegraphGraphQLAPIClient {
22+
constructor(
23+
private readonly endpoint: string,
24+
private readonly token: string,
25+
) {}
26+
27+
public async fetchSymbols(query: string, repositories: string[]): Promise<SymbolInfo[] | Error> {
28+
const response: any | Error = await this.fetchSourcegraphAPI<APIResponse<any>>(
29+
FUZZY_SYMBOLS_QUERY,
30+
{
31+
query: `type:symbol count:30 ${
32+
repositories.length > 0 ? `repo:^(${repositories.map(escapeRegExp).join('|')})$` : ''
33+
} ${query}`,
34+
},
35+
)
36+
37+
if (isError(response)) {
38+
return response
39+
}
40+
41+
try {
42+
const validatedData = FuzzySymbolsResponseSchema.parse(response.data)
43+
return transformToSymbols(validatedData)
44+
} catch (error) {
45+
return new Error(`Invalid response format: ${error}`)
46+
}
47+
}
48+
49+
public async fetchUsages(
50+
repository: string,
51+
path: string,
52+
startLine: number,
53+
startCharacter: number,
54+
endLine: number,
55+
endCharacter: number,
56+
): Promise<Usage[] | Error> {
57+
const response: any | Error = await this.fetchSourcegraphAPI<
58+
APIResponse<typeof UsagesForSymbolResponseSchema>
59+
>(USAGES_FOR_SYMBOL_QUERY, {
60+
repository,
61+
path,
62+
startLine,
63+
startCharacter,
64+
endLine,
65+
endCharacter,
66+
})
67+
68+
if (isError(response)) {
69+
return response
70+
}
71+
72+
try {
73+
// TODO(beyang): sort or filter by provenance
74+
const validatedData = UsagesForSymbolResponseSchema.parse(response.data)
75+
return transformToUsages(validatedData)
76+
} catch (error) {
77+
return new Error(`Invalid response format: ${error}`)
78+
}
79+
}
80+
81+
public async fetchBlob({
82+
repoName,
83+
revspec,
84+
path,
85+
startLine,
86+
endLine,
87+
}: {
88+
repoName: string
89+
revspec: string
90+
path: string
91+
startLine: number
92+
endLine: number
93+
}): Promise<BlobInfo | Error> {
94+
const response: any | Error = await this.fetchSourcegraphAPI<APIResponse<BlobInfo>>(BLOB_QUERY, {
95+
repoName,
96+
revspec,
97+
path,
98+
startLine,
99+
endLine,
100+
})
101+
102+
if (isError(response)) {
103+
return response
104+
}
105+
106+
try {
107+
const validatedData = BlobResponseSchema.parse(response.data)
108+
return {
109+
repoName,
110+
revision: revspec,
111+
path: validatedData.repository.commit.blob.path,
112+
range: {
113+
start: { line: startLine, character: 0 },
114+
end: { line: endLine, character: 0 },
115+
},
116+
content: validatedData.repository.commit.blob.content,
117+
}
118+
} catch (error) {
119+
return new Error(`Invalid response format: ${error}`)
120+
}
121+
}
122+
123+
public async fetchSourcegraphAPI<T>(
124+
query: string,
125+
variables: Record<string, any> = {},
126+
): Promise<T | Error> {
127+
const headers = new Headers()
128+
headers.set('Content-Type', 'application/json; charset=utf-8')
129+
headers.set('User-Agent', 'openctx-sourcegraph-search / 0.0.1')
130+
headers.set('Authorization', `token ${this.token}`)
131+
132+
const queryName = query.match(/^\s*(?:query|mutation)\s+(\w+)/m)?.[1] ?? 'unknown'
133+
const url = this.endpoint + '/.api/graphql?' + queryName
134+
135+
return fetch(url, {
136+
method: 'POST',
137+
body: JSON.stringify({ query, variables }),
138+
headers,
139+
})
140+
.then(verifyResponseCode)
141+
.then(response => response.json() as T)
142+
.catch(error => new Error(`accessing Sourcegraph GraphQL API: ${error} (${url})`))
143+
}
144+
}
145+
146+
async function verifyResponseCode(response: Response): Promise<Response> {
147+
if (!response.ok) {
148+
const body = await response.text()
149+
throw new Error(`HTTP status code ${response.status}${body ? `: ${body}` : ''}`)
150+
}
151+
return response
152+
}
153+
154+
export const isError = (value: unknown): value is Error => value instanceof Error
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import { z } from 'zod'
2+
3+
export interface BlobInfo {
4+
repoName: string
5+
revision: string
6+
path: string
7+
range: {
8+
start: { line: number; character: number }
9+
end: { line: number; character: number }
10+
}
11+
content: string
12+
}
13+
14+
export const BlobResponseSchema = z.object({
15+
repository: z.object({
16+
commit: z.object({
17+
blob: z.object({
18+
path: z.string(),
19+
url: z.string(),
20+
languages: z.array(z.string()),
21+
content: z.string(),
22+
}),
23+
}),
24+
}),
25+
})
26+
27+
export type BlobResponse = z.infer<typeof BlobResponseSchema>
28+
29+
export const BLOB_QUERY = `
30+
query Blob($repoName: String!, $revspec: String!, $path: String!, $startLine: Int!, $endLine: Int!) {
31+
repository(name: $repoName) {
32+
commit(rev: $revspec) {
33+
blob(path: $path) {
34+
path
35+
url
36+
languages
37+
content(startLine: $startLine, endLine: $endLine)
38+
}
39+
}
40+
}
41+
}`
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
import { z } from 'zod'
2+
3+
export interface SymbolInfo {
4+
name: string
5+
repositoryId: string
6+
repositoryName: string
7+
path: string
8+
range: {
9+
start: { line: number; character: number }
10+
end: { line: number; character: number }
11+
}
12+
}
13+
14+
export const FuzzySymbolsResponseSchema = z.object({
15+
search: z.object({
16+
results: z.object({
17+
results: z.array(
18+
z.object({
19+
__typename: z.string(),
20+
file: z.object({
21+
path: z.string(),
22+
}),
23+
symbols: z.array(
24+
z.object({
25+
name: z.string(),
26+
location: z.object({
27+
range: z.object({
28+
start: z.object({ line: z.number(), character: z.number() }),
29+
end: z.object({ line: z.number(), character: z.number() }),
30+
}),
31+
resource: z.object({
32+
path: z.string(),
33+
}),
34+
}),
35+
}),
36+
),
37+
repository: z.object({
38+
id: z.string(),
39+
name: z.string(),
40+
}),
41+
}),
42+
),
43+
}),
44+
}),
45+
})
46+
47+
export function transformToSymbols(response: z.infer<typeof FuzzySymbolsResponseSchema>): SymbolInfo[] {
48+
return response.search.results.results.flatMap(result => {
49+
return (result.symbols || []).map(symbol => ({
50+
name: symbol.name,
51+
repositoryId: result.repository.id,
52+
repositoryName: result.repository.name,
53+
path: symbol.location.resource.path,
54+
range: {
55+
start: {
56+
line: symbol.location.range.start.line,
57+
character: symbol.location.range.start.character,
58+
},
59+
end: {
60+
line: symbol.location.range.end.line,
61+
character: symbol.location.range.end.character,
62+
},
63+
},
64+
}))
65+
})
66+
}
67+
68+
export const FUZZY_SYMBOLS_QUERY = `
69+
query FuzzySymbols($query: String!) {
70+
search(patternType: regexp, query: $query) {
71+
results {
72+
results {
73+
... on FileMatch {
74+
__typename
75+
file {
76+
path
77+
}
78+
symbols {
79+
name
80+
location {
81+
range {
82+
start { line, character}
83+
end { line, character }
84+
}
85+
resource {
86+
path
87+
}
88+
}
89+
}
90+
repository {
91+
id
92+
name
93+
}
94+
}
95+
}
96+
}
97+
}
98+
}
99+
`

0 commit comments

Comments
 (0)