Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
1244b2c
feat: Add `RenameMemory.kt`.
dragoi75 Mar 4, 2026
81a8b11
feat: Add `MemoryAwareTransformation` abstract class.
dragoi75 Mar 4, 2026
f88b12d
feat: Add `PsiSignatureGenerator`.
dragoi75 Mar 4, 2026
39fed91
feat: Modify `RenameVariableTransformation` to use memory.
dragoi75 Mar 4, 2026
d9a0a40
feat: Modify `RenameMethodTransformation` to use memory.
dragoi75 Mar 4, 2026
01ded7d
feat: Modify `RenameClassTransformation` to use memory.
dragoi75 Mar 4, 2026
00a2900
fix: only store signature if memory in write mode
dragoi75 Mar 4, 2026
38cfd16
fix: indentation
dragoi75 Mar 4, 2026
123fc9c
fix: remove repeat LLM calls.
dragoi75 Mar 9, 2026
9200771
fix: for nested modules, path should contain `src/` not only start with.
dragoi75 Mar 22, 2026
f49e543
fix: Update signature generation for methods
dragoi75 Mar 22, 2026
9e162bb
fix: reduce control flow complexity (remove `.also`)
dragoi75 Mar 23, 2026
0c4c9d1
fix: Subprojects were not indexed correctly
dragoi75 Mar 22, 2026
45471d9
fix: nitpicks
dragoi75 Mar 25, 2026
8047065
feat: abstract the `useMemory` flag from the `MemoryAwareTransformation`
dragoi75 Mar 25, 2026
128b06d
fix: simplify memory handling for rename transformations
dragoi75 Mar 25, 2026
84ee106
refactor: move signature generation logic to extension functions
dragoi75 Mar 25, 2026
20c8802
fix: remove unused import
dragoi75 Mar 25, 2026
18fba0c
feat: add configurable memory directory support for transformations
dragoi75 Mar 25, 2026
69c7d6b
feat: make `Memory` generic, not rename-specific
dragoi75 Mar 25, 2026
15d8db0
fix: nits
dragoi75 Mar 26, 2026
cb754db
fix: change memory from `File` to `Path`
dragoi75 Mar 26, 2026
7aac88c
refactor: simplify memory state initialization and access
dragoi75 Mar 26, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,5 @@
.kotlin
.qodana
build
codecocoon.yml
codecocoon.yml
.codecocoon-memory
7 changes: 7 additions & 0 deletions codecocoon.example.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,13 @@ projectRoot: "/path/to/project/root"
# Optional: limit transformations to these files (relative to the root). Leave empty to target the entire project
files: []

# Optional: directory where memory files are stored (for deterministic rename transformations)
# If not specified, defaults to '.codecocoon-memory' in the same directory as this config file
# Can be:
# - Absolute path: "/absolute/path/to/memory"
# - Relative path: "my-memory-dir" (relative to this config file's directory)
memoryDir: ".codecocoon-memory"

# The transformation pipeline. Order matters. Each transformation has:
# - id: unique identifier
# - config: arbitrary nested settings; only the selected transformation should interpret it
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.github.pderakhshanfar.codecocoonplugin.executor

import com.github.pderakhshanfar.codecocoonplugin.common.FileContext
import com.github.pderakhshanfar.codecocoonplugin.memory.Memory
import com.github.pderakhshanfar.codecocoonplugin.transformation.Transformation

/**
Expand All @@ -13,10 +14,12 @@ interface TransformationExecutor {
*
* @param transformation The transformation to apply
* @param context The file context
* @param memory Optional persistent memory for storing transformation state (e.g., rename mappings)
* @return Result of the transformation
*/
suspend fun execute(
transformation: Transformation,
context: FileContext,
memory: Memory<String, String>? = null,
): TransformationResult
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package com.github.pderakhshanfar.codecocoonplugin.memory

