|
5 | 5 |
|
6 | 6 | import * as vscode from 'vscode'
|
7 | 7 | import { DefaultCodeWhispererClient } from '../client/codewhisperer'
|
8 |
| -import * as EditorContext from '../util/editorContext' |
9 | 8 | import * as CodeWhispererConstants from '../models/constants'
|
10 | 9 | import { vsCodeState, ConfigurationEntry } from '../models/model'
|
11 | 10 | import { getLogger } from '../../shared/logger'
|
12 | 11 | import { InlineCompletion } from './inlineCompletion'
|
13 | 12 | import { isCloud9 } from '../../shared/extensionUtilities'
|
14 | 13 | import { RecommendationHandler } from './recommendationHandler'
|
| 14 | +import { CodewhispererAutomatedTriggerType } from '../../shared/telemetry/telemetry' |
| 15 | +import { getTabSizeSetting } from '../../shared/utilities/editorUtilities' |
15 | 16 | import { isInlineCompletionEnabled } from '../util/commonUtil'
|
16 | 17 | import { InlineCompletionService } from './inlineCompletionService'
|
17 |
| -import { CodewhispererAutomatedTriggerType } from '../../shared/telemetry/telemetry' |
18 | 18 |
|
19 | 19 | const performance = globalThis.performance ?? require('perf_hooks').performance
|
20 | 20 |
|
@@ -49,118 +49,65 @@ export class KeyStrokeHandler {
|
49 | 49 | config: ConfigurationEntry
|
50 | 50 | ): Promise<void> {
|
51 | 51 | 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 |
67 | 59 |
|
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 |
76 | 73 | }
|
77 | 74 | }
|
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: { |
85 | 99 | break
|
86 |
| - } else { |
87 |
| - space++ |
88 | 100 | }
|
89 | 101 | }
|
90 |
| - if (isTab && space > 1 && space <= EditorContext.getTabSize()) { |
91 |
| - return 'SpecialCharacters' |
| 102 | + if (triggerType) { |
| 103 | + this.invokeAutomatedTrigger(triggerType, editor, client, config) |
92 | 104 | }
|
| 105 | + } catch (error) { |
| 106 | + getLogger().error('Automated Trigger Exception : ', error) |
| 107 | + getLogger().verbose(`Automated Trigger Exception : ${error}`) |
93 | 108 | }
|
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 '' |
113 | 109 | }
|
114 | 110 |
|
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 |
| - } |
164 | 111 | async invokeAutomatedTrigger(
|
165 | 112 | autoTriggerType: CodewhispererAutomatedTriggerType,
|
166 | 113 | editor: vscode.TextEditor,
|
@@ -215,3 +162,99 @@ export class KeyStrokeHandler {
|
215 | 162 | }
|
216 | 163 | }
|
217 | 164 | }
|
| 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 | +} |
0 commit comments