Skip to content

Commit 1318d29

Browse files
authored
feat: initial support for local project context (#949)
* feat: initial support for local project context * feat: dynamically import vector library * fix: various cleanup * fix: path test errors on Windows * feat: add support for enablement via LSP configuration * fix: better qualified name for local project enablement configuration
1 parent eded88f commit 1318d29

File tree

11 files changed

+2136
-991
lines changed

11 files changed

+2136
-991
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ build
55
**/*.tgz
66
!core/codewhisperer-streaming/amzn-codewhisperer-streaming-*.tgz
77
!core/q-developer-streaming-client/amzn-amazon-q-developer-streaming-client-*.tgz
8+
!server/aws-lsp-codewhisperer/types/types-local-indexing-*.tgz
89

910
.testresults/**
1011

app/aws-lsp-codewhisperer-runtimes/src/token-standalone.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import {
66
QChatServerProxy,
77
QConfigurationServerTokenProxy,
88
QNetTransformServerTokenProxy,
9+
QLocalProjectContextServerTokenProxy,
910
} from '@aws/lsp-codewhisperer'
1011
import { IdentityServer } from '@aws/lsp-identity'
1112

@@ -23,6 +24,7 @@ const props: RuntimeProps = {
2324
QNetTransformServerTokenProxy,
2425
QChatServerProxy,
2526
IdentityServer.create,
27+
QLocalProjectContextServerTokenProxy,
2628
],
2729
name: 'AWS CodeWhisperer',
2830
}

package-lock.json

Lines changed: 1554 additions & 991 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

server/aws-lsp-codewhisperer/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@
5151
"devDependencies": {
5252
"@types/adm-zip": "^0.5.5",
5353
"@types/archiver": "^6.0.2",
54+
"@types/local-indexing": "file:./types/types-local-indexing-1.0.0.tgz",
5455
"@types/lokijs": "^1.5.14",
5556
"@types/uuid": "^9.0.8",
5657
"@types/diff": "^7.0.2",
Lines changed: 217 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,217 @@
1+
import { LocalProjectContextController } from './localProjectContextController'
2+
import { SinonStub, stub, spy, assert as sinonAssert, match } from 'sinon'
3+
import * as assert from 'assert'
4+
import * as fs from 'fs'
5+
import { Dirent } from 'fs'
6+
import * as path from 'path'
7+
import { URI } from 'vscode-uri'
8+
9+
class LoggingMock {
10+
public error: SinonStub
11+
public info: SinonStub
12+
public log: SinonStub
13+
public warn: SinonStub
14+
15+
constructor() {
16+
this.error = stub()
17+
this.info = stub()
18+
this.log = stub()
19+
this.warn = stub()
20+
}
21+
}
22+
23+
describe('LocalProjectContextController', () => {
24+
let controller: LocalProjectContextController
25+
let logging: LoggingMock
26+
let mockWorkspaceFolders: any[]
27+
let vectorLibMock: any
28+
let fsStub: SinonStub
29+
30+
const BASE_PATH = path.join(__dirname, 'path', 'to', 'workspace1')
31+
32+
beforeEach(() => {
33+
logging = new LoggingMock()
34+
mockWorkspaceFolders = [
35+
{
36+
uri: URI.file(BASE_PATH),
37+
name: 'workspace1',
38+
},
39+
]
40+
41+
vectorLibMock = {
42+
start: stub().resolves({
43+
buildIndex: stub().resolves(),
44+
clear: stub().resolves(),
45+
queryVectorIndex: stub().resolves(['mockChunk1', 'mockChunk2']),
46+
queryInlineProjectContext: stub().resolves(['mockContext1']),
47+
updateIndexV2: stub().resolves(),
48+
}),
49+
}
50+
51+
fsStub = stub(fs.promises, 'readdir')
52+
fsStub.withArgs(match.string, { withFileTypes: true }).callsFake(path => {
53+
if (path.endsWith('workspace1')) {
54+
return Promise.resolve([createMockDirent('Test.java', false), createMockDirent('src', true)])
55+
} else if (path.endsWith('src')) {
56+
return Promise.resolve([createMockDirent('Main.java', false)])
57+
} else {
58+
return Promise.resolve([])
59+
}
60+
})
61+
controller = new LocalProjectContextController('testClient', mockWorkspaceFolders, logging as any)
62+
})
63+
64+
afterEach(() => {
65+
fsStub.restore()
66+
})
67+
68+
describe('init', () => {
69+
it('should initialize vector library successfully', async () => {
70+
await controller.init(vectorLibMock)
71+
72+
sinonAssert.notCalled(logging.error)
73+
sinonAssert.called(vectorLibMock.start)
74+
const vecLib = await vectorLibMock.start()
75+
sinonAssert.called(vecLib.buildIndex)
76+
})
77+
78+
it('should handle initialization errors', async () => {
79+
vectorLibMock.start.rejects(new Error('Init failed'))
80+
81+
await controller.init(vectorLibMock)
82+
83+
sinonAssert.called(logging.error)
84+
})
85+
})
86+
87+
describe('queryVectorIndex', () => {
88+
beforeEach(async () => {
89+
await controller.init(vectorLibMock)
90+
})
91+
92+
it('should return empty array when vector library is not initialized', async () => {
93+
const uninitializedController = new LocalProjectContextController(
94+
'testClient',
95+
mockWorkspaceFolders,
96+
logging as any
97+
)
98+
99+
const result = await uninitializedController.queryVectorIndex({ query: 'test' })
100+
assert.deepStrictEqual(result, [])
101+
})
102+
103+
it('should return chunks from vector library', async () => {
104+
const result = await controller.queryVectorIndex({ query: 'test' })
105+
assert.deepStrictEqual(result, ['mockChunk1', 'mockChunk2'])
106+
})
107+
108+
it('should handle query errors', async () => {
109+
const vecLib = await vectorLibMock.start()
110+
vecLib.queryVectorIndex.rejects(new Error('Query failed'))
111+
112+
const result = await controller.queryVectorIndex({ query: 'test' })
113+
assert.deepStrictEqual(result, [])
114+
sinonAssert.called(logging.error)
115+
})
116+
})
117+
118+
describe('queryInlineProjectContext', () => {
119+
beforeEach(async () => {
120+
await controller.init(vectorLibMock)
121+
})
122+
123+
it('should return empty array when vector library is not initialized', async () => {
124+
const uninitializedController = new LocalProjectContextController(
125+
'testClient',
126+
mockWorkspaceFolders,
127+
logging as any
128+
)
129+
130+
const result = await uninitializedController.queryInlineProjectContext({
131+
query: 'test',
132+
filePath: 'test.java',
133+
target: 'test',
134+
})
135+
assert.deepStrictEqual(result, [])
136+
})
137+
138+
it('should return context from vector library', async () => {
139+
const result = await controller.queryInlineProjectContext({
140+
query: 'test',
141+
filePath: 'test.java',
142+
target: 'test',
143+
})
144+
assert.deepStrictEqual(result, ['mockContext1'])
145+
})
146+
147+
it('should handle query errors', async () => {
148+
const vecLib = await vectorLibMock.start()
149+
vecLib.queryInlineProjectContext.rejects(new Error('Query failed'))
150+
151+
const result = await controller.queryInlineProjectContext({
152+
query: 'test',
153+
filePath: 'test.java',
154+
target: 'test',
155+
})
156+
assert.deepStrictEqual(result, [])
157+
sinonAssert.called(logging.error)
158+
})
159+
})
160+
161+
describe('updateIndex', () => {
162+
beforeEach(async () => {
163+
await controller.init(vectorLibMock)
164+
})
165+
166+
it('should do nothing when vector library is not initialized', async () => {
167+
const uninitializedController = new LocalProjectContextController(
168+
'testClient',
169+
mockWorkspaceFolders,
170+
logging as any
171+
)
172+
173+
await uninitializedController.updateIndex(['test.java'], 'add')
174+
sinonAssert.notCalled(logging.error)
175+
})
176+
177+
it('should update index successfully', async () => {
178+
const vecLib = await vectorLibMock.start()
179+
await controller.updateIndex(['test.java'], 'add')
180+
sinonAssert.called(vecLib.updateIndexV2)
181+
})
182+
183+
it('should handle update errors', async () => {
184+
const vecLib = await vectorLibMock.start()
185+
vecLib.updateIndexV2.rejects(new Error('Update failed'))
186+
187+
await controller.updateIndex(['test.java'], 'add')
188+
sinonAssert.called(logging.error)
189+
})
190+
})
191+
192+
describe('dispose', () => {
193+
it('should clear and remove vector library reference', async () => {
194+
await controller.init(vectorLibMock)
195+
await controller.dispose()
196+
197+
const vecLib = await vectorLibMock.start()
198+
sinonAssert.called(vecLib.clear)
199+
200+
const queryResult = await controller.queryVectorIndex({ query: 'test' })
201+
assert.deepStrictEqual(queryResult, [])
202+
})
203+
})
204+
})
205+
206+
function createMockDirent(name: string, isDirectory: boolean): Dirent {
207+
return {
208+
name,
209+
isDirectory: () => isDirectory,
210+
isFile: () => !isDirectory,
211+
isBlockDevice: () => false,
212+
isCharacterDevice: () => false,
213+
isFIFO: () => false,
214+
isSocket: () => false,
215+
isSymbolicLink: () => false,
216+
} as Dirent
217+
}

0 commit comments

Comments
 (0)