Skip to content

Commit 6ab0e54

Browse files
authored
fix(codewhisperer) update autotrigger filtering logic and fix enter key autotrigger blocked (#2899)
## Changes * Refactor keyStrokeHandler * fix bug: enter key auto-trigger was blocked by PR * fix(CodeWhisperer): interference with react plugin #2886 ## Observation and Definition * DeleteKey: empty string. e.g. '' * EnterKey: 1 new line char \n possibly followed by space(s) char. e.g. '\n', '\n ' * TabKey: the length of string should have space only and be a multiple of tabSize defined in the IDE or 4 by default e.g. ' ' * SpecialChar: char(s) like (, [, { etc. * isSingleLine: if the number of new line char in the string == 0, the only exception is EnterKey which has exact 1 new line char followed by spaces * isMultiLine: if the number of new line char in the string > 0, we will block cwspr service invocation
1 parent f82944e commit 6ab0e54

File tree

2 files changed

+198
-107
lines changed

2 files changed

+198
-107
lines changed

src/codewhisperer/service/keyStrokeHandler.ts

Lines changed: 147 additions & 104 deletions
Original file line numberDiff line numberDiff line change
@@ -5,16 +5,16 @@
55

66
import * as vscode from 'vscode'
77
import { DefaultCodeWhispererClient } from '../client/codewhisperer'
8-
import * as EditorContext from '../util/editorContext'
98
import * as CodeWhispererConstants from '../models/constants'
109
import { vsCodeState, ConfigurationEntry } from '../models/model'
1110
import { getLogger } from '../../shared/logger'
1211
import { InlineCompletion } from './inlineCompletion'
1312
import { isCloud9 } from '../../shared/extensionUtilities'
1413
import { RecommendationHandler } from './recommendationHandler'
14+
import { CodewhispererAutomatedTriggerType } from '../../shared/telemetry/telemetry'
15+
import { getTabSizeSetting } from '../../shared/utilities/editorUtilities'
1516
import { isInlineCompletionEnabled } from '../util/commonUtil'
1617
import { InlineCompletionService } from './inlineCompletionService'
17-
import { CodewhispererAutomatedTriggerType } from '../../shared/telemetry/telemetry'
1818

1919
const performance = globalThis.performance ?? require('perf_hooks').performance
2020

@@ -49,118 +49,65 @@ export class KeyStrokeHandler {
4949
config: ConfigurationEntry
5050
): Promise<void> {
5151
try {
52-
const changedText = this.getChangedText(event, config.isAutomatedTriggerEnabled, editor)
53-
if (changedText === '') {
54-
return
55-
}
56-
const autoTriggerType = this.getAutoTriggerReason(changedText)
57-
if (autoTriggerType === '') {
58-
return
59-
}
60-
const triggerTtype = autoTriggerType as CodewhispererAutomatedTriggerType
61-
this.invokeAutomatedTrigger(triggerTtype, editor, client, config)
62-
} catch (error) {
63-
getLogger().error('Automated Trigger Exception : ', error)
64-
getLogger().verbose(`Automated Trigger Exception : ${error}`)
65-
}
66-
}
52+
if (!config.isAutomatedTriggerEnabled) return
53+
54+
// Skip when output channel gains focus and invoke
55+
if (editor.document.languageId === 'Log') return
56+
57+
// Pause automated trigger when typed input matches recommendation prefix for inline suggestion
58+
if (InlineCompletion.instance.isTypeaheadInProgress) return
6759

68-
getAutoTriggerReason(changedText: string): string {
69-
for (const val of CodeWhispererConstants.specialCharactersList) {
70-
if (changedText.includes(val)) {
71-
this.specialChar = val
72-
if (val === CodeWhispererConstants.lineBreak) {
73-
return 'Enter'
74-
} else {
75-
return 'SpecialCharacters'
60+
// Time duration between 2 invocations should be greater than the threshold
61+
// This threshold does not applies to Enter | SpecialCharacters type auto trigger.
62+
const duration = Math.floor((performance.now() - RecommendationHandler.instance.lastInvocationTime) / 1000)
63+
if (duration < CodeWhispererConstants.invocationTimeIntervalThreshold) return
64+
65+
// Skip Cloud9 IntelliSense acceptance event
66+
if (
67+
isCloud9() &&
68+
event.contentChanges.length > 0 &&
69+
RecommendationHandler.instance.recommendations.length > 0
70+
) {
71+
if (event.contentChanges[0].text === RecommendationHandler.instance.recommendations[0].content) {
72+
return
7673
}
7774
}
78-
}
79-
if (changedText.includes(CodeWhispererConstants.space)) {
80-
let isTab = true
81-
let space = 0
82-
for (let i = 0; i < changedText.length; i++) {
83-
if (changedText[i] !== ' ') {
84-
isTab = false
75+
76+
let triggerType: CodewhispererAutomatedTriggerType | undefined
77+
const changedSource = new DefaultDocumentChangedType(event.contentChanges).checkChangeSource()
78+
switch (changedSource) {
79+
case DocumentChangedSource.EnterKey: {
80+
this.keyStrokeCount += 1
81+
triggerType = 'Enter'
82+
break
83+
}
84+
case DocumentChangedSource.SpecialCharsKey: {
85+
this.keyStrokeCount += 1
86+
triggerType = 'SpecialCharacters'
87+
break
88+
}
89+
case DocumentChangedSource.IntelliSense: {
90+
this.keyStrokeCount += 1
91+
triggerType = 'IntelliSenseAcceptance'
92+
break
93+
}
94+
case DocumentChangedSource.RegularKey: {
95+
this.keyStrokeCount += 1
96+
break
97+
}
98+
default: {
8599
break
86-
} else {
87-
space++
88100
}
89101
}
90-
if (isTab && space > 1 && space <= EditorContext.getTabSize()) {
91-
return 'SpecialCharacters'
102+
if (triggerType) {
103+
this.invokeAutomatedTrigger(triggerType, editor, client, config)
92104
}
105+
} catch (error) {
106+
getLogger().error('Automated Trigger Exception : ', error)
107+
getLogger().verbose(`Automated Trigger Exception : ${error}`)
93108
}
94-
/**
95-
* Time duration between 2 invocations should be greater than the threshold
96-
* This threshold does not applies to Enter | SpecialCharacters type auto trigger.
97-
*/
98-
const duration = Math.floor((performance.now() - RecommendationHandler.instance.lastInvocationTime) / 1000)
99-
if (duration < CodeWhispererConstants.invocationTimeIntervalThreshold) {
100-
return ''
101-
}
102-
if (this.keyStrokeCount >= CodeWhispererConstants.invocationKeyThreshold) {
103-
return 'KeyStrokeCount'
104-
} else {
105-
this.keyStrokeCount += 1
106-
}
107-
// Below condition is very likely a multi character insert when user accept native intelliSense suggestion
108-
// VS Code does not provider API for intelliSense suggestion acceptance
109-
if (changedText.length > 1 && !changedText.includes(' ') && changedText.length < 40 && !isCloud9()) {
110-
return 'IntelliSenseAcceptance'
111-
}
112-
return ''
113109
}
114110

115-
getChangedText(
116-
event: vscode.TextDocumentChangeEvent,
117-
isAutomatedTriggerEnabled: boolean,
118-
editor: vscode.TextEditor
119-
): string {
120-
if (!isAutomatedTriggerEnabled) {
121-
return ''
122-
}
123-
/**
124-
* Skip when output channel gains focus and invoke
125-
*/
126-
if (editor.document.languageId === 'Log') {
127-
return ''
128-
}
129-
130-
/**
131-
* Skip Cloud9 IntelliSense acceptance event
132-
*/
133-
if (
134-
isCloud9() &&
135-
event.contentChanges.length > 0 &&
136-
RecommendationHandler.instance.recommendations.length > 0
137-
) {
138-
if (event.contentChanges[0].text === RecommendationHandler.instance.recommendations[0].content) {
139-
return ''
140-
}
141-
}
142-
/**
143-
* Pause automated trigger when typed input matches recommendation prefix
144-
* for inline suggestion
145-
*/
146-
if (InlineCompletion.instance.isTypeaheadInProgress) {
147-
return ''
148-
}
149-
150-
/**
151-
* DO NOT auto trigger CodeWhisperer when appending muli-line snippets to document
152-
* DO NOT auto trigger CodeWhisperer when deleting or undo
153-
*/
154-
const changedText = event.contentChanges[0].text
155-
const changedRange = event.contentChanges[0].range
156-
if (!changedRange.isSingleLine || changedText === '') {
157-
return ''
158-
}
159-
if (changedText.split('\n').length > 1) {
160-
return ''
161-
}
162-
return changedText
163-
}
164111
async invokeAutomatedTrigger(
165112
autoTriggerType: CodewhispererAutomatedTriggerType,
166113
editor: vscode.TextEditor,
@@ -215,3 +162,99 @@ export class KeyStrokeHandler {
215162
}
216163
}
217164
}
165+
166+
export abstract class DocumentChangedType {
167+
constructor(protected readonly contentChanges: ReadonlyArray<vscode.TextDocumentContentChangeEvent>) {
168+
this.contentChanges = contentChanges
169+
}
170+
171+
abstract checkChangeSource(): DocumentChangedSource
172+
173+
// Enter key should always start with ONE '\n' and potentially following spaces due to IDE reformat
174+
protected isEnterKey(str: string): boolean {
175+
if (str.length === 0) return false
176+
return str[0] === '\n' && str.substring(1).trim().length === 0
177+
}
178+
179+
// Tab should consist of space char only ' ' and the length % tabSize should be 0
180+
protected isTabKey(str: string): boolean {
181+
const tabSize = getTabSizeSetting()
182+
if (str.length % tabSize === 0 && str.trim() === '') {
183+
return true
184+
}
185+
return false
186+
}
187+
188+
protected isUserTypingSpecialChar(str: string): boolean {
189+
return ['(', '()', '[', '[]', '{', '{}', ':'].includes(str)
190+
}
191+
192+
protected isSingleLine(str: string): boolean {
193+
let newLineCounts = 0
194+
for (const ch of str) {
195+
if (ch === '\n') newLineCounts += 1
196+
}
197+
198+
// since pressing Enter key possibly will generate string like '\n ' due to indention
199+
if (this.isEnterKey(str)) return true
200+
if (newLineCounts >= 1) return false
201+
return true
202+
}
203+
}
204+
205+
export class DefaultDocumentChangedType extends DocumentChangedType {
206+
constructor(contentChanges: ReadonlyArray<vscode.TextDocumentContentChangeEvent>) {
207+
super(contentChanges)
208+
}
209+
210+
checkChangeSource(): DocumentChangedSource {
211+
if (this.contentChanges.length === 0) {
212+
return DocumentChangedSource.Unknown
213+
}
214+
215+
// Case when event.contentChanges.length > 1
216+
if (this.contentChanges.length > 1) {
217+
return DocumentChangedSource.Reformatting
218+
}
219+
220+
// Case when event.contentChanges.length === 1
221+
const changedText = this.contentChanges[0].text
222+
223+
if (this.isSingleLine(changedText)) {
224+
if (changedText === '') {
225+
return DocumentChangedSource.Deletion
226+
} else if (this.isEnterKey(changedText)) {
227+
return DocumentChangedSource.EnterKey
228+
} else if (this.isTabKey(changedText)) {
229+
return DocumentChangedSource.TabKey
230+
} else if (this.isUserTypingSpecialChar(changedText)) {
231+
return DocumentChangedSource.SpecialCharsKey
232+
} else if (changedText.length === 1) {
233+
return DocumentChangedSource.RegularKey
234+
} else if (new RegExp('^[ ]+$').test(changedText)) {
235+
// single line && single place reformat should consist of space chars only
236+
return DocumentChangedSource.Reformatting
237+
} else if (new RegExp('^[\\S]+$').test(changedText)) {
238+
// match single word only, which is general case for intellisense suggestion, it's still possible intllisense suggest
239+
// multi-words code snippets
240+
return DocumentChangedSource.IntelliSense
241+
} else {
242+
return DocumentChangedSource.Unknown
243+
}
244+
}
245+
246+
// Won't trigger cwspr on multi-line changes
247+
return DocumentChangedSource.Unknown
248+
}
249+
}
250+
251+
export enum DocumentChangedSource {
252+
SpecialCharsKey = 'SpecialCharsKey',
253+
RegularKey = 'RegularKey',
254+
TabKey = 'TabKey',
255+
EnterKey = 'EnterKey',
256+
IntelliSense = 'IntelliSense',
257+
Reformatting = 'Reformatting',
258+
Deletion = 'Deletion',
259+
Unknown = 'Unknown',
260+
}

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

Lines changed: 51 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,14 @@ import * as vscode from 'vscode'
88
import * as sinon from 'sinon'
99
import * as codewhispererSdkClient from '../../../codewhisperer/client/codewhisperer'
1010
import { vsCodeState, ConfigurationEntry } from '../../../codewhisperer/models/model'
11-
import { KeyStrokeHandler } from '../../../codewhisperer/service/keyStrokeHandler'
11+
import {
12+
DocumentChangedSource,
13+
KeyStrokeHandler,
14+
DefaultDocumentChangedType,
15+
} from '../../../codewhisperer/service/keyStrokeHandler'
1216
import { InlineCompletion } from '../../../codewhisperer/service/inlineCompletion'
13-
import { InlineCompletionService } from '../../../codewhisperer/service/inlineCompletionService'
1417
import { createMockTextEditor, createTextDocumentChangeEvent, resetCodeWhispererGlobalVariables } from '../testUtil'
18+
import { InlineCompletionService } from '../../../codewhisperer/service/inlineCompletionService'
1519
import * as EditorContext from '../../../codewhisperer/util/editorContext'
1620
import { RecommendationHandler } from '../../../codewhisperer/service/recommendationHandler'
1721

@@ -79,7 +83,7 @@ describe('keyStrokeHandler', function () {
7983
const mockEvent: vscode.TextDocumentChangeEvent = createTextDocumentChangeEvent(
8084
mockEditor.document,
8185
new vscode.Range(new vscode.Position(0, 0), new vscode.Position(1, 0)),
82-
'print(n'
86+
'\nprint(n'
8387
)
8488
await KeyStrokeHandler.instance.processKeyStroke(mockEvent, mockEditor, mockClient, config)
8589
assert.ok(!invokeSpy.called)
@@ -219,4 +223,48 @@ describe('keyStrokeHandler', function () {
219223
assert.strictEqual(KeyStrokeHandler.instance.keyStrokeCount, 0)
220224
})
221225
})
226+
227+
describe('test checkChangeSource', function () {
228+
const tabStr = ' '.repeat(EditorContext.getTabSize())
229+
230+
const cases: [string, DocumentChangedSource][] = [
231+
['\n ', DocumentChangedSource.EnterKey],
232+
['\n', DocumentChangedSource.EnterKey],
233+
['(', DocumentChangedSource.SpecialCharsKey],
234+
['()', DocumentChangedSource.SpecialCharsKey],
235+
['{}', DocumentChangedSource.SpecialCharsKey],
236+
['(a, b):', DocumentChangedSource.Unknown],
237+
[':', DocumentChangedSource.SpecialCharsKey],
238+
['a', DocumentChangedSource.RegularKey],
239+
[tabStr, DocumentChangedSource.TabKey],
240+
['__str__', DocumentChangedSource.IntelliSense],
241+
['toString()', DocumentChangedSource.IntelliSense],
242+
['</p>', DocumentChangedSource.IntelliSense],
243+
[' ', DocumentChangedSource.Reformatting],
244+
['def add(a,b):\n return a + b\n', DocumentChangedSource.Unknown],
245+
['function suggestedByIntelliSense():', DocumentChangedSource.Unknown],
246+
]
247+
248+
cases.forEach(tuple => {
249+
const input = tuple[0]
250+
const expected = tuple[1]
251+
it(`test input ${input} should return ${expected}`, function () {
252+
const actual = new DefaultDocumentChangedType(
253+
createFakeDocumentChangeEvent(tuple[0])
254+
).checkChangeSource()
255+
assert.strictEqual(actual, expected)
256+
})
257+
})
258+
259+
function createFakeDocumentChangeEvent(str: string): ReadonlyArray<vscode.TextDocumentContentChangeEvent> {
260+
return [
261+
{
262+
range: new vscode.Range(new vscode.Position(0, 0), new vscode.Position(0, 5)),
263+
rangeOffset: 0,
264+
rangeLength: 0,
265+
text: str,
266+
},
267+
]
268+
}
269+
})
222270
})

0 commit comments

Comments
 (0)