Skip to content

Commit aa1f16c

Browse files
committed
feat(amazonq): Use codewhisperer language server for completions
1 parent e193d44 commit aa1f16c

File tree

7 files changed

+396
-3
lines changed

7 files changed

+396
-3
lines changed

package-lock.json

Lines changed: 91 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
/*!
2+
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
import {
7+
CancellationToken,
8+
InlineCompletionContext,
9+
InlineCompletionItem,
10+
InlineCompletionItemProvider,
11+
InlineCompletionList,
12+
Position,
13+
TextDocument,
14+
commands,
15+
languages,
16+
} from 'vscode'
17+
import { LanguageClient } from 'vscode-languageclient'
18+
import {
19+
InlineCompletionItemWithReferences,
20+
InlineCompletionListWithReferences,
21+
InlineCompletionWithReferencesParams,
22+
inlineCompletionWithReferencesRequestType,
23+
logInlineCompletionSessionResultsNotificationType,
24+
LogInlineCompletionSessionResultsParams,
25+
} from '@aws/language-server-runtimes/protocol'
26+
27+
export const CodewhispererInlineCompletionLanguages = [
28+
{ scheme: 'file', language: 'typescript' },
29+
{ scheme: 'file', language: 'javascript' },
30+
{ scheme: 'file', language: 'json' },
31+
{ scheme: 'file', language: 'yaml' },
32+
{ scheme: 'file', language: 'java' },
33+
{ scheme: 'file', language: 'go' },
34+
{ scheme: 'file', language: 'php' },
35+
{ scheme: 'file', language: 'rust' },
36+
{ scheme: 'file', language: 'kotlin' },
37+
{ scheme: 'file', language: 'terraform' },
38+
{ scheme: 'file', language: 'ruby' },
39+
{ scheme: 'file', language: 'shellscript' },
40+
{ scheme: 'file', language: 'dart' },
41+
{ scheme: 'file', language: 'lua' },
42+
{ scheme: 'file', language: 'powershell' },
43+
{ scheme: 'file', language: 'r' },
44+
{ scheme: 'file', language: 'swift' },
45+
{ scheme: 'file', language: 'systemverilog' },
46+
{ scheme: 'file', language: 'scala' },
47+
{ scheme: 'file', language: 'vue' },
48+
{ scheme: 'file', language: 'csharp' },
49+
]
50+
51+
export function registerInlineCompletion(languageClient: LanguageClient) {
52+
const inlineCompletionProvider = new AmazonQInlineCompletionItemProvider(languageClient)
53+
languages.registerInlineCompletionItemProvider(CodewhispererInlineCompletionLanguages, inlineCompletionProvider)
54+
55+
const onInlineAcceptance = async (
56+
sessionId: string,
57+
itemId: string,
58+
requestStartTime: number,
59+
firstCompletionDisplayLatency?: number
60+
) => {
61+
const params: LogInlineCompletionSessionResultsParams = {
62+
sessionId: sessionId,
63+
completionSessionResult: {
64+
[itemId]: {
65+
seen: true,
66+
accepted: true,
67+
discarded: false,
68+
},
69+
},
70+
totalSessionDisplayTime: Date.now() - requestStartTime,
71+
firstCompletionDisplayLatency: firstCompletionDisplayLatency,
72+
}
73+
languageClient.sendNotification(logInlineCompletionSessionResultsNotificationType as any, params)
74+
}
75+
commands.registerCommand('aws.sample-vscode-ext-amazonq.accept', onInlineAcceptance)
76+
}
77+
78+
export class AmazonQInlineCompletionItemProvider implements InlineCompletionItemProvider {
79+
constructor(private readonly languageClient: LanguageClient) {}
80+
81+
async provideInlineCompletionItems(
82+
document: TextDocument,
83+
position: Position,
84+
context: InlineCompletionContext,
85+
token: CancellationToken
86+
): Promise<InlineCompletionItem[] | InlineCompletionList> {
87+
const requestStartTime = Date.now()
88+
const request: InlineCompletionWithReferencesParams = {
89+
textDocument: {
90+
uri: document.uri.toString(),
91+
},
92+
position,
93+
context,
94+
}
95+
96+
const response = await this.languageClient.sendRequest(
97+
inlineCompletionWithReferencesRequestType as any,
98+
request,
99+
token
100+
)
101+
102+
const list: InlineCompletionListWithReferences = response as InlineCompletionListWithReferences
103+
this.languageClient.info(`Client: Received ${list.items.length} suggestions`)
104+
const firstCompletionDisplayLatency = Date.now() - requestStartTime
105+
106+
// Add completion session tracking and attach onAcceptance command to each item to record used decision
107+
list.items.forEach((item: InlineCompletionItemWithReferences) => {
108+
item.command = {
109+
command: 'aws.sample-vscode-ext-amazonq.accept',
110+
title: 'On acceptance',
111+
arguments: [list.sessionId, item.itemId, requestStartTime, firstCompletionDisplayLatency],
112+
}
113+
})
114+
115+
return list as InlineCompletionList
116+
}
117+
}

packages/amazonq/src/lsp/activation.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,9 @@
44
*/
55

