@@ -8,88 +8,122 @@ import * as telemetry from '../../shared/telemetry/telemetry'
8
8
import { getLogger } from '../../shared/logger/logger'
9
9
import { CodeWhispererConstants } from '../models/constants'
10
10
import globals from '../../shared/extensionGlobals'
11
+ import { vsCodeState } from '../models/model'
12
+ import { distance } from 'fastest-levenshtein'
13
+
14
+ interface CodeWhispererToken {
15
+ range : vscode . Range
16
+ text : string
17
+ accepted : number
18
+ }
19
+
11
20
/**
12
- * This singleton class is mainly used for calculating the percentage of user modification.
13
- * The current calculation method is (Levenshtein edit distance / acceptedSuggestion.length).
21
+ * This singleton class is mainly used for calculating the code written by codeWhisperer
14
22
*/
15
23
export class CodeWhispererCodeCoverageTracker {
16
- private _acceptedTokens : string [ ]
17
- private _totalTokens : string [ ]
24
+ private _acceptedTokens : { [ key : string ] : CodeWhispererToken [ ] }
25
+ private _totalTokens : { [ key : string ] : number }
18
26
private _timer ?: NodeJS . Timer
19
27
private _startTime : number
20
28
private _language : telemetry . CodewhispererLanguage
21
29
22
30
private constructor ( language : telemetry . CodewhispererLanguage , private readonly _globals : vscode . Memento ) {
23
- this . _acceptedTokens = [ ]
24
- this . _totalTokens = [ ]
31
+ this . _acceptedTokens = { }
32
+ this . _totalTokens = { }
25
33
this . _startTime = 0
26
34
this . _language = language
27
35
}
28
36
29
- public setAcceptedTokens ( recommendation : string ) {
30
- const terms = this . _globals . get < boolean > ( CodeWhispererConstants . termsAcceptedKey ) || false
31
- if ( ! terms ) return
32
-
33
- // generate accepted recoomendation token and stored in collection
34
- this . _acceptedTokens . push ( ...recommendation )
35
- this . _totalTokens . push ( ...recommendation )
37
+ public get acceptedTokens ( ) : { [ key : string ] : CodeWhispererToken [ ] } {
38
+ return this . _acceptedTokens
36
39
}
37
-
38
- public get AcceptedTokensLength ( ) : number {
39
- return this . _acceptedTokens . length
40
+ public get totalTokens ( ) : { [ key : string ] : number } {
41
+ return this . _totalTokens
40
42
}
41
43
42
- public setTotalTokens ( content : string ) {
43
- if ( this . _totalTokens . length === 0 && this . _timer == undefined ) {
44
- const currentDate = new globals . clock . Date ( )
45
- this . _startTime = currentDate . getTime ( )
46
- this . startTimer ( )
47
- }
48
-
49
- if ( content . length <= 2 ) {
50
- this . _totalTokens . push ( content )
51
- } else if ( content . length > 2 ) {
52
- this . _totalTokens . push ( ...content )
53
- }
44
+ public countAcceptedTokens ( range : vscode . Range , text : string , filename : string ) {
45
+ const terms = this . _globals . get < boolean > ( CodeWhispererConstants . termsAcceptedKey ) || false
46
+ if ( ! terms ) return
47
+ // generate accepted recommendation token and stored in collection
48
+ this . addAcceptedTokens ( filename , { range : range , text : text , accepted : text . length } )
49
+ this . addTotalTokens ( filename , text . length )
54
50
}
55
51
56
52
public flush ( ) {
57
53
const terms = this . _globals . get < boolean > ( CodeWhispererConstants . termsAcceptedKey ) || false
58
54
if ( ! terms ) {
59
- this . _totalTokens = [ ]
60
- this . _acceptedTokens = [ ]
55
+ this . _totalTokens = { }
56
+ this . _acceptedTokens = { }
61
57
this . closeTimer ( )
62
58
return
63
59
}
64
- this . emitCodeWhispererCodeContribution ( )
60
+ try {
61
+ this . emitCodeWhispererCodeContribution ( )
62
+ } catch ( error ) {
63
+ getLogger ( ) . error ( `Encountered ${ error } when emitting code contribution metric` )
64
+ }
65
+ }
66
+
67
+ public updateAcceptedTokensCount ( editor : vscode . TextEditor ) {
68
+ const filename = editor . document . fileName
69
+ if ( filename in this . _acceptedTokens ) {
70
+ for ( let i = 0 ; i < this . _acceptedTokens [ filename ] . length ; i ++ ) {
71
+ const oldText = this . _acceptedTokens [ filename ] [ i ] . text
72
+ const newText = editor . document . getText ( this . _acceptedTokens [ filename ] [ i ] . range )
73
+ this . _acceptedTokens [ filename ] [ i ] . accepted = this . getUnmodifiedAcceptedTokens ( oldText , newText )
74
+ }
75
+ }
76
+ }
77
+ // With edit distance, complicate usermodification can be considered as simple edit(add, delete, replace),
78
+ // and thus the unmodified part of recommendation length can be deducted/approximated
79
+ // ex. (modified > original): originalRecom: foo -> modifiedRecom: fobarbarbaro, distance = 9, delta = 12 - 9 = 3
80
+ // ex. (modified == original): originalRecom: helloworld -> modifiedRecom: HelloWorld, distance = 2, delta = 10 - 2 = 8
81
+ // ex. (modified < original): originalRecom: CodeWhisperer -> modifiedRecom: CODE, distance = 12, delta = 13 - 12 = 1
82
+ public getUnmodifiedAcceptedTokens ( origin : string , after : string ) {
83
+ return Math . max ( origin . length , after . length ) - distance ( origin , after )
65
84
}
66
85
67
86
public emitCodeWhispererCodeContribution ( ) {
68
- const totalTokens = this . _totalTokens
69
- const acceptedTokens = this . _acceptedTokens
70
- const percentCount = ( ( acceptedTokens . length / totalTokens . length ) * 100 ) . toFixed ( 2 )
87
+ let totalTokens = 0
88
+ for ( const filename in this . _totalTokens ) {
89
+ totalTokens += this . _totalTokens [ filename ]
90
+ }
91
+ if ( vscode . window . activeTextEditor ) {
92
+ this . updateAcceptedTokensCount ( vscode . window . activeTextEditor )
93
+ }
94
+ let acceptedTokens = 0
95
+ for ( const filename in this . _acceptedTokens ) {
96
+ this . _acceptedTokens [ filename ] . forEach ( v => {
97
+ if ( filename in this . _totalTokens && this . _totalTokens [ filename ] >= v . accepted ) {
98
+ acceptedTokens += v . accepted
99
+ }
100
+ } )
101
+ }
102
+ const percentCount = ( ( acceptedTokens / totalTokens ) * 100 ) . toFixed ( 2 )
71
103
const percentage = Math . round ( parseInt ( percentCount ) )
72
104
telemetry . recordCodewhispererCodePercentage ( {
73
- codewhispererTotalTokens : totalTokens . length ? totalTokens . length : 0 ,
105
+ codewhispererTotalTokens : totalTokens ,
74
106
codewhispererLanguage : this . _language ,
75
- codewhispererAcceptedTokens : acceptedTokens . length ? acceptedTokens . length : 0 ,
107
+ codewhispererAcceptedTokens : acceptedTokens ,
76
108
codewhispererPercentage : percentage ? percentage : 0 ,
77
109
} )
78
110
}
79
111
80
- public startTimer ( ) {
81
- if ( this . _timer !== undefined ) {
82
- return
83
- }
112
+ private tryStartTimer ( ) {
113
+ if ( this . _timer !== undefined ) return
114
+ const currentDate = new globals . clock . Date ( )
115
+ this . _startTime = currentDate . getTime ( )
84
116
this . _timer = setTimeout ( ( ) => {
85
117
try {
86
118
const currentTime = new globals . clock . Date ( ) . getTime ( )
87
119
const delay : number = CodeWhispererConstants . defaultCheckPeriodMillis
88
120
const diffTime : number = this . _startTime + delay
89
121
if ( diffTime <= currentTime ) {
90
- const totalTokens = this . _totalTokens
91
- const acceptedTokens = this . _acceptedTokens
92
- if ( totalTokens . length > 0 && acceptedTokens . length > 0 ) {
122
+ let totalTokens = 0
123
+ for ( const filename in this . _totalTokens ) {
124
+ totalTokens += this . _totalTokens [ filename ]
125
+ }
126
+ if ( totalTokens > 0 ) {
93
127
this . flush ( )
94
128
} else {
95
129
getLogger ( ) . debug (
@@ -100,28 +134,67 @@ export class CodeWhispererCodeCoverageTracker {
100
134
} catch ( e ) {
101
135
getLogger ( ) . verbose ( `Exception Thrown from CodeWhispererCodeCoverageTracker: ${ e } ` )
102
136
} finally {
103
- this . _totalTokens = [ ]
104
- this . _acceptedTokens = [ ]
137
+ this . _totalTokens = { }
138
+ this . _acceptedTokens = { }
105
139
this . _startTime = 0
106
140
this . closeTimer ( )
107
141
}
108
142
} , CodeWhispererConstants . defaultCheckPeriodMillis )
109
143
}
110
144
111
- public closeTimer ( ) {
145
+ private closeTimer ( ) {
112
146
if ( this . _timer !== undefined ) {
113
147
clearTimeout ( this . _timer )
114
148
this . _timer = undefined
115
149
}
116
150
}
117
151
118
- public static readonly instances = new Map < telemetry . CodewhispererLanguage , CodeWhispererCodeCoverageTracker > ( )
119
- public static getTracker (
120
- language : telemetry . CodewhispererLanguage = 'plaintext' ,
121
- memento : vscode . Memento
122
- ) : CodeWhispererCodeCoverageTracker {
123
- const instance = this . instances . get ( language ) ?? new this ( language , memento )
124
- this . instances . set ( language , instance )
125
- return instance
152
+ public addAcceptedTokens ( filename : string , token : CodeWhispererToken ) {
153
+ if ( ! ( filename in this . _acceptedTokens ) ) {
154
+ this . _acceptedTokens [ filename ] = [ ]
155
+ }
156
+ this . _acceptedTokens [ filename ] . push ( token )
157
+ }
158
+
159
+ public addTotalTokens ( filename : string , count : number ) {
160
+ if ( ! ( filename in this . _totalTokens ) ) {
161
+ this . _totalTokens [ filename ] = 0
162
+ }
163
+ this . _totalTokens [ filename ] += count
164
+ if ( this . _totalTokens [ filename ] < 0 ) {
165
+ this . _totalTokens [ filename ] = 0
166
+ }
167
+ }
168
+
169
+ public countTotalTokens ( e : vscode . TextDocumentChangeEvent ) {
170
+ // ignore no contentChanges. ignore contentChanges from other plugins (formatters)
171
+ // only include contentChanges from user action
172
+ if (
173
+ ! CodeWhispererConstants . supportedLanguages . includes ( e . document . languageId ) ||
174
+ vsCodeState . isCodeWhispererEditing ||
175
+ e . contentChanges . length !== 1
176
+ )
177
+ return
178
+ const content = e . contentChanges [ 0 ]
179
+ // do not count user tokens if user copies large chunk of code
180
+ if ( content . text . length > 20 ) return
181
+ this . tryStartTimer ( )
182
+ // deletion events has no text.
183
+ if ( content . text . length === 0 ) {
184
+ this . addTotalTokens ( e . document . fileName , - content . rangeLength )
185
+ } else {
186
+ this . addTotalTokens ( e . document . fileName , content . text . length )
187
+ }
188
+ }
189
+
190
+ public static readonly instances = new Map < string , CodeWhispererCodeCoverageTracker > ( )
191
+ public static getTracker ( language : string , memento : vscode . Memento ) : CodeWhispererCodeCoverageTracker | undefined {
192
+ if ( CodeWhispererConstants . supportedLanguages . includes ( language ) ) {
193
+ const instance =
194
+ this . instances . get ( language ) ?? new this ( language as telemetry . CodewhispererLanguage , memento )
195
+ this . instances . set ( language , instance )
196
+ return instance
197
+ }
198
+ return undefined
126
199
}
127
200
}
0 commit comments