diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqFeatureDev/util/DiffMetrics.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqFeatureDev/util/DiffMetrics.kt new file mode 100644 index 00000000000..23e9dcd4f3e --- /dev/null +++ b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqFeatureDev/util/DiffMetrics.kt @@ -0,0 +1,73 @@ +// Copyright 2024 Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package software.aws.toolkits.jetbrains.services.amazonqFeatureDev.util + +import com.intellij.diff.comparison.ComparisonManager +import com.intellij.diff.comparison.ComparisonPolicy +import com.intellij.diff.fragments.LineFragment +import com.intellij.openapi.progress.EmptyProgressIndicator + +data class DiffMetrics( + val insertedLines: Int, + val insertedCharacters: Int, +) + +fun lineEnding(content: String, curr: Int, end: Int): Int { + require(curr <= end) { "curr must be within end of range" } + require(end <= content.length) { "end must be within content" } + + return if (curr == end) { + -1 + } else if (content[curr] == '\r') { + if ((curr + 1 < end) && (content[curr + 1] == '\n')) { + 2 + } else { + 1 + } + } else if (content[curr] == '\n') { + 1 + } else { + -1 + } +} + +fun getDiffMetrics(before: String, after: String): DiffMetrics { + val comparisonManager = ComparisonManager.getInstance() + val fragments = comparisonManager.compareLines( + before, + after, + ComparisonPolicy.DEFAULT, + EmptyProgressIndicator() + ) + + var accLineCount = 0 + var accCharCount = 0 + + fragments.forEach { fragment: LineFragment -> + var curr = fragment.startOffset2 + val end = fragment.endOffset2 + + while (curr < end) { + accLineCount += 1 + + // Consume leading whitespace: + while (curr < end && lineEnding(after, curr, end) == -1 && after[curr].isWhitespace()) curr++ + + // Consume through EOL: + val lineContentStart = curr + while (curr < end && lineEnding(after, curr, end) == -1) curr++ + var lineContentEnd = curr + curr += maxOf(lineEnding(after, curr, end), 0) + + // Walk back trailing whitespace and record character count before continuing to next line: + while (lineContentEnd > lineContentStart && after[lineContentEnd - 1].isWhitespace()) lineContentEnd-- + accCharCount += lineContentEnd - lineContentStart + } + } + + return DiffMetrics( + insertedLines = accLineCount, + insertedCharacters = accCharCount, + ) +} diff --git a/plugins/amazonq/chat/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/amazonqFeatureDev/util/DiffMetricsTest.kt b/plugins/amazonq/chat/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/amazonqFeatureDev/util/DiffMetricsTest.kt new file mode 100644 index 00000000000..1283810dab2 --- /dev/null +++ b/plugins/amazonq/chat/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/amazonqFeatureDev/util/DiffMetricsTest.kt @@ -0,0 +1,126 @@ +// Copyright 2024 Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package software.aws.toolkits.jetbrains.services.amazonqFeatureDev.util + +import com.intellij.testFramework.LightPlatformTestCase +import com.intellij.testFramework.TestApplicationManager + +class DiffMetricsTest : LightPlatformTestCase() { + override fun setUp() { + super.setUp() + TestApplicationManager.getInstance() + } + + fun `test empty input`() { + val metrics = getDiffMetrics("", "") + assertEquals(0, metrics.insertedLines) + assertEquals(0, metrics.insertedCharacters) + } + + fun `test insertions are counted`() { + val before = """ + line1 + line2 + """.trimIndent() + + val after = """ + line1 + inserted + line2 + """.trimIndent() + + val metrics = getDiffMetrics(before, after) + assertEquals(1, metrics.insertedLines) + assertEquals(8, metrics.insertedCharacters) + } + + fun `test modifications are counted`() { + val before = """ + line1 + line2 + line3 + """.trimIndent() + + val after = """ + line1 + modified + line3 + """.trimIndent() + + val metrics = getDiffMetrics(before, after) + assertEquals(1, metrics.insertedLines) + assertEquals(8, metrics.insertedCharacters) + } + + fun `test deletions are counted`() { + val before = """ + line1 + line2 + line3 + """.trimIndent() + + val after = """ + line1 + line3 + """.trimIndent() + + val metrics = getDiffMetrics(before, after) + assertEquals(0, metrics.insertedLines) + assertEquals(0, metrics.insertedCharacters) + } + + fun `test multiline and multiple hunks are counted`() { + val before = """ + line1 + line2 + line3 + """.trimIndent() + + val after = """ + inserted1 + line1 + inserted2 + inserted3 + line3 + inserted4 + """.trimIndent() + + val metrics = getDiffMetrics(before, after) + assertEquals(4, metrics.insertedLines) + assertEquals(36, metrics.insertedCharacters) + } + + fun `test empty lines are counted`() { + val before = "line1" + val after = "line1\n\nline2" + val metrics = getDiffMetrics(before, after) + assertEquals(2, metrics.insertedLines) + assertEquals(5, metrics.insertedCharacters) + } + + fun `test trailing newline is not counted`() { + val before = "line1" + val after = "line1\nline2\n" + val metrics = getDiffMetrics(before, after) + assertEquals(1, metrics.insertedLines) + assertEquals(5, metrics.insertedCharacters) + } + + fun `test newline sequences are counted`() { + val before = "line1" + val after = "line1\nline2\rline3\r\nline4" + val metrics = getDiffMetrics(before, after) + assertEquals(3, metrics.insertedLines) + assertEquals(15, metrics.insertedCharacters) + } + + fun `test leading and trailing whitespace are not counted as characters`() { + val before = "line1\nline2" + val after = "line1\n line2" + + val metrics = getDiffMetrics(before, after) + assertEquals(1, metrics.insertedLines) + assertEquals(5, metrics.insertedCharacters) + } +}