Skip to content

Commit eb7b8e8

Browse files
authored
feat(codewhisperer): auto trigger improvements (#3093)
## Problem Current auto-trigger heuristics is not very responsive ## Solution Replacing the character number based triggering rule with an idle time based triggering rule to make the triggering more aggressive
1 parent 0a66eea commit eb7b8e8

File tree

7 files changed

+76
-67
lines changed

7 files changed

+76
-67
lines changed
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
{
2+
"type": "Feature",
3+
"description": "CodeWhisperer: more responsive Auto-Suggestions"
4+
}

src/codewhisperer/commands/invokeRecommendation.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@ import { DefaultCodeWhispererClient } from '../client/codewhisperer'
1010
import { InlineCompletion } from '../service/inlineCompletion'
1111
import { isCloud9 } from '../../shared/extensionUtilities'
1212
import { RecommendationHandler } from '../service/recommendationHandler'
13-
import { KeyStrokeHandler } from '../service/keyStrokeHandler'
1413
import { isInlineCompletionEnabled } from '../util/commonUtil'
1514
import { InlineCompletionService } from '../service/inlineCompletionService'
1615
import { AuthUtil } from '../util/authUtil'
@@ -52,7 +51,6 @@ export async function invokeRecommendation(
5251
RecommendationHandler.instance.isValidResponse()
5352
)
5453
}
55-
KeyStrokeHandler.instance.keyStrokeCount = 0
5654
if (isCloud9()) {
5755
if (RecommendationHandler.instance.isGenerateRecommendationInProgress) {
5856
return

src/codewhisperer/models/constants.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ export const promiseTimeoutLimit = 15 // seconds
2020

2121
export const invocationKeyThreshold = 15
2222

23+
export const idleTimerPollPeriod = 25 // milliseconds
24+
2325
export const specialCharactersList = ['{', '[', '(', ':', '\t', '\n']
2426

2527
export const normalTextChangeRegex = /[A-Za-z0-9]/g

src/codewhisperer/service/inlineCompletionService.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -419,6 +419,10 @@ export class InlineCompletionService {
419419
this.statusBar.show()
420420
}
421421

422+
isPaginationRunning(): boolean {
423+
return this._isPaginationRunning
424+
}
425+
422426
hideCodeWhispererStatusBar() {
423427
this.statusBar.hide()
424428
}

src/codewhisperer/service/keyStrokeHandler.ts

Lines changed: 48 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -30,11 +30,11 @@ export class KeyStrokeHandler {
3030
/**
3131
* Key stroke count for automated trigger
3232
*/
33-
public keyStrokeCount: number
33+
34+
private idleTriggerTimer?: NodeJS.Timer
3435

3536
constructor() {
3637
this.specialChar = ''
37-
this.keyStrokeCount = 0
3838
}
3939

4040
static #instance: KeyStrokeHandler
@@ -43,6 +43,49 @@ export class KeyStrokeHandler {
4343
return (this.#instance ??= new this())
4444
}
4545

46+
public startIdleTimeTriggerTimer(
47+
event: vscode.TextDocumentChangeEvent,
48+
editor: vscode.TextEditor,
49+
client: DefaultCodeWhispererClient,
50+
config: ConfigurationEntry
51+
) {
52+
if (this.idleTriggerTimer) {
53+
clearInterval(this.idleTriggerTimer)
54+
this.idleTriggerTimer = undefined
55+
}
56+
if (!this.shouldTriggerIdleTime()) {
57+
return
58+
}
59+
this.idleTriggerTimer = setInterval(() => {
60+
const duration = (performance.now() - RecommendationHandler.instance.lastInvocationTime) / 1000
61+
if (duration < CodeWhispererConstants.invocationTimeIntervalThreshold) {
62+
return
63+
}
64+
65+
try {
66+
this.invokeAutomatedTrigger('IdleTime', editor, client, config)
67+
} finally {
68+
if (this.idleTriggerTimer) {
69+
clearInterval(this.idleTriggerTimer)
70+
this.idleTriggerTimer = undefined
71+
}
72+
}
73+
}, CodeWhispererConstants.idleTimerPollPeriod)
74+
}
75+
76+
public shouldTriggerIdleTime(): boolean {
77+
if (isCloud9() && RecommendationHandler.instance.isGenerateRecommendationInProgress) {
78+
return false
79+
}
80+
if (isInlineCompletionEnabled() && InlineCompletionService.instance.isPaginationRunning()) {
81+
return false
82+
}
83+
if (InlineCompletion.instance.getIsActive || InlineCompletion.instance.isPaginationRunning()) {
84+
return false
85+
}
86+
return true
87+
}
88+
4689
async processKeyStroke(
4790
event: vscode.TextDocumentChangeEvent,
4891
editor: vscode.TextEditor,
@@ -77,35 +120,23 @@ export class KeyStrokeHandler {
77120

78121
let triggerType: CodewhispererAutomatedTriggerType | undefined
79122
const changedSource = new DefaultDocumentChangedType(event.contentChanges).checkChangeSource()
80-
// Time duration between 2 invocations should be greater than the threshold
81-
// This threshold does not applies to Enter | SpecialCharacters | IntelliSenseAcceptance type auto trigger.
82-
const duration = Math.floor((performance.now() - RecommendationHandler.instance.lastInvocationTime) / 1000)
123+
if ([DocumentChangedSource.RegularKey].includes(changedSource)) {
124+
this.startIdleTimeTriggerTimer(event, editor, client, config)
125+
}
83126
switch (changedSource) {
84127
case DocumentChangedSource.EnterKey: {
85-
this.keyStrokeCount += 1
86128
triggerType = 'Enter'
87129
break
88130
}
89131
case DocumentChangedSource.SpecialCharsKey: {
90-
this.keyStrokeCount += 1
91132
triggerType = 'SpecialCharacters'
92133
break
93134
}
94135
case DocumentChangedSource.IntelliSense: {
95-
this.keyStrokeCount += 1
96136
triggerType = 'IntelliSenseAcceptance'
97137
break
98138
}
99139
case DocumentChangedSource.RegularKey: {
100-
// text length can be greater than 1 in Cloud9
101-
this.keyStrokeCount += event.contentChanges[0].text.length
102-
if (
103-
this.keyStrokeCount >= 15 &&
104-
duration >= CodeWhispererConstants.invocationTimeIntervalThreshold
105-
) {
106-
triggerType = 'KeyStrokeCount'
107-
this.keyStrokeCount = 0
108-
}
109140
break
110141
}
111142
default: {
@@ -129,7 +160,6 @@ export class KeyStrokeHandler {
129160
config: ConfigurationEntry
130161
): Promise<void> {
131162
if (editor) {
132-
this.keyStrokeCount = 0
133163
if (isCloud9()) {
134164
if (RecommendationHandler.instance.isGenerateRecommendationInProgress) {
135165
return

src/test/codewhisperer/commands/invokeRecommendation.test.ts

Lines changed: 0 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@ import { ConfigurationEntry } from '../../../codewhisperer/models/model'
1111
import { invokeRecommendation } from '../../../codewhisperer/commands/invokeRecommendation'
1212
import { InlineCompletion } from '../../../codewhisperer/service/inlineCompletion'
1313
import { InlineCompletionService } from '../../../codewhisperer/service/inlineCompletionService'
14-
import { KeyStrokeHandler } from '../../../codewhisperer/service/keyStrokeHandler'
1514

1615
describe('invokeRecommendation', function () {
1716
describe('invokeRecommendation', function () {
@@ -40,18 +39,5 @@ describe('invokeRecommendation', function () {
4039
await invokeRecommendation(mockEditor, mockClient, config)
4140
assert.ok(getRecommendationStub.called || oldGetRecommendationStub.called)
4241
})
43-
44-
it('When called, keyStrokeCount should be set to 0', async function () {
45-
const mockEditor = createMockTextEditor()
46-
KeyStrokeHandler.instance.keyStrokeCount = 10
47-
const config: ConfigurationEntry = {
48-
isShowMethodsEnabled: true,
49-
isManualTriggerEnabled: true,
50-
isAutomatedTriggerEnabled: true,
51-
isIncludeSuggestionsWithCodeReferencesEnabled: true,
52-
}
53-
await invokeRecommendation(mockEditor, mockClient, config)
54-
assert.strictEqual(KeyStrokeHandler.instance.keyStrokeCount, 0)
55-
})
5642
})
5743
})

src/test/codewhisperer/service/keyStrokeHandler.test.ts

Lines changed: 18 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -33,9 +33,11 @@ describe('keyStrokeHandler', function () {
3333
})
3434
describe('processKeyStroke', async function () {
3535
let invokeSpy: sinon.SinonStub
36+
let startTimerSpy: sinon.SinonStub
3637
let mockClient: codewhispererSdkClient.DefaultCodeWhispererClient
3738
beforeEach(function () {
3839
invokeSpy = sinon.stub(KeyStrokeHandler.instance, 'invokeAutomatedTrigger')
40+
startTimerSpy = sinon.stub(KeyStrokeHandler.instance, 'startIdleTimeTriggerTimer')
3941
sinon.spy(RecommendationHandler.instance, 'getRecommendations')
4042
mockClient = new codewhispererSdkClient.DefaultCodeWhispererClient()
4143
resetCodeWhispererGlobalVariables()
@@ -62,6 +64,7 @@ describe('keyStrokeHandler', function () {
6264
const keyStrokeHandler = new KeyStrokeHandler()
6365
await keyStrokeHandler.processKeyStroke(mockEvent, mockEditor, mockClient, cfg)
6466
assert.ok(!invokeSpy.called)
67+
assert.ok(!startTimerSpy.called)
6568
})
6669

6770
it('Should not call invokeAutomatedTrigger when changed text matches active recommendation prefix', async function () {
@@ -87,6 +90,7 @@ describe('keyStrokeHandler', function () {
8790
)
8891
await KeyStrokeHandler.instance.processKeyStroke(mockEvent, mockEditor, mockClient, config)
8992
assert.ok(!invokeSpy.called)
93+
assert.ok(!startTimerSpy.called)
9094
})
9195

9296
it('Should not call invokeAutomatedTrigger when doing delete or undo (empty changed text)', async function () {
@@ -98,23 +102,10 @@ describe('keyStrokeHandler', function () {
98102
)
99103
await KeyStrokeHandler.instance.processKeyStroke(mockEvent, mockEditor, mockClient, config)
100104
assert.ok(!invokeSpy.called)
101-
})
102-
103-
it('Should not call invokeAutomatedTrigger if previous text input is within 2 seconds and it is not a specialcharacter trigger \n', async function () {
104-
KeyStrokeHandler.instance.keyStrokeCount = 14
105-
const mockEditor = createMockTextEditor()
106-
const mockEvent: vscode.TextDocumentChangeEvent = createTextDocumentChangeEvent(
107-
mockEditor.document,
108-
new vscode.Range(new vscode.Position(0, 0), new vscode.Position(0, 1)),
109-
'a'
110-
)
111-
RecommendationHandler.instance.lastInvocationTime = performance.now() - 1500
112-
await KeyStrokeHandler.instance.processKeyStroke(mockEvent, mockEditor, mockClient, config)
113-
assert.ok(!invokeSpy.called)
105+
assert.ok(!startTimerSpy.called)
114106
})
115107

116108
it('Should call invokeAutomatedTrigger if previous text input is within 2 seconds but the new input is new line', async function () {
117-
KeyStrokeHandler.instance.keyStrokeCount = 14
118109
const mockEditor = createMockTextEditor()
119110
const mockEvent: vscode.TextDocumentChangeEvent = createTextDocumentChangeEvent(
120111
mockEditor.document,
@@ -127,7 +118,6 @@ describe('keyStrokeHandler', function () {
127118
})
128119

129120
it('Should call invokeAutomatedTrigger if previous text input is within 2 seconds but the new input is a specialcharacter', async function () {
130-
KeyStrokeHandler.instance.keyStrokeCount = 14
131121
const mockEditor = createMockTextEditor()
132122
const mockEvent: vscode.TextDocumentChangeEvent = createTextDocumentChangeEvent(
133123
mockEditor.document,
@@ -200,31 +190,26 @@ describe('keyStrokeHandler', function () {
200190
assert.ok(!invokeSpy.called)
201191
})
202192

203-
it('Should call invokeAutomatedTrigger with arg KeyStrokeCount when invocationContext.keyStrokeCount reaches threshold', async function () {
193+
it('Should not start idle trigger timer when inputing special characters', async function () {
204194
const mockEditor = createMockTextEditor()
205195
const mockEvent: vscode.TextDocumentChangeEvent = createTextDocumentChangeEvent(
206196
mockEditor.document,
207197
new vscode.Range(new vscode.Position(0, 0), new vscode.Position(0, 1)),
208-
'a'
198+
'('
209199
)
210-
KeyStrokeHandler.instance.keyStrokeCount = 15
211200
await KeyStrokeHandler.instance.processKeyStroke(mockEvent, mockEditor, mockClient, config)
212-
invokeSpy('KeyStrokeCount', mockEditor, mockClient)
213-
assert.ok(invokeSpy.called)
201+
assert.ok(!startTimerSpy.called)
214202
})
215203

216-
it('Should not call invokeAutomatedTrigger when user input is not special character and keyStrokeCount does not reach threshold, should increase invocationContext.keyStrokeCount by 1', async function () {
204+
it('Should start idle trigger timer when inputing non-special characters', async function () {
217205
const mockEditor = createMockTextEditor()
218206
const mockEvent: vscode.TextDocumentChangeEvent = createTextDocumentChangeEvent(
219207
mockEditor.document,
220208
new vscode.Range(new vscode.Position(0, 0), new vscode.Position(0, 1)),
221209
'a'
222210
)
223-
RecommendationHandler.instance.lastInvocationTime = 0
224-
KeyStrokeHandler.instance.keyStrokeCount = 8
225211
await KeyStrokeHandler.instance.processKeyStroke(mockEvent, mockEditor, mockClient, config)
226-
assert.ok(!invokeSpy.called)
227-
assert.strictEqual(KeyStrokeHandler.instance.keyStrokeCount, 9)
212+
assert.ok(startTimerSpy.called)
228213
})
229214
})
230215

@@ -250,15 +235,15 @@ describe('keyStrokeHandler', function () {
250235
await keyStrokeHandler.invokeAutomatedTrigger('Enter', mockEditor, mockClient, config)
251236
assert.ok(getRecommendationsStub.calledOnce || oldGetRecommendationsStub.calledOnce)
252237
})
238+
})
253239

254-
it('should reset invocationContext.keyStrokeCount to 0', async function () {
255-
const mockEditor = createMockTextEditor()
256-
KeyStrokeHandler.instance.keyStrokeCount = 10
257-
sinon
258-
.stub(RecommendationHandler.instance, 'getServerResponse')
259-
.resolves([{ content: 'import math' }, { content: 'def two_sum(nums, target):' }])
260-
await KeyStrokeHandler.instance.invokeAutomatedTrigger('Enter', mockEditor, mockClient, config)
261-
assert.strictEqual(KeyStrokeHandler.instance.keyStrokeCount, 0)
240+
describe('shouldTriggerIdleTime', function () {
241+
it('should return false when inline is enabled and inline completion is in progress ', function () {
242+
const keyStrokeHandler = new KeyStrokeHandler()
243+
sinon.stub(InlineCompletionService.instance, 'isPaginationRunning').returns(true)
244+
sinon.stub(InlineCompletion.instance, 'isPaginationRunning').returns(true)
245+
const result = keyStrokeHandler.shouldTriggerIdleTime()
246+
assert.strictEqual(result, false)
262247
})
263248
})
264249

0 commit comments

Comments
 (0)