diff --git a/README.md b/README.md index 17b1656f..18df0129 100644 --- a/README.md +++ b/README.md @@ -76,18 +76,18 @@ Maven: com.simiacryptus skyenet-webui - 1.0.63 + 1.0.64 ``` Gradle: ```groovy -implementation group: 'com.simiacryptus', name: 'skyenet', version: '1.0.63' +implementation group: 'com.simiacryptus', name: 'skyenet', version: '1.0.64' ``` ```kotlin -implementation("com.simiacryptus:skyenet:1.0.63") +implementation("com.simiacryptus:skyenet:1.0.64") ``` ### 🌟 To Use diff --git a/core/build.gradle.kts b/core/build.gradle.kts index 5cb13c0e..2f5c96b0 100644 --- a/core/build.gradle.kts +++ b/core/build.gradle.kts @@ -33,7 +33,7 @@ val hsqldb_version = "2.7.2" dependencies { - implementation(group = "com.simiacryptus", name = "jo-penai", version = "1.0.63") + implementation(group = "com.simiacryptus", name = "jo-penai", version = "1.0.64") implementation(group = "org.hsqldb", name = "hsqldb", version = hsqldb_version) implementation("org.apache.commons:commons-text:1.11.0") diff --git a/core/src/main/kotlin/com/simiacryptus/skyenet/core/platform/ClientManager.kt b/core/src/main/kotlin/com/simiacryptus/skyenet/core/platform/ClientManager.kt index d7218a91..f01facae 100644 --- a/core/src/main/kotlin/com/simiacryptus/skyenet/core/platform/ClientManager.kt +++ b/core/src/main/kotlin/com/simiacryptus/skyenet/core/platform/ClientManager.kt @@ -132,7 +132,7 @@ open class ClientManager { apiBase: Map = APIProvider.values().associate { it to (it.base ?: "") }, scheduledPool: ListeningScheduledExecutorService = HttpClientManager.scheduledPool, workPool: ThreadPoolExecutor = HttpClientManager.workPool, - client: CloseableHttpClient = HttpClientManager.client + client: CloseableHttpClient = HttpClientManager.createHttpClient() ) : OpenAIClient( key = key, logLevel = Level.DEBUG, diff --git a/gradle.properties b/gradle.properties index f782053d..8d65520b 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,5 +1,5 @@ # Gradle Releases -> https://github.com/gradle/gradle/releases libraryGroup = com.simiacryptus.skyenet -libraryVersion = 1.0.82 +libraryVersion = 1.0.83 gradleVersion = 7.6.1 kotlin.daemon.jvmargs=-Xmx2g diff --git a/webui/build.gradle.kts b/webui/build.gradle.kts index 7ff978d6..15ce7fe3 100644 --- a/webui/build.gradle.kts +++ b/webui/build.gradle.kts @@ -35,7 +35,7 @@ val jetty_version = "11.0.18" val jackson_version = "2.17.0" dependencies { - implementation(group = "com.simiacryptus", name = "jo-penai", version = "1.0.63") { + implementation(group = "com.simiacryptus", name = "jo-penai", version = "1.0.64") { exclude(group = "org.slf4j", module = "slf4j-api") } @@ -96,9 +96,8 @@ dependencies { implementation(group = "commons-codec", name = "commons-codec", version = "1.16.0") implementation(group = "org.slf4j", name = "slf4j-api", version = "2.0.9") - runtimeOnly(group = "org.slf4j", name = "slf4j-simple", version = "2.0.9") - testImplementation(group = "ch.qos.logback", name = "logback-classic", version = "1.4.14") - testImplementation(group = "ch.qos.logback", name = "logback-core", version = "1.4.14") + runtimeOnly(group = "ch.qos.logback", name = "logback-classic", version = "1.4.14") + runtimeOnly(group = "ch.qos.logback", name = "logback-core", version = "1.4.14") testImplementation(kotlin("script-runtime")) testImplementation(group = "org.junit.jupiter", name = "junit-jupiter-api", version = "5.10.1") diff --git a/webui/src/main/kotlin/com/simiacryptus/diff/AddApplyFileDiffLinks.kt b/webui/src/main/kotlin/com/simiacryptus/diff/AddApplyFileDiffLinks.kt index a2344e97..973432c4 100644 --- a/webui/src/main/kotlin/com/simiacryptus/diff/AddApplyFileDiffLinks.kt +++ b/webui/src/main/kotlin/com/simiacryptus/diff/AddApplyFileDiffLinks.kt @@ -9,26 +9,27 @@ import com.simiacryptus.skyenet.set import com.simiacryptus.skyenet.webui.application.ApplicationInterface import com.simiacryptus.skyenet.webui.session.SocketManagerBase import com.simiacryptus.skyenet.webui.util.MarkdownUtil.renderMarkdown -import org.apache.commons.codec.digest.Md5Crypt import java.io.File -import java.nio.file.Files import java.nio.file.Path -import java.util.concurrent.TimeUnit import java.util.regex.Pattern import kotlin.io.path.readText + +// Function to reverse the order of lines in a string private fun String.reverseLines(): String = lines().reversed().joinToString("\n") +// Main function to add apply file diff links to the response fun SocketManagerBase.addApplyFileDiffLinks( root: Path, response: String, - handle: (Map) -> Unit, + handle: (Map) -> Unit = {}, ui: ApplicationInterface, api: API, ): String { + // Check if there's an unclosed code block and close it if necessary val initiator = "(?s)```[\\w]*\n".toRegex() if (response.contains(initiator) && !response.split(initiator, 2)[1].contains("\n```(?![^\n])".toRegex())) { - // Single diff block without the closing ``` due to LLM limitations... add it back + // Single diff block without the closing ``` due to LLM limitations... add it back and recurse return addApplyFileDiffLinks( root, response + "\n```", @@ -37,6 +38,7 @@ fun SocketManagerBase.addApplyFileDiffLinks( api ) } + // Define regex patterns for headers and code blocks val headerPattern = """(? false //block.groupValues[1] == "diff" -> true else -> true } @@ -60,6 +61,7 @@ fun SocketManagerBase.addApplyFileDiffLinks( else -> true } }.map { it.range to it }.toList() + // Process diff blocks and add patch links val withPatchLinks: String = diffs.fold(response) { markdown, diffBlock -> val header = headers.lastOrNull { it.first.endInclusive < diffBlock.first.start } val filename = resolve(root, header?.second ?: "Unknown") @@ -68,6 +70,7 @@ fun SocketManagerBase.addApplyFileDiffLinks( val regex = "(?s)```[^\n]*\n?${Pattern.quote(diffVal)}\n?```".toRegex() markdown.replace(regex, newValue) } + // Process code blocks and add save links val withSaveLinks = codeblocks.fold(withPatchLinks) { markdown, codeBlock -> val header = headers.lastOrNull { it.first.endInclusive < codeBlock.first.start } val filename = resolve(root, header?.second ?: "Unknown") @@ -123,6 +126,8 @@ fun SocketManagerBase.addApplyFileDiffLinks( } private val pattern_backticks = "`(.*)`".toRegex() + +// Function to resolve filenames, handling backticks and relative paths fun resolve(root: Path, filename: String): String { val filename = if (pattern_backticks.containsMatchIn(filename)) { pattern_backticks.find(filename)!!.groupValues[1] @@ -139,6 +144,7 @@ fun resolve(root: Path, filename: String): String { return filepath?.let { root.relativize(it).toString() } ?: filename } +// Extension function to recursively list all files in a directory fun File.recurseFiles(): List { val files = mutableListOf() if (isDirectory) { @@ -152,6 +158,7 @@ fun File.recurseFiles(): List { } +// Function to render a diff block with apply and revert options private fun SocketManagerBase.renderDiffBlock( root: Path, filename: String, @@ -159,25 +166,8 @@ private fun SocketManagerBase.renderDiffBlock( handle: (Map) -> Unit, ui: ApplicationInterface, api: API?, - watch: Boolean = false, ): String { - val diffTask = ui.newTask(root = false) - val prevCodeTask = ui.newTask(root = false) - val prevCodeTaskSB = prevCodeTask.add("") - val newCodeTask = ui.newTask(root = false) - val newCodeTaskSB = newCodeTask.add("") - val patchTask = ui.newTask(root = false) - val patchTaskSB = patchTask.add("") - val fixTask = ui.newTask(root = false) - val prevCode2Task = ui.newTask(root = false) - val prevCode2TaskSB = prevCode2Task.add("") - val newCode2Task = ui.newTask(root = false) - val newCode2TaskSB = newCode2Task.add("") - val patch2Task = ui.newTask(root = false) - val patch2TaskSB = patch2Task.add("") - - val filepath = path(root, filename) val prevCode = load(filepath) val relativize = try { @@ -185,17 +175,23 @@ private fun SocketManagerBase.renderDiffBlock( } catch (e: Throwable) { filepath } - var filehash: String = "" - val applydiffTask = ui.newTask(false) lateinit var hrefLink: StringBuilder - var isApplied = false var originalCode = load(filepath) - lateinit var revert: String var newCode = patch(originalCode, diffVal) + val diffTask = ui.newTask(root = false) + diffTask?.complete(renderMarkdown("```diff\n$diffVal\n```", ui = ui)) + // Create tasks for displaying code and patch information + val prevCodeTask = ui.newTask(root = false) + val prevCodeTaskSB = prevCodeTask.add("") + val newCodeTask = ui.newTask(root = false) + val newCodeTaskSB = newCodeTask.add("") + val patchTask = ui.newTask(root = false) + val patchTaskSB = patchTask.add("") + val fixTask = ui.newTask(root = false) val verifyFwdTabs = if (!newCode.isValid) displayMapInTabs( mapOf( "Code" to (prevCodeTask?.placeholder ?: ""), @@ -210,6 +206,14 @@ private fun SocketManagerBase.renderDiffBlock( "Echo" to (patchTask?.placeholder ?: ""), ) ) + + + val prevCode2Task = ui.newTask(root = false) + val prevCode2TaskSB = prevCode2Task.add("") + val newCode2Task = ui.newTask(root = false) + val newCode2TaskSB = newCode2Task.add("") + val patch2Task = ui.newTask(root = false) + val patch2TaskSB = patch2Task.add("") val verifyRevTabs = displayMapInTabs( mapOf( "Code" to (prevCode2Task?.placeholder ?: ""), @@ -217,21 +221,10 @@ private fun SocketManagerBase.renderDiffBlock( "Echo" to (patch2Task?.placeholder ?: ""), ) ) - val verifyTabs = displayMapInTabs( - mapOf( - "Forward" to verifyFwdTabs, - "Reverse" to verifyRevTabs, - ) - ) - val mainTabs = displayMapInTabs( - mapOf( - "Diff" to (diffTask?.placeholder ?: ""), - "Verify" to verifyTabs, - ) - ) - diffTask?.complete(renderMarkdown("```diff\n$diffVal\n```", ui = ui)) + lateinit var revert: String + // Create "Apply Diff" button var apply1 = hrefLink("Apply Diff", classname = "href-link cmd-button") { try { originalCode = load(filepath) @@ -240,19 +233,30 @@ private fun SocketManagerBase.renderDiffBlock( handle(mapOf(relativize!! to newCode.newCode)) hrefLink.set("""
Diff Applied
""" + revert) applydiffTask.complete() - isApplied = true } catch (e: Throwable) { hrefLink.append("""
Error: ${e.message}
""") applydiffTask.error(null, e) } } - if (!newCode.isValid) { - val fixPatchLink = hrefLink("Fix Patch", classname = "href-link cmd-button") { - try { - val header = fixTask.header("Attempting to fix patch...") - val patchFixer = SimpleActor( - prompt = """ + // Generate and display various code and patch information + newCode = patch(prevCode, diffVal) + val echoDiff = try { + IterativePatchUtil.generatePatch(prevCode, newCode.newCode) + } catch (e: Throwable) { + renderMarkdown("```\n${e.stackTraceToString()}\n```", ui = ui) + } + + if (echoDiff.isNotBlank()) { + + // Add "Fix Patch" button if the patch is not valid + if (!newCode.isValid) { + val fixPatchLink = hrefLink("Fix Patch", classname = "href-link cmd-button") { + try { + val header = fixTask.header("Attempting to fix patch...") + + val patchFixer = SimpleActor( + prompt = """ |You are a helpful AI that helps people with coding. | |Response should use one or more code patches in diff format within ```diff code blocks. @@ -288,19 +292,19 @@ private fun SocketManagerBase.renderDiffBlock( | }); |``` """.trimMargin(), - model = ChatModels.GPT4o, - temperature = 0.3 - ) + model = ChatModels.GPT4o, + temperature = 0.3 + ) - val echoDiff = try { - IterativePatchUtil.generatePatch(prevCode, newCode.newCode) - } catch (e: Throwable) { - renderMarkdown("```\n${e.stackTraceToString()}\n```", ui = ui) - } + val echoDiff = try { + IterativePatchUtil.generatePatch(prevCode, newCode.newCode) + } catch (e: Throwable) { + renderMarkdown("```\n${e.stackTraceToString()}\n```", ui = ui) + } - var answer = patchFixer.answer( - listOf( - """ + var answer = patchFixer.answer( + listOf( + """ |Code: |```${filename.split('.').lastOrNull() ?: ""} |$prevCode @@ -318,130 +322,118 @@ private fun SocketManagerBase.renderDiffBlock( | |Please provide a fix for the diff above in the form of a diff patch. """.trimMargin() - ), api as OpenAIClient - ) - answer = ui.socketManager?.addApplyFileDiffLinks(root, answer, handle, ui, api) ?: answer - header?.clear() - fixTask.complete(renderMarkdown(answer)) + ), api as OpenAIClient + ) + answer = ui.socketManager?.addApplyFileDiffLinks(root, answer, handle, ui, api) ?: answer + header?.clear() + fixTask.complete(renderMarkdown(answer)) + } catch (e: Throwable) { + log.error("Error in fix patch", e) + } + } + //apply1 += fixPatchLink + fixTask.complete(fixPatchLink) + } + + // Create "Apply Diff (Bottom to Top)" button + val apply2 = hrefLink("(Bottom to Top)", classname = "href-link cmd-button") { + try { + originalCode = load(filepath) + val originalLines = originalCode.reverseLines() + val diffLines = diffVal.reverseLines() + val patch1 = patch(originalLines, diffLines) + val newCode2 = patch1.newCode.reverseLines() + filepath?.toFile()?.writeText(newCode2, Charsets.UTF_8) ?: log.warn("File not found: $filepath") + handle(mapOf(relativize!! to newCode2)) + hrefLink.set("""
Diff Applied (Bottom to Top)
""" + revert) + applydiffTask.complete() } catch (e: Throwable) { - log.error("Error in fix patch", e) + hrefLink.append("""
Error: ${e.message}
""") + applydiffTask.error(null, e) } } - //apply1 += fixPatchLink - fixTask.complete(fixPatchLink) - } - val apply2 = hrefLink("(Bottom to Top)", classname = "href-link cmd-button") { - try { - originalCode = load(filepath) - val originalLines = originalCode.reverseLines() - val diffLines = diffVal.reverseLines() - val patch1 = patch(originalLines, diffLines) - val newCode2 = patch1.newCode.reverseLines() - filepath?.toFile()?.writeText(newCode2, Charsets.UTF_8) ?: log.warn("File not found: $filepath") - handle(mapOf(relativize!! to newCode2)) - hrefLink.set("""
Diff Applied (Bottom to Top)
""" + revert) - applydiffTask.complete() - isApplied = true - } catch (e: Throwable) { - hrefLink.append("""
Error: ${e.message}
""") - applydiffTask.error(null, e) + // Create "Revert" button + revert = hrefLink("Revert", classname = "href-link cmd-button") { + try { + save(filepath, originalCode) + handle(mapOf(relativize!! to originalCode)) + hrefLink.set("""
Reverted
""" + apply1 + apply2) + applydiffTask.complete() + } catch (e: Throwable) { + hrefLink.append("""
Error: ${e.message}
""") + applydiffTask.error(null, e) + } } + hrefLink = applydiffTask.complete(apply1 + "\n" + apply2)!! } - revert = hrefLink("Revert", classname = "href-link cmd-button") { - try { - save(filepath, originalCode) - handle(mapOf(relativize!! to originalCode)) - hrefLink.set("""
Reverted
""" + apply1 + apply2) - applydiffTask.complete() - isApplied = false - } catch (e: Throwable) { - hrefLink.append("""
Error: ${e.message}
""") - applydiffTask.error(null, e) - } + + newCodeTaskSB?.set( + renderMarkdown( + "# $filename\n\n```${filename.split('.').lastOrNull() ?: ""}\n${newCode}\n```", + ui = ui, tabs = false + ) + ) + newCodeTask.complete("") + prevCodeTaskSB?.set( + renderMarkdown( + "# $filename\n\n```${filename.split('.').lastOrNull() ?: ""}\n${prevCode}\n```", + ui = ui, tabs = false + ) + ) + prevCodeTask.complete("") + patchTaskSB?.set( + renderMarkdown( + "# $filename\n\n```diff\n ${echoDiff}\n```", + ui = ui, + tabs = false + ) + ) + patchTask.complete("") + val newCode2 = patch( + load(filepath).reverseLines(), + diffVal.reverseLines() + ).newCode.lines().reversed().joinToString("\n") + val echoDiff2 = try { + IterativePatchUtil.generatePatch(prevCode, newCode2) + } catch (e: Throwable) { + renderMarkdown("```\n${e.stackTraceToString()}\n```", ui = ui) } + newCode2TaskSB?.set( + renderMarkdown( + "# $filename\n\n```${filename.split('.').lastOrNull() ?: ""}\n${newCode2}\n```", + ui = ui, tabs = false + ) + ) + newCode2Task.complete("") + prevCode2TaskSB?.set( + renderMarkdown( + "# $filename\n\n```${filename.split('.').lastOrNull() ?: ""}\n${prevCode}\n```", + ui = ui, tabs = false + ) + ) + prevCode2Task.complete("") + patch2TaskSB?.set( + renderMarkdown( + "# $filename\n\n```diff\n ${echoDiff2}\n```", + ui = ui, + tabs = false + ) + ) + patch2Task.complete("") - hrefLink = applydiffTask.complete(apply1 + "\n" + apply2)!! - lateinit var scheduledFn: () -> Unit - var lastModifiedTime: Long = -1 - scheduledFn = { - try { - val currentModifiedTime = Files.getLastModifiedTime(filepath).toMillis() - if (currentModifiedTime != lastModifiedTime) { - lastModifiedTime = currentModifiedTime - val thisFilehash = Md5Crypt.md5Crypt(filepath?.toFile()?.readText()?.toByteArray()) - if (!isApplied && thisFilehash != filehash) { - val newCode = patch(prevCode, diffVal) - val echoDiff = try { - IterativePatchUtil.generatePatch(prevCode, newCode.newCode) - } catch (e: Throwable) { - renderMarkdown("```\n${e.stackTraceToString()}\n```", ui = ui) - } - newCodeTaskSB?.set( - renderMarkdown( - "# $filename\n\n```${filename.split('.').lastOrNull() ?: ""}\n${newCode}\n```", - ui = ui, tabs = false - ) - ) - newCodeTask.complete("") - prevCodeTaskSB?.set( - renderMarkdown( - "# $filename\n\n```${filename.split('.').lastOrNull() ?: ""}\n${prevCode}\n```", - ui = ui, tabs = false - ) - ) - prevCodeTask.complete("") - patchTaskSB?.set( - renderMarkdown( - "# $filename\n\n```diff\n ${echoDiff}\n```", - ui = ui, - tabs = false - ) - ) - patchTask.complete("") - val newCode2 = patch( - load(filepath).reverseLines(), - diffVal.reverseLines() - ).newCode.lines().reversed().joinToString("\n") - val echoDiff2 = try { - IterativePatchUtil.generatePatch(prevCode, newCode2) - } catch (e: Throwable) { - renderMarkdown("```\n${e.stackTraceToString()}\n```", ui = ui) - } - newCode2TaskSB?.set( - renderMarkdown( - "# $filename\n\n```${filename.split('.').lastOrNull() ?: ""}\n${newCode2}\n```", - ui = ui, tabs = false - ) - ) - newCode2Task.complete("") - prevCode2TaskSB?.set( - renderMarkdown( - "# $filename\n\n```${filename.split('.').lastOrNull() ?: ""}\n${prevCode}\n```", - ui = ui, tabs = false - ) - ) - prevCode2Task.complete("") - patch2TaskSB?.set( - renderMarkdown( - "# $filename\n\n```diff\n ${echoDiff2}\n```", - ui = ui, - tabs = false - ) - ) - patch2Task - .complete("") - filehash = thisFilehash - } - } - if (!isApplied && watch) { - scheduledThreadPoolExecutor.schedule(scheduledFn, 1000, TimeUnit.MILLISECONDS) - } - } catch (e: Throwable) { - log.error("Error in scheduled function", e) - } - } - scheduledThreadPoolExecutor.schedule(scheduledFn, 1000, TimeUnit.MILLISECONDS) + // Create main tabs for displaying diff and verification information + val mainTabs = displayMapInTabs( + mapOf( + "Diff" to (diffTask?.placeholder ?: ""), + "Verify" to displayMapInTabs( + mapOf( + "Forward" to verifyFwdTabs, + "Reverse" to verifyRevTabs, + ) + ), + ) + ) val newValue = if (newCode.isValid) { mainTabs + "\n" + applydiffTask.placeholder } else { @@ -452,6 +444,7 @@ private fun SocketManagerBase.renderDiffBlock( return newValue } +// Function to apply a patch to a code string private val patch = { code: String, diff: String -> val isCurlyBalanced = FileValidationUtils.isCurlyBalanced(code) val isSquareBalanced = FileValidationUtils.isSquareBalanced(code) @@ -474,6 +467,7 @@ private val patch = { code: String, diff: String -> } +// Function to load file contents private fun load( filepath: Path? ) = try { @@ -488,6 +482,7 @@ private fun load( "" } +// Function to save file contents private fun save( filepath: Path?, code: String @@ -499,6 +494,7 @@ private fun save( } } +// Function to resolve a file path private fun path(root: Path, filename: String): Path? { val filepath = try { findFile(root, filename) ?: root.resolve(filename) @@ -514,6 +510,7 @@ private fun path(root: Path, filename: String): Path? { return filepath } +// Function to find a file in the directory structure fun findFile(root: Path, filename: String): Path? { return try { when { @@ -542,6 +539,7 @@ fun findFile(root: Path, filename: String): Path? { } } +// Function to apply file diffs from a response string @Suppress("unused") fun applyFileDiffs( root: Path, @@ -560,20 +558,20 @@ fun applyFileDiffs( } }.map { it.range to it }.toList() diffs.forEach { diffBlock -> - val header = headers.lastOrNull { it.first.endInclusive < diffBlock.first.start } + val header = headers.lastOrNull { it.first.last < diffBlock.first.first } val filename = resolve(root, header?.second ?: "Unknown") val diffVal = diffBlock.second val filepath = root.resolve(filename) try { val originalCode = filepath.readText(Charsets.UTF_8) val newCode = patch(originalCode, diffVal) - filepath?.toFile()?.writeText(newCode.newCode, Charsets.UTF_8) ?: log.warn("File not found: $filepath") + filepath.toFile().writeText(newCode.newCode, Charsets.UTF_8) } catch (e: Throwable) { log.warn("Error", e) } } codeblocks.forEach { codeBlock -> - val header = headers.lastOrNull { it.first.endInclusive < codeBlock.first.start } + val header = headers.lastOrNull { it.first.last < codeBlock.first.first } val filename = resolve(root, header?.second ?: "Unknown") val filepath: Path? = root.resolve(filename) val codeValue = codeBlock.second.groupValues[2] diff --git a/webui/src/main/kotlin/com/simiacryptus/diff/IterativePatchUtil.kt b/webui/src/main/kotlin/com/simiacryptus/diff/IterativePatchUtil.kt index 45dfb2d8..10a82c87 100644 --- a/webui/src/main/kotlin/com/simiacryptus/diff/IterativePatchUtil.kt +++ b/webui/src/main/kotlin/com/simiacryptus/diff/IterativePatchUtil.kt @@ -2,8 +2,10 @@ package com.simiacryptus.diff +import com.simiacryptus.diff.IterativePatchUtil.LineType.* import org.apache.commons.text.similarity.LevenshteinDistance import org.slf4j.LoggerFactory +import kotlin.math.floor import kotlin.math.max import kotlin.math.min @@ -24,20 +26,20 @@ object IterativePatchUtil { var previousLine: LineRecord? = null, var nextLine: LineRecord? = null, var matchingLine: LineRecord? = null, - var type: LineType = LineType.CONTEXT, + var type: LineType = CONTEXT, var metrics: LineMetrics = LineMetrics() ) { override fun toString(): String { val sb = StringBuilder() sb.append("${index.toString().padStart(5, ' ')}: ") when (type) { - LineType.CONTEXT -> sb.append(" ") - LineType.ADD -> sb.append("+") - LineType.DELETE -> sb.append("-") + CONTEXT -> sb.append(" ") + ADD -> sb.append("+") + DELETE -> sb.append("-") } sb.append(" ") sb.append(line) - sb.append(" [({:${metrics.parenthesesDepth}, [:${metrics.squareBracketsDepth}, {:${metrics.curlyBracesDepth}]") + sb.append(" (${metrics.parenthesesDepth})[${metrics.squareBracketsDepth}]{${metrics.curlyBracesDepth}}") return sb.toString() } @@ -66,30 +68,25 @@ object IterativePatchUtil { } - /** - * Generates an optimal patch by comparing two code files. - * @param oldCode The original code. - * @param newCode The new code. - * @return The generated patch as a string. - */ fun generatePatch(oldCode: String, newCode: String): String { log.info("Starting patch generation process") val sourceLines = parseLines(oldCode) val newLines = parseLines(newCode) - link(sourceLines, newLines) + link(sourceLines, newLines, null) log.debug("Parsed and linked source lines: ${sourceLines.size}, new lines: ${newLines.size}") - val diff1 = diffLines(sourceLines, newLines) - val diff = truncateContext(diff1).toMutableList() - fixPatchLineOrder(diff) - annihilateNoopLinePairs(diff) - log.debug("Generated diff with ${diff.size} lines after processing") + markMovedLines(newLines) + val longDiff = newToPatch(newLines) + val shortDiff = truncateContext(longDiff).toMutableList() + fixPatchLineOrder(shortDiff) + annihilateNoopLinePairs(shortDiff) + log.debug("Generated diff with ${shortDiff.size} lines after processing") val patch = StringBuilder() // Generate the patch text - diff.forEach { line -> + shortDiff.forEach { line -> when (line.type) { - LineType.CONTEXT -> patch.append(" ${line.line}\n") - LineType.ADD -> patch.append("+ ${line.line}\n") - LineType.DELETE -> patch.append("- ${line.line}\n") + CONTEXT -> patch.append(" ${line.line}\n") + ADD -> patch.append("+ ${line.line}\n") + DELETE -> patch.append("- ${line.line}\n") } } log.info("Patch generation completed") @@ -108,7 +105,7 @@ object IterativePatchUtil { val sourceLines = parseLines(source) var patchLines = parsePatchLines(patch) log.debug("Parsed source lines: ${sourceLines.size}, initial patch lines: ${patchLines.size}") - link(sourceLines, patchLines) + link(sourceLines, patchLines, LevenshteinDistance()) // Filter out empty lines in the patch patchLines = patchLines.filter { it.line?.let { normalizeLine(it).isEmpty() } == false } @@ -126,11 +123,11 @@ object IterativePatchUtil { log.debug("Starting annihilation of no-op line pairs") val toRemove = mutableListOf>() var i = 0 - while (i < diff.size-1) { - if (diff[i].type == LineType.DELETE) { + while (i < diff.size - 1) { + if (diff[i].type == DELETE) { var j = i + 1 - while (j < diff.size && diff[j].type != LineType.CONTEXT) { - if (diff[j].type == LineType.ADD && + while (j < diff.size && diff[j].type != CONTEXT) { + if (diff[j].type == ADD && normalizeLine(diff[i].line ?: "") == normalizeLine(diff[j].line ?: "") ) { toRemove.add(Pair(i, j)) @@ -146,54 +143,98 @@ object IterativePatchUtil { log.debug("Removed ${toRemove.size} no-op line pairs") } - private fun diffLines( - sourceLines: List, + private fun markMovedLines(newLines: List) { + log.debug("Starting to mark moved lines") + // We start with the first line of the new (patched) code + var newLine = newLines.firstOrNull() + // We'll iterate through all lines of the new code + while (null != newLine) { + try { + // We only process lines that have a matching line in the source code + if (newLine.matchingLine != null) { + // Get the next line in the new code + var nextNewLine = newLine.nextLine ?: break + try { + // Skip any lines that don't have a match or are additions + // This helps us find the next "anchor" point in the new code + while (nextNewLine.matchingLine == null || nextNewLine.type == ADD) { + nextNewLine = nextNewLine.nextLine ?: break + } + if(nextNewLine.matchingLine == null || nextNewLine.type == ADD) break + // Get the corresponding line in the source code + val sourceLine = newLine.matchingLine!! + log.debug("Processing patch line ${newLine.index} with matching source line ${sourceLine.index}") + // Find the next line in the source code + var nextSourceLine = sourceLine.nextLine ?: continue + // Skip any lines in the source that don't have a match or are deletions + // This helps us find the next "anchor" point in the source code + while (nextSourceLine.matchingLine == null || nextSourceLine.type == DELETE) { + nextSourceLine = nextSourceLine.nextLine ?: break + } + if(nextSourceLine.matchingLine == null || nextSourceLine.type == DELETE) break + // If the next matching lines in source and new don't correspond, + // it means there's a moved block of code + while (nextNewLine.matchingLine != nextSourceLine) { + if (nextSourceLine.matchingLine != null) { + // Mark the line in the new code as an addition + nextSourceLine.type = DELETE + // Mark the corresponding line in the source code as a deletion + nextSourceLine.matchingLine!!.type = ADD + log.debug("Marked moved line: Patch[${nextSourceLine.index}] as ADD, Source[${nextSourceLine.matchingLine!!.index}] as DELETE") + } + // Move to the next line in the new code + nextSourceLine = nextSourceLine.nextLine ?: break + // Skip any lines that don't have a match or are additions + while (nextSourceLine.matchingLine == null || nextSourceLine.type == DELETE) { + nextSourceLine = nextSourceLine.nextLine ?: continue + } + } + } finally { + // Move to the next line to process in the outer loop + newLine = nextNewLine + } + } else { + // If the current line doesn't have a match, move to the next one + newLine = newLine.nextLine + } + } catch (e: Exception) { + log.error("Error marking moved lines", e) + } + } + // At this point, we've marked all moved lines in both the source and new code + log.debug("Finished marking moved lines") + } + + private fun newToPatch( newLines: List ): MutableList { val diff = mutableListOf() log.debug("Starting diff generation") - var sourceIndex = 0 - var newIndex = 0 // Generate raw patch without limited context windows - while (sourceIndex < sourceLines.size || newIndex < newLines.size) { + var newLine = newLines.firstOrNull() + while (newLine != null) { + val sourceLine = newLine.matchingLine when { - sourceIndex >= sourceLines.size -> { - // Add remaining new lines - diff.add(newLines[newIndex].copy(type = LineType.ADD)) - newIndex++ - } - - newIndex >= newLines.size -> { - // Delete remaining source lines - diff.add(sourceLines[sourceIndex].copy(type = LineType.DELETE)) - sourceIndex++ - } - - sourceLines[sourceIndex].matchingLine == newLines[newIndex] && - normalizeLine(sourceLines[sourceIndex].line ?: "") == normalizeLine( - newLines[newIndex].line ?: "" - ) -> { - // Lines match, add as context - diff.add(sourceLines[sourceIndex].copy(type = LineType.CONTEXT)) - sourceIndex++ - newIndex++ - } - - sourceLines[sourceIndex].matchingLine == null || - normalizeLine(sourceLines[sourceIndex].line ?: "") != normalizeLine( - newLines[newIndex].line ?: "" - ) -> { - // Source line has no match, it's a deletion - diff.add(sourceLines[sourceIndex].copy(type = LineType.DELETE)) - sourceIndex++ + sourceLine == null || newLine.type == ADD -> { + diff.add(LineRecord(newLine.index, newLine.line, type = ADD)) + log.debug("Added ADD line: ${newLine.line}") } else -> { - // New line has no match in source, it's an addition - diff.add(newLines[newIndex].copy(type = LineType.ADD)) - newIndex++ + // search for prior, unlinked source lines + var priorSourceLine = sourceLine.previousLine + val lineBuffer = mutableListOf() + while (priorSourceLine != null && (priorSourceLine.matchingLine == null || priorSourceLine?.type == DELETE)) { + // Note the deletion of the prior source line + lineBuffer.add(LineRecord(-1, priorSourceLine.line, type = DELETE)) + priorSourceLine = priorSourceLine.previousLine + } + diff.addAll(lineBuffer.reversed()) + diff.add(LineRecord(newLine.index, newLine.line, type = CONTEXT)) + log.debug("Added CONTEXT line: ${sourceLine.line}") } } + newLine = newLine.nextLine } log.debug("Generated diff with ${diff.size} lines") return diff @@ -203,46 +244,39 @@ object IterativePatchUtil { val contextSize = 3 // Number of context lines before and after changes log.debug("Truncating context with size $contextSize") val truncatedDiff = mutableListOf() - var inChange = false val contextBuffer = mutableListOf() - var lastChangeIndex = -1 for (i in diff.indices) { val line = diff[i] when { - line.type != LineType.CONTEXT -> { - if (!inChange) { - // Start of a change, add buffered context + line.type != CONTEXT -> { + // Start of a change, add buffered context + if(contextSize*2 < contextBuffer.size) { + if(truncatedDiff.isNotEmpty()) { + truncatedDiff.addAll(contextBuffer.take(contextSize)) + truncatedDiff.add(LineRecord(-1, "...", type = CONTEXT)) + } truncatedDiff.addAll(contextBuffer.takeLast(contextSize)) - contextBuffer.clear() - } - truncatedDiff.add(line) - inChange = true - lastChangeIndex = i - } - - inChange -> { - contextBuffer.add(line) - if (contextBuffer.size == contextSize) { - // End of a change, add buffered context + } else { truncatedDiff.addAll(contextBuffer) - contextBuffer.clear() - inChange = false } + contextBuffer.clear() + truncatedDiff.add(line) } else -> { contextBuffer.add(line) - if (contextBuffer.size > contextSize) { - contextBuffer.removeAt(0) - } } } } - // Add trailing context after the last change - if (lastChangeIndex != -1) { - val trailingContext = diff.subList(lastChangeIndex + 1, min(diff.size, lastChangeIndex + 1 + contextSize)) - truncatedDiff.addAll(trailingContext) + if(truncatedDiff.isEmpty()) { + return truncatedDiff } + if(contextSize < contextBuffer.size) { + truncatedDiff.addAll(contextBuffer.take(contextSize)) + } else { + truncatedDiff.addAll(contextBuffer) + } + // Add trailing context after the last change log.debug("Truncated diff size: ${truncatedDiff.size}") return truncatedDiff } @@ -258,7 +292,8 @@ object IterativePatchUtil { private fun link( sourceLines: List, - patchLines: List + patchLines: List, + levenshteinDistance: LevenshteinDistance? ) { // Step 1: Link all unique lines in the source and patch that match exactly log.info("Step 1: Linking unique matching lines") @@ -266,16 +301,17 @@ object IterativePatchUtil { // Step 2: Link all exact matches in the source and patch which are adjacent to established links log.info("Step 2: Linking adjacent matching lines") - linkAdjacentMatchingLines(sourceLines) + linkAdjacentMatchingLines(sourceLines, levenshteinDistance) log.info("Step 3: Performing subsequence linking") - subsequenceLinking(sourceLines, patchLines) + subsequenceLinking(sourceLines, patchLines, levenshteinDistance = levenshteinDistance) } private fun subsequenceLinking( sourceLines: List, patchLines: List, - depth: Int = 0 + depth: Int = 0, + levenshteinDistance: LevenshteinDistance? ) { log.debug("Subsequence linking at depth $depth") if (depth > 10 || sourceLines.isEmpty() || patchLines.isEmpty()) { @@ -285,15 +321,14 @@ object IterativePatchUtil { val patchSegment = patchLines.filter { it.matchingLine == null } if (sourceSegment.isNotEmpty() && patchSegment.isNotEmpty()) { var matchedLines = linkUniqueMatchingLines(sourceSegment, patchSegment) - matchedLines += linkAdjacentMatchingLines(sourceSegment) + matchedLines += linkAdjacentMatchingLines(sourceSegment, levenshteinDistance) if (matchedLines == 0) { matchedLines += matchFirstBrackets(sourceSegment, patchSegment) } if (matchedLines > 0) { - subsequenceLinking(sourceSegment, patchSegment, depth + 1) + subsequenceLinking(sourceSegment, patchSegment, depth + 1, levenshteinDistance) } log.debug("Matched $matchedLines lines in subsequence linking at depth $depth") - } } @@ -304,137 +339,89 @@ object IterativePatchUtil { log.debug("Starting to generate patched text") val patchedText: MutableList = mutableListOf() val usedPatchLines = mutableSetOf() - var sourceIndex = 0 + var sourceIndex = -1 var lastMatchedPatchIndex = -1 - while (sourceIndex < sourceLines.size) { - val codeLine = sourceLines[sourceIndex] + while (sourceIndex < sourceLines.size - 1) { + val codeLine = sourceLines[++sourceIndex] when { - codeLine.matchingLine?.type == LineType.DELETE -> { + codeLine.matchingLine?.type == DELETE -> { val patchLine = codeLine.matchingLine!! - var patchIndex = patchLines.indexOf(patchLine) log.debug("Deleting line: {}", codeLine) - updateContext(lastMatchedPatchIndex, patchIndex, patchLines, usedPatchLines, patchedText) - patchIndex = checkBeforeForInserts(sourceIndex, sourceLines, usedPatchLines, patchedText) - // Delete the line -- do not add to patched text - - patchIndex = patchLines.indexOf(patchLine) - patchIndex = checkAfterForInserts(patchIndex, patchLines, usedPatchLines, patchedText) - usedPatchLines.add(patchLine) - lastMatchedPatchIndex = patchIndex - sourceIndex++ + checkAfterForInserts(patchLine, usedPatchLines, patchedText) + lastMatchedPatchIndex = patchLine.index } codeLine.matchingLine != null -> { - val patchLine = codeLine.matchingLine!! - var patchIndex = patchLines.indexOf(patchLine) + val patchLine: LineRecord = codeLine.matchingLine!! log.debug("Patching line: {} <-> {}", codeLine, patchLine) - // Add context lines between last match and current match - updateContext(lastMatchedPatchIndex, patchIndex, patchLines, usedPatchLines, patchedText) - patchIndex = checkBeforeForInserts(patchIndex, patchLines, usedPatchLines, patchedText) - - patchedText.add(patchLine.line ?: "") // Add the patched line - - patchIndex = patchLines.indexOf(patchLine) - patchIndex = checkAfterForInserts(patchIndex, patchLines, usedPatchLines, patchedText) - + checkBeforeForInserts(patchLine, usedPatchLines, patchedText) usedPatchLines.add(patchLine) - lastMatchedPatchIndex = patchIndex - sourceIndex++ + patchedText.add(patchLine.line ?: "") // Add the patched line + checkAfterForInserts(patchLine, usedPatchLines, patchedText) + lastMatchedPatchIndex = patchLine.index } else -> { - // Check if this line is a context line in the patch - val contextPatchLine = patchLines.find { it.type == LineType.CONTEXT && it.line == codeLine.line } - if (contextPatchLine != null) { - log.debug("Added context line: {}", codeLine) - patchedText.add(contextPatchLine.line ?: "") - usedPatchLines.add(contextPatchLine) - } else { - log.debug("Added unmatched source line: {}", codeLine) - patchedText.add(codeLine.line ?: "") - } - sourceIndex++ + log.debug("Added unmatched source line: {}", codeLine) + patchedText.add(codeLine.line ?: "") } } } - // Add remaining context lines after the last match - if (lastMatchedPatchIndex != -1) { - for (i in lastMatchedPatchIndex + 1 until patchLines.size) { - val contextLine = patchLines[i] - if (contextLine.type == LineType.CONTEXT && !usedPatchLines.contains(contextLine)) { - patchedText.add(contextLine.line ?: "") - usedPatchLines.add(contextLine) - } + if (lastMatchedPatchIndex == -1) patchLines.filter { it.type == ADD && !usedPatchLines.contains(it) } + .forEach { line -> + log.debug("Added patch line: {}", line) + patchedText.add(line.line ?: "") } - } - // Add any remaining unused ADD lines from the patch - patchLines.filter { it.type == LineType.ADD && !usedPatchLines.contains(it) }.forEach { line -> - log.debug("Added remaining patch line: {}", line) - patchedText.add(line.line ?: "") - } log.debug("Generated patched text with ${patchedText.size} lines") return patchedText } - private fun updateContext( - lastMatchedPatchIndex: Int, - patchIndex: Int, - patchLines: List, - usedPatchLines: MutableSet, - patchedText: MutableList - ) { - if (lastMatchedPatchIndex != -1) { - for (i in lastMatchedPatchIndex + 1 until patchIndex) { - val contextLine = patchLines[i] - if (contextLine.type == LineType.CONTEXT && !usedPatchLines.contains(contextLine)) { - patchedText.add(contextLine.line ?: "") - usedPatchLines.add(contextLine) - } - } - } - } - - private fun checkAfterForInserts( - patchIndex: Int, - patchLines: List, + private fun checkBeforeForInserts( + patchLine: LineRecord, usedPatchLines: MutableSet, patchedText: MutableList - ): Int { - var patchIndex1 = patchIndex - while (patchIndex1 < patchLines.size - 1) { - val nextPatchLine = patchLines[++patchIndex1] - if (nextPatchLine.type == LineType.ADD && !usedPatchLines.contains(nextPatchLine)) { - log.debug("Added unmatched patch line: {}", nextPatchLine) - patchedText.add(nextPatchLine.line ?: "") - usedPatchLines.add(nextPatchLine) - } else { + ): LineRecord? { + val buffer = mutableListOf() + var prevPatchLine = patchLine.previousLine + while (null != prevPatchLine) { + if (prevPatchLine.type != ADD || usedPatchLines.contains(prevPatchLine)) { break } + + log.debug("Added unmatched patch line: {}", prevPatchLine) + buffer.add(prevPatchLine.line ?: "") + usedPatchLines.add(prevPatchLine) + prevPatchLine = prevPatchLine.previousLine } - return patchIndex1 + patchedText.addAll(buffer.reversed()) + return prevPatchLine } - private fun checkBeforeForInserts( - patchIndex: Int, - patchLines: List, + private fun checkAfterForInserts( + patchLine: LineRecord, usedPatchLines: MutableSet, patchedText: MutableList - ): Int { - var patchIndex1 = patchIndex - while (patchIndex1 > 0) { - val prevPatchLine = patchLines[--patchIndex1] - if (prevPatchLine.type == LineType.ADD && !usedPatchLines.contains(prevPatchLine)) { - log.debug("Added unmatched patch line: {}", prevPatchLine) - patchedText.add(prevPatchLine.line ?: "") - usedPatchLines.add(prevPatchLine) - } else { - break + ): LineRecord { + var nextPatchLine = patchLine.nextLine + while (null != nextPatchLine) { + while (nextPatchLine != null && ( + normalizeLine(nextPatchLine.line ?: "").isEmpty() || + (nextPatchLine.matchingLine == null && nextPatchLine.type == CONTEXT) + )) { + nextPatchLine = nextPatchLine.nextLine } + if (nextPatchLine == null) break + if (nextPatchLine.type != ADD) break + if (usedPatchLines.contains(nextPatchLine)) break + log.debug("Added unmatched patch line: {}", nextPatchLine) + patchedText.add(nextPatchLine.line ?: "") + usedPatchLines.add(nextPatchLine) + nextPatchLine = nextPatchLine.nextLine } - return patchIndex1 + return nextPatchLine ?: patchLine } private fun matchFirstBrackets(sourceLines: List, patchLines: List): Int { @@ -449,7 +436,7 @@ object IterativePatchUtil { it.line?.lineMetrics() != LineMetrics() }.filter { when (it.type) { - LineType.ADD -> false // ADD lines are not matched to source lines + ADD -> false // ADD lines are not matched to source lines else -> true } }.groupBy { normalizeLine(it.line!!) } @@ -483,7 +470,7 @@ object IterativePatchUtil { // Group patch lines by their normalized content, excluding ADD lines val patchLineMap = patchLines.filter { when (it.type) { - LineType.ADD -> false // ADD lines are not matched to source lines + ADD -> false // ADD lines are not matched to source lines else -> true } }.groupBy { normalizeLine(it.line!!) } @@ -511,11 +498,10 @@ object IterativePatchUtil { * Links lines that are adjacent to already linked lines and match exactly. * @param sourceLines The source lines with some established links. */ - private fun linkAdjacentMatchingLines(sourceLines: List): Int { + private fun linkAdjacentMatchingLines(sourceLines: List, levenshtein: LevenshteinDistance?): Int { log.debug("Starting to link adjacent matching lines. Source lines: ${sourceLines.size}") var foundMatch = true var matchedLines = 0 - val levenshteinDistance = LevenshteinDistance() // Continue linking until no more matches are found while (foundMatch) { log.debug("Starting new iteration to find adjacent matches") @@ -523,20 +509,24 @@ object IterativePatchUtil { for (sourceLine in sourceLines) { val patchLine = sourceLine.matchingLine ?: continue // Skip if there's no matching line - var patchPrev = patchLine.previousLine ?: continue - while (patchPrev.previousLine != null && - (patchPrev.type == LineType.ADD || normalizeLine(patchPrev.line ?: "").isEmpty()) + var patchPrev = patchLine.previousLine + while (patchPrev?.previousLine != null && + (patchPrev.type == ADD || normalizeLine(patchPrev.line ?: "").isEmpty()) ) { + require(patchPrev !== patchPrev.previousLine) patchPrev = patchPrev.previousLine!! } - var sourcePrev = sourceLine.previousLine ?: continue - while (sourcePrev.previousLine != null && (normalizeLine(sourcePrev.line ?: "").isEmpty())) { + var sourcePrev = sourceLine.previousLine + while (sourcePrev?.previousLine != null && (normalizeLine(sourcePrev.line ?: "").isEmpty())) { + require(sourcePrev !== sourcePrev.previousLine) sourcePrev = sourcePrev.previousLine!! } - if (sourcePrev.matchingLine == null && patchPrev.matchingLine == null) { // Skip if there's already a match - if (isMatch(sourcePrev, patchPrev, levenshteinDistance)) { // Check if the lines match exactly + if (sourcePrev != null && sourcePrev.matchingLine == null && + patchPrev != null && patchPrev.matchingLine == null + ) { // Skip if there's already a match + if (isMatch(sourcePrev, patchPrev, levenshtein)) { // Check if the lines match exactly sourcePrev.matchingLine = patchPrev patchPrev.matchingLine = sourcePrev foundMatch = true @@ -545,20 +535,24 @@ object IterativePatchUtil { } } - var patchNext = patchLine.nextLine ?: continue - while (patchNext.nextLine != null && - (patchNext.type == LineType.ADD || normalizeLine(patchNext.line ?: "").isEmpty()) + var patchNext = patchLine.nextLine + while (patchNext?.nextLine != null && + (patchNext.type == ADD || normalizeLine(patchNext.line ?: "").isEmpty()) ) { + require(patchNext !== patchNext.nextLine) patchNext = patchNext.nextLine!! } - var sourceNext = sourceLine.nextLine ?: continue - while (sourceNext.nextLine != null && (normalizeLine(sourceNext.line ?: "").isEmpty())) { + var sourceNext = sourceLine.nextLine + while (sourceNext?.nextLine != null && (normalizeLine(sourceNext.line ?: "").isEmpty())) { + require(sourceNext !== sourceNext.nextLine) sourceNext = sourceNext.nextLine!! } - if (sourceNext.matchingLine == null && patchNext.matchingLine == null) { - if (isMatch(sourceNext, patchNext, levenshteinDistance)) { + if (sourceNext != null && sourceNext.matchingLine == null && + patchNext != null && patchNext.matchingLine == null + ) { + if (isMatch(sourceNext, patchNext, levenshtein)) { sourceNext.matchingLine = patchNext patchNext.matchingLine = sourceNext foundMatch = true @@ -575,14 +569,16 @@ object IterativePatchUtil { private fun isMatch( sourcePrev: LineRecord, patchPrev: LineRecord, - levenshteinDistance: LevenshteinDistance + levenshteinDistance: LevenshteinDistance? ): Boolean { - var isMatch = normalizeLine(sourcePrev.line!!) == normalizeLine(patchPrev.line!!) - val length = max(sourcePrev.line!!.length, patchPrev.line!!.length) - if (!isMatch && length > 5) { // Check if the lines are similar using Levenshtein distance - val distance = levenshteinDistance.apply(sourcePrev.line, patchPrev.line) + val normalizeLineSource = normalizeLine(sourcePrev.line!!) + val normalizeLinePatch = normalizeLine(patchPrev.line!!) + var isMatch = normalizeLineSource == normalizeLinePatch + val length = max(normalizeLineSource.length, normalizeLinePatch.length) + if (!isMatch && length > 5 && null != levenshteinDistance) { // Check if the lines are similar using Levenshtein distance + val distance = levenshteinDistance.apply(normalizeLineSource, normalizeLinePatch) log.debug("Levenshtein distance: $distance") - isMatch = distance <= (length / 3) + isMatch = distance <= floor(length / 4.0).toInt() } return isMatch } @@ -608,8 +604,14 @@ object IterativePatchUtil { private fun setLinks(list: List): List { log.debug("Starting to set links for ${list.size} lines") for (i in list.indices) { - list[i].previousLine = if (i > 0) list[i - 1] else null - list[i].nextLine = if (i < list.size - 1) list[i + 1] else null + list[i].previousLine = if (i <= 0) null else { + require(list[i - 1] !== list[i]) + list[i - 1] + } + list[i].nextLine = if (i >= list.size - 1) null else { + require(list[i + 1] !== list[i]) + list[i + 1] + } } log.debug("Finished setting links for ${list.size} lines") return list @@ -636,9 +638,9 @@ object IterativePatchUtil { } }, type = when { - line.startsWith("+") -> LineType.ADD - line.startsWith("-") -> LineType.DELETE - else -> LineType.CONTEXT + line.startsWith("+") -> ADD + line.startsWith("-") -> DELETE + else -> CONTEXT } ) }.filter { it.line != null }).toMutableList() @@ -657,17 +659,27 @@ object IterativePatchUtil { do { swapped = false for (i in 0 until patchLines.size - 1) { - if (patchLines[i].type == LineType.ADD && patchLines[i + 1].type == LineType.DELETE) { + if (patchLines[i].type == ADD && patchLines[i + 1].type == DELETE) { swapped = true - val deleteLine = patchLines[i] - val addLine = patchLines[i + 1] + val addLine = patchLines[i] + val deleteLine = patchLines[i + 1] // Swap records and update pointers + val nextLine = deleteLine.nextLine + val previousLine = addLine.previousLine + + require(addLine !== deleteLine) + if (previousLine === deleteLine) { + throw RuntimeException("previousLine === deleteLine") + } + require(previousLine !== deleteLine) + require(nextLine !== addLine) + require(nextLine !== deleteLine) deleteLine.nextLine = addLine addLine.previousLine = deleteLine - deleteLine.previousLine = addLine.previousLine - addLine.nextLine = deleteLine.nextLine - patchLines[i] = addLine - patchLines[i + 1] = deleteLine + deleteLine.previousLine = previousLine + addLine.nextLine = nextLine + patchLines[i] = deleteLine + patchLines[i + 1] = addLine } } } while (swapped) @@ -704,7 +716,7 @@ object IterativePatchUtil { log.debug("Finished calculating line metrics") } - fun String.lineMetrics(): LineMetrics { + private fun String.lineMetrics(): LineMetrics { var parenthesesDepth = 0 var squareBracketsDepth = 0 var curlyBracesDepth = 0 diff --git a/webui/src/main/kotlin/com/simiacryptus/skyenet/Discussable.kt b/webui/src/main/kotlin/com/simiacryptus/skyenet/Discussable.kt index f2e11272..5d11d26f 100644 --- a/webui/src/main/kotlin/com/simiacryptus/skyenet/Discussable.kt +++ b/webui/src/main/kotlin/com/simiacryptus/skyenet/Discussable.kt @@ -64,7 +64,7 @@ ${ tabContent?.append("\n" + feedbackForm.placeholder) task.complete() } catch (e: Throwable) { - log.error("Error in main function", e) + log.error("Error in discussable", e) task.error(ui, e) task.complete(ui.hrefLink("🔄 Retry") { main(tabIndex = tabIndex, task = task) @@ -200,11 +200,17 @@ ${ val newTask = ui.newTask(false) val header = newTask.header("Processing...") tabs[tabs.label(idx)] = newTask.placeholder - main(idx, newTask) - //tabs.selectedTab = idx - header?.clear() - newTask.complete() - semaphore.acquire() + try { + main(idx, newTask) + //tabs.selectedTab = idx + semaphore.acquire() + } catch (e: Throwable) { + log.error("Error in main function", e) + task.error(ui, e) + } finally { + header?.clear() + newTask.complete() + } log.info("Returning result from Discussable") return atomicRef.get() } catch (e: Exception) { diff --git a/webui/src/main/kotlin/com/simiacryptus/skyenet/Retryable.kt b/webui/src/main/kotlin/com/simiacryptus/skyenet/Retryable.kt index 58d3237e..ff2d167b 100644 --- a/webui/src/main/kotlin/com/simiacryptus/skyenet/Retryable.kt +++ b/webui/src/main/kotlin/com/simiacryptus/skyenet/Retryable.kt @@ -14,7 +14,9 @@ open class Retryable( } open fun init() { - set(label(size), process(container)) + val tabLabel = label(size) + set(tabLabel, SessionTask.spinner) + set(tabLabel, process(container)) } override fun renderTabButtons(): String = """ diff --git a/webui/src/main/kotlin/com/simiacryptus/skyenet/TabbedDisplay.kt b/webui/src/main/kotlin/com/simiacryptus/skyenet/TabbedDisplay.kt index d06f1625..352ee890 100644 --- a/webui/src/main/kotlin/com/simiacryptus/skyenet/TabbedDisplay.kt +++ b/webui/src/main/kotlin/com/simiacryptus/skyenet/TabbedDisplay.kt @@ -14,7 +14,7 @@ open class TabbedDisplay( } val size: Int get() = tabs.size - open fun render() = """ + open fun render() = if(tabs.isEmpty()) "" else """
${renderTabButtons()} ${ diff --git a/webui/src/main/resources/application/favicon.svg b/webui/src/main/resources/application/favicon.svg index 6a09be9f..32b29bf6 100644 --- a/webui/src/main/resources/application/favicon.svg +++ b/webui/src/main/resources/application/favicon.svg @@ -194,7 +194,8 @@ C187.4,151.4,187.3,151.4,187.2,151.4z"/> - + @@ -436,7 +437,8 @@ c-4.1-2.3-1-4.4,0.7-6.2c12.5-12.6,25.1-25.2,37.6-37.8C181.5,156.1,182.7,154.5,184.3,152.8z"/> - + - + - + @@ -528,7 +532,8 @@ - + @@ -572,7 +577,8 @@ - + - + - + - - + - + - - - + + + - + - - + + - + - + @@ -689,8 +708,10 @@ c0.7-0.2,1.4-0.3,2-0.5c-0.6-4.1-1.1-8.2-1.7-12.3c-0.3,0.1-0.5,0.1-0.8,0.2C345.3,172.4,345.1,174.7,344.5,177.7z"/> - - + + { diff --git a/webui/src/main/resources/application/index.html b/webui/src/main/resources/application/index.html index 088e3fb3..11c466cf 100644 --- a/webui/src/main/resources/application/index.html +++ b/webui/src/main/resources/application/index.html @@ -16,10 +16,10 @@ rel="stylesheet"/> - - - - + + + + diff --git a/webui/src/main/resources/application/main.js b/webui/src/main/resources/application/main.js index 5fd70ad4..90ea7099 100644 --- a/webui/src/main/resources/application/main.js +++ b/webui/src/main/resources/application/main.js @@ -1,4 +1,16 @@ import {connect, queueMessage} from './chat.js'; +import { + applyToAllSvg, + closeModal, + findAncestor, + getSessionId, + refreshReplyForms, + refreshVerbose, + showModal, + substituteMessages, + toggleVerbose +} from './functions.js'; +import {restoreTabs, updateTabs} from './tabs.js'; let messageVersions = {}; window.messageMap = {}; // Make messageMap global @@ -8,6 +20,55 @@ let loadImages = "true"; let showMenubar = true; let messageDiv; +// Add debounce function +function debounce(func, wait) { + let timeout; + return function executedFunction(...args) { + const later = () => { + clearTimeout(timeout); + func(...args); + }; + clearTimeout(timeout); + timeout = setTimeout(later, wait); + }; +} + +// Create a debounced version of updateDocumentComponents +const debouncedUpdateDocumentComponents = debounce(updateDocumentComponents, 250); + +function updateDocumentComponents() { + try { + if (typeof Prism !== 'undefined') Prism.highlightAll(); + } catch (e) { + console.log("Error highlighting code: " + e); + } + try { + refreshVerbose(); + } catch (e) { + console.log("Error refreshing verbose: " + e); + } + try { + refreshReplyForms() + } catch (e) { + console.log("Error refreshing reply forms: " + e); + } + try { + if (typeof mermaid !== 'undefined') mermaid.run(); + } catch (e) { + console.log("Error running mermaid: " + e); + } + try { + applyToAllSvg(); + } catch (e) { + console.log("Error applying SVG pan zoom: " + e); + } + try { + updateTabs(); + } catch (e) { + console.log("Error updating tabs: " + e); + } +} + function onWebSocketText(event) { console.debug('WebSocket message:', event); const messagesDiv = document.getElementById('messages'); @@ -25,19 +86,19 @@ function onWebSocketText(event) { if (messageDiv) { messageDiv.innerHTML = messageContent; substituteMessages(messageId, messageDiv); + //requestAnimationFrame(() => updateNestedTabs(messageDiv)); } }); - if (messageDivs.length == 0 && !messageId.startsWith("z")) { + if (messageDivs.length === 0 && !messageId.startsWith("z")) { messageDiv = document.createElement('div'); messageDiv.className = 'message message-container ' + (messageId.startsWith('u') ? 'user-message' : 'response-message'); messageDiv.id = messageId; messageDiv.innerHTML = messageContent; if (messagesDiv) messagesDiv.appendChild(messageDiv); substituteMessages(messageId, messageDiv); - - // Scroll to the new message with smooth behavior - messageDiv.scrollIntoView({behavior: 'smooth', block: 'end'}); + //requestAnimationFrame(() => updateNestedTabs(messageDiv)); } + if (messagesDiv) messagesDiv.scrollTop = messagesDiv.scrollHeight; if (singleInput) { const mainInput = document.getElementById('main-input'); if (mainInput) { @@ -57,37 +118,7 @@ function onWebSocketText(event) { console.log("Error: Could not find .main-input"); } } - if (messagesDiv) messagesDiv.scrollTop = messagesDiv.scrollHeight; - try { - if (typeof Prism !== 'undefined') Prism.highlightAll(); - } catch (e) { - console.log("Error highlighting code: " + e); - } - try { - refreshVerbose(); - } catch (e) { - console.log("Error refreshing verbose: " + e); - } - try { - refreshReplyForms() - } catch (e) { - console.log("Error refreshing reply forms: " + e); - } - try { - if (typeof mermaid !== 'undefined') mermaid.run(); - } catch (e) { - console.log("Error running mermaid: " + e); - } - try { - applyToAllSvg(); - } catch (e) { - console.log("Error applying SVG pan zoom: " + e); - } - try { - updateTabs(); - } catch (e) { - console.log("Error updating tabs: " + e); - } + debouncedUpdateDocumentComponents(); } document.addEventListener('DOMContentLoaded', () => { @@ -99,33 +130,7 @@ document.addEventListener('DOMContentLoaded', () => { applyToAllSvg(); }, 5000); // Adjust the interval as needed - // Restore the selected tabs from localStorage before adding event listeners - document.querySelectorAll('.tabs-container').forEach(tabsContainer => { - const savedTab = localStorage.getItem(`selectedTab_${tabsContainer.id}`); - if (savedTab) { - const savedButton = tabsContainer.querySelector(`.tab-button[data-for-tab="${savedTab}"]`); - if (savedButton) { - savedButton.classList.add('active'); - const forTab = savedButton.getAttribute('data-for-tab'); - const selectedContent = tabsContainer.querySelector(`.tab-content[data-tab="${forTab}"]`); - if (selectedContent) { - selectedContent.classList.add('active'); - selectedContent.style.display = 'block'; - } - console.log(`Restored saved tab: ${savedTab}`); - } - } - }); - document.querySelectorAll('.tabs-container').forEach(tabsContainer => { - const savedTab = localStorage.getItem(`selectedTab_${tabsContainer.id}`); - if (savedTab) { - const savedButton = tabsContainer.querySelector(`.tab-button[data-for-tab="${savedTab}"]`); - if (savedButton) { - savedButton.click(); - console.log(`Restored saved tab: ${savedTab}`); - } - } - }); + restoreTabs(); const historyElement = document.getElementById('history'); if (historyElement) historyElement.addEventListener('click', () => showModal('sessions')); diff --git a/webui/src/main/resources/application/main.scss b/webui/src/main/resources/application/main.scss index 34cd9293..0d476ca5 100644 --- a/webui/src/main/resources/application/main.scss +++ b/webui/src/main/resources/application/main.scss @@ -1,25 +1,27 @@ @import '../shared/schemes/normal'; @import '../shared/main'; - .cmd-button { - display: inline-block; - padding: 8px 15px; - font-size: 14px; - cursor: pointer; - text-align: center; - text-decoration: none; - outline: none; - color: #fff; - background-color: #4CAF50; - border: none; - border-radius: 5px; - box-shadow: 0 9px #999; - } +.cmd-button { + display: inline-block; + padding: 8px 15px; + font-size: 14px; + cursor: pointer; + text-align: center; + text-decoration: none; + outline: none; + color: #fff; + background-color: #4CAF50; + border: none; + border-radius: 5px; + box-shadow: 0 9px #999; +} - .cmd-button:hover {background-color: #3e8e41} +.cmd-button:hover { + background-color: #3e8e41 +} - .cmd-button:active { - background-color: #3e8e41; - box-shadow: 0 5px #666; - transform: translateY(4px); - } \ No newline at end of file +.cmd-button:active { + background-color: #3e8e41; + box-shadow: 0 5px #666; + transform: translateY(4px); +} \ No newline at end of file diff --git a/webui/src/main/resources/application/tabs.js b/webui/src/main/resources/application/tabs.js index c161c796..df077274 100644 --- a/webui/src/main/resources/application/tabs.js +++ b/webui/src/main/resources/application/tabs.js @@ -2,57 +2,89 @@ const observer = new MutationObserver(updateTabs); const observerOptions = {childList: true, subtree: true}; const tabCache = new Map(); -function updateTabs() { +export function updateTabs() { const tabButtons = document.querySelectorAll('.tab-button'); + const tabsContainers = new Set(); tabButtons.forEach(button => { + const tabsContainer = button.closest('.tabs-container'); + tabsContainers.add(tabsContainer); if (button.hasListener) return; button.hasListener = true; - // console.log(`Adding click event listener to tab button: ${button.getAttribute('data-for-tab')}`); + // console.log(`Adding click event listener to tab button: ${button.getAttribute('data-for-tab')}, button element:`, button); button.addEventListener('click', (event) => { - // console.log(`Tab button clicked: ${button.getAttribute('data-for-tab')}`); + // console.log(`Tab button clicked: ${button.getAttribute('data-for-tab')}, event:`, event); event.stopPropagation(); const forTab = button.getAttribute('data-for-tab'); const tabsContainerId = button.closest('.tabs-container').id; - // console.log(`Tabs container ID: ${tabsContainerId}`); - // console.log(`Saving selected tab to localStorage: selectedTab_${tabsContainerId} = ${forTab}`); + // console.log(`Tabs container ID: ${tabsContainerId}, button:`, button); + // console.log(`Saving selected tab to localStorage: selectedTab_${tabsContainerId} = ${forTab}, button:`, button); try { localStorage.setItem(`selectedTab_${tabsContainerId}`, forTab); + tabCache.set(tabsContainerId, forTab); // Update the cache } catch (e) { console.warn('Failed to save tab state to localStorage:', e); } let tabsParent = button.closest('.tabs-container'); const tabButtons = tabsParent.querySelectorAll('.tab-button'); - for (let i = 0; i < tabButtons.length; i++) { - if (tabButtons[i].closest('.tabs-container') === tabsParent) { - tabButtons[i].classList.remove('active'); + tabButtons.forEach(btn => { + if (btn.closest('.tabs-container') === tabsParent) { + btn.classList.remove('active'); } - } + }); button.classList.add('active'); - // console.log(`Active tab set to: ${forTab}`); + // console.log(`Active tab set to: ${forTab}, button:`, button); let selectedContent = null; const tabContents = tabsParent.querySelectorAll('.tab-content'); - for (let i = 0; i < tabContents.length; i++) { - const content = tabContents[i]; - if (content.closest('.tabs-container') !== tabsParent) continue; + tabContents.forEach(content => { + if (content.closest('.tabs-container') !== tabsParent) return; if (content.getAttribute('data-tab') === forTab) { content.classList.add('active'); content.style.display = 'block'; // Ensure the content is displayed - // console.log(`Content displayed for tab: ${forTab}`); + // console.log(`Content displayed for tab: ${forTab}, content element:`, content); selectedContent = content; } else { content.classList.remove('active'); content.style.display = 'none'; // Ensure the content is hidden - // console.log(`Content hidden for tab: ${content.getAttribute('data-tab')}`); + // console.log(`Content hidden for tab: ${content.getAttribute('data-tab')}, content element:`, content); } - } + }); if (selectedContent !== null) { requestAnimationFrame(() => updateNestedTabs(selectedContent)); } }); - // Check if the current button should be activated based on localStorage - const savedTab = getSavedTab(button.closest('.tabs-container').id); - if (button.getAttribute('data-for-tab') === savedTab) { - button.dispatchEvent(new Event('click')); + }); + + // Restore the selected tabs from localStorage + tabsContainers.forEach(tabsContainer => { + const savedTab = getSavedTab(tabsContainer.id); + if (savedTab) { + const savedButton = tabsContainer.querySelector(`.tab-button[data-for-tab="${savedTab}"]`); + if (savedButton) { + savedButton.click(); // Simulate a click to activate the tab + // console.log(`Restored saved tab: ${savedTab}`); + } + } else { + tabsContainer.querySelector('.tab-button')?.click(); // Activate the first tab + } + }); +} + +export function restoreTabs() { + // Restore the selected tabs from localStorage before adding event listeners + document.querySelectorAll('.tabs-container').forEach(tabsContainer => { + const savedTab = localStorage.getItem(`selectedTab_${tabsContainer.id}`); + if (savedTab) { + const savedButton = tabsContainer.querySelector(`.tab-button[data-for-tab="${savedTab}"]`); + if (savedButton) { + savedButton.classList.add('active'); + const forTab = savedButton.getAttribute('data-for-tab'); + const selectedContent = tabsContainer.querySelector(`.tab-content[data-tab="${forTab}"]`); + if (selectedContent) { + selectedContent.classList.add('active'); + selectedContent.style.display = 'block'; + } + console.log(`Restored saved tab: ${savedTab}`); + } } }); } @@ -71,65 +103,47 @@ function getSavedTab(containerId) { } } -function updateNestedTabs(element) { - const tabsContainers = element.querySelectorAll('.tabs-container'); - for (let i = 0; i < tabsContainers.length; i++) { - const tabsContainer = tabsContainers[i]; - try { - // console.log(`Updating nested tabs for container: ${tabsContainer.id}`); - let hasActiveButton = false; - const nestedButtons = tabsContainer.querySelectorAll('.tab-button'); - for (let j = 0; j < nestedButtons.length; j++) { - const nestedButton = nestedButtons[j]; - } - if (nestedButton.classList.contains('active')) { - hasActiveButton = true; - // console.log(`Found active nested button: ${nestedButton.getAttribute('data-for-tab')}`); - } - if (!hasActiveButton) { - /* Determine if a tab-content element in this tabs-container has the active class. If so, use its data-tab value to find the matching button and ensure it is marked active */ - const activeContent = tabsContainer.querySelector('.tab-content.active'); - if (activeContent) { - const activeTab = activeContent.getAttribute('data-tab'); - const activeButton = tabsContainer.querySelector(`.tab-button[data-for-tab="${activeTab}"]`); - if (activeButton !== null) { - activeButton.classList.add('active'); - // console.log(`Set active nested button: ${activeTab}`); - } - } else { - /* Add 'active' to the class list of the first button */ - const firstButton = tabsContainer.querySelector('.tab-button'); - if (firstButton !== null) { - firstButton.classList.add('active'); - // console.log(`Set first nested button as active: ${firstButton.getAttribute('data-for-tab')}`); - } - } - } - const savedTab = getSavedTab(tabsContainer.id); - // console.log(`Retrieved saved tab from localStorage: selectedTab_${tabsContainer.id} = ${savedTab}`); - if (savedTab) { - const savedButton = tabsContainer.querySelector(`.tab-button[data-for-tab="${savedTab}"]`); - if (savedButton) { - savedButton.classList.add('active'); - const forTab = savedButton.getAttribute('data-for-tab'); - const selectedContent = tabsContainer.querySelector(`.tab-content[data-tab="${forTab}"]`); - if (selectedContent) { - selectedContent.classList.add('active'); - selectedContent.style.display = 'block'; + function updateNestedTabs(element) { + const tabsContainers = element.querySelectorAll('.tabs-container'); + tabsContainers.forEach(tabsContainer => { + try { + let hasActiveButton = false; + const nestedButtons = tabsContainer.querySelectorAll('.tab-button'); + nestedButtons.forEach(nestedButton => { + // console.log(`Checking nested button: ${nestedButton.getAttribute('data-for-tab')}, nestedButton element:`, nestedButton); + if (nestedButton.classList.contains('active')) { + hasActiveButton = true; + } + }); + if (!hasActiveButton) { + const activeContent = tabsContainer.querySelector('.tab-content.active'); + if (activeContent) { + const activeTab = activeContent.getAttribute('data-tab'); + const activeButton = tabsContainer.querySelector(`.tab-button[data-for-tab="${activeTab}"]`); + if (activeButton) { + activeButton.click(); // Simulate a click to activate the tab + } + } else { + tabsContainer.querySelector('.tab-button')?.click(); // Activate the first tab + } + } + const savedTab = getSavedTab(tabsContainer.id); + if (savedTab) { + const savedButton = tabsContainer.querySelector(`.tab-button[data-for-tab="${savedTab}"]`); + if (savedButton) { + if (!savedButton.classList.contains('active')) { + savedButton.click(); // Simulate a click to activate the tab only if it's not already active } - // console.log(`Restored saved tab: ${savedTab}`); - } - } - } catch (e) { - // console.log("Error updating tabs: " + e); - } - } -} + } + } + } catch (e) { + console.warn('Failed to update nested tabs:', e); + } + }); + } document.addEventListener('DOMContentLoaded', () => { - // console.log('Document loaded. Initializing tabs...'); updateTabs(); - updateNestedTabs(document); observer.observe(document.body, observerOptions); }); diff --git a/webui/src/main/resources/shared/_main.scss b/webui/src/main/resources/shared/_main.scss index b0a25e1d..52aab999 100644 --- a/webui/src/main/resources/shared/_main.scss +++ b/webui/src/main/resources/shared/_main.scss @@ -157,12 +157,16 @@ body { border-radius: $border-radius; box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1); transition: box-shadow 0.3s ease, background-color 0.3s ease; - &:hover { box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15); } } +pre { + max-height: 70vh; + overflow: auto; +} + .user-message { background-color: lighten($user-message-bg, 5%); border-left: 4px solid $user-message-border; diff --git a/webui/src/test/kotlin/com/simiacryptus/diff/IterativePatchUtilTest.kt b/webui/src/test/kotlin/com/simiacryptus/diff/IterativePatchUtilTest.kt index fd85a009..8acbbff2 100644 --- a/webui/src/test/kotlin/com/simiacryptus/diff/IterativePatchUtilTest.kt +++ b/webui/src/test/kotlin/com/simiacryptus/diff/IterativePatchUtilTest.kt @@ -1,6 +1,7 @@ package com.simiacryptus.diff import org.junit.jupiter.api.Assertions +import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Test class IterativePatchUtilTest { @@ -18,7 +19,7 @@ class IterativePatchUtilTest { line3 """.trimIndent() val result = IterativePatchUtil.applyPatch(source, patch) - Assertions.assertEquals(source.trim().replace("\r\n", "\n"), result.replace("\r\n", "\n")) + assertEquals(source.trim().replace("\r\n", "\n"), result.replace("\r\n", "\n")) } // ... (other existing tests) @@ -42,7 +43,7 @@ class IterativePatchUtilTest { line3 """.trimIndent() val result = IterativePatchUtil.applyPatch(source, patch) - Assertions.assertEquals(expected.trim().replace("\r\n", "\n"), result.replace("\r\n", "\n")) + assertEquals(expected.trim().replace("\r\n", "\n"), result.replace("\r\n", "\n")) } @Test @@ -64,7 +65,31 @@ class IterativePatchUtilTest { line3 """.trimIndent() val result = IterativePatchUtil.applyPatch(source, patch) - Assertions.assertEquals(expected.trim().replace("\r\n", "\n"), result.replace("\r\n", "\n")) + assertEquals(expected.trim().replace("\r\n", "\n"), result.replace("\r\n", "\n")) + } + + @Test + fun testPatchModifyLineWithComments() { + val source = """ + line1 + line3 + line2 + """.trimIndent() + val patch = """ + line1 + line3 + // This comment should be ignored + -line2 + +modifiedLine2 + # LLMs sometimes get chatty and add stuff to patches__ + """.trimIndent() + val expected = """ + line1 + line3 + modifiedLine2 + """.trimIndent() + val result = IterativePatchUtil.applyPatch(source, patch) + assertEquals(expected.trim().replace("\r\n", "\n"), result.replace("\r\n", "\n")) } @Test @@ -84,7 +109,65 @@ class IterativePatchUtilTest { line3 """.trimIndent() val result = IterativePatchUtil.applyPatch(source, patch) - Assertions.assertEquals(expected.trim().replace("\r\n", "\n"), result.replace("\r\n", "\n")) + assertEquals(expected.trim().replace("\r\n", "\n"), result.replace("\r\n", "\n")) + } + + @Test + fun testPatchAdd2Line2() { + val source = """ + line1 + + line2 + line3 + """.trimIndent() + val patch = """ + line1 + + lineA + + + lineB + + line2 + line3 + """.trimIndent() + val expected = """ + line1 + lineA + lineB + + line2 + line3 + """.trimIndent() + val result = IterativePatchUtil.applyPatch(source, patch) + assertEquals(expected.trim().replace("\r\n", "\n"), result.replace("\r\n", "\n")) + } + + @Test + fun testPatchAdd2Line3() { + val source = """ + line1 + + line2 + line3 + """.trimIndent() + val patch = """ + line1 + // extraneous comment + + lineA + + lineB + // llms sometimes get chatty and add stuff to patches + line2 + line3 + """.trimIndent() + val expected = """ + line1 + lineA + lineB + + line2 + line3 + """.trimIndent() + val result = IterativePatchUtil.applyPatch(source, patch) + assertEquals(expected.trim().replace("\r\n", "\n"), result.replace("\r\n", "\n")) } @Test @@ -148,12 +231,104 @@ class IterativePatchUtilTest { } """.trimIndent() val result = IterativePatchUtil.applyPatch(source, patch) - Assertions.assertEquals( + assertEquals( expected.replace("\\s*\r?\n\\s*".toRegex(), "\n"), result.replace("\\s*\r?\n\\s*".toRegex(), "\n") ) } + @Test + fun testFromData2() { + val source = """ + export class StandardChessModel implements GameModel { + geometry: BoardGeometry; + state: GameState; + private moveHistory: MoveHistory; + + constructor(initialBoard?: Piece[]) { + this.geometry = new StandardBoardGeometry(); + this.state = initialBoard ? this.initializeWithBoard(initialBoard) : this.initialize(); + this.moveHistory = new MoveHistory(this.state.board); + } + + redoMove(): GameState { + return this.getState(); + } + + isGameOver(): boolean { + return false; + } + + getWinner(): 'white' | 'black' | 'draw' | null { + return null; + } + + importState(stateString: string): GameState { + // Implement import state logic + const parsedState = JSON.parse(stateString); + // Validate and convert the parsed state to GameState + // For now, we'll just return the current state + return this.getState(); + } + + } + + // Similar changes for black pawns + """.trimIndent() + val patch = """ + | export class StandardChessModel implements GameModel { + | // ... other methods ... + | + |- getWinner(): 'white' | 'black' | 'draw' | null { + |+ getWinner(): ChessColor | 'draw' | null { + | return null; + | } + | + | // ... other methods ... + | } + """.trimMargin() + val expected = """ + export class StandardChessModel implements GameModel { + geometry: BoardGeometry; + state: GameState; + private moveHistory: MoveHistory; + + constructor(initialBoard?: Piece[]) { + this.geometry = new StandardBoardGeometry(); + this.state = initialBoard ? this.initializeWithBoard(initialBoard) : this.initialize(); + this.moveHistory = new MoveHistory(this.state.board); + } + + redoMove(): GameState { + return this.getState(); + } + + isGameOver(): boolean { + return false; + } + + getWinner(): ChessColor | 'draw' | null { + return null; + } + + importState(stateString: string): GameState { + // Implement import state logic + const parsedState = JSON.parse(stateString); + // Validate and convert the parsed state to GameState + // For now, we'll just return the current state + return this.getState(); + } + + } + + // Similar changes for black pawns + """.trimIndent() + val result = IterativePatchUtil.applyPatch(source, patch) + assertEquals( + expected.replace("\\s*\r?\n\\s*".toRegex(), "\n"), + result.replace("\\s*\r?\n\\s*".toRegex(), "\n") + ) + } @Test @@ -167,7 +342,7 @@ class IterativePatchUtilTest { val result = IterativePatchUtil.generatePatch(oldCode, newCode) val expected = """ """.trimMargin() - Assertions.assertEquals(expected.trim().replace("\r\n", "\n"), result.trim().replace("\r\n", "\n")) + assertEquals(expected.trim().replace("\r\n", "\n"), result.trim().replace("\r\n", "\n")) } @Test @@ -188,8 +363,9 @@ class IterativePatchUtilTest { | line1 | line2 |+ newLine + | line3 """.trimMargin() - Assertions.assertEquals(expected.trim().replace("\r\n", "\n"), result.trim().replace("\r\n", "\n")) + assertEquals(expected.trim().replace("\r\n", "\n"), result.trim().replace("\r\n", "\n")) } @Test @@ -209,7 +385,7 @@ class IterativePatchUtilTest { |- line2 | line3 """.trimMargin() - Assertions.assertEquals(expected.trim().replace("\r\n", "\n"), result.trim().replace("\r\n", "\n")) + assertEquals(expected.trim().replace("\r\n", "\n"), result.trim().replace("\r\n", "\n")) } @Test @@ -229,8 +405,9 @@ class IterativePatchUtilTest { | line1 |- line2 |+ modifiedLine2 + | line3 """.trimMargin() - Assertions.assertEquals( + assertEquals( expected.trim().replace("\r\n", "\n"), result.trim().replace("\r\n", "\n") ) @@ -263,10 +440,214 @@ class IterativePatchUtilTest { |+ // Modified comment |+ let x = 5; |+ return x > 0; + | } """.trimMargin() - Assertions.assertEquals( + assertEquals( expected.trim().replace("\r\n", "\n"), result.trim().replace("\r\n", "\n") ) } + + @Test + fun testGeneratePatchMoveLineUpwardsMultiplePositions() { + val oldCode = """ + line1 + line2 + line3 + line4 + line5 + line6 + """.trimIndent() + + val newCode = """ + line1 + line5 + line2 + line3 + line4 + line6 + """.trimIndent() + + val expectedPatch = """ + line1 + - line2 + - line3 + - line4 + line5 + + line2 + + line3 + + line4 + line6 + """.trimIndent() + + val actualPatch = IterativePatchUtil.generatePatch(oldCode, newCode) + assertEquals(expectedPatch, actualPatch) + } + + @Test + fun testGeneratePatchMoveLineDownwardsMultiplePositions() { + val oldCode = """ + line1 + line2 + line3 + line4 + line5 + line6 + """.trimIndent() + + val newCode = """ + line1 + line3 + line4 + line5 + line6 + line2 + """.trimIndent() + + val expectedPatch = """ + line1 + - line2 + line3 + line4 + line5 + line6 + + line2 + """.trimIndent() + + val actualPatch = IterativePatchUtil.generatePatch(oldCode, newCode) + assertEquals(expectedPatch, actualPatch) + } + + @Test + fun testGeneratePatchSwapLines() { + val oldCode = """ + line1 + line2 + line3 + line4 + line5 + line6 + """.trimIndent() + + val newCode = """ + line1 + line4 + line3 + line2 + line5 + line6 + """.trimIndent() + + val expectedPatch = """ + line1 + - line2 + - line3 + line4 + + line3 + + line2 + line5 + line6 + """.trimIndent() + + val actualPatch = IterativePatchUtil.generatePatch(oldCode, newCode) + assertEquals(expectedPatch, actualPatch) + } + + @Test + fun testGeneratePatchMoveAdjacentLines() { + val oldCode = """ + line1 + line2 + line3 + line4 + line5 + line6 + """.trimIndent() + + val newCode = """ + line1 + line4 + line5 + line2 + line3 + line6 + """.trimIndent() + + val expectedPatch = """ + line1 + - line2 + - line3 + line4 + line5 + + line2 + + line3 + line6 + """.trimIndent() + + val actualPatch = IterativePatchUtil.generatePatch(oldCode, newCode) + assertEquals(expectedPatch, actualPatch) + } + + @Test + fun testGeneratePatchMoveLineUpwards() { + val oldCode = """ + line1 + line2 + line3 + line4 + line5 + line6 + """.trimIndent() + val newCode = """ + line1 + line2 + line5 + line3 + line4 + line6 + """.trimIndent() + val expectedPatch = """ + line1 + line2 + - line3 + - line4 + line5 + + line3 + + line4 + line6 + """.trimIndent() + val actualPatch = IterativePatchUtil.generatePatch(oldCode, newCode) + assertEquals(expectedPatch, actualPatch) + } + + @Test + fun testGeneratePatchMoveLineDownwards() { + val oldCode = """ + line1 + line2 + line3 + line4 + line5 + line6 + """.trimIndent() + val newCode = """ + line1 + line3 + line4 + line5 + line2 + line6 + """.trimIndent() + val expectedPatch = """ + line1 + - line2 + line3 + line4 + line5 + + line2 + line6 + """.trimIndent() + val actualPatch = IterativePatchUtil.generatePatch(oldCode, newCode) + assertEquals(expectedPatch, actualPatch) + } }