Skip to content

Commit bce2fac

Browse files
authored
fix unexpected right context merging causes recommednation gets trimmed (#4108)
1 parent a9dff42 commit bce2fac

File tree

3 files changed

+144
-4
lines changed

3 files changed

+144
-4
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 issue where CodeWhisperer suggestions are sometimes trimmed"
4+
}

jetbrains-core/src/software/aws/toolkits/jetbrains/services/codewhisperer/service/CodeWhispererRecommendationManager.kt

Lines changed: 36 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
package software.aws.toolkits.jetbrains.services.codewhisperer.service
55

66
import com.intellij.openapi.components.service
7+
import org.jetbrains.annotations.VisibleForTesting
78
import software.amazon.awssdk.services.codewhispererruntime.model.Completion
89
import software.amazon.awssdk.services.codewhispererruntime.model.Span
910
import software.aws.toolkits.jetbrains.services.codewhisperer.model.DetailContext
@@ -134,16 +135,21 @@ class CodeWhispererRecommendationManager {
134135
val caret = requestContext.editor.caretModel.primaryCaret
135136
val rightContext = document.charsSequence.subSequence(caret.offset, document.charsSequence.length).toString()
136137
val recommendationContent = recommendation.content()
138+
return findRightContextOverlap(rightContext, recommendationContent)
139+
}
140+
141+
@VisibleForTesting
142+
fun findRightContextOverlap(rightContext: String, recommendationContent: String): String {
137143
val rightContextFirstLine = rightContext.substringBefore("\n")
138144
val overlap =
139145
if (rightContextFirstLine.isEmpty()) {
140146
val tempOverlap = overlap(recommendationContent, rightContext)
141-
if (tempOverlap.isEmpty()) overlap(recommendationContent.trimEnd(), rightContext.trimStart()) else tempOverlap
147+
if (tempOverlap.isEmpty()) overlap(recommendationContent.trimEnd(), trimExtraPrefixNewLine(rightContext)) else tempOverlap
142148
} else {
143149
// this is necessary to prevent display issue if first line of right context is not empty
144150
var tempOverlap = overlap(recommendationContent, rightContext)
145151
if (tempOverlap.isEmpty()) {
146-
tempOverlap = overlap(recommendationContent.trimEnd(), rightContext.trimStart())
152+
tempOverlap = overlap(recommendationContent.trimEnd(), trimExtraPrefixNewLine(rightContext))
147153
}
148154
if (recommendationContent.substring(0, recommendationContent.length - tempOverlap.length).none { it == '\n' }) {
149155
tempOverlap
@@ -166,5 +172,33 @@ class CodeWhispererRecommendationManager {
166172

167173
companion object {
168174
fun getInstance(): CodeWhispererRecommendationManager = service()
175+
176+
/**
177+
* a function to trim extra prefixing new line character (only leave 1 new line character)
178+
* example:
179+
* content = "\n\n\nfoo\n\nbar\nbaz"
180+
* return = "\nfoo\n\nbar\nbaz"
181+
*
182+
* example:
183+
* content = "\n\n\tfoobar\nbaz"
184+
* return = "\n\tfoobar\nbaz"
185+
*/
186+
fun trimExtraPrefixNewLine(content: String): String {
187+
if (content.isEmpty()) {
188+
return ""
189+
}
190+
191+
val firstChar = content.first()
192+
if (firstChar != '\n') {
193+
return content
194+
}
195+
196+
var index = 1
197+
while (index < content.length && content[index] == '\n') {
198+
index++
199+
}
200+
201+
return firstChar + content.substring(index)
202+
}
169203
}
170204
}

jetbrains-core/tst/software/aws/toolkits/jetbrains/services/codewhisperer/CodeWhispererRecommendationManagerTest.kt

Lines changed: 104 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,10 @@ import org.mockito.kotlin.any
1717
import org.mockito.kotlin.doReturn
1818
import org.mockito.kotlin.spy
1919
import org.mockito.kotlin.stub
20+
import software.amazon.awssdk.services.codewhispererruntime.model.Completion
2021
import software.aws.toolkits.core.utils.test.aString
2122
import software.aws.toolkits.jetbrains.services.codewhisperer.service.CodeWhispererRecommendationManager
23+
import software.aws.toolkits.jetbrains.services.codewhisperer.service.RequestContext
2224
import software.aws.toolkits.jetbrains.utils.rules.PythonCodeInsightTestFixtureRule
2325

2426
class CodeWhispererRecommendationManagerTest {
@@ -72,7 +74,7 @@ class CodeWhispererRecommendationManagerTest {
7274
fun `test duplicated recommendation after truncation will be discarded`() {
7375
val userInput = ""
7476
sut.stub {
75-
onGeneric { findRightContextOverlap(any(), any()) } doReturn "}"
77+
onGeneric { findRightContextOverlap(any<RequestContext>(), any<Completion>()) } doReturn "}"
7678
onGeneric { reformatReference(any(), any()) } doReturn aCompletion("def")
7779
}
7880
val detail = sut.buildDetailContext(
@@ -91,7 +93,7 @@ class CodeWhispererRecommendationManagerTest {
9193
fun `test blank recommendation after truncation will be discarded`() {
9294
val userInput = ""
9395
sut.stub {
94-
onGeneric { findRightContextOverlap(any(), any()) } doReturn "}"
96+
onGeneric { findRightContextOverlap(any<RequestContext>(), any<Completion>()) } doReturn "}"
9597
}
9698
val detail = sut.buildDetailContext(
9799
aRequestContext(project),
@@ -102,4 +104,104 @@ class CodeWhispererRecommendationManagerTest {
102104
assertThat(detail[0].isDiscarded).isTrue
103105
assertThat(detail[0].isTruncatedOnRight).isTrue
104106
}
107+
108+
@Test
109+
fun `overlap calculation should trim new line character starting from second character (index 1 of a string)`() {
110+
// recommendation is wrapped inside |recommendationContent|
111+
/**
112+
* public foo() {
113+
* re|turn foo
114+
*}|
115+
* public bar() {
116+
* return bar
117+
* }
118+
*/
119+
var overlap: String = sut.findRightContextOverlap(rightContext = " foo\n}\n\n\npublic bar () {\n\treturn bar\n}", recommendationContent = "turn foo\n}")
120+
assertThat(overlap).isEqualTo(" foo\n}")
121+
122+
/**
123+
* public foo() {
124+
* |return foo
125+
* }|
126+
*
127+
* public bar() {
128+
* return bar
129+
* }
130+
*/
131+
overlap = sut.findRightContextOverlap(rightContext = "\n\n\n\npublic bar() {\n\treturn bar\n}", recommendationContent = "return foo\n}")
132+
assertThat(overlap).isEqualTo("")
133+
134+
/**
135+
* println(|world)|;
136+
* String foo = "foo";
137+
*/
138+
overlap = sut.findRightContextOverlap(rightContext = "ld);\nString foo = \"foo\";", recommendationContent = "world)")
139+
assertThat(overlap).isEqualTo("ld)")
140+
141+
/**
142+
* return |has_d_at_end|
143+
*
144+
* def foo:
145+
* pass
146+
*/
147+
overlap = sut.findRightContextOverlap(rightContext = "\n\ndef foo():\n\tpass", recommendationContent = "has_d_at_end")
148+
assertThat(overlap).isEqualTo("")
149+
150+
/**
151+
* {
152+
* { foo: foo },
153+
* { bar: bar },
154+
* { |baz: baz }|
155+
* }
156+
*
157+
*/
158+
overlap = sut.findRightContextOverlap(rightContext = "\n}", recommendationContent = "baz: baz }")
159+
assertThat(overlap).isEqualTo("")
160+
161+
/**
162+
* |
163+
*
164+
* foo|
165+
*
166+
*/
167+
overlap = sut.findRightContextOverlap(rightContext = "\n\n\tfoo}", recommendationContent = "\n\tfoo")
168+
assertThat(overlap).isEqualTo("\n\tfoo")
169+
170+
/** A case we can't cover
171+
* def foo():
172+
* |print(foo)|
173+
*
174+
*
175+
* print(foo)
176+
*/
177+
overlap = sut.findRightContextOverlap(rightContext = "\n\n\n\tprint(foo)", recommendationContent = "print(foo)")
178+
assertThat(overlap).isEqualTo("")
179+
}
180+
181+
@Test
182+
fun `trim extra prefixing new line character`() {
183+
var actual: String = CodeWhispererRecommendationManager.trimExtraPrefixNewLine("")
184+
assertThat(actual).isEqualTo("")
185+
186+
actual = CodeWhispererRecommendationManager.trimExtraPrefixNewLine("f")
187+
assertThat(actual).isEqualTo("f")
188+
189+
actual = CodeWhispererRecommendationManager.trimExtraPrefixNewLine("\n\n")
190+
assertThat(actual).isEqualTo("\n")
191+
192+
actual = CodeWhispererRecommendationManager.trimExtraPrefixNewLine("foo")
193+
assertThat(actual).isEqualTo("foo")
194+
195+
actual = CodeWhispererRecommendationManager.trimExtraPrefixNewLine("\nfoo")
196+
assertThat(actual).isEqualTo("\nfoo")
197+
198+
actual = CodeWhispererRecommendationManager.trimExtraPrefixNewLine("\n\n\nfoo\nbar")
199+
assertThat(actual).isEqualTo("\nfoo\nbar")
200+
201+
actual = CodeWhispererRecommendationManager.trimExtraPrefixNewLine("\n\n foo\nbar")
202+
assertThat(actual).isEqualTo("\n foo\nbar")
203+
204+
actual = CodeWhispererRecommendationManager.trimExtraPrefixNewLine("\n\n\tfoo\nbar")
205+
assertThat(actual).isEqualTo("\n\tfoo\nbar")
206+
}
105207
}

0 commit comments

Comments
 (0)