Skip to content

Commit c45b8d4

Browse files
authored
[CodeWhisperer] Update %code tracker acceptedToken logic (#3230)
1 parent ffc499b commit c45b8d4

File tree

4 files changed

+155
-75
lines changed

4 files changed

+155
-75
lines changed

jetbrains-core/src/software/aws/toolkits/jetbrains/services/codewhisperer/editor/CodeWhispererEditorListener.kt

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,10 @@ class CodeWhispererEditorListener : EditorFactoryListener {
2323
CodeWhispererInvocationStatus.getInstance().documentChanged()
2424
editor.project?.let { project ->
2525
PsiDocumentManager.getInstance(project).getPsiFile(editor.document)?.codeWhispererLanguage ?. let { language ->
26-
CodeWhispererCodeCoverageTracker.getInstance(language).documentChanged(event)
26+
CodeWhispererCodeCoverageTracker.getInstance(language).apply {
27+
activateTrackerIfNotActive()
28+
documentChanged(event)
29+
}
2730
}
2831
}
2932
}

jetbrains-core/src/software/aws/toolkits/jetbrains/services/codewhisperer/telemetry/CodeWhispererCodeCoverageTracker.kt

Lines changed: 53 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,14 @@ package software.aws.toolkits.jetbrains.services.codewhisperer.telemetry
55

66
import com.intellij.openapi.Disposable
77
import com.intellij.openapi.application.ApplicationManager
8+
import com.intellij.openapi.application.runReadAction
89
import com.intellij.openapi.editor.RangeMarker
910
import com.intellij.openapi.editor.event.DocumentEvent
11+
import com.intellij.openapi.util.Key
12+
import com.intellij.refactoring.suggested.range
1013
import com.intellij.util.Alarm
1114
import com.intellij.util.AlarmFactory
15+
import info.debatty.java.stringsimilarity.Levenshtein
1216
import org.jetbrains.annotations.TestOnly
1317
import software.aws.toolkits.core.utils.debug
1418
import software.aws.toolkits.core.utils.getLogger
@@ -41,33 +45,60 @@ abstract class CodeWhispererCodeCoverageTracker(
4145
get() = totalTokens.get()
4246
val acceptedRecommendationsCount: Int
4347
get() = rangeMarkers.size
48+
private val isActive: AtomicBoolean = AtomicBoolean(false)
4449
private val alarm = AlarmFactory.getInstance().create(Alarm.ThreadToUse.POOLED_THREAD, this)
4550
private val isShuttingDown = AtomicBoolean(false)
4651
private var startTime: Instant = Instant.now()
4752

48-
init {
53+
@Synchronized
54+
fun activateTrackerIfNotActive() {
55+
if (!isTelemetryEnabled() || isActive.get()) return
4956
val conn = ApplicationManager.getApplication().messageBus.connect()
5057
conn.subscribe(
5158
CodeWhispererPopupManager.CODEWHISPERER_USER_ACTION_PERFORMED,
5259
object : CodeWhispererUserActionListener {
5360
override fun afterAccept(states: InvocationContext, sessionContext: SessionContext, rangeMarker: RangeMarker) {
5461
if (states.requestContext.fileContextInfo.programmingLanguage.toCodeWhispererLanguage() != language) return
5562
rangeMarkers.add(rangeMarker)
63+
val originalRecommendation = extractRangeMarkerString(rangeMarker)
64+
originalRecommendation?.let {
65+
rangeMarker.putUserData(KEY_REMAINING_RECOMMENDATION, it)
66+
}
5667
}
5768
}
5869
)
70+
startTime = Instant.now()
71+
isActive.set(true)
5972
scheduleCodeWhispererCodeCoverageTracker()
6073
}
6174

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
6478
if (event.isWholeTextReplaced) {
6579
LOG.debug { "event with isWholeTextReplaced flag: $event" }
6680
if (event.oldTimeStamp == 0L) return
6781
}
6882
addAndGetTotalTokens(event.newLength - event.oldLength)
6983
}
7084

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+
71102
private fun flush() {
72103
try {
73104
if (isTelemetryEnabled()) emitCodeWhispererCodeContribution()
@@ -103,9 +134,21 @@ abstract class CodeWhispererCodeCoverageTracker(
103134
}
104135

105136
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)
109152
}
110153

111154
// 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(
137180
}
138181

139182
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)
140187
private val LOG = getLogger<CodeWhispererCodeCoverageTracker>()
141188
private val instances: MutableMap<CodewhispererLanguage, CodeWhispererCodeCoverageTracker> = mutableMapOf()
142189

0 commit comments

Comments
 (0)