66
import vscode from 'vscode'
7+
import path from 'path'
78
import { AmazonQLSPDownloader } from './download'
9+
import { startLanguageServer } from './client'
810

911
export async function activate(ctx: vscode.ExtensionContext): Promise<void> {
1012
const serverPath = ctx.asAbsolutePath('resources/qdeveloperserver')
@@ -18,4 +20,5 @@ export async function activate(ctx: vscode.ExtensionContext): Promise<void> {
1820
*
1921
* TODO: actually hook up the language server
2022
*/
23+
await startLanguageServer(ctx, path.join(serverPath, 'aws-lsp-codewhisperer.js'))
2124
}

packages/amazonq/src/lsp/auth.ts

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
/*!
2+
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
import { window } from 'vscode'
7+
import { RequestType, ResponseMessage } from '@aws/language-server-runtimes/protocol'
8+
import * as jose from 'jose'
9+
import * as crypto from 'crypto'
10+
import { LanguageClient } from 'vscode-languageclient'
11+
import { AuthUtil } from 'aws-core-vscode/codewhisperer'
12+
import { Writable } from 'stream'
13+
14+
const encryptionKey = crypto.randomBytes(32)
15+
16+
/**
17+
* Sends a json payload to the language server, who is waiting to know what the encryption key is.
18+
* Code reference: https://github.com/aws/language-servers/blob/7da212185a5da75a72ce49a1a7982983f438651a/client/vscode/src/credentialsActivation.ts#L77
19+
*/
20+
export function writeEncryptionInit(stream: Writable): void {
21+
const request = {
22+
version: '1.0',
23+
mode: 'JWT',
24+
key: encryptionKey.toString('base64'),
25+
}
26+
stream.write(JSON.stringify(request))
27+
stream.write('\n')
28+
}
29+
30+
/**
31+
* Request for custom notifications that Update Credentials and tokens.
32+
* See core\aws-lsp-core\src\credentials\updateCredentialsRequest.ts for details
33+
*/
34+
export interface UpdateCredentialsRequest {
35+
/**
36+
* Encrypted token (JWT or PASETO)
37+
* The token's contents differ whether IAM or Bearer token is sent
38+
*/
39+
data: string
40+
/**
41+
* Used by the runtime based language servers.
42+
* Signals that this client will encrypt its credentials payloads.
43+
*/
44+
encrypted: boolean
45+
}
46+
47+
const notificationTypes = {
48+
updateBearerToken: new RequestType<UpdateCredentialsRequest, ResponseMessage, Error>(
49+
'aws/credentials/token/update'
50+
),
51+
}
52+
53+
export class AmazonQLSPAuth {
54+
constructor(private readonly client: LanguageClient) {}
55+
56+
async init() {
57+
const activeConnection = AuthUtil.instance.auth.activeConnection
58+
if (activeConnection?.type === 'sso') {
59+
// send the token to the language server
60+
const token = await AuthUtil.instance.getBearerToken()
61+
await this.updateBearerToken(token)
62+
void window.showErrorMessage(`Updated bearer token`)
63+
}
64+
}
65+
66+
private async updateBearerToken(token: string) {
67+
const request = await this.createUpdateCredentialsRequest({
68+
token,
69+
})
70+
71+
await this.client.sendRequest(notificationTypes.updateBearerToken.method, request)
72+
73+
this.client.info(`UpdateBearerToken: ${JSON.stringify(request)}`)
74+
}
75+
76+
private async createUpdateCredentialsRequest(data: any) {
77+
const payload = new TextEncoder().encode(JSON.stringify({ data }))
78+
79+
const jwt = await new jose.CompactEncrypt(payload)
80+
.setProtectedHeader({ alg: 'dir', enc: 'A256GCM' })
81+
.encrypt(encryptionKey)
82+
83+
return {
84+
data: jwt,
85+
encrypted: true,
86+
}
87+
}
88+
}

0 commit comments

Comments
 (0)