@@ -5,10 +5,14 @@ package software.aws.toolkits.jetbrains.services.codewhisperer.telemetry
5
5
6
6
import com.intellij.openapi.Disposable
7
7
import com.intellij.openapi.application.ApplicationManager
8
+ import com.intellij.openapi.application.runReadAction
8
9
import com.intellij.openapi.editor.RangeMarker
9
10
import com.intellij.openapi.editor.event.DocumentEvent
11
+ import com.intellij.openapi.util.Key
12
+ import com.intellij.refactoring.suggested.range
10
13
import com.intellij.util.Alarm
11
14
import com.intellij.util.AlarmFactory
15
+ import info.debatty.java.stringsimilarity.Levenshtein
12
16
import org.jetbrains.annotations.TestOnly
13
17
import software.aws.toolkits.core.utils.debug
14
18
import software.aws.toolkits.core.utils.getLogger
@@ -41,33 +45,60 @@ abstract class CodeWhispererCodeCoverageTracker(
41
45
get() = totalTokens.get()
42
46
val acceptedRecommendationsCount: Int
43
47
get() = rangeMarkers.size
48
+ private val isActive: AtomicBoolean = AtomicBoolean (false )
44
49
private val alarm = AlarmFactory .getInstance().create(Alarm .ThreadToUse .POOLED_THREAD , this )
45
50
private val isShuttingDown = AtomicBoolean (false )
46
51
private var startTime: Instant = Instant .now()
47
52
48
- init {
53
+ @Synchronized
54
+ fun activateTrackerIfNotActive () {
55
+ if (! isTelemetryEnabled() || isActive.get()) return
49
56
val conn = ApplicationManager .getApplication().messageBus.connect()
50
57
conn.subscribe(
51
58
CodeWhispererPopupManager .CODEWHISPERER_USER_ACTION_PERFORMED ,
52
59
object : CodeWhispererUserActionListener {
53
60
override fun afterAccept (states : InvocationContext , sessionContext : SessionContext , rangeMarker : RangeMarker ) {
54
61
if (states.requestContext.fileContextInfo.programmingLanguage.toCodeWhispererLanguage() != language) return
55
62
rangeMarkers.add(rangeMarker)
63
+ val originalRecommendation = extractRangeMarkerString(rangeMarker)
64
+ originalRecommendation?.let {
65
+ rangeMarker.putUserData(KEY_REMAINING_RECOMMENDATION , it)
66
+ }
56
67
}
57
68
}
58
69
)
70
+ startTime = Instant .now()
71
+ isActive.set(true )
59
72
scheduleCodeWhispererCodeCoverageTracker()
60
73
}
61
74
62
- fun documentChanged (event : DocumentEvent ) {
63
- // Added this condition to filter out IDE reloading files
75
+ internal fun documentChanged (event : DocumentEvent ) {
76
+ // When open a file for the first time, IDE will also emit DocumentEvent for loading with `isWholeTextReplaced = true`
77
+ // Added this condition to filter out those events
64
78
if (event.isWholeTextReplaced) {
65
79
LOG .debug { " event with isWholeTextReplaced flag: $event " }
66
80
if (event.oldTimeStamp == 0L ) return
67
81
}
68
82
addAndGetTotalTokens(event.newLength - event.oldLength)
69
83
}
70
84
85
+ internal fun extractRangeMarkerString (rangeMarker : RangeMarker ): String? = runReadAction {
86
+ rangeMarker.range?.let { myRange -> rangeMarker.document.getText(myRange) }
87
+ }
88
+
89
+ // With edit distance, complicate usermodification can be considered as simple edit(add, delete, replace),
90
+ // and thus the unmodified part of recommendation length can be deducted/approximated
91
+ // ex. (modified > original): originalRecom: foo -> modifiedRecom: fobarbarbaro, distance = 9, delta = 12 - 9 = 3
92
+ // ex. (modified == original): originalRecom: helloworld -> modifiedRecom: HelloWorld, distance = 2, delta = 10 - 2 = 8
93
+ // ex. (modified < original): originalRecom: CodeWhisperer -> modifiedRecom: CODE, distance = 12, delta = 13 - 12 = 1
94
+ internal fun getAcceptedTokensDelta (originalRecommendation : String , modifiedRecommendation : String ): Int {
95
+ val editDistance = getEditDistance(modifiedRecommendation, originalRecommendation).toInt()
96
+ return maxOf(originalRecommendation.length, modifiedRecommendation.length) - editDistance
97
+ }
98
+
99
+ protected open fun getEditDistance (modifiedString : String , originalString : String ): Double =
100
+ levenshteinChecker.distance(modifiedString, originalString)
101
+
71
102
private fun flush () {
72
103
try {
73
104
if (isTelemetryEnabled()) emitCodeWhispererCodeContribution()
@@ -103,9 +134,21 @@ abstract class CodeWhispererCodeCoverageTracker(
103
134
}
104
135
105
136
private fun emitCodeWhispererCodeContribution () {
106
- rangeMarkers.forEach {
107
- if (! it.isValid) return @forEach
108
- addAndGetAcceptedTokens(it.endOffset - it.startOffset)
137
+ rangeMarkers.forEach { rangeMarker ->
138
+ if (! rangeMarker.isValid) return @forEach
139
+ // if users add more code upon the recommendation generated from CodeWhisperer, we consider those added part as userToken but not CwsprTokens
140
+ val originalRecommendation = rangeMarker.getUserData(KEY_REMAINING_RECOMMENDATION )
141
+ val modifiedRecommendation = extractRangeMarkerString(rangeMarker)
142
+ if (originalRecommendation == null || modifiedRecommendation == null ) {
143
+ LOG .debug {
144
+ " failed to get accepted recommendation. " +
145
+ " OriginalRecommendation is null: ${originalRecommendation == null } ; " +
146
+ " ModifiedRecommendation is null: ${modifiedRecommendation == null } "
147
+ }
148
+ return @forEach
149
+ }
150
+ val delta = getAcceptedTokensDelta(originalRecommendation, modifiedRecommendation)
151
+ addAndGetAcceptedTokens(delta)
109
152
}
110
153
111
154
// percentage == null means totalTokens == 0 and users are not editing the document, thus we shouldn't emit telemetry for this
@@ -137,6 +180,10 @@ abstract class CodeWhispererCodeCoverageTracker(
137
180
}
138
181
139
182
companion object {
183
+ @JvmStatic
184
+ protected val levenshteinChecker = Levenshtein ()
185
+ private const val REMAINING_RECOMMENDATION = " remainingRecommendation"
186
+ private val KEY_REMAINING_RECOMMENDATION = Key <String >(REMAINING_RECOMMENDATION )
140
187
private val LOG = getLogger<CodeWhispererCodeCoverageTracker >()
141
188
private val instances: MutableMap <CodewhispererLanguage , CodeWhispererCodeCoverageTracker > = mutableMapOf ()
142
189
0 commit comments