diff --git a/package-lock.json b/package-lock.json index 555a6f92099..aeff74a10f4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10906,8 +10906,6 @@ }, "node_modules/@aws/chat-client-ui-types": { "version": "0.1.26", - "resolved": "https://registry.npmjs.org/@aws/chat-client-ui-types/-/chat-client-ui-types-0.1.26.tgz", - "integrity": "sha512-WlF0fP1nojueknr815dg6Ivs+Q3e5onvWTH1nI05jysSzUHjsWwFDBrsxqJXfaPIFhPrbQzHqoxHbhIwQ1OLuw==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -11818,7 +11816,7 @@ "node": "^18.19.0 || >=20.6.0" }, "peerDependencies": { - "@opentelemetry/api": "^1.3.0" + "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "node_modules/@opentelemetry/otlp-transformer": { @@ -11878,6 +11876,39 @@ "@opentelemetry/api": ">=1.4.0 <1.10.0" } }, + "node_modules/@opentelemetry/sdk-logs/node_modules/@opentelemetry/core": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.0.0.tgz", + "integrity": "sha512-SLX36allrcnVaPYG3R78F/UZZsBsvbc7lMCLx37LyH5MJ1KAAZ2E3mW9OAD3zGz0G8q/BtoS5VUrjzDydhD6LQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/sdk-logs/node_modules/@opentelemetry/resources": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.0.0.tgz", + "integrity": "sha512-rnZr6dML2z4IARI4zPGQV4arDikF/9OXZQzrC01dLmn0CZxU5U5OLd/m1T7YkGRj5UitjeoCtg/zorlgMQcdTg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.0.0", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.3.0 <1.10.0" + } + }, "node_modules/@opentelemetry/sdk-metrics": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-metrics/-/sdk-metrics-2.0.0.tgz", @@ -23932,9 +23963,8 @@ }, "node_modules/ts-node": { "version": "10.9.2", - "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", - "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", "dev": true, + "license": "MIT", "dependencies": { "@cspotcode/source-map-support": "^0.8.0", "@tsconfig/node10": "^1.0.7", diff --git a/packages/amazonq/src/app/inline/activation.ts b/packages/amazonq/src/app/inline/activation.ts index d786047b2aa..69515127441 100644 --- a/packages/amazonq/src/app/inline/activation.ts +++ b/packages/amazonq/src/app/inline/activation.ts @@ -6,68 +6,26 @@ import vscode from 'vscode' import { AuthUtil, - CodeSuggestionsState, - CodeWhispererCodeCoverageTracker, CodeWhispererConstants, - CodeWhispererSettings, - ConfigurationEntry, - DefaultCodeWhispererClient, - invokeRecommendation, isInlineCompletionEnabled, - KeyStrokeHandler, - RecommendationHandler, runtimeLanguageContext, TelemetryHelper, UserWrittenCodeTracker, vsCodeState, } from 'aws-core-vscode/codewhisperer' -import { Commands, getLogger, globals, sleep } from 'aws-core-vscode/shared' +import { globals, sleep } from 'aws-core-vscode/shared' export async function activate() { - const codewhispererSettings = CodeWhispererSettings.instance - const client = new DefaultCodeWhispererClient() - if (isInlineCompletionEnabled()) { await setSubscriptionsforInlineCompletion() await AuthUtil.instance.setVscodeContextProps() } - function getAutoTriggerStatus(): boolean { - return CodeSuggestionsState.instance.isSuggestionsEnabled() - } - - async function getConfigEntry(): Promise { - const isShowMethodsEnabled: boolean = - vscode.workspace.getConfiguration('editor').get('suggest.showMethods') || false - const isAutomatedTriggerEnabled: boolean = getAutoTriggerStatus() - const isManualTriggerEnabled: boolean = true - const isSuggestionsWithCodeReferencesEnabled = codewhispererSettings.isSuggestionsWithCodeReferencesEnabled() - - // TODO:remove isManualTriggerEnabled - return { - isShowMethodsEnabled, - isManualTriggerEnabled, - isAutomatedTriggerEnabled, - isSuggestionsWithCodeReferencesEnabled, - } - } - async function setSubscriptionsforInlineCompletion() { - RecommendationHandler.instance.subscribeSuggestionCommands() - /** * Automated trigger */ globals.context.subscriptions.push( - vscode.window.onDidChangeActiveTextEditor(async (editor) => { - await RecommendationHandler.instance.onEditorChange() - }), - vscode.window.onDidChangeWindowState(async (e) => { - await RecommendationHandler.instance.onFocusChange() - }), - vscode.window.onDidChangeTextEditorSelection(async (e) => { - await RecommendationHandler.instance.onCursorChange(e) - }), vscode.workspace.onDidChangeTextDocument(async (e) => { const editor = vscode.window.activeTextEditor if (!editor) { @@ -80,7 +38,6 @@ export async function activate() { return } - CodeWhispererCodeCoverageTracker.getTracker(e.document.languageId)?.countTotalTokens(e) UserWrittenCodeTracker.instance.onTextDocumentChange(e) /** * Handle this keystroke event only when @@ -105,19 +62,6 @@ export async function activate() { * Then this event can be processed by our code. */ await sleep(CodeWhispererConstants.vsCodeCursorUpdateDelay) - if (!RecommendationHandler.instance.isSuggestionVisible()) { - await KeyStrokeHandler.instance.processKeyStroke(e, editor, client, await getConfigEntry()) - } - }), - // manual trigger - Commands.register({ id: 'aws.amazonq.invokeInlineCompletion', autoconnect: true }, async () => { - invokeRecommendation( - vscode.window.activeTextEditor as vscode.TextEditor, - client, - await getConfigEntry() - ).catch((e) => { - getLogger().error('invokeRecommendation failed: %s', (e as Error).message) - }) }) ) } diff --git a/packages/amazonq/src/app/inline/completion.ts b/packages/amazonq/src/app/inline/completion.ts index be390cef34c..1e0716097bb 100644 --- a/packages/amazonq/src/app/inline/completion.ts +++ b/packages/amazonq/src/app/inline/completion.ts @@ -8,7 +8,6 @@ import { InlineCompletionContext, InlineCompletionItem, InlineCompletionItemProvider, - InlineCompletionList, Position, TextDocument, commands, @@ -16,6 +15,8 @@ import { Disposable, window, TextEditor, + InlineCompletionTriggerKind, + Range, } from 'vscode' import { LanguageClient } from 'vscode-languageclient' import { @@ -27,10 +28,20 @@ import { RecommendationService } from './recommendationService' import { CodeWhispererConstants, ReferenceHoverProvider, - ReferenceInlineProvider, ReferenceLogViewProvider, ImportAdderProvider, + CodeSuggestionsState, + vsCodeState, + inlineCompletionsDebounceDelay, + noInlineSuggestionsMsg, + ReferenceInlineProvider, } from 'aws-core-vscode/codewhisperer' +import { InlineGeneratingMessage } from './inlineGeneratingMessage' +import { LineTracker } from './stateTracker/lineTracker' +import { InlineTutorialAnnotation } from './tutorials/inlineTutorialAnnotation' +import { TelemetryHelper } from './telemetryHelper' +import { getLogger } from 'aws-core-vscode/shared' +import { debounce, messageUtils } from 'aws-core-vscode/utils' export class InlineCompletionManager implements Disposable { private disposable: Disposable @@ -38,26 +49,42 @@ export class InlineCompletionManager implements Disposable { private languageClient: LanguageClient private sessionManager: SessionManager private recommendationService: RecommendationService + private lineTracker: LineTracker + private incomingGeneratingMessage: InlineGeneratingMessage + private inlineTutorialAnnotation: InlineTutorialAnnotation private readonly logSessionResultMessageName = 'aws/logInlineCompletionSessionResults' - constructor(languageClient: LanguageClient) { + constructor( + languageClient: LanguageClient, + sessionManager: SessionManager, + lineTracker: LineTracker, + inlineTutorialAnnotation: InlineTutorialAnnotation + ) { this.languageClient = languageClient - this.sessionManager = new SessionManager() - this.recommendationService = new RecommendationService(this.sessionManager) + this.sessionManager = sessionManager + this.lineTracker = lineTracker + this.incomingGeneratingMessage = new InlineGeneratingMessage(this.lineTracker) + this.recommendationService = new RecommendationService(this.sessionManager, this.incomingGeneratingMessage) + this.inlineTutorialAnnotation = inlineTutorialAnnotation this.inlineCompletionProvider = new AmazonQInlineCompletionItemProvider( languageClient, this.recommendationService, - this.sessionManager + this.sessionManager, + this.inlineTutorialAnnotation ) this.disposable = languages.registerInlineCompletionItemProvider( CodeWhispererConstants.platformLanguageIds, this.inlineCompletionProvider ) + + this.lineTracker.ready() } public dispose(): void { if (this.disposable) { this.disposable.dispose() + this.incomingGeneratingMessage.dispose() + this.lineTracker.dispose() } } @@ -97,10 +124,23 @@ export class InlineCompletionManager implements Disposable { ) ReferenceLogViewProvider.instance.addReferenceLog(referenceLog) ReferenceHoverProvider.instance.addCodeReferences(item.insertText as string, item.references) + + // Show codelense for 5 seconds. + ReferenceInlineProvider.instance.setInlineReference( + startLine, + item.insertText as string, + item.references + ) + setTimeout(() => { + ReferenceInlineProvider.instance.removeInlineReference() + }, 5000) } if (item.mostRelevantMissingImports?.length) { await ImportAdderProvider.instance.onAcceptRecommendation(editor, item, startLine) } + this.sessionManager.incrementSuggestionCount() + // clear session manager states once accepted + this.sessionManager.clear() } commands.registerCommand('aws.amazonq.acceptInline', onInlineAcceptance) @@ -128,40 +168,10 @@ export class InlineCompletionManager implements Disposable { }, } this.languageClient.sendNotification(this.logSessionResultMessageName, params) + // clear session manager states once rejected + this.sessionManager.clear() } commands.registerCommand('aws.amazonq.rejectCodeSuggestion', onInlineRejection) - - /* - We have to overwrite the prev. and next. commands because the inlineCompletionProvider only contained the current item - To show prev. and next. recommendation we need to re-register a new provider with the previous or next item - */ - - const swapProviderAndShow = async () => { - await commands.executeCommand('editor.action.inlineSuggest.hide') - this.disposable.dispose() - this.disposable = languages.registerInlineCompletionItemProvider( - CodeWhispererConstants.platformLanguageIds, - new AmazonQInlineCompletionItemProvider( - this.languageClient, - this.recommendationService, - this.sessionManager, - false - ) - ) - await commands.executeCommand('editor.action.inlineSuggest.trigger') - } - - const prevCommandHandler = async () => { - this.sessionManager.decrementActiveIndex() - await swapProviderAndShow() - } - commands.registerCommand('editor.action.inlineSuggest.showPrevious', prevCommandHandler) - - const nextCommandHandler = async () => { - this.sessionManager.incrementActiveIndex() - await swapProviderAndShow() - } - commands.registerCommand('editor.action.inlineSuggest.showNext', nextCommandHandler) } } @@ -170,17 +180,88 @@ export class AmazonQInlineCompletionItemProvider implements InlineCompletionItem private readonly languageClient: LanguageClient, private readonly recommendationService: RecommendationService, private readonly sessionManager: SessionManager, - private readonly isNewSession: boolean = true + private readonly inlineTutorialAnnotation: InlineTutorialAnnotation ) {} - async provideInlineCompletionItems( + private readonly logSessionResultMessageName = 'aws/logInlineCompletionSessionResults' + provideInlineCompletionItems = debounce( + this._provideInlineCompletionItems.bind(this), + inlineCompletionsDebounceDelay, + true + ) + + private async _provideInlineCompletionItems( document: TextDocument, position: Position, context: InlineCompletionContext, token: CancellationToken - ): Promise { - if (this.isNewSession) { - // make service requests if it's a new session + ): Promise { + // prevent concurrent API calls and write to shared state variables + if (vsCodeState.isRecommendationsActive) { + return [] + } + try { + vsCodeState.isRecommendationsActive = true + const isAutoTrigger = context.triggerKind === InlineCompletionTriggerKind.Automatic + if (isAutoTrigger && !CodeSuggestionsState.instance.isSuggestionsEnabled()) { + // return early when suggestions are disabled with auto trigger + return [] + } + + // handling previous session + const prevSession = this.sessionManager.getActiveSession() + const prevSessionId = prevSession?.sessionId + const prevItemId = this.sessionManager.getActiveRecommendation()?.[0]?.itemId + const prevStartPosition = prevSession?.startPosition + const editor = window.activeTextEditor + if (prevSession && prevSessionId && prevItemId && prevStartPosition) { + const prefix = document.getText(new Range(prevStartPosition, position)) + const prevItemMatchingPrefix = [] + for (const item of this.sessionManager.getActiveRecommendation()) { + const text = typeof item.insertText === 'string' ? item.insertText : item.insertText.value + if (text.startsWith(prefix) && position.isAfterOrEqual(prevStartPosition)) { + item.command = { + command: 'aws.amazonq.acceptInline', + title: 'On acceptance', + arguments: [ + prevSessionId, + item, + editor, + prevSession?.requestStartTime, + position.line, + prevSession?.firstCompletionDisplayLatency, + ], + } + item.range = new Range(prevStartPosition, position) + prevItemMatchingPrefix.push(item as InlineCompletionItem) + } + } + // re-use previous suggestions as long as new typed prefix matches + if (prevItemMatchingPrefix.length > 0) { + getLogger().debug(`Re-using suggestions that match user typed characters`) + return prevItemMatchingPrefix + } + getLogger().debug(`Auto rejecting suggestions from previous session`) + // if no such suggestions, report the previous suggestion as Reject + const params: LogInlineCompletionSessionResultsParams = { + sessionId: prevSessionId, + completionSessionResult: { + [prevItemId]: { + seen: true, + accepted: false, + discarded: false, + }, + }, + } + this.languageClient.sendNotification(this.logSessionResultMessageName, params) + this.sessionManager.clear() + } + + // tell the tutorial that completions has been triggered + await this.inlineTutorialAnnotation.triggered(context.triggerKind) + TelemetryHelper.instance.setInvokeSuggestionStartTime() + TelemetryHelper.instance.setTriggerType(context.triggerKind) + await this.recommendationService.getAllRecommendations( this.languageClient, document, @@ -188,34 +269,95 @@ export class AmazonQInlineCompletionItemProvider implements InlineCompletionItem context, token ) - } - // get active item from session for displaying - const items = this.sessionManager.getActiveRecommendation() - const session = this.sessionManager.getActiveSession() - if (!session || !items.length) { - return [] - } - const editor = window.activeTextEditor - for (const item of items) { - item.command = { - command: 'aws.amazonq.acceptInline', - title: 'On acceptance', - arguments: [ - session.sessionId, - item, - editor, - session.requestStartTime, - position.line, - session.firstCompletionDisplayLatency, - ], + // get active item from session for displaying + const items = this.sessionManager.getActiveRecommendation() + const itemId = this.sessionManager.getActiveRecommendation()?.[0]?.itemId + const session = this.sessionManager.getActiveSession() + + // Show message to user when manual invoke fails to produce results. + if (items.length === 0 && context.triggerKind === InlineCompletionTriggerKind.Invoke) { + void messageUtils.showTimedMessage(noInlineSuggestionsMsg, 2000) } - ReferenceInlineProvider.instance.setInlineReference( - position.line, - item.insertText as string, - item.references - ) - ImportAdderProvider.instance.onShowRecommendation(document, position.line, item) + + if (!session || !items.length || !editor) { + getLogger().debug( + `Failed to produce inline suggestion results. Received ${items.length} items from service` + ) + return [] + } + + const cursorPosition = document.validatePosition(position) + + if (position.isAfter(editor.selection.active)) { + getLogger().debug(`Cursor moved behind trigger position. Discarding suggestion...`) + const params: LogInlineCompletionSessionResultsParams = { + sessionId: session.sessionId, + completionSessionResult: { + [itemId]: { + seen: false, + accepted: false, + discarded: true, + }, + }, + } + this.languageClient.sendNotification(this.logSessionResultMessageName, params) + this.sessionManager.clear() + return [] + } + + // the user typed characters from invoking suggestion cursor position to receiving suggestion position + const typeahead = document.getText(new Range(position, editor.selection.active)) + + const itemsMatchingTypeahead = [] + + for (const item of items) { + item.insertText = typeof item.insertText === 'string' ? item.insertText : item.insertText.value + if (item.insertText.startsWith(typeahead)) { + item.command = { + command: 'aws.amazonq.acceptInline', + title: 'On acceptance', + arguments: [ + session.sessionId, + item, + editor, + session.requestStartTime, + cursorPosition.line, + session.firstCompletionDisplayLatency, + ], + } + item.range = new Range(cursorPosition, cursorPosition) + itemsMatchingTypeahead.push(item) + ImportAdderProvider.instance.onShowRecommendation(document, cursorPosition.line, item) + } + } + + // report discard if none of suggestions match typeahead + if (itemsMatchingTypeahead.length === 0) { + getLogger().debug( + `Suggestion does not match user typeahead from insertion position. Discarding suggestion...` + ) + const params: LogInlineCompletionSessionResultsParams = { + sessionId: session.sessionId, + completionSessionResult: { + [itemId]: { + seen: false, + accepted: false, + discarded: true, + }, + }, + } + this.languageClient.sendNotification(this.logSessionResultMessageName, params) + this.sessionManager.clear() + return [] + } + + // suggestions returned here will be displayed on screen + return itemsMatchingTypeahead as InlineCompletionItem[] + } catch (e) { + getLogger('amazonqLsp').error('Failed to provide completion items: %O', e) + return [] + } finally { + vsCodeState.isRecommendationsActive = false } - return items as InlineCompletionItem[] } } diff --git a/packages/amazonq/src/app/inline/inlineGeneratingMessage.ts b/packages/amazonq/src/app/inline/inlineGeneratingMessage.ts new file mode 100644 index 00000000000..6c2d97fdad2 --- /dev/null +++ b/packages/amazonq/src/app/inline/inlineGeneratingMessage.ts @@ -0,0 +1,98 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import { editorUtilities } from 'aws-core-vscode/shared' +import * as vscode from 'vscode' +import { LineSelection, LineTracker } from './stateTracker/lineTracker' +import { AuthUtil } from 'aws-core-vscode/codewhisperer' +import { cancellableDebounce } from 'aws-core-vscode/utils' + +/** + * Manages the inline ghost text message show when Inline Suggestions is "thinking". + */ +export class InlineGeneratingMessage implements vscode.Disposable { + private readonly _disposable: vscode.Disposable + + private readonly cwLineHintDecoration: vscode.TextEditorDecorationType = + vscode.window.createTextEditorDecorationType({ + after: { + margin: '0 0 0 3em', + contentText: 'Amazon Q is generating...', + textDecoration: 'none', + fontWeight: 'normal', + fontStyle: 'normal', + color: 'var(--vscode-editorCodeLens-foreground)', + }, + rangeBehavior: vscode.DecorationRangeBehavior.OpenOpen, + isWholeLine: true, + }) + + constructor(private readonly lineTracker: LineTracker) { + this._disposable = vscode.Disposable.from( + AuthUtil.instance.auth.onDidChangeConnectionState(async (e) => { + if (e.state !== 'authenticating') { + this.hideGenerating() + } + }), + AuthUtil.instance.secondaryAuth.onDidChangeActiveConnection(async () => { + this.hideGenerating() + }) + ) + } + + dispose() { + this._disposable.dispose() + } + + readonly refreshDebounced = cancellableDebounce(async () => { + await this._refresh(true) + }, 1000) + + async showGenerating(triggerType: vscode.InlineCompletionTriggerKind) { + if (triggerType === vscode.InlineCompletionTriggerKind.Invoke) { + // if user triggers on demand, immediately update the UI and cancel the previous debounced update if there is one + this.refreshDebounced.cancel() + await this._refresh(true) + } else { + await this.refreshDebounced.promise() + } + } + + async _refresh(shouldDisplay: boolean) { + const editor = vscode.window.activeTextEditor + if (!editor) { + return + } + + const selections = this.lineTracker.selections + if (!editor || !selections || !editorUtilities.isTextEditor(editor)) { + this.hideGenerating() + return + } + + if (!AuthUtil.instance.isConnectionValid()) { + this.hideGenerating() + return + } + + await this.updateDecorations(editor, selections, shouldDisplay) + } + + hideGenerating() { + vscode.window.activeTextEditor?.setDecorations(this.cwLineHintDecoration, []) + } + + async updateDecorations(editor: vscode.TextEditor, lines: LineSelection[], shouldDisplay: boolean) { + const range = editor.document.validateRange( + new vscode.Range(lines[0].active, lines[0].active, lines[0].active, lines[0].active) + ) + + if (shouldDisplay) { + editor.setDecorations(this.cwLineHintDecoration, [range]) + } else { + editor.setDecorations(this.cwLineHintDecoration, []) + } + } +} diff --git a/packages/amazonq/src/app/inline/recommendationService.ts b/packages/amazonq/src/app/inline/recommendationService.ts index 45dd0099ebd..eab2fc874b8 100644 --- a/packages/amazonq/src/app/inline/recommendationService.ts +++ b/packages/amazonq/src/app/inline/recommendationService.ts @@ -11,9 +11,15 @@ import { import { CancellationToken, InlineCompletionContext, Position, TextDocument } from 'vscode' import { LanguageClient } from 'vscode-languageclient' import { SessionManager } from './sessionManager' +import { InlineGeneratingMessage } from './inlineGeneratingMessage' +import { CodeWhispererStatusBarManager } from 'aws-core-vscode/codewhisperer' +import { TelemetryHelper } from './telemetryHelper' export class RecommendationService { - constructor(private readonly sessionManager: SessionManager) {} + constructor( + private readonly sessionManager: SessionManager, + private readonly inlineGeneratingMessage: InlineGeneratingMessage + ) {} async getAllRecommendations( languageClient: LanguageClient, @@ -30,29 +36,56 @@ export class RecommendationService { context, } const requestStartTime = Date.now() + const statusBar = CodeWhispererStatusBarManager.instance + TelemetryHelper.instance.setInvokeSuggestionStartTime() + TelemetryHelper.instance.setPreprocessEndTime() + TelemetryHelper.instance.setSdkApiCallStartTime() - // Handle first request - const firstResult: InlineCompletionListWithReferences = await languageClient.sendRequest( - inlineCompletionWithReferencesRequestType as any, - request, - token - ) + try { + // Show UI indicators that we are generating suggestions + await this.inlineGeneratingMessage.showGenerating(context.triggerKind) + await statusBar.setLoading() - const firstCompletionDisplayLatency = Date.now() - requestStartTime - this.sessionManager.startSession( - firstResult.sessionId, - firstResult.items, - requestStartTime, - firstCompletionDisplayLatency - ) + // Handle first request + const firstResult: InlineCompletionListWithReferences = await languageClient.sendRequest( + inlineCompletionWithReferencesRequestType.method, + request, + token + ) - if (firstResult.partialResultToken) { - // If there are more results to fetch, handle them in the background - this.processRemainingRequests(languageClient, request, firstResult, token).catch((error) => { - languageClient.warn(`Error when getting suggestions: ${error}`) - }) - } else { - this.sessionManager.closeSession() + // Set telemetry data for the first response + TelemetryHelper.instance.setSdkApiCallEndTime() + TelemetryHelper.instance.setSessionId(firstResult.sessionId) + if (firstResult.items.length > 0) { + TelemetryHelper.instance.setFirstResponseRequestId(firstResult.items[0].itemId) + } + TelemetryHelper.instance.setFirstSuggestionShowTime() + + const firstCompletionDisplayLatency = Date.now() - requestStartTime + this.sessionManager.startSession( + firstResult.sessionId, + firstResult.items, + requestStartTime, + position, + firstCompletionDisplayLatency + ) + + if (firstResult.partialResultToken) { + // If there are more results to fetch, handle them in the background + this.processRemainingRequests(languageClient, request, firstResult, token).catch((error) => { + languageClient.warn(`Error when getting suggestions: ${error}`) + }) + } else { + this.sessionManager.closeSession() + + // No more results to fetch, mark pagination as complete + TelemetryHelper.instance.setAllPaginationEndTime() + TelemetryHelper.instance.tryRecordClientComponentLatency() + } + } finally { + // Remove all UI indicators of message generation since we are done + this.inlineGeneratingMessage.hideGenerating() + void statusBar.refreshStatusBar() // effectively "stop loading" } } @@ -66,13 +99,18 @@ export class RecommendationService { while (nextToken) { const request = { ...initialRequest, partialResultToken: nextToken } const result: InlineCompletionListWithReferences = await languageClient.sendRequest( - inlineCompletionWithReferencesRequestType as any, + inlineCompletionWithReferencesRequestType.method, request, token ) this.sessionManager.updateSessionSuggestions(result.items) nextToken = result.partialResultToken } + this.sessionManager.closeSession() + + // All pagination requests completed + TelemetryHelper.instance.setAllPaginationEndTime() + TelemetryHelper.instance.tryRecordClientComponentLatency() } } diff --git a/packages/amazonq/src/app/inline/sessionManager.ts b/packages/amazonq/src/app/inline/sessionManager.ts index 4b70a684001..6e052ddbfbe 100644 --- a/packages/amazonq/src/app/inline/sessionManager.ts +++ b/packages/amazonq/src/app/inline/sessionManager.ts @@ -2,7 +2,7 @@ * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: Apache-2.0 */ - +import * as vscode from 'vscode' import { InlineCompletionItemWithReferences } from '@aws/language-server-runtimes-types' // TODO: add more needed data to the session interface @@ -13,17 +13,20 @@ interface CodeWhispererSession { isRequestInProgress: boolean requestStartTime: number firstCompletionDisplayLatency?: number + startPosition: vscode.Position } export class SessionManager { private activeSession?: CodeWhispererSession - private activeIndex: number = 0 + private _acceptedSuggestionCount: number = 0 + constructor() {} public startSession( sessionId: string, suggestions: InlineCompletionItemWithReferences[], requestStartTime: number, + startPosition: vscode.Position, firstCompletionDisplayLatency?: number ) { this.activeSession = { @@ -31,9 +34,9 @@ export class SessionManager { suggestions, isRequestInProgress: true, requestStartTime, + startPosition, firstCompletionDisplayLatency, } - this.activeIndex = 0 } public closeSession() { @@ -54,49 +57,19 @@ export class SessionManager { this.activeSession.suggestions = [...this.activeSession.suggestions, ...suggestions] } - public incrementActiveIndex() { - const suggestionCount = this.activeSession?.suggestions?.length - if (!suggestionCount) { - return - } - this.activeIndex === suggestionCount - 1 ? suggestionCount - 1 : this.activeIndex++ + public getActiveRecommendation(): InlineCompletionItemWithReferences[] { + return this.activeSession?.suggestions ?? [] } - public decrementActiveIndex() { - this.activeIndex === 0 ? 0 : this.activeIndex-- + public get acceptedSuggestionCount(): number { + return this._acceptedSuggestionCount } - /* - We have to maintain the active suggestion index ourselves because VS Code doesn't expose which suggestion it's currently showing - In order to keep track of the right suggestion state, and for features such as reference tracker, this hack is still needed - */ - - public getActiveRecommendation(): InlineCompletionItemWithReferences[] { - let suggestionCount = this.activeSession?.suggestions.length - if (!suggestionCount) { - return [] - } - if (suggestionCount === 1 && this.activeSession?.isRequestInProgress) { - suggestionCount += 1 - } - - const activeSuggestion = this.activeSession?.suggestions[this.activeIndex] - if (!activeSuggestion) { - return [] - } - const items = [activeSuggestion] - // to make the total number of suggestions match the actual number - for (let i = 1; i < suggestionCount; i++) { - items.push({ - ...activeSuggestion, - insertText: `${i}`, - }) - } - return items + public incrementSuggestionCount() { + this._acceptedSuggestionCount += 1 } public clear() { this.activeSession = undefined - this.activeIndex = 0 } } diff --git a/packages/amazonq/src/app/inline/stateTracker/lineTracker.ts b/packages/amazonq/src/app/inline/stateTracker/lineTracker.ts new file mode 100644 index 00000000000..58bee329a40 --- /dev/null +++ b/packages/amazonq/src/app/inline/stateTracker/lineTracker.ts @@ -0,0 +1,178 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import * as vscode from 'vscode' +import { editorUtilities, setContext } from 'aws-core-vscode/shared' + +export interface LineSelection { + anchor: number + active: number +} + +export interface LinesChangeEvent { + readonly editor: vscode.TextEditor | undefined + readonly selections: LineSelection[] | undefined + + readonly reason: 'editor' | 'selection' | 'content' +} + +/** + * This class providees a single interface to manage and access users' "line" selections + * Callers could use it by subscribing onDidChangeActiveLines to do UI updates or logic needed to be executed when line selections get changed + */ +export class LineTracker implements vscode.Disposable { + private _onDidChangeActiveLines = new vscode.EventEmitter() + get onDidChangeActiveLines(): vscode.Event { + return this._onDidChangeActiveLines.event + } + + private _editor: vscode.TextEditor | undefined + private _disposable: vscode.Disposable | undefined + + private _selections: LineSelection[] | undefined + get selections(): LineSelection[] | undefined { + return this._selections + } + + private _onReady: vscode.EventEmitter = new vscode.EventEmitter() + get onReady(): vscode.Event { + return this._onReady.event + } + + private _ready: boolean = false + get isReady() { + return this._ready + } + + constructor() { + this._disposable = vscode.Disposable.from( + vscode.window.onDidChangeActiveTextEditor(async (e) => { + await this.onActiveTextEditorChanged(e) + }), + vscode.window.onDidChangeTextEditorSelection(async (e) => { + await this.onTextEditorSelectionChanged(e) + }), + vscode.workspace.onDidChangeTextDocument((e) => { + this.onContentChanged(e) + }) + ) + + queueMicrotask(async () => await this.onActiveTextEditorChanged(vscode.window.activeTextEditor)) + } + + dispose() { + this._disposable?.dispose() + } + + ready() { + if (this._ready) { + throw new Error('Linetracker is already activated') + } + + this._ready = true + queueMicrotask(() => this._onReady.fire()) + } + + // @VisibleForTesting + async onActiveTextEditorChanged(editor: vscode.TextEditor | undefined) { + if (editor === this._editor) { + return + } + + this._editor = editor + this._selections = toLineSelections(editor?.selections) + if (this._selections && this._selections[0]) { + const s = this._selections.map((item) => item.active + 1) + await setContext('codewhisperer.activeLine', s) + } + + this.notifyLinesChanged('editor') + } + + // @VisibleForTesting + async onTextEditorSelectionChanged(e: vscode.TextEditorSelectionChangeEvent) { + // If this isn't for our cached editor and its not a real editor -- kick out + if (this._editor !== e.textEditor && !editorUtilities.isTextEditor(e.textEditor)) { + return + } + + const selections = toLineSelections(e.selections) + if (this._editor === e.textEditor && this.includes(selections)) { + return + } + + this._editor = e.textEditor + this._selections = selections + if (this._selections && this._selections[0]) { + const s = this._selections.map((item) => item.active + 1) + await setContext('codewhisperer.activeLine', s) + } + + this.notifyLinesChanged('selection') + } + + // @VisibleForTesting + onContentChanged(e: vscode.TextDocumentChangeEvent) { + const editor = vscode.window.activeTextEditor + if (e.document === editor?.document && e.contentChanges.length > 0 && editorUtilities.isTextEditor(editor)) { + this._editor = editor + this._selections = toLineSelections(this._editor?.selections) + + this.notifyLinesChanged('content') + } + } + + notifyLinesChanged(reason: 'editor' | 'selection' | 'content') { + const e: LinesChangeEvent = { editor: this._editor, selections: this.selections, reason: reason } + this._onDidChangeActiveLines.fire(e) + } + + includes(selections: LineSelection[]): boolean + includes(line: number, options?: { activeOnly: boolean }): boolean + includes(lineOrSelections: number | LineSelection[], options?: { activeOnly: boolean }): boolean { + if (typeof lineOrSelections !== 'number') { + return isIncluded(lineOrSelections, this._selections) + } + + if (this._selections === undefined || this._selections.length === 0) { + return false + } + + const line = lineOrSelections + const activeOnly = options?.activeOnly ?? true + + for (const selection of this._selections) { + if ( + line === selection.active || + (!activeOnly && + ((selection.anchor >= line && line >= selection.active) || + (selection.active >= line && line >= selection.anchor))) + ) { + return true + } + } + return false + } +} + +function isIncluded(selections: LineSelection[] | undefined, within: LineSelection[] | undefined): boolean { + if (selections === undefined && within === undefined) { + return true + } + if (selections === undefined || within === undefined || selections.length !== within.length) { + return false + } + + return selections.every((s, i) => { + const match = within[i] + return s.active === match.active && s.anchor === match.anchor + }) +} + +function toLineSelections(selections: readonly vscode.Selection[]): LineSelection[] +function toLineSelections(selections: readonly vscode.Selection[] | undefined): LineSelection[] | undefined +function toLineSelections(selections: readonly vscode.Selection[] | undefined) { + return selections?.map((s) => ({ active: s.active.line, anchor: s.anchor.line })) +} diff --git a/packages/amazonq/src/app/inline/telemetryHelper.ts b/packages/amazonq/src/app/inline/telemetryHelper.ts new file mode 100644 index 00000000000..dffd267bee1 --- /dev/null +++ b/packages/amazonq/src/app/inline/telemetryHelper.ts @@ -0,0 +1,162 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import { AuthUtil, getSelectedCustomization } from 'aws-core-vscode/codewhisperer' +import { CodewhispererLanguage } from 'aws-core-vscode/shared' +import { CodewhispererTriggerType, telemetry } from 'aws-core-vscode/telemetry' +import { InlineCompletionTriggerKind } from 'vscode' + +export class TelemetryHelper { + // Variables needed for client component latency + private _invokeSuggestionStartTime = 0 + private _preprocessEndTime = 0 + private _sdkApiCallStartTime = 0 + private _sdkApiCallEndTime = 0 + private _allPaginationEndTime = 0 + private _firstSuggestionShowTime = 0 + private _firstResponseRequestId = '' + private _sessionId = '' + private _language: CodewhispererLanguage = 'java' + private _triggerType: CodewhispererTriggerType = 'OnDemand' + + constructor() {} + + static #instance: TelemetryHelper + + public static get instance() { + return (this.#instance ??= new this()) + } + + public resetClientComponentLatencyTime() { + this._invokeSuggestionStartTime = 0 + this._preprocessEndTime = 0 + this._sdkApiCallStartTime = 0 + this._sdkApiCallEndTime = 0 + this._firstSuggestionShowTime = 0 + this._allPaginationEndTime = 0 + this._firstResponseRequestId = '' + } + + public setInvokeSuggestionStartTime() { + this.resetClientComponentLatencyTime() + this._invokeSuggestionStartTime = performance.now() + } + + get invokeSuggestionStartTime(): number { + return this._invokeSuggestionStartTime + } + + public setPreprocessEndTime() { + this._preprocessEndTime = performance.now() + } + + get preprocessEndTime(): number { + return this._preprocessEndTime + } + + public setSdkApiCallStartTime() { + if (this._sdkApiCallStartTime === 0) { + this._sdkApiCallStartTime = performance.now() + } + } + + get sdkApiCallStartTime(): number { + return this._sdkApiCallStartTime + } + + public setSdkApiCallEndTime() { + if (this._sdkApiCallEndTime === 0 && this._sdkApiCallStartTime !== 0) { + this._sdkApiCallEndTime = performance.now() + } + } + + get sdkApiCallEndTime(): number { + return this._sdkApiCallEndTime + } + + public setAllPaginationEndTime() { + if (this._allPaginationEndTime === 0 && this._sdkApiCallEndTime !== 0) { + this._allPaginationEndTime = performance.now() + } + } + + get allPaginationEndTime(): number { + return this._allPaginationEndTime + } + + public setFirstSuggestionShowTime() { + if (this._firstSuggestionShowTime === 0 && this._sdkApiCallEndTime !== 0) { + this._firstSuggestionShowTime = performance.now() + } + } + + get firstSuggestionShowTime(): number { + return this._firstSuggestionShowTime + } + + public setFirstResponseRequestId(requestId: string) { + if (this._firstResponseRequestId === '') { + this._firstResponseRequestId = requestId + } + } + + get firstResponseRequestId(): string { + return this._firstResponseRequestId + } + + public setSessionId(sessionId: string) { + if (this._sessionId === '') { + this._sessionId = sessionId + } + } + + get sessionId(): string { + return this._sessionId + } + + public setLanguage(language: CodewhispererLanguage) { + this._language = language + } + + get language(): CodewhispererLanguage { + return this._language + } + + public setTriggerType(triggerType: InlineCompletionTriggerKind) { + if (triggerType === InlineCompletionTriggerKind.Invoke) { + this._triggerType = 'OnDemand' + } else if (triggerType === InlineCompletionTriggerKind.Automatic) { + this._triggerType = 'AutoTrigger' + } + } + + get triggerType(): string { + return this._triggerType + } + + // report client component latency after all pagination call finish + // and at least one suggestion is shown to the user + public tryRecordClientComponentLatency() { + if (this._firstSuggestionShowTime === 0 || this._allPaginationEndTime === 0) { + return + } + telemetry.codewhisperer_clientComponentLatency.emit({ + codewhispererAllCompletionsLatency: this._allPaginationEndTime - this._sdkApiCallStartTime, + codewhispererCompletionType: 'Line', + codewhispererCredentialFetchingLatency: 0, // no longer relevant, because we don't re-build the sdk. Flare already has that set + codewhispererCustomizationArn: getSelectedCustomization().arn, + codewhispererEndToEndLatency: this._firstSuggestionShowTime - this._invokeSuggestionStartTime, + codewhispererFirstCompletionLatency: this._sdkApiCallEndTime - this._sdkApiCallStartTime, + codewhispererLanguage: this._language, + codewhispererPostprocessingLatency: this._firstSuggestionShowTime - this._sdkApiCallEndTime, + codewhispererPreprocessingLatency: this._preprocessEndTime - this._invokeSuggestionStartTime, + codewhispererRequestId: this._firstResponseRequestId, + codewhispererSessionId: this._sessionId, + codewhispererTriggerType: this._triggerType, + credentialStartUrl: AuthUtil.instance.startUrl, + result: 'Succeeded', + }) + } +} diff --git a/packages/amazonq/src/inlineChat/decorations/inlineLineAnnotationController.ts b/packages/amazonq/src/app/inline/tutorials/inlineChatTutorialAnnotation.ts similarity index 72% rename from packages/amazonq/src/inlineChat/decorations/inlineLineAnnotationController.ts rename to packages/amazonq/src/app/inline/tutorials/inlineChatTutorialAnnotation.ts index 9ec5e08122d..1208b4766af 100644 --- a/packages/amazonq/src/inlineChat/decorations/inlineLineAnnotationController.ts +++ b/packages/amazonq/src/app/inline/tutorials/inlineChatTutorialAnnotation.ts @@ -3,14 +3,15 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { Container } from 'aws-core-vscode/codewhisperer' import * as vscode from 'vscode' +import { InlineTutorialAnnotation } from './inlineTutorialAnnotation' +import { globals } from 'aws-core-vscode/shared' -export class InlineLineAnnotationController { +export class InlineChatTutorialAnnotation { private enabled: boolean = true - constructor(context: vscode.ExtensionContext) { - context.subscriptions.push( + constructor(private readonly inlineTutorialAnnotation: InlineTutorialAnnotation) { + globals.context.subscriptions.push( vscode.window.onDidChangeTextEditorSelection(async ({ selections, textEditor }) => { let showShow = false @@ -33,12 +34,12 @@ export class InlineLineAnnotationController { private async setVisible(editor: vscode.TextEditor, visible: boolean) { let needsRefresh: boolean if (visible) { - needsRefresh = await Container.instance.lineAnnotationController.tryShowInlineHint() + needsRefresh = await this.inlineTutorialAnnotation.tryShowInlineHint() } else { - needsRefresh = await Container.instance.lineAnnotationController.tryHideInlineHint() + needsRefresh = await this.inlineTutorialAnnotation.tryHideInlineHint() } if (needsRefresh) { - await Container.instance.lineAnnotationController.refresh(editor, 'codewhisperer') + await this.inlineTutorialAnnotation.refresh(editor, 'codewhisperer') } } diff --git a/packages/amazonq/src/app/inline/tutorials/inlineTutorialAnnotation.ts b/packages/amazonq/src/app/inline/tutorials/inlineTutorialAnnotation.ts new file mode 100644 index 00000000000..bd12b1d28dd --- /dev/null +++ b/packages/amazonq/src/app/inline/tutorials/inlineTutorialAnnotation.ts @@ -0,0 +1,526 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import * as vscode from 'vscode' +import * as os from 'os' +import { + AnnotationChangeSource, + AuthUtil, + inlinehintKey, + runtimeLanguageContext, + TelemetryHelper, +} from 'aws-core-vscode/codewhisperer' +import { editorUtilities, getLogger, globals, setContext, vscodeUtilities } from 'aws-core-vscode/shared' +import { LinesChangeEvent, LineSelection, LineTracker } from '../stateTracker/lineTracker' +import { telemetry } from 'aws-core-vscode/telemetry' +import { cancellableDebounce } from 'aws-core-vscode/utils' +import { SessionManager } from '../sessionManager' + +const case3TimeWindow = 30000 // 30 seconds + +const maxSmallIntegerV8 = 2 ** 30 // Max number that can be stored in V8's smis (small integers) + +function fromId(id: string | undefined, sessionManager: SessionManager): AnnotationState | undefined { + switch (id) { + case AutotriggerState.id: + return new AutotriggerState(sessionManager) + case PressTabState.id: + return new AutotriggerState(sessionManager) + case ManualtriggerState.id: + return new ManualtriggerState() + case TryMoreExState.id: + return new TryMoreExState() + case EndState.id: + return new EndState() + case InlineChatState.id: + return new InlineChatState() + default: + return undefined + } +} + +interface AnnotationState { + id: string + suppressWhileRunning: boolean + decorationRenderOptions?: vscode.ThemableDecorationAttachmentRenderOptions + + text: () => string + updateState(changeSource: AnnotationChangeSource, force: boolean): AnnotationState | undefined + isNextState(state: AnnotationState | undefined): boolean +} + +/** + * case 1: How Cwspr triggers + * Trigger Criteria: + * User opens an editor file && + * CW is not providing a suggestion && + * User has not accepted any suggestion + * + * Exit criteria: + * User accepts 1 suggestion + * + */ +export class AutotriggerState implements AnnotationState { + static id = 'codewhisperer_learnmore_case_1' + id = AutotriggerState.id + + suppressWhileRunning = true + text = () => 'Amazon Q Tip 1/3: Start typing to get suggestions ([ESC] to exit)' + static acceptedCount = 0 + + constructor(private readonly sessionManager: SessionManager) {} + + updateState(changeSource: AnnotationChangeSource, force: boolean): AnnotationState | undefined { + if (AutotriggerState.acceptedCount < this.sessionManager.acceptedSuggestionCount) { + return new ManualtriggerState() + } else if (this.sessionManager.getActiveRecommendation().length > 0) { + return new PressTabState(this.sessionManager) + } else { + return this + } + } + + isNextState(state: AnnotationState | undefined): boolean { + return state instanceof ManualtriggerState + } +} + +/** + * case 1-a: Tab to accept + * Trigger Criteria: + * Case 1 && + * Inline suggestion is being shown + * + * Exit criteria: + * User accepts 1 suggestion + */ +export class PressTabState implements AnnotationState { + static id = 'codewhisperer_learnmore_case_1a' + id = PressTabState.id + + suppressWhileRunning = false + + text = () => 'Amazon Q Tip 1/3: Press [TAB] to accept ([ESC] to exit)' + + constructor(private readonly sessionManager: SessionManager) {} + + updateState(changeSource: AnnotationChangeSource, force: boolean): AnnotationState | undefined { + return new AutotriggerState(this.sessionManager).updateState(changeSource, force) + } + + isNextState(state: AnnotationState | undefined): boolean { + return state instanceof ManualtriggerState + } +} + +/** + * case 2: Manual trigger + * Trigger Criteria: + * User exists case 1 && + * User navigates to a new line + * + * Exit criteria: + * User inokes manual trigger shortcut + */ +export class ManualtriggerState implements AnnotationState { + static id = 'codewhisperer_learnmore_case_2' + id = ManualtriggerState.id + + suppressWhileRunning = true + + text = () => { + if (os.platform() === 'win32') { + return 'Amazon Q Tip 2/3: Invoke suggestions with [Alt] + [C] ([ESC] to exit)' + } + + return 'Amazon Q Tip 2/3: Invoke suggestions with [Option] + [C] ([ESC] to exit)' + } + hasManualTrigger: boolean = false + hasValidResponse: boolean = false + + updateState(changeSource: AnnotationChangeSource, force: boolean): AnnotationState | undefined { + if (this.hasManualTrigger && this.hasValidResponse) { + if (changeSource !== 'codewhisperer') { + return new TryMoreExState() + } else { + return undefined + } + } else { + return this + } + } + + isNextState(state: AnnotationState | undefined): boolean { + return state instanceof TryMoreExState + } +} + +/** + * case 3: Learn more + * Trigger Criteria: + * User exists case 2 && + * User navigates to a new line + * + * Exit criteria: + * User accepts or rejects the suggestion + */ +export class TryMoreExState implements AnnotationState { + static id = 'codewhisperer_learnmore_case_3' + id = TryMoreExState.id + + suppressWhileRunning = true + + text = () => 'Amazon Q Tip 3/3: For settings, open the Amazon Q menu from the status bar ([ESC] to exit)' + updateState(changeSource: AnnotationChangeSource, force: boolean): AnnotationState { + if (force) { + return new EndState() + } + return this + } + + isNextState(state: AnnotationState | undefined): boolean { + return state instanceof EndState + } + + static learnmoeCount: number = 0 +} + +export class EndState implements AnnotationState { + static id = 'codewhisperer_learnmore_end' + id = EndState.id + + suppressWhileRunning = true + text = () => '' + updateState(changeSource: AnnotationChangeSource, force: boolean): AnnotationState { + return this + } + isNextState(state: AnnotationState): boolean { + return false + } +} + +export class InlineChatState implements AnnotationState { + static id = 'amazonq_annotation_inline_chat' + id = InlineChatState.id + suppressWhileRunning = false + + text = () => { + if (os.platform() === 'darwin') { + return 'Amazon Q: Edit \u2318I' + } + return 'Amazon Q: Edit (Ctrl+I)' + } + updateState(_changeSource: AnnotationChangeSource, _force: boolean): AnnotationState { + return this + } + isNextState(_state: AnnotationState | undefined): boolean { + return false + } +} + +/** + * There are + * - existing users + * - new users + * -- new users who has not seen tutorial + * -- new users who has seen tutorial + * + * "existing users" should have the context key "CODEWHISPERER_AUTO_TRIGGER_ENABLED" + * "new users who has seen tutorial" should have the context key "inlineKey" and "CODEWHISPERER_AUTO_TRIGGER_ENABLED" + * the remaining grouop of users should belong to "new users who has not seen tutorial" + */ +export class InlineTutorialAnnotation implements vscode.Disposable { + private readonly _disposable: vscode.Disposable + private _editor: vscode.TextEditor | undefined + + private _currentState: AnnotationState + + private readonly cwLineHintDecoration: vscode.TextEditorDecorationType = + vscode.window.createTextEditorDecorationType({ + after: { + margin: '0 0 0 3em', + // "borderRadius" and "padding" are not available on "after" type of decoration, this is a hack to inject these css prop to "after" content. Refer to https://github.com/microsoft/vscode/issues/68845 + textDecoration: ';border-radius:0.25rem;padding:0rem 0.5rem;', + width: 'fit-content', + }, + rangeBehavior: vscode.DecorationRangeBehavior.OpenOpen, + }) + + constructor( + private readonly lineTracker: LineTracker, + private readonly sessionManager: SessionManager + ) { + const cachedState = fromId(globals.globalState.get(inlinehintKey), sessionManager) + const cachedAutotriggerEnabled = globals.globalState.get('CODEWHISPERER_AUTO_TRIGGER_ENABLED') + + // new users (has or has not seen tutorial) + if (cachedAutotriggerEnabled === undefined || cachedState !== undefined) { + this._currentState = cachedState ?? new AutotriggerState(this.sessionManager) + getLogger().debug( + `codewhisperer: new user login, activating inline tutorial. (autotriggerEnabled=${cachedAutotriggerEnabled}; inlineState=${cachedState?.id})` + ) + } else { + this._currentState = new EndState() + getLogger().debug(`codewhisperer: existing user login, disabling inline tutorial.`) + } + + this._disposable = vscode.Disposable.from( + vscodeUtilities.subscribeOnce(this.lineTracker.onReady)(async (_) => { + await this.onReady() + }), + this.lineTracker.onDidChangeActiveLines(async (e) => { + await this.onActiveLinesChanged(e) + }), + AuthUtil.instance.auth.onDidChangeConnectionState(async (e) => { + if (e.state !== 'authenticating') { + await this.refresh(vscode.window.activeTextEditor, 'editor') + } + }), + AuthUtil.instance.secondaryAuth.onDidChangeActiveConnection(async () => { + await this.refresh(vscode.window.activeTextEditor, 'editor') + }) + ) + } + + dispose() { + this._disposable.dispose() + } + + private _isReady: boolean = false + + private async onReady(): Promise { + this._isReady = !(this._currentState instanceof EndState) + await this._refresh(vscode.window.activeTextEditor, 'editor') + } + + async triggered(triggerType: vscode.InlineCompletionTriggerKind): Promise { + await telemetry.withTraceId(async () => { + if (!this._isReady) { + return + } + + if (this._currentState instanceof ManualtriggerState) { + if ( + triggerType === vscode.InlineCompletionTriggerKind.Invoke && + this._currentState.hasManualTrigger === false + ) { + this._currentState.hasManualTrigger = true + } + if ( + this.sessionManager.getActiveRecommendation().length > 0 && + this._currentState.hasValidResponse === false + ) { + this._currentState.hasValidResponse = true + } + } + + await this.refresh(vscode.window.activeTextEditor, 'codewhisperer') + }, TelemetryHelper.instance.traceId) + } + + isTutorialDone(): boolean { + return this._currentState.id === new EndState().id + } + + isInlineChatHint(): boolean { + return this._currentState.id === new InlineChatState().id + } + + async dismissTutorial() { + this._currentState = new EndState() + await setContext('aws.codewhisperer.tutorial.workInProgress', false) + await globals.globalState.update(inlinehintKey, this._currentState.id) + } + + /** + * Trys to show the inline hint, if the tutorial is not finished it will not be shown + */ + async tryShowInlineHint(): Promise { + if (this.isTutorialDone()) { + this._isReady = true + this._currentState = new InlineChatState() + return true + } + return false + } + + async tryHideInlineHint(): Promise { + if (this._currentState instanceof InlineChatState) { + this._currentState = new EndState() + return true + } + return false + } + + private async onActiveLinesChanged(e: LinesChangeEvent) { + if (!this._isReady) { + return + } + + this.clear() + + await this.refresh(e.editor, e.reason) + } + + clear() { + this._editor?.setDecorations(this.cwLineHintDecoration, []) + } + + async refresh(editor: vscode.TextEditor | undefined, source: AnnotationChangeSource, force?: boolean) { + if (force) { + this.refreshDebounced.cancel() + await this._refresh(editor, source, true) + } else { + await this.refreshDebounced.promise(editor, source) + } + } + + private readonly refreshDebounced = cancellableDebounce( + async (editor: vscode.TextEditor | undefined, source: AnnotationChangeSource, force?: boolean) => { + await this._refresh(editor, source, force) + }, + 250 + ) + + private async _refresh(editor: vscode.TextEditor | undefined, source: AnnotationChangeSource, force?: boolean) { + if (!this._isReady) { + this.clear() + return + } + + if (this.isTutorialDone()) { + this.clear() + return + } + + if (editor === undefined && this._editor === undefined) { + this.clear() + return + } + + const selections = this.lineTracker.selections + if (editor === undefined || selections === undefined || !editorUtilities.isTextEditor(editor)) { + this.clear() + return + } + + if (this._editor !== editor) { + // Clear any annotations on the previously active editor + this.clear() + this._editor = editor + } + + // Make sure the editor hasn't died since the await above and that we are still on the same line(s) + if (editor.document === undefined || !this.lineTracker.includes(selections)) { + this.clear() + return + } + + if (!AuthUtil.instance.isConnectionValid()) { + this.clear() + return + } + + // Disable Tips when language is not supported by Amazon Q. + if (!runtimeLanguageContext.isLanguageSupported(editor.document)) { + return + } + + await this.updateDecorations(editor, selections, source, force) + } + + private async updateDecorations( + editor: vscode.TextEditor, + lines: LineSelection[], + source: AnnotationChangeSource, + force?: boolean + ) { + const range = editor.document.validateRange( + new vscode.Range(lines[0].active, maxSmallIntegerV8, lines[0].active, maxSmallIntegerV8) + ) + + const decorationOptions = this.getInlineDecoration(editor, lines, source, force) as + | vscode.DecorationOptions + | undefined + + if (decorationOptions === undefined) { + this.clear() + await setContext('aws.codewhisperer.tutorial.workInProgress', false) + return + } else if (this.isTutorialDone()) { + // special case + // Endstate is meaningless and doesnt need to be rendered + this.clear() + await this.dismissTutorial() + return + } else if (decorationOptions.renderOptions?.after?.contentText === new TryMoreExState().text()) { + // special case + // case 3 exit criteria is to fade away in 30s + setTimeout(async () => { + await this.refresh(editor, source, true) + }, case3TimeWindow) + } + + decorationOptions.range = range + + await globals.globalState.update(inlinehintKey, this._currentState.id) + if (!this.isInlineChatHint()) { + await setContext('aws.codewhisperer.tutorial.workInProgress', true) + } + editor.setDecorations(this.cwLineHintDecoration, [decorationOptions]) + } + + getInlineDecoration( + editor: vscode.TextEditor, + lines: LineSelection[], + source: AnnotationChangeSource, + force?: boolean + ): Partial | undefined { + const isCWRunning = this.sessionManager.getActiveSession()?.isRequestInProgress ?? false + + const textOptions: vscode.ThemableDecorationAttachmentRenderOptions = { + contentText: '', + fontWeight: 'normal', + fontStyle: 'normal', + textDecoration: 'none', + color: 'var(--vscode-editor-background)', + backgroundColor: 'var(--vscode-foreground)', + } + + if (isCWRunning && this._currentState.suppressWhileRunning) { + return undefined + } + + const updatedState: AnnotationState | undefined = this._currentState.updateState(source, force ?? false) + + if (updatedState === undefined) { + return undefined + } + + if (this._currentState.isNextState(updatedState)) { + // special case because PressTabState is part of case_1 (1a) which possibly jumps directly from case_1a to case_2 and miss case_1 + if (this._currentState instanceof PressTabState) { + telemetry.ui_click.emit({ elementId: AutotriggerState.id, passive: true }) + } + telemetry.ui_click.emit({ elementId: this._currentState.id, passive: true }) + } + + // update state + this._currentState = updatedState + + // take snapshot of accepted session so that we can compre if there is delta -> users accept 1 suggestion after seeing this state + AutotriggerState.acceptedCount = this.sessionManager.acceptedSuggestionCount + + textOptions.contentText = this._currentState.text() + + return { + renderOptions: { after: textOptions }, + } + } + + public get currentState(): AnnotationState { + return this._currentState + } +} diff --git a/packages/amazonq/src/extension.ts b/packages/amazonq/src/extension.ts index 1a9d3c5facc..65caea3b2c8 100644 --- a/packages/amazonq/src/extension.ts +++ b/packages/amazonq/src/extension.ts @@ -134,7 +134,7 @@ export async function activateAmazonQCommon(context: vscode.ExtensionContext, is // for AL2, start LSP if glibc patch is found await activateAmazonqLsp(context) } - if (!Experiments.instance.get('amazonqLSPInline', false)) { + if (!Experiments.instance.get('amazonqLSPInline', true)) { await activateInlineCompletion() } diff --git a/packages/amazonq/src/extensionNode.ts b/packages/amazonq/src/extensionNode.ts index 8224b9ce310..d42fafea058 100644 --- a/packages/amazonq/src/extensionNode.ts +++ b/packages/amazonq/src/extensionNode.ts @@ -25,7 +25,6 @@ import { DevOptions } from 'aws-core-vscode/dev' import { Auth, AuthUtils, getTelemetryMetadataForConn, isAnySsoConnection } from 'aws-core-vscode/auth' import api from './api' import { activate as activateCWChat } from './app/chat/activation' -import { activate as activateInlineChat } from './inlineChat/activation' import { beta } from 'aws-core-vscode/dev' import { activate as activateNotifications, NotificationsController } from 'aws-core-vscode/notifications' import { AuthState, AuthUtil } from 'aws-core-vscode/codewhisperer' @@ -73,7 +72,6 @@ async function activateAmazonQNode(context: vscode.ExtensionContext) { } activateAgents() await activateTransformationHub(extContext as ExtContext) - activateInlineChat(context) const authProvider = new CommonAuthViewProvider( context, diff --git a/packages/amazonq/src/inlineChat/activation.ts b/packages/amazonq/src/inlineChat/activation.ts index a42dfdb3e02..9f196f31ba3 100644 --- a/packages/amazonq/src/inlineChat/activation.ts +++ b/packages/amazonq/src/inlineChat/activation.ts @@ -5,8 +5,15 @@ import * as vscode from 'vscode' import { InlineChatController } from './controller/inlineChatController' import { registerInlineCommands } from './command/registerInlineCommands' +import { LanguageClient } from 'vscode-languageclient' +import { InlineChatTutorialAnnotation } from '../app/inline/tutorials/inlineChatTutorialAnnotation' -export function activate(context: vscode.ExtensionContext) { - const inlineChatController = new InlineChatController(context) +export function activate( + context: vscode.ExtensionContext, + client: LanguageClient, + encryptionKey: Buffer, + inlineChatTutorialAnnotation: InlineChatTutorialAnnotation +) { + const inlineChatController = new InlineChatController(context, client, encryptionKey, inlineChatTutorialAnnotation) registerInlineCommands(context, inlineChatController) } diff --git a/packages/amazonq/src/inlineChat/controller/inlineChatController.ts b/packages/amazonq/src/inlineChat/controller/inlineChatController.ts index 7ace8d0095e..7151a8f9723 100644 --- a/packages/amazonq/src/inlineChat/controller/inlineChatController.ts +++ b/packages/amazonq/src/inlineChat/controller/inlineChatController.ts @@ -14,6 +14,7 @@ import { CodelensProvider } from '../codeLenses/codeLenseProvider' import { PromptMessage, ReferenceLogController } from 'aws-core-vscode/codewhispererChat' import { CodeWhispererSettings } from 'aws-core-vscode/codewhisperer' import { UserWrittenCodeTracker } from 'aws-core-vscode/codewhisperer' +import { LanguageClient } from 'vscode-languageclient' import { codicon, getIcon, @@ -23,8 +24,9 @@ import { Timeout, textDocumentUtil, isSageMaker, + Experiments, } from 'aws-core-vscode/shared' -import { InlineLineAnnotationController } from '../decorations/inlineLineAnnotationController' +import { InlineChatTutorialAnnotation } from '../../app/inline/tutorials/inlineChatTutorialAnnotation' export class InlineChatController { private task: InlineTask | undefined @@ -32,15 +34,24 @@ export class InlineChatController { private readonly inlineChatProvider: InlineChatProvider private readonly codeLenseProvider: CodelensProvider private readonly referenceLogController = new ReferenceLogController() - private readonly inlineLineAnnotationController: InlineLineAnnotationController + private readonly inlineChatTutorialAnnotation: InlineChatTutorialAnnotation + private readonly computeDiffAndRenderOnEditor: (query: string) => Promise private userQuery: string | undefined private listeners: vscode.Disposable[] = [] - constructor(context: vscode.ExtensionContext) { - this.inlineChatProvider = new InlineChatProvider() + constructor( + context: vscode.ExtensionContext, + client: LanguageClient, + encryptionKey: Buffer, + inlineChatTutorialAnnotation: InlineChatTutorialAnnotation + ) { + this.inlineChatProvider = new InlineChatProvider(client, encryptionKey) this.inlineChatProvider.onErrorOccured(() => this.handleError()) this.codeLenseProvider = new CodelensProvider(context) - this.inlineLineAnnotationController = new InlineLineAnnotationController(context) + this.inlineChatTutorialAnnotation = inlineChatTutorialAnnotation + this.computeDiffAndRenderOnEditor = Experiments.instance.get('amazonqLSPInlineChat', false) + ? this.computeDiffAndRenderOnEditorLSP.bind(this) + : this.computeDiffAndRenderOnEditorLocal.bind(this) } public async createTask( @@ -138,7 +149,7 @@ export class InlineChatController { this.codeLenseProvider.updateLenses(task) if (task.state === TaskState.InProgress) { if (vscode.window.activeTextEditor) { - await this.inlineLineAnnotationController.hide(vscode.window.activeTextEditor) + await this.inlineChatTutorialAnnotation.hide(vscode.window.activeTextEditor) } } await this.refreshCodeLenses(task) @@ -164,7 +175,7 @@ export class InlineChatController { this.listeners = [] this.task = undefined - this.inlineLineAnnotationController.enable() + this.inlineChatTutorialAnnotation.enable() await setContext('amazonq.inline.codelensShortcutEnabled', undefined) } @@ -205,8 +216,8 @@ export class InlineChatController { this.userQuery = query await textDocumentUtil.addEofNewline(editor) this.task = await this.createTask(query, editor.document, editor.selection) - await this.inlineLineAnnotationController.disable(editor) - await this.computeDiffAndRenderOnEditor(query, editor.document).catch(async (err) => { + await this.inlineChatTutorialAnnotation.disable(editor) + await this.computeDiffAndRenderOnEditor(query).catch(async (err) => { getLogger().error('computeDiffAndRenderOnEditor error: %s', (err as Error)?.message) if (err instanceof Error) { void vscode.window.showErrorMessage(`Amazon Q: ${err.message}`) @@ -218,7 +229,46 @@ export class InlineChatController { }) } - private async computeDiffAndRenderOnEditor(query: string, document: vscode.TextDocument) { + private async computeDiffAndRenderOnEditorLSP(query: string) { + if (!this.task) { + return + } + + await this.updateTaskAndLenses(this.task, TaskState.InProgress) + getLogger().info(`inline chat query:\n${query}`) + const uuid = randomUUID() + const message: PromptMessage = { + message: query, + messageId: uuid, + command: undefined, + userIntent: undefined, + tabID: uuid, + } + + const response = await this.inlineChatProvider.processPromptMessageLSP(message) + + // TODO: add tests for this case. + if (!response.body) { + getLogger().warn('Empty body in inline chat response') + await this.handleError() + return + } + + // Update inline diff view + const textDiff = computeDiff(response.body, this.task, false) + const decorations = computeDecorations(this.task) + this.task.decorations = decorations + await this.applyDiff(this.task, textDiff ?? []) + this.decorator.applyDecorations(this.task) + + // Update Codelenses + await this.updateTaskAndLenses(this.task, TaskState.WaitingForDecision) + await setContext('amazonq.inline.codelensShortcutEnabled', true) + this.undoListener(this.task) + } + + // TODO: remove this implementation in favor of LSP + private async computeDiffAndRenderOnEditorLocal(query: string) { if (!this.task) { return } diff --git a/packages/amazonq/src/inlineChat/provider/inlineChatProvider.ts b/packages/amazonq/src/inlineChat/provider/inlineChatProvider.ts index e6534d65532..cfa3798945c 100644 --- a/packages/amazonq/src/inlineChat/provider/inlineChatProvider.ts +++ b/packages/amazonq/src/inlineChat/provider/inlineChatProvider.ts @@ -8,6 +8,8 @@ import { CodeWhispererStreamingServiceException, GenerateAssistantResponseCommandOutput, } from '@amzn/codewhisperer-streaming' +import { LanguageClient } from 'vscode-languageclient' +import { inlineChatRequestType } from '@aws/language-server-runtimes/protocol' import { AuthUtil, getSelectedCustomization } from 'aws-core-vscode/codewhisperer' import { ChatSessionStorage, @@ -25,6 +27,9 @@ import { codeWhispererClient } from 'aws-core-vscode/codewhisperer' import type { InlineChatEvent } from 'aws-core-vscode/codewhisperer' import { InlineTask } from '../controller/inlineTask' import { extractAuthFollowUp } from 'aws-core-vscode/amazonq' +import { InlineChatParams, InlineChatResult } from '@aws/language-server-runtimes-types' +import { decryptResponse, encryptRequest } from '../../lsp/encryption' +import { getCursorState } from '../../lsp/utils' export class InlineChatProvider { private readonly editorContextExtractor: EditorContextExtractor @@ -34,13 +39,49 @@ export class InlineChatProvider { private errorEmitter = new vscode.EventEmitter() public onErrorOccured = this.errorEmitter.event - public constructor() { + public constructor( + private readonly client: LanguageClient, + private readonly encryptionKey: Buffer + ) { this.editorContextExtractor = new EditorContextExtractor() this.userIntentRecognizer = new UserIntentRecognizer() this.sessionStorage = new ChatSessionStorage() this.triggerEventsStorage = new TriggerEventsStorage() } + private getCurrentEditorParams(prompt: string): InlineChatParams { + const editor = vscode.window.activeTextEditor + if (!editor) { + throw new ToolkitError('No active editor') + } + + const documentUri = editor.document.uri.toString() + const cursorState = getCursorState(editor.selections) + return { + prompt: { + prompt, + }, + cursorState, + textDocument: { + uri: documentUri, + }, + } + } + + public async processPromptMessageLSP(message: PromptMessage): Promise { + // TODO: handle partial responses. + getLogger().info('Making inline chat request with message %O', message) + const params = this.getCurrentEditorParams(message.message ?? '') + + const inlineChatRequest = await encryptRequest(params, this.encryptionKey) + const response = await this.client.sendRequest(inlineChatRequestType.method, inlineChatRequest) + const inlineChatResponse = await decryptResponse(response, this.encryptionKey) + this.client.info(`Logging response for inline chat ${JSON.stringify(inlineChatResponse)}`) + + return inlineChatResponse + } + + // TODO: remove in favor of LSP implementation. public async processPromptMessage(message: PromptMessage) { return this.editorContextExtractor .extractContextForTrigger('ChatMessage') diff --git a/packages/amazonq/src/lsp/chat/messages.ts b/packages/amazonq/src/lsp/chat/messages.ts index 0178050b4a4..4daa56e681f 100644 --- a/packages/amazonq/src/lsp/chat/messages.ts +++ b/packages/amazonq/src/lsp/chat/messages.ts @@ -63,7 +63,6 @@ import { import { v4 as uuidv4 } from 'uuid' import * as vscode from 'vscode' import { Disposable, LanguageClient, Position, TextDocumentIdentifier } from 'vscode-languageclient' -import * as jose from 'jose' import { AmazonQChatViewProvider } from './webviewProvider' import { AuthUtil, ReferenceLogViewProvider } from 'aws-core-vscode/codewhisperer' import { amazonQDiffScheme, AmazonQPromptSettings, messages, openUrl, isTextEditor } from 'aws-core-vscode/shared' @@ -76,6 +75,8 @@ import { } from 'aws-core-vscode/amazonq' import { telemetry, TelemetryBase } from 'aws-core-vscode/telemetry' import { isValidResponseError } from './error' +import { decryptResponse, encryptRequest } from '../encryption' +import { getCursorState } from '../utils' import { focusAmazonQPanel } from './commands' export function registerActiveEditorChangeListener(languageClient: LanguageClient) { @@ -132,21 +133,6 @@ export function registerLanguageServerEventListener(languageClient: LanguageClie }) } -function getCursorState(selection: readonly vscode.Selection[]) { - return selection.map((s) => ({ - range: { - start: { - line: s.start.line, - character: s.start.character, - }, - end: { - line: s.end.line, - character: s.end.character, - }, - }, - })) -} - export function registerMessageListeners( languageClient: LanguageClient, provider: AmazonQChatViewProvider, @@ -252,21 +238,12 @@ export function registerMessageListeners( const cancellationToken = new CancellationTokenSource() chatStreamTokens.set(chatParams.tabId, cancellationToken) - const chatDisposable = languageClient.onProgress( - chatRequestType, - partialResultToken, - (partialResult) => { - // Store the latest partial result - if (typeof partialResult === 'string' && encryptionKey) { - void decodeRequest(partialResult, encryptionKey).then( - (decoded) => (lastPartialResult = decoded) - ) - } else { - lastPartialResult = partialResult as ChatResult + const chatDisposable = languageClient.onProgress(chatRequestType, partialResultToken, (partialResult) => + handlePartialResult(partialResult, encryptionKey, provider, chatParams.tabId).then( + (result) => { + lastPartialResult = result } - - void handlePartialResult(partialResult, encryptionKey, provider, chatParams.tabId) - } + ) ) const editor = @@ -562,29 +539,6 @@ function isServerEvent(command: string) { return command.startsWith('aws/chat/') || command === 'telemetry/event' } -async function encryptRequest(params: T, encryptionKey: Buffer): Promise<{ message: string } | T> { - const payload = new TextEncoder().encode(JSON.stringify(params)) - - const encryptedMessage = await new jose.CompactEncrypt(payload) - .setProtectedHeader({ alg: 'dir', enc: 'A256GCM' }) - .encrypt(encryptionKey) - - return { message: encryptedMessage } -} - -async function decodeRequest(request: string, key: Buffer): Promise { - const result = await jose.jwtDecrypt(request, key, { - clockTolerance: 60, // Allow up to 60 seconds to account for clock differences - contentEncryptionAlgorithms: ['A256GCM'], - keyManagementAlgorithms: ['dir'], - }) - - if (!result.payload) { - throw new Error('JWT payload not found') - } - return result.payload as T -} - /** * Decodes partial chat responses from the language server before sending them to mynah UI */ @@ -594,10 +548,7 @@ async function handlePartialResult( provider: AmazonQChatViewProvider, tabId: string ) { - const decryptedMessage = - typeof partialResult === 'string' && encryptionKey - ? await decodeRequest(partialResult, encryptionKey) - : (partialResult as T) + const decryptedMessage = await decryptResponse(partialResult, encryptionKey) if (decryptedMessage.body !== undefined) { void provider.webview?.postMessage({ @@ -607,6 +558,7 @@ async function handlePartialResult( tabId: tabId, }) } + return decryptedMessage } /** @@ -620,8 +572,8 @@ async function handleCompleteResult( tabId: string, disposable: Disposable ) { - const decryptedMessage = - typeof result === 'string' && encryptionKey ? await decodeRequest(result, encryptionKey) : (result as T) + const decryptedMessage = await decryptResponse(result, encryptionKey) + void provider.webview?.postMessage({ command: chatRequestType.method, params: decryptedMessage, diff --git a/packages/amazonq/src/lsp/client.ts b/packages/amazonq/src/lsp/client.ts index 55198852d96..c359ac73ded 100644 --- a/packages/amazonq/src/lsp/client.ts +++ b/packages/amazonq/src/lsp/client.ts @@ -18,7 +18,12 @@ import { ResponseMessage, WorkspaceFolder, } from '@aws/language-server-runtimes/protocol' -import { AuthUtil, CodeWhispererSettings, getSelectedCustomization } from 'aws-core-vscode/codewhisperer' +import { + AuthUtil, + CodeWhispererSettings, + getSelectedCustomization, + TelemetryHelper, +} from 'aws-core-vscode/codewhisperer' import { Settings, createServerOptions, @@ -38,7 +43,12 @@ import { processUtils } from 'aws-core-vscode/shared' import { activate } from './chat/activation' import { AmazonQResourcePaths } from './lspInstaller' import { ConfigSection, isValidConfigSection, pushConfigUpdate, toAmazonQLSPLogLevel } from './config' +import { activate as activateInlineChat } from '../inlineChat/activation' import { telemetry } from 'aws-core-vscode/telemetry' +import { SessionManager } from '../app/inline/sessionManager' +import { LineTracker } from '../app/inline/stateTracker/lineTracker' +import { InlineTutorialAnnotation } from '../app/inline/tutorials/inlineTutorialAnnotation' +import { InlineChatTutorialAnnotation } from '../app/inline/tutorials/inlineChatTutorialAnnotation' const localize = nls.loadMessageBundle() const logger = getLogger('amazonqLsp.lspClient') @@ -165,7 +175,7 @@ export async function startLanguageServer( const auth = await initializeAuth(client) - await onLanguageServerReady(auth, client, resourcePaths, toDispose) + await onLanguageServerReady(extensionContext, auth, client, resourcePaths, toDispose) return client } @@ -177,24 +187,26 @@ async function initializeAuth(client: LanguageClient): Promise { } async function onLanguageServerReady( + extensionContext: vscode.ExtensionContext, auth: AmazonQLspAuth, client: LanguageClient, resourcePaths: AmazonQResourcePaths, toDispose: vscode.Disposable[] ) { - if (Experiments.instance.get('amazonqLSPInline', false)) { - const inlineManager = new InlineCompletionManager(client) - inlineManager.registerInlineCompletion() - toDispose.push( - inlineManager, - Commands.register({ id: 'aws.amazonq.invokeInlineCompletion', autoconnect: true }, async () => { - await vscode.commands.executeCommand('editor.action.inlineSuggest.trigger') - }), - vscode.workspace.onDidCloseTextDocument(async () => { - await vscode.commands.executeCommand('aws.amazonq.rejectCodeSuggestion') - }) - ) - } + const sessionManager = new SessionManager() + + // keeps track of the line changes + const lineTracker = new LineTracker() + + // tutorial for inline suggestions + const inlineTutorialAnnotation = new InlineTutorialAnnotation(lineTracker, sessionManager) + + // tutorial for inline chat + const inlineChatTutorialAnnotation = new InlineChatTutorialAnnotation(inlineTutorialAnnotation) + + const inlineManager = new InlineCompletionManager(client, sessionManager, lineTracker, inlineTutorialAnnotation) + inlineManager.registerInlineCompletion() + activateInlineChat(extensionContext, client, encryptionKey, inlineChatTutorialAnnotation) if (Experiments.instance.get('amazonqChatLSP', true)) { await activate(client, encryptionKey, resourcePaths.ui) @@ -215,6 +227,38 @@ async function onLanguageServerReady( } toDispose.push( + inlineManager, + Commands.register({ id: 'aws.amazonq.invokeInlineCompletion', autoconnect: true }, async () => { + await vscode.commands.executeCommand('editor.action.inlineSuggest.trigger') + }), + Commands.register('aws.amazonq.refreshAnnotation', async (forceProceed: boolean) => { + telemetry.record({ + traceId: TelemetryHelper.instance.traceId, + }) + + const editor = vscode.window.activeTextEditor + if (editor) { + if (forceProceed) { + await inlineTutorialAnnotation.refresh(editor, 'codewhisperer', true) + } else { + await inlineTutorialAnnotation.refresh(editor, 'codewhisperer') + } + } + }), + Commands.register('aws.amazonq.dismissTutorial', async () => { + const editor = vscode.window.activeTextEditor + if (editor) { + inlineTutorialAnnotation.clear() + try { + telemetry.ui_click.emit({ elementId: `dismiss_${inlineTutorialAnnotation.currentState.id}` }) + } catch (_) {} + await inlineTutorialAnnotation.dismissTutorial() + getLogger().debug(`codewhisperer: user dismiss tutorial.`) + } + }), + vscode.workspace.onDidCloseTextDocument(async () => { + await vscode.commands.executeCommand('aws.amazonq.rejectCodeSuggestion') + }), AuthUtil.instance.auth.onDidChangeActiveConnection(async () => { await auth.refreshConnection() }), diff --git a/packages/amazonq/src/lsp/config.ts b/packages/amazonq/src/lsp/config.ts index 1760fb51401..66edc9ff6f1 100644 --- a/packages/amazonq/src/lsp/config.ts +++ b/packages/amazonq/src/lsp/config.ts @@ -3,15 +3,14 @@ * SPDX-License-Identifier: Apache-2.0 */ import * as vscode from 'vscode' -import { DevSettings, getServiceEnvVarConfig } from 'aws-core-vscode/shared' -import { LspConfig } from 'aws-core-vscode/amazonq' +import { DevSettings, getServiceEnvVarConfig, BaseLspInstaller } from 'aws-core-vscode/shared' import { LanguageClient } from 'vscode-languageclient' import { DidChangeConfigurationNotification, updateConfigurationRequestType, } from '@aws/language-server-runtimes/protocol' -export interface ExtendedAmazonQLSPConfig extends LspConfig { +export interface ExtendedAmazonQLSPConfig extends BaseLspInstaller.LspConfig { ui?: string } diff --git a/packages/amazonq/src/lsp/encryption.ts b/packages/amazonq/src/lsp/encryption.ts new file mode 100644 index 00000000000..246c64f476b --- /dev/null +++ b/packages/amazonq/src/lsp/encryption.ts @@ -0,0 +1,34 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +import * as jose from 'jose' + +export async function encryptRequest(params: T, encryptionKey: Buffer): Promise<{ message: string } | T> { + const payload = new TextEncoder().encode(JSON.stringify(params)) + + const encryptedMessage = await new jose.CompactEncrypt(payload) + .setProtectedHeader({ alg: 'dir', enc: 'A256GCM' }) + .encrypt(encryptionKey) + + return { message: encryptedMessage } +} + +export async function decryptResponse(response: unknown, key: Buffer | undefined) { + // Note that casts are required since language client requests return 'unknown' type. + // If we can't decrypt, return original response casted. + if (typeof response !== 'string' || key === undefined) { + return response as T + } + + const result = await jose.jwtDecrypt(response, key, { + clockTolerance: 60, // Allow up to 60 seconds to account for clock differences + contentEncryptionAlgorithms: ['A256GCM'], + keyManagementAlgorithms: ['dir'], + }) + + if (!result.payload) { + throw new Error('JWT payload not found') + } + return result.payload as T +} diff --git a/packages/amazonq/src/lsp/utils.ts b/packages/amazonq/src/lsp/utils.ts new file mode 100644 index 00000000000..f5b010c536b --- /dev/null +++ b/packages/amazonq/src/lsp/utils.ts @@ -0,0 +1,26 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +import * as vscode from 'vscode' +import { CursorState } from '@aws/language-server-runtimes-types' + +/** + * Convert from vscode selection type to the general CursorState expected by the AmazonQLSP. + * @param selection + * @returns + */ +export function getCursorState(selection: readonly vscode.Selection[]): CursorState[] { + return selection.map((s) => ({ + range: { + start: { + line: s.start.line, + character: s.start.character, + }, + end: { + line: s.end.line, + character: s.end.character, + }, + }, + })) +} diff --git a/packages/amazonq/test/e2e/inline/inline.test.ts b/packages/amazonq/test/e2e/inline/inline.test.ts index 43a9f67ab73..bcc41851eca 100644 --- a/packages/amazonq/test/e2e/inline/inline.test.ts +++ b/packages/amazonq/test/e2e/inline/inline.test.ts @@ -5,18 +5,10 @@ import * as vscode from 'vscode' import assert from 'assert' -import { - closeAllEditors, - getTestWindow, - registerAuthHook, - resetCodeWhispererGlobalVariables, - TestFolder, - toTextEditor, - using, -} from 'aws-core-vscode/test' -import { RecommendationHandler, RecommendationService, session } from 'aws-core-vscode/codewhisperer' +import { closeAllEditors, registerAuthHook, TestFolder, toTextEditor, using } from 'aws-core-vscode/test' import { Commands, globals, sleep, waitUntil, collectionUtil } from 'aws-core-vscode/shared' import { loginToIdC } from '../amazonq/utils/setup' +import { vsCodeState } from 'aws-core-vscode/codewhisperer' describe('Amazon Q Inline', async function () { const retries = 3 @@ -40,7 +32,6 @@ describe('Amazon Q Inline', async function () { const folder = await TestFolder.create() tempFolder = folder.path await closeAllEditors() - await resetCodeWhispererGlobalVariables() }) afterEach(async function () { @@ -54,7 +45,6 @@ describe('Amazon Q Inline', async function () { const events = getUserTriggerDecision() console.table({ 'telemetry events': JSON.stringify(events), - 'recommendation service status': RecommendationService.instance.isRunning, }) } @@ -71,31 +61,6 @@ describe('Amazon Q Inline', async function () { }) } - async function waitForRecommendations() { - const suggestionShown = await waitUntil(async () => session.getSuggestionState(0) === 'Showed', waitOptions) - if (!suggestionShown) { - throw new Error(`Suggestion did not show. Suggestion States: ${JSON.stringify(session.suggestionStates)}`) - } - const suggestionVisible = await waitUntil( - async () => RecommendationHandler.instance.isSuggestionVisible(), - waitOptions - ) - if (!suggestionVisible) { - throw new Error( - `Suggestions failed to become visible. Suggestion States: ${JSON.stringify(session.suggestionStates)}` - ) - } - console.table({ - 'suggestions states': JSON.stringify(session.suggestionStates), - 'valid recommendation': RecommendationHandler.instance.isValidResponse(), - 'recommendation service status': RecommendationService.instance.isRunning, - recommendations: session.recommendations, - }) - if (!RecommendationHandler.instance.isValidResponse()) { - throw new Error('Did not find a valid response') - } - } - /** * Waits for a specific telemetry event to be emitted with the expected suggestion state. * It looks like there might be a potential race condition in codewhisperer causing telemetry @@ -149,8 +114,9 @@ describe('Amazon Q Inline', async function () { await invokeCompletion() originalEditorContents = vscode.window.activeTextEditor?.document.getText() - // wait until the ghost text appears - await waitForRecommendations() + // wait until all the recommendations have finished + await waitUntil(() => Promise.resolve(vsCodeState.isRecommendationsActive === true), waitOptions) + await waitUntil(() => Promise.resolve(vsCodeState.isRecommendationsActive === false), waitOptions) } beforeEach(async () => { @@ -163,14 +129,12 @@ describe('Amazon Q Inline', async function () { try { await setup() console.log(`test run ${attempt} succeeded`) - logUserDecisionStatus() break } catch (e) { console.log(`test run ${attempt} failed`) console.log(e) logUserDecisionStatus() attempt++ - await resetCodeWhispererGlobalVariables() } } if (attempt === retries) { @@ -216,29 +180,6 @@ describe('Amazon Q Inline', async function () { assert.deepStrictEqual(vscode.window.activeTextEditor?.document.getText(), originalEditorContents) }) }) - - it(`${name} invoke on unsupported filetype`, async function () { - await setupEditor({ - name: 'test.zig', - contents: `fn doSomething() void { - - }`, - }) - - /** - * Add delay between editor loading and invoking completion - * @see beforeEach in supported filetypes for more information - */ - await sleep(1000) - await invokeCompletion() - - if (name === 'automatic') { - // It should never get triggered since its not a supported file type - assert.deepStrictEqual(RecommendationService.instance.isRunning, false) - } else { - await getTestWindow().waitForMessage('currently not supported by Amazon Q inline suggestions') - } - }) }) } }) diff --git a/packages/amazonq/test/e2e/lsp/amazonqLsp.test.ts b/packages/amazonq/test/e2e/lsp/amazonqLsp.test.ts index d3e90ec4e8e..f4a60ff282b 100644 --- a/packages/amazonq/test/e2e/lsp/amazonqLsp.test.ts +++ b/packages/amazonq/test/e2e/lsp/amazonqLsp.test.ts @@ -6,13 +6,13 @@ import { AmazonQLspInstaller } from '../../../src/lsp/lspInstaller' import { defaultAmazonQLspConfig } from '../../../src/lsp/config' import { createLspInstallerTests } from './lspInstallerUtil' -import { LspConfig } from 'aws-core-vscode/amazonq' +import { BaseLspInstaller } from 'aws-core-vscode/shared' describe('AmazonQLSP', () => { createLspInstallerTests({ suiteName: 'AmazonQLSPInstaller', lspConfig: defaultAmazonQLspConfig, - createInstaller: (lspConfig?: LspConfig) => new AmazonQLspInstaller(lspConfig), + createInstaller: (lspConfig?: BaseLspInstaller.LspConfig) => new AmazonQLspInstaller(lspConfig), targetContents: [ { bytes: 0, diff --git a/packages/amazonq/test/e2e/lsp/lspInstallerUtil.ts b/packages/amazonq/test/e2e/lsp/lspInstallerUtil.ts index c7ca7a4ff9b..d4251959756 100644 --- a/packages/amazonq/test/e2e/lsp/lspInstallerUtil.ts +++ b/packages/amazonq/test/e2e/lsp/lspInstallerUtil.ts @@ -18,7 +18,6 @@ import { } from 'aws-core-vscode/shared' import * as semver from 'semver' import { assertTelemetry } from 'aws-core-vscode/test' -import { LspConfig, LspController } from 'aws-core-vscode/amazonq' import { LanguageServerSetup } from 'aws-core-vscode/telemetry' function createVersion(version: string, contents: TargetContent[]) { @@ -44,8 +43,8 @@ export function createLspInstallerTests({ resetEnv, }: { suiteName: string - lspConfig: LspConfig - createInstaller: (lspConfig?: LspConfig) => BaseLspInstaller.BaseLspInstaller + lspConfig: BaseLspInstaller.LspConfig + createInstaller: (lspConfig?: BaseLspInstaller.LspConfig) => BaseLspInstaller.BaseLspInstaller targetContents: TargetContent[] setEnv: (path: string) => void resetEnv: () => void @@ -60,8 +59,6 @@ export function createLspInstallerTests({ installer = createInstaller() tempDir = await makeTemporaryToolkitFolder() sandbox.stub(LanguageServerResolver.prototype, 'defaultDownloadFolder').returns(tempDir) - // Called on extension activation and can contaminate telemetry. - sandbox.stub(LspController.prototype, 'trySetupLsp') }) afterEach(async () => { diff --git a/packages/amazonq/test/e2e/lsp/workspaceContextLsp.test.ts b/packages/amazonq/test/e2e/lsp/workspaceContextLsp.test.ts deleted file mode 100644 index 75d57949c0b..00000000000 --- a/packages/amazonq/test/e2e/lsp/workspaceContextLsp.test.ts +++ /dev/null @@ -1,42 +0,0 @@ -/*! - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -import * as os from 'os' -import { createLspInstallerTests } from './lspInstallerUtil' -import { defaultAmazonQWorkspaceLspConfig, LspClient, LspConfig, WorkspaceLspInstaller } from 'aws-core-vscode/amazonq' -import assert from 'assert' - -describe('AmazonQWorkspaceLSP', () => { - createLspInstallerTests({ - suiteName: 'AmazonQWorkspaceLSPInstaller', - lspConfig: defaultAmazonQWorkspaceLspConfig, - createInstaller: (lspConfig?: LspConfig) => new WorkspaceLspInstaller.WorkspaceLspInstaller(lspConfig), - targetContents: [ - { - bytes: 0, - filename: `qserver-${os.platform()}-${os.arch()}.zip`, - hashes: [], - url: 'http://fakeurl', - }, - ], - setEnv: (path: string) => { - process.env.__AMAZONQWORKSPACELSP_PATH = path - }, - resetEnv: () => { - delete process.env.__AMAZONQWORKSPACELSP_PATH - }, - }) - - it('activates', async () => { - const ok = await LspClient.instance.waitUntilReady() - if (!ok) { - assert.fail('Workspace context language server failed to become ready') - } - const serverUsage = await LspClient.instance.getLspServerUsage() - if (!serverUsage) { - assert.fail('Unable to verify that the workspace context language server has been activated') - } - }) -}) diff --git a/packages/amazonq/test/unit/amazonq/apps/inline/completion.test.ts b/packages/amazonq/test/unit/amazonq/apps/inline/completion.test.ts index d2182329e45..fbc28feefbb 100644 --- a/packages/amazonq/test/unit/amazonq/apps/inline/completion.test.ts +++ b/packages/amazonq/test/unit/amazonq/apps/inline/completion.test.ts @@ -3,18 +3,32 @@ * SPDX-License-Identifier: Apache-2.0 */ import sinon from 'sinon' -import { CancellationToken, commands, languages, Position } from 'vscode' +import { + CancellationToken, + commands, + InlineCompletionItem, + languages, + Position, + window, + Range, + InlineCompletionTriggerKind, +} from 'vscode' import assert from 'assert' import { LanguageClient } from 'vscode-languageclient' +import { StringValue } from 'vscode-languageserver-types' import { AmazonQInlineCompletionItemProvider, InlineCompletionManager } from '../../../../../src/app/inline/completion' import { RecommendationService } from '../../../../../src/app/inline/recommendationService' import { SessionManager } from '../../../../../src/app/inline/sessionManager' -import { createMockDocument, createMockTextEditor } from 'aws-core-vscode/test' +import { createMockDocument, createMockTextEditor, getTestWindow, installFakeClock } from 'aws-core-vscode/test' import { + noInlineSuggestionsMsg, ReferenceHoverProvider, - ReferenceInlineProvider, ReferenceLogViewProvider, + vsCodeState, } from 'aws-core-vscode/codewhisperer' +import { InlineGeneratingMessage } from '../../../../../src/app/inline/inlineGeneratingMessage' +import { LineTracker } from '../../../../../src/app/inline/stateTracker/lineTracker' +import { InlineTutorialAnnotation } from '../../../../../src/app/inline/tutorials/inlineTutorialAnnotation' describe('InlineCompletionManager', () => { let manager: InlineCompletionManager @@ -32,7 +46,7 @@ describe('InlineCompletionManager', () => { let hoverReferenceStub: sinon.SinonStub const mockDocument = createMockDocument() const mockEditor = createMockTextEditor() - const mockPosition = { line: 0, character: 0 } as Position + const mockPosition = new Position(0, 0) const mockContext = { triggerKind: 1, selectedCompletionInfo: undefined } const mockToken = { isCancellationRequested: false } as CancellationToken const fakeReferences = [ @@ -52,6 +66,11 @@ describe('InlineCompletionManager', () => { insertText: 'test', references: fakeReferences, }, + { + itemId: 'test-item2', + insertText: 'import math\ndef two_sum(nums, target):\n', + references: fakeReferences, + }, ] beforeEach(() => { @@ -72,7 +91,10 @@ describe('InlineCompletionManager', () => { sendNotification: sendNotificationStub, } as unknown as LanguageClient - manager = new InlineCompletionManager(languageClient) + const sessionManager = new SessionManager() + const lineTracker = new LineTracker() + const inlineTutorialAnnotation = new InlineTutorialAnnotation(lineTracker, sessionManager) + manager = new InlineCompletionManager(languageClient, sessionManager, lineTracker, inlineTutorialAnnotation) getActiveSessionStub = sandbox.stub(manager['sessionManager'], 'getActiveSession') getActiveRecommendationStub = sandbox.stub(manager['sessionManager'], 'getActiveRecommendation') getReferenceStub = sandbox.stub(ReferenceLogViewProvider, 'getReferenceLog') @@ -213,46 +235,6 @@ describe('InlineCompletionManager', () => { assert(registerProviderStub.calledTwice) // Once in constructor, once after rejection }) }) - - describe('previous command', () => { - it('should register and handle previous command correctly', async () => { - const prevCommandCall = registerCommandStub - .getCalls() - .find((call) => call.args[0] === 'editor.action.inlineSuggest.showPrevious') - - assert(prevCommandCall, 'Previous command should be registered') - - if (prevCommandCall) { - const handler = prevCommandCall.args[1] - await handler() - - assert(executeCommandStub.calledWith('editor.action.inlineSuggest.hide')) - assert(disposableStub.calledOnce) - assert(registerProviderStub.calledTwice) - assert(executeCommandStub.calledWith('editor.action.inlineSuggest.trigger')) - } - }) - }) - - describe('next command', () => { - it('should register and handle next command correctly', async () => { - const nextCommandCall = registerCommandStub - .getCalls() - .find((call) => call.args[0] === 'editor.action.inlineSuggest.showNext') - - assert(nextCommandCall, 'Next command should be registered') - - if (nextCommandCall) { - const handler = nextCommandCall.args[1] - await handler() - - assert(executeCommandStub.calledWith('editor.action.inlineSuggest.hide')) - assert(disposableStub.calledOnce) - assert(registerProviderStub.calledTwice) - assert(executeCommandStub.calledWith('editor.action.inlineSuggest.trigger')) - } - }) - }) }) describe('AmazonQInlineCompletionItemProvider', () => { @@ -261,15 +243,18 @@ describe('InlineCompletionManager', () => { let provider: AmazonQInlineCompletionItemProvider let getAllRecommendationsStub: sinon.SinonStub let recommendationService: RecommendationService - let setInlineReferenceStub: sinon.SinonStub + let inlineTutorialAnnotation: InlineTutorialAnnotation beforeEach(() => { - recommendationService = new RecommendationService(mockSessionManager) - setInlineReferenceStub = sandbox.stub(ReferenceInlineProvider.instance, 'setInlineReference') - + const lineTracker = new LineTracker() + const activeStateController = new InlineGeneratingMessage(lineTracker) + inlineTutorialAnnotation = new InlineTutorialAnnotation(lineTracker, mockSessionManager) + recommendationService = new RecommendationService(mockSessionManager, activeStateController) + vsCodeState.isRecommendationsActive = false mockSessionManager = { getActiveSession: getActiveSessionStub, getActiveRecommendation: getActiveRecommendationStub, + clear: () => {}, } as unknown as SessionManager getActiveSessionStub.returns({ @@ -281,12 +266,14 @@ describe('InlineCompletionManager', () => { getActiveRecommendationStub.returns(mockSuggestions) getAllRecommendationsStub = sandbox.stub(recommendationService, 'getAllRecommendations') getAllRecommendationsStub.resolves() + sandbox.stub(window, 'activeTextEditor').value(createMockTextEditor()) }), - it('should call recommendation service to get new suggestions for new sessions', async () => { + it('should call recommendation service to get new suggestions(matching typeahead) for new sessions', async () => { provider = new AmazonQInlineCompletionItemProvider( languageClient, recommendationService, - mockSessionManager + mockSessionManager, + inlineTutorialAnnotation ) const items = await provider.provideInlineCompletionItems( mockDocument, @@ -295,41 +282,135 @@ describe('InlineCompletionManager', () => { mockToken ) assert(getAllRecommendationsStub.calledOnce) - assert.deepStrictEqual(items, mockSuggestions) + assert.deepStrictEqual(items, [mockSuggestions[1]]) }), - it('should not call recommendation service for existing sessions', async () => { + it('should handle reference if there is any', async () => { provider = new AmazonQInlineCompletionItemProvider( languageClient, recommendationService, mockSessionManager, - false + inlineTutorialAnnotation ) - const items = await provider.provideInlineCompletionItems( + await provider.provideInlineCompletionItems(mockDocument, mockPosition, mockContext, mockToken) + }), + it('should add a range to the completion item when missing', async function () { + provider = new AmazonQInlineCompletionItemProvider( + languageClient, + recommendationService, + mockSessionManager, + inlineTutorialAnnotation + ) + getActiveRecommendationStub.returns([ + { + insertText: 'testText', + itemId: 'itemId', + }, + { + insertText: 'testText2', + itemId: 'itemId2', + range: undefined, + }, + ]) + const cursorPosition = new Position(5, 6) + const result = await provider.provideInlineCompletionItems( + mockDocument, + cursorPosition, + mockContext, + mockToken + ) + + for (const item of result) { + assert.deepStrictEqual(item.range, new Range(cursorPosition, cursorPosition)) + } + }), + it('should handle StringValue instead of strings', async function () { + provider = new AmazonQInlineCompletionItemProvider( + languageClient, + recommendationService, + mockSessionManager, + inlineTutorialAnnotation + ) + const expectedText = `${mockSuggestions[1].insertText}this is my text` + getActiveRecommendationStub.returns([ + { + insertText: { + kind: 'snippet', + value: `${mockSuggestions[1].insertText}this is my text`, + } satisfies StringValue, + itemId: 'itemId', + }, + ]) + const result = await provider.provideInlineCompletionItems( mockDocument, mockPosition, mockContext, mockToken ) - assert(getAllRecommendationsStub.notCalled) - assert.deepStrictEqual(items, mockSuggestions) + + assert.strictEqual(result[0].insertText, expectedText) }), - it('should handle reference if there is any', async () => { + it('shows message to user when manual invoke fails to produce results', async function () { provider = new AmazonQInlineCompletionItemProvider( languageClient, recommendationService, mockSessionManager, - false + inlineTutorialAnnotation ) - await provider.provideInlineCompletionItems(mockDocument, mockPosition, mockContext, mockToken) - assert(setInlineReferenceStub.calledOnce) - assert( - setInlineReferenceStub.calledWithExactly( - mockPosition.line, - mockSuggestions[0].insertText, - fakeReferences - ) + getActiveRecommendationStub.returns([]) + const messageShown = new Promise((resolve) => + getTestWindow().onDidShowMessage((e) => { + assert.strictEqual(e.message, noInlineSuggestionsMsg) + resolve(true) + }) + ) + await provider.provideInlineCompletionItems( + mockDocument, + mockPosition, + { triggerKind: InlineCompletionTriggerKind.Invoke, selectedCompletionInfo: undefined }, + mockToken + ) + await messageShown + }) + describe('debounce behavior', function () { + let clock: ReturnType + + beforeEach(function () { + clock = installFakeClock() + }) + + after(function () { + clock.uninstall() + }) + + it('should only trigger once on rapid events', async () => { + provider = new AmazonQInlineCompletionItemProvider( + languageClient, + recommendationService, + mockSessionManager, + inlineTutorialAnnotation ) + const p1 = provider.provideInlineCompletionItems(mockDocument, mockPosition, mockContext, mockToken) + const p2 = provider.provideInlineCompletionItems(mockDocument, mockPosition, mockContext, mockToken) + const p3 = provider.provideInlineCompletionItems( + mockDocument, + new Position(1, 26), + mockContext, + mockToken + ) + + await clock.tickAsync(1000) + + // All promises should be the same object when debounced properly. + assert.strictEqual(p1, p2) + assert.strictEqual(p1, p3) + await p1 + await p2 + const r3 = await p3 + + // calls the function with the latest provided args. + assert.deepStrictEqual((r3 as InlineCompletionItem[])[0].range?.end, new Position(1, 26)) }) + }) }) }) }) diff --git a/packages/amazonq/test/unit/amazonq/apps/inline/inlineTracker.test.ts b/packages/amazonq/test/unit/amazonq/apps/inline/inlineTracker.test.ts new file mode 100644 index 00000000000..6b9490c72a5 --- /dev/null +++ b/packages/amazonq/test/unit/amazonq/apps/inline/inlineTracker.test.ts @@ -0,0 +1,299 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import { LineSelection, LineTracker, AuthUtil } from 'aws-core-vscode/codewhisperer' +import sinon from 'sinon' +import { Disposable, TextEditor, Position, Range, Selection } from 'vscode' +import { toTextEditor } from 'aws-core-vscode/test' +import assert from 'assert' +import { waitUntil } from 'aws-core-vscode/shared' + +describe('LineTracker class', function () { + let sut: LineTracker + let disposable: Disposable + let editor: TextEditor + let sandbox: sinon.SinonSandbox + let counts = { + editor: 0, + selection: 0, + content: 0, + } + + beforeEach(async function () { + sut = new LineTracker() + sandbox = sinon.createSandbox() + counts = { + editor: 0, + selection: 0, + content: 0, + } + disposable = sut.onDidChangeActiveLines((e) => { + if (e.reason === 'content') { + counts.content++ + } else if (e.reason === 'selection') { + counts.selection++ + } else if (e.reason === 'editor') { + counts.editor++ + } + }) + + sandbox.stub(AuthUtil.instance, 'isConnected').returns(true) + sandbox.stub(AuthUtil.instance, 'isConnectionExpired').returns(false) + }) + + afterEach(function () { + disposable.dispose() + sut.dispose() + sandbox.restore() + }) + + function assertEmptyCounts() { + assert.deepStrictEqual(counts, { + editor: 0, + selection: 0, + content: 0, + }) + } + + it('ready will emit onReady event', async function () { + let messageReceived = 0 + disposable = sut.onReady((_) => { + messageReceived++ + }) + + assert.strictEqual(sut.isReady, false) + sut.ready() + + await waitUntil( + async () => { + if (messageReceived !== 0) { + return + } + }, + { interval: 1000 } + ) + + assert.strictEqual(sut.isReady, true) + assert.strictEqual(messageReceived, 1) + }) + + describe('includes', function () { + // util function to help set up LineTracker.selections + async function setEditorSelection(selections: LineSelection[]): Promise { + const editor = await toTextEditor('\n\n\n\n\n\n\n\n\n\n', 'foo.py', undefined, { + preview: false, + }) + + const vscodeSelections = selections.map((s) => { + return new Selection(new Position(s.anchor, 0), new Position(s.active, 0)) + }) + + await sut.onTextEditorSelectionChanged({ + textEditor: editor, + selections: vscodeSelections, + kind: undefined, + }) + + assert.deepStrictEqual(sut.selections, selections) + return editor + } + + it('exact match when array of selections are provided', async function () { + const selections = [ + { + anchor: 1, + active: 1, + }, + { + anchor: 3, + active: 3, + }, + ] + + editor = await setEditorSelection(selections) + assert.deepStrictEqual(sut.selections, selections) + + let actual = sut.includes([ + { active: 1, anchor: 1 }, + { active: 3, anchor: 3 }, + ]) + assert.strictEqual(actual, true) + + actual = sut.includes([ + { active: 2, anchor: 2 }, + { active: 4, anchor: 4 }, + ]) + assert.strictEqual(actual, false) + + // both active && anchor have to be the same + actual = sut.includes([ + { active: 1, anchor: 0 }, + { active: 3, anchor: 0 }, + ]) + assert.strictEqual(actual, false) + + // different length would simply return false + actual = sut.includes([ + { active: 1, anchor: 1 }, + { active: 3, anchor: 3 }, + { active: 5, anchor: 5 }, + ]) + assert.strictEqual(actual, false) + }) + + it('match active line if line number and activeOnly option are provided', async function () { + const selections = [ + { + anchor: 1, + active: 1, + }, + { + anchor: 3, + active: 3, + }, + ] + + editor = await setEditorSelection(selections) + assert.deepStrictEqual(sut.selections, selections) + + let actual = sut.includes(1, { activeOnly: true }) + assert.strictEqual(actual, true) + + actual = sut.includes(2, { activeOnly: true }) + assert.strictEqual(actual, false) + }) + + it('range match if line number and activeOnly is set to false', async function () { + const selections = [ + { + anchor: 0, + active: 2, + }, + { + anchor: 4, + active: 6, + }, + ] + + editor = await setEditorSelection(selections) + assert.deepStrictEqual(sut.selections, selections) + + for (const line of [0, 1, 2]) { + const actual = sut.includes(line, { activeOnly: false }) + assert.strictEqual(actual, true) + } + + for (const line of [4, 5, 6]) { + const actual = sut.includes(line, { activeOnly: false }) + assert.strictEqual(actual, true) + } + + let actual = sut.includes(3, { activeOnly: false }) + assert.strictEqual(actual, false) + + actual = sut.includes(7, { activeOnly: false }) + assert.strictEqual(actual, false) + }) + }) + + describe('onContentChanged', function () { + it('should fire lineChangedEvent and set current line selection', async function () { + editor = await toTextEditor('\n\n\n\n\n', 'foo.py', undefined, { preview: false }) + editor.selection = new Selection(new Position(5, 0), new Position(5, 0)) + assertEmptyCounts() + + sut.onContentChanged({ + document: editor.document, + contentChanges: [{ text: 'a', range: new Range(0, 0, 0, 0), rangeOffset: 0, rangeLength: 0 }], + reason: undefined, + }) + + assert.deepStrictEqual(counts, { ...counts, content: 1 }) + assert.deepStrictEqual(sut.selections, [ + { + anchor: 5, + active: 5, + }, + ]) + }) + }) + + describe('onTextEditorSelectionChanged', function () { + it('should fire lineChangedEvent if selection changes and set current line selection', async function () { + editor = await toTextEditor('\n\n\n\n\n', 'foo.py', undefined, { preview: false }) + editor.selection = new Selection(new Position(3, 0), new Position(3, 0)) + assertEmptyCounts() + + await sut.onTextEditorSelectionChanged({ + textEditor: editor, + selections: [new Selection(new Position(3, 0), new Position(3, 0))], + kind: undefined, + }) + + assert.deepStrictEqual(counts, { ...counts, selection: 1 }) + assert.deepStrictEqual(sut.selections, [ + { + anchor: 3, + active: 3, + }, + ]) + + // if selection is included in the existing selections, won't emit an event + await sut.onTextEditorSelectionChanged({ + textEditor: editor, + selections: [new Selection(new Position(3, 0), new Position(3, 0))], + kind: undefined, + }) + + assert.deepStrictEqual(counts, { ...counts, selection: 1 }) + assert.deepStrictEqual(sut.selections, [ + { + anchor: 3, + active: 3, + }, + ]) + }) + + it('should not fire lineChangedEvent if uri scheme is debug || output', async function () { + // if the editor is not a text editor, won't emit an event and selection will be set to undefined + async function assertLineChanged(schema: string) { + const anotherEditor = await toTextEditor('', 'bar.log', undefined, { preview: false }) + const uri = anotherEditor.document.uri + sandbox.stub(uri, 'scheme').get(() => schema) + + await sut.onTextEditorSelectionChanged({ + textEditor: anotherEditor, + selections: [new Selection(new Position(3, 0), new Position(3, 0))], + kind: undefined, + }) + + assert.deepStrictEqual(counts, { ...counts }) + } + + await assertLineChanged('debug') + await assertLineChanged('output') + }) + }) + + describe('onActiveTextEditorChanged', function () { + it('shoudl fire lineChangedEvent', async function () { + const selections: Selection[] = [new Selection(0, 0, 1, 1)] + + editor = { selections: selections } as any + + assertEmptyCounts() + + await sut.onActiveTextEditorChanged(editor) + + assert.deepStrictEqual(counts, { ...counts, editor: 1 }) + assert.deepStrictEqual(sut.selections, [ + { + anchor: 0, + active: 1, + }, + ]) + }) + }) +}) diff --git a/packages/amazonq/test/unit/amazonq/apps/inline/recommendationService.test.ts b/packages/amazonq/test/unit/amazonq/apps/inline/recommendationService.test.ts index b3628e22c35..57eca77b147 100644 --- a/packages/amazonq/test/unit/amazonq/apps/inline/recommendationService.test.ts +++ b/packages/amazonq/test/unit/amazonq/apps/inline/recommendationService.test.ts @@ -10,6 +10,8 @@ import assert from 'assert' import { RecommendationService } from '../../../../../src/app/inline/recommendationService' import { SessionManager } from '../../../../../src/app/inline/sessionManager' import { createMockDocument } from 'aws-core-vscode/test' +import { LineTracker } from '../../../../../src/app/inline/stateTracker/lineTracker' +import { InlineGeneratingMessage } from '../../../../../src/app/inline/inlineGeneratingMessage' describe('RecommendationService', () => { let languageClient: LanguageClient @@ -28,7 +30,9 @@ describe('RecommendationService', () => { } as InlineCompletionItem const mockPartialResultToken = 'some-random-token' const sessionManager = new SessionManager() - const service = new RecommendationService(sessionManager) + const lineTracker = new LineTracker() + const activeStateController = new InlineGeneratingMessage(lineTracker) + const service = new RecommendationService(sessionManager, activeStateController) beforeEach(() => { sandbox = sinon.createSandbox() @@ -107,13 +111,6 @@ describe('RecommendationService', () => { ...expectedRequestArgs, partialResultToken: mockPartialResultToken, }) - - // Verify session management - const items = sessionManager.getActiveRecommendation() - assert.deepStrictEqual(items, [mockInlineCompletionItemOne, { insertText: '1' } as InlineCompletionItem]) - sessionManager.incrementActiveIndex() - const items2 = sessionManager.getActiveRecommendation() - assert.deepStrictEqual(items2, [mockInlineCompletionItemTwo, { insertText: '1' } as InlineCompletionItem]) }) }) }) diff --git a/packages/amazonq/test/unit/amazonq/lsp/config.test.ts b/packages/amazonq/test/unit/amazonq/lsp/config.test.ts index 0327395fe1a..69b15d6e311 100644 --- a/packages/amazonq/test/unit/amazonq/lsp/config.test.ts +++ b/packages/amazonq/test/unit/amazonq/lsp/config.test.ts @@ -7,97 +7,73 @@ import assert from 'assert' import { DevSettings } from 'aws-core-vscode/shared' import sinon from 'sinon' import { defaultAmazonQLspConfig, ExtendedAmazonQLSPConfig, getAmazonQLspConfig } from '../../../../src/lsp/config' -import { defaultAmazonQWorkspaceLspConfig, getAmazonQWorkspaceLspConfig, LspConfig } from 'aws-core-vscode/amazonq' -for (const [name, config, defaultConfig, setEnv, resetEnv] of [ - [ - 'getAmazonQLspConfig', - getAmazonQLspConfig, - defaultAmazonQLspConfig, - (envConfig: ExtendedAmazonQLSPConfig) => { - process.env.__AMAZONQLSP_MANIFEST_URL = envConfig.manifestUrl - process.env.__AMAZONQLSP_SUPPORTED_VERSIONS = envConfig.supportedVersions - process.env.__AMAZONQLSP_ID = envConfig.id - process.env.__AMAZONQLSP_PATH = envConfig.path - process.env.__AMAZONQLSP_UI = envConfig.ui - }, - () => { - delete process.env.__AMAZONQLSP_MANIFEST_URL - delete process.env.__AMAZONQLSP_SUPPORTED_VERSIONS - delete process.env.__AMAZONQLSP_ID - delete process.env.__AMAZONQLSP_PATH - delete process.env.__AMAZONQLSP_UI - }, - ], - [ - 'getAmazonQWorkspaceLspConfig', - getAmazonQWorkspaceLspConfig, - defaultAmazonQWorkspaceLspConfig, - (envConfig: LspConfig) => { - process.env.__AMAZONQWORKSPACELSP_MANIFEST_URL = envConfig.manifestUrl - process.env.__AMAZONQWORKSPACELSP_SUPPORTED_VERSIONS = envConfig.supportedVersions - process.env.__AMAZONQWORKSPACELSP_ID = envConfig.id - process.env.__AMAZONQWORKSPACELSP_PATH = envConfig.path - }, - () => { - delete process.env.__AMAZONQWORKSPACELSP_MANIFEST_URL - delete process.env.__AMAZONQWORKSPACELSP_SUPPORTED_VERSIONS - delete process.env.__AMAZONQWORKSPACELSP_ID - delete process.env.__AMAZONQWORKSPACELSP_PATH - }, - ], -] as const) { - describe(name, () => { - let sandbox: sinon.SinonSandbox - let serviceConfigStub: sinon.SinonStub - const settingConfig: LspConfig = { - manifestUrl: 'https://custom.url/manifest.json', - supportedVersions: '4.0.0', - id: 'AmazonQSetting', - suppressPromptPrefix: config().suppressPromptPrefix, - path: '/custom/path', - ...(name === 'getAmazonQLspConfig' && { ui: '/chat/client/location' }), - } +describe('getAmazonQLspConfig', () => { + let sandbox: sinon.SinonSandbox + let serviceConfigStub: sinon.SinonStub + const settingConfig: ExtendedAmazonQLSPConfig = { + manifestUrl: 'https://custom.url/manifest.json', + supportedVersions: '4.0.0', + id: 'AmazonQSetting', + suppressPromptPrefix: getAmazonQLspConfig().suppressPromptPrefix, + path: '/custom/path', + ui: '/chat/client/location', + } - beforeEach(() => { - sandbox = sinon.createSandbox() + beforeEach(() => { + sandbox = sinon.createSandbox() - serviceConfigStub = sandbox.stub() - sandbox.stub(DevSettings, 'instance').get(() => ({ - getServiceConfig: serviceConfigStub, - })) - }) + serviceConfigStub = sandbox.stub() + sandbox.stub(DevSettings, 'instance').get(() => ({ + getServiceConfig: serviceConfigStub, + })) + }) - afterEach(() => { - sandbox.restore() - resetEnv() - }) + afterEach(() => { + sandbox.restore() + resetEnv() + }) - it('uses default config', () => { - serviceConfigStub.returns({}) - assert.deepStrictEqual(config(), defaultConfig) - }) + it('uses default config', () => { + serviceConfigStub.returns({}) + assert.deepStrictEqual(getAmazonQLspConfig(), defaultAmazonQLspConfig) + }) - it('overrides path', () => { - const path = '/custom/path/to/lsp' - serviceConfigStub.returns({ path }) + it('overrides path', () => { + const path = '/custom/path/to/lsp' + serviceConfigStub.returns({ path }) - assert.deepStrictEqual(config(), { - ...defaultConfig, - path, - }) + assert.deepStrictEqual(getAmazonQLspConfig(), { + ...defaultAmazonQLspConfig, + path, }) + }) - it('overrides default settings', () => { - serviceConfigStub.returns(settingConfig) + it('overrides default settings', () => { + serviceConfigStub.returns(settingConfig) - assert.deepStrictEqual(config(), settingConfig) - }) + assert.deepStrictEqual(getAmazonQLspConfig(), settingConfig) + }) - it('environment variable takes precedence over settings', () => { - setEnv(settingConfig) - serviceConfigStub.returns({}) - assert.deepStrictEqual(config(), settingConfig) - }) + it('environment variable takes precedence over settings', () => { + setEnv(settingConfig) + serviceConfigStub.returns({}) + assert.deepStrictEqual(getAmazonQLspConfig(), settingConfig) }) -} + + function setEnv(envConfig: ExtendedAmazonQLSPConfig) { + process.env.__AMAZONQLSP_MANIFEST_URL = envConfig.manifestUrl + process.env.__AMAZONQLSP_SUPPORTED_VERSIONS = envConfig.supportedVersions + process.env.__AMAZONQLSP_ID = envConfig.id + process.env.__AMAZONQLSP_PATH = envConfig.path + process.env.__AMAZONQLSP_UI = envConfig.ui + } + + function resetEnv() { + delete process.env.__AMAZONQLSP_MANIFEST_URL + delete process.env.__AMAZONQLSP_SUPPORTED_VERSIONS + delete process.env.__AMAZONQLSP_ID + delete process.env.__AMAZONQLSP_PATH + delete process.env.__AMAZONQLSP_UI + } +}) diff --git a/packages/amazonq/test/unit/amazonq/lsp/encryption.test.ts b/packages/amazonq/test/unit/amazonq/lsp/encryption.test.ts new file mode 100644 index 00000000000..06a901edde6 --- /dev/null +++ b/packages/amazonq/test/unit/amazonq/lsp/encryption.test.ts @@ -0,0 +1,27 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import * as assert from 'assert' +import { decryptResponse, encryptRequest } from '../../../../src/lsp/encryption' +import { encryptionKey } from '../../../../src/lsp/auth' + +describe('LSP encryption', function () { + it('encrypt and decrypt invert eachother with same key', async function () { + const key = encryptionKey + const request = { + id: 0, + name: 'my Request', + isRealRequest: false, + metadata: { + tags: ['tag1', 'tag2'], + }, + } + const encryptedPayload = await encryptRequest(request, key) + const message = (encryptedPayload as { message: string }).message + const decrypted = await decryptResponse(message, key) + + assert.deepStrictEqual(decrypted, request) + }) +}) diff --git a/packages/amazonq/test/unit/amazonq/lsp/lspClient.test.ts b/packages/amazonq/test/unit/amazonq/lsp/lspClient.test.ts deleted file mode 100644 index 369cda5402d..00000000000 --- a/packages/amazonq/test/unit/amazonq/lsp/lspClient.test.ts +++ /dev/null @@ -1,72 +0,0 @@ -/*! - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ -import * as sinon from 'sinon' -import assert from 'assert' -import { globals, getNodeExecutableName } from 'aws-core-vscode/shared' -import { LspClient, lspClient as lspClientModule } from 'aws-core-vscode/amazonq' - -describe('Amazon Q LSP client', function () { - let lspClient: LspClient - let encryptFunc: sinon.SinonSpy - - beforeEach(async function () { - sinon.stub(globals, 'isWeb').returns(false) - lspClient = new LspClient() - encryptFunc = sinon.spy(lspClient, 'encrypt') - }) - - it('encrypts payload of query ', async () => { - await lspClient.queryVectorIndex('mock_input') - assert.ok(encryptFunc.calledOnce) - assert.ok(encryptFunc.calledWith(JSON.stringify({ query: 'mock_input' }))) - const value = await encryptFunc.returnValues[0] - // verifies JWT encryption header - assert.ok(value.startsWith(`eyJhbGciOiJkaXIiLCJlbmMiOiJBMjU2R0NNIn0`)) - }) - - it('encrypts payload of index files ', async () => { - await lspClient.buildIndex(['fileA'], 'path', 'all') - assert.ok(encryptFunc.calledOnce) - assert.ok( - encryptFunc.calledWith( - JSON.stringify({ - filePaths: ['fileA'], - projectRoot: 'path', - config: 'all', - language: '', - }) - ) - ) - const value = await encryptFunc.returnValues[0] - // verifies JWT encryption header - assert.ok(value.startsWith(`eyJhbGciOiJkaXIiLCJlbmMiOiJBMjU2R0NNIn0`)) - }) - - it('encrypt removes readable information', async () => { - const sample = 'hello' - const encryptedSample = await lspClient.encrypt(sample) - assert.ok(!encryptedSample.includes('hello')) - }) - - it('validates node executable + lsp bundle', async () => { - await assert.rejects(async () => { - await lspClientModule.activate(globals.context, { - // Mimic the `LspResolution` type. - node: 'node.bogus.exe', - lsp: 'fake/lsp.js', - }) - }, /.*failed to run basic .*node.*exitcode.*node\.bogus\.exe.*/) - await assert.rejects(async () => { - await lspClientModule.activate(globals.context, { - node: getNodeExecutableName(), - lsp: 'fake/lsp.js', - }) - }, /.*failed to run .*exitcode.*node.*lsp\.js/) - }) - - afterEach(() => { - sinon.restore() - }) -}) diff --git a/packages/amazonq/test/unit/codewhisperer/commands/invokeRecommendation.test.ts b/packages/amazonq/test/unit/codewhisperer/commands/invokeRecommendation.test.ts deleted file mode 100644 index 68cebe37bb1..00000000000 --- a/packages/amazonq/test/unit/codewhisperer/commands/invokeRecommendation.test.ts +++ /dev/null @@ -1,43 +0,0 @@ -/*! - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -import assert from 'assert' -import * as sinon from 'sinon' -import { resetCodeWhispererGlobalVariables, createMockTextEditor } from 'aws-core-vscode/test' -import { - ConfigurationEntry, - invokeRecommendation, - InlineCompletionService, - isInlineCompletionEnabled, - DefaultCodeWhispererClient, -} from 'aws-core-vscode/codewhisperer' - -describe('invokeRecommendation', function () { - describe('invokeRecommendation', function () { - let getRecommendationStub: sinon.SinonStub - let mockClient: DefaultCodeWhispererClient - - beforeEach(async function () { - await resetCodeWhispererGlobalVariables() - getRecommendationStub = sinon.stub(InlineCompletionService.instance, 'getPaginatedRecommendation') - }) - - afterEach(function () { - sinon.restore() - }) - - it('Should call getPaginatedRecommendation with OnDemand as trigger type when inline completion is enabled', async function () { - const mockEditor = createMockTextEditor() - const config: ConfigurationEntry = { - isShowMethodsEnabled: true, - isManualTriggerEnabled: true, - isAutomatedTriggerEnabled: true, - isSuggestionsWithCodeReferencesEnabled: true, - } - await invokeRecommendation(mockEditor, mockClient, config) - assert.strictEqual(getRecommendationStub.called, isInlineCompletionEnabled()) - }) - }) -}) diff --git a/packages/amazonq/test/unit/codewhisperer/commands/onAcceptance.test.ts b/packages/amazonq/test/unit/codewhisperer/commands/onAcceptance.test.ts deleted file mode 100644 index 0471aaa3601..00000000000 --- a/packages/amazonq/test/unit/codewhisperer/commands/onAcceptance.test.ts +++ /dev/null @@ -1,64 +0,0 @@ -/*! - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -import assert from 'assert' -import * as vscode from 'vscode' -import * as sinon from 'sinon' -import { onAcceptance, AcceptedSuggestionEntry, session, CodeWhispererTracker } from 'aws-core-vscode/codewhisperer' -import { resetCodeWhispererGlobalVariables, createMockTextEditor } from 'aws-core-vscode/test' - -describe('onAcceptance', function () { - describe('onAcceptance', function () { - beforeEach(async function () { - await resetCodeWhispererGlobalVariables() - session.reset() - }) - - afterEach(function () { - sinon.restore() - session.reset() - }) - - it('Should enqueue an event object to tracker', async function () { - const mockEditor = createMockTextEditor() - const trackerSpy = sinon.spy(CodeWhispererTracker.prototype, 'enqueue') - const fakeReferences = [ - { - message: '', - licenseName: 'MIT', - repository: 'http://github.com/fake', - recommendationContentSpan: { - start: 0, - end: 10, - }, - }, - ] - await onAcceptance({ - editor: mockEditor, - range: new vscode.Range(new vscode.Position(1, 0), new vscode.Position(1, 26)), - effectiveRange: new vscode.Range(new vscode.Position(1, 0), new vscode.Position(1, 26)), - acceptIndex: 0, - recommendation: "print('Hello World!')", - requestId: '', - sessionId: '', - triggerType: 'OnDemand', - completionType: 'Line', - language: 'python', - references: fakeReferences, - }) - const actualArg = trackerSpy.getCall(0).args[0] as AcceptedSuggestionEntry - assert.ok(trackerSpy.calledOnce) - assert.strictEqual(actualArg.originalString, 'def two_sum(nums, target):') - assert.strictEqual(actualArg.requestId, '') - assert.strictEqual(actualArg.sessionId, '') - assert.strictEqual(actualArg.triggerType, 'OnDemand') - assert.strictEqual(actualArg.completionType, 'Line') - assert.strictEqual(actualArg.language, 'python') - assert.deepStrictEqual(actualArg.startPosition, new vscode.Position(1, 0)) - assert.deepStrictEqual(actualArg.endPosition, new vscode.Position(1, 26)) - assert.strictEqual(actualArg.index, 0) - }) - }) -}) diff --git a/packages/amazonq/test/unit/codewhisperer/commands/onInlineAcceptance.test.ts b/packages/amazonq/test/unit/codewhisperer/commands/onInlineAcceptance.test.ts deleted file mode 100644 index ed3bc99fa34..00000000000 --- a/packages/amazonq/test/unit/codewhisperer/commands/onInlineAcceptance.test.ts +++ /dev/null @@ -1,43 +0,0 @@ -/*! - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -import assert from 'assert' -import * as vscode from 'vscode' -import * as sinon from 'sinon' -import { resetCodeWhispererGlobalVariables, createMockTextEditor } from 'aws-core-vscode/test' -import { onInlineAcceptance, RecommendationHandler, session } from 'aws-core-vscode/codewhisperer' - -describe('onInlineAcceptance', function () { - describe('onInlineAcceptance', function () { - beforeEach(async function () { - await resetCodeWhispererGlobalVariables() - session.reset() - }) - - afterEach(function () { - sinon.restore() - session.reset() - }) - - it('Should dispose inline completion provider', async function () { - const mockEditor = createMockTextEditor() - const spy = sinon.spy(RecommendationHandler.instance, 'disposeInlineCompletion') - await onInlineAcceptance({ - editor: mockEditor, - range: new vscode.Range(new vscode.Position(1, 0), new vscode.Position(1, 21)), - effectiveRange: new vscode.Range(new vscode.Position(1, 0), new vscode.Position(1, 21)), - acceptIndex: 0, - recommendation: "print('Hello World!')", - requestId: '', - sessionId: '', - triggerType: 'OnDemand', - completionType: 'Line', - language: 'python', - references: undefined, - }) - assert.ok(spy.calledWith()) - }) - }) -}) diff --git a/packages/amazonq/test/unit/codewhisperer/service/completionProvider.test.ts b/packages/amazonq/test/unit/codewhisperer/service/completionProvider.test.ts deleted file mode 100644 index 956999d64ad..00000000000 --- a/packages/amazonq/test/unit/codewhisperer/service/completionProvider.test.ts +++ /dev/null @@ -1,117 +0,0 @@ -/*! - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -import assert from 'assert' -import * as vscode from 'vscode' - -import { - getCompletionItems, - getCompletionItem, - getLabel, - Recommendation, - RecommendationHandler, - session, -} from 'aws-core-vscode/codewhisperer' -import { createMockDocument, resetCodeWhispererGlobalVariables } from 'aws-core-vscode/test' - -describe('completionProviderService', function () { - beforeEach(async function () { - await resetCodeWhispererGlobalVariables() - }) - - describe('getLabel', function () { - it('should return correct label given recommendation longer than Constants.LABEL_LENGTH', function () { - const mockLongRecommendation = ` - const metaDataFile = path.join(__dirname, 'nls.metadata.json'); - const locale = getUserDefinedLocale(argvConfig);` - const expected = '\n const m..' - assert.strictEqual(getLabel(mockLongRecommendation), expected) - }) - - it('should return correct label given short recommendation', function () { - const mockShortRecommendation = 'function onReady()' - const expected = 'function onReady()..' - assert.strictEqual(getLabel(mockShortRecommendation), expected) - }) - }) - - describe('getCompletionItem', function () { - it('should return targetCompletionItem given input', function () { - session.startPos = new vscode.Position(0, 0) - RecommendationHandler.instance.requestId = 'mock_requestId_getCompletionItem' - session.sessionId = 'mock_sessionId_getCompletionItem' - const mockPosition = new vscode.Position(0, 1) - const mockRecommendationDetail: Recommendation = { - content: "\n\t\tconsole.log('Hello world!');\n\t}", - } - const mockRecommendationIndex = 1 - const mockDocument = createMockDocument('', 'test.ts', 'typescript') - const expected: vscode.CompletionItem = { - label: "\n\t\tconsole.log('Hell..", - kind: 1, - detail: 'CodeWhisperer', - documentation: new vscode.MarkdownString().appendCodeblock( - "\n\t\tconsole.log('Hello world!');\n\t}", - 'typescript' - ), - sortText: '0000000002', - preselect: true, - insertText: new vscode.SnippetString("\n\t\tconsole.log('Hello world!');\n\t}"), - keepWhitespace: true, - command: { - command: 'aws.amazonq.accept', - title: 'On acceptance', - arguments: [ - new vscode.Range(0, 0, 0, 0), - 1, - "\n\t\tconsole.log('Hello world!');\n\t}", - 'mock_requestId_getCompletionItem', - 'mock_sessionId_getCompletionItem', - 'OnDemand', - 'Line', - 'typescript', - undefined, - ], - }, - } - const actual = getCompletionItem( - mockDocument, - mockPosition, - mockRecommendationDetail, - mockRecommendationIndex - ) - assert.deepStrictEqual(actual.command, expected.command) - assert.strictEqual(actual.sortText, expected.sortText) - assert.strictEqual(actual.label, expected.label) - assert.strictEqual(actual.kind, expected.kind) - assert.strictEqual(actual.preselect, expected.preselect) - assert.strictEqual(actual.keepWhitespace, expected.keepWhitespace) - assert.strictEqual(JSON.stringify(actual.documentation), JSON.stringify(expected.documentation)) - assert.strictEqual(JSON.stringify(actual.insertText), JSON.stringify(expected.insertText)) - }) - }) - - describe('getCompletionItems', function () { - it('should return completion items for each non-empty recommendation', async function () { - session.recommendations = [ - { content: "\n\t\tconsole.log('Hello world!');\n\t}" }, - { content: '\nvar a = 10' }, - ] - const mockPosition = new vscode.Position(0, 0) - const mockDocument = createMockDocument('', 'test.ts', 'typescript') - const actual = getCompletionItems(mockDocument, mockPosition) - assert.strictEqual(actual.length, 2) - }) - - it('should return empty completion items when recommendation is empty', async function () { - session.recommendations = [] - const mockPosition = new vscode.Position(14, 83) - const mockDocument = createMockDocument() - const actual = getCompletionItems(mockDocument, mockPosition) - const expected: vscode.CompletionItem[] = [] - assert.deepStrictEqual(actual, expected) - }) - }) -}) diff --git a/packages/amazonq/test/unit/codewhisperer/service/inlineCompletionService.test.ts b/packages/amazonq/test/unit/codewhisperer/service/inlineCompletionService.test.ts deleted file mode 100644 index 18fd7d2f21b..00000000000 --- a/packages/amazonq/test/unit/codewhisperer/service/inlineCompletionService.test.ts +++ /dev/null @@ -1,255 +0,0 @@ -/*! - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -import * as vscode from 'vscode' -import assert from 'assert' -import * as sinon from 'sinon' -import { - CodeWhispererStatusBar, - InlineCompletionService, - ReferenceInlineProvider, - RecommendationHandler, - CodeSuggestionsState, - ConfigurationEntry, - CWInlineCompletionItemProvider, - session, - AuthUtil, - listCodeWhispererCommandsId, - DefaultCodeWhispererClient, -} from 'aws-core-vscode/codewhisperer' -import { createMockTextEditor, resetCodeWhispererGlobalVariables, createMockDocument } from 'aws-core-vscode/test' - -describe('inlineCompletionService', function () { - beforeEach(async function () { - await resetCodeWhispererGlobalVariables() - }) - - describe('getPaginatedRecommendation', function () { - const config: ConfigurationEntry = { - isShowMethodsEnabled: true, - isManualTriggerEnabled: true, - isAutomatedTriggerEnabled: true, - isSuggestionsWithCodeReferencesEnabled: true, - } - - let mockClient: DefaultCodeWhispererClient - - beforeEach(async function () { - mockClient = new DefaultCodeWhispererClient() - await resetCodeWhispererGlobalVariables() - }) - - afterEach(function () { - sinon.restore() - }) - - it('should call checkAndResetCancellationTokens before showing inline and next token to be null', async function () { - const mockEditor = createMockTextEditor() - sinon.stub(RecommendationHandler.instance, 'getRecommendations').resolves({ - result: 'Succeeded', - errorMessage: undefined, - recommendationCount: 1, - }) - const checkAndResetCancellationTokensStub = sinon.stub( - RecommendationHandler.instance, - 'checkAndResetCancellationTokens' - ) - session.recommendations = [{ content: "\n\t\tconsole.log('Hello world!');\n\t}" }, { content: '' }] - await InlineCompletionService.instance.getPaginatedRecommendation( - mockClient, - mockEditor, - 'OnDemand', - config - ) - assert.ok(checkAndResetCancellationTokensStub.called) - assert.strictEqual(RecommendationHandler.instance.hasNextToken(), false) - }) - }) - - describe('clearInlineCompletionStates', function () { - it('should remove inline reference and recommendations', async function () { - const fakeReferences = [ - { - message: '', - licenseName: 'MIT', - repository: 'http://github.com/fake', - recommendationContentSpan: { - start: 0, - end: 10, - }, - }, - ] - ReferenceInlineProvider.instance.setInlineReference(1, 'test', fakeReferences) - session.recommendations = [{ content: "\n\t\tconsole.log('Hello world!');\n\t}" }, { content: '' }] - session.language = 'python' - - assert.ok(session.recommendations.length > 0) - await RecommendationHandler.instance.clearInlineCompletionStates() - assert.strictEqual(ReferenceInlineProvider.instance.refs.length, 0) - assert.strictEqual(session.recommendations.length, 0) - }) - }) - - describe('truncateOverlapWithRightContext', function () { - const fileName = 'test.py' - const language = 'python' - const rightContext = 'return target\n' - const doc = `import math\ndef two_sum(nums, target):\n` - const provider = new CWInlineCompletionItemProvider(0, 0, [], '', new vscode.Position(0, 0), '') - - it('removes overlap with right context from suggestion', async function () { - const mockSuggestion = 'return target\n' - const mockEditor = createMockTextEditor(`${doc}${rightContext}`, fileName, language) - const cursorPosition = new vscode.Position(2, 0) - const result = provider.truncateOverlapWithRightContext(mockEditor.document, mockSuggestion, cursorPosition) - assert.strictEqual(result, '') - }) - - it('only removes the overlap part from suggestion', async function () { - const mockSuggestion = 'print(nums)\nreturn target\n' - const mockEditor = createMockTextEditor(`${doc}${rightContext}`, fileName, language) - const cursorPosition = new vscode.Position(2, 0) - const result = provider.truncateOverlapWithRightContext(mockEditor.document, mockSuggestion, cursorPosition) - assert.strictEqual(result, 'print(nums)\n') - }) - - it('only removes the last overlap pattern from suggestion', async function () { - const mockSuggestion = 'return target\nprint(nums)\nreturn target\n' - const mockEditor = createMockTextEditor(`${doc}${rightContext}`, fileName, language) - const cursorPosition = new vscode.Position(2, 0) - const result = provider.truncateOverlapWithRightContext(mockEditor.document, mockSuggestion, cursorPosition) - assert.strictEqual(result, 'return target\nprint(nums)\n') - }) - - it('returns empty string if the remaining suggestion only contains white space', async function () { - const mockSuggestion = 'return target\n ' - const mockEditor = createMockTextEditor(`${doc}${rightContext}`, fileName, language) - const cursorPosition = new vscode.Position(2, 0) - const result = provider.truncateOverlapWithRightContext(mockEditor.document, mockSuggestion, cursorPosition) - assert.strictEqual(result, '') - }) - - it('returns the original suggestion if no match found', async function () { - const mockSuggestion = 'import numpy\n' - const mockEditor = createMockTextEditor(`${doc}${rightContext}`, fileName, language) - const cursorPosition = new vscode.Position(2, 0) - const result = provider.truncateOverlapWithRightContext(mockEditor.document, mockSuggestion, cursorPosition) - assert.strictEqual(result, 'import numpy\n') - }) - - it('ignores the space at the end of recommendation', async function () { - const mockSuggestion = 'return target\n\n\n\n\n' - const mockEditor = createMockTextEditor(`${doc}${rightContext}`, fileName, language) - const cursorPosition = new vscode.Position(2, 0) - const result = provider.truncateOverlapWithRightContext(mockEditor.document, mockSuggestion, cursorPosition) - assert.strictEqual(result, '') - }) - }) -}) - -describe('CWInlineCompletionProvider', function () { - beforeEach(async function () { - await resetCodeWhispererGlobalVariables() - }) - - describe('provideInlineCompletionItems', function () { - beforeEach(async function () { - await resetCodeWhispererGlobalVariables() - }) - - afterEach(function () { - sinon.restore() - }) - - it('should return undefined if position is before RecommendationHandler start pos', async function () { - const position = new vscode.Position(0, 0) - const document = createMockDocument() - const fakeContext = { triggerKind: 0, selectedCompletionInfo: undefined } - const token = new vscode.CancellationTokenSource().token - const provider = new CWInlineCompletionItemProvider(0, 0, [], '', new vscode.Position(1, 1), '') - const result = await provider.provideInlineCompletionItems(document, position, fakeContext, token) - - assert.ok(result === undefined) - }) - }) -}) - -describe('codewhisperer status bar', function () { - let sandbox: sinon.SinonSandbox - let statusBar: TestStatusBar - let service: InlineCompletionService - - class TestStatusBar extends CodeWhispererStatusBar { - constructor() { - super() - } - - getStatusBar() { - return this.statusBar - } - } - - beforeEach(async function () { - await resetCodeWhispererGlobalVariables() - sandbox = sinon.createSandbox() - statusBar = new TestStatusBar() - service = new InlineCompletionService(statusBar) - }) - - afterEach(function () { - sandbox.restore() - }) - - it('shows correct status bar when auth is not connected', async function () { - sandbox.stub(AuthUtil.instance, 'isConnectionValid').returns(false) - sandbox.stub(AuthUtil.instance, 'isConnectionExpired').returns(false) - - await service.refreshStatusBar() - - const actualStatusBar = statusBar.getStatusBar() - assert.strictEqual(actualStatusBar.text, '$(chrome-close) Amazon Q') - assert.strictEqual(actualStatusBar.command, listCodeWhispererCommandsId) - assert.deepStrictEqual(actualStatusBar.backgroundColor, new vscode.ThemeColor('statusBarItem.errorBackground')) - }) - - it('shows correct status bar when auth is connected', async function () { - sandbox.stub(AuthUtil.instance, 'isConnectionValid').returns(true) - sandbox.stub(CodeSuggestionsState.instance, 'isSuggestionsEnabled').returns(true) - - await service.refreshStatusBar() - - const actualStatusBar = statusBar.getStatusBar() - assert.strictEqual(actualStatusBar.text, '$(debug-start) Amazon Q') - assert.strictEqual(actualStatusBar.command, listCodeWhispererCommandsId) - assert.deepStrictEqual(actualStatusBar.backgroundColor, undefined) - }) - - it('shows correct status bar when auth is connected but paused', async function () { - sandbox.stub(AuthUtil.instance, 'isConnectionValid').returns(true) - sandbox.stub(CodeSuggestionsState.instance, 'isSuggestionsEnabled').returns(false) - - await service.refreshStatusBar() - - const actualStatusBar = statusBar.getStatusBar() - assert.strictEqual(actualStatusBar.text, '$(debug-pause) Amazon Q') - assert.strictEqual(actualStatusBar.command, listCodeWhispererCommandsId) - assert.deepStrictEqual(actualStatusBar.backgroundColor, undefined) - }) - - it('shows correct status bar when auth is expired', async function () { - sandbox.stub(AuthUtil.instance, 'isConnectionValid').returns(false) - sandbox.stub(AuthUtil.instance, 'isConnectionExpired').returns(true) - - await service.refreshStatusBar() - - const actualStatusBar = statusBar.getStatusBar() - assert.strictEqual(actualStatusBar.text, '$(debug-disconnect) Amazon Q') - assert.strictEqual(actualStatusBar.command, listCodeWhispererCommandsId) - assert.deepStrictEqual( - actualStatusBar.backgroundColor, - new vscode.ThemeColor('statusBarItem.warningBackground') - ) - }) -}) diff --git a/packages/amazonq/test/unit/codewhisperer/service/keyStrokeHandler.test.ts b/packages/amazonq/test/unit/codewhisperer/service/keyStrokeHandler.test.ts deleted file mode 100644 index 4b6a5291f22..00000000000 --- a/packages/amazonq/test/unit/codewhisperer/service/keyStrokeHandler.test.ts +++ /dev/null @@ -1,237 +0,0 @@ -/*! - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -import assert from 'assert' -import * as vscode from 'vscode' -import * as sinon from 'sinon' -import * as codewhispererSdkClient from 'aws-core-vscode/codewhisperer' -import { - createMockTextEditor, - createTextDocumentChangeEvent, - resetCodeWhispererGlobalVariables, -} from 'aws-core-vscode/test' -import * as EditorContext from 'aws-core-vscode/codewhisperer' -import { - ConfigurationEntry, - DocumentChangedSource, - KeyStrokeHandler, - DefaultDocumentChangedType, - RecommendationService, - ClassifierTrigger, - isInlineCompletionEnabled, - RecommendationHandler, - InlineCompletionService, -} from 'aws-core-vscode/codewhisperer' - -describe('keyStrokeHandler', function () { - const config: ConfigurationEntry = { - isShowMethodsEnabled: true, - isManualTriggerEnabled: true, - isAutomatedTriggerEnabled: true, - isSuggestionsWithCodeReferencesEnabled: true, - } - beforeEach(async function () { - await resetCodeWhispererGlobalVariables() - }) - describe('processKeyStroke', async function () { - let invokeSpy: sinon.SinonStub - let startTimerSpy: sinon.SinonStub - let mockClient: codewhispererSdkClient.DefaultCodeWhispererClient - beforeEach(async function () { - invokeSpy = sinon.stub(KeyStrokeHandler.instance, 'invokeAutomatedTrigger') - startTimerSpy = sinon.stub(KeyStrokeHandler.instance, 'startIdleTimeTriggerTimer') - sinon.spy(RecommendationHandler.instance, 'getRecommendations') - mockClient = new codewhispererSdkClient.DefaultCodeWhispererClient() - await resetCodeWhispererGlobalVariables() - sinon.stub(mockClient, 'listRecommendations') - sinon.stub(mockClient, 'generateRecommendations') - }) - afterEach(function () { - sinon.restore() - }) - - it('Whatever the input is, should skip when automatic trigger is turned off, should not call invokeAutomatedTrigger', async function () { - const mockEditor = createMockTextEditor() - const mockEvent: vscode.TextDocumentChangeEvent = createTextDocumentChangeEvent( - mockEditor.document, - new vscode.Range(new vscode.Position(0, 0), new vscode.Position(0, 1)), - ' ' - ) - const cfg: ConfigurationEntry = { - isShowMethodsEnabled: true, - isManualTriggerEnabled: true, - isAutomatedTriggerEnabled: false, - isSuggestionsWithCodeReferencesEnabled: true, - } - const keyStrokeHandler = new KeyStrokeHandler() - await keyStrokeHandler.processKeyStroke(mockEvent, mockEditor, mockClient, cfg) - assert.ok(!invokeSpy.called) - assert.ok(!startTimerSpy.called) - }) - - it('Should not call invokeAutomatedTrigger when changed text across multiple lines', async function () { - await testShouldInvoke('\nprint(n', false) - }) - - it('Should not call invokeAutomatedTrigger when doing delete or undo (empty changed text)', async function () { - await testShouldInvoke('', false) - }) - - it('Should call invokeAutomatedTrigger with Enter when inputing \n', async function () { - await testShouldInvoke('\n', true) - }) - - it('Should call invokeAutomatedTrigger with Enter when inputing \r\n', async function () { - await testShouldInvoke('\r\n', true) - }) - - it('Should call invokeAutomatedTrigger with SpecialCharacter when inputing {', async function () { - await testShouldInvoke('{', true) - }) - - it('Should not call invokeAutomatedTrigger for non-special characters for classifier language if classifier says no', async function () { - sinon.stub(ClassifierTrigger.instance, 'shouldTriggerFromClassifier').returns(false) - await testShouldInvoke('a', false) - }) - - it('Should call invokeAutomatedTrigger for non-special characters for classifier language if classifier says yes', async function () { - sinon.stub(ClassifierTrigger.instance, 'shouldTriggerFromClassifier').returns(true) - await testShouldInvoke('a', true) - }) - - it('Should skip invoking if there is immediate right context on the same line and not a single }', async function () { - const casesForSuppressTokenFilling = [ - { - rightContext: 'add', - shouldInvoke: false, - }, - { - rightContext: '}', - shouldInvoke: true, - }, - { - rightContext: '} ', - shouldInvoke: true, - }, - { - rightContext: ')', - shouldInvoke: true, - }, - { - rightContext: ') ', - shouldInvoke: true, - }, - { - rightContext: ' add', - shouldInvoke: true, - }, - { - rightContext: ' ', - shouldInvoke: true, - }, - { - rightContext: '\naddTwo', - shouldInvoke: true, - }, - ] - - for (const o of casesForSuppressTokenFilling) { - await testShouldInvoke('{', o.shouldInvoke, o.rightContext) - } - }) - - async function testShouldInvoke(input: string, shouldTrigger: boolean, rightContext: string = '') { - const mockEditor = createMockTextEditor(rightContext, 'test.js', 'javascript', 0, 0) - const mockEvent: vscode.TextDocumentChangeEvent = createTextDocumentChangeEvent( - mockEditor.document, - new vscode.Range(new vscode.Position(0, 0), new vscode.Position(0, 1)), - input - ) - await KeyStrokeHandler.instance.processKeyStroke(mockEvent, mockEditor, mockClient, config) - assert.strictEqual( - invokeSpy.called, - shouldTrigger, - `invokeAutomatedTrigger ${shouldTrigger ? 'NOT' : 'WAS'} called for rightContext: "${rightContext}"` - ) - } - }) - - describe('invokeAutomatedTrigger', function () { - let mockClient: codewhispererSdkClient.DefaultCodeWhispererClient - beforeEach(async function () { - sinon.restore() - mockClient = new codewhispererSdkClient.DefaultCodeWhispererClient() - await resetCodeWhispererGlobalVariables() - sinon.stub(mockClient, 'listRecommendations') - sinon.stub(mockClient, 'generateRecommendations') - }) - afterEach(function () { - sinon.restore() - }) - - it('should call getPaginatedRecommendation when inline completion is enabled', async function () { - const mockEditor = createMockTextEditor() - const keyStrokeHandler = new KeyStrokeHandler() - const mockEvent: vscode.TextDocumentChangeEvent = createTextDocumentChangeEvent( - mockEditor.document, - new vscode.Range(new vscode.Position(0, 0), new vscode.Position(0, 1)), - ' ' - ) - const getRecommendationsStub = sinon.stub(InlineCompletionService.instance, 'getPaginatedRecommendation') - await keyStrokeHandler.invokeAutomatedTrigger('Enter', mockEditor, mockClient, config, mockEvent) - assert.strictEqual(getRecommendationsStub.called, isInlineCompletionEnabled()) - }) - }) - - describe('shouldTriggerIdleTime', function () { - it('should return false when inline is enabled and inline completion is in progress ', function () { - const keyStrokeHandler = new KeyStrokeHandler() - sinon.stub(RecommendationService.instance, 'isRunning').get(() => true) - const result = keyStrokeHandler.shouldTriggerIdleTime() - assert.strictEqual(result, !isInlineCompletionEnabled()) - }) - }) - - describe('test checkChangeSource', function () { - const tabStr = ' '.repeat(EditorContext.getTabSize()) - - const cases: [string, DocumentChangedSource][] = [ - ['\n ', DocumentChangedSource.EnterKey], - ['\n', DocumentChangedSource.EnterKey], - ['(', DocumentChangedSource.SpecialCharsKey], - ['()', DocumentChangedSource.SpecialCharsKey], - ['{}', DocumentChangedSource.SpecialCharsKey], - ['(a, b):', DocumentChangedSource.Unknown], - [':', DocumentChangedSource.SpecialCharsKey], - ['a', DocumentChangedSource.RegularKey], - [tabStr, DocumentChangedSource.TabKey], - [' ', DocumentChangedSource.Reformatting], - ['def add(a,b):\n return a + b\n', DocumentChangedSource.Unknown], - ['function suggestedByIntelliSense():', DocumentChangedSource.Unknown], - ] - - for (const tuple of cases) { - const input = tuple[0] - const expected = tuple[1] - it(`test input ${input} should return ${expected}`, function () { - const actual = new DefaultDocumentChangedType( - createFakeDocumentChangeEvent(tuple[0]) - ).checkChangeSource() - assert.strictEqual(actual, expected) - }) - } - - function createFakeDocumentChangeEvent(str: string): ReadonlyArray { - return [ - { - range: new vscode.Range(new vscode.Position(0, 0), new vscode.Position(0, 5)), - rangeOffset: 0, - rangeLength: 0, - text: str, - }, - ] - } - }) -}) diff --git a/packages/amazonq/test/unit/codewhisperer/service/recommendationHandler.test.ts b/packages/amazonq/test/unit/codewhisperer/service/recommendationHandler.test.ts deleted file mode 100644 index 86dfc5e514c..00000000000 --- a/packages/amazonq/test/unit/codewhisperer/service/recommendationHandler.test.ts +++ /dev/null @@ -1,271 +0,0 @@ -/*! - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -import assert from 'assert' -import * as vscode from 'vscode' -import * as sinon from 'sinon' -import { - ReferenceInlineProvider, - session, - AuthUtil, - DefaultCodeWhispererClient, - RecommendationsList, - ConfigurationEntry, - RecommendationHandler, - CodeWhispererCodeCoverageTracker, - supplementalContextUtil, -} from 'aws-core-vscode/codewhisperer' -import { - assertTelemetryCurried, - stub, - createMockTextEditor, - resetCodeWhispererGlobalVariables, -} from 'aws-core-vscode/test' -// import * as supplementalContextUtil from 'aws-core-vscode/codewhisperer' - -describe('recommendationHandler', function () { - const config: ConfigurationEntry = { - isShowMethodsEnabled: true, - isManualTriggerEnabled: true, - isAutomatedTriggerEnabled: true, - isSuggestionsWithCodeReferencesEnabled: true, - } - beforeEach(async function () { - await resetCodeWhispererGlobalVariables() - }) - - describe('getRecommendations', async function () { - const mockClient = stub(DefaultCodeWhispererClient) - const mockEditor = createMockTextEditor() - const testStartUrl = 'testStartUrl' - - beforeEach(async function () { - sinon.restore() - await resetCodeWhispererGlobalVariables() - mockClient.listRecommendations.resolves({}) - mockClient.generateRecommendations.resolves({}) - RecommendationHandler.instance.clearRecommendations() - sinon.stub(AuthUtil.instance, 'startUrl').value(testStartUrl) - }) - - afterEach(function () { - sinon.restore() - }) - - it('should assign correct recommendations given input', async function () { - assert.strictEqual(CodeWhispererCodeCoverageTracker.instances.size, 0) - assert.strictEqual( - CodeWhispererCodeCoverageTracker.getTracker(mockEditor.document.languageId)?.serviceInvocationCount, - 0 - ) - - const mockServerResult = { - recommendations: [{ content: "print('Hello World!')" }, { content: '' }], - $response: { - requestId: 'test_request', - httpResponse: { - headers: { - 'x-amzn-sessionid': 'test_request', - }, - }, - }, - } - const handler = new RecommendationHandler() - sinon.stub(handler, 'getServerResponse').resolves(mockServerResult) - await handler.getRecommendations(mockClient, mockEditor, 'AutoTrigger', config, 'Enter', false) - const actual = session.recommendations - const expected: RecommendationsList = [{ content: "print('Hello World!')" }, { content: '' }] - assert.deepStrictEqual(actual, expected) - assert.strictEqual( - CodeWhispererCodeCoverageTracker.getTracker(mockEditor.document.languageId)?.serviceInvocationCount, - 1 - ) - }) - - it('should assign request id correctly', async function () { - const mockServerResult = { - recommendations: [{ content: "print('Hello World!')" }, { content: '' }], - $response: { - requestId: 'test_request', - httpResponse: { - headers: { - 'x-amzn-sessionid': 'test_request', - }, - }, - }, - } - const handler = new RecommendationHandler() - sinon.stub(handler, 'getServerResponse').resolves(mockServerResult) - sinon.stub(handler, 'isCancellationRequested').returns(false) - await handler.getRecommendations(mockClient, mockEditor, 'AutoTrigger', config, 'Enter', false) - assert.strictEqual(handler.requestId, 'test_request') - assert.strictEqual(session.sessionId, 'test_request') - assert.strictEqual(session.triggerType, 'AutoTrigger') - }) - - it('should call telemetry function that records a CodeWhisperer service invocation', async function () { - const mockServerResult = { - recommendations: [{ content: "print('Hello World!')" }, { content: '' }], - $response: { - requestId: 'test_request', - httpResponse: { - headers: { - 'x-amzn-sessionid': 'test_request', - }, - }, - }, - } - const handler = new RecommendationHandler() - sinon.stub(handler, 'getServerResponse').resolves(mockServerResult) - sinon.stub(supplementalContextUtil, 'fetchSupplementalContext').resolves({ - isUtg: false, - isProcessTimeout: false, - supplementalContextItems: [], - contentsLength: 100, - latency: 0, - strategy: 'empty', - }) - sinon.stub(performance, 'now').returns(0.0) - session.startPos = new vscode.Position(1, 0) - session.startCursorOffset = 2 - await handler.getRecommendations(mockClient, mockEditor, 'AutoTrigger', config, 'Enter') - const assertTelemetry = assertTelemetryCurried('codewhisperer_serviceInvocation') - assertTelemetry({ - codewhispererRequestId: 'test_request', - codewhispererSessionId: 'test_request', - codewhispererLastSuggestionIndex: 1, - codewhispererTriggerType: 'AutoTrigger', - codewhispererAutomatedTriggerType: 'Enter', - codewhispererImportRecommendationEnabled: true, - result: 'Succeeded', - codewhispererLineNumber: 1, - codewhispererCursorOffset: 38, - codewhispererLanguage: 'python', - credentialStartUrl: testStartUrl, - codewhispererSupplementalContextIsUtg: false, - codewhispererSupplementalContextTimeout: false, - codewhispererSupplementalContextLatency: 0, - codewhispererSupplementalContextLength: 100, - }) - }) - }) - - describe('isValidResponse', function () { - afterEach(function () { - sinon.restore() - }) - it('should return true if any response is not empty', function () { - const handler = new RecommendationHandler() - session.recommendations = [ - { - content: - '\n // Use the console to output debug info…n of the command with the "command" variable', - }, - { content: '' }, - ] - assert.ok(handler.isValidResponse()) - }) - - it('should return false if response is empty', function () { - const handler = new RecommendationHandler() - session.recommendations = [] - assert.ok(!handler.isValidResponse()) - }) - - it('should return false if all response has no string length', function () { - const handler = new RecommendationHandler() - session.recommendations = [{ content: '' }, { content: '' }] - assert.ok(!handler.isValidResponse()) - }) - }) - - describe('setCompletionType/getCompletionType', function () { - beforeEach(function () { - sinon.restore() - }) - - it('should set the completion type to block given a multi-line suggestion', function () { - session.setCompletionType(0, { content: 'test\n\n \t\r\nanother test' }) - assert.strictEqual(session.getCompletionType(0), 'Block') - - session.setCompletionType(0, { content: 'test\ntest\n' }) - assert.strictEqual(session.getCompletionType(0), 'Block') - - session.setCompletionType(0, { content: '\n \t\r\ntest\ntest' }) - assert.strictEqual(session.getCompletionType(0), 'Block') - }) - - it('should set the completion type to line given a single-line suggestion', function () { - session.setCompletionType(0, { content: 'test' }) - assert.strictEqual(session.getCompletionType(0), 'Line') - - session.setCompletionType(0, { content: 'test\r\t ' }) - assert.strictEqual(session.getCompletionType(0), 'Line') - }) - - it('should set the completion type to line given a multi-line completion but only one-lien of non-blank sequence', function () { - session.setCompletionType(0, { content: 'test\n\t' }) - assert.strictEqual(session.getCompletionType(0), 'Line') - - session.setCompletionType(0, { content: 'test\n ' }) - assert.strictEqual(session.getCompletionType(0), 'Line') - - session.setCompletionType(0, { content: 'test\n\r' }) - assert.strictEqual(session.getCompletionType(0), 'Line') - - session.setCompletionType(0, { content: '\n\n\n\ntest' }) - assert.strictEqual(session.getCompletionType(0), 'Line') - }) - }) - - describe('on event change', async function () { - beforeEach(function () { - const fakeReferences = [ - { - message: '', - licenseName: 'MIT', - repository: 'http://github.com/fake', - recommendationContentSpan: { - start: 0, - end: 10, - }, - }, - ] - ReferenceInlineProvider.instance.setInlineReference(1, 'test', fakeReferences) - session.sessionId = '' - RecommendationHandler.instance.requestId = '' - }) - - it('should remove inline reference onEditorChange', async function () { - session.sessionId = 'aSessionId' - RecommendationHandler.instance.requestId = 'aRequestId' - await RecommendationHandler.instance.onEditorChange() - assert.strictEqual(ReferenceInlineProvider.instance.refs.length, 0) - }) - it('should remove inline reference onFocusChange', async function () { - session.sessionId = 'aSessionId' - RecommendationHandler.instance.requestId = 'aRequestId' - await RecommendationHandler.instance.onFocusChange() - assert.strictEqual(ReferenceInlineProvider.instance.refs.length, 0) - }) - it('should not remove inline reference on cursor change from typing', async function () { - await RecommendationHandler.instance.onCursorChange({ - textEditor: createMockTextEditor(), - selections: [], - kind: vscode.TextEditorSelectionChangeKind.Keyboard, - }) - assert.strictEqual(ReferenceInlineProvider.instance.refs.length, 1) - }) - - it('should remove inline reference on cursor change from mouse movement', async function () { - await RecommendationHandler.instance.onCursorChange({ - textEditor: vscode.window.activeTextEditor!, - selections: [], - kind: vscode.TextEditorSelectionChangeKind.Mouse, - }) - assert.strictEqual(ReferenceInlineProvider.instance.refs.length, 0) - }) - }) -}) diff --git a/packages/amazonq/test/unit/codewhisperer/service/referenceLogViewProvider.test.ts b/packages/amazonq/test/unit/codewhisperer/service/referenceLogViewProvider.test.ts index 1c1b6322675..dcacf745a57 100644 --- a/packages/amazonq/test/unit/codewhisperer/service/referenceLogViewProvider.test.ts +++ b/packages/amazonq/test/unit/codewhisperer/service/referenceLogViewProvider.test.ts @@ -5,7 +5,6 @@ import assert from 'assert' import { createMockTextEditor, resetCodeWhispererGlobalVariables } from 'aws-core-vscode/test' import { ReferenceLogViewProvider, LicenseUtil } from 'aws-core-vscode/codewhisperer' - describe('referenceLogViewProvider', function () { beforeEach(async function () { await resetCodeWhispererGlobalVariables() @@ -66,4 +65,39 @@ describe('referenceLogViewProvider', function () { assert.ok(!actual.includes(LicenseUtil.getLicenseHtml('MIT'))) }) }) + + it('accepts references from CW and language server', async function () { + const cwReference = { + licenseName: 'MIT', + repository: 'TEST_REPO', + url: 'cw.com', + recommendationContentSpan: { + start: 0, + end: 10, + }, + } + + const flareReference = { + referenceName: 'test reference', + referenceUrl: 'flare.com', + licenseName: 'apache', + position: { + startCharacter: 0, + endCharacter: 10, + }, + } + + const actual = ReferenceLogViewProvider.getReferenceLog( + '', + [cwReference, flareReference], + createMockTextEditor() + ) + + assert.ok(actual.includes('MIT')) + assert.ok(actual.includes('apache')) + assert.ok(actual.includes('TEST_REPO')) + assert.ok(actual.includes('test reference')) + assert.ok(actual.includes('flare.com')) + assert.ok(actual.includes('cw.com')) + }) }) diff --git a/packages/amazonq/test/unit/codewhisperer/service/telemetry.test.ts b/packages/amazonq/test/unit/codewhisperer/service/telemetry.test.ts index 0f1429f130b..1f11661f002 100644 --- a/packages/amazonq/test/unit/codewhisperer/service/telemetry.test.ts +++ b/packages/amazonq/test/unit/codewhisperer/service/telemetry.test.ts @@ -19,9 +19,7 @@ import { DefaultCodeWhispererClient, ListRecommendationsResponse, Recommendation, - invokeRecommendation, ConfigurationEntry, - RecommendationHandler, session, vsCodeCursorUpdateDelay, AuthUtil, @@ -113,7 +111,6 @@ describe.skip('CodeWhisperer telemetry', async function () { }) async function resetStates() { - await RecommendationHandler.instance.clearInlineCompletionStates() await resetCodeWhispererGlobalVariables() } @@ -424,7 +421,6 @@ describe.skip('CodeWhisperer telemetry', async function () { assert.strictEqual(session.sessionId, 'session_id_1') assert.deepStrictEqual(session.requestIdList, ['request_id_1', 'request_id_1', 'request_id_1_2']) - await RecommendationHandler.instance.onEditorChange() assertSessionClean() await backspace(editor) // todo: without this, the following manual trigger will not be displayed in the test, investigate and fix it @@ -500,7 +496,6 @@ describe.skip('CodeWhisperer telemetry', async function () { await manualTrigger(editor, client, config) await assertTextEditorContains('') - await RecommendationHandler.instance.onFocusChange() assertTelemetry('codewhisperer_userTriggerDecision', [ session1UserTriggerEvent({ codewhispererSuggestionState: 'Reject' }), ]) @@ -513,7 +508,6 @@ async function manualTrigger( client: DefaultCodeWhispererClient, config: ConfigurationEntry ) { - await invokeRecommendation(editor, client, config) await waitUntilSuggestionSeen() } diff --git a/packages/amazonq/test/unit/codewhisperer/tracker/codewhispererCodeCoverageTracker.test.ts b/packages/amazonq/test/unit/codewhisperer/tracker/codewhispererCodeCoverageTracker.test.ts deleted file mode 100644 index ee001b3328d..00000000000 --- a/packages/amazonq/test/unit/codewhisperer/tracker/codewhispererCodeCoverageTracker.test.ts +++ /dev/null @@ -1,560 +0,0 @@ -/*! - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -import assert from 'assert' -import * as sinon from 'sinon' -import * as vscode from 'vscode' -import { - CodeWhispererCodeCoverageTracker, - vsCodeState, - TelemetryHelper, - AuthUtil, - getUnmodifiedAcceptedTokens, -} from 'aws-core-vscode/codewhisperer' -import { createMockDocument, createMockTextEditor, resetCodeWhispererGlobalVariables } from 'aws-core-vscode/test' -import { globals } from 'aws-core-vscode/shared' -import { assertTelemetryCurried } from 'aws-core-vscode/test' - -describe('codewhispererCodecoverageTracker', function () { - const language = 'python' - - describe('test getTracker', function () { - afterEach(async function () { - await resetCodeWhispererGlobalVariables() - CodeWhispererCodeCoverageTracker.instances.clear() - }) - - it('unsupported language', function () { - assert.strictEqual(CodeWhispererCodeCoverageTracker.getTracker('vb'), undefined) - assert.strictEqual(CodeWhispererCodeCoverageTracker.getTracker('ipynb'), undefined) - }) - - it('supported language', function () { - assert.notStrictEqual(CodeWhispererCodeCoverageTracker.getTracker('python'), undefined) - assert.notStrictEqual(CodeWhispererCodeCoverageTracker.getTracker('javascriptreact'), undefined) - assert.notStrictEqual(CodeWhispererCodeCoverageTracker.getTracker('java'), undefined) - assert.notStrictEqual(CodeWhispererCodeCoverageTracker.getTracker('javascript'), undefined) - assert.notStrictEqual(CodeWhispererCodeCoverageTracker.getTracker('cpp'), undefined) - assert.notStrictEqual(CodeWhispererCodeCoverageTracker.getTracker('ruby'), undefined) - assert.notStrictEqual(CodeWhispererCodeCoverageTracker.getTracker('go'), undefined) - }) - - it('supported language and should return singleton object per language', function () { - let instance1: CodeWhispererCodeCoverageTracker | undefined - let instance2: CodeWhispererCodeCoverageTracker | undefined - instance1 = CodeWhispererCodeCoverageTracker.getTracker('java') - instance2 = CodeWhispererCodeCoverageTracker.getTracker('java') - assert.notStrictEqual(instance1, undefined) - assert.strictEqual(Object.is(instance1, instance2), true) - - instance1 = CodeWhispererCodeCoverageTracker.getTracker('python') - instance2 = CodeWhispererCodeCoverageTracker.getTracker('python') - assert.notStrictEqual(instance1, undefined) - assert.strictEqual(Object.is(instance1, instance2), true) - - instance1 = CodeWhispererCodeCoverageTracker.getTracker('javascriptreact') - instance2 = CodeWhispererCodeCoverageTracker.getTracker('javascriptreact') - assert.notStrictEqual(instance1, undefined) - assert.strictEqual(Object.is(instance1, instance2), true) - }) - }) - - describe('test isActive', function () { - let tracker: CodeWhispererCodeCoverageTracker | undefined - - afterEach(async function () { - await resetCodeWhispererGlobalVariables() - CodeWhispererCodeCoverageTracker.instances.clear() - sinon.restore() - }) - - it('inactive case: telemetryEnable = true, isConnected = false', function () { - sinon.stub(TelemetryHelper.instance, 'isTelemetryEnabled').returns(true) - sinon.stub(AuthUtil.instance, 'isConnected').returns(false) - - tracker = CodeWhispererCodeCoverageTracker.getTracker('python') - if (!tracker) { - assert.fail() - } - - assert.strictEqual(tracker.isActive(), false) - }) - - it('inactive case: telemetryEnabled = false, isConnected = false', function () { - sinon.stub(TelemetryHelper.instance, 'isTelemetryEnabled').returns(false) - sinon.stub(AuthUtil.instance, 'isConnected').returns(false) - - tracker = CodeWhispererCodeCoverageTracker.getTracker('java') - if (!tracker) { - assert.fail() - } - - assert.strictEqual(tracker.isActive(), false) - }) - - it('active case: telemetryEnabled = true, isConnected = true', function () { - sinon.stub(TelemetryHelper.instance, 'isTelemetryEnabled').returns(true) - sinon.stub(AuthUtil.instance, 'isConnected').returns(true) - - tracker = CodeWhispererCodeCoverageTracker.getTracker('javascript') - if (!tracker) { - assert.fail() - } - assert.strictEqual(tracker.isActive(), true) - }) - }) - - describe('updateAcceptedTokensCount', function () { - let tracker: CodeWhispererCodeCoverageTracker | undefined - - beforeEach(async function () { - await resetCodeWhispererGlobalVariables() - tracker = CodeWhispererCodeCoverageTracker.getTracker(language) - if (tracker) { - sinon.stub(tracker, 'isActive').returns(true) - } - }) - - afterEach(function () { - sinon.restore() - CodeWhispererCodeCoverageTracker.instances.clear() - }) - - it('Should compute edit distance to update the accepted tokens', function () { - if (!tracker) { - assert.fail() - } - const editor = createMockTextEditor('def addTwoNumbers(a, b):\n') - - tracker.addAcceptedTokens(editor.document.fileName, { - range: new vscode.Range(0, 0, 0, 25), - text: `def addTwoNumbers(x, y):\n`, - accepted: 25, - }) - tracker.addTotalTokens(editor.document.fileName, 100) - tracker.updateAcceptedTokensCount(editor) - assert.strictEqual(tracker?.acceptedTokens[editor.document.fileName][0].accepted, 23) - }) - }) - - describe('getUnmodifiedAcceptedTokens', function () { - let tracker: CodeWhispererCodeCoverageTracker | undefined - - beforeEach(async function () { - await resetCodeWhispererGlobalVariables() - tracker = CodeWhispererCodeCoverageTracker.getTracker(language) - if (tracker) { - sinon.stub(tracker, 'isActive').returns(true) - } - }) - - afterEach(function () { - sinon.restore() - CodeWhispererCodeCoverageTracker.instances.clear() - }) - - it('Should return correct unmodified accepted tokens count', function () { - assert.strictEqual(getUnmodifiedAcceptedTokens('foo', 'fou'), 2) - assert.strictEqual(getUnmodifiedAcceptedTokens('foo', 'f11111oo'), 3) - assert.strictEqual(getUnmodifiedAcceptedTokens('foo', 'fo'), 2) - assert.strictEqual(getUnmodifiedAcceptedTokens('helloworld', 'HelloWorld'), 8) - assert.strictEqual(getUnmodifiedAcceptedTokens('helloworld', 'World'), 4) - assert.strictEqual(getUnmodifiedAcceptedTokens('CodeWhisperer', 'CODE'), 1) - assert.strictEqual(getUnmodifiedAcceptedTokens('CodeWhisperer', 'CodeWhispererGood'), 13) - }) - }) - - describe('countAcceptedTokens', function () { - let tracker: CodeWhispererCodeCoverageTracker | undefined - - beforeEach(async function () { - await resetCodeWhispererGlobalVariables() - tracker = CodeWhispererCodeCoverageTracker.getTracker(language) - if (tracker) { - sinon.stub(tracker, 'isActive').returns(true) - } - }) - - afterEach(function () { - sinon.restore() - CodeWhispererCodeCoverageTracker.instances.clear() - }) - - it('Should skip when tracker is not active', function () { - if (!tracker) { - assert.fail() - } - tracker.countAcceptedTokens(new vscode.Range(0, 0, 0, 1), 'a', 'test.py') - const spy = sinon.spy(CodeWhispererCodeCoverageTracker.prototype, 'addAcceptedTokens') - assert.ok(!spy.called) - }) - - it('Should increase AcceptedTokens', function () { - if (!tracker) { - assert.fail() - } - tracker.countAcceptedTokens(new vscode.Range(0, 0, 0, 1), 'a', 'test.py') - assert.deepStrictEqual(tracker.acceptedTokens['test.py'][0], { - range: new vscode.Range(0, 0, 0, 1), - text: 'a', - accepted: 1, - }) - }) - it('Should increase TotalTokens', function () { - if (!tracker) { - assert.fail() - } - tracker.countAcceptedTokens(new vscode.Range(0, 0, 0, 1), 'a', 'test.py') - tracker.countAcceptedTokens(new vscode.Range(0, 0, 0, 1), 'b', 'test.py') - assert.deepStrictEqual(tracker.totalTokens['test.py'], 2) - }) - }) - - describe('countTotalTokens', function () { - let tracker: CodeWhispererCodeCoverageTracker | undefined - - beforeEach(async function () { - await resetCodeWhispererGlobalVariables() - tracker = CodeWhispererCodeCoverageTracker.getTracker(language) - if (tracker) { - sinon.stub(tracker, 'isActive').returns(true) - } - }) - - afterEach(function () { - sinon.restore() - CodeWhispererCodeCoverageTracker.instances.clear() - }) - - it('Should skip when content change size is more than 50', function () { - if (!tracker) { - assert.fail() - } - tracker.countTotalTokens({ - reason: undefined, - document: createMockDocument(), - contentChanges: [ - { - range: new vscode.Range(0, 0, 0, 600), - rangeOffset: 0, - rangeLength: 600, - text: 'def twoSum(nums, target):\nfor '.repeat(20), - }, - ], - }) - assert.strictEqual(Object.keys(tracker.totalTokens).length, 0) - }) - - it('Should not skip when content change size is less than 50', function () { - if (!tracker) { - assert.fail() - } - tracker.countTotalTokens({ - reason: undefined, - document: createMockDocument(), - contentChanges: [ - { - range: new vscode.Range(0, 0, 0, 49), - rangeOffset: 0, - rangeLength: 49, - text: 'a = 123'.repeat(7), - }, - ], - }) - assert.strictEqual(Object.keys(tracker.totalTokens).length, 1) - assert.strictEqual(Object.values(tracker.totalTokens)[0], 49) - }) - - it('Should skip when CodeWhisperer is editing', function () { - if (!tracker) { - assert.fail() - } - vsCodeState.isCodeWhispererEditing = true - tracker.countTotalTokens({ - reason: undefined, - document: createMockDocument(), - contentChanges: [ - { - range: new vscode.Range(0, 0, 0, 30), - rangeOffset: 0, - rangeLength: 30, - text: 'def twoSum(nums, target):\nfor', - }, - ], - }) - const startedSpy = sinon.spy(CodeWhispererCodeCoverageTracker.prototype, 'addTotalTokens') - assert.ok(!startedSpy.called) - }) - - it('Should not reduce tokens when delete', function () { - if (!tracker) { - assert.fail() - } - const doc = createMockDocument('import math', 'test.py', 'python') - tracker.countTotalTokens({ - reason: undefined, - document: doc, - contentChanges: [ - { - range: new vscode.Range(0, 0, 0, 1), - rangeOffset: 0, - rangeLength: 0, - text: 'a', - }, - ], - }) - tracker.countTotalTokens({ - reason: undefined, - document: doc, - contentChanges: [ - { - range: new vscode.Range(0, 0, 0, 1), - rangeOffset: 0, - rangeLength: 0, - text: 'b', - }, - ], - }) - assert.strictEqual(tracker?.totalTokens[doc.fileName], 2) - tracker.countTotalTokens({ - reason: undefined, - document: doc, - contentChanges: [ - { - range: new vscode.Range(0, 0, 0, 1), - rangeOffset: 1, - rangeLength: 1, - text: '', - }, - ], - }) - assert.strictEqual(tracker?.totalTokens[doc.fileName], 2) - }) - - it('Should add tokens when type', function () { - if (!tracker) { - assert.fail() - } - const doc = createMockDocument('import math', 'test.py', 'python') - tracker.countTotalTokens({ - reason: undefined, - document: doc, - contentChanges: [ - { - range: new vscode.Range(0, 0, 0, 1), - rangeOffset: 0, - rangeLength: 0, - text: 'a', - }, - ], - }) - assert.strictEqual(tracker?.totalTokens[doc.fileName], 1) - }) - - it('Should add tokens when hitting enter with indentation', function () { - if (!tracker) { - assert.fail() - } - const doc = createMockDocument('def h():', 'test.py', 'python') - tracker.countTotalTokens({ - reason: undefined, - document: doc, - contentChanges: [ - { - range: new vscode.Range(0, 0, 0, 8), - rangeOffset: 0, - rangeLength: 0, - text: '\n ', - }, - ], - }) - assert.strictEqual(tracker?.totalTokens[doc.fileName], 1) - }) - - it('Should add tokens when hitting enter with indentation in Windows', function () { - if (!tracker) { - assert.fail() - } - const doc = createMockDocument('def h():', 'test.py', 'python') - tracker.countTotalTokens({ - reason: undefined, - document: doc, - contentChanges: [ - { - range: new vscode.Range(0, 0, 0, 8), - rangeOffset: 0, - rangeLength: 0, - text: '\r\n ', - }, - ], - }) - assert.strictEqual(tracker?.totalTokens[doc.fileName], 1) - }) - - it('Should add tokens when hitting enter with indentation in Java', function () { - if (!tracker) { - assert.fail() - } - const doc = createMockDocument('class A() {', 'test.java', 'java') - tracker.countTotalTokens({ - reason: undefined, - document: doc, - contentChanges: [ - { - range: new vscode.Range(0, 0, 0, 11), - rangeOffset: 0, - rangeLength: 0, - text: '', - }, - { - range: new vscode.Range(0, 0, 0, 11), - rangeOffset: 0, - rangeLength: 0, - text: '\n\t\t', - }, - ], - }) - assert.strictEqual(tracker?.totalTokens[doc.fileName], 1) - }) - - it('Should add tokens when inserting closing brackets', function () { - if (!tracker) { - assert.fail() - } - const doc = createMockDocument('a=', 'test.py', 'python') - tracker.countTotalTokens({ - reason: undefined, - document: doc, - contentChanges: [ - { - range: new vscode.Range(0, 0, 0, 3), - rangeOffset: 0, - rangeLength: 0, - text: '[]', - }, - ], - }) - assert.strictEqual(tracker?.totalTokens[doc.fileName], 2) - }) - - it('Should add tokens when inserting closing brackets in Java', function () { - if (!tracker) { - assert.fail() - } - const doc = createMockDocument('class A ', 'test.java', 'java') - tracker.countTotalTokens({ - reason: undefined, - document: doc, - contentChanges: [ - { - range: new vscode.Range(0, 0, 0, 8), - rangeOffset: 0, - rangeLength: 0, - text: '{}', - }, - { - range: new vscode.Range(0, 0, 0, 8), - rangeOffset: 0, - rangeLength: 0, - text: '', - }, - ], - }) - assert.strictEqual(tracker?.totalTokens[doc.fileName], 2) - }) - }) - - describe('flush', function () { - let tracker: CodeWhispererCodeCoverageTracker | undefined - - beforeEach(async function () { - await resetCodeWhispererGlobalVariables() - tracker = CodeWhispererCodeCoverageTracker.getTracker(language) - if (tracker) { - sinon.stub(tracker, 'isActive').returns(true) - } - }) - - afterEach(function () { - sinon.restore() - CodeWhispererCodeCoverageTracker.instances.clear() - }) - - it('Should not send codecoverage telemetry if tracker is not active', function () { - if (!tracker) { - assert.fail() - } - sinon.restore() - sinon.stub(tracker, 'isActive').returns(false) - - tracker.addAcceptedTokens(`test.py`, { range: new vscode.Range(0, 0, 0, 7), text: `print()`, accepted: 7 }) - tracker.addTotalTokens(`test.py`, 100) - tracker.flush() - const data = globals.telemetry.logger.query({ - metricName: 'codewhisperer_codePercentage', - excludeKeys: ['awsAccount'], - }) - assert.strictEqual(data.length, 0) - }) - }) - - describe('emitCodeWhispererCodeContribution', function () { - let tracker: CodeWhispererCodeCoverageTracker | undefined - - beforeEach(async function () { - await resetCodeWhispererGlobalVariables() - tracker = CodeWhispererCodeCoverageTracker.getTracker(language) - if (tracker) { - sinon.stub(tracker, 'isActive').returns(true) - } - }) - - afterEach(function () { - sinon.restore() - CodeWhispererCodeCoverageTracker.instances.clear() - }) - - it('should emit correct code coverage telemetry in python file', async function () { - const tracker = CodeWhispererCodeCoverageTracker.getTracker(language) - - const assertTelemetry = assertTelemetryCurried('codewhisperer_codePercentage') - tracker?.incrementServiceInvocationCount() - tracker?.addAcceptedTokens(`test.py`, { range: new vscode.Range(0, 0, 0, 7), text: `print()`, accepted: 7 }) - tracker?.addTotalTokens(`test.py`, 100) - tracker?.emitCodeWhispererCodeContribution() - assertTelemetry({ - codewhispererTotalTokens: 100, - codewhispererLanguage: language, - codewhispererAcceptedTokens: 7, - codewhispererSuggestedTokens: 7, - codewhispererPercentage: 7, - successCount: 1, - }) - }) - - it('should emit correct code coverage telemetry when success count = 0', async function () { - const tracker = CodeWhispererCodeCoverageTracker.getTracker('java') - - const assertTelemetry = assertTelemetryCurried('codewhisperer_codePercentage') - tracker?.addAcceptedTokens(`test.java`, { - range: new vscode.Range(0, 0, 0, 18), - text: `public static main`, - accepted: 18, - }) - tracker?.incrementServiceInvocationCount() - tracker?.incrementServiceInvocationCount() - tracker?.addTotalTokens(`test.java`, 30) - tracker?.emitCodeWhispererCodeContribution() - assertTelemetry({ - codewhispererTotalTokens: 30, - codewhispererLanguage: 'java', - codewhispererAcceptedTokens: 18, - codewhispererSuggestedTokens: 18, - codewhispererPercentage: 60, - successCount: 2, - }) - }) - }) -}) diff --git a/packages/amazonq/test/unit/codewhisperer/util/bm25.test.ts b/packages/amazonq/test/unit/codewhisperer/util/bm25.test.ts deleted file mode 100644 index 0a3c4b17d60..00000000000 --- a/packages/amazonq/test/unit/codewhisperer/util/bm25.test.ts +++ /dev/null @@ -1,117 +0,0 @@ -/*! - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -import assert from 'assert' -import { BM25Okapi } from 'aws-core-vscode/codewhisperer' - -describe('bm25', function () { - it('simple case 1', function () { - const query = 'windy London' - const corpus = ['Hello there good man!', 'It is quite windy in London', 'How is the weather today?'] - - const sut = new BM25Okapi(corpus) - const actual = sut.score(query) - - assert.deepStrictEqual(actual, [ - { - content: 'Hello there good man!', - index: 0, - score: 0, - }, - { - content: 'It is quite windy in London', - index: 1, - score: 0.937294722506405, - }, - { - content: 'How is the weather today?', - index: 2, - score: 0, - }, - ]) - - assert.deepStrictEqual(sut.topN(query, 1), [ - { - content: 'It is quite windy in London', - index: 1, - score: 0.937294722506405, - }, - ]) - }) - - it('simple case 2', function () { - const query = 'codewhisperer is a machine learning powered code generator' - const corpus = [ - 'codewhisperer goes GA at April 2023', - 'machine learning tool is the trending topic!!! :)', - 'codewhisperer is good =))))', - 'codewhisperer vs. copilot, which code generator better?', - 'copilot is a AI code generator too', - 'it is so amazing!!', - ] - - const sut = new BM25Okapi(corpus) - const actual = sut.score(query) - - assert.deepStrictEqual(actual, [ - { - content: 'codewhisperer goes GA at April 2023', - index: 0, - score: 0, - }, - { - content: 'machine learning tool is the trending topic!!! :)', - index: 1, - score: 2.597224531416621, - }, - { - content: 'codewhisperer is good =))))', - index: 2, - score: 0.3471790843435529, - }, - { - content: 'codewhisperer vs. copilot, which code generator better?', - index: 3, - score: 1.063018436525109, - }, - { - content: 'copilot is a AI code generator too', - index: 4, - score: 2.485359418462239, - }, - { - content: 'it is so amazing!!', - index: 5, - score: 0.3154033715392277, - }, - ]) - - assert.deepStrictEqual(sut.topN(query, 1), [ - { - content: 'machine learning tool is the trending topic!!! :)', - index: 1, - score: 2.597224531416621, - }, - ]) - - assert.deepStrictEqual(sut.topN(query, 3), [ - { - content: 'machine learning tool is the trending topic!!! :)', - index: 1, - score: 2.597224531416621, - }, - { - content: 'copilot is a AI code generator too', - index: 4, - score: 2.485359418462239, - }, - { - content: 'codewhisperer vs. copilot, which code generator better?', - index: 3, - score: 1.063018436525109, - }, - ]) - }) -}) diff --git a/packages/amazonq/test/unit/codewhisperer/util/closingBracketUtil.test.ts b/packages/amazonq/test/unit/codewhisperer/util/closingBracketUtil.test.ts deleted file mode 100644 index bfdf9dc3d29..00000000000 --- a/packages/amazonq/test/unit/codewhisperer/util/closingBracketUtil.test.ts +++ /dev/null @@ -1,389 +0,0 @@ -/*! - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -import * as vscode from 'vscode' -import assert from 'assert' -import { handleExtraBrackets } from 'aws-core-vscode/codewhisperer' -import { toTextEditor } from 'aws-core-vscode/test' - -describe('closingBracketUtil', function () { - /** - * leftContext + recommendation + rightContext - * startStart start end endEnd - */ - describe('handleExtraBrackets', function () { - async function assertClosingSymbolsHandler( - leftContext: string, - rightContext: string, - recommendation: string, - expected: string - ) { - const editor = await toTextEditor(leftContext + recommendation + rightContext, 'test.txt') - const document = editor.document - - const startStart = document.positionAt(0) - const endEnd = document.positionAt(editor.document.getText().length) - const start = document.positionAt(leftContext.length) - const end = document.positionAt(leftContext.length + recommendation.length) - - const left = document.getText(new vscode.Range(startStart, start)) - const right = document.getText(new vscode.Range(end, endEnd)) - const reco = document.getText(new vscode.Range(start, end)) - - assert.strictEqual(left, leftContext) - assert.strictEqual(right, rightContext) - assert.strictEqual(reco, recommendation) - - await handleExtraBrackets(editor, end, start) - - assert.strictEqual(editor.document.getText(), expected) - } - - it('should remove extra closing symbol', async function () { - /** - * public static void mergeSort(int[|] nums) { - * mergeSort(nums, 0, nums.length - 1); - * }|]) - */ - await assertClosingSymbolsHandler( - String.raw`public static void mergeSort(int[`, - String.raw`])`, - String.raw`] nums) { - mergeSort(nums, 0, nums.length - 1); -}`, - String.raw`public static void mergeSort(int[] nums) { - mergeSort(nums, 0, nums.length - 1); -}` - ) - - /** - * fun genericFunction<|T>(value: T): T { - * return value - * }|> - */ - await assertClosingSymbolsHandler( - String.raw`fun genericFunction<`, - String.raw`>`, - String.raw`T>(value: T): T { - return value -}`, - String.raw`fun genericFunction(value: T): T { - return value -}` - ) - - /** - * function getProperty(obj: T, key: K) {|> - */ - await assertClosingSymbolsHandler( - String.raw`function getProperty`, - String.raw`K extends keyof T>(obj: T, key: K) {`, - String.raw`function getProperty(obj: T, key: K) {` - ) - - /** - * public class Main { - * public static void main(|args: String[]) { - * System.out.println("Hello World"); - * }|) - * } - */ - await assertClosingSymbolsHandler( - String.raw`public class Main { - public static void main(`, - String.raw`) -}`, - String.raw`args: String[]) { - System.out.println("Hello World"); - }`, - String.raw`public class Main { - public static void main(args: String[]) { - System.out.println("Hello World"); - } -}` - ) - - /** - * function add2Numbers(a: number: b: number) { - * return a + b - * }) - */ - await assertClosingSymbolsHandler( - 'function add2Numbers(', - ')', - 'a: number, b: number) {\n return a + b\n}', - `function add2Numbers(a: number, b: number) {\n return a + b\n}` - ) - - /** - * function sum(a: number, b: number, c: number) { - * return a + b + c - * }) - */ - await assertClosingSymbolsHandler( - 'function sum(a: number, b: number, ', - ')', - 'c: number) {\n return a + b + c\n}', - `function sum(a: number, b: number, c: number) {\n return a + b + c\n}` - ) - - /** - * const aString = "hello world";" - */ - await assertClosingSymbolsHandler( - 'const aString = "', - '"', - 'hello world";', - `const aString = "hello world";` - ) - - /** - * { - * "userName": "john", - * "department": "codewhisperer"", - * } - */ - await assertClosingSymbolsHandler( - '{\n\t"userName": "john",\n\t"', - '"\n}', - 'department": "codewhisperer",', - '{\n\t"userName": "john",\n\t"department": "codewhisperer",\n}' - ) - - /** - * const someArray = [|"element1", "element2"];|] - */ - await assertClosingSymbolsHandler( - 'const anArray = [', - ']', - '"element1", "element2"];', - `const anArray = ["element1", "element2"];` - ) - - /** - * export const launchTemplates: { [key: string]: AmazonEC2.LaunchTemplate } = { - * lt1: { launchTemplateId: "lt-1", launchTemplateName: "foo" }, - * lt2: { launchTemplateId: "lt-2345", launchTemplateName: "bar" }, - * lt3: { |launchTemplateId: "lt-678919", launchTemplateName: "foobar" },| - * }; - */ - await assertClosingSymbolsHandler( - String.raw`export const launchTemplates: { [key: string]: AmazonEC2.LaunchTemplate } = { - lt1: { launchTemplateId: "lt-1", launchTemplateName: "foo" }, - lt2: { launchTemplateId: "lt-2345", launchTemplateName: "bar" }, - lt3: { `, - String.raw` - };`, - String.raw`launchTemplateId: "lt-678919", launchTemplateName: "foobar" },`, - String.raw`export const launchTemplates: { [key: string]: AmazonEC2.LaunchTemplate } = { - lt1: { launchTemplateId: "lt-1", launchTemplateName: "foo" }, - lt2: { launchTemplateId: "lt-2345", launchTemplateName: "bar" }, - lt3: { launchTemplateId: "lt-678919", launchTemplateName: "foobar" }, - };` - ) - - /** - * genericFunction<|T>|> () { - * if (T isInstanceOf string) { - * console.log(T) - * } else { - * // Do nothing - * } - * } - */ - await assertClosingSymbolsHandler( - String.raw`genericFunction<`, - String.raw`> () { - if (T isInstanceOf string) { - console.log(T) - } else { - // Do nothing - } -}`, - 'T>', - String.raw`genericFunction () { - if (T isInstanceOf string) { - console.log(T) - } else { - // Do nothing - } -}` - ) - - /** - * const rawStr = "|Foo";|" - * const anotherStr = "Bar" - */ - await assertClosingSymbolsHandler( - 'const rawStr = "', - '\nconst anotherStr = "Bar";', - 'Foo";', - String.raw`const rawStr = "Foo"; -const anotherStr = "Bar";` - ) - }) - - it('should not remove extra closing symbol', async function () { - /** - * describe('Foo', () => { - * describe('Bar', function () => { - * it('Boo', |() => { - * expect(true).toBe(true) - * }|) - * }) - * }) - */ - await assertClosingSymbolsHandler( - String.raw`describe('Foo', () => { - describe('Bar', function () { - it('Boo', `, - String.raw`) - }) -})`, - String.raw`() => { - expect(true).toBe(true) - }`, - String.raw`describe('Foo', () => { - describe('Bar', function () { - it('Boo', () => { - expect(true).toBe(true) - }) - }) -})` - ) - - /** - * function add2Numbers(|a: nuumber, b: number) { - * return a + b; - * }| - */ - await assertClosingSymbolsHandler( - 'function add2Numbers(', - '', - 'a: number, b: number) {\n return a + b;\n}', - `function add2Numbers(a: number, b: number) {\n return a + b;\n}` - ) - - /** - * export const launchTemplates: { [key: string]: AmazonEC2.LaunchTemplate } = { - * lt1: { launchTemplateId: "lt-1", launchTemplateName: "foo" }, - * lt2: { launchTemplateId: "lt-2345", launchTemplateName: "bar" }, - * lt3: |{ launchTemplateId: "lt-3456", launchTemplateName: "baz" },| - * } - */ - await assertClosingSymbolsHandler( - 'export const launchTemplates: { [key: string]: AmazonEC2.LaunchTemplate } = {\n lt1: { launchTemplateId: "lt-1", launchTemplateName: "foo" },\n lt2: { launchTemplateId: "lt-2345", launchTemplateName: "bar" },\n lt3: ', - '\n};', - '{ launchTemplateId: "lt-3456", launchTemplateName: "baz" },', - `export const launchTemplates: { [key: string]: AmazonEC2.LaunchTemplate } = {\n lt1: { launchTemplateId: "lt-1", launchTemplateName: "foo" },\n lt2: { launchTemplateId: "lt-2345", launchTemplateName: "bar" },\n lt3: { launchTemplateId: "lt-3456", launchTemplateName: "baz" },\n};` - ) - - /** - * export const launchTemplates: { [key: string]: AmazonEC2.LaunchTemplate } = { - * lt1: { launchTemplateId: "lt-1", launchTemplateName: "foo" }, - * lt2: { launchTemplateId: "lt-2345", launchTemplateName: "bar" }, - * |lt3: { launchTemplateId: "lt-3456", launchTemplateName: "baz" },| - * } - */ - await assertClosingSymbolsHandler( - 'export const launchTemplates: { [key: string]: AmazonEC2.LaunchTemplate } = {\n lt1: { launchTemplateId: "lt-1", launchTemplateName: "foo" },\n lt2: { launchTemplateId: "lt-2345", launchTemplateName: "bar" },\n ', - '\n};', - 'lt3: { launchTemplateId: "lt-3456", launchTemplateName: "baz" },', - 'export const launchTemplates: { [key: string]: AmazonEC2.LaunchTemplate } = {\n lt1: { launchTemplateId: "lt-1", launchTemplateName: "foo" },\n lt2: { launchTemplateId: "lt-2345", launchTemplateName: "bar" },\n lt3: { launchTemplateId: "lt-3456", launchTemplateName: "baz" },\n};' - ) - - /** - * const aString = "|hello world";| - */ - await assertClosingSymbolsHandler( - 'const aString = "', - '', - 'hello world";', - 'const aString = "hello world";' - ) - - /** genericFunction<|T> ()|> { - * if (T isInstanceOf string) { - * console.log(T) - * } else { - * // Do nothing - * } - * } - */ - await assertClosingSymbolsHandler( - 'genericFunction<', - String.raw` { - if (T isInstanceOf string) { - console.log(T) - } else { - // Do nothing - } -}`, - 'T> ()', - String.raw`genericFunction () { - if (T isInstanceOf string) { - console.log(T) - } else { - // Do nothing - } -}` - ) - - /** - * const rawStr = "|Foo";| - * const anotherStr = "Bar" - */ - await assertClosingSymbolsHandler( - 'const rawStr = "', - String.raw` -const anotherStr = "Bar";`, - 'Foo";', - String.raw`const rawStr = "Foo"; -const anotherStr = "Bar";` - ) - - /** - * function shouldReturnAhtmlDiv( { name } : Props) { - * if (!name) { - * return undefined - * } - * - * return ( - *
- * { name } - *
- * |) - * } - */ - await assertClosingSymbolsHandler( - String.raw`function shouldReturnAhtmlDiv( { name } : Props) { - if (!name) { - return undefined - } - - return ( -
- { name } -
`, - String.raw`function shouldReturnAhtmlDiv( { name } : Props) { - if (!name) { - return undefined - } - - return ( -
- { name } -
- ) -}` - ) - }) - }) -}) diff --git a/packages/amazonq/test/unit/codewhisperer/util/codeParsingUtil.test.ts b/packages/amazonq/test/unit/codewhisperer/util/codeParsingUtil.test.ts deleted file mode 100644 index 2a2ad8bb34e..00000000000 --- a/packages/amazonq/test/unit/codewhisperer/util/codeParsingUtil.test.ts +++ /dev/null @@ -1,327 +0,0 @@ -/*! - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -import { - PlatformLanguageId, - extractClasses, - extractFunctions, - isTestFile, - utgLanguageConfigs, -} from 'aws-core-vscode/codewhisperer' -import assert from 'assert' -import { createTestWorkspaceFolder, toTextDocument } from 'aws-core-vscode/test' - -describe('RegexValidationForPython', () => { - it('should extract all function names from a python file content', () => { - // TODO: Replace this variable based testing to read content from File. - // const filePath = vscode.Uri.file('./testData/samplePython.py').fsPath; - // const fileContent = fs.readFileSync('./testData/samplePython.py' , 'utf-8'); - // const regex = /function\s+(\w+)/g; - - const result = extractFunctions(pythonFileContent, utgLanguageConfigs['python'].functionExtractionPattern) - assert.strictEqual(result.length, 13) - assert.deepStrictEqual(result, [ - 'hello_world', - 'add_numbers', - 'multiply_numbers', - 'sum_numbers', - 'divide_numbers', - '__init__', - 'add', - 'multiply', - 'square', - 'from_sum', - '__init__', - 'triple', - 'main', - ]) - }) - - it('should extract all class names from a file content', () => { - const result = extractClasses(pythonFileContent, utgLanguageConfigs['python'].classExtractionPattern) - assert.deepStrictEqual(result, ['Calculator']) - }) -}) - -describe('RegexValidationForJava', () => { - it('should extract all function names from a java file content', () => { - // TODO: Replace this variable based testing to read content from File. - // const filePath = vscode.Uri.file('./testData/samplePython.py').fsPath; - // const fileContent = fs.readFileSync('./testData/samplePython.py' , 'utf-8'); - // const regex = /function\s+(\w+)/g; - - const result = extractFunctions(javaFileContent, utgLanguageConfigs['java'].functionExtractionPattern) - assert.strictEqual(result.length, 5) - assert.deepStrictEqual(result, ['sayHello', 'doSomething', 'square', 'manager', 'ABCFUNCTION']) - }) - - it('should extract all class names from a java file content', () => { - const result = extractClasses(javaFileContent, utgLanguageConfigs['java'].classExtractionPattern) - assert.deepStrictEqual(result, ['Test']) - }) -}) - -describe('isTestFile', () => { - let testWsFolder: string - beforeEach(async function () { - testWsFolder = (await createTestWorkspaceFolder()).uri.fsPath - }) - - it('validate by file path', async function () { - const langs = new Map([ - ['java', '.java'], - ['python', '.py'], - ['typescript', '.ts'], - ['javascript', '.js'], - ['typescriptreact', '.tsx'], - ['javascriptreact', '.jsx'], - ]) - const testFilePathsWithoutExt = [ - '/test/MyClass', - '/test/my_class', - '/tst/MyClass', - '/tst/my_class', - '/tests/MyClass', - '/tests/my_class', - ] - - const srcFilePathsWithoutExt = [ - '/src/MyClass', - 'MyClass', - 'foo/bar/MyClass', - 'foo/my_class', - 'my_class', - 'anyFolderOtherThanTest/foo/myClass', - ] - - for (const [languageId, ext] of langs) { - const testFilePaths = testFilePathsWithoutExt.map((it) => it + ext) - for (const testFilePath of testFilePaths) { - const actual = await isTestFile(testFilePath, { languageId: languageId }) - assert.strictEqual(actual, true) - } - - const srcFilePaths = srcFilePathsWithoutExt.map((it) => it + ext) - for (const srcFilePath of srcFilePaths) { - const actual = await isTestFile(srcFilePath, { languageId: languageId }) - assert.strictEqual(actual, false) - } - } - }) - - async function assertIsTestFile( - fileNames: string[], - config: { languageId: PlatformLanguageId }, - expected: boolean - ) { - for (const fileName of fileNames) { - const document = await toTextDocument('', fileName, testWsFolder) - const actual = await isTestFile(document.uri.fsPath, { languageId: config.languageId }) - assert.strictEqual(actual, expected) - } - } - - it('validate by file name', async function () { - const camelCaseSrc = ['Foo.java', 'Bar.java', 'Baz.java'] - await assertIsTestFile(camelCaseSrc, { languageId: 'java' }, false) - - const camelCaseTst = ['FooTest.java', 'BarTests.java'] - await assertIsTestFile(camelCaseTst, { languageId: 'java' }, true) - - const snakeCaseSrc = ['foo.py', 'bar.py'] - await assertIsTestFile(snakeCaseSrc, { languageId: 'python' }, false) - - const snakeCaseTst = ['test_foo.py', 'bar_test.py'] - await assertIsTestFile(snakeCaseTst, { languageId: 'python' }, true) - - const javascriptSrc = ['Foo.js', 'bar.js'] - await assertIsTestFile(javascriptSrc, { languageId: 'javascript' }, false) - - const javascriptTst = ['Foo.test.js', 'Bar.spec.js'] - await assertIsTestFile(javascriptTst, { languageId: 'javascript' }, true) - - const typescriptSrc = ['Foo.ts', 'bar.ts'] - await assertIsTestFile(typescriptSrc, { languageId: 'typescript' }, false) - - const typescriptTst = ['Foo.test.ts', 'Bar.spec.ts'] - await assertIsTestFile(typescriptTst, { languageId: 'typescript' }, true) - - const jsxSrc = ['Foo.jsx', 'Bar.jsx'] - await assertIsTestFile(jsxSrc, { languageId: 'javascriptreact' }, false) - - const jsxTst = ['Foo.test.jsx', 'Bar.spec.jsx'] - await assertIsTestFile(jsxTst, { languageId: 'javascriptreact' }, true) - }) - - it('should return true if the file name matches the test filename pattern - Java', async () => { - const filePaths = ['/path/to/MyClassTest.java', '/path/to/TestMyClass.java', '/path/to/MyClassTests.java'] - const language = 'java' - - for (const filePath of filePaths) { - const result = await isTestFile(filePath, { languageId: language }) - assert.strictEqual(result, true) - } - }) - - it('should return false if the file name does not match the test filename pattern - Java', async () => { - const filePaths = ['/path/to/MyClass.java', '/path/to/MyClass_test.java', '/path/to/test_MyClass.java'] - const language = 'java' - - for (const filePath of filePaths) { - const result = await isTestFile(filePath, { languageId: language }) - assert.strictEqual(result, false) - } - }) - - it('should return true if the file name does not match the test filename pattern - Python', async () => { - const filePaths = ['/path/to/util_test.py', '/path/to/test_util.py'] - const language = 'python' - - for (const filePath of filePaths) { - const result = await isTestFile(filePath, { languageId: language }) - assert.strictEqual(result, true) - } - }) - - it('should return false if the file name does not match the test filename pattern - Python', async () => { - const filePaths = ['/path/to/util.py', '/path/to/utilTest.java', '/path/to/Testutil.java'] - const language = 'python' - - for (const filePath of filePaths) { - const result = await isTestFile(filePath, { languageId: language }) - assert.strictEqual(result, false) - } - }) - - it('should return false if the language is not supported', async () => { - const filePath = '/path/to/MyClass.cpp' - const language = 'c++' - const result = await isTestFile(filePath, { languageId: language }) - assert.strictEqual(result, false) - }) -}) - -const pythonFileContent = ` -# Single-line import statements -import os -import numpy as np -from typing import List, Tuple - -# Multi-line import statements -from collections import ( - defaultdict, - Counter -) - -# Relative imports -from . import module1 -from ..subpackage import module2 - -# Wildcard imports -from mypackage import * -from mypackage.module import * - -# Aliased imports -import pandas as pd -from mypackage import module1 as m1, module2 as m2 - -def hello_world(): - print("Hello, world!") - -def add_numbers(x, y): - return x + y - -def multiply_numbers(x=1, y=1): - return x * y - -def sum_numbers(*args): - total = 0 - for num in args: - total += num - return total - -def divide_numbers(x, y=1, *args, **kwargs): - result = x / y - for arg in args: - result /= arg - for _, value in kwargs.items(): - result /= value - return result - -class Calculator: - def __init__(self, x, y): - self.x = x - self.y = y - - def add(self): - return self.x + self.y - - def multiply(self): - return self.x * self.y - - @staticmethod - def square(x): - return x ** 2 - - @classmethod - def from_sum(cls, x, y): - return cls(x+y, 0) - - class InnerClass: - def __init__(self, z): - self.z = z - - def triple(self): - return self.z * 3 - -def main(): - print(hello_world()) - print(add_numbers(3, 5)) - print(multiply_numbers(3, 5)) - print(sum_numbers(1, 2, 3, 4, 5)) - print(divide_numbers(10, 2, 5, 2, a=2, b=3)) - - calc = Calculator(3, 5) - print(calc.add()) - print(calc.multiply()) - print(Calculator.square(3)) - print(Calculator.from_sum(2, 3).add()) - - inner = Calculator.InnerClass(5) - print(inner.triple()) - -if __name__ == "__main__": - main() -` - -const javaFileContent = ` -@Annotation -public class Test { - Test() { - // Do something here - } - - //Additional commenting - public static void sayHello() { - System.out.println("Hello, World!"); - } - - private void doSomething(int x, int y) throws Exception { - int z = x + y; - System.out.println("The sum of " + x + " and " + y + " is " + z); - } - - protected static int square(int x) { - return x * x; - } - - private static void manager(int a, int b) { - return a+b; - } - - public int ABCFUNCTION( int ABC, int PQR) { - return ABC + PQR; - } -}` diff --git a/packages/amazonq/test/unit/codewhisperer/util/commonUtil.test.ts b/packages/amazonq/test/unit/codewhisperer/util/commonUtil.test.ts deleted file mode 100644 index 5694b33365d..00000000000 --- a/packages/amazonq/test/unit/codewhisperer/util/commonUtil.test.ts +++ /dev/null @@ -1,81 +0,0 @@ -/*! - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -import assert from 'assert' -import { - JsonConfigFileNamingConvention, - checkLeftContextKeywordsForJson, - getPrefixSuffixOverlap, -} from 'aws-core-vscode/codewhisperer' - -describe('commonUtil', function () { - describe('getPrefixSuffixOverlap', function () { - it('Should return correct overlap', async function () { - assert.strictEqual(getPrefixSuffixOverlap('32rasdgvdsg', 'sg462ydfgbs'), `sg`) - assert.strictEqual(getPrefixSuffixOverlap('32rasdgbreh', 'brehsega'), `breh`) - assert.strictEqual(getPrefixSuffixOverlap('42y24hsd', '42y24hsdzqq23'), `42y24hsd`) - assert.strictEqual(getPrefixSuffixOverlap('ge23yt1', 'ge23yt1'), `ge23yt1`) - assert.strictEqual(getPrefixSuffixOverlap('1sgdbsfbwsergsa', 'a1sgdbsfbwsergs'), `a`) - assert.strictEqual(getPrefixSuffixOverlap('xxa', 'xa'), `xa`) - }) - - it('Should return empty overlap for prefix suffix not matching cases', async function () { - assert.strictEqual(getPrefixSuffixOverlap('1sgdbsfbwsergsa', '1sgdbsfbwsergs'), ``) - assert.strictEqual(getPrefixSuffixOverlap('1sgdbsfbwsergsab', '1sgdbsfbwsergs'), ``) - assert.strictEqual(getPrefixSuffixOverlap('2135t12', 'v2135t12'), ``) - assert.strictEqual(getPrefixSuffixOverlap('2135t12', 'zv2135t12'), ``) - assert.strictEqual(getPrefixSuffixOverlap('xa', 'xxa'), ``) - }) - - it('Should return empty overlap for empty string input', async function () { - assert.strictEqual(getPrefixSuffixOverlap('ergwsghws', ''), ``) - assert.strictEqual(getPrefixSuffixOverlap('', 'asfegw4eh'), ``) - }) - }) - - describe('checkLeftContextKeywordsForJson', function () { - it('Should return true for valid left context keywords', async function () { - assert.strictEqual( - checkLeftContextKeywordsForJson('foo.json', 'Create an S3 Bucket named CodeWhisperer', 'json'), - true - ) - }) - it('Should return false for invalid left context keywords', async function () { - assert.strictEqual( - checkLeftContextKeywordsForJson( - 'foo.json', - 'Create an S3 Bucket named CodeWhisperer in Cloudformation', - 'json' - ), - false - ) - }) - - for (const jsonConfigFile of JsonConfigFileNamingConvention) { - it(`should evalute by filename ${jsonConfigFile}`, function () { - assert.strictEqual(checkLeftContextKeywordsForJson(jsonConfigFile, 'foo', 'json'), false) - - assert.strictEqual(checkLeftContextKeywordsForJson(jsonConfigFile.toUpperCase(), 'bar', 'json'), false) - - assert.strictEqual(checkLeftContextKeywordsForJson(jsonConfigFile.toUpperCase(), 'baz', 'json'), false) - }) - - const upperCaseFilename = jsonConfigFile.toUpperCase() - it(`should evalute by filename and case insensitive ${upperCaseFilename}`, function () { - assert.strictEqual(checkLeftContextKeywordsForJson(upperCaseFilename, 'foo', 'json'), false) - - assert.strictEqual( - checkLeftContextKeywordsForJson(upperCaseFilename.toUpperCase(), 'bar', 'json'), - false - ) - - assert.strictEqual( - checkLeftContextKeywordsForJson(upperCaseFilename.toUpperCase(), 'baz', 'json'), - false - ) - }) - } - }) -}) diff --git a/packages/amazonq/test/unit/codewhisperer/util/crossFileContextUtil.test.ts b/packages/amazonq/test/unit/codewhisperer/util/crossFileContextUtil.test.ts deleted file mode 100644 index 91e26e36111..00000000000 --- a/packages/amazonq/test/unit/codewhisperer/util/crossFileContextUtil.test.ts +++ /dev/null @@ -1,454 +0,0 @@ -/*! - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -import assert from 'assert' -import * as FakeTimers from '@sinonjs/fake-timers' -import * as vscode from 'vscode' -import * as sinon from 'sinon' -import * as crossFile from 'aws-core-vscode/codewhisperer' -import { - aLongStringWithLineCount, - aStringWithLineCount, - createMockTextEditor, - installFakeClock, -} from 'aws-core-vscode/test' -import { FeatureConfigProvider, crossFileContextConfig } from 'aws-core-vscode/codewhisperer' -import { - assertTabCount, - closeAllEditors, - createTestWorkspaceFolder, - toTextEditor, - shuffleList, - toFile, -} from 'aws-core-vscode/test' -import { areEqual, normalize } from 'aws-core-vscode/shared' -import * as path from 'path' -import { LspController } from 'aws-core-vscode/amazonq' - -let tempFolder: string - -describe('crossFileContextUtil', function () { - const fakeCancellationToken: vscode.CancellationToken = { - isCancellationRequested: false, - onCancellationRequested: sinon.spy(), - } - - let mockEditor: vscode.TextEditor - let clock: FakeTimers.InstalledClock - - before(function () { - clock = installFakeClock() - }) - - after(function () { - clock.uninstall() - }) - - afterEach(function () { - sinon.restore() - }) - - describe('fetchSupplementalContextForSrc', function () { - beforeEach(async function () { - tempFolder = (await createTestWorkspaceFolder()).uri.fsPath - }) - - afterEach(async function () { - sinon.restore() - }) - - it.skip('for control group, should return opentabs context where there will be 3 chunks and each chunk should contains 50 lines', async function () { - sinon.stub(FeatureConfigProvider.instance, 'getProjectContextGroup').returns('control') - await toTextEditor(aStringWithLineCount(200), 'CrossFile.java', tempFolder, { preview: false }) - const myCurrentEditor = await toTextEditor('', 'TargetFile.java', tempFolder, { - preview: false, - }) - - await assertTabCount(2) - - const actual = await crossFile.fetchSupplementalContextForSrc(myCurrentEditor, fakeCancellationToken) - assert.ok(actual) - assert.strictEqual(actual.supplementalContextItems.length, 3) - assert.strictEqual(actual.supplementalContextItems[0].content.split('\n').length, 50) - assert.strictEqual(actual.supplementalContextItems[1].content.split('\n').length, 50) - assert.strictEqual(actual.supplementalContextItems[2].content.split('\n').length, 50) - }) - - it('for t1 group, should return repomap + opentabs context, should not exceed 20k total length', async function () { - await toTextEditor(aLongStringWithLineCount(200), 'CrossFile.java', tempFolder, { preview: false }) - const myCurrentEditor = await toTextEditor('', 'TargetFile.java', tempFolder, { - preview: false, - }) - - await assertTabCount(2) - - sinon.stub(FeatureConfigProvider.instance, 'getProjectContextGroup').returns('t1') - sinon - .stub(LspController.instance, 'queryInlineProjectContext') - .withArgs(sinon.match.any, sinon.match.any, 'codemap') - .resolves([ - { - content: 'foo'.repeat(3000), - score: 0, - filePath: 'q-inline', - }, - ]) - - const actual = await crossFile.fetchSupplementalContextForSrc(myCurrentEditor, fakeCancellationToken) - assert.ok(actual) - assert.strictEqual(actual.supplementalContextItems.length, 3) - assert.strictEqual(actual?.strategy, 'codemap') - assert.deepEqual(actual?.supplementalContextItems[0], { - content: 'foo'.repeat(3000), - score: 0, - filePath: 'q-inline', - }) - assert.strictEqual(actual.supplementalContextItems[1].content.split('\n').length, 50) - assert.strictEqual(actual.supplementalContextItems[2].content.split('\n').length, 50) - }) - - it.skip('for t2 group, should return global bm25 context and no repomap', async function () { - await toTextEditor(aStringWithLineCount(200), 'CrossFile.java', tempFolder, { preview: false }) - const myCurrentEditor = await toTextEditor('', 'TargetFile.java', tempFolder, { - preview: false, - }) - - await assertTabCount(2) - - sinon.stub(FeatureConfigProvider.instance, 'getProjectContextGroup').returns('t2') - sinon - .stub(LspController.instance, 'queryInlineProjectContext') - .withArgs(sinon.match.any, sinon.match.any, 'bm25') - .resolves([ - { - content: 'foo', - score: 5, - filePath: 'foo.java', - }, - { - content: 'bar', - score: 4, - filePath: 'bar.java', - }, - { - content: 'baz', - score: 3, - filePath: 'baz.java', - }, - { - content: 'qux', - score: 2, - filePath: 'qux.java', - }, - { - content: 'quux', - score: 1, - filePath: 'quux.java', - }, - ]) - - const actual = await crossFile.fetchSupplementalContextForSrc(myCurrentEditor, fakeCancellationToken) - assert.ok(actual) - assert.strictEqual(actual.supplementalContextItems.length, 5) - assert.strictEqual(actual?.strategy, 'bm25') - - assert.deepEqual(actual?.supplementalContextItems[0], { - content: 'foo', - score: 5, - filePath: 'foo.java', - }) - - assert.deepEqual(actual?.supplementalContextItems[1], { - content: 'bar', - score: 4, - filePath: 'bar.java', - }) - assert.deepEqual(actual?.supplementalContextItems[2], { - content: 'baz', - score: 3, - filePath: 'baz.java', - }) - - assert.deepEqual(actual?.supplementalContextItems[3], { - content: 'qux', - score: 2, - filePath: 'qux.java', - }) - - assert.deepEqual(actual?.supplementalContextItems[4], { - content: 'quux', - score: 1, - filePath: 'quux.java', - }) - }) - }) - - describe('non supported language should return undefined', function () { - it('c++', async function () { - mockEditor = createMockTextEditor('content', 'fileName', 'cpp') - const actual = await crossFile.fetchSupplementalContextForSrc(mockEditor, fakeCancellationToken) - assert.strictEqual(actual, undefined) - }) - - it('ruby', async function () { - mockEditor = createMockTextEditor('content', 'fileName', 'ruby') - - const actual = await crossFile.fetchSupplementalContextForSrc(mockEditor, fakeCancellationToken) - - assert.strictEqual(actual, undefined) - }) - }) - - describe('getCrossFileCandidate', function () { - before(async function () { - this.timeout(60000) - }) - - beforeEach(async function () { - tempFolder = (await createTestWorkspaceFolder()).uri.fsPath - }) - - afterEach(async function () { - await closeAllEditors() - }) - - it('should return opened files, exclude test files and sorted ascendingly by file distance', async function () { - const targetFile = path.join('src', 'service', 'microService', 'CodeWhispererFileContextProvider.java') - const fileWithDistance3 = path.join('src', 'service', 'CodewhispererRecommendationService.java') - const fileWithDistance5 = path.join('src', 'util', 'CodeWhispererConstants.java') - const fileWithDistance6 = path.join('src', 'ui', 'popup', 'CodeWhispererPopupManager.java') - const fileWithDistance7 = path.join('src', 'ui', 'popup', 'components', 'CodeWhispererPopup.java') - const fileWithDistance8 = path.join( - 'src', - 'ui', - 'popup', - 'components', - 'actions', - 'AcceptRecommendationAction.java' - ) - const testFile1 = path.join('test', 'service', 'CodeWhispererFileContextProviderTest.java') - const testFile2 = path.join('test', 'ui', 'CodeWhispererPopupManagerTest.java') - - const expectedFilePaths = [ - fileWithDistance3, - fileWithDistance5, - fileWithDistance6, - fileWithDistance7, - fileWithDistance8, - ] - - const shuffledFilePaths = shuffleList(expectedFilePaths) - - for (const filePath of shuffledFilePaths) { - await toTextEditor('', filePath, tempFolder, { preview: false }) - } - - await toTextEditor('', testFile1, tempFolder, { preview: false }) - await toTextEditor('', testFile2, tempFolder, { preview: false }) - const editor = await toTextEditor('', targetFile, tempFolder, { preview: false }) - - await assertTabCount(shuffledFilePaths.length + 3) - - const actual = await crossFile.getCrossFileCandidates(editor) - - assert.ok(actual.length === 5) - for (const [index, actualFile] of actual.entries()) { - const expectedFile = path.join(tempFolder, expectedFilePaths[index]) - assert.strictEqual(normalize(expectedFile), normalize(actualFile)) - assert.ok(areEqual(tempFolder, actualFile, expectedFile)) - } - }) - }) - - describe.skip('partial support - control group', function () { - const fileExtLists: string[] = [] - - before(async function () { - this.timeout(60000) - }) - - beforeEach(async function () { - tempFolder = (await createTestWorkspaceFolder()).uri.fsPath - }) - - afterEach(async function () { - await closeAllEditors() - }) - - for (const fileExt of fileExtLists) { - it('should be empty if userGroup is control', async function () { - const editor = await toTextEditor('content-1', `file-1.${fileExt}`, tempFolder) - await toTextEditor('content-2', `file-2.${fileExt}`, tempFolder, { preview: false }) - await toTextEditor('content-3', `file-3.${fileExt}`, tempFolder, { preview: false }) - await toTextEditor('content-4', `file-4.${fileExt}`, tempFolder, { preview: false }) - - await assertTabCount(4) - - const actual = await crossFile.fetchSupplementalContextForSrc(editor, fakeCancellationToken) - - assert.ok(actual && actual.supplementalContextItems.length === 0) - }) - } - }) - - describe.skip('partial support - crossfile group', function () { - const fileExtLists: string[] = [] - - before(async function () { - this.timeout(60000) - }) - - beforeEach(async function () { - tempFolder = (await createTestWorkspaceFolder()).uri.fsPath - }) - - afterEach(async function () { - await closeAllEditors() - }) - - for (const fileExt of fileExtLists) { - it('should be non empty if usergroup is Crossfile', async function () { - const editor = await toTextEditor('content-1', `file-1.${fileExt}`, tempFolder) - await toTextEditor('content-2', `file-2.${fileExt}`, tempFolder, { preview: false }) - await toTextEditor('content-3', `file-3.${fileExt}`, tempFolder, { preview: false }) - await toTextEditor('content-4', `file-4.${fileExt}`, tempFolder, { preview: false }) - - await assertTabCount(4) - - const actual = await crossFile.fetchSupplementalContextForSrc(editor, fakeCancellationToken) - - assert.ok(actual && actual.supplementalContextItems.length !== 0) - }) - } - }) - - describe('full support', function () { - const fileExtLists = ['java', 'js', 'ts', 'py', 'tsx', 'jsx'] - - before(async function () { - this.timeout(60000) - }) - - beforeEach(async function () { - tempFolder = (await createTestWorkspaceFolder()).uri.fsPath - }) - - afterEach(async function () { - sinon.restore() - await closeAllEditors() - }) - - for (const fileExt of fileExtLists) { - it(`supplemental context for file ${fileExt} should be non empty`, async function () { - sinon.stub(FeatureConfigProvider.instance, 'getProjectContextGroup').returns('control') - sinon - .stub(LspController.instance, 'queryInlineProjectContext') - .withArgs(sinon.match.any, sinon.match.any, 'codemap') - .resolves([ - { - content: 'foo', - score: 0, - filePath: 'q-inline', - }, - ]) - const editor = await toTextEditor('content-1', `file-1.${fileExt}`, tempFolder) - await toTextEditor('content-2', `file-2.${fileExt}`, tempFolder, { preview: false }) - await toTextEditor('content-3', `file-3.${fileExt}`, tempFolder, { preview: false }) - await toTextEditor('content-4', `file-4.${fileExt}`, tempFolder, { preview: false }) - - await assertTabCount(4) - - const actual = await crossFile.fetchSupplementalContextForSrc(editor, fakeCancellationToken) - - assert.ok(actual && actual.supplementalContextItems.length !== 0) - }) - } - }) - - describe('splitFileToChunks', function () { - beforeEach(async function () { - tempFolder = (await createTestWorkspaceFolder()).uri.fsPath - }) - - it('should split file to a chunk of 2 lines', async function () { - const filePath = path.join(tempFolder, 'file.txt') - await toFile('line_1\nline_2\nline_3\nline_4\nline_5\nline_6\nline_7', filePath) - - const chunks = await crossFile.splitFileToChunks(filePath, 2) - - assert.strictEqual(chunks.length, 4) - assert.strictEqual(chunks[0].content, 'line_1\nline_2') - assert.strictEqual(chunks[1].content, 'line_3\nline_4') - assert.strictEqual(chunks[2].content, 'line_5\nline_6') - assert.strictEqual(chunks[3].content, 'line_7') - }) - - it('should split file to a chunk of 5 lines', async function () { - const filePath = path.join(tempFolder, 'file.txt') - await toFile('line_1\nline_2\nline_3\nline_4\nline_5\nline_6\nline_7', filePath) - - const chunks = await crossFile.splitFileToChunks(filePath, 5) - - assert.strictEqual(chunks.length, 2) - assert.strictEqual(chunks[0].content, 'line_1\nline_2\nline_3\nline_4\nline_5') - assert.strictEqual(chunks[1].content, 'line_6\nline_7') - }) - - it('codewhisperer crossfile config should use 50 lines', async function () { - const filePath = path.join(tempFolder, 'file.txt') - await toFile(aStringWithLineCount(210), filePath) - - const chunks = await crossFile.splitFileToChunks(filePath, crossFileContextConfig.numberOfLinesEachChunk) - - // (210 / 50) + 1 - assert.strictEqual(chunks.length, 5) - // line0 -> line49 - assert.strictEqual(chunks[0].content, aStringWithLineCount(50, 0)) - // line50 -> line99 - assert.strictEqual(chunks[1].content, aStringWithLineCount(50, 50)) - // line100 -> line149 - assert.strictEqual(chunks[2].content, aStringWithLineCount(50, 100)) - // line150 -> line199 - assert.strictEqual(chunks[3].content, aStringWithLineCount(50, 150)) - // line 200 -> line209 - assert.strictEqual(chunks[4].content, aStringWithLineCount(10, 200)) - }) - - it('linkChunks should add another chunk which will link to the first chunk and chunk.nextContent should reflect correct value', async function () { - const filePath = path.join(tempFolder, 'file.txt') - await toFile(aStringWithLineCount(210), filePath) - - const chunks = await crossFile.splitFileToChunks(filePath, crossFileContextConfig.numberOfLinesEachChunk) - const linkedChunks = crossFile.linkChunks(chunks) - - // 210 / 50 + 2 - assert.strictEqual(linkedChunks.length, 6) - - // 0th - assert.strictEqual(linkedChunks[0].content, aStringWithLineCount(3, 0)) - assert.strictEqual(linkedChunks[0].nextContent, aStringWithLineCount(50, 0)) - - // 1st - assert.strictEqual(linkedChunks[1].content, aStringWithLineCount(50, 0)) - assert.strictEqual(linkedChunks[1].nextContent, aStringWithLineCount(50, 50)) - - // 2nd - assert.strictEqual(linkedChunks[2].content, aStringWithLineCount(50, 50)) - assert.strictEqual(linkedChunks[2].nextContent, aStringWithLineCount(50, 100)) - - // 3rd - assert.strictEqual(linkedChunks[3].content, aStringWithLineCount(50, 100)) - assert.strictEqual(linkedChunks[3].nextContent, aStringWithLineCount(50, 150)) - - // 4th - assert.strictEqual(linkedChunks[4].content, aStringWithLineCount(50, 150)) - assert.strictEqual(linkedChunks[4].nextContent, aStringWithLineCount(10, 200)) - - // 5th - assert.strictEqual(linkedChunks[5].content, aStringWithLineCount(10, 200)) - assert.strictEqual(linkedChunks[5].nextContent, aStringWithLineCount(10, 200)) - }) - }) -}) diff --git a/packages/amazonq/test/unit/codewhisperer/util/editorContext.test.ts b/packages/amazonq/test/unit/codewhisperer/util/editorContext.test.ts deleted file mode 100644 index 3875dbbd0f2..00000000000 --- a/packages/amazonq/test/unit/codewhisperer/util/editorContext.test.ts +++ /dev/null @@ -1,392 +0,0 @@ -/*! - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ -import assert from 'assert' -import * as codewhispererClient from 'aws-core-vscode/codewhisperer' -import * as EditorContext from 'aws-core-vscode/codewhisperer' -import { - createMockDocument, - createMockTextEditor, - createMockClientRequest, - resetCodeWhispererGlobalVariables, - toTextEditor, - createTestWorkspaceFolder, - closeAllEditors, -} from 'aws-core-vscode/test' -import { globals } from 'aws-core-vscode/shared' -import { GenerateCompletionsRequest } from 'aws-core-vscode/codewhisperer' -import * as vscode from 'vscode' - -export function createNotebookCell( - document: vscode.TextDocument = createMockDocument('def example():\n return "test"'), - kind: vscode.NotebookCellKind = vscode.NotebookCellKind.Code, - notebook: vscode.NotebookDocument = {} as any, - index: number = 0, - outputs: vscode.NotebookCellOutput[] = [], - metadata: { readonly [key: string]: any } = {}, - executionSummary?: vscode.NotebookCellExecutionSummary -): vscode.NotebookCell { - return { - document, - kind, - notebook, - index, - outputs, - metadata, - executionSummary, - } -} - -describe('editorContext', function () { - let telemetryEnabledDefault: boolean - let tempFolder: string - - beforeEach(async function () { - await resetCodeWhispererGlobalVariables() - telemetryEnabledDefault = globals.telemetry.telemetryEnabled - }) - - afterEach(async function () { - await globals.telemetry.setTelemetryEnabled(telemetryEnabledDefault) - }) - - describe('extractContextForCodeWhisperer', function () { - it('Should return expected context', function () { - const editor = createMockTextEditor('import math\ndef two_sum(nums, target):\n', 'test.py', 'python', 1, 17) - const actual = EditorContext.extractContextForCodeWhisperer(editor) - const expected: codewhispererClient.FileContext = { - fileUri: 'file:///test.py', - filename: 'test.py', - programmingLanguage: { - languageName: 'python', - }, - leftFileContent: 'import math\ndef two_sum(nums,', - rightFileContent: ' target):\n', - } - assert.deepStrictEqual(actual, expected) - }) - - it('Should return expected context within max char limit', function () { - const editor = createMockTextEditor( - 'import math\ndef ' + 'a'.repeat(10340) + 'two_sum(nums, target):\n', - 'test.py', - 'python', - 1, - 17 - ) - const actual = EditorContext.extractContextForCodeWhisperer(editor) - const expected: codewhispererClient.FileContext = { - fileUri: 'file:///test.py', - filename: 'test.py', - programmingLanguage: { - languageName: 'python', - }, - leftFileContent: 'import math\ndef aaaaaaaaaaaaa', - rightFileContent: 'a'.repeat(10240), - } - assert.deepStrictEqual(actual, expected) - }) - - it('in a notebook, includes context from other cells', async function () { - const cells: vscode.NotebookCellData[] = [ - new vscode.NotebookCellData(vscode.NotebookCellKind.Markup, 'Previous cell', 'python'), - new vscode.NotebookCellData( - vscode.NotebookCellKind.Code, - 'import numpy as np\nimport pandas as pd\n\ndef analyze_data(df):\n # Current cell with cursor here', - 'python' - ), - new vscode.NotebookCellData( - vscode.NotebookCellKind.Code, - '# Process the data\nresult = analyze_data(df)\nprint(result)', - 'python' - ), - ] - - const document = await vscode.workspace.openNotebookDocument( - 'jupyter-notebook', - new vscode.NotebookData(cells) - ) - const editor: any = { - document: document.cellAt(1).document, - selection: { active: new vscode.Position(4, 13) }, - } - - const actual = EditorContext.extractContextForCodeWhisperer(editor) - const expected: codewhispererClient.FileContext = { - fileUri: editor.document.uri.toString(), - filename: 'Untitled-1.py', - programmingLanguage: { - languageName: 'python', - }, - leftFileContent: - '# Previous cell\nimport numpy as np\nimport pandas as pd\n\ndef analyze_data(df):\n # Current', - rightFileContent: - ' cell with cursor here\n# Process the data\nresult = analyze_data(df)\nprint(result)\n', - } - assert.deepStrictEqual(actual, expected) - }) - }) - - describe('getFileName', function () { - it('Should return expected filename given a document reading test.py', function () { - const editor = createMockTextEditor('', 'test.py', 'python', 1, 17) - const actual = EditorContext.getFileName(editor) - const expected = 'test.py' - assert.strictEqual(actual, expected) - }) - - it('Should return expected filename for a long filename', async function () { - const editor = createMockTextEditor('', 'a'.repeat(1500), 'python', 1, 17) - const actual = EditorContext.getFileName(editor) - const expected = 'a'.repeat(1024) - assert.strictEqual(actual, expected) - }) - }) - - describe('getFileRelativePath', function () { - this.beforeEach(async function () { - tempFolder = (await createTestWorkspaceFolder()).uri.fsPath - }) - - it('Should return a new filename with correct extension given a .ipynb file', function () { - const languageToExtension = new Map([ - ['python', 'py'], - ['rust', 'rs'], - ['javascript', 'js'], - ['typescript', 'ts'], - ['c', 'c'], - ]) - - for (const [language, extension] of languageToExtension.entries()) { - const editor = createMockTextEditor('', 'test.ipynb', language, 1, 17) - const actual = EditorContext.getFileRelativePath(editor) - const expected = 'test.' + extension - assert.strictEqual(actual, expected) - } - }) - - it('Should return relative path', async function () { - const editor = await toTextEditor('tttt', 'test.py', tempFolder) - const actual = EditorContext.getFileRelativePath(editor) - const expected = 'test.py' - assert.strictEqual(actual, expected) - }) - - afterEach(async function () { - await closeAllEditors() - }) - }) - - describe('getNotebookCellContext', function () { - it('Should return cell text for python code cells when language is python', function () { - const mockCodeCell = createNotebookCell(createMockDocument('def example():\n return "test"')) - const result = EditorContext.getNotebookCellContext(mockCodeCell, 'python') - assert.strictEqual(result, 'def example():\n return "test"') - }) - - it('Should return java comments for python code cells when language is java', function () { - const mockCodeCell = createNotebookCell(createMockDocument('def example():\n return "test"')) - const result = EditorContext.getNotebookCellContext(mockCodeCell, 'java') - assert.strictEqual(result, '// def example():\n// return "test"') - }) - - it('Should return python comments for java code cells when language is python', function () { - const mockCodeCell = createNotebookCell(createMockDocument('println(1 + 1);', 'somefile.ipynb', 'java')) - const result = EditorContext.getNotebookCellContext(mockCodeCell, 'python') - assert.strictEqual(result, '# println(1 + 1);') - }) - - it('Should add python comment prefixes for markdown cells when language is python', function () { - const mockMarkdownCell = createNotebookCell( - createMockDocument('# Heading\nThis is a markdown cell'), - vscode.NotebookCellKind.Markup - ) - const result = EditorContext.getNotebookCellContext(mockMarkdownCell, 'python') - assert.strictEqual(result, '# # Heading\n# This is a markdown cell') - }) - - it('Should add java comment prefixes for markdown cells when language is java', function () { - const mockMarkdownCell = createNotebookCell( - createMockDocument('# Heading\nThis is a markdown cell'), - vscode.NotebookCellKind.Markup - ) - const result = EditorContext.getNotebookCellContext(mockMarkdownCell, 'java') - assert.strictEqual(result, '// # Heading\n// This is a markdown cell') - }) - }) - - describe('getNotebookCellsSliceContext', function () { - it('Should extract content from cells in reverse order up to maxLength from prefix cells', function () { - const mockCells = [ - createNotebookCell(createMockDocument('First cell content')), - createNotebookCell(createMockDocument('Second cell content')), - createNotebookCell(createMockDocument('Third cell content')), - ] - - const result = EditorContext.getNotebookCellsSliceContext(mockCells, 100, 'python', false) - assert.strictEqual(result, 'First cell content\nSecond cell content\nThird cell content\n') - }) - - it('Should extract content from cells in reverse order up to maxLength from suffix cells', function () { - const mockCells = [ - createNotebookCell(createMockDocument('First cell content')), - createNotebookCell(createMockDocument('Second cell content')), - createNotebookCell(createMockDocument('Third cell content')), - ] - - const result = EditorContext.getNotebookCellsSliceContext(mockCells, 100, 'python', true) - assert.strictEqual(result, 'First cell content\nSecond cell content\nThird cell content\n') - }) - - it('Should respect maxLength parameter from prefix cells', function () { - const mockCells = [ - createNotebookCell(createMockDocument('First')), - createNotebookCell(createMockDocument('Second')), - createNotebookCell(createMockDocument('Third')), - createNotebookCell(createMockDocument('Fourth')), - ] - // Should only include part of second cell and the last two cells - const result = EditorContext.getNotebookCellsSliceContext(mockCells, 15, 'python', false) - assert.strictEqual(result, 'd\nThird\nFourth\n') - }) - - it('Should respect maxLength parameter from suffix cells', function () { - const mockCells = [ - createNotebookCell(createMockDocument('First')), - createNotebookCell(createMockDocument('Second')), - createNotebookCell(createMockDocument('Third')), - createNotebookCell(createMockDocument('Fourth')), - ] - - // Should only include first cell and part of second cell - const result = EditorContext.getNotebookCellsSliceContext(mockCells, 15, 'python', true) - assert.strictEqual(result, 'First\nSecond\nTh') - }) - - it('Should handle empty cells array from prefix cells', function () { - const result = EditorContext.getNotebookCellsSliceContext([], 100, 'python', false) - assert.strictEqual(result, '') - }) - - it('Should handle empty cells array from suffix cells', function () { - const result = EditorContext.getNotebookCellsSliceContext([], 100, 'python', true) - assert.strictEqual(result, '') - }) - - it('Should add python comments to markdown prefix cells', function () { - const mockCells = [ - createNotebookCell(createMockDocument('# Heading\nThis is markdown'), vscode.NotebookCellKind.Markup), - createNotebookCell(createMockDocument('def example():\n return "test"')), - ] - const result = EditorContext.getNotebookCellsSliceContext(mockCells, 100, 'python', false) - assert.strictEqual(result, '# # Heading\n# This is markdown\ndef example():\n return "test"\n') - }) - - it('Should add python comments to markdown suffix cells', function () { - const mockCells = [ - createNotebookCell(createMockDocument('# Heading\nThis is markdown'), vscode.NotebookCellKind.Markup), - createNotebookCell(createMockDocument('def example():\n return "test"')), - ] - - const result = EditorContext.getNotebookCellsSliceContext(mockCells, 100, 'python', true) - assert.strictEqual(result, '# # Heading\n# This is markdown\ndef example():\n return "test"\n') - }) - - it('Should add java comments to markdown and python prefix cells when language is java', function () { - const mockCells = [ - createNotebookCell(createMockDocument('# Heading\nThis is markdown'), vscode.NotebookCellKind.Markup), - createNotebookCell(createMockDocument('def example():\n return "test"')), - ] - const result = EditorContext.getNotebookCellsSliceContext(mockCells, 100, 'java', false) - assert.strictEqual(result, '// # Heading\n// This is markdown\n// def example():\n// return "test"\n') - }) - - it('Should add java comments to markdown and python suffix cells when language is java', function () { - const mockCells = [ - createNotebookCell(createMockDocument('# Heading\nThis is markdown'), vscode.NotebookCellKind.Markup), - createNotebookCell(createMockDocument('println(1 + 1);', 'somefile.ipynb', 'java')), - ] - - const result = EditorContext.getNotebookCellsSliceContext(mockCells, 100, 'java', true) - assert.strictEqual(result, '// # Heading\n// This is markdown\nprintln(1 + 1);\n') - }) - - it('Should handle code prefix cells with different languages', function () { - const mockCells = [ - createNotebookCell( - createMockDocument('println(1 + 1);', 'somefile.ipynb', 'java'), - vscode.NotebookCellKind.Code - ), - createNotebookCell(createMockDocument('def example():\n return "test"')), - ] - const result = EditorContext.getNotebookCellsSliceContext(mockCells, 100, 'python', false) - assert.strictEqual(result, '# println(1 + 1);\ndef example():\n return "test"\n') - }) - - it('Should handle code suffix cells with different languages', function () { - const mockCells = [ - createNotebookCell( - createMockDocument('println(1 + 1);', 'somefile.ipynb', 'java'), - vscode.NotebookCellKind.Code - ), - createNotebookCell(createMockDocument('def example():\n return "test"')), - ] - const result = EditorContext.getNotebookCellsSliceContext(mockCells, 100, 'python', true) - assert.strictEqual(result, '# println(1 + 1);\ndef example():\n return "test"\n') - }) - }) - - describe('validateRequest', function () { - it('Should return false if request filename.length is invalid', function () { - const req = createMockClientRequest() - req.fileContext.filename = '' - assert.ok(!EditorContext.validateRequest(req)) - }) - - it('Should return false if request programming language is invalid', function () { - const req = createMockClientRequest() - req.fileContext.programmingLanguage.languageName = '' - assert.ok(!EditorContext.validateRequest(req)) - req.fileContext.programmingLanguage.languageName = 'a'.repeat(200) - assert.ok(!EditorContext.validateRequest(req)) - }) - - it('Should return false if request left or right context exceeds max length', function () { - const req = createMockClientRequest() - req.fileContext.leftFileContent = 'a'.repeat(256000) - assert.ok(!EditorContext.validateRequest(req)) - req.fileContext.leftFileContent = 'a' - req.fileContext.rightFileContent = 'a'.repeat(256000) - assert.ok(!EditorContext.validateRequest(req)) - }) - - it('Should return true if above conditions are not met', function () { - const req = createMockClientRequest() - assert.ok(EditorContext.validateRequest(req)) - }) - }) - - describe('getLeftContext', function () { - it('Should return expected left context', function () { - const editor = createMockTextEditor('import math\ndef two_sum(nums, target):\n', 'test.py', 'python', 1, 17) - const actual = EditorContext.getLeftContext(editor, 1) - const expected = '...wo_sum(nums, target)' - assert.strictEqual(actual, expected) - }) - }) - - describe('buildListRecommendationRequest', function () { - it('Should return expected fields for optOut, nextToken and reference config', async function () { - const nextToken = 'testToken' - const optOutPreference = false - await globals.telemetry.setTelemetryEnabled(false) - const editor = createMockTextEditor('import math\ndef two_sum(nums, target):\n', 'test.py', 'python', 1, 17) - const actual = await EditorContext.buildListRecommendationRequest(editor, nextToken, optOutPreference) - - assert.strictEqual(actual.request.nextToken, nextToken) - assert.strictEqual((actual.request as GenerateCompletionsRequest).optOutPreference, 'OPTOUT') - assert.strictEqual(actual.request.referenceTrackerConfiguration?.recommendationsWithReferences, 'BLOCK') - }) - }) -}) diff --git a/packages/amazonq/test/unit/codewhisperer/util/globalStateUtil.test.ts b/packages/amazonq/test/unit/codewhisperer/util/globalStateUtil.test.ts deleted file mode 100644 index 24062a81b7c..00000000000 --- a/packages/amazonq/test/unit/codewhisperer/util/globalStateUtil.test.ts +++ /dev/null @@ -1,42 +0,0 @@ -/*! - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -import assert from 'assert' -import * as sinon from 'sinon' -import { resetCodeWhispererGlobalVariables } from 'aws-core-vscode/test' -import { getLogger } from 'aws-core-vscode/shared' -import { resetIntelliSenseState, vsCodeState } from 'aws-core-vscode/codewhisperer' - -describe('globalStateUtil', function () { - let loggerSpy: sinon.SinonSpy - - beforeEach(async function () { - await resetCodeWhispererGlobalVariables() - vsCodeState.isIntelliSenseActive = true - loggerSpy = sinon.spy(getLogger(), 'info') - }) - - this.afterEach(function () { - sinon.restore() - }) - - it('Should skip when CodeWhisperer is turned off', async function () { - const isManualTriggerEnabled = false - const isAutomatedTriggerEnabled = false - resetIntelliSenseState(isManualTriggerEnabled, isAutomatedTriggerEnabled, true) - assert.ok(!loggerSpy.called) - }) - - it('Should skip when invocationContext is not active', async function () { - vsCodeState.isIntelliSenseActive = false - resetIntelliSenseState(false, false, true) - assert.ok(!loggerSpy.called) - }) - - it('Should skip when no valid recommendations', async function () { - resetIntelliSenseState(true, true, false) - assert.ok(!loggerSpy.called) - }) -}) diff --git a/packages/amazonq/test/unit/codewhisperer/util/supplemetalContextUtil.test.ts b/packages/amazonq/test/unit/codewhisperer/util/supplemetalContextUtil.test.ts deleted file mode 100644 index a42b0aa6158..00000000000 --- a/packages/amazonq/test/unit/codewhisperer/util/supplemetalContextUtil.test.ts +++ /dev/null @@ -1,265 +0,0 @@ -/*! - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -import assert from 'assert' -import * as FakeTimers from '@sinonjs/fake-timers' -import * as vscode from 'vscode' -import * as sinon from 'sinon' -import * as os from 'os' -import * as crossFile from 'aws-core-vscode/codewhisperer' -import { TestFolder, assertTabCount, installFakeClock } from 'aws-core-vscode/test' -import { CodeWhispererSupplementalContext, FeatureConfigProvider } from 'aws-core-vscode/codewhisperer' -import { toTextEditor } from 'aws-core-vscode/test' -import { LspController } from 'aws-core-vscode/amazonq' - -const newLine = os.EOL - -describe('supplementalContextUtil', function () { - let testFolder: TestFolder - let clock: FakeTimers.InstalledClock - - const fakeCancellationToken: vscode.CancellationToken = { - isCancellationRequested: false, - onCancellationRequested: sinon.spy(), - } - - before(function () { - clock = installFakeClock() - }) - - after(function () { - clock.uninstall() - }) - - beforeEach(async function () { - testFolder = await TestFolder.create() - sinon.stub(FeatureConfigProvider.instance, 'getProjectContextGroup').returns('control') - }) - - afterEach(function () { - sinon.restore() - }) - - describe('fetchSupplementalContext', function () { - describe('openTabsContext', function () { - it('opentabContext should include chunks if non empty', async function () { - sinon - .stub(LspController.instance, 'queryInlineProjectContext') - .withArgs(sinon.match.any, sinon.match.any, 'codemap') - .resolves([ - { - content: 'foo', - score: 0, - filePath: 'q-inline', - }, - ]) - await toTextEditor('class Foo', 'Foo.java', testFolder.path, { preview: false }) - await toTextEditor('class Bar', 'Bar.java', testFolder.path, { preview: false }) - await toTextEditor('class Baz', 'Baz.java', testFolder.path, { preview: false }) - - const editor = await toTextEditor('public class Foo {}', 'Query.java', testFolder.path, { - preview: false, - }) - - await assertTabCount(4) - - const actual = await crossFile.fetchSupplementalContext(editor, fakeCancellationToken) - assert.ok(actual?.supplementalContextItems.length === 4) - }) - - it('opentabsContext should filter out empty chunks', async function () { - // open 3 files as supplemental context candidate files but none of them have contents - await toTextEditor('', 'Foo.java', testFolder.path, { preview: false }) - await toTextEditor('', 'Bar.java', testFolder.path, { preview: false }) - await toTextEditor('', 'Baz.java', testFolder.path, { preview: false }) - - const editor = await toTextEditor('public class Foo {}', 'Query.java', testFolder.path, { - preview: false, - }) - - await assertTabCount(4) - - const actual = await crossFile.fetchSupplementalContext(editor, fakeCancellationToken) - assert.ok(actual?.supplementalContextItems.length === 0) - }) - }) - }) - - describe('truncation', function () { - it('truncate context should do nothing if everything fits in constraint', function () { - const chunkA: crossFile.CodeWhispererSupplementalContextItem = { - content: 'a', - filePath: 'a.java', - score: 0, - } - const chunkB: crossFile.CodeWhispererSupplementalContextItem = { - content: 'b', - filePath: 'b.java', - score: 1, - } - const chunks = [chunkA, chunkB] - - const supplementalContext: CodeWhispererSupplementalContext = { - isUtg: false, - isProcessTimeout: false, - supplementalContextItems: chunks, - contentsLength: 25000, - latency: 0, - strategy: 'codemap', - } - - const actual = crossFile.truncateSuppelementalContext(supplementalContext) - assert.strictEqual(actual.supplementalContextItems.length, 2) - assert.strictEqual(actual.supplementalContextItems[0].content, 'a') - assert.strictEqual(actual.supplementalContextItems[1].content, 'b') - }) - - it('truncateLineByLine should drop the last line if max length is greater than threshold', function () { - const input = - repeatString('a', 11) + - newLine + - repeatString('b', 11) + - newLine + - repeatString('c', 11) + - newLine + - repeatString('d', 11) + - newLine + - repeatString('e', 11) - - assert.ok(input.length > 50) - const actual = crossFile.truncateLineByLine(input, 50) - assert.ok(actual.length <= 50) - - const input2 = repeatString(`b${newLine}`, 10) - const actual2 = crossFile.truncateLineByLine(input2, 8) - assert.ok(actual2.length <= 8) - }) - - it('truncation context should make context length per item lte 10240 cap', function () { - const chunkA: crossFile.CodeWhispererSupplementalContextItem = { - content: repeatString(`a${newLine}`, 4000), - filePath: 'a.java', - score: 0, - } - const chunkB: crossFile.CodeWhispererSupplementalContextItem = { - content: repeatString(`b${newLine}`, 6000), - filePath: 'b.java', - score: 1, - } - const chunkC: crossFile.CodeWhispererSupplementalContextItem = { - content: repeatString(`c${newLine}`, 1000), - filePath: 'c.java', - score: 2, - } - const chunkD: crossFile.CodeWhispererSupplementalContextItem = { - content: repeatString(`d${newLine}`, 1500), - filePath: 'd.java', - score: 3, - } - - assert.ok( - chunkA.content.length + chunkB.content.length + chunkC.content.length + chunkD.content.length > 20480 - ) - - const supplementalContext: CodeWhispererSupplementalContext = { - isUtg: false, - isProcessTimeout: false, - supplementalContextItems: [chunkA, chunkB, chunkC, chunkD], - contentsLength: 25000, - latency: 0, - strategy: 'codemap', - } - - const actual = crossFile.truncateSuppelementalContext(supplementalContext) - assert.strictEqual(actual.supplementalContextItems.length, 3) - assert.ok(actual.contentsLength <= 20480) - assert.strictEqual(actual.strategy, 'codemap') - }) - - it('truncate context should make context items lte 5', function () { - const chunkA: crossFile.CodeWhispererSupplementalContextItem = { - content: 'a', - filePath: 'a.java', - score: 0, - } - const chunkB: crossFile.CodeWhispererSupplementalContextItem = { - content: 'b', - filePath: 'b.java', - score: 1, - } - const chunkC: crossFile.CodeWhispererSupplementalContextItem = { - content: 'c', - filePath: 'c.java', - score: 2, - } - const chunkD: crossFile.CodeWhispererSupplementalContextItem = { - content: 'd', - filePath: 'd.java', - score: 3, - } - const chunkE: crossFile.CodeWhispererSupplementalContextItem = { - content: 'e', - filePath: 'e.java', - score: 4, - } - const chunkF: crossFile.CodeWhispererSupplementalContextItem = { - content: 'f', - filePath: 'f.java', - score: 5, - } - const chunkG: crossFile.CodeWhispererSupplementalContextItem = { - content: 'g', - filePath: 'g.java', - score: 6, - } - const chunks = [chunkA, chunkB, chunkC, chunkD, chunkE, chunkF, chunkG] - - assert.strictEqual(chunks.length, 7) - - const supplementalContext: CodeWhispererSupplementalContext = { - isUtg: false, - isProcessTimeout: false, - supplementalContextItems: chunks, - contentsLength: 25000, - latency: 0, - strategy: 'codemap', - } - - const actual = crossFile.truncateSuppelementalContext(supplementalContext) - assert.strictEqual(actual.supplementalContextItems.length, 5) - }) - - describe('truncate line by line', function () { - it('should return empty if empty string is provided', function () { - const input = '' - const actual = crossFile.truncateLineByLine(input, 50) - assert.strictEqual(actual, '') - }) - - it('should return empty if 0 max length is provided', function () { - const input = 'aaaaa' - const actual = crossFile.truncateLineByLine(input, 0) - assert.strictEqual(actual, '') - }) - - it('should flip the value if negative max length is provided', function () { - const input = `aaaaa${newLine}bbbbb` - const actual = crossFile.truncateLineByLine(input, -6) - const expected = crossFile.truncateLineByLine(input, 6) - assert.strictEqual(actual, expected) - assert.strictEqual(actual, 'aaaaa') - }) - }) - }) -}) - -function repeatString(s: string, n: number): string { - let output = '' - for (let i = 0; i < n; i++) { - output += s - } - - return output -} diff --git a/packages/amazonq/test/unit/codewhisperer/util/utgUtils.test.ts b/packages/amazonq/test/unit/codewhisperer/util/utgUtils.test.ts deleted file mode 100644 index 67359b8a6fc..00000000000 --- a/packages/amazonq/test/unit/codewhisperer/util/utgUtils.test.ts +++ /dev/null @@ -1,63 +0,0 @@ -/*! - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -import assert from 'assert' -import * as utgUtils from 'aws-core-vscode/codewhisperer' - -describe('shouldFetchUtgContext', () => { - it('fully supported language', function () { - assert.ok(utgUtils.shouldFetchUtgContext('java')) - }) - - it('partially supported language', () => { - assert.strictEqual(utgUtils.shouldFetchUtgContext('python'), false) - }) - - it('not supported language', () => { - assert.strictEqual(utgUtils.shouldFetchUtgContext('typescript'), undefined) - - assert.strictEqual(utgUtils.shouldFetchUtgContext('javascript'), undefined) - - assert.strictEqual(utgUtils.shouldFetchUtgContext('javascriptreact'), undefined) - - assert.strictEqual(utgUtils.shouldFetchUtgContext('typescriptreact'), undefined) - - assert.strictEqual(utgUtils.shouldFetchUtgContext('scala'), undefined) - - assert.strictEqual(utgUtils.shouldFetchUtgContext('shellscript'), undefined) - - assert.strictEqual(utgUtils.shouldFetchUtgContext('csharp'), undefined) - - assert.strictEqual(utgUtils.shouldFetchUtgContext('c'), undefined) - }) -}) - -describe('guessSrcFileName', function () { - it('should return undefined if no matching regex', function () { - assert.strictEqual(utgUtils.guessSrcFileName('Foo.java', 'java'), undefined) - assert.strictEqual(utgUtils.guessSrcFileName('folder1/foo.py', 'python'), undefined) - assert.strictEqual(utgUtils.guessSrcFileName('Bar.js', 'javascript'), undefined) - }) - - it('java', function () { - assert.strictEqual(utgUtils.guessSrcFileName('FooTest.java', 'java'), 'Foo.java') - assert.strictEqual(utgUtils.guessSrcFileName('FooTests.java', 'java'), 'Foo.java') - }) - - it('python', function () { - assert.strictEqual(utgUtils.guessSrcFileName('test_foo.py', 'python'), 'foo.py') - assert.strictEqual(utgUtils.guessSrcFileName('foo_test.py', 'python'), 'foo.py') - }) - - it('typescript', function () { - assert.strictEqual(utgUtils.guessSrcFileName('Foo.test.ts', 'typescript'), 'Foo.ts') - assert.strictEqual(utgUtils.guessSrcFileName('Foo.spec.ts', 'typescript'), 'Foo.ts') - }) - - it('javascript', function () { - assert.strictEqual(utgUtils.guessSrcFileName('Foo.test.js', 'javascript'), 'Foo.js') - assert.strictEqual(utgUtils.guessSrcFileName('Foo.spec.js', 'javascript'), 'Foo.js') - }) -}) diff --git a/packages/core/src/amazonq/index.ts b/packages/core/src/amazonq/index.ts index 14c0e4a59a0..3b7737b3547 100644 --- a/packages/core/src/amazonq/index.ts +++ b/packages/core/src/amazonq/index.ts @@ -15,9 +15,6 @@ export { walkthroughInlineSuggestionsExample, walkthroughSecurityScanExample, } from './onboardingPage/walkthrough' -export { LspController } from './lsp/lspController' -export { LspClient } from './lsp/lspClient' -export * as lspClient from './lsp/lspClient' export { api } from './extApi' export { AmazonQChatViewProvider } from './webview/webView' export { amazonQHelpUrl } from '../shared/constants' @@ -40,8 +37,6 @@ export { ExtensionMessage } from '../amazonq/webview/ui/commands' export { CodeReference } from '../codewhispererChat/view/connector/connector' export { extractAuthFollowUp } from './util/authUtils' export { Messenger } from './commons/connector/baseMessenger' -export * from './lsp/config' -export * as WorkspaceLspInstaller from './lsp/workspaceInstaller' export * as secondaryAuth from '../auth/secondaryAuth' export * as authConnection from '../auth/connection' export * as featureConfig from './webview/generators/featureConfig' diff --git a/packages/core/src/amazonq/lsp/config.ts b/packages/core/src/amazonq/lsp/config.ts deleted file mode 100644 index 5670d0d0ce4..00000000000 --- a/packages/core/src/amazonq/lsp/config.ts +++ /dev/null @@ -1,31 +0,0 @@ -/*! - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -import { DevSettings } from '../../shared/settings' -import { getServiceEnvVarConfig } from '../../shared/vscode/env' - -export interface LspConfig { - manifestUrl: string - supportedVersions: string - id: string - suppressPromptPrefix: string - path?: string -} - -export const defaultAmazonQWorkspaceLspConfig: LspConfig = { - manifestUrl: 'https://aws-toolkit-language-servers.amazonaws.com/q-context/manifest.json', - supportedVersions: '0.1.47', - id: 'AmazonQ-Workspace', // used across IDEs for identifying global storage/local disk locations. Do not change. - suppressPromptPrefix: 'amazonQWorkspace', - path: undefined, -} - -export function getAmazonQWorkspaceLspConfig(): LspConfig { - return { - ...defaultAmazonQWorkspaceLspConfig, - ...(DevSettings.instance.getServiceConfig('amazonqWorkspaceLsp', {}) as LspConfig), - ...getServiceEnvVarConfig('amazonqWorkspaceLsp', Object.keys(defaultAmazonQWorkspaceLspConfig)), - } -} diff --git a/packages/core/src/amazonq/lsp/lspClient.ts b/packages/core/src/amazonq/lsp/lspClient.ts deleted file mode 100644 index eba89c961c4..00000000000 --- a/packages/core/src/amazonq/lsp/lspClient.ts +++ /dev/null @@ -1,378 +0,0 @@ -/*! - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -/*! - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - */ -import * as vscode from 'vscode' -import { oneMB } from '../../shared/utilities/processUtils' -import * as path from 'path' -import * as nls from 'vscode-nls' -import * as crypto from 'crypto' -import * as jose from 'jose' - -import { Disposable, ExtensionContext } from 'vscode' - -import { LanguageClient, LanguageClientOptions } from 'vscode-languageclient' -import { - BuildIndexRequestPayload, - BuildIndexRequestType, - GetUsageRequestType, - IndexConfig, - QueryInlineProjectContextRequestType, - QueryVectorIndexRequestType, - UpdateIndexV2RequestPayload, - UpdateIndexV2RequestType, - QueryRepomapIndexRequestType, - GetRepomapIndexJSONRequestType, - Usage, - GetContextCommandItemsRequestType, - ContextCommandItem, - GetIndexSequenceNumberRequestType, - GetContextCommandPromptRequestType, - AdditionalContextPrompt, -} from './types' -import { CodeWhispererSettings } from '../../codewhisperer/util/codewhispererSettings' -import { fs } from '../../shared/fs/fs' -import { getLogger } from '../../shared/logger/logger' -import globals from '../../shared/extensionGlobals' -import { ResourcePaths } from '../../shared/lsp/types' -import { createServerOptions, validateNodeExe } from '../../shared/lsp/utils/platform' -import { waitUntil } from '../../shared/utilities/timeoutUtils' - -const localize = nls.loadMessageBundle() - -const key = crypto.randomBytes(32) -const logger = getLogger('amazonqWorkspaceLsp') - -/** - * LspClient manages the API call between VS Code extension and LSP server - * It encryptes the payload of API call. - */ -export class LspClient { - static #instance: LspClient - client: LanguageClient | undefined - - public static get instance() { - return (this.#instance ??= new this()) - } - - constructor() { - this.client = undefined - } - - async encrypt(payload: string) { - return await new jose.CompactEncrypt(new TextEncoder().encode(payload)) - .setProtectedHeader({ alg: 'dir', enc: 'A256GCM' }) - .encrypt(key) - } - - async buildIndex(paths: string[], rootPath: string, config: IndexConfig) { - const payload: BuildIndexRequestPayload = { - filePaths: paths, - projectRoot: rootPath, - config: config, - language: '', - } - try { - const encryptedRequest = await this.encrypt(JSON.stringify(payload)) - const resp = await this.client?.sendRequest(BuildIndexRequestType, encryptedRequest) - return resp - } catch (e) { - logger.error(`buildIndex error: ${e}`) - return undefined - } - } - - async queryVectorIndex(request: string) { - try { - const encryptedRequest = await this.encrypt( - JSON.stringify({ - query: request, - }) - ) - const resp = await this.client?.sendRequest(QueryVectorIndexRequestType, encryptedRequest) - return resp - } catch (e) { - logger.error(`queryVectorIndex error: ${e}`) - return [] - } - } - - async queryInlineProjectContext(query: string, path: string, target: 'default' | 'codemap' | 'bm25') { - try { - const request = JSON.stringify({ - query: query, - filePath: path, - target, - }) - const encrypted = await this.encrypt(request) - const resp: any = await this.client?.sendRequest(QueryInlineProjectContextRequestType, encrypted) - return resp - } catch (e) { - logger.error(`queryInlineProjectContext error: ${e}`) - throw e - } - } - - async getLspServerUsage(): Promise { - if (this.client) { - return (await this.client.sendRequest(GetUsageRequestType, '')) as Usage - } - } - - async updateIndex(filePath: string[], mode: 'update' | 'remove' | 'add' | 'context_command_symbol_update') { - const payload: UpdateIndexV2RequestPayload = { - filePaths: filePath, - updateMode: mode, - } - try { - const encryptedRequest = await this.encrypt(JSON.stringify(payload)) - const resp = await this.client?.sendRequest(UpdateIndexV2RequestType, encryptedRequest) - return resp - } catch (e) { - logger.error(`updateIndex error: ${e}`) - return undefined - } - } - async queryRepomapIndex(filePaths: string[]) { - try { - const request = JSON.stringify({ - filePaths: filePaths, - }) - const resp: any = await this.client?.sendRequest(QueryRepomapIndexRequestType, await this.encrypt(request)) - return resp - } catch (e) { - logger.error(`QueryRepomapIndex error: ${e}`) - throw e - } - } - async getRepoMapJSON() { - try { - const request = JSON.stringify({}) - const resp: any = await this.client?.sendRequest( - GetRepomapIndexJSONRequestType, - await this.encrypt(request) - ) - return resp - } catch (e) { - logger.error(`queryInlineProjectContext error: ${e}`) - throw e - } - } - - async getContextCommandItems(): Promise { - try { - const workspaceFolders = vscode.workspace.workspaceFolders || [] - const request = JSON.stringify({ - workspaceFolders: workspaceFolders.map((it) => it.uri.fsPath), - }) - const resp: any = await this.client?.sendRequest( - GetContextCommandItemsRequestType, - await this.encrypt(request) - ) - return resp - } catch (e) { - logger.error(`getContextCommandItems error: ${e}`) - throw e - } - } - - async getContextCommandPrompt(contextCommandItems: ContextCommandItem[]): Promise { - try { - const request = JSON.stringify({ - contextCommands: contextCommandItems, - }) - const resp: any = await this.client?.sendRequest( - GetContextCommandPromptRequestType, - await this.encrypt(request) - ) - return resp || [] - } catch (e) { - logger.error(`getContextCommandPrompt error: ${e}`) - throw e - } - } - - async getIndexSequenceNumber(): Promise { - try { - const request = JSON.stringify({}) - const resp: any = await this.client?.sendRequest( - GetIndexSequenceNumberRequestType, - await this.encrypt(request) - ) - return resp - } catch (e) { - logger.error(`getIndexSequenceNumber error: ${e}`) - throw e - } - } - - async waitUntilReady() { - return waitUntil( - async () => { - if (this.client === undefined) { - return false - } - await this.client.onReady() - return true - }, - { interval: 500, timeout: 60_000 * 3, truthy: true } - ) - } -} - -/** - * Activates the language server (assumes the LSP server has already been downloaded): - * 1. start LSP server running over IPC protocol. - * 2. create a output channel named Amazon Q Language Server. - */ -export async function activate(extensionContext: ExtensionContext, resourcePaths: ResourcePaths) { - LspClient.instance // Tickle the singleton... :/ - const toDispose = extensionContext.subscriptions - - let rangeFormatting: Disposable | undefined - // The debug options for the server - // --inspect=6009: runs the server in Node's Inspector mode so VS Code can attach to the server for debugging - const debugOptions = { execArgv: ['--nolazy', '--preserve-symlinks', '--stdio'] } - const workerThreads = CodeWhispererSettings.instance.getIndexWorkerThreads() - const gpu = CodeWhispererSettings.instance.isLocalIndexGPUEnabled() - - if (gpu) { - process.env.Q_ENABLE_GPU = 'true' - } else { - delete process.env.Q_ENABLE_GPU - } - if (workerThreads > 0 && workerThreads < 100) { - process.env.Q_WORKER_THREADS = workerThreads.toString() - } else { - delete process.env.Q_WORKER_THREADS - } - - const serverModule = resourcePaths.lsp - const memoryWarnThreshold = 800 * oneMB - - const serverOptions = createServerOptions({ - encryptionKey: key, - executable: [resourcePaths.node], - serverModule, - // TODO(jmkeyes): we always use the debug options...? - execArgv: debugOptions.execArgv, - warnThresholds: { memory: memoryWarnThreshold }, - }) - - const documentSelector = [{ scheme: 'file', language: '*' }] - - await validateNodeExe([resourcePaths.node], resourcePaths.lsp, debugOptions.execArgv, logger) - - // Options to control the language client - const clientOptions: LanguageClientOptions = { - // Register the server for json documents - documentSelector, - initializationOptions: { - handledSchemaProtocols: ['file', 'untitled'], // language server only loads file-URI. Fetching schemas with other protocols ('http'...) are made on the client. - provideFormatter: false, // tell the server to not provide formatting capability and ignore the `aws.stepfunctions.asl.format.enable` setting. - // this is used by LSP to determine index cache path, move to this folder so that when extension updates index is not deleted. - extensionPath: path.join(fs.getUserHomeDir(), '.aws', 'amazonq', 'cache'), - }, - // Log to the Amazon Q Logs so everything is in a single channel - // TODO: Add prefix to the language server logs so it is easier to search - outputChannel: globals.logOutputChannel, - } - - // Create the language client and start the client. - LspClient.instance.client = new LanguageClient( - 'amazonq', - localize('amazonq.server.name', 'Amazon Q Language Server'), - serverOptions, - clientOptions - ) - LspClient.instance.client.registerProposedFeatures() - - const disposable = LspClient.instance.client.start() - toDispose.push(disposable) - - let savedDocument: vscode.Uri | undefined = undefined - - const onAdd = async (filePaths: string[]) => { - const indexSeqNum = await LspClient.instance.getIndexSequenceNumber() - await LspClient.instance.updateIndex(filePaths, 'add') - await waitUntil( - async () => { - const newIndexSeqNum = await LspClient.instance.getIndexSequenceNumber() - if (newIndexSeqNum > indexSeqNum) { - await vscode.commands.executeCommand(`aws.amazonq.updateContextCommandItems`) - return true - } - return false - }, - { interval: 500, timeout: 5_000, truthy: true } - ) - } - const onRemove = async (filePaths: string[]) => { - const indexSeqNum = await LspClient.instance.getIndexSequenceNumber() - await LspClient.instance.updateIndex(filePaths, 'remove') - await waitUntil( - async () => { - const newIndexSeqNum = await LspClient.instance.getIndexSequenceNumber() - if (newIndexSeqNum > indexSeqNum) { - await vscode.commands.executeCommand(`aws.amazonq.updateContextCommandItems`) - return true - } - return false - }, - { interval: 500, timeout: 5_000, truthy: true } - ) - } - - toDispose.push( - vscode.workspace.onDidSaveTextDocument((document) => { - if (document.uri.scheme !== 'file') { - return - } - savedDocument = document.uri - }), - vscode.window.onDidChangeActiveTextEditor((editor) => { - if (savedDocument && editor && editor.document.uri.fsPath !== savedDocument.fsPath) { - void LspClient.instance.updateIndex([savedDocument.fsPath], 'update') - } - // user created a new empty file using File -> New File - // these events will not be captured by vscode.workspace.onDidCreateFiles - // because it was created by File Explorer(Win) or Finder(MacOS) - // TODO: consider using a high performance fs watcher - if (editor?.document.getText().length === 0) { - void onAdd([editor.document.uri.fsPath]) - } - }), - vscode.workspace.onDidCreateFiles(async (e) => { - await onAdd(e.files.map((f) => f.fsPath)) - }), - vscode.workspace.onDidDeleteFiles(async (e) => { - await onRemove(e.files.map((f) => f.fsPath)) - }), - vscode.workspace.onDidRenameFiles(async (e) => { - await onRemove(e.files.map((f) => f.oldUri.fsPath)) - await onAdd(e.files.map((f) => f.newUri.fsPath)) - }) - ) - - return LspClient.instance.client.onReady().then( - () => { - const disposableFunc = { dispose: () => rangeFormatting?.dispose() as void } - toDispose.push(disposableFunc) - }, - (reason) => { - logger.error('client.onReady() failed: %O', reason) - } - ) -} - -export async function deactivate(): Promise { - if (!LspClient.instance.client) { - return undefined - } - return LspClient.instance.client.stop() -} diff --git a/packages/core/src/amazonq/lsp/lspController.ts b/packages/core/src/amazonq/lsp/lspController.ts deleted file mode 100644 index 5a1b84b7c49..00000000000 --- a/packages/core/src/amazonq/lsp/lspController.ts +++ /dev/null @@ -1,237 +0,0 @@ -/*! - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -import * as vscode from 'vscode' -import * as path from 'path' -import { getLogger } from '../../shared/logger/logger' -import { CurrentWsFolders, collectFilesForIndex } from '../../shared/utilities/workspaceUtils' -import { activate as activateLsp, LspClient } from './lspClient' -import { telemetry } from '../../shared/telemetry/telemetry' -import { isCloud9 } from '../../shared/extensionUtilities' -import globals, { isWeb } from '../../shared/extensionGlobals' -import { isAmazonLinux2 } from '../../shared/vscode/env' -import { WorkspaceLspInstaller } from './workspaceInstaller' -import { lspSetupStage } from '../../shared/lsp/utils/setupStage' -import { RelevantTextDocumentAddition } from '../../codewhispererChat/controllers/chat/model' -import { waitUntil } from '../../shared/utilities/timeoutUtils' - -export interface Chunk { - readonly filePath: string - readonly content: string - readonly context?: string - readonly relativePath?: string - readonly programmingLanguage?: string - readonly startLine?: number - readonly endLine?: number -} -export interface BuildIndexConfig { - startUrl?: string - maxIndexSize: number - isVectorIndexEnabled: boolean -} - -/* - * LSP Controller manages the status of Amazon Q Workspace Indexing LSP: - * 1. Downloading, verifying and installing LSP using DEXP LSP manifest and CDN. - * 2. Managing the LSP states. There are a couple of possible LSP states: - * Not installed. Installed. Running. Indexing. Indexing Done. - * LSP Controller converts the input and output of LSP APIs. - * The IDE extension code should invoke LSP API via this controller. - * 3. It perform pre-process and post process of LSP APIs - * Pre-process the input to Index Files API - * Post-process the output from Query API - */ -export class LspController { - static #instance: LspController - private _isIndexingInProgress = false - private _contextCommandSymbolsUpdated = false - private logger = getLogger('amazonqWorkspaceLsp') - - public static get instance() { - return (this.#instance ??= new this()) - } - - isIndexingInProgress() { - return this._isIndexingInProgress - } - - async query(s: string): Promise { - const chunks: Chunk[] | undefined = await LspClient.instance.queryVectorIndex(s) - const resp: RelevantTextDocumentAddition[] = [] - if (chunks) { - for (const chunk of chunks) { - const text = chunk.context ? chunk.context : chunk.content - if (chunk.programmingLanguage && chunk.programmingLanguage !== 'unknown') { - resp.push({ - text: text, - relativeFilePath: chunk.relativePath ? chunk.relativePath : path.basename(chunk.filePath), - programmingLanguage: { - languageName: chunk.programmingLanguage, - }, - startLine: chunk.startLine ?? -1, - endLine: chunk.endLine ?? -1, - }) - } else { - resp.push({ - text: text, - relativeFilePath: chunk.relativePath ? chunk.relativePath : path.basename(chunk.filePath), - startLine: chunk.startLine ?? -1, - endLine: chunk.endLine ?? -1, - }) - } - } - } - return resp - } - - async queryInlineProjectContext(query: string, path: string, target: 'bm25' | 'codemap' | 'default') { - try { - return await LspClient.instance.queryInlineProjectContext(query, path, target) - } catch (e) { - if (e instanceof Error) { - this.logger.error(`unexpected error while querying inline project context, e=${e.message}`) - } - return [] - } - } - - async buildIndex(buildIndexConfig: BuildIndexConfig) { - this.logger.info(`Starting to build index of project`) - const start = performance.now() - const projPaths = (vscode.workspace.workspaceFolders ?? []).map((folder) => folder.uri.fsPath) - if (projPaths.length === 0) { - this.logger.info(`Skipping building index. No projects found in workspace`) - return - } - projPaths.sort() - try { - this._isIndexingInProgress = true - const projRoot = projPaths[0] - const files = await collectFilesForIndex( - projPaths, - vscode.workspace.workspaceFolders as CurrentWsFolders, - true, - buildIndexConfig.maxIndexSize * 1024 * 1024 - ) - const totalSizeBytes = files.reduce( - (accumulator, currentFile) => accumulator + currentFile.fileSizeBytes, - 0 - ) - this.logger.info(`Found ${files.length} files in current project ${projPaths}`) - const config = buildIndexConfig.isVectorIndexEnabled ? 'all' : 'default' - const r = files.map((f) => f.fileUri.fsPath) - const resp = await LspClient.instance.buildIndex(r, projRoot, config) - if (resp) { - this.logger.debug(`Finish building index of project`) - const usage = await LspClient.instance.getLspServerUsage() - telemetry.amazonq_indexWorkspace.emit({ - duration: performance.now() - start, - result: 'Succeeded', - amazonqIndexFileCount: files.length, - amazonqIndexMemoryUsageInMB: usage ? usage.memoryUsage / (1024 * 1024) : undefined, - amazonqIndexCpuUsagePercentage: usage ? usage.cpuUsage : undefined, - amazonqIndexFileSizeInMB: totalSizeBytes / (1024 * 1024), - amazonqVectorIndexEnabled: buildIndexConfig.isVectorIndexEnabled, - credentialStartUrl: buildIndexConfig.startUrl, - }) - } else { - this.logger.error(`Failed to build index of project`) - telemetry.amazonq_indexWorkspace.emit({ - duration: performance.now() - start, - result: 'Failed', - amazonqIndexFileCount: 0, - amazonqIndexFileSizeInMB: 0, - amazonqVectorIndexEnabled: buildIndexConfig.isVectorIndexEnabled, - reason: `Unknown`, - }) - } - } catch (error) { - // TODO: use telemetry.run() - this.logger.error(`Failed to build index of project`) - telemetry.amazonq_indexWorkspace.emit({ - duration: performance.now() - start, - result: 'Failed', - amazonqIndexFileCount: 0, - amazonqIndexFileSizeInMB: 0, - amazonqVectorIndexEnabled: buildIndexConfig.isVectorIndexEnabled, - reason: `${error instanceof Error ? error.name : 'Unknown'}`, - reasonDesc: `Error when building index. ${error instanceof Error ? error.message : error}`, - }) - } finally { - this._isIndexingInProgress = false - } - } - - async trySetupLsp(context: vscode.ExtensionContext, buildIndexConfig: BuildIndexConfig) { - if (isCloud9() || isWeb() || isAmazonLinux2()) { - this.logger.warn('Skipping LSP setup. LSP is not compatible with the current environment. ') - // do not do anything if in Cloud9 or Web mode or in AL2 (AL2 does not support node v18+) - return - } - setImmediate(async () => { - try { - await this.setupLsp(context) - await vscode.commands.executeCommand(`aws.amazonq.updateContextCommandItems`) - void LspController.instance.buildIndex(buildIndexConfig) - // log the LSP server CPU and Memory usage per 30 minutes. - globals.clock.setInterval( - async () => { - const usage = await LspClient.instance.getLspServerUsage() - if (usage) { - this.logger.info( - `LSP server CPU ${usage.cpuUsage}%, LSP server Memory ${ - usage.memoryUsage / (1024 * 1024) - }MB ` - ) - } - }, - 30 * 60 * 1000 - ) - } catch (e) { - this.logger.error(`LSP failed to activate ${e}`) - } - }) - } - /** - * Updates context command symbols once per session by synchronizing with the LSP client index. - * Context menu will contain file and folders to begin with, - * then this asynchronous function should be invoked after the files and folders are found - * the LSP then further starts to parse workspace and find symbols, which takes - * anywhere from 5 seconds to about 40 seconds, depending on project size. - * @returns {Promise} - */ - async updateContextCommandSymbolsOnce() { - if (this._contextCommandSymbolsUpdated) { - return - } - this._contextCommandSymbolsUpdated = true - getLogger().debug(`Start adding symbols to context picker menu`) - try { - const indexSeqNum = await LspClient.instance.getIndexSequenceNumber() - await LspClient.instance.updateIndex([], 'context_command_symbol_update') - await waitUntil( - async () => { - const newIndexSeqNum = await LspClient.instance.getIndexSequenceNumber() - if (newIndexSeqNum > indexSeqNum) { - await vscode.commands.executeCommand(`aws.amazonq.updateContextCommandItems`) - return true - } - return false - }, - { interval: 1000, timeout: 60_000, truthy: true } - ) - } catch (err) { - this.logger.error(`Failed to find symbols`) - } - } - - private async setupLsp(context: vscode.ExtensionContext) { - await lspSetupStage('all', async () => { - const installResult = await new WorkspaceLspInstaller().resolve() - await lspSetupStage('launch', async () => activateLsp(context, installResult.resourcePaths)) - this.logger.info('LSP activated') - }) - } -} diff --git a/packages/core/src/amazonq/lsp/types.ts b/packages/core/src/amazonq/lsp/types.ts deleted file mode 100644 index 2940ce240c8..00000000000 --- a/packages/core/src/amazonq/lsp/types.ts +++ /dev/null @@ -1,150 +0,0 @@ -/*! - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -import { RequestType } from 'vscode-languageserver' - -export type IndexRequestPayload = { - filePaths: string[] - rootPath: string - refresh: boolean -} - -export type ClearRequest = string - -export const ClearRequestType: RequestType = new RequestType('lsp/clear') - -export type QueryRequest = string - -export const QueryRequestType: RequestType = new RequestType('lsp/query') - -export type GetUsageRequest = string - -export const GetUsageRequestType: RequestType = new RequestType('lsp/getUsage') - -export interface Usage { - memoryUsage: number - cpuUsage: number -} - -export type BuildIndexRequestPayload = { - filePaths: string[] - projectRoot: string - config: string - language: string -} - -export type BuildIndexRequest = string - -export const BuildIndexRequestType: RequestType = new RequestType('lsp/buildIndex') - -export type UpdateIndexV2Request = string - -export type UpdateIndexV2RequestPayload = { filePaths: string[]; updateMode: string } - -export const UpdateIndexV2RequestType: RequestType = new RequestType( - 'lsp/updateIndexV2' -) - -export type QueryInlineProjectContextRequest = string -export type QueryInlineProjectContextRequestPayload = { - query: string - filePath: string - target: string -} -export const QueryInlineProjectContextRequestType: RequestType = - new RequestType('lsp/queryInlineProjectContext') - -export type QueryVectorIndexRequestPayload = { query: string } - -export type QueryVectorIndexRequest = string - -export const QueryVectorIndexRequestType: RequestType = new RequestType( - 'lsp/queryVectorIndex' -) - -export type IndexConfig = 'all' | 'default' - -// RepoMapData -export type QueryRepomapIndexRequestPayload = { filePaths: string[] } -export type QueryRepomapIndexRequest = string -export const QueryRepomapIndexRequestType: RequestType = new RequestType( - 'lsp/queryRepomapIndex' -) -export type GetRepomapIndexJSONRequest = string -export const GetRepomapIndexJSONRequestType: RequestType = new RequestType( - 'lsp/getRepomapIndexJSON' -) - -export type GetContextCommandItemsRequestPayload = { workspaceFolders: string[] } -export type GetContextCommandItemsRequest = string -export const GetContextCommandItemsRequestType: RequestType = new RequestType( - 'lsp/getContextCommandItems' -) - -export type GetIndexSequenceNumberRequest = string -export const GetIndexSequenceNumberRequestType: RequestType = new RequestType( - 'lsp/getIndexSequenceNumber' -) - -export type ContextCommandItemType = 'file' | 'folder' | 'code' - -export type SymbolType = - | 'Class' - | 'Function' - | 'Interface' - | 'Type' - | 'Enum' - | 'Struct' - | 'Delegate' - | 'Namespace' - | 'Object' - | 'Module' - | 'Method' - -export interface Position { - line: number - column: number -} -export interface Span { - start: Position - end: Position -} - -// LSP definition of DocumentSymbol - -export interface DocumentSymbol { - name: string - kind: SymbolType - range: Span -} - -export interface ContextCommandItem { - workspaceFolder: string - type: ContextCommandItemType - relativePath: string - symbol?: DocumentSymbol - id?: string -} - -export type GetContextCommandPromptRequestPayload = { - contextCommands: { - workspaceFolder: string - type: 'file' | 'folder' - relativePath: string - }[] -} -export type GetContextCommandPromptRequest = string -export const GetContextCommandPromptRequestType: RequestType = - new RequestType('lsp/getContextCommandPrompt') - -export interface AdditionalContextPrompt { - content: string - name: string - description: string - startLine: number - endLine: number - filePath: string - relativePath: string -} diff --git a/packages/core/src/amazonq/lsp/workspaceInstaller.ts b/packages/core/src/amazonq/lsp/workspaceInstaller.ts deleted file mode 100644 index 99e70f20cbf..00000000000 --- a/packages/core/src/amazonq/lsp/workspaceInstaller.ts +++ /dev/null @@ -1,39 +0,0 @@ -/*! - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -import path from 'path' -import { ResourcePaths } from '../../shared/lsp/types' -import { getNodeExecutableName } from '../../shared/lsp/utils/platform' -import { fs } from '../../shared/fs/fs' -import { BaseLspInstaller } from '../../shared/lsp/baseLspInstaller' -import { getAmazonQWorkspaceLspConfig, LspConfig } from './config' - -export class WorkspaceLspInstaller extends BaseLspInstaller { - constructor(lspConfig: LspConfig = getAmazonQWorkspaceLspConfig()) { - super(lspConfig, 'amazonqWorkspaceLsp') - } - - protected override async postInstall(assetDirectory: string): Promise { - const resourcePaths = this.resourcePaths(assetDirectory) - await fs.chmod(resourcePaths.node, 0o755) - } - - protected override resourcePaths(assetDirectory?: string): ResourcePaths { - // local version - if (!assetDirectory) { - return { - lsp: this.config.path ?? '', - node: getNodeExecutableName(), - } - } - - const lspNodeName = - process.platform === 'win32' ? getNodeExecutableName() : `node-${process.platform}-${process.arch}` - return { - lsp: path.join(assetDirectory, `qserver-${process.platform}-${process.arch}/qserver/lspServer.js`), - node: path.join(assetDirectory, lspNodeName), - } - } -} diff --git a/packages/core/src/codewhisperer/activation.ts b/packages/core/src/codewhisperer/activation.ts index e52e08bb98b..d6dd7fdc61d 100644 --- a/packages/core/src/codewhisperer/activation.ts +++ b/packages/core/src/codewhisperer/activation.ts @@ -5,8 +5,6 @@ import * as vscode from 'vscode' import * as nls from 'vscode-nls' -import { getTabSizeSetting } from '../shared/utilities/editorUtilities' -import * as EditorContext from './util/editorContext' import * as CodeWhispererConstants from './models/constants' import { CodeSuggestionsState, @@ -16,7 +14,6 @@ import { CodeScanIssue, CodeIssueGroupingStrategyState, } from './models/model' -import { acceptSuggestion } from './commands/onInlineAcceptance' import { CodeWhispererSettings } from './util/codewhispererSettings' import { ExtContext } from '../shared/extensions' import { CodeWhispererTracker } from './tracker/codewhispererTracker' @@ -64,20 +61,16 @@ import { updateSecurityDiagnosticCollection, } from './service/diagnosticsProvider' import { SecurityPanelViewProvider, openEditorAtRange } from './views/securityPanelViewProvider' -import { RecommendationHandler } from './service/recommendationHandler' import { Commands, registerCommandErrorHandler, registerDeclaredCommands } from '../shared/vscode/commands2' -import { InlineCompletionService, refreshStatusBar } from './service/inlineCompletionService' -import { isInlineCompletionEnabled } from './util/commonUtil' +import { refreshStatusBar } from './service/statusBar' import { AuthUtil } from './util/authUtil' import { ImportAdderProvider } from './service/importAdderProvider' -import { TelemetryHelper } from './util/telemetryHelper' import { openUrl } from '../shared/utilities/vsCodeUtils' import { notifyNewCustomizations, onProfileChangedListener } from './util/customizationUtil' import { CodeWhispererCommandBackend, CodeWhispererCommandDeclarations } from './commands/gettingStartedPageCommands' import { SecurityIssueHoverProvider } from './service/securityIssueHoverProvider' import { SecurityIssueCodeActionProvider } from './service/securityIssueCodeActionProvider' import { listCodeWhispererCommands } from './ui/statusBarMenu' -import { Container } from './service/serviceContainer' import { debounceStartSecurityScan } from './commands/startSecurityScan' import { securityScanLanguageContext } from './util/securityScanLanguageContext' import { registerWebviewErrorHandler } from '../webviews/server' @@ -137,7 +130,6 @@ export async function activate(context: ExtContext): Promise { const client = new codewhispererClient.DefaultCodeWhispererClient() // Service initialization - const container = Container.instance ReferenceInlineProvider.instance ImportAdderProvider.instance @@ -149,10 +141,6 @@ export async function activate(context: ExtContext): Promise { * Configuration change */ vscode.workspace.onDidChangeConfiguration(async (configurationChangeEvent) => { - if (configurationChangeEvent.affectsConfiguration('editor.tabSize')) { - EditorContext.updateTabSize(getTabSizeSetting()) - } - if (configurationChangeEvent.affectsConfiguration('amazonQ.showCodeWithReferences')) { ReferenceLogViewProvider.instance.update() if (auth.isEnterpriseSsoInUse()) { @@ -215,20 +203,21 @@ export async function activate(context: ExtContext): Promise { await openSettings('amazonQ') } }), - Commands.register('aws.amazonq.refreshAnnotation', async (forceProceed: boolean) => { - telemetry.record({ - traceId: TelemetryHelper.instance.traceId, - }) - - const editor = vscode.window.activeTextEditor - if (editor) { - if (forceProceed) { - await container.lineAnnotationController.refresh(editor, 'codewhisperer', true) - } else { - await container.lineAnnotationController.refresh(editor, 'codewhisperer') - } - } - }), + // TODO port this to lsp + // Commands.register('aws.amazonq.refreshAnnotation', async (forceProceed: boolean) => { + // telemetry.record({ + // traceId: TelemetryHelper.instance.traceId, + // }) + + // const editor = vscode.window.activeTextEditor + // if (editor) { + // if (forceProceed) { + // await container.lineAnnotationController.refresh(editor, 'codewhisperer', true) + // } else { + // await container.lineAnnotationController.refresh(editor, 'codewhisperer') + // } + // } + // }), // show introduction showIntroduction.register(), // toggle code suggestions @@ -300,22 +289,10 @@ export async function activate(context: ExtContext): Promise { // notify new customizations notifyNewCustomizationsCmd.register(), selectRegionProfileCommand.register(), - /** - * On recommendation acceptance - */ - acceptSuggestion.register(context), // direct CodeWhisperer connection setup with customization connectWithCustomization.register(), - // on text document close. - vscode.workspace.onDidCloseTextDocument((e) => { - if (isInlineCompletionEnabled() && e.uri.fsPath !== InlineCompletionService.instance.filePath()) { - return - } - RecommendationHandler.instance.reportUserDecisions(-1) - }), - vscode.languages.registerHoverProvider( [...CodeWhispererConstants.platformLanguageIds], ReferenceHoverProvider.instance @@ -473,7 +450,6 @@ export async function activate(context: ExtContext): Promise { }) await Commands.tryExecute('aws.amazonq.refreshConnectionCallback') - container.ready() function setSubscriptionsForCodeIssues() { context.extensionContext.subscriptions.push( @@ -511,7 +487,6 @@ export async function activate(context: ExtContext): Promise { } export async function shutdown() { - RecommendationHandler.instance.reportUserDecisions(-1) await CodeWhispererTracker.getTracker().shutdown() } diff --git a/packages/core/src/codewhisperer/commands/basicCommands.ts b/packages/core/src/codewhisperer/commands/basicCommands.ts index a24c6ade704..f2b67c49593 100644 --- a/packages/core/src/codewhisperer/commands/basicCommands.ts +++ b/packages/core/src/codewhisperer/commands/basicCommands.ts @@ -145,7 +145,7 @@ export const showReferenceLog = Commands.declare( if (_ !== placeholder) { source = 'ellipsesMenu' } - await vscode.commands.executeCommand('workbench.view.extension.aws-codewhisperer-reference-log') + await vscode.commands.executeCommand(`${ReferenceLogViewProvider.viewType}.focus`) } ) diff --git a/packages/core/src/codewhisperer/commands/invokeRecommendation.ts b/packages/core/src/codewhisperer/commands/invokeRecommendation.ts deleted file mode 100644 index 37fcb965774..00000000000 --- a/packages/core/src/codewhisperer/commands/invokeRecommendation.ts +++ /dev/null @@ -1,45 +0,0 @@ -/*! - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -import * as vscode from 'vscode' -import { vsCodeState, ConfigurationEntry } from '../models/model' -import { resetIntelliSenseState } from '../util/globalStateUtil' -import { DefaultCodeWhispererClient } from '../client/codewhisperer' -import { RecommendationHandler } from '../service/recommendationHandler' -import { session } from '../util/codeWhispererSession' -import { RecommendationService } from '../service/recommendationService' - -/** - * This function is for manual trigger CodeWhisperer - */ - -export async function invokeRecommendation( - editor: vscode.TextEditor, - client: DefaultCodeWhispererClient, - config: ConfigurationEntry -) { - if (!editor || !config.isManualTriggerEnabled) { - return - } - - /** - * Skip when output channel gains focus and invoke - */ - if (editor.document.languageId === 'Log') { - return - } - /** - * When using intelliSense, if invocation position changed, reject previous active recommendations - */ - if (vsCodeState.isIntelliSenseActive && editor.selection.active !== session.startPos) { - resetIntelliSenseState( - config.isManualTriggerEnabled, - config.isAutomatedTriggerEnabled, - RecommendationHandler.instance.isValidResponse() - ) - } - - await RecommendationService.instance.generateRecommendation(client, editor, 'OnDemand', config, undefined) -} diff --git a/packages/core/src/codewhisperer/commands/onAcceptance.ts b/packages/core/src/codewhisperer/commands/onAcceptance.ts deleted file mode 100644 index e13c197cefd..00000000000 --- a/packages/core/src/codewhisperer/commands/onAcceptance.ts +++ /dev/null @@ -1,85 +0,0 @@ -/*! - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -import * as vscode from 'vscode' -import { vsCodeState, OnRecommendationAcceptanceEntry } from '../models/model' -import { runtimeLanguageContext } from '../util/runtimeLanguageContext' -import { CodeWhispererTracker } from '../tracker/codewhispererTracker' -import { CodeWhispererCodeCoverageTracker } from '../tracker/codewhispererCodeCoverageTracker' -import { getLogger } from '../../shared/logger/logger' -import { handleExtraBrackets } from '../util/closingBracketUtil' -import { RecommendationHandler } from '../service/recommendationHandler' -import { ReferenceLogViewProvider } from '../service/referenceLogViewProvider' -import { ReferenceHoverProvider } from '../service/referenceHoverProvider' -import path from 'path' - -/** - * This function is called when user accepts a intelliSense suggestion or an inline suggestion - */ -export async function onAcceptance(acceptanceEntry: OnRecommendationAcceptanceEntry) { - RecommendationHandler.instance.cancelPaginatedRequest() - /** - * Format document - */ - if (acceptanceEntry.editor) { - const languageContext = runtimeLanguageContext.getLanguageContext( - acceptanceEntry.editor.document.languageId, - path.extname(acceptanceEntry.editor.document.fileName) - ) - const start = acceptanceEntry.range.start - const end = acceptanceEntry.range.end - - // codewhisperer will be doing editing while formatting. - // formatting should not trigger consoals auto trigger - vsCodeState.isCodeWhispererEditing = true - /** - * Mitigation to right context handling mainly for auto closing bracket use case - */ - try { - await handleExtraBrackets(acceptanceEntry.editor, end, start) - } catch (error) { - getLogger().error(`${error} in handleAutoClosingBrackets`) - } - // move cursor to end of suggestion before doing code format - // after formatting, the end position will still be editor.selection.active - acceptanceEntry.editor.selection = new vscode.Selection(end, end) - - vsCodeState.isCodeWhispererEditing = false - CodeWhispererTracker.getTracker().enqueue({ - time: new Date(), - fileUrl: acceptanceEntry.editor.document.uri, - originalString: acceptanceEntry.editor.document.getText(new vscode.Range(start, end)), - startPosition: start, - endPosition: end, - requestId: acceptanceEntry.requestId, - sessionId: acceptanceEntry.sessionId, - index: acceptanceEntry.acceptIndex, - triggerType: acceptanceEntry.triggerType, - completionType: acceptanceEntry.completionType, - language: languageContext.language, - }) - const insertedCoderange = new vscode.Range(start, end) - CodeWhispererCodeCoverageTracker.getTracker(languageContext.language)?.countAcceptedTokens( - insertedCoderange, - acceptanceEntry.editor.document.getText(insertedCoderange), - acceptanceEntry.editor.document.fileName - ) - if (acceptanceEntry.references !== undefined) { - const referenceLog = ReferenceLogViewProvider.getReferenceLog( - acceptanceEntry.recommendation, - acceptanceEntry.references, - acceptanceEntry.editor - ) - ReferenceLogViewProvider.instance.addReferenceLog(referenceLog) - ReferenceHoverProvider.instance.addCodeReferences( - acceptanceEntry.recommendation, - acceptanceEntry.references - ) - } - } - - // at the end of recommendation acceptance, report user decisions and clear recommendations. - RecommendationHandler.instance.reportUserDecisions(acceptanceEntry.acceptIndex) -} diff --git a/packages/core/src/codewhisperer/commands/onInlineAcceptance.ts b/packages/core/src/codewhisperer/commands/onInlineAcceptance.ts deleted file mode 100644 index 50af478ba57..00000000000 --- a/packages/core/src/codewhisperer/commands/onInlineAcceptance.ts +++ /dev/null @@ -1,146 +0,0 @@ -/*! - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -import * as vscode from 'vscode' -import * as CodeWhispererConstants from '../models/constants' -import { vsCodeState, OnRecommendationAcceptanceEntry } from '../models/model' -import { runtimeLanguageContext } from '../util/runtimeLanguageContext' -import { CodeWhispererTracker } from '../tracker/codewhispererTracker' -import { CodeWhispererCodeCoverageTracker } from '../tracker/codewhispererCodeCoverageTracker' -import { getLogger } from '../../shared/logger/logger' -import { RecommendationHandler } from '../service/recommendationHandler' -import { sleep } from '../../shared/utilities/timeoutUtils' -import { handleExtraBrackets } from '../util/closingBracketUtil' -import { Commands } from '../../shared/vscode/commands2' -import { isInlineCompletionEnabled } from '../util/commonUtil' -import { ExtContext } from '../../shared/extensions' -import { onAcceptance } from './onAcceptance' -import * as codewhispererClient from '../client/codewhisperer' -import { - CodewhispererCompletionType, - CodewhispererLanguage, - CodewhispererTriggerType, -} from '../../shared/telemetry/telemetry.gen' -import { ReferenceLogViewProvider } from '../service/referenceLogViewProvider' -import { ReferenceHoverProvider } from '../service/referenceHoverProvider' -import { ImportAdderProvider } from '../service/importAdderProvider' -import { session } from '../util/codeWhispererSession' -import path from 'path' -import { RecommendationService } from '../service/recommendationService' -import { Container } from '../service/serviceContainer' -import { telemetry } from '../../shared/telemetry/telemetry' -import { TelemetryHelper } from '../util/telemetryHelper' -import { UserWrittenCodeTracker } from '../tracker/userWrittenCodeTracker' - -export const acceptSuggestion = Commands.declare( - 'aws.amazonq.accept', - (context: ExtContext) => - async ( - range: vscode.Range, - effectiveRange: vscode.Range, - acceptIndex: number, - recommendation: string, - requestId: string, - sessionId: string, - triggerType: CodewhispererTriggerType, - completionType: CodewhispererCompletionType, - language: CodewhispererLanguage, - references: codewhispererClient.References - ) => { - telemetry.record({ - traceId: TelemetryHelper.instance.traceId, - }) - - RecommendationService.instance.incrementAcceptedCount() - const editor = vscode.window.activeTextEditor - await Container.instance.lineAnnotationController.refresh(editor, 'codewhisperer') - const onAcceptanceFunc = isInlineCompletionEnabled() ? onInlineAcceptance : onAcceptance - await onAcceptanceFunc({ - editor, - range, - effectiveRange, - acceptIndex, - recommendation, - requestId, - sessionId, - triggerType, - completionType, - language, - references, - }) - } -) -/** - * This function is called when user accepts a intelliSense suggestion or an inline suggestion - */ -export async function onInlineAcceptance(acceptanceEntry: OnRecommendationAcceptanceEntry) { - RecommendationHandler.instance.cancelPaginatedRequest() - RecommendationHandler.instance.disposeInlineCompletion() - - if (acceptanceEntry.editor) { - await sleep(CodeWhispererConstants.vsCodeCursorUpdateDelay) - const languageContext = runtimeLanguageContext.getLanguageContext( - acceptanceEntry.editor.document.languageId, - path.extname(acceptanceEntry.editor.document.fileName) - ) - const start = acceptanceEntry.range.start - const end = acceptanceEntry.editor.selection.active - - vsCodeState.isCodeWhispererEditing = true - /** - * Mitigation to right context handling mainly for auto closing bracket use case - */ - try { - // Do not handle extra bracket if there is a right context merge - if (acceptanceEntry.recommendation === session.recommendations[acceptanceEntry.acceptIndex].content) { - await handleExtraBrackets(acceptanceEntry.editor, end, acceptanceEntry.effectiveRange.start) - } - await ImportAdderProvider.instance.onAcceptRecommendation( - acceptanceEntry.editor, - session.recommendations[acceptanceEntry.acceptIndex], - start.line - ) - } catch (error) { - getLogger().error(`${error} in handling extra brackets or imports`) - } finally { - vsCodeState.isCodeWhispererEditing = false - } - - CodeWhispererTracker.getTracker().enqueue({ - time: new Date(), - fileUrl: acceptanceEntry.editor.document.uri, - originalString: acceptanceEntry.editor.document.getText(new vscode.Range(start, end)), - startPosition: start, - endPosition: end, - requestId: acceptanceEntry.requestId, - sessionId: acceptanceEntry.sessionId, - index: acceptanceEntry.acceptIndex, - triggerType: acceptanceEntry.triggerType, - completionType: acceptanceEntry.completionType, - language: languageContext.language, - }) - const insertedCoderange = new vscode.Range(start, end) - CodeWhispererCodeCoverageTracker.getTracker(languageContext.language)?.countAcceptedTokens( - insertedCoderange, - acceptanceEntry.editor.document.getText(insertedCoderange), - acceptanceEntry.editor.document.fileName - ) - UserWrittenCodeTracker.instance.onQFinishesEdits() - if (acceptanceEntry.references !== undefined) { - const referenceLog = ReferenceLogViewProvider.getReferenceLog( - acceptanceEntry.recommendation, - acceptanceEntry.references, - acceptanceEntry.editor - ) - ReferenceLogViewProvider.instance.addReferenceLog(referenceLog) - ReferenceHoverProvider.instance.addCodeReferences( - acceptanceEntry.recommendation, - acceptanceEntry.references - ) - } - - RecommendationHandler.instance.reportUserDecisions(acceptanceEntry.acceptIndex) - } -} diff --git a/packages/core/src/codewhisperer/index.ts b/packages/core/src/codewhisperer/index.ts index 4235ae28668..d782b2abefe 100644 --- a/packages/core/src/codewhisperer/index.ts +++ b/packages/core/src/codewhisperer/index.ts @@ -9,13 +9,6 @@ export * from './models/model' export * from './models/constants' export * from './commands/basicCommands' export * from './commands/types' -export { - AutotriggerState, - EndState, - ManualtriggerState, - PressTabState, - TryMoreExState, -} from './views/lineAnnotationController' export type { TransformationProgressUpdate, TransformationStep, @@ -43,7 +36,7 @@ export { codeWhispererClient, } from './client/codewhisperer' export { listCodeWhispererCommands, listCodeWhispererCommandsId } from './ui/statusBarMenu' -export { refreshStatusBar, CodeWhispererStatusBar, InlineCompletionService } from './service/inlineCompletionService' +export { refreshStatusBar, CodeWhispererStatusBarManager } from './service/statusBar' export { SecurityIssueHoverProvider } from './service/securityIssueHoverProvider' export { SecurityIssueCodeActionProvider } from './service/securityIssueCodeActionProvider' export { @@ -53,48 +46,29 @@ export { IssueItem, SeverityItem, } from './service/securityIssueTreeViewProvider' -export { invokeRecommendation } from './commands/invokeRecommendation' -export { onAcceptance } from './commands/onAcceptance' export { CodeWhispererTracker } from './tracker/codewhispererTracker' -export { RecommendationHandler } from './service/recommendationHandler' export { CodeWhispererUserGroupSettings } from './util/userGroupUtil' export { session } from './util/codeWhispererSession' -export { onInlineAcceptance } from './commands/onInlineAcceptance' export { stopTransformByQ } from './commands/startTransformByQ' -export { getCompletionItems, getCompletionItem, getLabel } from './service/completionProvider' export { featureDefinitions, FeatureConfigProvider } from '../shared/featureConfig' export { ReferenceInlineProvider } from './service/referenceInlineProvider' export { ReferenceHoverProvider } from './service/referenceHoverProvider' -export { CWInlineCompletionItemProvider } from './service/inlineCompletionItemProvider' -export { RecommendationService } from './service/recommendationService' -export { ClassifierTrigger } from './service/classifierTrigger' -export { DocumentChangedSource, KeyStrokeHandler, DefaultDocumentChangedType } from './service/keyStrokeHandler' export { ReferenceLogViewProvider } from './service/referenceLogViewProvider' export { ImportAdderProvider } from './service/importAdderProvider' export { LicenseUtil } from './util/licenseUtil' export { SecurityIssueProvider } from './service/securityIssueProvider' export { listScanResults, mapToAggregatedList, pollScanJobStatus } from './service/securityScanHandler' -export { CodeWhispererCodeCoverageTracker } from './tracker/codewhispererCodeCoverageTracker' export { TelemetryHelper } from './util/telemetryHelper' export { LineSelection, LineTracker } from './tracker/lineTracker' -export { BM25Okapi } from './util/supplementalContext/rankBm25' -export { handleExtraBrackets } from './util/closingBracketUtil' export { runtimeLanguageContext, RuntimeLanguageContext } from './util/runtimeLanguageContext' export * as startSecurityScan from './commands/startSecurityScan' -export * from './util/supplementalContext/utgUtils' -export * from './util/supplementalContext/crossFileContextUtil' -export * from './util/editorContext' export * from './util/showSsoPrompt' export * from './util/securityScanLanguageContext' export * from './util/importAdderUtil' -export * from './util/globalStateUtil' export * from './util/zipUtil' export * from './util/diagnosticsUtil' export * from './util/commonUtil' -export * from './util/supplementalContext/codeParsingUtil' -export * from './util/supplementalContext/supplementalContextUtil' export * from './util/codewhispererSettings' -export * as supplementalContextUtil from './util/supplementalContext/supplementalContextUtil' export * from './service/diagnosticsProvider' export * as diagnosticsProvider from './service/diagnosticsProvider' export * from './ui/codeWhispererNodes' diff --git a/packages/core/src/codewhisperer/models/constants.ts b/packages/core/src/codewhisperer/models/constants.ts index 2cfdad9c870..319127cba20 100644 --- a/packages/core/src/codewhisperer/models/constants.ts +++ b/packages/core/src/codewhisperer/models/constants.ts @@ -138,7 +138,7 @@ export const runningSecurityScan = 'Reviewing project for code issues...' export const runningFileScan = 'Reviewing current file for code issues...' -export const noSuggestions = 'No suggestions from Amazon Q' +export const noInlineSuggestionsMsg = 'No suggestions from Amazon Q' export const licenseFilter = 'Amazon Q suggestions were filtered due to reference settings' @@ -180,15 +180,9 @@ export const securityScanLearnMoreUri = 'https://docs.aws.amazon.com/amazonq/lat export const identityPoolID = 'us-east-1:70717e99-906f-4add-908c-bd9074a2f5b9' /** - * the interval of the background thread invocation, which is triggered by the timer + * Delay for making requests once the user stops typing. Without a delay, inline suggestions request is triggered every keystroke. */ -export const defaultCheckPeriodMillis = 1000 * 60 * 5 - -// suggestion show delay, in milliseconds -export const suggestionShowDelay = 250 - -// add 200ms more delay on top of inline default 30-50ms -export const inlineSuggestionShowDelay = 200 +export const inlineCompletionsDebounceDelay = 200 export const referenceLog = 'Code Reference Log' diff --git a/packages/core/src/codewhisperer/models/model.ts b/packages/core/src/codewhisperer/models/model.ts index d77c52254bc..279469353fb 100644 --- a/packages/core/src/codewhisperer/models/model.ts +++ b/packages/core/src/codewhisperer/models/model.ts @@ -33,6 +33,10 @@ interface VsCodeState { * Flag indicates whether codewhisperer is doing vscode.TextEditor.edit */ isCodeWhispererEditing: boolean + /** + * Keeps track of whether or not recommendations are currently running + */ + isRecommendationsActive: boolean /** * Timestamp of previous user edit */ @@ -44,6 +48,9 @@ interface VsCodeState { export const vsCodeState: VsCodeState = { isIntelliSenseActive: false, isCodeWhispererEditing: false, + // hack to globally keep track of whether or not recommendations are currently running. This allows us to know + // when recommendations have ran during e2e tests + isRecommendationsActive: false, lastUserModificationTime: 0, isFreeTierLimitReached: false, } diff --git a/packages/core/src/codewhisperer/service/classifierTrigger.ts b/packages/core/src/codewhisperer/service/classifierTrigger.ts deleted file mode 100644 index 842d5312e68..00000000000 --- a/packages/core/src/codewhisperer/service/classifierTrigger.ts +++ /dev/null @@ -1,609 +0,0 @@ -/*! - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -import os from 'os' -import * as vscode from 'vscode' -import { CodewhispererAutomatedTriggerType } from '../../shared/telemetry/telemetry' -import { extractContextForCodeWhisperer } from '../util/editorContext' -import { TelemetryHelper } from '../util/telemetryHelper' -import { ProgrammingLanguage } from '../client/codewhispereruserclient' - -interface normalizedCoefficients { - readonly lineNum: number - readonly lenLeftCur: number - readonly lenLeftPrev: number - readonly lenRight: number -} -/* - uses ML classifier to determine if user input should trigger CWSPR service - */ -export class ClassifierTrigger { - static #instance: ClassifierTrigger - - public static get instance() { - return (this.#instance ??= new this()) - } - - // ML classifier trigger threshold - private triggerThreshold = 0.43 - - // ML classifier coefficients - // os coefficient - private osCoefficientMap: Readonly> = { - 'Mac OS X': -0.1552, - 'Windows 10': -0.0238, - Windows: 0.0412, - win32: -0.0559, - } - - // trigger type coefficient - private triggerTypeCoefficientMap: Readonly> = { - SpecialCharacters: 0.0209, - Enter: 0.2853, - } - - private languageCoefficientMap: Readonly> = { - java: -0.4622, - javascript: -0.4688, - python: -0.3052, - typescript: -0.6084, - tsx: -0.6084, - jsx: -0.4688, - shell: -0.4718, - ruby: -0.7356, - sql: -0.4937, - rust: -0.4309, - kotlin: -0.4739, - php: -0.3917, - csharp: -0.3475, - go: -0.3504, - scala: -0.534, - cpp: -0.1734, - json: 0, - yaml: -0.3, - tf: -0.55, - } - - // other metadata coefficient - private lineNumCoefficient = -0.0416 - private lengthOfLeftCurrentCoefficient = -1.1747 - private lengthOfLeftPrevCoefficient = 0.4033 - private lengthOfRightCoefficient = -0.3321 - private prevDecisionAcceptCoefficient = 0.5397 - private prevDecisionRejectCoefficient = -0.1656 - private prevDecisionOtherCoefficient = 0 - private ideVscode = -0.1905 - private lengthLeft0To5 = -0.8756 - private lengthLeft5To10 = -0.5463 - private lengthLeft10To20 = -0.4081 - private lengthLeft20To30 = -0.3272 - private lengthLeft30To40 = -0.2442 - private lengthLeft40To50 = -0.1471 - - // intercept of logistic regression classifier - private intercept = 0.3738713 - - private maxx: normalizedCoefficients = { - lineNum: 4631.0, - lenLeftCur: 157.0, - lenLeftPrev: 176.0, - lenRight: 10239.0, - } - - private minn: normalizedCoefficients = { - lineNum: 0.0, - lenLeftCur: 0.0, - lenLeftPrev: 0.0, - lenRight: 0.0, - } - - // character and keywords coefficient - private charCoefficient: Readonly> = { - throw: 1.5868, - ';': -1.268, - any: -1.1565, - '7': -1.1347, - false: -1.1307, - nil: -1.0653, - elif: 1.0122, - '9': -1.0098, - pass: -1.0058, - True: -1.0002, - False: -0.9434, - '6': -0.9222, - true: -0.9142, - None: -0.9027, - '8': -0.9013, - break: -0.8475, - '}': -0.847, - '5': -0.8414, - '4': -0.8197, - '1': -0.8085, - '\\': -0.8019, - static: -0.7748, - '0': -0.77, - end: -0.7617, - '(': 0.7239, - '/': -0.7104, - where: -0.6981, - readonly: -0.6741, - async: -0.6723, - '3': -0.654, - continue: -0.6413, - struct: -0.64, - try: -0.6369, - float: -0.6341, - using: 0.6079, - '@': 0.6016, - '|': 0.5993, - impl: 0.5808, - private: -0.5746, - for: 0.5741, - '2': -0.5634, - let: -0.5187, - foreach: 0.5186, - select: -0.5148, - export: -0.5, - mut: -0.4921, - ')': -0.463, - ']': -0.4611, - when: 0.4602, - virtual: -0.4583, - extern: -0.4465, - catch: 0.4446, - new: 0.4394, - val: -0.4339, - map: 0.4284, - case: 0.4271, - throws: 0.4221, - null: -0.4197, - protected: -0.4133, - q: 0.4125, - except: 0.4115, - ': ': 0.4072, - '^': -0.407, - ' ': 0.4066, - $: 0.3981, - this: 0.3962, - switch: 0.3947, - '*': -0.3931, - module: 0.3912, - array: 0.385, - '=': 0.3828, - p: 0.3728, - ON: 0.3708, - '`': 0.3693, - u: 0.3658, - a: 0.3654, - require: 0.3646, - '>': -0.3644, - const: -0.3476, - o: 0.3423, - sizeof: 0.3416, - object: 0.3362, - w: 0.3345, - print: 0.3344, - range: 0.3336, - if: 0.3324, - abstract: -0.3293, - var: -0.3239, - i: 0.321, - while: 0.3138, - J: 0.3137, - c: 0.3118, - await: -0.3072, - from: 0.3057, - f: 0.302, - echo: 0.2995, - '#': 0.2984, - e: 0.2962, - r: 0.2925, - mod: 0.2893, - loop: 0.2874, - t: 0.2832, - '~': 0.282, - final: -0.2816, - del: 0.2785, - override: -0.2746, - ref: -0.2737, - h: 0.2693, - m: 0.2681, - '{': 0.2674, - implements: 0.2672, - inline: -0.2642, - match: 0.2613, - with: -0.261, - x: 0.2597, - namespace: -0.2596, - operator: 0.2573, - double: -0.2563, - source: -0.2482, - import: -0.2419, - NULL: -0.2399, - l: 0.239, - or: 0.2378, - s: 0.2366, - then: 0.2354, - W: 0.2354, - y: 0.2333, - local: 0.2288, - is: 0.2282, - n: 0.2254, - '+': -0.2251, - G: 0.223, - public: -0.2229, - WHERE: 0.2224, - list: 0.2204, - Q: 0.2204, - '[': 0.2136, - VALUES: 0.2134, - H: 0.2105, - g: 0.2094, - else: -0.208, - bool: -0.2066, - long: -0.2059, - R: 0.2025, - S: 0.2021, - d: 0.2003, - V: 0.1974, - K: -0.1961, - '<': 0.1958, - debugger: -0.1929, - NOT: -0.1911, - b: 0.1907, - boolean: -0.1891, - z: -0.1866, - LIKE: -0.1793, - raise: 0.1782, - L: 0.1768, - fn: 0.176, - delete: 0.1714, - unsigned: -0.1675, - auto: -0.1648, - finally: 0.1616, - k: 0.1599, - as: 0.156, - instanceof: 0.1558, - '&': 0.1554, - E: 0.1551, - M: 0.1542, - I: 0.1503, - Y: 0.1493, - typeof: 0.1475, - j: 0.1445, - INTO: 0.1442, - IF: 0.1437, - next: 0.1433, - undef: -0.1427, - THEN: -0.1416, - v: 0.1415, - C: 0.1383, - P: 0.1353, - AND: -0.1345, - constructor: 0.1337, - void: -0.1336, - class: -0.1328, - defer: 0.1316, - begin: 0.1306, - FROM: -0.1304, - SET: 0.1291, - decimal: -0.1278, - friend: 0.1277, - SELECT: -0.1265, - event: 0.1259, - lambda: 0.1253, - enum: 0.1215, - A: 0.121, - lock: 0.1187, - ensure: 0.1184, - '%': 0.1177, - isset: 0.1175, - O: 0.1174, - '.': 0.1146, - UNION: -0.1145, - alias: -0.1129, - template: -0.1102, - WHEN: 0.1093, - rescue: 0.1083, - DISTINCT: -0.1074, - trait: -0.1073, - D: 0.1062, - in: 0.1045, - internal: -0.1029, - ',': 0.1027, - static_cast: 0.1016, - do: -0.1005, - OR: 0.1003, - AS: -0.1001, - interface: 0.0996, - super: 0.0989, - B: 0.0963, - U: 0.0962, - T: 0.0943, - CALL: -0.0918, - BETWEEN: -0.0915, - N: 0.0897, - yield: 0.0867, - done: -0.0857, - string: -0.0837, - out: -0.0831, - volatile: -0.0819, - retry: 0.0816, - '?': -0.0796, - number: -0.0791, - short: 0.0787, - sealed: -0.0776, - package: 0.0765, - OPEN: -0.0756, - base: 0.0735, - and: 0.0729, - exit: 0.0726, - _: 0.0721, - keyof: -0.072, - def: 0.0713, - crate: -0.0706, - '-': -0.07, - FUNCTION: 0.0692, - declare: -0.0678, - include: 0.0671, - COUNT: -0.0669, - INDEX: -0.0666, - CLOSE: -0.0651, - fi: -0.0644, - uint: 0.0624, - params: 0.0575, - HAVING: 0.0575, - byte: -0.0575, - clone: -0.0552, - char: -0.054, - func: 0.0538, - never: -0.053, - unset: -0.0524, - unless: -0.051, - esac: -0.0509, - shift: -0.0507, - require_once: 0.0486, - ELSE: -0.0477, - extends: 0.0461, - elseif: 0.0452, - mutable: -0.0451, - asm: 0.0449, - '!': 0.0446, - LIMIT: 0.0444, - ushort: -0.0438, - '"': -0.0433, - Z: 0.0431, - exec: -0.0431, - IS: -0.0429, - DECLARE: -0.0425, - __LINE__: -0.0424, - BEGIN: -0.0418, - typedef: 0.0414, - EXIT: -0.0412, - "'": 0.041, - function: -0.0393, - dyn: -0.039, - wchar_t: -0.0388, - unique: -0.0383, - include_once: 0.0367, - stackalloc: 0.0359, - RETURN: -0.0356, - const_cast: 0.035, - MAX: 0.0341, - assert: -0.0331, - JOIN: -0.0328, - use: 0.0318, - GET: 0.0317, - VIEW: 0.0314, - move: 0.0308, - typename: 0.0308, - die: 0.0305, - asserts: -0.0304, - reinterpret_cast: -0.0302, - USING: -0.0289, - elsif: -0.0285, - FIRST: -0.028, - self: -0.0278, - RETURNING: -0.0278, - symbol: -0.0273, - OFFSET: 0.0263, - bigint: 0.0253, - register: -0.0237, - union: -0.0227, - return: -0.0227, - until: -0.0224, - endfor: -0.0213, - implicit: -0.021, - LOOP: 0.0195, - pub: 0.0182, - global: 0.0179, - EXCEPTION: 0.0175, - delegate: 0.0173, - signed: -0.0163, - FOR: 0.0156, - unsafe: 0.014, - NEXT: -0.0133, - IN: 0.0129, - MIN: -0.0123, - go: -0.0112, - type: -0.0109, - explicit: -0.0107, - eval: -0.0104, - int: -0.0099, - CASE: -0.0096, - END: 0.0084, - UPDATE: 0.0074, - default: 0.0072, - chan: 0.0068, - fixed: 0.0066, - not: -0.0052, - X: -0.0047, - endforeach: 0.0031, - goto: 0.0028, - empty: 0.0022, - checked: 0.0012, - F: -0.001, - } - - public getThreshold() { - return this.triggerThreshold - } - - public recordClassifierResultForManualTrigger(editor: vscode.TextEditor) { - this.shouldTriggerFromClassifier(undefined, editor, undefined, true) - } - - public recordClassifierResultForAutoTrigger( - editor: vscode.TextEditor, - triggerType?: CodewhispererAutomatedTriggerType, - event?: vscode.TextDocumentChangeEvent - ) { - if (!triggerType) { - return - } - this.shouldTriggerFromClassifier(event, editor, triggerType, true) - } - - public shouldTriggerFromClassifier( - event: vscode.TextDocumentChangeEvent | undefined, - editor: vscode.TextEditor, - autoTriggerType: string | undefined, - shouldRecordResult: boolean = false - ): boolean { - const fileContext = extractContextForCodeWhisperer(editor) - const osPlatform = this.normalizeOsName(os.platform(), os.version()) - const char = event ? event.contentChanges[0].text : '' - const lineNum = editor.selection.active.line - const classifierResult = this.getClassifierResult( - fileContext.leftFileContent, - fileContext.rightFileContent, - osPlatform, - autoTriggerType, - char, - lineNum, - fileContext.programmingLanguage - ) - - const threshold = this.getThreshold() - - const shouldTrigger = classifierResult > threshold - if (shouldRecordResult) { - TelemetryHelper.instance.setClassifierResult(classifierResult) - TelemetryHelper.instance.setClassifierThreshold(threshold) - } - return shouldTrigger - } - - private getClassifierResult( - leftContext: string, - rightContext: string, - os: string, - triggerType: string | undefined, - char: string, - lineNum: number, - language: ProgrammingLanguage - ): number { - const leftContextLines = leftContext.split(/\r?\n/) - const leftContextAtCurrentLine = leftContextLines[leftContextLines.length - 1] - const tokens = leftContextAtCurrentLine.trim().split(' ') - let keyword = '' - const lastToken = tokens[tokens.length - 1] - if (lastToken && lastToken.length > 1) { - keyword = lastToken - } - const lengthOfLeftCurrent = leftContextLines[leftContextLines.length - 1].length - const lengthOfLeftPrev = leftContextLines[leftContextLines.length - 2]?.length ?? 0 - const lengthOfRight = rightContext.trim().length - - const triggerTypeCoefficient: number = this.triggerTypeCoefficientMap[triggerType || ''] ?? 0 - const osCoefficient: number = this.osCoefficientMap[os] ?? 0 - const charCoefficient: number = this.charCoefficient[char] ?? 0 - const keyWordCoefficient: number = this.charCoefficient[keyword] ?? 0 - const ideCoefficient = this.ideVscode - - const previousDecision = TelemetryHelper.instance.getLastTriggerDecisionForClassifier() - const languageCoefficients = Object.values(this.languageCoefficientMap) - const avrgCoefficient = - languageCoefficients.length > 0 - ? languageCoefficients.reduce((a, b) => a + b) / languageCoefficients.length - : 0 - const languageCoefficient = this.languageCoefficientMap[language.languageName] ?? avrgCoefficient - - let previousDecisionCoefficient = 0 - if (previousDecision === 'Accept') { - previousDecisionCoefficient = this.prevDecisionAcceptCoefficient - } else if (previousDecision === 'Reject') { - previousDecisionCoefficient = this.prevDecisionRejectCoefficient - } else if (previousDecision === 'Discard' || previousDecision === 'Empty') { - previousDecisionCoefficient = this.prevDecisionOtherCoefficient - } - - let leftContextLengthCoefficient = 0 - if (leftContext.length >= 0 && leftContext.length < 5) { - leftContextLengthCoefficient = this.lengthLeft0To5 - } else if (leftContext.length >= 5 && leftContext.length < 10) { - leftContextLengthCoefficient = this.lengthLeft5To10 - } else if (leftContext.length >= 10 && leftContext.length < 20) { - leftContextLengthCoefficient = this.lengthLeft10To20 - } else if (leftContext.length >= 20 && leftContext.length < 30) { - leftContextLengthCoefficient = this.lengthLeft20To30 - } else if (leftContext.length >= 30 && leftContext.length < 40) { - leftContextLengthCoefficient = this.lengthLeft30To40 - } else if (leftContext.length >= 40 && leftContext.length < 50) { - leftContextLengthCoefficient = this.lengthLeft40To50 - } - - const result = - (this.lengthOfRightCoefficient * (lengthOfRight - this.minn.lenRight)) / - (this.maxx.lenRight - this.minn.lenRight) + - (this.lengthOfLeftCurrentCoefficient * (lengthOfLeftCurrent - this.minn.lenLeftCur)) / - (this.maxx.lenLeftCur - this.minn.lenLeftCur) + - (this.lengthOfLeftPrevCoefficient * (lengthOfLeftPrev - this.minn.lenLeftPrev)) / - (this.maxx.lenLeftPrev - this.minn.lenLeftPrev) + - (this.lineNumCoefficient * (lineNum - this.minn.lineNum)) / (this.maxx.lineNum - this.minn.lineNum) + - osCoefficient + - triggerTypeCoefficient + - charCoefficient + - keyWordCoefficient + - ideCoefficient + - this.intercept + - previousDecisionCoefficient + - languageCoefficient + - leftContextLengthCoefficient - - return sigmoid(result) - } - - private normalizeOsName(name: string, version: string | undefined): string { - const lowercaseName = name.toLowerCase() - if (lowercaseName.includes('windows')) { - if (!version) { - return 'Windows' - } else if (version.includes('Windows NT 10') || version.startsWith('10')) { - return 'Windows 10' - } else if (version.includes('6.1')) { - return 'Windows 7' - } else if (version.includes('6.3')) { - return 'Windows 8.1' - } else { - return 'Windows' - } - } else if ( - lowercaseName.includes('macos') || - lowercaseName.includes('mac os') || - lowercaseName.includes('darwin') - ) { - return 'Mac OS X' - } else if (lowercaseName.includes('linux')) { - return 'Linux' - } else { - return name - } - } -} - -const sigmoid = (x: number) => { - return 1 / (1 + Math.exp(-x)) -} diff --git a/packages/core/src/codewhisperer/service/completionProvider.ts b/packages/core/src/codewhisperer/service/completionProvider.ts deleted file mode 100644 index 226d04dec2b..00000000000 --- a/packages/core/src/codewhisperer/service/completionProvider.ts +++ /dev/null @@ -1,77 +0,0 @@ -/*! - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -import * as vscode from 'vscode' -import * as CodeWhispererConstants from '../models/constants' -import { runtimeLanguageContext } from '../util/runtimeLanguageContext' -import { Recommendation } from '../client/codewhisperer' -import { LicenseUtil } from '../util/licenseUtil' -import { RecommendationHandler } from './recommendationHandler' -import { session } from '../util/codeWhispererSession' -import path from 'path' -/** - * completion provider for intelliSense popup - */ -export function getCompletionItems(document: vscode.TextDocument, position: vscode.Position) { - const completionItems: vscode.CompletionItem[] = [] - for (const [index, recommendation] of session.recommendations.entries()) { - completionItems.push(getCompletionItem(document, position, recommendation, index)) - session.setSuggestionState(index, 'Showed') - } - return completionItems -} - -export function getCompletionItem( - document: vscode.TextDocument, - position: vscode.Position, - recommendationDetail: Recommendation, - recommendationIndex: number -) { - const start = session.startPos - const range = new vscode.Range(start, start) - const recommendation = recommendationDetail.content - const completionItem = new vscode.CompletionItem(recommendation) - completionItem.insertText = new vscode.SnippetString(recommendation) - completionItem.documentation = new vscode.MarkdownString().appendCodeblock(recommendation, document.languageId) - completionItem.kind = vscode.CompletionItemKind.Method - completionItem.detail = CodeWhispererConstants.completionDetail - completionItem.keepWhitespace = true - completionItem.label = getLabel(recommendation) - completionItem.preselect = true - completionItem.sortText = String(recommendationIndex + 1).padStart(10, '0') - completionItem.range = new vscode.Range(start, position) - const languageContext = runtimeLanguageContext.getLanguageContext( - document.languageId, - path.extname(document.fileName) - ) - let references: typeof recommendationDetail.references - if (recommendationDetail.references !== undefined && recommendationDetail.references.length > 0) { - references = recommendationDetail.references - const licenses = [ - ...new Set(references.map((r) => `[${r.licenseName}](${LicenseUtil.getLicenseHtml(r.licenseName)})`)), - ].join(', ') - completionItem.documentation.appendMarkdown(CodeWhispererConstants.suggestionDetailReferenceText(licenses)) - } - completionItem.command = { - command: 'aws.amazonq.accept', - title: 'On acceptance', - arguments: [ - range, - recommendationIndex, - recommendation, - RecommendationHandler.instance.requestId, - session.sessionId, - session.triggerType, - session.getCompletionType(recommendationIndex), - languageContext.language, - references, - ], - } - return completionItem -} - -export function getLabel(recommendation: string): string { - return recommendation.slice(0, CodeWhispererConstants.labelLength) + '..' -} diff --git a/packages/core/src/codewhisperer/service/inlineCompletionItemProvider.ts b/packages/core/src/codewhisperer/service/inlineCompletionItemProvider.ts deleted file mode 100644 index a6c424c321d..00000000000 --- a/packages/core/src/codewhisperer/service/inlineCompletionItemProvider.ts +++ /dev/null @@ -1,194 +0,0 @@ -/*! - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ -import vscode, { Position } from 'vscode' -import { getPrefixSuffixOverlap } from '../util/commonUtil' -import { Recommendation } from '../client/codewhisperer' -import { session } from '../util/codeWhispererSession' -import { TelemetryHelper } from '../util/telemetryHelper' -import { runtimeLanguageContext } from '../util/runtimeLanguageContext' -import { ReferenceInlineProvider } from './referenceInlineProvider' -import { ImportAdderProvider } from './importAdderProvider' -import { application } from '../util/codeWhispererApplication' -import path from 'path' -import { UserWrittenCodeTracker } from '../tracker/userWrittenCodeTracker' - -export class CWInlineCompletionItemProvider implements vscode.InlineCompletionItemProvider { - private activeItemIndex: number | undefined - private nextMove: number - private recommendations: Recommendation[] - private requestId: string - private startPos: Position - private nextToken: string - - private _onDidShow: vscode.EventEmitter = new vscode.EventEmitter() - public readonly onDidShow: vscode.Event = this._onDidShow.event - - public constructor( - itemIndex: number | undefined, - firstMove: number, - recommendations: Recommendation[], - requestId: string, - startPos: Position, - nextToken: string - ) { - this.activeItemIndex = itemIndex - this.nextMove = firstMove - this.recommendations = recommendations - this.requestId = requestId - this.startPos = startPos - this.nextToken = nextToken - } - - get getActiveItemIndex() { - return this.activeItemIndex - } - - public clearActiveItemIndex() { - this.activeItemIndex = undefined - } - - // iterate suggestions and stop at index 0 or index len - 1 - private getIteratingIndexes() { - const len = this.recommendations.length - const startIndex = this.activeItemIndex ? this.activeItemIndex : 0 - const index = [] - if (this.nextMove === 0) { - for (let i = 0; i < len; i++) { - index.push((startIndex + i) % len) - } - } else if (this.nextMove === -1) { - for (let i = startIndex - 1; i >= 0; i--) { - index.push(i) - } - index.push(startIndex) - } else { - for (let i = startIndex + 1; i < len; i++) { - index.push(i) - } - index.push(startIndex) - } - return index - } - - truncateOverlapWithRightContext(document: vscode.TextDocument, suggestion: string, pos: vscode.Position): string { - const trimmedSuggestion = suggestion.trim() - // limit of 5000 for right context matching - const rightContext = document.getText(new vscode.Range(pos, document.positionAt(document.offsetAt(pos) + 5000))) - const overlap = getPrefixSuffixOverlap(trimmedSuggestion, rightContext) - const overlapIndex = suggestion.lastIndexOf(overlap) - if (overlapIndex >= 0) { - const truncated = suggestion.slice(0, overlapIndex) - return truncated.trim().length ? truncated : '' - } else { - return suggestion - } - } - - getInlineCompletionItem( - document: vscode.TextDocument, - r: Recommendation, - start: vscode.Position, - end: vscode.Position, - index: number, - prefix: string - ): vscode.InlineCompletionItem | undefined { - if (!r.content.startsWith(prefix)) { - return undefined - } - const effectiveStart = document.positionAt(document.offsetAt(start) + prefix.length) - const truncatedSuggestion = this.truncateOverlapWithRightContext(document, r.content, end) - if (truncatedSuggestion.length === 0) { - if (session.getSuggestionState(index) !== 'Showed') { - session.setSuggestionState(index, 'Discard') - } - return undefined - } - TelemetryHelper.instance.lastSuggestionInDisplay = truncatedSuggestion - return { - insertText: truncatedSuggestion, - range: new vscode.Range(start, end), - command: { - command: 'aws.amazonq.accept', - title: 'On acceptance', - arguments: [ - new vscode.Range(start, end), - new vscode.Range(effectiveStart, end), - index, - truncatedSuggestion, - this.requestId, - session.sessionId, - session.triggerType, - session.getCompletionType(index), - runtimeLanguageContext.getLanguageContext(document.languageId, path.extname(document.fileName)) - .language, - r.references, - ], - }, - } - } - - // the returned completion items will always only contain one valid item - // this is to trace the current index of visible completion item - // so that reference tracker can show - // This hack can be removed once inlineCompletionAdditions API becomes public - provideInlineCompletionItems( - document: vscode.TextDocument, - position: vscode.Position, - _context: vscode.InlineCompletionContext, - _token: vscode.CancellationToken - ): vscode.ProviderResult { - if (position.line < 0 || position.isBefore(this.startPos)) { - application()._clearCodeWhispererUIListener.fire() - this.activeItemIndex = undefined - return - } - - // There's a chance that the startPos is no longer valid in the current document (e.g. - // when CodeWhisperer got triggered by 'Enter', the original startPos is with indentation - // but then this indentation got removed by VSCode when another new line is inserted, - // before the code reaches here). In such case, we need to update the startPos to be a - // valid one. Otherwise, inline completion which utilizes this position will function - // improperly. - const start = document.validatePosition(this.startPos) - const end = position - const iteratingIndexes = this.getIteratingIndexes() - const prefix = document.getText(new vscode.Range(start, end)).replace(/\r\n/g, '\n') - const matchedCount = session.recommendations.filter( - (r) => r.content.length > 0 && r.content.startsWith(prefix) && r.content !== prefix - ).length - for (const i of iteratingIndexes) { - const r = session.recommendations[i] - const item = this.getInlineCompletionItem(document, r, start, end, i, prefix) - if (item === undefined) { - continue - } - this.activeItemIndex = i - session.setSuggestionState(i, 'Showed') - ReferenceInlineProvider.instance.setInlineReference(this.startPos.line, r.content, r.references) - ImportAdderProvider.instance.onShowRecommendation(document, this.startPos.line, r) - this.nextMove = 0 - TelemetryHelper.instance.setFirstSuggestionShowTime() - session.setPerceivedLatency() - UserWrittenCodeTracker.instance.onQStartsMakingEdits() - this._onDidShow.fire() - if (matchedCount >= 2 || this.nextToken !== '') { - const result = [item] - for (let j = 0; j < matchedCount - 1; j++) { - result.push({ - insertText: `${ - typeof item.insertText === 'string' ? item.insertText : item.insertText.value - }${j}`, - range: item.range, - }) - } - return result - } - return [item] - } - application()._clearCodeWhispererUIListener.fire() - this.activeItemIndex = undefined - return [] - } -} diff --git a/packages/core/src/codewhisperer/service/inlineCompletionService.ts b/packages/core/src/codewhisperer/service/inlineCompletionService.ts deleted file mode 100644 index cc9887adb1f..00000000000 --- a/packages/core/src/codewhisperer/service/inlineCompletionService.ts +++ /dev/null @@ -1,273 +0,0 @@ -/*! - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ -import * as vscode from 'vscode' -import { CodeSuggestionsState, ConfigurationEntry, GetRecommendationsResponse, vsCodeState } from '../models/model' -import * as CodeWhispererConstants from '../models/constants' -import { DefaultCodeWhispererClient } from '../client/codewhisperer' -import { RecommendationHandler } from './recommendationHandler' -import { CodewhispererAutomatedTriggerType, CodewhispererTriggerType } from '../../shared/telemetry/telemetry' -import { showTimedMessage } from '../../shared/utilities/messages' -import { getLogger } from '../../shared/logger/logger' -import { TelemetryHelper } from '../util/telemetryHelper' -import { AuthUtil } from '../util/authUtil' -import { shared } from '../../shared/utilities/functionUtils' -import { ClassifierTrigger } from './classifierTrigger' -import { getSelectedCustomization } from '../util/customizationUtil' -import { codicon, getIcon } from '../../shared/icons' -import { session } from '../util/codeWhispererSession' -import { noSuggestions } from '../models/constants' -import { Commands } from '../../shared/vscode/commands2' -import { listCodeWhispererCommandsId } from '../ui/statusBarMenu' - -export class InlineCompletionService { - private maxPage = 100 - private statusBar: CodeWhispererStatusBar - private _showRecommendationTimer?: NodeJS.Timer - - constructor(statusBar: CodeWhispererStatusBar = CodeWhispererStatusBar.instance) { - this.statusBar = statusBar - - RecommendationHandler.instance.onDidReceiveRecommendation((e) => { - this.startShowRecommendationTimer() - }) - - CodeSuggestionsState.instance.onDidChangeState(() => { - return this.refreshStatusBar() - }) - } - - static #instance: InlineCompletionService - - public static get instance() { - return (this.#instance ??= new this()) - } - - filePath(): string | undefined { - return RecommendationHandler.instance.documentUri?.fsPath - } - - private sharedTryShowRecommendation = shared( - RecommendationHandler.instance.tryShowRecommendation.bind(RecommendationHandler.instance) - ) - - private startShowRecommendationTimer() { - if (this._showRecommendationTimer) { - clearInterval(this._showRecommendationTimer) - this._showRecommendationTimer = undefined - } - this._showRecommendationTimer = setInterval(() => { - const delay = performance.now() - vsCodeState.lastUserModificationTime - if (delay < CodeWhispererConstants.inlineSuggestionShowDelay) { - return - } - this.sharedTryShowRecommendation() - .catch((e) => { - getLogger().error('tryShowRecommendation failed: %s', (e as Error).message) - }) - .finally(() => { - if (this._showRecommendationTimer) { - clearInterval(this._showRecommendationTimer) - this._showRecommendationTimer = undefined - } - }) - }, CodeWhispererConstants.showRecommendationTimerPollPeriod) - } - - async getPaginatedRecommendation( - client: DefaultCodeWhispererClient, - editor: vscode.TextEditor, - triggerType: CodewhispererTriggerType, - config: ConfigurationEntry, - autoTriggerType?: CodewhispererAutomatedTriggerType, - event?: vscode.TextDocumentChangeEvent - ): Promise { - if (vsCodeState.isCodeWhispererEditing || RecommendationHandler.instance.isSuggestionVisible()) { - return { - result: 'Failed', - errorMessage: 'Amazon Q is already running', - recommendationCount: 0, - } - } - - // Call report user decisions once to report recommendations leftover from last invocation. - RecommendationHandler.instance.reportUserDecisions(-1) - TelemetryHelper.instance.setInvokeSuggestionStartTime() - ClassifierTrigger.instance.recordClassifierResultForAutoTrigger(editor, autoTriggerType, event) - - const triggerChar = event?.contentChanges[0]?.text - if (autoTriggerType === 'SpecialCharacters' && triggerChar) { - TelemetryHelper.instance.setTriggerCharForUserTriggerDecision(triggerChar) - } - const isAutoTrigger = triggerType === 'AutoTrigger' - if (AuthUtil.instance.isConnectionExpired()) { - await AuthUtil.instance.notifyReauthenticate(isAutoTrigger) - return { - result: 'Failed', - errorMessage: 'auth', - recommendationCount: 0, - } - } - - await this.setState('loading') - - RecommendationHandler.instance.checkAndResetCancellationTokens() - RecommendationHandler.instance.documentUri = editor.document.uri - let response: GetRecommendationsResponse = { - result: 'Failed', - errorMessage: undefined, - recommendationCount: 0, - } - try { - let page = 0 - while (page < this.maxPage) { - response = await RecommendationHandler.instance.getRecommendations( - client, - editor, - triggerType, - config, - autoTriggerType, - true, - page - ) - if (RecommendationHandler.instance.checkAndResetCancellationTokens()) { - RecommendationHandler.instance.reportUserDecisions(-1) - await vscode.commands.executeCommand('aws.amazonq.refreshStatusBar') - if (triggerType === 'OnDemand' && session.recommendations.length === 0) { - void showTimedMessage(response.errorMessage ? response.errorMessage : noSuggestions, 2000) - } - return { - result: 'Failed', - errorMessage: 'cancelled', - recommendationCount: 0, - } - } - if (!RecommendationHandler.instance.hasNextToken()) { - break - } - page++ - } - } catch (error) { - getLogger().error(`Error ${error} in getPaginatedRecommendation`) - } - await vscode.commands.executeCommand('aws.amazonq.refreshStatusBar') - if (triggerType === 'OnDemand' && session.recommendations.length === 0) { - void showTimedMessage(response.errorMessage ? response.errorMessage : noSuggestions, 2000) - } - TelemetryHelper.instance.tryRecordClientComponentLatency() - - return { - result: 'Succeeded', - errorMessage: undefined, - recommendationCount: session.recommendations.length, - } - } - - /** Updates the status bar to represent the latest CW state */ - refreshStatusBar() { - if (AuthUtil.instance.isConnectionValid()) { - if (AuthUtil.instance.requireProfileSelection()) { - return this.setState('needsProfile') - } - return this.setState('ok') - } else if (AuthUtil.instance.isConnectionExpired()) { - return this.setState('expired') - } else { - return this.setState('notConnected') - } - } - - private async setState(state: keyof typeof states) { - switch (state) { - case 'loading': { - await this.statusBar.setState('loading') - break - } - case 'ok': { - await this.statusBar.setState('ok', CodeSuggestionsState.instance.isSuggestionsEnabled()) - break - } - case 'expired': { - await this.statusBar.setState('expired') - break - } - case 'notConnected': { - await this.statusBar.setState('notConnected') - break - } - case 'needsProfile': { - await this.statusBar.setState('needsProfile') - break - } - } - } -} - -/** The states that the completion service can be in */ -const states = { - loading: 'loading', - ok: 'ok', - expired: 'expired', - notConnected: 'notConnected', - needsProfile: 'needsProfile', -} as const - -export class CodeWhispererStatusBar { - protected statusBar: vscode.StatusBarItem = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Left, 1) - - static #instance: CodeWhispererStatusBar - static get instance() { - return (this.#instance ??= new this()) - } - - protected constructor() {} - - async setState(state: keyof Omit): Promise - async setState(status: keyof Pick, isSuggestionsEnabled: boolean): Promise - async setState(status: keyof typeof states, isSuggestionsEnabled?: boolean): Promise { - const statusBar = this.statusBar - statusBar.command = listCodeWhispererCommandsId - statusBar.backgroundColor = undefined - - const title = 'Amazon Q' - switch (status) { - case 'loading': { - const selectedCustomization = getSelectedCustomization() - statusBar.text = codicon` ${getIcon('vscode-loading~spin')} ${title}${ - selectedCustomization.arn === '' ? '' : ` | ${selectedCustomization.name}` - }` - break - } - case 'ok': { - const selectedCustomization = getSelectedCustomization() - const icon = isSuggestionsEnabled ? getIcon('vscode-debug-start') : getIcon('vscode-debug-pause') - statusBar.text = codicon`${icon} ${title}${ - selectedCustomization.arn === '' ? '' : ` | ${selectedCustomization.name}` - }` - break - } - - case 'expired': { - statusBar.text = codicon` ${getIcon('vscode-debug-disconnect')} ${title}` - statusBar.backgroundColor = new vscode.ThemeColor('statusBarItem.warningBackground') - break - } - case 'needsProfile': - case 'notConnected': - statusBar.text = codicon` ${getIcon('vscode-chrome-close')} ${title}` - statusBar.backgroundColor = new vscode.ThemeColor('statusBarItem.errorBackground') - break - } - - statusBar.show() - } -} - -/** In this module due to circulare dependency issues */ -export const refreshStatusBar = Commands.declare( - { id: 'aws.amazonq.refreshStatusBar', logging: false }, - () => async () => { - await InlineCompletionService.instance.refreshStatusBar() - } -) diff --git a/packages/core/src/codewhisperer/service/keyStrokeHandler.ts b/packages/core/src/codewhisperer/service/keyStrokeHandler.ts deleted file mode 100644 index 49ef633a98f..00000000000 --- a/packages/core/src/codewhisperer/service/keyStrokeHandler.ts +++ /dev/null @@ -1,267 +0,0 @@ -/*! - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -import * as vscode from 'vscode' -import { DefaultCodeWhispererClient } from '../client/codewhisperer' -import * as CodeWhispererConstants from '../models/constants' -import { ConfigurationEntry } from '../models/model' -import { getLogger } from '../../shared/logger/logger' -import { RecommendationHandler } from './recommendationHandler' -import { CodewhispererAutomatedTriggerType } from '../../shared/telemetry/telemetry' -import { getTabSizeSetting } from '../../shared/utilities/editorUtilities' -import { isInlineCompletionEnabled } from '../util/commonUtil' -import { ClassifierTrigger } from './classifierTrigger' -import { extractContextForCodeWhisperer } from '../util/editorContext' -import { RecommendationService } from './recommendationService' - -/** - * This class is for CodeWhisperer auto trigger - */ -export class KeyStrokeHandler { - /** - * Special character which automated triggers codewhisperer - */ - public specialChar: string - /** - * Key stroke count for automated trigger - */ - - private idleTriggerTimer?: NodeJS.Timer - - public lastInvocationTime?: number - - constructor() { - this.specialChar = '' - } - - static #instance: KeyStrokeHandler - - public static get instance() { - return (this.#instance ??= new this()) - } - - public startIdleTimeTriggerTimer( - event: vscode.TextDocumentChangeEvent, - editor: vscode.TextEditor, - client: DefaultCodeWhispererClient, - config: ConfigurationEntry - ) { - if (this.idleTriggerTimer) { - clearInterval(this.idleTriggerTimer) - this.idleTriggerTimer = undefined - } - if (!this.shouldTriggerIdleTime()) { - return - } - this.idleTriggerTimer = setInterval(() => { - const duration = (performance.now() - RecommendationHandler.instance.lastInvocationTime) / 1000 - if (duration < CodeWhispererConstants.invocationTimeIntervalThreshold) { - return - } - - this.invokeAutomatedTrigger('IdleTime', editor, client, config, event) - .catch((e) => { - getLogger().error('invokeAutomatedTrigger failed: %s', (e as Error).message) - }) - .finally(() => { - if (this.idleTriggerTimer) { - clearInterval(this.idleTriggerTimer) - this.idleTriggerTimer = undefined - } - }) - }, CodeWhispererConstants.idleTimerPollPeriod) - } - - public shouldTriggerIdleTime(): boolean { - if (isInlineCompletionEnabled() && RecommendationService.instance.isRunning) { - return false - } - return true - } - - async processKeyStroke( - event: vscode.TextDocumentChangeEvent, - editor: vscode.TextEditor, - client: DefaultCodeWhispererClient, - config: ConfigurationEntry - ): Promise { - try { - if (!config.isAutomatedTriggerEnabled) { - return - } - - // Skip when output channel gains focus and invoke - if (editor.document.languageId === 'Log') { - return - } - - const { rightFileContent } = extractContextForCodeWhisperer(editor) - const rightContextLines = rightFileContent.split(/\r?\n/) - const rightContextAtCurrentLine = rightContextLines[0] - // we do not want to trigger when there is immediate right context on the same line - // with "}" being an exception because of IDE auto-complete - if ( - rightContextAtCurrentLine.length && - !rightContextAtCurrentLine.startsWith(' ') && - rightContextAtCurrentLine.trim() !== '}' && - rightContextAtCurrentLine.trim() !== ')' - ) { - return - } - - let triggerType: CodewhispererAutomatedTriggerType | undefined - const changedSource = new DefaultDocumentChangedType(event.contentChanges).checkChangeSource() - - switch (changedSource) { - case DocumentChangedSource.EnterKey: { - triggerType = 'Enter' - break - } - case DocumentChangedSource.SpecialCharsKey: { - triggerType = 'SpecialCharacters' - break - } - case DocumentChangedSource.RegularKey: { - triggerType = ClassifierTrigger.instance.shouldTriggerFromClassifier(event, editor, triggerType) - ? 'Classifier' - : undefined - break - } - default: { - break - } - } - - if (triggerType) { - await this.invokeAutomatedTrigger(triggerType, editor, client, config, event) - } - } catch (error) { - getLogger().verbose(`Automated Trigger Exception : ${error}`) - } - } - - async invokeAutomatedTrigger( - autoTriggerType: CodewhispererAutomatedTriggerType, - editor: vscode.TextEditor, - client: DefaultCodeWhispererClient, - config: ConfigurationEntry, - event: vscode.TextDocumentChangeEvent - ): Promise { - if (!editor) { - return - } - - // RecommendationHandler.instance.reportUserDecisionOfRecommendation(editor, -1) - await RecommendationService.instance.generateRecommendation( - client, - editor, - 'AutoTrigger', - config, - autoTriggerType - ) - } -} - -export abstract class DocumentChangedType { - constructor(protected readonly contentChanges: ReadonlyArray) { - this.contentChanges = contentChanges - } - - abstract checkChangeSource(): DocumentChangedSource - - // Enter key should always start with ONE '\n' or '\r\n' and potentially following spaces due to IDE reformat - protected isEnterKey(str: string): boolean { - if (str.length === 0) { - return false - } - return ( - (str.startsWith('\r\n') && str.substring(2).trim() === '') || - (str[0] === '\n' && str.substring(1).trim() === '') - ) - } - - // Tab should consist of space char only ' ' and the length % tabSize should be 0 - protected isTabKey(str: string): boolean { - const tabSize = getTabSizeSetting() - if (str.length % tabSize === 0 && str.trim() === '') { - return true - } - return false - } - - protected isUserTypingSpecialChar(str: string): boolean { - return ['(', '()', '[', '[]', '{', '{}', ':'].includes(str) - } - - protected isSingleLine(str: string): boolean { - let newLineCounts = 0 - for (const ch of str) { - if (ch === '\n') { - newLineCounts += 1 - } - } - - // since pressing Enter key possibly will generate string like '\n ' due to indention - if (this.isEnterKey(str)) { - return true - } - if (newLineCounts >= 1) { - return false - } - return true - } -} - -export class DefaultDocumentChangedType extends DocumentChangedType { - constructor(contentChanges: ReadonlyArray) { - super(contentChanges) - } - - checkChangeSource(): DocumentChangedSource { - if (this.contentChanges.length === 0) { - return DocumentChangedSource.Unknown - } - - // event.contentChanges.length will be 2 when user press Enter key multiple times - if (this.contentChanges.length > 2) { - return DocumentChangedSource.Reformatting - } - - // Case when event.contentChanges.length === 1 - const changedText = this.contentChanges[0].text - - if (this.isSingleLine(changedText)) { - if (changedText === '') { - return DocumentChangedSource.Deletion - } else if (this.isEnterKey(changedText)) { - return DocumentChangedSource.EnterKey - } else if (this.isTabKey(changedText)) { - return DocumentChangedSource.TabKey - } else if (this.isUserTypingSpecialChar(changedText)) { - return DocumentChangedSource.SpecialCharsKey - } else if (changedText.length === 1) { - return DocumentChangedSource.RegularKey - } else if (new RegExp('^[ ]+$').test(changedText)) { - // single line && single place reformat should consist of space chars only - return DocumentChangedSource.Reformatting - } else { - return DocumentChangedSource.Unknown - } - } - - // Won't trigger cwspr on multi-line changes - return DocumentChangedSource.Unknown - } -} - -export enum DocumentChangedSource { - SpecialCharsKey = 'SpecialCharsKey', - RegularKey = 'RegularKey', - TabKey = 'TabKey', - EnterKey = 'EnterKey', - Reformatting = 'Reformatting', - Deletion = 'Deletion', - Unknown = 'Unknown', -} diff --git a/packages/core/src/codewhisperer/service/recommendationHandler.ts b/packages/core/src/codewhisperer/service/recommendationHandler.ts deleted file mode 100644 index 8ab491b32e0..00000000000 --- a/packages/core/src/codewhisperer/service/recommendationHandler.ts +++ /dev/null @@ -1,724 +0,0 @@ -/*! - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -import * as vscode from 'vscode' -import { extensionVersion } from '../../shared/vscode/env' -import { RecommendationsList, DefaultCodeWhispererClient, CognitoCredentialsError } from '../client/codewhisperer' -import * as EditorContext from '../util/editorContext' -import * as CodeWhispererConstants from '../models/constants' -import { ConfigurationEntry, GetRecommendationsResponse, vsCodeState } from '../models/model' -import { runtimeLanguageContext } from '../util/runtimeLanguageContext' -import { AWSError } from 'aws-sdk' -import { isAwsError } from '../../shared/errors' -import { TelemetryHelper } from '../util/telemetryHelper' -import { getLogger } from '../../shared/logger/logger' -import { hasVendedIamCredentials } from '../../auth/auth' -import { - asyncCallWithTimeout, - isInlineCompletionEnabled, - isVscHavingRegressionInlineCompletionApi, -} from '../util/commonUtil' -import { showTimedMessage } from '../../shared/utilities/messages' -import { - CodewhispererAutomatedTriggerType, - CodewhispererCompletionType, - CodewhispererGettingStartedTask, - CodewhispererTriggerType, - telemetry, -} from '../../shared/telemetry/telemetry' -import { CodeWhispererCodeCoverageTracker } from '../tracker/codewhispererCodeCoverageTracker' -import { invalidCustomizationMessage } from '../models/constants' -import { getSelectedCustomization, switchToBaseCustomizationAndNotify } from '../util/customizationUtil' -import { session } from '../util/codeWhispererSession' -import { Commands } from '../../shared/vscode/commands2' -import globals from '../../shared/extensionGlobals' -import { noSuggestions, updateInlineLockKey } from '../models/constants' -import AsyncLock from 'async-lock' -import { AuthUtil } from '../util/authUtil' -import { CWInlineCompletionItemProvider } from './inlineCompletionItemProvider' -import { application } from '../util/codeWhispererApplication' -import { openUrl } from '../../shared/utilities/vsCodeUtils' -import { indent } from '../../shared/utilities/textUtilities' -import path from 'path' -import { isIamConnection } from '../../auth/connection' -import { UserWrittenCodeTracker } from '../tracker/userWrittenCodeTracker' - -/** - * This class is for getRecommendation/listRecommendation API calls and its states - * It does not contain UI/UX related logic - */ - -/** - * Commands as a level of indirection so that declare doesn't intercept any registrations for the - * language server implementation. - * - * Otherwise you'll get: - * "Unable to launch amazonq language server: Command "aws.amazonq.rejectCodeSuggestion" has already been declared by the Toolkit" - */ -function createCommands() { - // below commands override VS Code inline completion commands - const prevCommand = Commands.declare('editor.action.inlineSuggest.showPrevious', () => async () => { - await RecommendationHandler.instance.showRecommendation(-1) - }) - const nextCommand = Commands.declare('editor.action.inlineSuggest.showNext', () => async () => { - await RecommendationHandler.instance.showRecommendation(1) - }) - - const rejectCommand = Commands.declare('aws.amazonq.rejectCodeSuggestion', () => async () => { - telemetry.record({ - traceId: TelemetryHelper.instance.traceId, - }) - - await vscode.commands.executeCommand('editor.action.inlineSuggest.hide') - RecommendationHandler.instance.reportUserDecisions(-1) - await Commands.tryExecute('aws.amazonq.refreshAnnotation') - }) - - return { - prevCommand, - nextCommand, - rejectCommand, - } -} - -const lock = new AsyncLock({ maxPending: 1 }) - -export class RecommendationHandler { - public lastInvocationTime: number - // TODO: remove this requestId - public requestId: string - private nextToken: string - private cancellationToken: vscode.CancellationTokenSource - private _onDidReceiveRecommendation: vscode.EventEmitter = new vscode.EventEmitter() - public readonly onDidReceiveRecommendation: vscode.Event = this._onDidReceiveRecommendation.event - private inlineCompletionProvider?: CWInlineCompletionItemProvider - private inlineCompletionProviderDisposable?: vscode.Disposable - private reject: vscode.Disposable - private next: vscode.Disposable - private prev: vscode.Disposable - private _timer?: NodeJS.Timer - documentUri: vscode.Uri | undefined = undefined - - constructor() { - this.requestId = '' - this.nextToken = '' - this.lastInvocationTime = performance.now() - CodeWhispererConstants.invocationTimeIntervalThreshold * 1000 - this.cancellationToken = new vscode.CancellationTokenSource() - this.prev = new vscode.Disposable(() => {}) - this.next = new vscode.Disposable(() => {}) - this.reject = new vscode.Disposable(() => {}) - } - - static #instance: RecommendationHandler - - public static get instance() { - return (this.#instance ??= new this()) - } - - isValidResponse(): boolean { - return session.recommendations.some((r) => r.content.trim() !== '') - } - - async getServerResponse( - triggerType: CodewhispererTriggerType, - isManualTriggerOn: boolean, - promise: Promise - ): Promise { - const timeoutMessage = hasVendedIamCredentials() - ? 'Generate recommendation timeout.' - : 'List recommendation timeout' - if (isManualTriggerOn && triggerType === 'OnDemand' && hasVendedIamCredentials()) { - return vscode.window.withProgress( - { - location: vscode.ProgressLocation.Notification, - title: CodeWhispererConstants.pendingResponse, - cancellable: false, - }, - async () => { - return await asyncCallWithTimeout( - promise, - timeoutMessage, - CodeWhispererConstants.promiseTimeoutLimit * 1000 - ) - } - ) - } - return await asyncCallWithTimeout(promise, timeoutMessage, CodeWhispererConstants.promiseTimeoutLimit * 1000) - } - - async getTaskTypeFromEditorFileName(filePath: string): Promise { - if (filePath.includes('CodeWhisperer_generate_suggestion')) { - return 'autoTrigger' - } else if (filePath.includes('CodeWhisperer_manual_invoke')) { - return 'manualTrigger' - } else if (filePath.includes('CodeWhisperer_use_comments')) { - return 'commentAsPrompt' - } else if (filePath.includes('CodeWhisperer_navigate_suggestions')) { - return 'navigation' - } else if (filePath.includes('Generate_unit_tests')) { - return 'unitTest' - } else { - return undefined - } - } - - async getRecommendations( - client: DefaultCodeWhispererClient, - editor: vscode.TextEditor, - triggerType: CodewhispererTriggerType, - config: ConfigurationEntry, - autoTriggerType?: CodewhispererAutomatedTriggerType, - pagination: boolean = true, - page: number = 0, - generate: boolean = isIamConnection(AuthUtil.instance.conn) - ): Promise { - let invocationResult: 'Succeeded' | 'Failed' = 'Failed' - let errorMessage: string | undefined = undefined - let errorCode: string | undefined = undefined - - if (!editor) { - return Promise.resolve({ - result: invocationResult, - errorMessage: errorMessage, - recommendationCount: 0, - }) - } - let recommendations: RecommendationsList = [] - let requestId = '' - let sessionId = '' - let reason = '' - let startTime = 0 - let latency = 0 - let nextToken = '' - let shouldRecordServiceInvocation = true - session.language = runtimeLanguageContext.getLanguageContext( - editor.document.languageId, - path.extname(editor.document.fileName) - ).language - session.taskType = await this.getTaskTypeFromEditorFileName(editor.document.fileName) - - if (pagination && !generate) { - if (page === 0) { - session.requestContext = await EditorContext.buildListRecommendationRequest( - editor as vscode.TextEditor, - this.nextToken, - config.isSuggestionsWithCodeReferencesEnabled - ) - } else { - session.requestContext = { - request: { - ...session.requestContext.request, - // Putting nextToken assignment in the end so it overwrites the existing nextToken - nextToken: this.nextToken, - }, - supplementalMetadata: session.requestContext.supplementalMetadata, - } - } - } else { - session.requestContext = await EditorContext.buildGenerateRecommendationRequest(editor as vscode.TextEditor) - } - const request = session.requestContext.request - // record preprocessing end time - TelemetryHelper.instance.setPreprocessEndTime() - - // set start pos for non pagination call or first pagination call - if (!pagination || (pagination && page === 0)) { - session.startPos = editor.selection.active - session.startCursorOffset = editor.document.offsetAt(session.startPos) - session.leftContextOfCurrentLine = EditorContext.getLeftContext(editor, session.startPos.line) - session.triggerType = triggerType - session.autoTriggerType = autoTriggerType - - /** - * Validate request - */ - if (!EditorContext.validateRequest(request)) { - getLogger().verbose('Invalid Request: %O', request) - const languageName = request.fileContext.programmingLanguage.languageName - if (!runtimeLanguageContext.isLanguageSupported(languageName)) { - errorMessage = `${languageName} is currently not supported by Amazon Q inline suggestions` - } - return Promise.resolve({ - result: invocationResult, - errorMessage: errorMessage, - recommendationCount: 0, - }) - } - } - - try { - startTime = performance.now() - this.lastInvocationTime = startTime - const mappedReq = runtimeLanguageContext.mapToRuntimeLanguage(request) - const codewhispererPromise = - pagination && !generate - ? client.listRecommendations(mappedReq) - : client.generateRecommendations(mappedReq) - const resp = await this.getServerResponse(triggerType, config.isManualTriggerEnabled, codewhispererPromise) - TelemetryHelper.instance.setSdkApiCallEndTime() - latency = startTime !== 0 ? performance.now() - startTime : 0 - if ('recommendations' in resp) { - recommendations = (resp && resp.recommendations) || [] - } else { - recommendations = (resp && resp.completions) || [] - } - invocationResult = 'Succeeded' - requestId = resp?.$response && resp?.$response?.requestId - nextToken = resp?.nextToken ? resp?.nextToken : '' - sessionId = resp?.$response?.httpResponse?.headers['x-amzn-sessionid'] - TelemetryHelper.instance.setFirstResponseRequestId(requestId) - if (page === 0) { - session.setTimeToFirstRecommendation(performance.now()) - } - if (nextToken === '') { - TelemetryHelper.instance.setAllPaginationEndTime() - } - } catch (error) { - if (error instanceof CognitoCredentialsError) { - shouldRecordServiceInvocation = false - } - if (latency === 0) { - latency = startTime !== 0 ? performance.now() - startTime : 0 - } - getLogger().error('amazonq inline-suggest: Invocation Exception : %s', (error as Error).message) - if (isAwsError(error)) { - errorMessage = error.message - requestId = error.requestId || '' - errorCode = error.code - reason = `CodeWhisperer Invocation Exception: ${error?.code ?? error?.name ?? 'unknown'}` - await this.onThrottlingException(error, triggerType) - - if (error?.code === 'AccessDeniedException' && errorMessage?.includes('no identity-based policy')) { - getLogger().error('amazonq inline-suggest: AccessDeniedException : %s', (error as Error).message) - void vscode.window - .showErrorMessage(`CodeWhisperer: ${error?.message}`, CodeWhispererConstants.settingsLearnMore) - .then(async (resp) => { - if (resp === CodeWhispererConstants.settingsLearnMore) { - void openUrl(vscode.Uri.parse(CodeWhispererConstants.learnMoreUri)) - } - }) - await vscode.commands.executeCommand('aws.amazonq.enableCodeSuggestions', false) - } - } else { - errorMessage = error instanceof Error ? error.message : String(error) - reason = error ? String(error) : 'unknown' - } - } finally { - const timezone = Intl.DateTimeFormat().resolvedOptions().timeZone - - let msg = indent( - `codewhisperer: request-id: ${requestId}, - timestamp(epoch): ${Date.now()}, - timezone: ${timezone}, - datetime: ${new Date().toLocaleString([], { timeZone: timezone })}, - vscode version: '${vscode.version}', - extension version: '${extensionVersion}', - filename: '${EditorContext.getFileName(editor)}', - left context of line: '${session.leftContextOfCurrentLine}', - line number: ${session.startPos.line}, - character location: ${session.startPos.character}, - latency: ${latency} ms. - Recommendations:`, - 4, - true - ).trimStart() - for (const [index, item] of recommendations.entries()) { - msg += `\n ${index.toString().padStart(2, '0')}: ${indent(item.content, 8, true).trim()}` - session.requestIdList.push(requestId) - } - getLogger('nextEditPrediction').debug(`codeWhisper request ${requestId}`) - if (invocationResult === 'Succeeded') { - CodeWhispererCodeCoverageTracker.getTracker(session.language)?.incrementServiceInvocationCount() - UserWrittenCodeTracker.instance.onQFeatureInvoked() - } else { - if ( - (errorMessage?.includes(invalidCustomizationMessage) && errorCode === 'AccessDeniedException') || - errorCode === 'ResourceNotFoundException' - ) { - getLogger() - .debug(`The selected customization is no longer available. Retrying with the default model. - Failed request id: ${requestId}`) - await switchToBaseCustomizationAndNotify() - await this.getRecommendations( - client, - editor, - triggerType, - config, - autoTriggerType, - pagination, - page, - true - ) - } - } - - if (shouldRecordServiceInvocation) { - TelemetryHelper.instance.recordServiceInvocationTelemetry( - requestId, - sessionId, - session.recommendations.length + recommendations.length - 1, - invocationResult, - latency, - session.language, - session.taskType, - reason, - session.requestContext.supplementalMetadata - ) - } - } - - if (this.isCancellationRequested()) { - return Promise.resolve({ - result: invocationResult, - errorMessage: errorMessage, - recommendationCount: session.recommendations.length, - }) - } - - const typedPrefix = editor.document - .getText(new vscode.Range(session.startPos, editor.selection.active)) - .replace('\r\n', '\n') - if (recommendations.length > 0) { - TelemetryHelper.instance.setTypeAheadLength(typedPrefix.length) - // mark suggestions that does not match typeahead when arrival as Discard - // these suggestions can be marked as Showed if typeahead can be removed with new inline API - for (const [i, r] of recommendations.entries()) { - const recommendationIndex = i + session.recommendations.length - if ( - !r.content.startsWith(typedPrefix) && - session.getSuggestionState(recommendationIndex) === undefined - ) { - session.setSuggestionState(recommendationIndex, 'Discard') - } - session.setCompletionType(recommendationIndex, r) - } - session.recommendations = pagination ? session.recommendations.concat(recommendations) : recommendations - if (isInlineCompletionEnabled() && this.hasAtLeastOneValidSuggestion(typedPrefix)) { - this._onDidReceiveRecommendation.fire() - } - } - - this.requestId = requestId - session.sessionId = sessionId - this.nextToken = nextToken - - // send Empty userDecision event if user receives no recommendations in this session at all. - if (invocationResult === 'Succeeded' && nextToken === '') { - // case 1: empty list of suggestion [] - if (session.recommendations.length === 0) { - session.requestIdList.push(requestId) - // Received an empty list of recommendations - TelemetryHelper.instance.recordUserDecisionTelemetryForEmptyList( - session.requestIdList, - sessionId, - page, - runtimeLanguageContext.getLanguageContext( - editor.document.languageId, - path.extname(editor.document.fileName) - ).language, - session.requestContext.supplementalMetadata - ) - } - // case 2: non empty list of suggestion but with (a) empty content or (b) non-matching typeahead - else if (!this.hasAtLeastOneValidSuggestion(typedPrefix)) { - this.reportUserDecisions(-1) - } - } - return Promise.resolve({ - result: invocationResult, - errorMessage: errorMessage, - recommendationCount: session.recommendations.length, - }) - } - - hasAtLeastOneValidSuggestion(typedPrefix: string): boolean { - return session.recommendations.some((r) => r.content.trim() !== '' && r.content.startsWith(typedPrefix)) - } - - cancelPaginatedRequest() { - this.nextToken = '' - this.cancellationToken.cancel() - } - - isCancellationRequested() { - return this.cancellationToken.token.isCancellationRequested - } - - checkAndResetCancellationTokens() { - if (this.isCancellationRequested()) { - this.cancellationToken.dispose() - this.cancellationToken = new vscode.CancellationTokenSource() - this.nextToken = '' - return true - } - return false - } - /** - * Clear recommendation state - */ - clearRecommendations() { - session.requestIdList = [] - session.recommendations = [] - session.suggestionStates = new Map() - session.completionTypes = new Map() - this.requestId = '' - session.sessionId = '' - this.nextToken = '' - session.requestContext.supplementalMetadata = undefined - } - - async clearInlineCompletionStates() { - try { - vsCodeState.isCodeWhispererEditing = false - application()._clearCodeWhispererUIListener.fire() - this.cancelPaginatedRequest() - this.clearRecommendations() - this.disposeInlineCompletion() - await vscode.commands.executeCommand('aws.amazonq.refreshStatusBar') - // fix a regression that requires user to hit Esc twice to clear inline ghost text - // because disposing a provider does not clear the UX - if (isVscHavingRegressionInlineCompletionApi()) { - await vscode.commands.executeCommand('editor.action.inlineSuggest.hide') - } - } finally { - this.clearRejectionTimer() - } - } - - reportDiscardedUserDecisions() { - for (const [i, _] of session.recommendations.entries()) { - session.setSuggestionState(i, 'Discard') - } - this.reportUserDecisions(-1) - } - - /** - * Emits telemetry reflecting user decision for current recommendation. - */ - reportUserDecisions(acceptIndex: number) { - if (session.sessionId === '' || this.requestId === '') { - return - } - TelemetryHelper.instance.recordUserDecisionTelemetry( - session.requestIdList, - session.sessionId, - session.recommendations, - acceptIndex, - session.recommendations.length, - session.completionTypes, - session.suggestionStates, - session.requestContext.supplementalMetadata - ) - if (isInlineCompletionEnabled()) { - this.clearInlineCompletionStates().catch((e) => { - getLogger().error('clearInlineCompletionStates failed: %s', (e as Error).message) - }) - } - } - - hasNextToken(): boolean { - return this.nextToken !== '' - } - - canShowRecommendationInIntelliSense( - editor: vscode.TextEditor, - showPrompt: boolean = false, - response: GetRecommendationsResponse - ): boolean { - const reject = () => { - this.reportUserDecisions(-1) - } - if (!this.isValidResponse()) { - if (showPrompt) { - void showTimedMessage(response.errorMessage ? response.errorMessage : noSuggestions, 3000) - } - reject() - return false - } - // do not show recommendation if cursor is before invocation position - // also mark as Discard - if (editor.selection.active.isBefore(session.startPos)) { - for (const [i, _] of session.recommendations.entries()) { - session.setSuggestionState(i, 'Discard') - } - reject() - return false - } - - // do not show recommendation if typeahead does not match - // also mark as Discard - const typedPrefix = editor.document.getText( - new vscode.Range( - session.startPos.line, - session.startPos.character, - editor.selection.active.line, - editor.selection.active.character - ) - ) - if (!session.recommendations[0].content.startsWith(typedPrefix.trimStart())) { - for (const [i, _] of session.recommendations.entries()) { - session.setSuggestionState(i, 'Discard') - } - reject() - return false - } - return true - } - - async onThrottlingException(awsError: AWSError, triggerType: CodewhispererTriggerType) { - if ( - awsError.code === 'ThrottlingException' && - awsError.message.includes(CodeWhispererConstants.throttlingMessage) - ) { - if (triggerType === 'OnDemand') { - void vscode.window.showErrorMessage(CodeWhispererConstants.freeTierLimitReached) - } - vsCodeState.isFreeTierLimitReached = true - } - } - - public disposeInlineCompletion() { - this.inlineCompletionProviderDisposable?.dispose() - this.inlineCompletionProvider = undefined - } - - private disposeCommandOverrides() { - this.prev.dispose() - this.reject.dispose() - this.next.dispose() - } - - // These commands override the vs code inline completion commands - // They are subscribed when suggestion starts and disposed when suggestion is accepted/rejected - // to avoid impacting other plugins or user who uses this API - private registerCommandOverrides() { - const { prevCommand, nextCommand, rejectCommand } = createCommands() - this.prev = prevCommand.register() - this.next = nextCommand.register() - this.reject = rejectCommand.register() - } - - subscribeSuggestionCommands() { - this.disposeCommandOverrides() - this.registerCommandOverrides() - globals.context.subscriptions.push(this.prev) - globals.context.subscriptions.push(this.next) - globals.context.subscriptions.push(this.reject) - } - - async showRecommendation(indexShift: number, noSuggestionVisible: boolean = false) { - await lock.acquire(updateInlineLockKey, async () => { - if (!vscode.window.state.focused) { - this.reportDiscardedUserDecisions() - return - } - const inlineCompletionProvider = new CWInlineCompletionItemProvider( - this.inlineCompletionProvider?.getActiveItemIndex, - indexShift, - session.recommendations, - this.requestId, - session.startPos, - this.nextToken - ) - this.inlineCompletionProviderDisposable?.dispose() - // when suggestion is active, registering a new provider will let VS Code invoke inline API automatically - this.inlineCompletionProviderDisposable = vscode.languages.registerInlineCompletionItemProvider( - Object.assign([], CodeWhispererConstants.platformLanguageIds), - inlineCompletionProvider - ) - this.inlineCompletionProvider = inlineCompletionProvider - - if (isVscHavingRegressionInlineCompletionApi() && !noSuggestionVisible) { - // fix a regression in new VS Code when disposing and re-registering - // a new provider does not auto refresh the inline suggestion widget - // by manually refresh it - await vscode.commands.executeCommand('editor.action.inlineSuggest.hide') - await vscode.commands.executeCommand('editor.action.inlineSuggest.trigger') - } - if (noSuggestionVisible) { - await vscode.commands.executeCommand(`editor.action.inlineSuggest.trigger`) - this.sendPerceivedLatencyTelemetry() - } - }) - } - - async onEditorChange() { - this.reportUserDecisions(-1) - } - - async onFocusChange() { - this.reportUserDecisions(-1) - } - - async onCursorChange(e: vscode.TextEditorSelectionChangeEvent) { - // we do not want to reset the states for keyboard events because they can be typeahead - if ( - e.kind !== vscode.TextEditorSelectionChangeKind.Keyboard && - vscode.window.activeTextEditor === e.textEditor - ) { - application()._clearCodeWhispererUIListener.fire() - // when cursor change due to mouse movement we need to reset the active item index for inline - if (e.kind === vscode.TextEditorSelectionChangeKind.Mouse) { - this.inlineCompletionProvider?.clearActiveItemIndex() - } - } - } - - isSuggestionVisible(): boolean { - return this.inlineCompletionProvider?.getActiveItemIndex !== undefined - } - - async tryShowRecommendation() { - const editor = vscode.window.activeTextEditor - if (editor === undefined) { - return - } - if (this.isSuggestionVisible()) { - // do not force refresh the tooltip to avoid suggestion "flashing" - return - } - if ( - editor.selection.active.isBefore(session.startPos) || - editor.document.uri.fsPath !== this.documentUri?.fsPath - ) { - for (const [i, _] of session.recommendations.entries()) { - session.setSuggestionState(i, 'Discard') - } - this.reportUserDecisions(-1) - } else if (session.recommendations.length > 0) { - await this.showRecommendation(0, true) - } - } - - private clearRejectionTimer() { - if (this._timer !== undefined) { - clearInterval(this._timer) - this._timer = undefined - } - } - - private sendPerceivedLatencyTelemetry() { - if (vscode.window.activeTextEditor) { - const languageContext = runtimeLanguageContext.getLanguageContext( - vscode.window.activeTextEditor.document.languageId, - vscode.window.activeTextEditor.document.fileName.substring( - vscode.window.activeTextEditor.document.fileName.lastIndexOf('.') + 1 - ) - ) - telemetry.codewhisperer_perceivedLatency.emit({ - codewhispererRequestId: this.requestId, - codewhispererSessionId: session.sessionId, - codewhispererTriggerType: session.triggerType, - codewhispererCompletionType: session.getCompletionType(0), - codewhispererCustomizationArn: getSelectedCustomization().arn, - codewhispererLanguage: languageContext.language, - duration: performance.now() - this.lastInvocationTime, - passive: true, - credentialStartUrl: AuthUtil.instance.startUrl, - result: 'Succeeded', - }) - } - } -} diff --git a/packages/core/src/codewhisperer/service/recommendationService.ts b/packages/core/src/codewhisperer/service/recommendationService.ts deleted file mode 100644 index de78b435913..00000000000 --- a/packages/core/src/codewhisperer/service/recommendationService.ts +++ /dev/null @@ -1,122 +0,0 @@ -/*! - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ -import * as vscode from 'vscode' -import { ConfigurationEntry, GetRecommendationsResponse } from '../models/model' -import { isInlineCompletionEnabled } from '../util/commonUtil' -import { - CodewhispererAutomatedTriggerType, - CodewhispererTriggerType, - telemetry, -} from '../../shared/telemetry/telemetry' -import { InlineCompletionService } from '../service/inlineCompletionService' -import { ClassifierTrigger } from './classifierTrigger' -import { DefaultCodeWhispererClient } from '../client/codewhisperer' -import { randomUUID } from '../../shared/crypto' -import { TelemetryHelper } from '../util/telemetryHelper' -import { AuthUtil } from '../util/authUtil' - -export interface SuggestionActionEvent { - readonly editor: vscode.TextEditor | undefined - readonly isRunning: boolean - readonly triggerType: CodewhispererTriggerType - readonly response: GetRecommendationsResponse | undefined -} - -export class RecommendationService { - static #instance: RecommendationService - - private _isRunning: boolean = false - get isRunning() { - return this._isRunning - } - - private _onSuggestionActionEvent = new vscode.EventEmitter() - get suggestionActionEvent(): vscode.Event { - return this._onSuggestionActionEvent.event - } - - private _acceptedSuggestionCount: number = 0 - get acceptedSuggestionCount() { - return this._acceptedSuggestionCount - } - - private _totalValidTriggerCount: number = 0 - get totalValidTriggerCount() { - return this._totalValidTriggerCount - } - - public static get instance() { - return (this.#instance ??= new RecommendationService()) - } - - incrementAcceptedCount() { - this._acceptedSuggestionCount++ - } - - incrementValidTriggerCount() { - this._totalValidTriggerCount++ - } - - async generateRecommendation( - client: DefaultCodeWhispererClient, - editor: vscode.TextEditor, - triggerType: CodewhispererTriggerType, - config: ConfigurationEntry, - autoTriggerType?: CodewhispererAutomatedTriggerType, - event?: vscode.TextDocumentChangeEvent - ) { - // TODO: should move all downstream auth check(inlineCompletionService, recommendationHandler etc) to here(upstream) instead of spreading everywhere - if (AuthUtil.instance.isConnected() && AuthUtil.instance.requireProfileSelection()) { - return - } - - if (this._isRunning) { - return - } - - /** - * Use an existing trace ID if invoked through a command (e.g., manual invocation), - * otherwise generate a new trace ID - */ - const traceId = telemetry.attributes?.traceId ?? randomUUID() - TelemetryHelper.instance.setTraceId(traceId) - await telemetry.withTraceId(async () => { - if (isInlineCompletionEnabled()) { - if (triggerType === 'OnDemand') { - ClassifierTrigger.instance.recordClassifierResultForManualTrigger(editor) - } - - this._isRunning = true - let response: GetRecommendationsResponse | undefined = undefined - - try { - this._onSuggestionActionEvent.fire({ - editor: editor, - isRunning: true, - triggerType: triggerType, - response: undefined, - }) - - response = await InlineCompletionService.instance.getPaginatedRecommendation( - client, - editor, - triggerType, - config, - autoTriggerType, - event - ) - } finally { - this._isRunning = false - this._onSuggestionActionEvent.fire({ - editor: editor, - isRunning: false, - triggerType: triggerType, - response: response, - }) - } - } - }, traceId) - } -} diff --git a/packages/core/src/codewhisperer/service/referenceLogViewProvider.ts b/packages/core/src/codewhisperer/service/referenceLogViewProvider.ts index 9ec20b8cb44..d51424b1c46 100644 --- a/packages/core/src/codewhisperer/service/referenceLogViewProvider.ts +++ b/packages/core/src/codewhisperer/service/referenceLogViewProvider.ts @@ -4,13 +4,15 @@ */ import * as vscode from 'vscode' -import { References } from '../client/codewhisperer' import { LicenseUtil } from '../util/licenseUtil' import * as CodeWhispererConstants from '../models/constants' import { CodeWhispererSettings } from '../util/codewhispererSettings' import globals from '../../shared/extensionGlobals' import { AuthUtil } from '../util/authUtil' import { session } from '../util/codeWhispererSession' +import CodeWhispererClient from '../client/codewhispererclient' +import CodeWhispererUserClient from '../client/codewhispereruserclient' +import { InlineCompletionItemWithReferences } from '@aws/language-server-runtimes-types' export class ReferenceLogViewProvider implements vscode.WebviewViewProvider { public static readonly viewType = 'aws.codeWhisperer.referenceLog' @@ -52,28 +54,23 @@ export class ReferenceLogViewProvider implements vscode.WebviewViewProvider { } } - public static getReferenceLog(recommendation: string, references: References, editor: vscode.TextEditor): string { + public static getReferenceLog(recommendation: string, references: Reference[], editor: vscode.TextEditor): string { const filePath = editor.document.uri.path const time = new Date().toLocaleString() let text = `` for (const reference of references) { + const standardReference = toStandardReference(reference) if ( - reference.recommendationContentSpan === undefined || - reference.recommendationContentSpan.start === undefined || - reference.recommendationContentSpan.end === undefined + standardReference.position === undefined || + standardReference.position.start === undefined || + standardReference.position.end === undefined ) { continue } - const code = recommendation.substring( - reference.recommendationContentSpan.start, - reference.recommendationContentSpan.end - ) - const firstCharLineNumber = - editor.document.positionAt(session.startCursorOffset + reference.recommendationContentSpan.start).line + - 1 - const lastCharLineNumber = - editor.document.positionAt(session.startCursorOffset + reference.recommendationContentSpan.end - 1) - .line + 1 + const { start, end } = standardReference.position + const code = recommendation.substring(start, end) + const firstCharLineNumber = editor.document.positionAt(session.startCursorOffset + start).line + 1 + const lastCharLineNumber = editor.document.positionAt(session.startCursorOffset + end - 1).line + 1 let lineInfo = `` if (firstCharLineNumber === lastCharLineNumber) { lineInfo = `(line at ${firstCharLineNumber})` @@ -84,11 +81,11 @@ export class ReferenceLogViewProvider implements vscode.WebviewViewProvider { text += `And ` } - let license = `${reference.licenseName}` - let repository = reference.repository?.length ? reference.repository : 'unknown' - if (reference.url?.length) { - repository = `${reference.repository}` - license = `${reference.licenseName || 'unknown'}` + let license = `${standardReference.licenseName}` + let repository = standardReference.repository?.length ? standardReference.repository : 'unknown' + if (standardReference.url?.length) { + repository = `${standardReference.repository}` + license = `${standardReference.licenseName || 'unknown'}` } text += @@ -144,3 +141,48 @@ export class ReferenceLogViewProvider implements vscode.WebviewViewProvider { ` } } + +/** + * Reference log needs to support references directly from CW, as well as those from Flare. These references have different shapes, so we standarize them here. + */ +type GetInnerType = T extends (infer U)[] ? U : never +type Reference = + | CodeWhispererClient.Reference + | CodeWhispererUserClient.Reference + | GetInnerType + +type StandardizedReference = { + licenseName?: string + position?: { + start?: number + end?: number + } + repository?: string + url?: string +} + +/** + * Convert a general reference to the standardized format expected by the reference log. + * @param ref + * @returns + */ +function toStandardReference(ref: Reference): StandardizedReference { + const isCWReference = (ref: any) => ref.recommendationContentSpan !== undefined + + if (isCWReference(ref)) { + const castRef = ref as CodeWhispererClient.Reference + return { + licenseName: castRef.licenseName!, + position: { start: castRef.recommendationContentSpan?.start, end: castRef.recommendationContentSpan?.end }, + repository: castRef.repository, + url: castRef.url, + } + } + const castRef = ref as GetInnerType + return { + licenseName: castRef.licenseName, + position: { start: castRef.position?.startCharacter, end: castRef.position?.endCharacter }, + repository: castRef.referenceName, + url: castRef.referenceUrl, + } +} diff --git a/packages/core/src/codewhisperer/service/statusBar.ts b/packages/core/src/codewhisperer/service/statusBar.ts new file mode 100644 index 00000000000..6aacfec73b7 --- /dev/null +++ b/packages/core/src/codewhisperer/service/statusBar.ts @@ -0,0 +1,147 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +import * as vscode from 'vscode' +import { CodeSuggestionsState } from '../models/model' +import { AuthUtil } from '../util/authUtil' +import { getSelectedCustomization } from '../util/customizationUtil' +import { codicon, getIcon } from '../../shared/icons' +import { Commands } from '../../shared/vscode/commands2' +import { listCodeWhispererCommandsId } from '../ui/statusBarMenu' + +export class CodeWhispererStatusBarManager { + private statusBar: CodeWhispererStatusBar + + constructor(statusBar: CodeWhispererStatusBar = CodeWhispererStatusBar.instance) { + this.statusBar = statusBar + + CodeSuggestionsState.instance.onDidChangeState(() => { + return this.refreshStatusBar() + }) + } + + static #instance: CodeWhispererStatusBarManager + + public static get instance() { + return (this.#instance ??= new this()) + } + + /** Updates the status bar to represent the latest CW state */ + refreshStatusBar() { + if (AuthUtil.instance.isConnectionValid()) { + if (AuthUtil.instance.requireProfileSelection()) { + return this.setState('needsProfile') + } + return this.setState('ok') + } else if (AuthUtil.instance.isConnectionExpired()) { + return this.setState('expired') + } else { + return this.setState('notConnected') + } + } + + /** + * Sets the status bar in to a "loading state", effectively showing + * the spinning circle. + * + * When loading is done, call {@link refreshStatusBar} to update the + * status bar to the latest state. + */ + async setLoading(): Promise { + await this.setState('loading') + } + + private async setState(state: keyof typeof states) { + switch (state) { + case 'loading': { + await this.statusBar.setState('loading') + break + } + case 'ok': { + await this.statusBar.setState('ok', CodeSuggestionsState.instance.isSuggestionsEnabled()) + break + } + case 'expired': { + await this.statusBar.setState('expired') + break + } + case 'notConnected': { + await this.statusBar.setState('notConnected') + break + } + case 'needsProfile': { + await this.statusBar.setState('needsProfile') + break + } + } + } +} + +/** The states that the completion service can be in */ +const states = { + loading: 'loading', + ok: 'ok', + expired: 'expired', + notConnected: 'notConnected', + needsProfile: 'needsProfile', +} as const + +class CodeWhispererStatusBar { + protected statusBar: vscode.StatusBarItem = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Left, 1) + + static #instance: CodeWhispererStatusBar + static get instance() { + return (this.#instance ??= new this()) + } + + protected constructor() {} + + async setState(state: keyof Omit): Promise + async setState(status: keyof Pick, isSuggestionsEnabled: boolean): Promise + async setState(status: keyof typeof states, isSuggestionsEnabled?: boolean): Promise { + const statusBar = this.statusBar + statusBar.command = listCodeWhispererCommandsId + statusBar.backgroundColor = undefined + + const title = 'Amazon Q' + switch (status) { + case 'loading': { + const selectedCustomization = getSelectedCustomization() + statusBar.text = codicon` ${getIcon('vscode-loading~spin')} ${title}${ + selectedCustomization.arn === '' ? '' : ` | ${selectedCustomization.name}` + }` + break + } + case 'ok': { + const selectedCustomization = getSelectedCustomization() + const icon = isSuggestionsEnabled ? getIcon('vscode-debug-start') : getIcon('vscode-debug-pause') + statusBar.text = codicon`${icon} ${title}${ + selectedCustomization.arn === '' ? '' : ` | ${selectedCustomization.name}` + }` + break + } + + case 'expired': { + statusBar.text = codicon` ${getIcon('vscode-debug-disconnect')} ${title}` + statusBar.backgroundColor = new vscode.ThemeColor('statusBarItem.warningBackground') + break + } + case 'needsProfile': + case 'notConnected': + statusBar.text = codicon` ${getIcon('vscode-chrome-close')} ${title}` + statusBar.backgroundColor = new vscode.ThemeColor('statusBarItem.errorBackground') + break + } + + statusBar.show() + } +} + +/** In this module due to circular dependency issues */ +export const refreshStatusBar = Commands.declare( + { id: 'aws.amazonq.refreshStatusBar', logging: false }, + () => async () => { + await CodeWhispererStatusBarManager.instance.refreshStatusBar() + } +) diff --git a/packages/core/src/codewhisperer/tracker/codewhispererCodeCoverageTracker.ts b/packages/core/src/codewhisperer/tracker/codewhispererCodeCoverageTracker.ts deleted file mode 100644 index 0989f022245..00000000000 --- a/packages/core/src/codewhisperer/tracker/codewhispererCodeCoverageTracker.ts +++ /dev/null @@ -1,319 +0,0 @@ -/*! - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -import * as vscode from 'vscode' -import { getLogger } from '../../shared/logger/logger' -import * as CodeWhispererConstants from '../models/constants' -import globals from '../../shared/extensionGlobals' -import { vsCodeState } from '../models/model' -import { CodewhispererLanguage, telemetry } from '../../shared/telemetry/telemetry' -import { runtimeLanguageContext } from '../util/runtimeLanguageContext' -import { TelemetryHelper } from '../util/telemetryHelper' -import { AuthUtil } from '../util/authUtil' -import { getSelectedCustomization } from '../util/customizationUtil' -import { codeWhispererClient as client } from '../client/codewhisperer' -import { isAwsError } from '../../shared/errors' -import { getUnmodifiedAcceptedTokens } from '../util/commonUtil' - -interface CodeWhispererToken { - range: vscode.Range - text: string - accepted: number -} - -const autoClosingKeystrokeInputs = ['[]', '{}', '()', '""', "''"] - -/** - * This singleton class is mainly used for calculating the code written by codeWhisperer - * TODO: Remove this tracker, uses user written code tracker instead. - * This is kept in codebase for server side backward compatibility until service fully switch to user written code - */ -export class CodeWhispererCodeCoverageTracker { - private _acceptedTokens: { [key: string]: CodeWhispererToken[] } - private _totalTokens: { [key: string]: number } - private _timer?: NodeJS.Timer - private _startTime: number - private _language: CodewhispererLanguage - private _serviceInvocationCount: number - - private constructor(language: CodewhispererLanguage) { - this._acceptedTokens = {} - this._totalTokens = {} - this._startTime = 0 - this._language = language - this._serviceInvocationCount = 0 - } - - public get serviceInvocationCount(): number { - return this._serviceInvocationCount - } - - public get acceptedTokens(): { [key: string]: CodeWhispererToken[] } { - return this._acceptedTokens - } - - public get totalTokens(): { [key: string]: number } { - return this._totalTokens - } - - public isActive(): boolean { - return TelemetryHelper.instance.isTelemetryEnabled() && AuthUtil.instance.isConnected() - } - - public incrementServiceInvocationCount() { - this._serviceInvocationCount += 1 - } - - public flush() { - if (!this.isActive()) { - this._totalTokens = {} - this._acceptedTokens = {} - this.closeTimer() - return - } - try { - this.emitCodeWhispererCodeContribution() - } catch (error) { - getLogger().error(`Encountered ${error} when emitting code contribution metric`) - } - } - - // TODO: Improve the range tracking of the accepted recommendation - // TODO: use the editor of the filename, not the current editor - public updateAcceptedTokensCount(editor: vscode.TextEditor) { - const filename = editor.document.fileName - if (filename in this._acceptedTokens) { - for (let i = 0; i < this._acceptedTokens[filename].length; i++) { - const oldText = this._acceptedTokens[filename][i].text - const newText = editor.document.getText(this._acceptedTokens[filename][i].range) - this._acceptedTokens[filename][i].accepted = getUnmodifiedAcceptedTokens(oldText, newText) - } - } - } - - public emitCodeWhispererCodeContribution() { - let totalTokens = 0 - for (const filename in this._totalTokens) { - totalTokens += this._totalTokens[filename] - } - if (vscode.window.activeTextEditor) { - this.updateAcceptedTokensCount(vscode.window.activeTextEditor) - } - // the accepted characters without counting user modification - let acceptedTokens = 0 - // the accepted characters after calculating user modification - let unmodifiedAcceptedTokens = 0 - for (const filename in this._acceptedTokens) { - for (const v of this._acceptedTokens[filename]) { - if (filename in this._totalTokens && this._totalTokens[filename] >= v.accepted) { - unmodifiedAcceptedTokens += v.accepted - acceptedTokens += v.text.length - } - } - } - const percentCount = ((acceptedTokens / totalTokens) * 100).toFixed(2) - const percentage = Math.round(parseInt(percentCount)) - const selectedCustomization = getSelectedCustomization() - if (this._serviceInvocationCount <= 0) { - getLogger().debug(`Skip emiting code contribution metric`) - return - } - telemetry.codewhisperer_codePercentage.emit({ - codewhispererTotalTokens: totalTokens, - codewhispererLanguage: this._language, - codewhispererAcceptedTokens: unmodifiedAcceptedTokens, - codewhispererSuggestedTokens: acceptedTokens, - codewhispererPercentage: percentage ? percentage : 0, - successCount: this._serviceInvocationCount, - codewhispererCustomizationArn: selectedCustomization.arn === '' ? undefined : selectedCustomization.arn, - credentialStartUrl: AuthUtil.instance.startUrl, - }) - - client - .sendTelemetryEvent({ - telemetryEvent: { - codeCoverageEvent: { - customizationArn: selectedCustomization.arn === '' ? undefined : selectedCustomization.arn, - programmingLanguage: { - languageName: runtimeLanguageContext.toRuntimeLanguage(this._language), - }, - acceptedCharacterCount: acceptedTokens, - unmodifiedAcceptedCharacterCount: unmodifiedAcceptedTokens, - totalCharacterCount: totalTokens, - timestamp: new Date(Date.now()), - }, - }, - profileArn: AuthUtil.instance.regionProfileManager.activeRegionProfile?.arn, - }) - .then() - .catch((error) => { - let requestId: string | undefined - if (isAwsError(error)) { - requestId = error.requestId - } - - getLogger().debug( - `Failed to sendTelemetryEvent to CodeWhisperer, requestId: ${requestId ?? ''}, message: ${ - error.message - }` - ) - }) - } - - private tryStartTimer() { - if (this._timer !== undefined) { - return - } - const currentDate = new globals.clock.Date() - this._startTime = currentDate.getTime() - this._timer = setTimeout(() => { - try { - const currentTime = new globals.clock.Date().getTime() - const delay: number = CodeWhispererConstants.defaultCheckPeriodMillis - const diffTime: number = this._startTime + delay - if (diffTime <= currentTime) { - let totalTokens = 0 - for (const filename in this._totalTokens) { - totalTokens += this._totalTokens[filename] - } - if (totalTokens > 0) { - this.flush() - } else { - getLogger().debug( - `CodeWhispererCodeCoverageTracker: skipped telemetry due to empty tokens array` - ) - } - } - } catch (e) { - getLogger().verbose(`Exception Thrown from CodeWhispererCodeCoverageTracker: ${e}`) - } finally { - this.resetTracker() - this.closeTimer() - } - }, CodeWhispererConstants.defaultCheckPeriodMillis) - } - - private resetTracker() { - this._totalTokens = {} - this._acceptedTokens = {} - this._startTime = 0 - this._serviceInvocationCount = 0 - } - - private closeTimer() { - if (this._timer !== undefined) { - clearTimeout(this._timer) - this._timer = undefined - } - } - - public addAcceptedTokens(filename: string, token: CodeWhispererToken) { - if (!(filename in this._acceptedTokens)) { - this._acceptedTokens[filename] = [] - } - this._acceptedTokens[filename].push(token) - } - - public addTotalTokens(filename: string, count: number) { - if (!(filename in this._totalTokens)) { - this._totalTokens[filename] = 0 - } - this._totalTokens[filename] += count - if (this._totalTokens[filename] < 0) { - this._totalTokens[filename] = 0 - } - } - - public countAcceptedTokens(range: vscode.Range, text: string, filename: string) { - if (!this.isActive()) { - return - } - // generate accepted recommendation token and stored in collection - this.addAcceptedTokens(filename, { range: range, text: text, accepted: text.length }) - this.addTotalTokens(filename, text.length) - } - - // For below 2 edge cases - // 1. newline character with indentation - // 2. 2 character insertion of closing brackets - public getCharacterCountFromComplexEvent(e: vscode.TextDocumentChangeEvent) { - function countChanges(cond: boolean, text: string): number { - if (!cond) { - return 0 - } - if ((text.startsWith('\n') || text.startsWith('\r\n')) && text.trim().length === 0) { - return 1 - } - if (autoClosingKeystrokeInputs.includes(text)) { - return 2 - } - return 0 - } - if (e.contentChanges.length === 2) { - const text1 = e.contentChanges[0].text - const text2 = e.contentChanges[1].text - const text2Count = countChanges(text1.length === 0, text2) - const text1Count = countChanges(text2.length === 0, text1) - return text2Count > 0 ? text2Count : text1Count - } else if (e.contentChanges.length === 1) { - return countChanges(true, e.contentChanges[0].text) - } - return 0 - } - - public isFromUserKeystroke(e: vscode.TextDocumentChangeEvent) { - return e.contentChanges.length === 1 && e.contentChanges[0].text.length === 1 - } - - public countTotalTokens(e: vscode.TextDocumentChangeEvent) { - // ignore no contentChanges. ignore contentChanges from other plugins (formatters) - // only include contentChanges from user keystroke input(one character input). - // Also ignore deletion events due to a known issue of tracking deleted CodeWhiperer tokens. - if (!runtimeLanguageContext.isLanguageSupported(e.document.languageId) || vsCodeState.isCodeWhispererEditing) { - return - } - // a user keystroke input can be - // 1. content change with 1 character insertion - // 2. newline character with indentation - // 3. 2 character insertion of closing brackets - if (this.isFromUserKeystroke(e)) { - this.tryStartTimer() - this.addTotalTokens(e.document.fileName, 1) - } else if (this.getCharacterCountFromComplexEvent(e) !== 0) { - this.tryStartTimer() - const characterIncrease = this.getCharacterCountFromComplexEvent(e) - this.addTotalTokens(e.document.fileName, characterIncrease) - } - // also include multi character input within 50 characters (not from CWSPR) - else if ( - e.contentChanges.length === 1 && - e.contentChanges[0].text.length > 1 && - TelemetryHelper.instance.lastSuggestionInDisplay !== e.contentChanges[0].text - ) { - const multiCharInputSize = e.contentChanges[0].text.length - - // select 50 as the cut-off threshold for counting user input. - // ignore all white space multi char input, this usually comes from reformat. - if (multiCharInputSize < 50 && e.contentChanges[0].text.trim().length > 0) { - this.addTotalTokens(e.document.fileName, multiCharInputSize) - } - } - } - - public static readonly instances = new Map() - - public static getTracker(language: string): CodeWhispererCodeCoverageTracker | undefined { - if (!runtimeLanguageContext.isLanguageSupported(language)) { - return undefined - } - const cwsprLanguage = runtimeLanguageContext.normalizeLanguage(language) - if (!cwsprLanguage) { - return undefined - } - const instance = this.instances.get(cwsprLanguage) ?? new this(cwsprLanguage) - this.instances.set(cwsprLanguage, instance) - return instance - } -} diff --git a/packages/core/src/codewhisperer/util/closingBracketUtil.ts b/packages/core/src/codewhisperer/util/closingBracketUtil.ts deleted file mode 100644 index 466ca31a0b9..00000000000 --- a/packages/core/src/codewhisperer/util/closingBracketUtil.ts +++ /dev/null @@ -1,262 +0,0 @@ -/*! - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -import * as vscode from 'vscode' -import * as CodeWhispererConstants from '../models/constants' - -interface bracketMapType { - [k: string]: string -} - -const quotes = ["'", '"', '`'] -const parenthesis = ['(', '[', '{', ')', ']', '}', '<', '>'] - -const closeToOpen: bracketMapType = { - ')': '(', - ']': '[', - '}': '{', - '>': '<', -} - -const openToClose: bracketMapType = { - '(': ')', - '[': ']', - '{': '}', - '<': '>', -} - -/** - * LeftContext | Recommendation | RightContext - * This function aims to resolve symbols which are redundant and need to be removed - * The high level logic is as followed - * 1. Pair non-paired closing symbols(parenthesis, brackets, quotes) existing in the "recommendation" with non-paired symbols existing in the "leftContext" - * 2. Remove non-paired closing symbols existing in the "rightContext" - * @param endPosition: end position of the effective recommendation written by CodeWhisperer - * @param startPosition: start position of the effective recommendation by CodeWhisperer - * - * for example given file context ('|' is where we trigger the service): - * anArray.pu| - * recommendation returned: "sh(element);" - * typeahead: "sh(" - * the effective recommendation written by CodeWhisperer: "element);" - */ -export async function handleExtraBrackets( - editor: vscode.TextEditor, - endPosition: vscode.Position, - startPosition: vscode.Position -) { - const recommendation = editor.document.getText(new vscode.Range(startPosition, endPosition)) - const endOffset = editor.document.offsetAt(endPosition) - const startOffset = editor.document.offsetAt(startPosition) - const leftContext = editor.document.getText( - new vscode.Range( - startPosition, - editor.document.positionAt(Math.max(startOffset - CodeWhispererConstants.charactersLimit, 0)) - ) - ) - - const rightContext = editor.document.getText( - new vscode.Range( - editor.document.positionAt(endOffset), - editor.document.positionAt(endOffset + CodeWhispererConstants.charactersLimit) - ) - ) - const bracketsToRemove = getBracketsToRemove( - editor, - recommendation, - leftContext, - rightContext, - endPosition, - startPosition - ) - - const quotesToRemove = getQuotesToRemove( - editor, - recommendation, - leftContext, - rightContext, - endPosition, - startPosition - ) - - const symbolsToRemove = [...bracketsToRemove, ...quotesToRemove] - - if (symbolsToRemove.length) { - await removeBracketsFromRightContext(editor, symbolsToRemove, endPosition) - } -} - -const removeBracketsFromRightContext = async ( - editor: vscode.TextEditor, - idxToRemove: number[], - endPosition: vscode.Position -) => { - const offset = editor.document.offsetAt(endPosition) - - await editor.edit( - (editBuilder) => { - for (const idx of idxToRemove) { - const range = new vscode.Range( - editor.document.positionAt(offset + idx), - editor.document.positionAt(offset + idx + 1) - ) - editBuilder.delete(range) - } - }, - { undoStopAfter: false, undoStopBefore: false } - ) -} - -function getBracketsToRemove( - editor: vscode.TextEditor, - recommendation: string, - leftContext: string, - rightContext: string, - end: vscode.Position, - start: vscode.Position -) { - const unpairedClosingsInReco = nonClosedClosingParen(recommendation) - const unpairedOpeningsInLeftContext = nonClosedOpneingParen(leftContext, unpairedClosingsInReco.length) - const unpairedClosingsInRightContext = nonClosedClosingParen(rightContext) - - const toRemove: number[] = [] - - let i = 0 - let j = 0 - let k = 0 - while (i < unpairedOpeningsInLeftContext.length && j < unpairedClosingsInReco.length) { - const opening = unpairedOpeningsInLeftContext[i] - const closing = unpairedClosingsInReco[j] - - const isPaired = closeToOpen[closing.char] === opening.char - const rightContextCharToDelete = unpairedClosingsInRightContext[k] - - if (isPaired) { - if (rightContextCharToDelete && rightContextCharToDelete.char === closing.char) { - const rightContextStart = editor.document.offsetAt(end) + 1 - const symbolPosition = editor.document.positionAt( - rightContextStart + rightContextCharToDelete.strOffset - ) - const lineCnt = recommendation.split('\n').length - 1 - const isSameline = symbolPosition.line - lineCnt === start.line - - if (isSameline) { - toRemove.push(rightContextCharToDelete.strOffset) - } - - k++ - } - } - - i++ - j++ - } - - return toRemove -} - -function getQuotesToRemove( - editor: vscode.TextEditor, - recommendation: string, - leftContext: string, - rightContext: string, - endPosition: vscode.Position, - startPosition: vscode.Position -) { - let leftQuote: string | undefined = undefined - let leftIndex: number | undefined = undefined - for (let i = leftContext.length - 1; i >= 0; i--) { - const char = leftContext[i] - if (quotes.includes(char)) { - leftQuote = char - leftIndex = leftContext.length - i - break - } - } - - let rightQuote: string | undefined = undefined - let rightIndex: number | undefined = undefined - for (let i = 0; i < rightContext.length; i++) { - const char = rightContext[i] - if (quotes.includes(char)) { - rightQuote = char - rightIndex = i - break - } - } - - let quoteCountInReco = 0 - if (leftQuote && rightQuote && leftQuote === rightQuote) { - for (const char of recommendation) { - if (quotes.includes(char) && char === leftQuote) { - quoteCountInReco++ - } - } - } - - if (leftIndex !== undefined && rightIndex !== undefined && quoteCountInReco % 2 !== 0) { - const p = editor.document.positionAt(editor.document.offsetAt(endPosition) + rightIndex) - - if (endPosition.line === startPosition.line && endPosition.line === p.line) { - return [rightIndex] - } - } - - return [] -} - -function nonClosedOpneingParen(str: string, cnt?: number): { char: string; strOffset: number }[] { - const resultSet: { char: string; strOffset: number }[] = [] - const stack: string[] = [] - - for (let i = str.length - 1; i >= 0; i--) { - const char = str[i] - if (char! in parenthesis) { - continue - } - - if (char in closeToOpen) { - stack.push(char) - if (cnt && cnt === resultSet.length) { - return resultSet - } - } else if (char in openToClose) { - if (stack.length !== 0 && stack[stack.length - 1] === openToClose[char]) { - stack.pop() - } else { - resultSet.push({ char: char, strOffset: i }) - } - } - } - - return resultSet -} - -function nonClosedClosingParen(str: string, cnt?: number): { char: string; strOffset: number }[] { - const resultSet: { char: string; strOffset: number }[] = [] - const stack: string[] = [] - - for (let i = 0; i < str.length; i++) { - const char = str[i] - if (char! in parenthesis) { - continue - } - - if (char in openToClose) { - stack.push(char) - if (cnt && cnt === resultSet.length) { - return resultSet - } - } else if (char in closeToOpen) { - if (stack.length !== 0 && stack[stack.length - 1] === closeToOpen[char]) { - stack.pop() - } else { - resultSet.push({ char: char, strOffset: i }) - } - } - } - - return resultSet -} diff --git a/packages/core/src/codewhisperer/util/commonUtil.ts b/packages/core/src/codewhisperer/util/commonUtil.ts index d2df78f1369..729d3b7ed12 100644 --- a/packages/core/src/codewhisperer/util/commonUtil.ts +++ b/packages/core/src/codewhisperer/util/commonUtil.ts @@ -3,80 +3,18 @@ * SPDX-License-Identifier: Apache-2.0 */ -import * as vscode from 'vscode' -import * as semver from 'semver' import { distance } from 'fastest-levenshtein' import { getInlineSuggestEnabled } from '../../shared/utilities/editorUtilities' -import { - AWSTemplateCaseInsensitiveKeyWords, - AWSTemplateKeyWords, - JsonConfigFileNamingConvention, -} from '../models/constants' export function getLocalDatetime() { const timezone = Intl.DateTimeFormat().resolvedOptions().timeZone return new Date().toLocaleString([], { timeZone: timezone }) } -export function asyncCallWithTimeout(asyncPromise: Promise, message: string, timeLimit: number): Promise { - let timeoutHandle: NodeJS.Timeout - const timeoutPromise = new Promise((_resolve, reject) => { - timeoutHandle = setTimeout(() => reject(new Error(message)), timeLimit) - }) - return Promise.race([asyncPromise, timeoutPromise]).then((result) => { - clearTimeout(timeoutHandle) - return result as T - }) -} - export function isInlineCompletionEnabled() { return getInlineSuggestEnabled() } -// This is the VS Code version that started to have regressions in inline completion API -export function isVscHavingRegressionInlineCompletionApi() { - return semver.gte(vscode.version, '1.78.0') && getInlineSuggestEnabled() -} - -export function getFileExt(languageId: string) { - switch (languageId) { - case 'java': - return '.java' - case 'python': - return '.py' - default: - break - } - return undefined -} - -/** - * Returns the longest overlap between the Suffix of firstString and Prefix of second string - * getPrefixSuffixOverlap("adwg31", "31ggrs") = "31" - */ -export function getPrefixSuffixOverlap(firstString: string, secondString: string) { - let i = Math.min(firstString.length, secondString.length) - while (i > 0) { - if (secondString.slice(0, i) === firstString.slice(-i)) { - break - } - i-- - } - return secondString.slice(0, i) -} - -export function checkLeftContextKeywordsForJson(fileName: string, leftFileContent: string, language: string): boolean { - if ( - language === 'json' && - !AWSTemplateKeyWords.some((substring) => leftFileContent.includes(substring)) && - !AWSTemplateCaseInsensitiveKeyWords.some((substring) => leftFileContent.toLowerCase().includes(substring)) && - !JsonConfigFileNamingConvention.has(fileName.toLowerCase()) - ) { - return true - } - return false -} - // With edit distance, complicate usermodification can be considered as simple edit(add, delete, replace), // and thus the unmodified part of recommendation length can be deducted/approximated // ex. (modified > original): originalRecom: foo -> modifiedRecom: fobarbarbaro, distance = 9, delta = 12 - 9 = 3 diff --git a/packages/core/src/codewhisperer/util/editorContext.ts b/packages/core/src/codewhisperer/util/editorContext.ts deleted file mode 100644 index 95df5eb509a..00000000000 --- a/packages/core/src/codewhisperer/util/editorContext.ts +++ /dev/null @@ -1,425 +0,0 @@ -/*! - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -import * as vscode from 'vscode' -import * as codewhispererClient from '../client/codewhisperer' -import * as path from 'path' -import * as CodeWhispererConstants from '../models/constants' -import { getTabSizeSetting } from '../../shared/utilities/editorUtilities' -import { truncate } from '../../shared/utilities/textUtilities' -import { getLogger } from '../../shared/logger/logger' -import { runtimeLanguageContext } from './runtimeLanguageContext' -import { fetchSupplementalContext } from './supplementalContext/supplementalContextUtil' -import { editorStateMaxLength, supplementalContextTimeoutInMs } from '../models/constants' -import { getSelectedCustomization } from './customizationUtil' -import { selectFrom } from '../../shared/utilities/tsUtils' -import { checkLeftContextKeywordsForJson } from './commonUtil' -import { CodeWhispererSupplementalContext } from '../models/model' -import { getOptOutPreference } from '../../shared/telemetry/util' -import { indent } from '../../shared/utilities/textUtilities' -import { isInDirectory } from '../../shared/filesystemUtilities' -import { AuthUtil } from './authUtil' -import { predictionTracker } from '../nextEditPrediction/activation' - -let tabSize: number = getTabSizeSetting() - -function getEnclosingNotebook(editor: vscode.TextEditor): vscode.NotebookDocument | undefined { - // For notebook cells, find the existing notebook with a cell that matches the current editor. - return vscode.workspace.notebookDocuments.find( - (nb) => - nb.notebookType === 'jupyter-notebook' && nb.getCells().some((cell) => cell.document === editor.document) - ) -} - -export function getNotebookContext( - notebook: vscode.NotebookDocument, - editor: vscode.TextEditor, - languageName: string, - caretLeftFileContext: string, - caretRightFileContext: string -) { - // Expand the context for a cell inside of a noteboo with whatever text fits from the preceding and subsequent cells - const allCells = notebook.getCells() - const cellIndex = allCells.findIndex((cell) => cell.document === editor.document) - // Extract text from prior cells if there is enough room in left file context - if (caretLeftFileContext.length < CodeWhispererConstants.charactersLimit - 1) { - const leftCellsText = getNotebookCellsSliceContext( - allCells.slice(0, cellIndex), - CodeWhispererConstants.charactersLimit - (caretLeftFileContext.length + 1), - languageName, - true - ) - if (leftCellsText.length > 0) { - caretLeftFileContext = addNewlineIfMissing(leftCellsText) + caretLeftFileContext - } - } - // Extract text from subsequent cells if there is enough room in right file context - if (caretRightFileContext.length < CodeWhispererConstants.charactersLimit - 1) { - const rightCellsText = getNotebookCellsSliceContext( - allCells.slice(cellIndex + 1), - CodeWhispererConstants.charactersLimit - (caretRightFileContext.length + 1), - languageName, - false - ) - if (rightCellsText.length > 0) { - caretRightFileContext = addNewlineIfMissing(caretRightFileContext) + rightCellsText - } - } - return { caretLeftFileContext, caretRightFileContext } -} - -export function getNotebookCellContext(cell: vscode.NotebookCell, referenceLanguage?: string): string { - // Extract the text verbatim if the cell is code and the cell has the same language. - // Otherwise, add the correct comment string for the reference language - const cellText = cell.document.getText() - if ( - cell.kind === vscode.NotebookCellKind.Markup || - (runtimeLanguageContext.normalizeLanguage(cell.document.languageId) ?? cell.document.languageId) !== - referenceLanguage - ) { - const commentPrefix = runtimeLanguageContext.getSingleLineCommentPrefix(referenceLanguage) - if (commentPrefix === '') { - return cellText - } - return cell.document - .getText() - .split('\n') - .map((line) => `${commentPrefix}${line}`) - .join('\n') - } - return cellText -} - -export function getNotebookCellsSliceContext( - cells: vscode.NotebookCell[], - maxLength: number, - referenceLanguage: string, - fromStart: boolean -): string { - // Extract context from array of notebook cells that fits inside `maxLength` characters, - // from either the start or the end of the array. - let output: string[] = [] - if (!fromStart) { - cells = cells.reverse() - } - cells.some((cell) => { - const cellText = addNewlineIfMissing(getNotebookCellContext(cell, referenceLanguage)) - if (cellText.length > 0) { - if (cellText.length >= maxLength) { - if (fromStart) { - output.push(cellText.substring(0, maxLength)) - } else { - output.push(cellText.substring(cellText.length - maxLength)) - } - return true - } - output.push(cellText) - maxLength -= cellText.length - } - }) - if (!fromStart) { - output = output.reverse() - } - return output.join('') -} - -export function addNewlineIfMissing(text: string): string { - if (text.length > 0 && !text.endsWith('\n')) { - text += '\n' - } - return text -} - -export function extractContextForCodeWhisperer(editor: vscode.TextEditor): codewhispererClient.FileContext { - const document = editor.document - const curPos = editor.selection.active - const offset = document.offsetAt(curPos) - - let caretLeftFileContext = editor.document.getText( - new vscode.Range( - document.positionAt(offset - CodeWhispererConstants.charactersLimit), - document.positionAt(offset) - ) - ) - let caretRightFileContext = editor.document.getText( - new vscode.Range( - document.positionAt(offset), - document.positionAt(offset + CodeWhispererConstants.charactersLimit) - ) - ) - let languageName = 'plaintext' - if (!checkLeftContextKeywordsForJson(document.fileName, caretLeftFileContext, editor.document.languageId)) { - languageName = runtimeLanguageContext.resolveLang(editor.document) - } - if (editor.document.uri.scheme === 'vscode-notebook-cell') { - const notebook = getEnclosingNotebook(editor) - if (notebook) { - ;({ caretLeftFileContext, caretRightFileContext } = getNotebookContext( - notebook, - editor, - languageName, - caretLeftFileContext, - caretRightFileContext - )) - } - } - - return { - fileUri: editor.document.uri.toString().substring(0, CodeWhispererConstants.filenameCharsLimit), - filename: getFileRelativePath(editor), - programmingLanguage: { - languageName: languageName, - }, - leftFileContent: caretLeftFileContext, - rightFileContent: caretRightFileContext, - } as codewhispererClient.FileContext -} - -export function getFileName(editor: vscode.TextEditor): string { - const fileName = path.basename(editor.document.fileName) - return fileName.substring(0, CodeWhispererConstants.filenameCharsLimit) -} - -export function getFileRelativePath(editor: vscode.TextEditor): string { - const fileName = path.basename(editor.document.fileName) - let relativePath = '' - const workspaceFolder = vscode.workspace.getWorkspaceFolder(editor.document.uri) - if (!workspaceFolder) { - relativePath = fileName - } else { - const workspacePath = workspaceFolder.uri.fsPath - const filePath = editor.document.uri.fsPath - relativePath = path.relative(workspacePath, filePath) - } - // For notebook files, we want to use the programming language for each cell for the code suggestions, so change - // the filename sent in the request to reflect that language - if (relativePath.endsWith('.ipynb')) { - const fileExtension = runtimeLanguageContext.getLanguageExtensionForNotebook(editor.document.languageId) - if (fileExtension !== undefined) { - const filenameWithNewExtension = relativePath.substring(0, relativePath.length - 5) + fileExtension - return filenameWithNewExtension.substring(0, CodeWhispererConstants.filenameCharsLimit) - } - } - return relativePath.substring(0, CodeWhispererConstants.filenameCharsLimit) -} - -async function getWorkspaceId(editor: vscode.TextEditor): Promise { - try { - const workspaceIds: { workspaces: { workspaceRoot: string; workspaceId: string }[] } = - await vscode.commands.executeCommand('aws.amazonq.getWorkspaceId') - for (const item of workspaceIds.workspaces) { - const path = vscode.Uri.parse(item.workspaceRoot).fsPath - if (isInDirectory(path, editor.document.uri.fsPath)) { - return item.workspaceId - } - } - } catch (err) { - getLogger().warn(`No workspace id found ${err}`) - } - return undefined -} - -export async function buildListRecommendationRequest( - editor: vscode.TextEditor, - nextToken: string, - allowCodeWithReference: boolean -): Promise<{ - request: codewhispererClient.ListRecommendationsRequest - supplementalMetadata: CodeWhispererSupplementalContext | undefined -}> { - const fileContext = extractContextForCodeWhisperer(editor) - - const tokenSource = new vscode.CancellationTokenSource() - setTimeout(() => { - tokenSource.cancel() - }, supplementalContextTimeoutInMs) - - const supplementalContexts = await fetchSupplementalContext(editor, tokenSource.token) - - logSupplementalContext(supplementalContexts) - - // Get predictionSupplementalContext from PredictionTracker - let predictionSupplementalContext: codewhispererClient.SupplementalContext[] = [] - if (predictionTracker) { - predictionSupplementalContext = await predictionTracker.generatePredictionSupplementalContext() - } - - const selectedCustomization = getSelectedCustomization() - const completionSupplementalContext: codewhispererClient.SupplementalContext[] = supplementalContexts - ? supplementalContexts.supplementalContextItems.map((v) => { - return selectFrom(v, 'content', 'filePath') - }) - : [] - - const profile = AuthUtil.instance.regionProfileManager.activeRegionProfile - - const editorState = getEditorState(editor, fileContext) - - // Combine inline and prediction supplemental contexts - const finalSupplementalContext = completionSupplementalContext.concat(predictionSupplementalContext) - return { - request: { - fileContext: fileContext, - nextToken: nextToken, - referenceTrackerConfiguration: { - recommendationsWithReferences: allowCodeWithReference ? 'ALLOW' : 'BLOCK', - }, - supplementalContexts: finalSupplementalContext, - editorState: editorState, - maxResults: CodeWhispererConstants.maxRecommendations, - customizationArn: selectedCustomization.arn === '' ? undefined : selectedCustomization.arn, - optOutPreference: getOptOutPreference(), - workspaceId: await getWorkspaceId(editor), - profileArn: profile?.arn, - }, - supplementalMetadata: supplementalContexts, - } -} - -export async function buildGenerateRecommendationRequest(editor: vscode.TextEditor): Promise<{ - request: codewhispererClient.GenerateRecommendationsRequest - supplementalMetadata: CodeWhispererSupplementalContext | undefined -}> { - const fileContext = extractContextForCodeWhisperer(editor) - - const tokenSource = new vscode.CancellationTokenSource() - // the supplement context fetch mechanisms each has a timeout of supplementalContextTimeoutInMs - // adding 10 ms for overall timeout as buffer - setTimeout(() => { - tokenSource.cancel() - }, supplementalContextTimeoutInMs + 10) - const supplementalContexts = await fetchSupplementalContext(editor, tokenSource.token) - - logSupplementalContext(supplementalContexts) - - return { - request: { - fileContext: fileContext, - maxResults: CodeWhispererConstants.maxRecommendations, - supplementalContexts: supplementalContexts?.supplementalContextItems ?? [], - }, - supplementalMetadata: supplementalContexts, - } -} - -export function validateRequest( - req: codewhispererClient.ListRecommendationsRequest | codewhispererClient.GenerateRecommendationsRequest -): boolean { - const isLanguageNameValid = - req.fileContext.programmingLanguage.languageName !== undefined && - req.fileContext.programmingLanguage.languageName.length >= 1 && - req.fileContext.programmingLanguage.languageName.length <= 128 && - (runtimeLanguageContext.isLanguageSupported(req.fileContext.programmingLanguage.languageName) || - runtimeLanguageContext.isFileFormatSupported( - req.fileContext.filename.substring(req.fileContext.filename.lastIndexOf('.') + 1) - )) - const isFileNameValid = !(req.fileContext.filename === undefined || req.fileContext.filename.length < 1) - const isFileContextValid = !( - req.fileContext.leftFileContent.length > CodeWhispererConstants.charactersLimit || - req.fileContext.rightFileContent.length > CodeWhispererConstants.charactersLimit - ) - if (isFileNameValid && isLanguageNameValid && isFileContextValid) { - return true - } - return false -} - -export function updateTabSize(val: number): void { - tabSize = val -} - -export function getTabSize(): number { - return tabSize -} - -export function getEditorState(editor: vscode.TextEditor, fileContext: codewhispererClient.FileContext): any { - try { - const cursorPosition = editor.selection.active - const cursorOffset = editor.document.offsetAt(cursorPosition) - const documentText = editor.document.getText() - - // Truncate if document content is too large (defined in constants.ts) - let fileText = documentText - if (documentText.length > editorStateMaxLength) { - const halfLength = Math.floor(editorStateMaxLength / 2) - - // Use truncate function to get the text around the cursor position - const leftPart = truncate(documentText.substring(0, cursorOffset), -halfLength, '') - const rightPart = truncate(documentText.substring(cursorOffset), halfLength, '') - - fileText = leftPart + rightPart - } - - return { - document: { - programmingLanguage: { - languageName: fileContext.programmingLanguage.languageName, - }, - relativeFilePath: fileContext.filename, - text: fileText, - }, - cursorState: { - position: { - line: editor.selection.active.line, - character: editor.selection.active.character, - }, - }, - } - } catch (error) { - getLogger().error(`Error generating editor state: ${error}`) - return undefined - } -} - -export function getLeftContext(editor: vscode.TextEditor, line: number): string { - let lineText = '' - try { - if (editor && editor.document.lineAt(line)) { - lineText = editor.document.lineAt(line).text - if (lineText.length > CodeWhispererConstants.contextPreviewLen) { - lineText = - '...' + - lineText.substring( - lineText.length - CodeWhispererConstants.contextPreviewLen - 1, - lineText.length - 1 - ) - } - } - } catch (error) { - getLogger().error(`Error when getting left context ${error}`) - } - - return lineText -} - -function logSupplementalContext(supplementalContext: CodeWhispererSupplementalContext | undefined) { - if (!supplementalContext) { - return - } - - let logString = indent( - `CodeWhispererSupplementalContext: - isUtg: ${supplementalContext.isUtg}, - isProcessTimeout: ${supplementalContext.isProcessTimeout}, - contentsLength: ${supplementalContext.contentsLength}, - latency: ${supplementalContext.latency} - strategy: ${supplementalContext.strategy}`, - 4, - true - ).trimStart() - - for (const [index, context] of supplementalContext.supplementalContextItems.entries()) { - logString += indent(`\nChunk ${index}:\n`, 4, true) - logString += indent( - `Path: ${context.filePath} - Length: ${context.content.length} - Score: ${context.score}`, - 8, - true - ) - } - - getLogger().debug(logString) -} diff --git a/packages/core/src/codewhisperer/util/globalStateUtil.ts b/packages/core/src/codewhisperer/util/globalStateUtil.ts deleted file mode 100644 index 55376a83546..00000000000 --- a/packages/core/src/codewhisperer/util/globalStateUtil.ts +++ /dev/null @@ -1,23 +0,0 @@ -/*! - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -import { vsCodeState } from '../models/model' - -export function resetIntelliSenseState( - isManualTriggerEnabled: boolean, - isAutomatedTriggerEnabled: boolean, - hasResponse: boolean -) { - /** - * Skip when CodeWhisperer service is turned off - */ - if (!isManualTriggerEnabled && !isAutomatedTriggerEnabled) { - return - } - - if (vsCodeState.isIntelliSenseActive && hasResponse) { - vsCodeState.isIntelliSenseActive = false - } -} diff --git a/packages/core/src/codewhisperer/util/supplementalContext/codeParsingUtil.ts b/packages/core/src/codewhisperer/util/supplementalContext/codeParsingUtil.ts deleted file mode 100644 index c73a2eebaa4..00000000000 --- a/packages/core/src/codewhisperer/util/supplementalContext/codeParsingUtil.ts +++ /dev/null @@ -1,130 +0,0 @@ -/*! - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -import * as vscode from 'vscode' -import path = require('path') -import { normalize } from '../../../shared/utilities/pathUtils' - -// TODO: functionExtractionPattern, classExtractionPattern, imposrtStatementRegex are not scalable and we will deprecate and remove the usage in the near future -export interface utgLanguageConfig { - extension: string - testFilenamePattern: RegExp[] - functionExtractionPattern?: RegExp - classExtractionPattern?: RegExp - importStatementRegExp?: RegExp -} - -export const utgLanguageConfigs: Record = { - // Java regexes are not working efficiently for class or function extraction - java: { - extension: '.java', - testFilenamePattern: [/^(.+)Test(\.java)$/, /(.+)Tests(\.java)$/, /Test(.+)(\.java)$/], - functionExtractionPattern: - /(?:(?:public|private|protected)\s+)(?:static\s+)?(?:[\w<>]+\s+)?(\w+)\s*\([^)]*\)\s*(?:(?:throws\s+\w+)?\s*)[{;]/gm, // TODO: Doesn't work for generice T functions. - classExtractionPattern: /(?<=^|\n)\s*public\s+class\s+(\w+)/gm, // TODO: Verify these. - importStatementRegExp: /import .*\.([a-zA-Z0-9]+);/, - }, - python: { - extension: '.py', - testFilenamePattern: [/^test_(.+)(\.py)$/, /^(.+)_test(\.py)$/], - functionExtractionPattern: /def\s+([a-zA-Z_][a-zA-Z0-9_]*)\s*\(/g, // Worked fine - classExtractionPattern: /^class\s+(\w+)\s*:/gm, - importStatementRegExp: /from (.*) import.*/, - }, - typescript: { - extension: '.ts', - testFilenamePattern: [/^(.+)\.test(\.ts|\.tsx)$/, /^(.+)\.spec(\.ts|\.tsx)$/], - }, - javascript: { - extension: '.js', - testFilenamePattern: [/^(.+)\.test(\.js|\.jsx)$/, /^(.+)\.spec(\.js|\.jsx)$/], - }, - typescriptreact: { - extension: '.tsx', - testFilenamePattern: [/^(.+)\.test(\.ts|\.tsx)$/, /^(.+)\.spec(\.ts|\.tsx)$/], - }, - javascriptreact: { - extension: '.jsx', - testFilenamePattern: [/^(.+)\.test(\.js|\.jsx)$/, /^(.+)\.spec(\.js|\.jsx)$/], - }, -} - -export function extractFunctions(fileContent: string, regex?: RegExp) { - if (!regex) { - return [] - } - const functionNames: string[] = [] - let match: RegExpExecArray | null - - while ((match = regex.exec(fileContent)) !== null) { - functionNames.push(match[1]) - } - return functionNames -} - -export function extractClasses(fileContent: string, regex?: RegExp) { - if (!regex) { - return [] - } - const classNames: string[] = [] - let match: RegExpExecArray | null - - while ((match = regex.exec(fileContent)) !== null) { - classNames.push(match[1]) - } - return classNames -} - -export function countSubstringMatches(arr1: string[], arr2: string[]): number { - let count = 0 - for (const str1 of arr1) { - for (const str2 of arr2) { - if (str2.toLowerCase().includes(str1.toLowerCase())) { - count++ - } - } - } - return count -} - -export async function isTestFile( - filePath: string, - languageConfig: { - languageId: vscode.TextDocument['languageId'] - fileContent?: string - } -): Promise { - const normalizedFilePath = normalize(filePath) - const pathContainsTest = - normalizedFilePath.includes('tests/') || - normalizedFilePath.includes('test/') || - normalizedFilePath.includes('tst/') - const fileNameMatchTestPatterns = isTestFileByName(normalizedFilePath, languageConfig.languageId) - - if (pathContainsTest || fileNameMatchTestPatterns) { - return true - } - - return false -} - -function isTestFileByName(filePath: string, language: vscode.TextDocument['languageId']): boolean { - const languageConfig = utgLanguageConfigs[language] - if (!languageConfig) { - // We have enabled the support only for python and Java for this check - // as we depend on Regex for this validation. - return false - } - const testFilenamePattern = languageConfig.testFilenamePattern - - const filename = path.basename(filePath) - for (const pattern of testFilenamePattern) { - if (pattern.test(filename)) { - return true - } - } - - return false -} diff --git a/packages/core/src/codewhisperer/util/supplementalContext/crossFileContextUtil.ts b/packages/core/src/codewhisperer/util/supplementalContext/crossFileContextUtil.ts deleted file mode 100644 index db1d7f312b2..00000000000 --- a/packages/core/src/codewhisperer/util/supplementalContext/crossFileContextUtil.ts +++ /dev/null @@ -1,395 +0,0 @@ -/*! - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -import * as vscode from 'vscode' -import path = require('path') -import { BM25Document, BM25Okapi } from './rankBm25' -import { - crossFileContextConfig, - supplementalContextTimeoutInMs, - supplementalContextMaxTotalLength, -} from '../../models/constants' -import { isTestFile } from './codeParsingUtil' -import { getFileDistance } from '../../../shared/filesystemUtilities' -import { getOpenFilesInWindow } from '../../../shared/utilities/editorUtilities' -import { getLogger } from '../../../shared/logger/logger' -import { - CodeWhispererSupplementalContext, - CodeWhispererSupplementalContextItem, - SupplementalContextStrategy, -} from '../../models/model' -import { LspController } from '../../../amazonq/lsp/lspController' -import { waitUntil } from '../../../shared/utilities/timeoutUtils' -import { FeatureConfigProvider } from '../../../shared/featureConfig' -import fs from '../../../shared/fs/fs' - -type CrossFileSupportedLanguage = - | 'java' - | 'python' - | 'javascript' - | 'typescript' - | 'javascriptreact' - | 'typescriptreact' - -// TODO: ugly, can we make it prettier? like we have to manually type 'java', 'javascriptreact' which is error prone -// TODO: Move to another config file or constants file -// Supported language to its corresponding file ext -const supportedLanguageToDialects: Readonly>> = { - java: new Set(['.java']), - python: new Set(['.py']), - javascript: new Set(['.js', '.jsx']), - javascriptreact: new Set(['.js', '.jsx']), - typescript: new Set(['.ts', '.tsx']), - typescriptreact: new Set(['.ts', '.tsx']), -} - -function isCrossFileSupported(languageId: string): languageId is CrossFileSupportedLanguage { - return Object.keys(supportedLanguageToDialects).includes(languageId) -} - -interface Chunk { - fileName: string - content: string - nextContent: string - score?: number -} - -/** - * `none`: supplementalContext is not supported - * `opentabs`: opentabs_BM25 - * `codemap`: repomap + opentabs BM25 - * `bm25`: global_BM25 - * `default`: repomap + global_BM25 - */ -type SupplementalContextConfig = 'none' | 'opentabs' | 'codemap' | 'bm25' | 'default' - -export async function fetchSupplementalContextForSrc( - editor: vscode.TextEditor, - cancellationToken: vscode.CancellationToken -): Promise | undefined> { - const supplementalContextConfig = getSupplementalContextConfig(editor.document.languageId) - - // not supported case - if (supplementalContextConfig === 'none') { - return undefined - } - - // fallback to opentabs if projectContext timeout - const opentabsContextPromise = waitUntil( - async function () { - return await fetchOpentabsContext(editor, cancellationToken) - }, - { timeout: supplementalContextTimeoutInMs, interval: 5, truthy: false } - ) - - // opentabs context will use bm25 and users' open tabs to fetch supplemental context - if (supplementalContextConfig === 'opentabs') { - const supContext = (await opentabsContextPromise) ?? [] - return { - supplementalContextItems: supContext, - strategy: supContext.length === 0 ? 'empty' : 'opentabs', - } - } - - // codemap will use opentabs context plus repomap if it's present - if (supplementalContextConfig === 'codemap') { - let strategy: SupplementalContextStrategy = 'empty' - let hasCodemap: boolean = false - let hasOpentabs: boolean = false - const opentabsContextAndCodemap = await waitUntil( - async function () { - const result: CodeWhispererSupplementalContextItem[] = [] - const opentabsContext = await fetchOpentabsContext(editor, cancellationToken) - const codemap = await fetchProjectContext(editor, 'codemap') - - function addToResult(items: CodeWhispererSupplementalContextItem[]) { - for (const item of items) { - const curLen = result.reduce((acc, i) => acc + i.content.length, 0) - if (curLen + item.content.length < supplementalContextMaxTotalLength) { - result.push(item) - } - } - } - - if (codemap && codemap.length > 0) { - addToResult(codemap) - hasCodemap = true - } - - if (opentabsContext && opentabsContext.length > 0) { - addToResult(opentabsContext) - hasOpentabs = true - } - - return result - }, - { timeout: supplementalContextTimeoutInMs, interval: 5, truthy: false } - ) - - if (hasCodemap) { - strategy = 'codemap' - } else if (hasOpentabs) { - strategy = 'opentabs' - } else { - strategy = 'empty' - } - - return { - supplementalContextItems: opentabsContextAndCodemap ?? [], - strategy: strategy, - } - } - - // global bm25 without repomap - if (supplementalContextConfig === 'bm25') { - const projectBM25Promise = waitUntil( - async function () { - return await fetchProjectContext(editor, 'bm25') - }, - { timeout: supplementalContextTimeoutInMs, interval: 5, truthy: false } - ) - - const [projectContext, opentabsContext] = await Promise.all([projectBM25Promise, opentabsContextPromise]) - if (projectContext && projectContext.length > 0) { - return { - supplementalContextItems: projectContext, - strategy: 'bm25', - } - } - - const supContext = opentabsContext ?? [] - return { - supplementalContextItems: supContext, - strategy: supContext.length === 0 ? 'empty' : 'opentabs', - } - } - - // global bm25 with repomap - const projectContextAndCodemapPromise = waitUntil( - async function () { - return await fetchProjectContext(editor, 'default') - }, - { timeout: supplementalContextTimeoutInMs, interval: 5, truthy: false } - ) - - const [projectContext, opentabsContext] = await Promise.all([ - projectContextAndCodemapPromise, - opentabsContextPromise, - ]) - if (projectContext && projectContext.length > 0) { - return { - supplementalContextItems: projectContext, - strategy: 'default', - } - } - - return { - supplementalContextItems: opentabsContext ?? [], - strategy: 'opentabs', - } -} - -export async function fetchProjectContext( - editor: vscode.TextEditor, - target: 'default' | 'codemap' | 'bm25' -): Promise { - const inputChunkContent = getInputChunk(editor) - - const inlineProjectContext: { content: string; score: number; filePath: string }[] = - await LspController.instance.queryInlineProjectContext( - inputChunkContent.content, - editor.document.uri.fsPath, - target - ) - - return inlineProjectContext -} - -export async function fetchOpentabsContext( - editor: vscode.TextEditor, - cancellationToken: vscode.CancellationToken -): Promise { - const codeChunksCalculated = crossFileContextConfig.numberOfChunkToFetch - - // Step 1: Get relevant cross files to refer - const relevantCrossFilePaths = await getCrossFileCandidates(editor) - - // Step 2: Split files to chunks with upper bound on chunkCount - // We restrict the total number of chunks to improve on latency. - // Chunk linking is required as we want to pass the next chunk value for matched chunk. - let chunkList: Chunk[] = [] - for (const relevantFile of relevantCrossFilePaths) { - const chunks: Chunk[] = await splitFileToChunks(relevantFile, crossFileContextConfig.numberOfLinesEachChunk) - const linkedChunks = linkChunks(chunks) - chunkList.push(...linkedChunks) - if (chunkList.length >= codeChunksCalculated) { - break - } - } - - // it's required since chunkList.push(...) is likely giving us a list of size > 60 - chunkList = chunkList.slice(0, codeChunksCalculated) - - // Step 3: Generate Input chunk (10 lines left of cursor position) - // and Find Best K chunks w.r.t input chunk using BM25 - const inputChunk: Chunk = getInputChunk(editor) - const bestChunks: Chunk[] = findBestKChunkMatches(inputChunk, chunkList, crossFileContextConfig.topK) - - // Step 4: Transform best chunks to supplemental contexts - const supplementalContexts: CodeWhispererSupplementalContextItem[] = [] - let totalLength = 0 - for (const chunk of bestChunks) { - totalLength += chunk.nextContent.length - - if (totalLength > crossFileContextConfig.maximumTotalLength) { - break - } - - supplementalContexts.push({ - filePath: chunk.fileName, - content: chunk.nextContent, - score: chunk.score, - }) - } - - // DO NOT send code chunk with empty content - getLogger().debug(`CodeWhisperer finished fetching crossfile context out of ${relevantCrossFilePaths.length} files`) - return supplementalContexts -} - -function findBestKChunkMatches(chunkInput: Chunk, chunkReferences: Chunk[], k: number): Chunk[] { - const chunkContentList = chunkReferences.map((chunk) => chunk.content) - - // performBM25Scoring returns the output in a sorted order (descending of scores) - const top3: BM25Document[] = new BM25Okapi(chunkContentList).topN(chunkInput.content, crossFileContextConfig.topK) - - return top3.map((doc) => { - // reference to the original metadata since BM25.top3 will sort the result - const chunkIndex = doc.index - const chunkReference = chunkReferences[chunkIndex] - return { - content: chunkReference.content, - fileName: chunkReference.fileName, - nextContent: chunkReference.nextContent, - score: doc.score, - } - }) -} - -/* This extract 10 lines to the left of the cursor from trigger file. - * This will be the inputquery to bm25 matching against list of cross-file chunks - */ -function getInputChunk(editor: vscode.TextEditor) { - const chunkSize = crossFileContextConfig.numberOfLinesEachChunk - const cursorPosition = editor.selection.active - const startLine = Math.max(cursorPosition.line - chunkSize, 0) - const endLine = Math.max(cursorPosition.line - 1, 0) - const inputChunkContent = editor.document.getText( - new vscode.Range(startLine, 0, endLine, editor.document.lineAt(endLine).text.length) - ) - const inputChunk: Chunk = { fileName: editor.document.fileName, content: inputChunkContent, nextContent: '' } - return inputChunk -} - -/** - * Util to decide if we need to fetch crossfile context since CodeWhisperer CrossFile Context feature is gated by userGroup and language level - * @param languageId: VSCode language Identifier - * @returns specifically returning undefined if the langueage is not supported, - * otherwise true/false depending on if the language is fully supported or not belonging to the user group - */ -function getSupplementalContextConfig(languageId: vscode.TextDocument['languageId']): SupplementalContextConfig { - if (!isCrossFileSupported(languageId)) { - return 'none' - } - - const group = FeatureConfigProvider.instance.getProjectContextGroup() - switch (group) { - default: - return 'codemap' - } -} - -/** - * This linking is required from science experimentations to pass the next contnet chunk - * when a given chunk context passes the match in BM25. - * Special handling is needed for last(its next points to its own) and first chunk - */ -export function linkChunks(chunks: Chunk[]) { - const updatedChunks: Chunk[] = [] - - // This additional chunk is needed to create a next pointer to chunk 0. - const firstChunk = chunks[0] - const firstChunkSubContent = firstChunk.content.split('\n').slice(0, 3).join('\n').trimEnd() - const newFirstChunk = { - fileName: firstChunk.fileName, - content: firstChunkSubContent, - nextContent: firstChunk.content, - } - updatedChunks.push(newFirstChunk) - - const n = chunks.length - for (let i = 0; i < n; i++) { - const chunk = chunks[i] - const nextChunk = i < n - 1 ? chunks[i + 1] : chunk - - chunk.nextContent = nextChunk.content - updatedChunks.push(chunk) - } - - return updatedChunks -} - -export async function splitFileToChunks(filePath: string, chunkSize: number): Promise { - const chunks: Chunk[] = [] - - const fileContent = (await fs.readFileText(filePath)).trimEnd() - const lines = fileContent.split('\n') - - for (let i = 0; i < lines.length; i += chunkSize) { - const chunkContent = lines.slice(i, Math.min(i + chunkSize, lines.length)).join('\n') - const chunk = { fileName: filePath, content: chunkContent.trimEnd(), nextContent: '' } - chunks.push(chunk) - } - return chunks -} - -/** - * This function will return relevant cross files sorted by file distance for the given editor file - * by referencing open files, imported files and same package files. - */ -export async function getCrossFileCandidates(editor: vscode.TextEditor): Promise { - const targetFile = editor.document.uri.fsPath - const language = editor.document.languageId as CrossFileSupportedLanguage - const dialects = supportedLanguageToDialects[language] - - /** - * Consider a file which - * 1. is different from the target - * 2. has the same file extension or it's one of the dialect of target file (e.g .js vs. .jsx) - * 3. is not a test file - */ - const unsortedCandidates = await getOpenFilesInWindow(async (candidateFile) => { - return ( - targetFile !== candidateFile && - (path.extname(targetFile) === path.extname(candidateFile) || - (dialects && dialects.has(path.extname(candidateFile)))) && - !(await isTestFile(candidateFile, { languageId: language })) - ) - }) - - return unsortedCandidates - .map((candidate) => { - return { - file: candidate, - fileDistance: getFileDistance(targetFile, candidate), - } - }) - .sort((file1, file2) => { - return file1.fileDistance - file2.fileDistance - }) - .map((fileToDistance) => { - return fileToDistance.file - }) -} diff --git a/packages/core/src/codewhisperer/util/supplementalContext/rankBm25.ts b/packages/core/src/codewhisperer/util/supplementalContext/rankBm25.ts deleted file mode 100644 index a2c77e0b10f..00000000000 --- a/packages/core/src/codewhisperer/util/supplementalContext/rankBm25.ts +++ /dev/null @@ -1,137 +0,0 @@ -/*! - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -// Implementation inspired by https://github.com/dorianbrown/rank_bm25/blob/990470ebbe6b28c18216fd1a8b18fe7446237dd6/rank_bm25.py#L52 - -export interface BM25Document { - content: string - /** The score that the document receives. */ - score: number - - index: number -} - -export abstract class BM25 { - protected readonly corpusSize: number - protected readonly avgdl: number - protected readonly idf: Map = new Map() - protected readonly docLen: number[] = [] - protected readonly docFreqs: Map[] = [] - protected readonly nd: Map = new Map() - - constructor( - protected readonly corpus: string[], - protected readonly tokenizer: (str: string) => string[] = defaultTokenizer, - protected readonly k1: number, - protected readonly b: number, - protected readonly epsilon: number - ) { - this.corpusSize = corpus.length - - let numDoc = 0 - for (const document of corpus.map((document) => { - return tokenizer(document) - })) { - this.docLen.push(document.length) - numDoc += document.length - - const frequencies = new Map() - for (const word of document) { - frequencies.set(word, (frequencies.get(word) || 0) + 1) - } - this.docFreqs.push(frequencies) - - for (const [word, _] of frequencies.entries()) { - this.nd.set(word, (this.nd.get(word) || 0) + 1) - } - } - - this.avgdl = numDoc / this.corpusSize - - this.calIdf(this.nd) - } - - abstract calIdf(nd: Map): void - - abstract score(query: string): BM25Document[] - - topN(query: string, n: number): BM25Document[] { - const notSorted = this.score(query) - const sorted = notSorted.sort((a, b) => b.score - a.score) - return sorted.slice(0, Math.min(n, sorted.length)) - } -} - -export class BM25Okapi extends BM25 { - constructor(corpus: string[], tokenizer: (str: string) => string[] = defaultTokenizer) { - super(corpus, tokenizer, 1.5, 0.75, 0.25) - } - - calIdf(nd: Map): void { - let idfSum = 0 - - const negativeIdfs: string[] = [] - for (const [word, freq] of nd) { - const idf = Math.log(this.corpusSize - freq + 0.5) - Math.log(freq + 0.5) - this.idf.set(word, idf) - idfSum += idf - - if (idf < 0) { - negativeIdfs.push(word) - } - } - - const averageIdf = idfSum / this.idf.size - const eps = this.epsilon * averageIdf - for (const word of negativeIdfs) { - this.idf.set(word, eps) - } - } - - score(query: string): BM25Document[] { - const queryWords = defaultTokenizer(query) - return this.docFreqs.map((docFreq, index) => { - let score = 0 - for (const [_, queryWord] of queryWords.entries()) { - const queryWordFreqForDocument = docFreq.get(queryWord) || 0 - const numerator = (this.idf.get(queryWord) || 0.0) * queryWordFreqForDocument * (this.k1 + 1) - const denominator = - queryWordFreqForDocument + this.k1 * (1 - this.b + (this.b * this.docLen[index]) / this.avgdl) - - score += numerator / denominator - } - - return { - content: this.corpus[index], - score: score, - index: index, - } - }) - } -} - -// TODO: This is a very simple tokenizer, we want to replace this by more sophisticated one. -function defaultTokenizer(content: string): string[] { - const regex = /\w+/g - const words = content.split(' ') - const result = [] - for (const word of words) { - const wordList = findAll(word, regex) - result.push(...wordList) - } - - return result -} - -function findAll(str: string, re: RegExp): string[] { - let match: RegExpExecArray | null - const matches: string[] = [] - - while ((match = re.exec(str)) !== null) { - matches.push(match[0]) - } - - return matches -} diff --git a/packages/core/src/codewhisperer/util/supplementalContext/supplementalContextUtil.ts b/packages/core/src/codewhisperer/util/supplementalContext/supplementalContextUtil.ts deleted file mode 100644 index bd214ace44e..00000000000 --- a/packages/core/src/codewhisperer/util/supplementalContext/supplementalContextUtil.ts +++ /dev/null @@ -1,137 +0,0 @@ -/*! - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -import { fetchSupplementalContextForTest } from './utgUtils' -import { fetchSupplementalContextForSrc } from './crossFileContextUtil' -import { isTestFile } from './codeParsingUtil' -import * as vscode from 'vscode' -import { CancellationError } from '../../../shared/utilities/timeoutUtils' -import { ToolkitError } from '../../../shared/errors' -import { getLogger } from '../../../shared/logger/logger' -import { CodeWhispererSupplementalContext } from '../../models/model' -import * as os from 'os' -import { crossFileContextConfig } from '../../models/constants' - -export async function fetchSupplementalContext( - editor: vscode.TextEditor, - cancellationToken: vscode.CancellationToken -): Promise { - const timesBeforeFetching = performance.now() - - const isUtg = await isTestFile(editor.document.uri.fsPath, { - languageId: editor.document.languageId, - fileContent: editor.document.getText(), - }) - - let supplementalContextPromise: Promise< - Pick | undefined - > - - if (isUtg) { - supplementalContextPromise = fetchSupplementalContextForTest(editor, cancellationToken) - } else { - supplementalContextPromise = fetchSupplementalContextForSrc(editor, cancellationToken) - } - - return supplementalContextPromise - .then((value) => { - if (value) { - const resBeforeTruncation = { - isUtg: isUtg, - isProcessTimeout: false, - supplementalContextItems: value.supplementalContextItems.filter( - (item) => item.content.trim().length !== 0 - ), - contentsLength: value.supplementalContextItems.reduce((acc, curr) => acc + curr.content.length, 0), - latency: performance.now() - timesBeforeFetching, - strategy: value.strategy, - } - - return truncateSuppelementalContext(resBeforeTruncation) - } else { - return undefined - } - }) - .catch((err) => { - if (err instanceof ToolkitError && err.cause instanceof CancellationError) { - return { - isUtg: isUtg, - isProcessTimeout: true, - supplementalContextItems: [], - contentsLength: 0, - latency: performance.now() - timesBeforeFetching, - strategy: 'empty', - } - } else { - getLogger().error( - `Fail to fetch supplemental context for target file ${editor.document.fileName}: ${err}` - ) - return undefined - } - }) -} - -/** - * Requirement - * - Maximum 5 supplemental context. - * - Each chunk can't exceed 10240 characters - * - Sum of all chunks can't exceed 20480 characters - */ -export function truncateSuppelementalContext( - context: CodeWhispererSupplementalContext -): CodeWhispererSupplementalContext { - let c = context.supplementalContextItems.map((item) => { - if (item.content.length > crossFileContextConfig.maxLengthEachChunk) { - return { - ...item, - content: truncateLineByLine(item.content, crossFileContextConfig.maxLengthEachChunk), - } - } else { - return item - } - }) - - if (c.length > crossFileContextConfig.maxContextCount) { - c = c.slice(0, crossFileContextConfig.maxContextCount) - } - - let curTotalLength = c.reduce((acc, cur) => { - return acc + cur.content.length - }, 0) - while (curTotalLength >= 20480 && c.length - 1 >= 0) { - const last = c[c.length - 1] - c = c.slice(0, -1) - curTotalLength -= last.content.length - } - - return { - ...context, - supplementalContextItems: c, - contentsLength: curTotalLength, - } -} - -export function truncateLineByLine(input: string, l: number): string { - const maxLength = l > 0 ? l : -1 * l - if (input.length === 0) { - return '' - } - - const shouldAddNewLineBack = input.endsWith(os.EOL) - let lines = input.trim().split(os.EOL) - let curLen = input.length - while (curLen > maxLength && lines.length - 1 >= 0) { - const last = lines[lines.length - 1] - lines = lines.slice(0, -1) - curLen -= last.length + 1 - } - - const r = lines.join(os.EOL) - if (shouldAddNewLineBack) { - return r + os.EOL - } else { - return r - } -} diff --git a/packages/core/src/codewhisperer/util/supplementalContext/utgUtils.ts b/packages/core/src/codewhisperer/util/supplementalContext/utgUtils.ts deleted file mode 100644 index 0d33969773e..00000000000 --- a/packages/core/src/codewhisperer/util/supplementalContext/utgUtils.ts +++ /dev/null @@ -1,229 +0,0 @@ -/*! - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -import * as path from 'path' -import { fs } from '../../../shared/fs/fs' -import * as vscode from 'vscode' -import { - countSubstringMatches, - extractClasses, - extractFunctions, - isTestFile, - utgLanguageConfig, - utgLanguageConfigs, -} from './codeParsingUtil' -import { ToolkitError } from '../../../shared/errors' -import { supplemetalContextFetchingTimeoutMsg } from '../../models/constants' -import { CancellationError } from '../../../shared/utilities/timeoutUtils' -import { utgConfig } from '../../models/constants' -import { getOpenFilesInWindow } from '../../../shared/utilities/editorUtilities' -import { getLogger } from '../../../shared/logger/logger' -import { CodeWhispererSupplementalContext, CodeWhispererSupplementalContextItem, UtgStrategy } from '../../models/model' - -const utgSupportedLanguages: vscode.TextDocument['languageId'][] = ['java', 'python'] - -type UtgSupportedLanguage = (typeof utgSupportedLanguages)[number] - -function isUtgSupportedLanguage(languageId: vscode.TextDocument['languageId']): languageId is UtgSupportedLanguage { - return utgSupportedLanguages.includes(languageId) -} - -export function shouldFetchUtgContext(languageId: vscode.TextDocument['languageId']): boolean | undefined { - if (!isUtgSupportedLanguage(languageId)) { - return undefined - } - - return languageId === 'java' -} - -/** - * This function attempts to find a focal file for the given trigger file. - * Attempt 1: If naming patterns followed correctly, source file can be found by name referencing. - * Attempt 2: Compare the function and class names of trigger file and all other open files in editor - * to find the closest match. - * Once found the focal file, we split it into multiple pieces as supplementalContext. - * @param editor - * @returns - */ -export async function fetchSupplementalContextForTest( - editor: vscode.TextEditor, - cancellationToken: vscode.CancellationToken -): Promise | undefined> { - const shouldProceed = shouldFetchUtgContext(editor.document.languageId) - - if (!shouldProceed) { - return shouldProceed === undefined ? undefined : { supplementalContextItems: [], strategy: 'empty' } - } - - const languageConfig = utgLanguageConfigs[editor.document.languageId] - - // TODO (Metrics): 1. Total number of calls to fetchSupplementalContextForTest - throwIfCancelled(cancellationToken) - - let crossSourceFile = await findSourceFileByName(editor, languageConfig, cancellationToken) - if (crossSourceFile) { - // TODO (Metrics): 2. Success count for fetchSourceFileByName (find source file by name) - getLogger().debug(`CodeWhisperer finished fetching utg context by file name`) - return { - supplementalContextItems: await generateSupplementalContextFromFocalFile( - crossSourceFile, - 'byName', - cancellationToken - ), - strategy: 'byName', - } - } - throwIfCancelled(cancellationToken) - - crossSourceFile = await findSourceFileByContent(editor, languageConfig, cancellationToken) - if (crossSourceFile) { - // TODO (Metrics): 3. Success count for fetchSourceFileByContent (find source file by content) - getLogger().debug(`CodeWhisperer finished fetching utg context by file content`) - return { - supplementalContextItems: await generateSupplementalContextFromFocalFile( - crossSourceFile, - 'byContent', - cancellationToken - ), - strategy: 'byContent', - } - } - - // TODO (Metrics): 4. Failure count - when unable to find focal file (supplemental context empty) - getLogger().debug(`CodeWhisperer failed to fetch utg context`) - return { - supplementalContextItems: [], - strategy: 'empty', - } -} - -async function generateSupplementalContextFromFocalFile( - filePath: string, - strategy: UtgStrategy, - cancellationToken: vscode.CancellationToken -): Promise { - const fileContent = await fs.readFileText(vscode.Uri.parse(filePath!).fsPath) - - // DO NOT send code chunk with empty content - if (fileContent.trim().length === 0) { - return [] - } - - return [ - { - filePath: filePath, - content: 'UTG\n' + fileContent.slice(0, Math.min(fileContent.length, utgConfig.maxSegmentSize)), - }, - ] -} - -async function findSourceFileByContent( - editor: vscode.TextEditor, - languageConfig: utgLanguageConfig, - cancellationToken: vscode.CancellationToken -): Promise { - const testFileContent = await fs.readFileText(editor.document.fileName) - const testElementList = extractFunctions(testFileContent, languageConfig.functionExtractionPattern) - - throwIfCancelled(cancellationToken) - - testElementList.push(...extractClasses(testFileContent, languageConfig.classExtractionPattern)) - - throwIfCancelled(cancellationToken) - - let sourceFilePath: string | undefined = undefined - let maxMatchCount = 0 - - if (testElementList.length === 0) { - // TODO: Add metrics here, as unable to parse test file using Regex. - return sourceFilePath - } - - const relevantFilePaths = await getRelevantUtgFiles(editor) - - throwIfCancelled(cancellationToken) - - // TODO (Metrics):Add metrics for relevantFilePaths length - for (const filePath of relevantFilePaths) { - throwIfCancelled(cancellationToken) - - const fileContent = await fs.readFileText(filePath) - const elementList = extractFunctions(fileContent, languageConfig.functionExtractionPattern) - elementList.push(...extractClasses(fileContent, languageConfig.classExtractionPattern)) - const matchCount = countSubstringMatches(elementList, testElementList) - if (matchCount > maxMatchCount) { - maxMatchCount = matchCount - sourceFilePath = filePath - } - } - return sourceFilePath -} - -async function getRelevantUtgFiles(editor: vscode.TextEditor): Promise { - const targetFile = editor.document.uri.fsPath - const language = editor.document.languageId - - return await getOpenFilesInWindow(async (candidateFile) => { - return ( - targetFile !== candidateFile && - path.extname(targetFile) === path.extname(candidateFile) && - !(await isTestFile(candidateFile, { languageId: language })) - ) - }) -} - -export function guessSrcFileName( - testFileName: string, - languageId: vscode.TextDocument['languageId'] -): string | undefined { - const languageConfig = utgLanguageConfigs[languageId] - if (!languageConfig) { - return undefined - } - - for (const pattern of languageConfig.testFilenamePattern) { - try { - const match = testFileName.match(pattern) - if (match) { - return match[1] + match[2] - } - } catch (err) { - if (err instanceof Error) { - getLogger().error( - `codewhisperer: error while guessing source file name from file ${testFileName} and pattern ${pattern}: ${err.message}` - ) - } - } - } - - return undefined -} - -async function findSourceFileByName( - editor: vscode.TextEditor, - languageConfig: utgLanguageConfig, - cancellationToken: vscode.CancellationToken -): Promise { - const testFileName = path.basename(editor.document.fileName) - const assumedSrcFileName = guessSrcFileName(testFileName, editor.document.languageId) - if (!assumedSrcFileName) { - return undefined - } - - const sourceFiles = await vscode.workspace.findFiles(`**/${assumedSrcFileName}`) - - throwIfCancelled(cancellationToken) - - if (sourceFiles.length > 0) { - return sourceFiles[0].toString() - } - return undefined -} - -function throwIfCancelled(token: vscode.CancellationToken): void | never { - if (token.isCancellationRequested) { - throw new ToolkitError(supplemetalContextFetchingTimeoutMsg, { cause: new CancellationError('timeout') }) - } -} diff --git a/packages/core/src/codewhisperer/views/activeStateController.ts b/packages/core/src/codewhisperer/views/activeStateController.ts index b3c991a9d38..614003d02ff 100644 --- a/packages/core/src/codewhisperer/views/activeStateController.ts +++ b/packages/core/src/codewhisperer/views/activeStateController.ts @@ -6,13 +6,9 @@ import * as vscode from 'vscode' import { LineSelection, LinesChangeEvent } from '../tracker/lineTracker' import { isTextEditor } from '../../shared/utilities/editorUtilities' -import { RecommendationService, SuggestionActionEvent } from '../service/recommendationService' import { subscribeOnce } from '../../shared/utilities/vsCodeUtils' import { Container } from '../service/serviceContainer' -import { RecommendationHandler } from '../service/recommendationHandler' import { cancellableDebounce } from '../../shared/utilities/functionUtils' -import { telemetry } from '../../shared/telemetry/telemetry' -import { TelemetryHelper } from '../util/telemetryHelper' export class ActiveStateController implements vscode.Disposable { private readonly _disposable: vscode.Disposable @@ -34,14 +30,6 @@ export class ActiveStateController implements vscode.Disposable { constructor(private readonly container: Container) { this._disposable = vscode.Disposable.from( - RecommendationService.instance.suggestionActionEvent(async (e) => { - await telemetry.withTraceId(async () => { - await this.onSuggestionActionEvent(e) - }, TelemetryHelper.instance.traceId) - }), - RecommendationHandler.instance.onDidReceiveRecommendation(async (_) => { - await this.onDidReceiveRecommendation() - }), this.container.lineTracker.onDidChangeActiveLines(async (e) => { await this.onActiveLinesChanged(e) }), @@ -70,33 +58,6 @@ export class ActiveStateController implements vscode.Disposable { await this._refresh(vscode.window.activeTextEditor) } - private async onSuggestionActionEvent(e: SuggestionActionEvent) { - if (!this._isReady) { - return - } - - this.clear(e.editor) // do we need this? - if (e.triggerType === 'OnDemand' && e.isRunning) { - // if user triggers on demand, immediately update the UI and cancel the previous debounced update if there is one - this.refreshDebounced.cancel() - await this._refresh(this._editor) - } else { - await this.refreshDebounced.promise(e.editor) - } - } - - private async onDidReceiveRecommendation() { - if (!this._isReady) { - return - } - - if (this._editor && this._editor === vscode.window.activeTextEditor) { - // receives recommendation, immediately update the UI and cacnel the debounced update if there is one - this.refreshDebounced.cancel() - await this._refresh(this._editor, false) - } - } - private async onActiveLinesChanged(e: LinesChangeEvent) { if (!this._isReady) { return @@ -147,7 +108,7 @@ export class ActiveStateController implements vscode.Disposable { if (shouldDisplay !== undefined) { await this.updateDecorations(editor, selections, shouldDisplay) } else { - await this.updateDecorations(editor, selections, RecommendationService.instance.isRunning) + await this.updateDecorations(editor, selections, true) } } diff --git a/packages/core/src/codewhisperer/views/lineAnnotationController.ts b/packages/core/src/codewhisperer/views/lineAnnotationController.ts index 8b1d38ed7ae..c449f5ab1d9 100644 --- a/packages/core/src/codewhisperer/views/lineAnnotationController.ts +++ b/packages/core/src/codewhisperer/views/lineAnnotationController.ts @@ -9,18 +9,13 @@ import { LineSelection, LinesChangeEvent } from '../tracker/lineTracker' import { isTextEditor } from '../../shared/utilities/editorUtilities' import { cancellableDebounce } from '../../shared/utilities/functionUtils' import { subscribeOnce } from '../../shared/utilities/vsCodeUtils' -import { RecommendationService } from '../service/recommendationService' import { AnnotationChangeSource, inlinehintKey } from '../models/constants' import globals from '../../shared/extensionGlobals' import { Container } from '../service/serviceContainer' import { telemetry } from '../../shared/telemetry/telemetry' import { getLogger } from '../../shared/logger/logger' -import { Commands } from '../../shared/vscode/commands2' -import { session } from '../util/codeWhispererSession' -import { RecommendationHandler } from '../service/recommendationHandler' import { runtimeLanguageContext } from '../util/runtimeLanguageContext' import { setContext } from '../../shared/vscode/setContext' -import { TelemetryHelper } from '../util/telemetryHelper' const case3TimeWindow = 30000 // 30 seconds @@ -75,13 +70,7 @@ export class AutotriggerState implements AnnotationState { static acceptedCount = 0 updateState(changeSource: AnnotationChangeSource, force: boolean): AnnotationState | undefined { - if (AutotriggerState.acceptedCount < RecommendationService.instance.acceptedSuggestionCount) { - return new ManualtriggerState() - } else if (session.recommendations.length > 0 && RecommendationHandler.instance.isSuggestionVisible()) { - return new PressTabState() - } else { - return this - } + return undefined } isNextState(state: AnnotationState | undefined): boolean { @@ -268,28 +257,6 @@ export class LineAnnotationController implements vscode.Disposable { subscribeOnce(this.container.lineTracker.onReady)(async (_) => { await this.onReady() }), - RecommendationService.instance.suggestionActionEvent(async (e) => { - await telemetry.withTraceId(async () => { - if (!this._isReady) { - return - } - - if (this._currentState instanceof ManualtriggerState) { - if (e.triggerType === 'OnDemand' && this._currentState.hasManualTrigger === false) { - this._currentState.hasManualTrigger = true - } - if ( - e.response?.recommendationCount !== undefined && - e.response?.recommendationCount > 0 && - this._currentState.hasValidResponse === false - ) { - this._currentState.hasValidResponse = true - } - } - - await this.refresh(e.editor, 'codewhisperer') - }, TelemetryHelper.instance.traceId) - }), this.container.lineTracker.onDidChangeActiveLines(async (e) => { await this.onActiveLinesChanged(e) }), @@ -300,17 +267,6 @@ export class LineAnnotationController implements vscode.Disposable { }), this.container.auth.secondaryAuth.onDidChangeActiveConnection(async () => { await this.refresh(vscode.window.activeTextEditor, 'editor') - }), - Commands.register('aws.amazonq.dismissTutorial', async () => { - const editor = vscode.window.activeTextEditor - if (editor) { - this.clear() - try { - telemetry.ui_click.emit({ elementId: `dismiss_${this._currentState.id}` }) - } catch (_) {} - await this.dismissTutorial() - getLogger().debug(`codewhisperer: user dismiss tutorial.`) - } }) ) } @@ -484,7 +440,7 @@ export class LineAnnotationController implements vscode.Disposable { source: AnnotationChangeSource, force?: boolean ): Partial | undefined { - const isCWRunning = RecommendationService.instance.isRunning + const isCWRunning = true const textOptions: vscode.ThemableDecorationAttachmentRenderOptions = { contentText: '', @@ -517,9 +473,9 @@ export class LineAnnotationController implements vscode.Disposable { this._currentState = updatedState // take snapshot of accepted session so that we can compre if there is delta -> users accept 1 suggestion after seeing this state - AutotriggerState.acceptedCount = RecommendationService.instance.acceptedSuggestionCount + AutotriggerState.acceptedCount = 0 // take snapshot of total trigger count so that we can compare if there is delta -> users accept/reject suggestions after seeing this state - TryMoreExState.triggerCount = RecommendationService.instance.totalValidTriggerCount + TryMoreExState.triggerCount = 0 textOptions.contentText = this._currentState.text() diff --git a/packages/core/src/codewhispererChat/controllers/chat/controller.ts b/packages/core/src/codewhispererChat/controllers/chat/controller.ts index ba2072eb6dc..1be0c0332f5 100644 --- a/packages/core/src/codewhispererChat/controllers/chat/controller.ts +++ b/packages/core/src/codewhispererChat/controllers/chat/controller.ts @@ -59,7 +59,6 @@ import { triggerPayloadToChatRequest } from './chatRequest/converter' import { AuthUtil } from '../../../codewhisperer/util/authUtil' import { openUrl } from '../../../shared/utilities/vsCodeUtils' import { randomUUID } from '../../../shared/crypto' -import { LspController } from '../../../amazonq/lsp/lspController' import { CodeWhispererSettings } from '../../../codewhisperer/util/codewhispererSettings' import { getSelectedCustomization } from '../../../codewhisperer/util/customizationUtil' import { getHttpStatusCode, AwsClientResponseError } from '../../../shared/errors' @@ -70,8 +69,6 @@ import { inspect } from '../../../shared/utilities/collectionUtils' import { DefaultAmazonQAppInitContext } from '../../../amazonq/apps/initContext' import globals from '../../../shared/extensionGlobals' import { MynahIconsType, MynahUIDataModel, QuickActionCommand } from '@aws/mynah-ui' -import { LspClient } from '../../../amazonq/lsp/lspClient' -import { AdditionalContextPrompt, ContextCommandItem, ContextCommandItemType } from '../../../amazonq/lsp/types' import { workspaceCommand } from '../../../amazonq/webview/ui/tabs/constants' import fs from '../../../shared/fs/fs' import { FeatureConfigProvider, Features } from '../../../shared/featureConfig' @@ -80,9 +77,6 @@ import { getUserPromptsDirectory, promptFileExtension, createSavedPromptCommandId, - aditionalContentNameLimit, - additionalContentInnerContextLimit, - workspaceChunkMaxSize, defaultContextLengths, } from '../../constants' import { ChatSession } from '../../clients/chat/v0/chat' @@ -527,7 +521,6 @@ export class ChatController { commands: [{ command: commandName, description: commandDescription }], }) } - const symbolsCmd: QuickActionCommand = contextCommand[0].commands?.[3] const promptsCmd: QuickActionCommand = contextCommand[0].commands?.[4] // Check for user prompts @@ -543,7 +536,7 @@ export class ChatController { command: path.basename(name, promptFileExtension), icon: 'magic' as MynahIconsType, id: 'prompt', - label: 'file' as ContextCommandItemType, + // label: 'file' as ContextCommandItemType, route: [userPromptsDirectory, name], })) ) @@ -559,47 +552,7 @@ export class ChatController { icon: 'list-add' as MynahIconsType, }) - const lspClientReady = await LspClient.instance.waitUntilReady() - if (lspClientReady) { - const contextCommandItems = await LspClient.instance.getContextCommandItems() - const folderCmd: QuickActionCommand = contextCommand[0].commands?.[1] - const filesCmd: QuickActionCommand = contextCommand[0].commands?.[2] - - for (const contextCommandItem of contextCommandItems) { - const wsFolderName = path.basename(contextCommandItem.workspaceFolder) - if (contextCommandItem.type === 'file') { - filesCmd.children?.[0].commands.push({ - command: path.basename(contextCommandItem.relativePath), - description: path.join(wsFolderName, contextCommandItem.relativePath), - route: [contextCommandItem.workspaceFolder, contextCommandItem.relativePath], - label: 'file' as ContextCommandItemType, - id: contextCommandItem.id, - icon: 'file' as MynahIconsType, - }) - } else if (contextCommandItem.type === 'folder') { - folderCmd.children?.[0].commands.push({ - command: path.basename(contextCommandItem.relativePath), - description: path.join(wsFolderName, contextCommandItem.relativePath), - route: [contextCommandItem.workspaceFolder, contextCommandItem.relativePath], - label: 'folder' as ContextCommandItemType, - id: contextCommandItem.id, - icon: 'folder' as MynahIconsType, - }) - } else if (contextCommandItem.symbol && symbolsCmd.children) { - symbolsCmd.children?.[0].commands.push({ - command: contextCommandItem.symbol.name, - description: `${contextCommandItem.symbol.kind}, ${path.join(wsFolderName, contextCommandItem.relativePath)}, L${contextCommandItem.symbol.range.start.line}-${contextCommandItem.symbol.range.end.line}`, - route: [contextCommandItem.workspaceFolder, contextCommandItem.relativePath], - label: 'code' as ContextCommandItemType, - id: contextCommandItem.id, - icon: 'code-block' as MynahIconsType, - }) - } - } - } - this.messenger.sendContextCommandData(contextCommand) - void LspController.instance.updateContextCommandSymbolsOnce() } private handlePromptCreate(tabID: string) { @@ -1006,7 +959,7 @@ export class ChatController { } private async resolveContextCommandPayload(triggerPayload: TriggerPayload, session: ChatSession) { - const contextCommands: ContextCommandItem[] = [] + const contextCommands: any[] = [] // Check for workspace rules to add to context const workspaceRules = await this.collectWorkspaceRules() @@ -1017,7 +970,7 @@ export class ChatController { vscode.workspace.getWorkspaceFolder(vscode.Uri.parse(rule))?.uri?.path || '' return { workspaceFolder: workspaceFolderPath, - type: 'file' as ContextCommandItemType, + type: 'file' as any, relativePath: path.relative(workspaceFolderPath, rule), } }) @@ -1029,7 +982,7 @@ export class ChatController { if (typeof context !== 'string' && context.route && context.route.length === 2) { contextCommands.push({ workspaceFolder: context.route[0] || '', - type: (context.label || '') as ContextCommandItemType, + type: (context.label || '') as any, relativePath: context.route[1] || '', id: context.id, }) @@ -1044,45 +997,6 @@ export class ChatController { return [] } workspaceFolders.sort() - const workspaceFolder = workspaceFolders[0] - for (const contextCommand of contextCommands) { - session.relativePathToWorkspaceRoot.set(contextCommand.workspaceFolder, contextCommand.workspaceFolder) - } - let prompts: AdditionalContextPrompt[] = [] - try { - prompts = await LspClient.instance.getContextCommandPrompt(contextCommands) - } catch (e) { - // todo: handle @workspace used before indexing is ready - getLogger().verbose(`Could not get context command prompts: ${e}`) - } - - triggerPayload.contextLengths.additionalContextLengths = this.telemetryHelper.getContextLengths(prompts) - for (const prompt of prompts.slice(0, 20)) { - // Add system prompt for user prompts and workspace rules - const contextType = this.telemetryHelper.getContextType(prompt) - const description = - contextType === 'rule' || contextType === 'prompt' - ? `You must follow the instructions in ${prompt.relativePath}. Below are lines ${prompt.startLine}-${prompt.endLine} of this file:\n` - : prompt.description - - // Handle user prompts outside the workspace - const relativePath = prompt.filePath.startsWith(getUserPromptsDirectory()) - ? path.basename(prompt.filePath) - : path.relative(workspaceFolder, prompt.filePath) - - const entry = { - name: prompt.name.substring(0, aditionalContentNameLimit), - description: description.substring(0, aditionalContentNameLimit), - innerContext: prompt.content.substring(0, additionalContentInnerContextLimit), - type: contextType, - relativePath: relativePath, - startLine: prompt.startLine, - endLine: prompt.endLine, - } - - triggerPayload.additionalContents.push(entry) - } - getLogger().info(`Retrieved chunks of additional context count: ${triggerPayload.additionalContents.length} `) } private async generateResponse( @@ -1130,25 +1044,6 @@ export class ChatController { if (triggerPayload.useRelevantDocuments) { triggerPayload.message = triggerPayload.message.replace(/@workspace/, '') if (CodeWhispererSettings.instance.isLocalIndexEnabled()) { - const start = performance.now() - const relevantTextDocuments = await LspController.instance.query(triggerPayload.message) - for (const relevantDocument of relevantTextDocuments) { - if (relevantDocument.text && relevantDocument.text.length > 0) { - triggerPayload.contextLengths.workspaceContextLength += relevantDocument.text.length - if (relevantDocument.text.length > workspaceChunkMaxSize) { - relevantDocument.text = relevantDocument.text.substring(0, workspaceChunkMaxSize) - getLogger().debug(`Truncating @workspace chunk: ${relevantDocument.relativeFilePath} `) - } - triggerPayload.relevantTextDocuments.push(relevantDocument) - } - } - - for (const doc of triggerPayload.relevantTextDocuments) { - getLogger().info( - `amazonq: Using workspace files ${doc.relativeFilePath}, content(partial): ${doc.text?.substring(0, 200)}, start line: ${doc.startLine}, end line: ${doc.endLine}` - ) - } - triggerPayload.projectContextQueryLatencyMs = performance.now() - start } else { this.messenger.sendOpenSettingsMessage(triggerID, tabID) return diff --git a/packages/core/src/codewhispererChat/controllers/chat/messenger/messenger.ts b/packages/core/src/codewhispererChat/controllers/chat/messenger/messenger.ts index 8c914686ad4..ab059ecb22d 100644 --- a/packages/core/src/codewhispererChat/controllers/chat/messenger/messenger.ts +++ b/packages/core/src/codewhispererChat/controllers/chat/messenger/messenger.ts @@ -40,7 +40,6 @@ import { FeatureAuthState } from '../../../../codewhisperer/util/authUtil' import { CodeScanIssue } from '../../../../codewhisperer/models/model' import { marked } from 'marked' import { JSDOM } from 'jsdom' -import { LspController } from '../../../../amazonq/lsp/lspController' import { extractCodeBlockLanguage } from '../../../../shared/markdown' import { extractAuthFollowUp } from '../../../../amazonq/util/authUtils' import { helpMessage } from '../../../../amazonq/webview/ui/texts/constants' @@ -290,11 +289,7 @@ export class Messenger { relatedContent: { title: 'Sources', content: relatedSuggestions as any }, }) } - if ( - triggerPayload.relevantTextDocuments && - triggerPayload.relevantTextDocuments.length > 0 && - LspController.instance.isIndexingInProgress() - ) { + if (triggerPayload.relevantTextDocuments && triggerPayload.relevantTextDocuments.length > 0) { this.dispatcher.sendChatMessage( new ChatMessage( { diff --git a/packages/core/src/codewhispererChat/controllers/chat/telemetryHelper.ts b/packages/core/src/codewhispererChat/controllers/chat/telemetryHelper.ts index 2d9e01db9a0..ac914e77b6b 100644 --- a/packages/core/src/codewhispererChat/controllers/chat/telemetryHelper.ts +++ b/packages/core/src/codewhispererChat/controllers/chat/telemetryHelper.ts @@ -2,7 +2,7 @@ * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: Apache-2.0 */ -import * as path from 'path' + import { UserIntent } from '@amzn/codewhisperer-streaming' import { AmazonqAddMessage, @@ -28,7 +28,6 @@ import { ResponseBodyLinkClickMessage, SourceLinkClickMessage, TriggerPayload, - AdditionalContextLengths, AdditionalContextInfo, } from './model' import { TriggerEvent, TriggerEventsStorage } from '../../storages/triggerEvents' @@ -43,9 +42,6 @@ import { supportedLanguagesList } from '../chat/chatRequest/converter' import { AuthUtil } from '../../../codewhisperer/util/authUtil' import { getSelectedCustomization } from '../../../codewhisperer/util/customizationUtil' import { undefinedIfEmpty } from '../../../shared/utilities/textUtilities' -import { AdditionalContextPrompt } from '../../../amazonq/lsp/types' -import { getUserPromptsDirectory, promptFileExtension } from '../../constants' -import { isInDirectory } from '../../../shared/filesystemUtilities' import { sleep } from '../../../shared/utilities/timeoutUtils' import { FileDiagnostic, @@ -164,40 +160,6 @@ export class CWCTelemetryHelper { telemetry.amazonq_exitFocusChat.emit({ result: 'Succeeded', passive: true }) } - public getContextType(prompt: AdditionalContextPrompt): string { - if (prompt.filePath.endsWith(promptFileExtension)) { - if (isInDirectory(path.join('.amazonq', 'rules'), prompt.relativePath)) { - return 'rule' - } else if (isInDirectory(getUserPromptsDirectory(), prompt.filePath)) { - return 'prompt' - } - } - return 'file' - } - - public getContextLengths(prompts: AdditionalContextPrompt[]): AdditionalContextLengths { - let fileContextLength = 0 - let promptContextLength = 0 - let ruleContextLength = 0 - - for (const prompt of prompts) { - const type = this.getContextType(prompt) - switch (type) { - case 'rule': - ruleContextLength += prompt.content.length - break - case 'file': - fileContextLength += prompt.content.length - break - case 'prompt': - promptContextLength += prompt.content.length - break - } - } - - return { fileContextLength, promptContextLength, ruleContextLength } - } - public async recordFeedback(message: ChatItemFeedbackMessage) { const logger = getLogger() try { diff --git a/packages/core/src/dev/activation.ts b/packages/core/src/dev/activation.ts index 8ce0f6aab11..16b5d7e53ad 100644 --- a/packages/core/src/dev/activation.ts +++ b/packages/core/src/dev/activation.ts @@ -25,7 +25,6 @@ import { NotificationsController } from '../notifications/controller' import { DevNotificationsState } from '../notifications/types' import { QuickPickItem } from 'vscode' import { ChildProcess } from '../shared/utilities/processUtils' -import { WorkspaceLspInstaller } from '../amazonq/lsp/workspaceInstaller' interface MenuOption { readonly label: string @@ -451,12 +450,6 @@ const resettableFeatures: readonly ResettableFeature[] = [ detail: 'Resets memory/global state for the notifications panel (includes dismissed, onReceive).', executor: resetNotificationsState, }, - { - name: 'workspace lsp', - label: 'Download Lsp ', - detail: 'Resets workspace LSP', - executor: resetWorkspaceLspDownload, - }, ] as const // TODO this is *somewhat* similar to `openStorageFromInput`. If we need another @@ -545,10 +538,6 @@ async function resetNotificationsState() { await targetNotificationsController.reset() } -async function resetWorkspaceLspDownload() { - await new WorkspaceLspInstaller().resolve() -} - async function editNotifications() { const storageKey = 'aws.notifications.dev' const current = globalState.get(storageKey) ?? {} diff --git a/packages/core/src/shared/index.ts b/packages/core/src/shared/index.ts index 9a08a7afaf3..226ae6280b8 100644 --- a/packages/core/src/shared/index.ts +++ b/packages/core/src/shared/index.ts @@ -50,6 +50,9 @@ export * as env from './vscode/env' export * from './vscode/commands2' export * from './utilities/pathUtils' export * from './utilities/zipStream' +export * as editorUtilities from './utilities/editorUtilities' +export * as functionUtilities from './utilities/functionUtils' +export * as vscodeUtilities from './utilities/vsCodeUtils' export * from './errors' export { isTextEditor } from './utilities/editorUtilities' export * as messages from './utilities/messages' diff --git a/packages/core/src/shared/lsp/baseLspInstaller.ts b/packages/core/src/shared/lsp/baseLspInstaller.ts index 0aeca1dfda4..7acf58ad788 100644 --- a/packages/core/src/shared/lsp/baseLspInstaller.ts +++ b/packages/core/src/shared/lsp/baseLspInstaller.ts @@ -5,7 +5,6 @@ import * as nodePath from 'path' import vscode from 'vscode' -import { LspConfig } from '../../amazonq/lsp/config' import { LanguageServerResolver } from './lspResolver' import { ManifestResolver } from './manifestResolver' import { LspResolution, ResourcePaths } from './types' @@ -14,6 +13,14 @@ import { Range } from 'semver' import { getLogger } from '../logger/logger' import type { Logger, LogTopic } from '../logger/logger' +export interface LspConfig { + manifestUrl: string + supportedVersions: string + id: string + suppressPromptPrefix: string + path?: string +} + export abstract class BaseLspInstaller { private logger: Logger diff --git a/packages/core/src/shared/settings-toolkit.gen.ts b/packages/core/src/shared/settings-toolkit.gen.ts index 59a637a4870..10020cf51f9 100644 --- a/packages/core/src/shared/settings-toolkit.gen.ts +++ b/packages/core/src/shared/settings-toolkit.gen.ts @@ -44,6 +44,7 @@ export const toolkitSettings = { "jsonResourceModification": {}, "amazonqLSP": {}, "amazonqLSPInline": {}, + "amazonqLSPInlineChat": {}, "amazonqChatLSP": {} }, "aws.resources.enabledResources": {}, diff --git a/packages/core/src/shared/telemetry/exemptMetrics.ts b/packages/core/src/shared/telemetry/exemptMetrics.ts index a3fc8d5ad78..4e0deacc058 100644 --- a/packages/core/src/shared/telemetry/exemptMetrics.ts +++ b/packages/core/src/shared/telemetry/exemptMetrics.ts @@ -29,6 +29,8 @@ const validationExemptMetrics: Set = new Set([ 'codewhisperer_codePercentage', 'codewhisperer_userModification', 'codewhisperer_userTriggerDecision', + 'codewhisperer_perceivedLatency', // flare doesn't currently set result property + 'codewhisperer_serviceInvocation', // flare doesn't currently set result property 'dynamicresource_selectResources', 'dynamicresource_copyIdentifier', 'dynamicresource_mutateResource', diff --git a/packages/core/src/shared/utilities/functionUtils.ts b/packages/core/src/shared/utilities/functionUtils.ts index cbf89340ade..214721b1cdb 100644 --- a/packages/core/src/shared/utilities/functionUtils.ts +++ b/packages/core/src/shared/utilities/functionUtils.ts @@ -93,9 +93,10 @@ export function memoize(fn: (...args: U) => T): (...args: U) */ export function debounce( cb: (...args: Input) => Output | Promise, - delay: number = 0 + delay: number = 0, + useLastCall: boolean = false ): (...args: Input) => Promise { - return cancellableDebounce(cb, delay).promise + return cancellableDebounce(cb, delay, useLastCall).promise } /** @@ -104,10 +105,12 @@ export function debounce( */ export function cancellableDebounce( cb: (...args: Input) => Output | Promise, - delay: number = 0 + delay: number = 0, + useLastCall: boolean = false ): { promise: (...args: Input) => Promise; cancel: () => void } { let timeout: Timeout | undefined let promise: Promise | undefined + let lastestArgs: Input | undefined const cancel = (): void => { if (timeout) { @@ -119,6 +122,7 @@ export function cancellableDebounce( return { promise: (...args: Input) => { + lastestArgs = args timeout?.refresh() return (promise ??= new Promise((resolve, reject) => { @@ -126,7 +130,8 @@ export function cancellableDebounce( timeout.onCompletion(async () => { timeout = promise = undefined try { - resolve(await cb(...args)) + const argsToUse = useLastCall ? lastestArgs! : args + resolve(await cb(...argsToUse)) } catch (err) { reject(err) } diff --git a/packages/core/src/shared/utilities/index.ts b/packages/core/src/shared/utilities/index.ts index 520390b5204..ecf753090ca 100644 --- a/packages/core/src/shared/utilities/index.ts +++ b/packages/core/src/shared/utilities/index.ts @@ -6,3 +6,4 @@ export { isExtensionInstalled, isExtensionActive } from './vsCodeUtils' export { VSCODE_EXTENSION_ID } from '../extensions' export * from './functionUtils' +export * as messageUtils from './messages' diff --git a/packages/core/src/test/codewhisperer/commands/basicCommands.test.ts b/packages/core/src/test/codewhisperer/commands/basicCommands.test.ts index 936e7d84cd6..b911c9687ee 100644 --- a/packages/core/src/test/codewhisperer/commands/basicCommands.test.ts +++ b/packages/core/src/test/codewhisperer/commands/basicCommands.test.ts @@ -57,7 +57,7 @@ import { waitUntil } from '../../../shared/utilities/timeoutUtils' import { listCodeWhispererCommands } from '../../../codewhisperer/ui/statusBarMenu' import { CodeScanIssue, CodeScansState, CodeSuggestionsState, codeScanState } from '../../../codewhisperer/models/model' import { cwQuickPickSource } from '../../../codewhisperer/commands/types' -import { refreshStatusBar } from '../../../codewhisperer/service/inlineCompletionService' +import { refreshStatusBar } from '../../../codewhisperer/service/statusBar' import { focusAmazonQPanel } from '../../../codewhispererChat/commands/registerCommands' import * as diagnosticsProvider from '../../../codewhisperer/service/diagnosticsProvider' import { randomUUID } from '../../../shared/crypto' diff --git a/packages/core/src/test/codewhisperer/testUtil.ts b/packages/core/src/test/codewhisperer/testUtil.ts index f3b82fd3850..dd8188b1006 100644 --- a/packages/core/src/test/codewhisperer/testUtil.ts +++ b/packages/core/src/test/codewhisperer/testUtil.ts @@ -14,7 +14,6 @@ import { } from '../../codewhisperer/models/model' import { MockDocument } from '../fake/fakeDocument' import { getLogger } from '../../shared/logger' -import { CodeWhispererCodeCoverageTracker } from '../../codewhisperer/tracker/codewhispererCodeCoverageTracker' import globals from '../../shared/extensionGlobals' import { session } from '../../codewhisperer/util/codeWhispererSession' import { DefaultAWSClientBuilder, ServiceOptions } from '../../shared/awsClientBuilder' @@ -23,7 +22,6 @@ import { HttpResponse, Service } from 'aws-sdk' import userApiConfig = require('./../../codewhisperer/client/user-service-2.json') import CodeWhispererUserClient = require('../../codewhisperer/client/codewhispereruserclient') import { codeWhispererClient } from '../../codewhisperer/client/codewhisperer' -import { RecommendationHandler } from '../../codewhisperer/service/recommendationHandler' import * as model from '../../codewhisperer/models/model' import { stub } from '../utilities/stubber' import { Dirent } from 'fs' // eslint-disable-line no-restricted-imports @@ -31,12 +29,10 @@ import { Dirent } from 'fs' // eslint-disable-line no-restricted-imports export async function resetCodeWhispererGlobalVariables() { vsCodeState.isIntelliSenseActive = false vsCodeState.isCodeWhispererEditing = false - CodeWhispererCodeCoverageTracker.instances.clear() globals.telemetry.logger.clear() session.reset() await globals.globalState.clear() await CodeSuggestionsState.instance.setSuggestionsEnabled(true) - await RecommendationHandler.instance.clearInlineCompletionStates() } export function createMockDocument( diff --git a/packages/core/src/test/codewhisperer/zipUtil.test.ts b/packages/core/src/test/codewhisperer/zipUtil.test.ts index a82db4a6840..e6c4f4148e5 100644 --- a/packages/core/src/test/codewhisperer/zipUtil.test.ts +++ b/packages/core/src/test/codewhisperer/zipUtil.test.ts @@ -16,7 +16,6 @@ import { ToolkitError } from '../../shared/errors' import { fs } from '../../shared/fs/fs' import { tempDirPath } from '../../shared/filesystemUtilities' import { CodeWhispererConstants } from '../../codewhisperer/indexNode' -import { LspClient } from '../../amazonq/lsp/lspClient' describe('zipUtil', function () { const workspaceFolder = getTestWorkspaceFolder() @@ -179,23 +178,5 @@ describe('zipUtil', function () { assert.strictEqual(result.language, 'java') assert.strictEqual(result.scannedFiles.size, 4) }) - - it('Should handle file system errors during directory creation', async function () { - sinon.stub(LspClient, 'instance').get(() => ({ - getRepoMapJSON: sinon.stub().resolves('{"mock": "data"}'), - })) - sinon.stub(fs, 'mkdir').rejects(new Error('Directory creation failed')) - - await assert.rejects(() => zipUtil.generateZipTestGen(appRoot, false), /Directory creation failed/) - }) - - it('Should handle zip project errors', async function () { - sinon.stub(LspClient, 'instance').get(() => ({ - getRepoMapJSON: sinon.stub().resolves('{"mock": "data"}'), - })) - sinon.stub(zipUtil, 'zipProject' as keyof ZipUtil).rejects(new Error('Zip failed')) - - await assert.rejects(() => zipUtil.generateZipTestGen(appRoot, false), /Zip failed/) - }) }) }) diff --git a/packages/core/src/test/shared/utilities/functionUtils.test.ts b/packages/core/src/test/shared/utilities/functionUtils.test.ts index 7880d11ff63..b675fe74feb 100644 --- a/packages/core/src/test/shared/utilities/functionUtils.test.ts +++ b/packages/core/src/test/shared/utilities/functionUtils.test.ts @@ -152,6 +152,33 @@ describe('debounce', function () { assert.strictEqual(counter, 2) }) + describe('useLastCall option', function () { + let args: number[] + let clock: ReturnType + let addToArgs: (i: number) => void + + before(function () { + args = [] + clock = installFakeClock() + addToArgs = (n: number) => args.push(n) + }) + + afterEach(function () { + clock.uninstall() + args.length = 0 + }) + + it('only calls with the last args', async function () { + const debounced = debounce(addToArgs, 10, true) + const p1 = debounced(1) + const p2 = debounced(2) + const p3 = debounced(3) + await clock.tickAsync(100) + await Promise.all([p1, p2, p3]) + assert.deepStrictEqual(args, [3]) + }) + }) + describe('window rolling', function () { let clock: ReturnType const calls: ReturnType[] = [] diff --git a/packages/core/src/testE2E/codewhisperer/referenceTracker.test.ts b/packages/core/src/testE2E/codewhisperer/referenceTracker.test.ts deleted file mode 100644 index 0038795ad89..00000000000 --- a/packages/core/src/testE2E/codewhisperer/referenceTracker.test.ts +++ /dev/null @@ -1,125 +0,0 @@ -/*! - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -import assert from 'assert' -import * as codewhispererClient from '../../codewhisperer/client/codewhisperer' -import { ConfigurationEntry } from '../../codewhisperer/models/model' -import { setValidConnection, skipTestIfNoValidConn } from '../util/connection' -import { RecommendationHandler } from '../../codewhisperer/service/recommendationHandler' -import { createMockTextEditor, resetCodeWhispererGlobalVariables } from '../../test/codewhisperer/testUtil' -import { invokeRecommendation } from '../../codewhisperer/commands/invokeRecommendation' -import { session } from '../../codewhisperer/util/codeWhispererSession' - -/* -New model deployment may impact references returned. - -These tests: - 1) are not required for github approval flow - 2) will be auto-skipped until fix for manual runs is posted. -*/ - -const leftContext = `InAuto.GetContent( - InAuto.servers.auto, "vendors.json", - function (data) { - let block = ''; - for(let i = 0; i < data.length; i++) { - block += '' + cars[i].title + ''; - } - $('#cars').html(block); - });` - -describe('CodeWhisperer service invocation', async function () { - let validConnection: boolean - const client = new codewhispererClient.DefaultCodeWhispererClient() - const configWithRefs: ConfigurationEntry = { - isShowMethodsEnabled: true, - isManualTriggerEnabled: true, - isAutomatedTriggerEnabled: true, - isSuggestionsWithCodeReferencesEnabled: true, - } - const configWithNoRefs: ConfigurationEntry = { - isShowMethodsEnabled: true, - isManualTriggerEnabled: true, - isAutomatedTriggerEnabled: true, - isSuggestionsWithCodeReferencesEnabled: false, - } - - before(async function () { - validConnection = await setValidConnection() - }) - - beforeEach(function () { - void resetCodeWhispererGlobalVariables() - RecommendationHandler.instance.clearRecommendations() - // TODO: remove this line (this.skip()) when these tests no longer auto-skipped - this.skip() - // valid connection required to run tests - skipTestIfNoValidConn(validConnection, this) - }) - - it('trigger known to return recs with references returns rec with reference', async function () { - // check that handler is empty before invocation - const requestIdBefore = RecommendationHandler.instance.requestId - const sessionIdBefore = session.sessionId - const validRecsBefore = RecommendationHandler.instance.isValidResponse() - - assert.ok(requestIdBefore.length === 0) - assert.ok(sessionIdBefore.length === 0) - assert.ok(!validRecsBefore) - - const doc = leftContext + rightContext - const filename = 'test.js' - const language = 'javascript' - const line = 5 - const character = 39 - const mockEditor = createMockTextEditor(doc, filename, language, line, character) - - await invokeRecommendation(mockEditor, client, configWithRefs) - - const requestId = RecommendationHandler.instance.requestId - const sessionId = session.sessionId - const validRecs = RecommendationHandler.instance.isValidResponse() - const references = session.recommendations[0].references - - assert.ok(requestId.length > 0) - assert.ok(sessionId.length > 0) - assert.ok(validRecs) - assert.ok(references !== undefined) - // TODO: uncomment this assert when this test is no longer auto-skipped - // assert.ok(references.length > 0) - }) - - // This test will fail if user is logged in with IAM identity center - it('trigger known to return rec with references does not return rec with references when reference tracker setting is off', async function () { - // check that handler is empty before invocation - const requestIdBefore = RecommendationHandler.instance.requestId - const sessionIdBefore = session.sessionId - const validRecsBefore = RecommendationHandler.instance.isValidResponse() - - assert.ok(requestIdBefore.length === 0) - assert.ok(sessionIdBefore.length === 0) - assert.ok(!validRecsBefore) - - const doc = leftContext + rightContext - const filename = 'test.js' - const language = 'javascript' - const line = 5 - const character = 39 - const mockEditor = createMockTextEditor(doc, filename, language, line, character) - - await invokeRecommendation(mockEditor, client, configWithNoRefs) - - const requestId = RecommendationHandler.instance.requestId - const sessionId = session.sessionId - const validRecs = RecommendationHandler.instance.isValidResponse() - - assert.ok(requestId.length > 0) - assert.ok(sessionId.length > 0) - // no recs returned because example request returns 1 rec with reference, so no recs returned when references off - assert.ok(!validRecs) - }) -}) diff --git a/packages/core/src/testE2E/codewhisperer/serviceInvocations.test.ts b/packages/core/src/testE2E/codewhisperer/serviceInvocations.test.ts deleted file mode 100644 index d4265d13982..00000000000 --- a/packages/core/src/testE2E/codewhisperer/serviceInvocations.test.ts +++ /dev/null @@ -1,124 +0,0 @@ -/*! - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -import assert from 'assert' -import * as vscode from 'vscode' -import * as path from 'path' -import { setValidConnection, skipTestIfNoValidConn } from '../util/connection' -import { ConfigurationEntry } from '../../codewhisperer/models/model' -import * as codewhispererClient from '../../codewhisperer/client/codewhisperer' -import { RecommendationHandler } from '../../codewhisperer/service/recommendationHandler' -import { - createMockTextEditor, - createTextDocumentChangeEvent, - resetCodeWhispererGlobalVariables, -} from '../../test/codewhisperer/testUtil' -import { KeyStrokeHandler } from '../../codewhisperer/service/keyStrokeHandler' -import { sleep } from '../../shared/utilities/timeoutUtils' -import { invokeRecommendation } from '../../codewhisperer/commands/invokeRecommendation' -import { getTestWorkspaceFolder } from '../../testInteg/integrationTestsUtilities' -import { session } from '../../codewhisperer/util/codeWhispererSession' - -describe('CodeWhisperer service invocation', async function () { - let validConnection: boolean - const client = new codewhispererClient.DefaultCodeWhispererClient() - const config: ConfigurationEntry = { - isShowMethodsEnabled: true, - isManualTriggerEnabled: true, - isAutomatedTriggerEnabled: true, - isSuggestionsWithCodeReferencesEnabled: true, - } - - before(async function () { - validConnection = await setValidConnection() - }) - - beforeEach(function () { - void resetCodeWhispererGlobalVariables() - RecommendationHandler.instance.clearRecommendations() - // valid connection required to run tests - skipTestIfNoValidConn(validConnection, this) - }) - - it('manual trigger returns valid recommendation response', async function () { - // check that handler is empty before invocation - const requestIdBefore = RecommendationHandler.instance.requestId - const sessionIdBefore = session.sessionId - const validRecsBefore = RecommendationHandler.instance.isValidResponse() - - assert.ok(requestIdBefore.length === 0) - assert.ok(sessionIdBefore.length === 0) - assert.ok(!validRecsBefore) - - const mockEditor = createMockTextEditor() - await invokeRecommendation(mockEditor, client, config) - - const requestId = RecommendationHandler.instance.requestId - const sessionId = session.sessionId - const validRecs = RecommendationHandler.instance.isValidResponse() - - assert.ok(requestId.length > 0) - assert.ok(sessionId.length > 0) - assert.ok(validRecs) - }) - - it('auto trigger returns valid recommendation response', async function () { - // check that handler is empty before invocation - const requestIdBefore = RecommendationHandler.instance.requestId - const sessionIdBefore = session.sessionId - const validRecsBefore = RecommendationHandler.instance.isValidResponse() - - assert.ok(requestIdBefore.length === 0) - assert.ok(sessionIdBefore.length === 0) - assert.ok(!validRecsBefore) - - const mockEditor = createMockTextEditor() - - const mockEvent: vscode.TextDocumentChangeEvent = createTextDocumentChangeEvent( - mockEditor.document, - new vscode.Range(new vscode.Position(0, 0), new vscode.Position(0, 1)), - '\n' - ) - - await KeyStrokeHandler.instance.processKeyStroke(mockEvent, mockEditor, client, config) - // wait for 5 seconds to allow time for response to be generated - await sleep(5000) - - const requestId = RecommendationHandler.instance.requestId - const sessionId = session.sessionId - const validRecs = RecommendationHandler.instance.isValidResponse() - - assert.ok(requestId.length > 0) - assert.ok(sessionId.length > 0) - assert.ok(validRecs) - }) - - it('invocation in unsupported language does not generate a request', async function () { - const workspaceFolder = getTestWorkspaceFolder() - const appRoot = path.join(workspaceFolder, 'go1-plain-sam-app') - const appCodePath = path.join(appRoot, 'hello-world', 'main.go') - - // check that handler is empty before invocation - const requestIdBefore = RecommendationHandler.instance.requestId - const sessionIdBefore = session.sessionId - const validRecsBefore = RecommendationHandler.instance.isValidResponse() - - assert.ok(requestIdBefore.length === 0) - assert.ok(sessionIdBefore.length === 0) - assert.ok(!validRecsBefore) - - const doc = await vscode.workspace.openTextDocument(vscode.Uri.file(appCodePath)) - const editor = await vscode.window.showTextDocument(doc) - await invokeRecommendation(editor, client, config) - - const requestId = RecommendationHandler.instance.requestId - const sessionId = session.sessionId - const validRecs = RecommendationHandler.instance.isValidResponse() - - assert.ok(requestId.length === 0) - assert.ok(sessionId.length === 0) - assert.ok(!validRecs) - }) -}) diff --git a/packages/core/src/testInteg/perf/buildIndex.test.ts b/packages/core/src/testInteg/perf/buildIndex.test.ts deleted file mode 100644 index d60de3bdc3a..00000000000 --- a/packages/core/src/testInteg/perf/buildIndex.test.ts +++ /dev/null @@ -1,79 +0,0 @@ -/*! - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -import { performanceTest } from '../../shared/performance/performance' -import * as sinon from 'sinon' -import * as vscode from 'vscode' -import assert from 'assert' -import { LspClient, LspController } from '../../amazonq' -import { LanguageClient, ServerOptions } from 'vscode-languageclient' -import { createTestWorkspace } from '../../test/testUtil' -import { BuildIndexRequestType, GetUsageRequestType } from '../../amazonq/lsp/types' -import { fs, getRandomString } from '../../shared' -import { FileSystem } from '../../shared/fs/fs' -import { getFsCallsUpperBound } from './utilities' - -interface SetupResult { - clientReqStub: sinon.SinonStub - fsSpy: sinon.SinonSpiedInstance - findFilesSpy: sinon.SinonSpy -} - -async function verifyResult(setup: SetupResult) { - // A correct run makes 2 requests, but don't want to make it exact to avoid over-sensitivity to implementation. If we make 10+ something is likely wrong. - assert.ok(setup.clientReqStub.callCount >= 2 && setup.clientReqStub.callCount <= 10) - assert.ok(setup.clientReqStub.calledWith(BuildIndexRequestType)) - assert.ok(setup.clientReqStub.calledWith(GetUsageRequestType)) - - assert.strictEqual(getFsCallsUpperBound(setup.fsSpy), 0, 'should not make any fs calls') - assert.ok(setup.findFilesSpy.callCount <= 2, 'findFiles should not be called more than twice') -} - -async function setupWithWorkspace(numFiles: number, options: { fileContent: string }): Promise { - // Force VSCode to find my test workspace only to keep test contained and controlled. - const testWorksapce = await createTestWorkspace(numFiles, options) - sinon.stub(vscode.workspace, 'workspaceFolders').value([testWorksapce]) - - // Avoid sending real request to lsp. - const clientReqStub = sinon.stub(LanguageClient.prototype, 'sendRequest').resolves(true) - const fsSpy = sinon.spy(fs) - const findFilesSpy = sinon.spy(vscode.workspace, 'findFiles') - LspClient.instance.client = new LanguageClient('amazonq', 'test-client', {} as ServerOptions, {}) - return { clientReqStub, fsSpy, findFilesSpy } -} - -describe('buildIndex', function () { - describe('performanceTests', function () { - afterEach(function () { - sinon.restore() - }) - performanceTest({}, 'indexing many small files', function () { - return { - setup: async () => setupWithWorkspace(250, { fileContent: '0123456789' }), - execute: async () => { - await LspController.instance.buildIndex({ - startUrl: '', - maxIndexSize: 30, - isVectorIndexEnabled: true, - }) - }, - verify: verifyResult, - } - }) - performanceTest({}, 'indexing few large files', function () { - return { - setup: async () => setupWithWorkspace(10, { fileContent: getRandomString(1000) }), - execute: async () => { - await LspController.instance.buildIndex({ - startUrl: '', - maxIndexSize: 30, - isVectorIndexEnabled: true, - }) - }, - verify: verifyResult, - } - }) - }) -}) diff --git a/packages/toolkit/package.json b/packages/toolkit/package.json index f509647ab10..e4a6639c3a1 100644 --- a/packages/toolkit/package.json +++ b/packages/toolkit/package.json @@ -258,6 +258,10 @@ "amazonqChatLSP": { "type": "boolean", "default": true + }, + "amazonqLSPInlineChat": { + "type": "boolean", + "default": false } }, "additionalProperties": false