Skip to content

Commit 394eba5

Browse files
authored
Merge branch 'main' into bugfix/telemetry
2 parents 1452279 + 4a48c96 commit 394eba5

File tree

8 files changed

+309
-29
lines changed

8 files changed

+309
-29
lines changed
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
{
2+
"type" : "bugfix",
3+
"description" : "Fix IDE auto completion settings potentially overwritten by Q inline suggestion"
4+
}

plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/popup/CodeWhispererPopupManager.kt

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33

44
package software.aws.toolkits.jetbrains.services.codewhisperer.popup
55

6-
import com.intellij.codeInsight.CodeInsightSettings
76
import com.intellij.codeInsight.hint.ParameterInfoController
87
import com.intellij.codeInsight.lookup.LookupManager
98
import com.intellij.idea.AppMode
@@ -264,14 +263,19 @@ class CodeWhispererPopupManager {
264263

265264
fun cancelPopup(popup: JBPopup) {
266265
popup.cancel()
266+
Disposer.dispose(popup)
267267
}
268268

269269
fun closePopup(popup: JBPopup) {
270270
popup.closeOk(null)
271+
Disposer.dispose(popup)
271272
}
272273

273274
fun closePopup() {
274-
myPopup?.closeOk(null)
275+
myPopup?.let {
276+
it.closeOk(null)
277+
Disposer.dispose(it)
278+
}
275279
}
276280

277281
fun showPopup(
@@ -342,11 +346,6 @@ class CodeWhispererPopupManager {
342346
popup.size = popup.preferredContentSize
343347
}
344348
} else {
345-
val originalAutoPopupCompletionLookup = CodeInsightSettings.getInstance().AUTO_POPUP_COMPLETION_LOOKUP
346-
CodeInsightSettings.getInstance().AUTO_POPUP_COMPLETION_LOOKUP = false
347-
Disposer.register(popup) {
348-
CodeInsightSettings.getInstance().AUTO_POPUP_COMPLETION_LOOKUP = originalAutoPopupCompletionLookup
349-
}
350349
if (!AppMode.isRemoteDevHost()) {
351350
popup.show(relativePopupLocationToEditor)
352351
} else {

plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/service/CodeWhispererService.kt

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,9 @@
33

44
package software.aws.toolkits.jetbrains.services.codewhisperer.service
55

6-
import com.intellij.codeInsight.CodeInsightSettings
76
import com.intellij.codeInsight.hint.HintManager
87
import com.intellij.notification.NotificationAction
8+
import com.intellij.openapi.Disposable
99
import com.intellij.openapi.application.ApplicationInfo
1010
import com.intellij.openapi.application.ApplicationManager
1111
import com.intellij.openapi.application.runInEdt
@@ -69,6 +69,7 @@ import software.aws.toolkits.jetbrains.services.codewhisperer.popup.CodeWhispere
6969
import software.aws.toolkits.jetbrains.services.codewhisperer.settings.CodeWhispererSettings
7070
import software.aws.toolkits.jetbrains.services.codewhisperer.telemetry.CodeWhispererTelemetryService
7171
import software.aws.toolkits.jetbrains.services.codewhisperer.util.CaretMovement
72+
import software.aws.toolkits.jetbrains.services.codewhisperer.util.CodeInsightsSettingsFacade
7273
import software.aws.toolkits.jetbrains.services.codewhisperer.util.CodeWhispererConstants
7374
import software.aws.toolkits.jetbrains.services.codewhisperer.util.CodeWhispererConstants.SUPPLEMENTAL_CONTEXT_TIMEOUT
7475
import software.aws.toolkits.jetbrains.services.codewhisperer.util.CodeWhispererUtil.getCompletionType
@@ -90,9 +91,14 @@ import software.aws.toolkits.telemetry.CodewhispererTriggerType
9091
import java.util.concurrent.TimeUnit
9192

9293
@Service
93-
class CodeWhispererService {
94+
class CodeWhispererService : Disposable {
95+
private val codeInsightSettingsFacade = CodeInsightsSettingsFacade()
9496
private var refreshFailure: Int = 0
9597

98+
init {
99+
Disposer.register(this, codeInsightSettingsFacade)
100+
}
101+
96102
fun showRecommendationsInPopup(
97103
editor: Editor,
98104
triggerTypeInfo: TriggerTypeInfo,
@@ -691,16 +697,8 @@ class CodeWhispererService {
691697
}
692698

693699
private fun addPopupChildDisposables(popup: JBPopup) {
694-
val originalTabExitsBracketsAndQuotes = CodeInsightSettings.getInstance().TAB_EXITS_BRACKETS_AND_QUOTES
695-
CodeInsightSettings.getInstance().TAB_EXITS_BRACKETS_AND_QUOTES = false
696-
Disposer.register(popup) {
697-
CodeInsightSettings.getInstance().TAB_EXITS_BRACKETS_AND_QUOTES = originalTabExitsBracketsAndQuotes
698-
}
699-
val originalAutoPopupCompletionLookup = CodeInsightSettings.getInstance().AUTO_POPUP_COMPLETION_LOOKUP
700-
CodeInsightSettings.getInstance().AUTO_POPUP_COMPLETION_LOOKUP = false
701-
Disposer.register(popup) {
702-
CodeInsightSettings.getInstance().AUTO_POPUP_COMPLETION_LOOKUP = originalAutoPopupCompletionLookup
703-
}
700+
codeInsightSettingsFacade.disableCodeInsightUntil(popup)
701+
704702
Disposer.register(popup) {
705703
CodeWhispererPopupManager.getInstance().reset()
706704
}
@@ -763,6 +761,8 @@ class CodeWhispererService {
763761
HintManager.getInstance().showErrorHint(editor, message, HintManager.UNDER)
764762
}
765763

764+
override fun dispose() {}
765+
766766
companion object {
767767
private val LOG = getLogger<CodeWhispererService>()
768768
private const val MAX_REFRESH_ATTEMPT = 3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
// Copyright 2024 Amazon.com, Inc. or its affiliates. All Rights Reserved.
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
package software.aws.toolkits.jetbrains.services.codewhisperer.util
5+
6+
import com.intellij.codeInsight.CodeInsightSettings
7+
import com.intellij.openapi.Disposable
8+
import com.intellij.openapi.util.Disposer
9+
import com.intellij.openapi.util.SimpleModificationTracker
10+
import org.jetbrains.annotations.VisibleForTesting
11+
import software.aws.toolkits.core.utils.error
12+
import software.aws.toolkits.core.utils.getLogger
13+
import kotlin.reflect.KMutableProperty
14+
15+
class CodeInsightsSettingsFacade : SimpleModificationTracker(), Disposable {
16+
private inner class ChangeAndRevert<T : Any>(
17+
val p: KMutableProperty<T>,
18+
val value: T,
19+
parentDisposable: Disposable
20+
) {
21+
val origin: T = p.getter.call()
22+
var isComplete: Boolean = false
23+
private set
24+
25+
init {
26+
Disposer.register(parentDisposable) {
27+
revert()
28+
}
29+
}
30+
31+
fun commit(): ChangeAndRevert<T> {
32+
p.setter.call(value)
33+
return this
34+
}
35+
36+
fun revert() {
37+
if (isComplete) {
38+
return
39+
}
40+
41+
p.setter.call(origin)
42+
isComplete = true
43+
}
44+
}
45+
46+
private val settings by lazy {
47+
CodeInsightSettings.getInstance()
48+
}
49+
50+
private var pendingReverts = listOf<ChangeAndRevert<*>>()
51+
val pendingRevertCounts: Int
52+
get() = pendingReverts.size
53+
54+
@VisibleForTesting
55+
internal fun revertAll() {
56+
if (pendingReverts.count { !it.isComplete } == 0) {
57+
return
58+
}
59+
60+
pendingReverts.forEach {
61+
it.revert()
62+
}
63+
64+
pendingReverts = pendingReverts.filter {
65+
!it.isComplete
66+
}.toMutableList()
67+
}
68+
69+
fun disableCodeInsightUntil(parentDisposable: Disposable) {
70+
revertAll()
71+
val toReverts = mutableListOf<ChangeAndRevert<*>>()
72+
73+
ChangeAndRevert(settings::TAB_EXITS_BRACKETS_AND_QUOTES, false, parentDisposable).commit().also {
74+
toReverts.add(it)
75+
}
76+
77+
ChangeAndRevert(settings::AUTO_POPUP_COMPLETION_LOOKUP, false, parentDisposable).commit().also {
78+
toReverts.add(it)
79+
}
80+
81+
if (pendingReverts.count { !it.isComplete } != 0) {
82+
LOG.error { "trying to overwrite users' settings without reverting all previous overwrites" }
83+
}
84+
pendingReverts = toReverts
85+
86+
Disposer.register(parentDisposable) {
87+
revertAll()
88+
}
89+
}
90+
91+
override fun dispose() {
92+
revertAll()
93+
}
94+
95+
companion object {
96+
val LOG = getLogger<CodeInsightsSettingsFacade>()
97+
}
98+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
// Copyright 2024 Amazon.com, Inc. or its affiliates. All Rights Reserved.
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
package software.aws.toolkits.jetbrains.services.codewhisperer.util
5+
6+
import com.intellij.codeInsight.CodeInsightSettings
7+
import com.intellij.openapi.Disposable
8+
import com.intellij.openapi.application.ApplicationManager
9+
import com.intellij.openapi.util.Disposer
10+
import com.intellij.testFramework.ProjectExtension
11+
import com.intellij.testFramework.junit5.TestDisposable
12+
import com.intellij.testFramework.replaceService
13+
import org.assertj.core.api.Assertions.assertThat
14+
import org.junit.jupiter.api.BeforeEach
15+
import org.junit.jupiter.api.Test
16+
import org.junit.jupiter.api.extension.RegisterExtension
17+
import org.mockito.kotlin.spy
18+
import org.mockito.kotlin.times
19+
import org.mockito.kotlin.verify
20+
21+
class CodeInsightsSettingsFacadeTest {
22+
private lateinit var settings: CodeInsightSettings
23+
private lateinit var sut: CodeInsightsSettingsFacade
24+
25+
@TestDisposable
26+
private lateinit var disposable: Disposable
27+
28+
companion object {
29+
@JvmField
30+
@RegisterExtension
31+
val projectExtension = ProjectExtension()
32+
}
33+
34+
@BeforeEach
35+
fun setUp() {
36+
sut = spy(CodeInsightsSettingsFacade())
37+
settings = spy { CodeInsightSettings() }
38+
39+
ApplicationManager.getApplication().replaceService(
40+
CodeInsightSettings::class.java,
41+
settings,
42+
disposable
43+
)
44+
}
45+
46+
@Test
47+
fun `disableCodeInsightUntil should revert when parent is disposed`() {
48+
val myFakePopup = Disposable {}.also {
49+
Disposer.register(disposable, it)
50+
}
51+
52+
// assume users' enable the following two codeinsight functionalities
53+
settings.TAB_EXITS_BRACKETS_AND_QUOTES = true
54+
assertThat(settings.TAB_EXITS_BRACKETS_AND_QUOTES).isTrue
55+
settings.AUTOCOMPLETE_ON_CODE_COMPLETION = true
56+
assertThat(settings.AUTO_POPUP_COMPLETION_LOOKUP).isTrue
57+
58+
// codewhisperer disable them while popup is shown
59+
sut.disableCodeInsightUntil(myFakePopup)
60+
61+
assertThat(settings.TAB_EXITS_BRACKETS_AND_QUOTES).isFalse
62+
assertThat(settings.AUTO_POPUP_COMPLETION_LOOKUP).isFalse
63+
assertThat(sut.pendingRevertCounts).isEqualTo(2)
64+
65+
// popup is closed and disposed
66+
Disposer.dispose(myFakePopup)
67+
68+
// revert changes made by codewhisperer
69+
verify(sut, times(2)).revertAll()
70+
assertThat(settings.TAB_EXITS_BRACKETS_AND_QUOTES).isTrue
71+
assertThat(settings.AUTO_POPUP_COMPLETION_LOOKUP).isTrue
72+
}
73+
74+
@Test
75+
fun `revertAll should revert back all changes made by codewhisperer`() {
76+
settings.TAB_EXITS_BRACKETS_AND_QUOTES = true
77+
assertThat(settings.TAB_EXITS_BRACKETS_AND_QUOTES).isTrue
78+
settings.AUTOCOMPLETE_ON_CODE_COMPLETION = true
79+
assertThat(settings.AUTO_POPUP_COMPLETION_LOOKUP).isTrue
80+
81+
sut.disableCodeInsightUntil(disposable)
82+
83+
assertThat(settings.TAB_EXITS_BRACKETS_AND_QUOTES).isFalse
84+
assertThat(settings.AUTO_POPUP_COMPLETION_LOOKUP).isFalse
85+
86+
assertThat(sut.pendingRevertCounts).isEqualTo(2)
87+
88+
sut.revertAll()
89+
assertThat(sut.pendingRevertCounts).isEqualTo(0)
90+
assertThat(settings.TAB_EXITS_BRACKETS_AND_QUOTES).isTrue
91+
assertThat(settings.AUTO_POPUP_COMPLETION_LOOKUP).isTrue
92+
}
93+
94+
@Test
95+
fun `disableCodeInsightUntil should always flush pending reverts before making next changes`() {
96+
val myFakePopup = Disposable {}.also {
97+
Disposer.register(disposable, it)
98+
}
99+
val myAnotherFakePopup = Disposable {}.also {
100+
Disposer.register(disposable, it)
101+
}
102+
103+
// assume users' enable the following two codeinsight functionalities
104+
settings.TAB_EXITS_BRACKETS_AND_QUOTES = true
105+
assertThat(settings.TAB_EXITS_BRACKETS_AND_QUOTES).isTrue
106+
settings.AUTOCOMPLETE_ON_CODE_COMPLETION = true
107+
assertThat(settings.AUTO_POPUP_COMPLETION_LOOKUP).isTrue
108+
109+
// codewhisperer disable them while popup_1 is shown
110+
sut.disableCodeInsightUntil(myFakePopup)
111+
assertThat(settings.TAB_EXITS_BRACKETS_AND_QUOTES).isFalse
112+
assertThat(settings.AUTO_POPUP_COMPLETION_LOOKUP).isFalse
113+
assertThat(sut.pendingRevertCounts).isEqualTo(2)
114+
verify(sut, times(1)).revertAll()
115+
116+
// unexpected issue happens and popup_1 is not disposed correctly and popup_2 is created
117+
sut.disableCodeInsightUntil(myAnotherFakePopup)
118+
assertThat(settings.TAB_EXITS_BRACKETS_AND_QUOTES).isFalse
119+
assertThat(settings.AUTO_POPUP_COMPLETION_LOOKUP).isFalse
120+
// should still be 2 because previous ones should be reverted before preceding next changes
121+
assertThat(sut.pendingRevertCounts).isEqualTo(2)
122+
verify(sut, times(1 + 1)).revertAll()
123+
124+
Disposer.dispose(myAnotherFakePopup)
125+
126+
assertThat(sut.pendingRevertCounts).isEqualTo(0)
127+
verify(sut, times(1 + 1 + 1)).revertAll()
128+
assertThat(settings.TAB_EXITS_BRACKETS_AND_QUOTES).isTrue
129+
assertThat(settings.AUTO_POPUP_COMPLETION_LOOKUP).isTrue
130+
}
131+
132+
@Test
133+
fun `dispose should call revertAll to revert all changes made by CodeWhisperer`() {
134+
sut.dispose()
135+
verify(sut).revertAll()
136+
}
137+
}

plugins/core/jetbrains-community/src/software/aws/toolkits/jetbrains/core/credentials/DefaultToolkitConnectionManager.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
package software.aws.toolkits.jetbrains.core.credentials
55

66
import com.intellij.ide.ActivityTracker
7+
import com.intellij.openapi.application.ApplicationInfo
78
import com.intellij.openapi.application.ApplicationManager
89
import com.intellij.openapi.components.PersistentStateComponent
910
import com.intellij.openapi.components.State
@@ -76,6 +77,7 @@ class DefaultToolkitConnectionManager : ToolkitConnectionManager, PersistentStat
7677

7778
null
7879
} ?: defaultConnection?.let {
80+
if (ApplicationInfo.getInstance().build.productCode == "GW") return null
7981
if (feature.supportsConnectionType(it)) {
8082
return it
8183
}

0 commit comments

Comments
 (0)