/**
* Generic persistent storage interface for key-value pairs.
*
* Implements [AutoCloseable] to ensure automatic persistence on resource cleanup.
* Use with `.use {}` blocks to guarantee data is saved:
*
* ```kotlin
* PersistentMemory(projectName, memoryDir).use { memory ->
* memory.put("key", "value")
* // memory.save() called automatically on close
* }
* ```
*
* @param K Key type
* @param V Value type
*/
interface Memory<K, V> : AutoCloseable {

/**
* Retrieves the value associated with the given key, or null if not found.
*/
fun get(key: K): V?

/**
* Puts a new key-value pair into the memory, returning the previous value if it existed.
*/
fun put(key: K, value: V): V?

fun size(): Int

fun save()

override fun close() {
save()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -125,9 +125,7 @@ class HeadlessModeStarter : ApplicationStarter {
*
* This function sets up predefined transformations that are available for use.
* Each transformation is identified by a unique ID and is associated with a factory
* function that creates an instance of the transformation when invoked. Specifically,
* this implementation registers the "add-comment-transformation," which adds comments
* to the beginning of files.
* function that creates an instance of the transformation when invoked.
*
* The registration process ensures that the transformation is correctly mapped by its
* unique ID in the registry, allowing it to be referenced dynamically during execution.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import com.github.pderakhshanfar.codecocoonplugin.executor.TransformationExecuto
import com.github.pderakhshanfar.codecocoonplugin.executor.TransformationResult
import com.github.pderakhshanfar.codecocoonplugin.intellij.psi.psiFile
import com.github.pderakhshanfar.codecocoonplugin.intellij.vfs.findVirtualFile
import com.github.pderakhshanfar.codecocoonplugin.memory.Memory
import com.github.pderakhshanfar.codecocoonplugin.transformation.Transformation
import com.intellij.openapi.application.ReadResult
import com.intellij.openapi.application.readAction
Expand All @@ -26,11 +27,14 @@ class IntelliJTransformationExecutor(

override suspend fun execute(
transformation: Transformation,
context: FileContext
context: FileContext,
memory: Memory<String, String>?
): TransformationResult {
return try {
when (transformation) {
is IntelliJAwareTransformation -> executeIntelliJTransformation(transformation, context)
is IntelliJAwareTransformation -> {
executeIntelliJTransformation(transformation, context, memory)
}
else -> TransformationResult.Failure("Transformation ${transformation.id} must implement `IntelliJAwareTransformation`")
}
} catch (err: Exception) {
Expand All @@ -42,6 +46,7 @@ class IntelliJTransformationExecutor(
private suspend fun executeIntelliJTransformation(
transformation: IntelliJAwareTransformation,
context: FileContext,
memory: Memory<String, String>?
): TransformationResult {
val virtualFile = project.findVirtualFile(context)
?: return TransformationResult.Failure(
Expand All @@ -55,7 +60,7 @@ class IntelliJTransformationExecutor(
} ?: return TransformationResult.Failure("Cannot get PSI for file: ${context.relativePath}")

// Run transformation directly - self-managed transformations handle EDT requirements internally
return transformation.apply(psiFile, virtualFile)
return transformation.apply(psiFile, virtualFile, memory)
}

// Regular transformations need writeCommandAction wrapper
Expand All @@ -66,7 +71,7 @@ class IntelliJTransformationExecutor(
)

writeCommandAction(project, transformation.id) {
transformation.apply(psiFile, virtualFile)
transformation.apply(psiFile, virtualFile, memory)
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package com.github.pderakhshanfar.codecocoonplugin.components.transformations
import com.github.pderakhshanfar.codecocoonplugin.common.Language
import com.github.pderakhshanfar.codecocoonplugin.executor.TransformationResult
import com.github.pderakhshanfar.codecocoonplugin.intellij.psi.document
import com.github.pderakhshanfar.codecocoonplugin.memory.Memory
import com.github.pderakhshanfar.codecocoonplugin.transformation.TextBasedTransformation
import com.github.pderakhshanfar.codecocoonplugin.transformation.require
import com.intellij.openapi.vfs.VirtualFile
Expand Down Expand Up @@ -31,6 +32,7 @@ class AddCommentTransformation(
override fun apply(
psiFile: PsiFile,
virtualFile: VirtualFile,
memory: Memory<String, String>?
): TransformationResult = try {
val comment = createComment(psiFile.name, message)
val document = psiFile.document()
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.github.pderakhshanfar.codecocoonplugin.components.transformations

import com.github.pderakhshanfar.codecocoonplugin.executor.TransformationResult
import com.github.pderakhshanfar.codecocoonplugin.memory.Memory
import com.github.pderakhshanfar.codecocoonplugin.transformation.Transformation
import com.intellij.openapi.application.readAction
import com.intellij.openapi.vfs.VirtualFile
Expand All @@ -23,11 +24,13 @@ interface IntelliJAwareTransformation : Transformation {
*
* @param psiFile The PSI representation of the file
* @param virtualFile The virtual file being transformed
* @param memory Optional persistent memory for storing transformation state
* @return Result of the transformation
*/
fun apply(
psiFile: PsiFile,
virtualFile: VirtualFile
virtualFile: VirtualFile,
memory: Memory<String, String>? = null
): TransformationResult

companion object {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import com.github.pderakhshanfar.codecocoonplugin.components.transformations.Mov
import com.github.pderakhshanfar.codecocoonplugin.executor.TransformationResult
import com.github.pderakhshanfar.codecocoonplugin.intellij.logging.withStdout
import com.github.pderakhshanfar.codecocoonplugin.java.JavaTransformation
import com.github.pderakhshanfar.codecocoonplugin.memory.Memory
import com.github.pderakhshanfar.codecocoonplugin.suggestions.SuggestionsApi
import com.github.pderakhshanfar.codecocoonplugin.transformation.require
import com.intellij.openapi.application.ApplicationManager
Expand Down Expand Up @@ -64,6 +65,7 @@ class MoveFileIntoSuggestedDirectoryTransformation private constructor(
override fun apply(
psiFile: PsiFile,
virtualFile: VirtualFile,
memory: Memory<String, String>?
): TransformationResult {
// validate that this is a Java file
if (psiFile !is PsiJavaFile) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@ import com.github.pderakhshanfar.codecocoonplugin.executor.TransformationResult
import com.github.pderakhshanfar.codecocoonplugin.intellij.logging.withStdout
import com.github.pderakhshanfar.codecocoonplugin.intellij.psi.document
import com.github.pderakhshanfar.codecocoonplugin.java.JavaTransformation
import com.github.pderakhshanfar.codecocoonplugin.memory.Memory
import com.github.pderakhshanfar.codecocoonplugin.memory.PsiSignatureGenerator
import com.github.pderakhshanfar.codecocoonplugin.transformation.requireOrDefault
import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.application.readAction
import com.intellij.openapi.diagnostic.thisLogger
Expand All @@ -34,9 +37,13 @@ class RenameClassTransformation(
private val logger = thisLogger().withStdout()

override fun apply(
psiFile: PsiFile, virtualFile: VirtualFile
psiFile: PsiFile,
virtualFile: VirtualFile,
memory: Memory<String, String>?
): TransformationResult {
val result = try {
val useMemory = config.requireOrDefault<Boolean>("useMemory", defaultValue = false)

val document = withReadAction { psiFile.document() }
val modifiedFiles = mutableSetOf<PsiFile>()
val value = if (document != null) {
Expand All @@ -48,19 +55,32 @@ class RenameClassTransformation(

logger.info(" ⏲ Generating rename suggestions for ${eligibleClasses.size} classes...")

val renameSuggestions = runBlocking {
eligibleClasses.associateWith { psiClass -> getNewClassNames(psiClass) }
val renameSuggestions = if (useMemory) {
extractRenamesFromMemory(eligibleClasses, memory)
} else {
runBlocking { generateRenames(eligibleClasses) }
}

val successfulRenames = eligibleClasses.mapNotNull { psiClass ->
val className = withReadAction { psiClass.name }
val suggestions = renameSuggestions[psiClass] ?: return@mapNotNull null

// Generate signature before renaming
val signature = withReadAction { PsiSignatureGenerator.generateSignature(psiClass) }
if (signature == null) {
logger.warn(" ⊘ Could not generate signature for class $className")
return@mapNotNull null
}

// Try each suggestion until one succeeds
for (suggestion in suggestions) {
val files = tryRenameClassAndUsages(psiFile.project, psiClass, suggestion)
if (files != null) {
modifiedFiles.addAll(files)
if (!useMemory) {
memory?.put(signature, suggestion)
logger.info(" ✓ Stored rename in memory: `$signature` -> `$suggestion`")
}
return@mapNotNull psiClass to suggestion
}
}
Expand All @@ -72,6 +92,7 @@ class RenameClassTransformation(
val renamedCount = successfulRenames.size
val totalCandidates = eligibleClasses.size
val skipped = totalCandidates - renamedCount

TransformationResult.Success(
message = "Renamed ${renamedCount}/${totalCandidates} classes in ${virtualFile.name}${if (skipped > 0) " (skipped: $skipped)" else ""}",
filesModified = modifiedFiles.size
Expand All @@ -98,7 +119,41 @@ class RenameClassTransformation(
val fieldNames: List<String>
)

private suspend fun getNewClassNames(psiClass: PsiClass, count: Int = DEFAULT_SUGGESTED_NAMES_SIZE): List<String> {
/**
* Extracts rename suggestions from memory for classes.
*/
private fun extractRenamesFromMemory(
classes: List<PsiClass>,
memory: Memory<String, String>?
): Map<PsiClass, List<String>> {
return classes.associateWith { psiClass ->
val signature = withReadAction { PsiSignatureGenerator.generateSignature(psiClass) }
if (signature == null) {
logger.warn("Could not generate signature for class")
return@associateWith emptyList()
}

val cachedName = memory?.get(signature)
if (cachedName != null) {
logger.info(" ↳ Using cached rename: $signature -> $cachedName")
listOf(cachedName)
} else {
logger.warn(" ⊘ Signature not found in memory: $signature")
emptyList()
}
}
}

/**
* Generates rename suggestions for all classes using LLM.
*/
private suspend fun generateRenames(classes: List<PsiClass>): Map<PsiClass, List<String>> {
return classes.associateWith { psiClass ->
generateNewClassNames(psiClass)
}
}

private suspend fun generateNewClassNames(psiClass: PsiClass, count: Int = DEFAULT_SUGGESTED_NAMES_SIZE): List<String> {
val context = readAction {
val type = when {
psiClass.isInterface -> "interface"
Expand Down
Loading
Loading