Skip to content

Commit fb62929

Browse files
Will-ShaoHualeigaolhayemaxi
authored
feat(amazonq): Add project context to inline completion (#5729)
## Problem 1. Inline Completion of Q is not context aware. 2. The `@workspace` index does not update on file add/remove. ## Solution 1. We are adding new global BM25 index and class/function digest from imported files to provide project context for inline completion. This feature will be added as AB testing. In this PR, there will be no customer facing changes since it is hidden behind a AB testing flag. We will toggle it a few days later. For this, we added new LSP APIs. 2. One customer facing change, when user add or remove a file in the VS Code IDE, its `@workspace` index will be updated The net-browserify is added to prevent npm run package failure for browser. --- <!--- REMINDER: Ensure that your PR meets the guidelines in CONTRIBUTING.md --> License: I confirm that my contribution is made under the terms of the Apache 2.0 license. --------- Co-authored-by: Lei Gao <[email protected]> Co-authored-by: Lei Gao <[email protected]> Co-authored-by: Maxim Hayes <[email protected]>
1 parent 798a4fd commit fb62929

File tree

11 files changed

+228
-85
lines changed

11 files changed

+228
-85
lines changed
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
{
2+
"type": "Bug Fix",
3+
"description": "Update `@workspace` index when adding or deleting a file"
4+
}

packages/amazonq/src/app/chat/activation.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,11 @@ export async function activate(context: ExtensionContext) {
2525
await amazonq.TryChatCodeLensProvider.register(appInitContext.onDidChangeAmazonQVisibility.event)
2626

2727
const setupLsp = funcUtil.debounce(async () => {
28-
void amazonq.LspController.instance.trySetupLsp(context)
28+
void amazonq.LspController.instance.trySetupLsp(context, {
29+
startUrl: AuthUtil.instance.startUrl,
30+
maxIndexSize: CodeWhispererSettings.instance.getMaxIndexSize(),
31+
isVectorIndexEnabled: CodeWhispererSettings.instance.isLocalIndexEnabled(),
32+
})
2933
}, 5000)
3034

3135
context.subscriptions.push(

packages/amazonq/test/unit/amazonq/lsp/lspClient.test.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ describe('Amazon Q LSP client', function () {
1818
})
1919

2020
it('encrypts payload of query ', async () => {
21-
await lspClient.query('mock_input')
21+
await lspClient.queryVectorIndex('mock_input')
2222
assert.ok(encryptFunc.calledOnce)
2323
assert.ok(encryptFunc.calledWith(JSON.stringify({ query: 'mock_input' })))
2424
const value = await encryptFunc.returnValues[0]
@@ -27,14 +27,15 @@ describe('Amazon Q LSP client', function () {
2727
})
2828

2929
it('encrypts payload of index files ', async () => {
30-
await lspClient.indexFiles(['fileA'], 'path', false)
30+
await lspClient.buildIndex(['fileA'], 'path', 'all')
3131
assert.ok(encryptFunc.calledOnce)
3232
assert.ok(
3333
encryptFunc.calledWith(
3434
JSON.stringify({
3535
filePaths: ['fileA'],
36-
rootPath: 'path',
37-
refresh: false,
36+
projectRoot: 'path',
37+
config: 'all',
38+
language: '',
3839
})
3940
)
4041
)

packages/core/src/amazonq/lsp/lspClient.ts

Lines changed: 61 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,17 @@ import * as jose from 'jose'
1717
import { Disposable, ExtensionContext } from 'vscode'
1818

1919
import { LanguageClient, LanguageClientOptions, ServerOptions, TransportKind } from 'vscode-languageclient'
20-
import { GetUsageRequestType, IndexRequestType, QueryRequestType, UpdateIndexRequestType, Usage } from './types'
20+
import {
21+
BuildIndexRequestPayload,
22+
BuildIndexRequestType,
23+
GetUsageRequestType,
24+
IndexConfig,
25+
QueryInlineProjectContextRequestType,
26+
QueryVectorIndexRequestType,
27+
UpdateIndexV2RequestPayload,
28+
UpdateIndexV2RequestType,
29+
Usage,
30+
} from './types'
2131
import { Writable } from 'stream'
2232
import { CodeWhispererSettings } from '../../codewhisperer/util/codewhispererSettings'
2333
import { fs, getLogger } from '../../shared'
@@ -61,52 +71,67 @@ export class LspClient {
6171
.encrypt(key)
6272
}
6373

64-
async indexFiles(request: string[], rootPath: string, refresh: boolean) {
74+
async buildIndex(paths: string[], rootPath: string, config: IndexConfig) {
75+
const payload: BuildIndexRequestPayload = {
76+
filePaths: paths,
77+
projectRoot: rootPath,
78+
config: config,
79+
language: '',
80+
}
6581
try {
66-
const encryptedRequest = await this.encrypt(
67-
JSON.stringify({
68-
filePaths: request,
69-
rootPath: rootPath,
70-
refresh: refresh,
71-
})
72-
)
73-
const resp = await this.client?.sendRequest(IndexRequestType, encryptedRequest)
82+
const encryptedRequest = await this.encrypt(JSON.stringify(payload))
83+
const resp = await this.client?.sendRequest(BuildIndexRequestType, encryptedRequest)
7484
return resp
7585
} catch (e) {
76-
getLogger().error(`LspClient: indexFiles error: ${e}`)
86+
getLogger().error(`LspClient: buildIndex error: ${e}`)
7787
return undefined
7888
}
7989
}
8090

81-
async query(request: string) {
91+
async queryVectorIndex(request: string) {
8292
try {
8393
const encryptedRequest = await this.encrypt(
8494
JSON.stringify({
8595
query: request,
8696
})
8797
)
88-
const resp = await this.client?.sendRequest(QueryRequestType, encryptedRequest)
98+
const resp = await this.client?.sendRequest(QueryVectorIndexRequestType, encryptedRequest)
8999
return resp
90100
} catch (e) {
91-
getLogger().error(`LspClient: query error: ${e}`)
101+
getLogger().error(`LspClient: queryVectorIndex error: ${e}`)
92102
return []
93103
}
94104
}
95105

106+
async queryInlineProjectContext(query: string, path: string) {
107+
try {
108+
const request = JSON.stringify({
109+
query: query,
110+
filePath: path,
111+
})
112+
const encrypted = await this.encrypt(request)
113+
const resp: any = await this.client?.sendRequest(QueryInlineProjectContextRequestType, encrypted)
114+
return resp
115+
} catch (e) {
116+
getLogger().error(`LspClient: queryInlineProjectContext error: ${e}`)
117+
throw e
118+
}
119+
}
120+
96121
async getLspServerUsage(): Promise<Usage | undefined> {
97122
if (this.client) {
98123
return (await this.client.sendRequest(GetUsageRequestType, '')) as Usage
99124
}
100125
}
101126

102-
async updateIndex(filePath: string) {
127+
async updateIndex(filePath: string[], mode: 'update' | 'remove' | 'add') {
128+
const payload: UpdateIndexV2RequestPayload = {
129+
filePaths: filePath,
130+
updateMode: mode,
131+
}
103132
try {
104-
const encryptedRequest = await this.encrypt(
105-
JSON.stringify({
106-
filePath: filePath,
107-
})
108-
)
109-
const resp = await this.client?.sendRequest(UpdateIndexRequestType, encryptedRequest)
133+
const encryptedRequest = await this.encrypt(JSON.stringify(payload))
134+
const resp = await this.client?.sendRequest(UpdateIndexV2RequestType, encryptedRequest)
110135
return resp
111136
} catch (e) {
112137
getLogger().error(`LspClient: updateIndex error: ${e}`)
@@ -197,15 +222,26 @@ export async function activate(extensionContext: ExtensionContext) {
197222
return
198223
}
199224
savedDocument = document.uri
200-
})
201-
)
202-
toDispose.push(
225+
}),
203226
vscode.window.onDidChangeActiveTextEditor((editor) => {
204227
if (savedDocument && editor && editor.document.uri.fsPath !== savedDocument.fsPath) {
205-
void LspClient.instance.updateIndex(savedDocument.fsPath)
228+
void LspClient.instance.updateIndex([savedDocument.fsPath], 'update')
206229
}
230+
}),
231+
vscode.workspace.onDidCreateFiles((e) => {
232+
void LspClient.instance.updateIndex(
233+
e.files.map((f) => f.fsPath),
234+
'add'
235+
)
236+
}),
237+
vscode.workspace.onDidDeleteFiles((e) => {
238+
void LspClient.instance.updateIndex(
239+
e.files.map((f) => f.fsPath),
240+
'remove'
241+
)
207242
})
208243
)
244+
209245
return LspClient.instance.client.onReady().then(() => {
210246
const disposableFunc = { dispose: () => rangeFormatting?.dispose() as void }
211247
toDispose.push(disposableFunc)

packages/core/src/amazonq/lsp/lspController.ts

Lines changed: 38 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,10 @@ import { LspClient } from './lspClient'
1515
import AdmZip from 'adm-zip'
1616
import { RelevantTextDocument } from '@amzn/codewhisperer-streaming'
1717
import { makeTemporaryToolkitFolder, tryRemoveFolder } from '../../shared/filesystemUtilities'
18-
import { CodeWhispererSettings } from '../../codewhisperer/util/codewhispererSettings'
1918
import { activate as activateLsp } from './lspClient'
2019
import { telemetry } from '../../shared/telemetry'
2120
import { isCloud9 } from '../../shared/extensionUtilities'
2221
import { fs, globals, ToolkitError } from '../../shared'
23-
import { AuthUtil } from '../../codewhisperer'
2422
import { isWeb } from '../../shared/extensionGlobals'
2523
import { getUserAgent } from '../../shared/telemetry/util'
2624
import { isAmazonInternalOs } from '../../shared/vscode/env'
@@ -68,9 +66,16 @@ export interface Manifest {
6866
}
6967
const manifestUrl = 'https://aws-toolkit-language-servers.amazonaws.com/q-context/manifest.json'
7068
// this LSP client in Q extension is only going to work with these LSP server versions
71-
const supportedLspServerVersions = ['0.1.13']
69+
const supportedLspServerVersions = ['0.1.22', '0.1.19']
7270

7371
const nodeBinName = process.platform === 'win32' ? 'node.exe' : 'node'
72+
73+
export interface BuildIndexConfig {
74+
startUrl?: string
75+
maxIndexSize: number
76+
isVectorIndexEnabled: boolean
77+
}
78+
7479
/*
7580
* LSP Controller manages the status of Amazon Q LSP:
7681
* 1. Downloading, verifying and installing LSP using DEXP LSP manifest and CDN.
@@ -281,7 +286,7 @@ export class LspController {
281286
}
282287

283288
async query(s: string): Promise<RelevantTextDocument[]> {
284-
const chunks: Chunk[] | undefined = await LspClient.instance.query(s)
289+
const chunks: Chunk[] | undefined = await LspClient.instance.queryVectorIndex(s)
285290
const resp: RelevantTextDocument[] = []
286291
chunks?.forEach((chunk) => {
287292
const text = chunk.context ? chunk.context : chunk.content
@@ -303,7 +308,15 @@ export class LspController {
303308
return resp
304309
}
305310

306-
async buildIndex() {
311+
async queryInlineProjectContext(query: string, path: string) {
312+
try {
313+
return await LspClient.instance.queryInlineProjectContext(query, path)
314+
} catch (e) {
315+
return []
316+
}
317+
}
318+
319+
async buildIndex(buildIndexConfig: BuildIndexConfig) {
307320
getLogger().info(`LspController: Starting to build index of project`)
308321
const start = performance.now()
309322
const projPaths = getProjectPaths()
@@ -318,18 +331,16 @@ export class LspController {
318331
projPaths,
319332
vscode.workspace.workspaceFolders as CurrentWsFolders,
320333
true,
321-
CodeWhispererSettings.instance.getMaxIndexSize() * 1024 * 1024
334+
buildIndexConfig.maxIndexSize * 1024 * 1024
322335
)
323336
const totalSizeBytes = files.reduce(
324337
(accumulator, currentFile) => accumulator + currentFile.fileSizeBytes,
325338
0
326339
)
327340
getLogger().info(`LspController: Found ${files.length} files in current project ${getProjectPaths()}`)
328-
const resp = await LspClient.instance.indexFiles(
329-
files.map((f) => f.fileUri.fsPath),
330-
projRoot,
331-
false
332-
)
341+
const config = buildIndexConfig.isVectorIndexEnabled ? 'all' : 'default'
342+
const r = files.map((f) => f.fileUri.fsPath)
343+
const resp = await LspClient.instance.buildIndex(r, projRoot, config)
333344
if (resp) {
334345
getLogger().debug(`LspController: Finish building index of project`)
335346
const usage = await LspClient.instance.getLspServerUsage()
@@ -340,31 +351,36 @@ export class LspController {
340351
amazonqIndexMemoryUsageInMB: usage ? usage.memoryUsage / (1024 * 1024) : undefined,
341352
amazonqIndexCpuUsagePercentage: usage ? usage.cpuUsage : undefined,
342353
amazonqIndexFileSizeInMB: totalSizeBytes / (1024 * 1024),
343-
credentialStartUrl: AuthUtil.instance.startUrl,
354+
credentialStartUrl: buildIndexConfig.startUrl,
344355
})
345356
} else {
346-
getLogger().error(`LspController: Failed to build index of project`)
347-
telemetry.amazonq_indexWorkspace.emit({
348-
duration: performance.now() - start,
349-
result: 'Failed',
350-
amazonqIndexFileCount: 0,
351-
amazonqIndexFileSizeInMB: 0,
352-
})
357+
// TODO: Re-enable this code path for LSP 0.1.20+
358+
// getLogger().error(`LspController: Failed to build index of project`)
359+
// telemetry.amazonq_indexWorkspace.emit({
360+
// duration: performance.now() - start,
361+
// result: 'Failed',
362+
// amazonqIndexFileCount: 0,
363+
// amazonqIndexFileSizeInMB: 0,
364+
// reason: `Unknown`,
365+
// })
353366
}
354-
} catch (e) {
367+
} catch (error) {
368+
//TODO: use telemetry.run()
355369
getLogger().error(`LspController: Failed to build index of project`)
356370
telemetry.amazonq_indexWorkspace.emit({
357371
duration: performance.now() - start,
358372
result: 'Failed',
359373
amazonqIndexFileCount: 0,
360374
amazonqIndexFileSizeInMB: 0,
375+
reason: `${error instanceof Error ? error.name : 'Unknown'}`,
376+
reasonDesc: `Error when building index. ${error instanceof Error ? error.message : error}`,
361377
})
362378
} finally {
363379
this._isIndexingInProgress = false
364380
}
365381
}
366382

367-
async trySetupLsp(context: vscode.ExtensionContext) {
383+
async trySetupLsp(context: vscode.ExtensionContext, buildIndexConfig: BuildIndexConfig) {
368384
if (isCloud9() || isWeb() || isAmazonInternalOs()) {
369385
getLogger().warn('LspController: Skipping LSP setup. LSP is not compatible with the current environment. ')
370386
// do not do anything if in Cloud9 or Web mode or in AL2 (AL2 does not support node v18+)
@@ -378,9 +394,7 @@ export class LspController {
378394
try {
379395
await activateLsp(context)
380396
getLogger().info('LspController: LSP activated')
381-
if (CodeWhispererSettings.instance.isLocalIndexEnabled()) {
382-
void LspController.instance.buildIndex()
383-
}
397+
void LspController.instance.buildIndex(buildIndexConfig)
384398
// log the LSP server CPU and Memory usage per 30 minutes.
385399
globals.clock.setInterval(
386400
async () => {

packages/core/src/amazonq/lsp/types.ts

Lines changed: 37 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,6 @@ export type IndexRequestPayload = {
1111
refresh: boolean
1212
}
1313

14-
export type IndexRequest = string
15-
16-
export const IndexRequestType: RequestType<IndexRequest, any, any> = new RequestType('lsp/index')
17-
1814
export type ClearRequest = string
1915

2016
export const ClearRequestType: RequestType<ClearRequest, any, any> = new RequestType('lsp/clear')
@@ -23,10 +19,6 @@ export type QueryRequest = string
2319

2420
export const QueryRequestType: RequestType<QueryRequest, any, any> = new RequestType('lsp/query')
2521

26-
export type UpdateIndexRequest = string
27-
28-
export const UpdateIndexRequestType: RequestType<UpdateIndexRequest, any, any> = new RequestType('lsp/updateIndex')
29-
3022
export type GetUsageRequest = string
3123

3224
export const GetUsageRequestType: RequestType<GetUsageRequest, any, any> = new RequestType('lsp/getUsage')
@@ -35,3 +27,40 @@ export interface Usage {
3527
memoryUsage: number
3628
cpuUsage: number
3729
}
30+
31+
export type BuildIndexRequestPayload = {
32+
filePaths: string[]
33+
projectRoot: string
34+
config: string
35+
language: string
36+
}
37+
38+
export type BuildIndexRequest = string
39+
40+
export const BuildIndexRequestType: RequestType<BuildIndexRequest, any, any> = new RequestType('lsp/buildIndex')
41+
42+
export type UpdateIndexV2Request = string
43+
44+
export type UpdateIndexV2RequestPayload = { filePaths: string[]; updateMode: string }
45+
46+
export const UpdateIndexV2RequestType: RequestType<UpdateIndexV2Request, any, any> = new RequestType(
47+
'lsp/updateIndexV2'
48+
)
49+
50+
export type QueryInlineProjectContextRequest = string
51+
export type QueryInlineProjectContextRequestPayload = {
52+
query: string
53+
filePath: string
54+
}
55+
export const QueryInlineProjectContextRequestType: RequestType<QueryInlineProjectContextRequest, any, any> =
56+
new RequestType('lsp/queryInlineProjectContext')
57+
58+
export type QueryVectorIndexRequestPayload = { query: string }
59+
60+
export type QueryVectorIndexRequest = string
61+
62+
export const QueryVectorIndexRequestType: RequestType<QueryVectorIndexRequest, any, any> = new RequestType(
63+
'lsp/queryVectorIndex'
64+
)
65+
66+
export type IndexConfig = 'all' | 'default'

packages/core/src/codewhisperer/models/model.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ export type UtgStrategy = 'ByName' | 'ByContent'
4848

4949
export type CrossFileStrategy = 'OpenTabs_BM25'
5050

51-
export type SupplementalContextStrategy = CrossFileStrategy | UtgStrategy | 'Empty'
51+
export type SupplementalContextStrategy = CrossFileStrategy | UtgStrategy | 'Empty' | 'LSP'
5252

5353
export interface CodeWhispererSupplementalContext {
5454
isUtg: boolean

0 commit comments

Comments
 (0)