Skip to content

Add case-insensitive duplicate detection for icon names#895

Merged
egorikftp merged 3 commits intomainfrom
bug/case-insensitive
Feb 23, 2026
Merged

Add case-insensitive duplicate detection for icon names#895
egorikftp merged 3 commits intomainfrom
bug/case-insensitive

Conversation

@egorikftp
Copy link
Member

@egorikftp egorikftp commented Feb 23, 2026

Screen.Recording.2026-02-23.at.11.22.37.mov

📝 Changelog

If this PR introduces user-facing changes, please update the relevant Unreleased section in changelogs:

@coderabbitai
Copy link

coderabbitai bot commented Feb 23, 2026

Walkthrough

Adds validations across CLI, Gradle plugin, and IDEA plugin to detect exact duplicate icon names and case-insensitive duplicates. Integrates duplicate checks before generation (CLI/Gradle) with nested-pack and useFlatPackage-aware grouping. Extends IDEA plugin validation API (adds ValidationError.HasCaseInsensitiveDuplicates and updates List.checkImportIssues(useFlatPackage: Boolean)). Adds unit tests covering exact and case-insensitive collision scenarios, a new IconNameFormatterTest case, small UI synchronization in ConfirmTextField, and updates changelogs.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 2.86% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title 'Add case-insensitive duplicate detection for icon names' clearly and concisely summarizes the main change—adding detection for case-insensitive duplicates in icon names.
Description check ✅ Passed The PR description includes issue reference (#823), a video demonstration, and confirms that all three required changelog updates (IntelliJ Plugin, CLI, Gradle Plugin) have been completed.
Linked Issues check ✅ Passed The PR fully addresses issue #823 by implementing case-insensitive and exact duplicate detection across all three tools (CLI, Gradle Plugin, IntelliJ Plugin) and nested pack scenarios.
Out of Scope Changes check ✅ Passed All changes are directly related to duplicate detection: validation logic, tests, UI state management, and documentation updates align with resolving issue #823.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch bug/case-insensitive

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (8)
tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/jewel/textfield/ConfirmTextField.kt (1)

46-50: Consider guarding against overwriting in-progress user edits while focused

The LaunchedEffect correctly syncs external prop changes (e.g., after collision-based name normalization) into the internal state. The equality guard prevents spurious updates.

One edge case: if the parent updates text while the user is actively editing (field is focused), this will silently overwrite their in-progress input. Adding a !focused guard avoids that disruption while still propagating updates when the field is idle.

♻️ Proposed optional guard
-    // Update text field state when external text changes
-    LaunchedEffect(text) {
-        if (state.text.toString() != text) {
-            state.setTextAndPlaceCursorAtEnd(text)
-        }
-    }
+    // Update text field state when external text changes (only when not actively editing)
+    LaunchedEffect(text) {
+        if (!focused && state.text.toString() != text) {
+            state.setTextAndPlaceCursorAtEnd(text)
+        }
+    }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/jewel/textfield/ConfirmTextField.kt`
around lines 46 - 50, The LaunchedEffect(text) block can overwrite an actively
edited value; modify the condition to only apply external text when the text
differs AND the field is not focused (i.e., add a !focused check) so updates
propagate when idle but don't clobber in-progress edits; update the
LaunchedEffect(text) condition that calls state.setTextAndPlaceCursorAtEnd(text)
to include the focused guard.
components/parser/unified/src/commonTest/kotlin/io/github/composegears/valkyrie/parser/unified/util/IconNameFormatterTest.kt (1)

53-65: Consider renaming for accuracy.

The test verifies formatter output, not collision detection logic — that's exercised in IconValidationTest and FailedConfigurationTest. A name like check formatting that produces case-insensitive collision would be more precise, but this is purely a nit.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@components/parser/unified/src/commonTest/kotlin/io/github/composegears/valkyrie/parser/unified/util/IconNameFormatterTest.kt`
around lines 53 - 65, Rename the test function currently named `check
case-insensitive collision detection` to a clearer name that reflects it asserts
formatter output, e.g. `check formatting that produces case-insensitive
collision`, and update any references; this is the test that calls
`IconNameFormatter.format(fileName)` with `IconTest` cases (the two inputs
"test-icon.svg" and "testicon.svg") and should not be confused with collision
logic exercised elsewhere (`IconValidationTest`, `FailedConfigurationTest`).
tools/gradle-plugin/src/main/kotlin/io/github/composegears/valkyrie/gradle/internal/task/GenerateImageVectorsTask.kt (2)

422-455: Duplicate logic: inline nested-pack checks replicate checkDuplicatesInIconNames.

The exact and case-insensitive detection code inside iconsByPack.forEach (lines 426–455) is a verbatim copy of checkDuplicatesInIconNames. The only difference is how the context string is formatted. Delegate to the existing helper to remove the duplication.

♻️ Proposed refactor
 iconsByPack.forEach { (packIdentifier, filesInPack) ->
     val names = filesInPack.map { IconNameFormatter.format(name = it.name) }
-
-    // Check exact duplicates within this pack/group
-    val exactDuplicates = names
-        .groupBy { it }
-        .filter { it.value.size > 1 }
-        .keys
-        .toList()
-
-    if (exactDuplicates.isNotEmpty()) {
-        throw GradleException(
-            "Found duplicate icon names in \"$packIdentifier\": ${exactDuplicates.joinToString(", ")}. " +
-                "Each icon must have a unique name. " +
-                "Please rename the source files to avoid duplicates.",
-        )
-    }
-
-    // Check case-insensitive duplicates within this pack/group
-    val caseInsensitiveDuplicates = names
-        .groupBy { it.lowercase() }
-        .filter { it.value.size > 1 && it.value.distinct().size > 1 }
-        .values
-        .flatten()
-        .distinct()
-
-    if (caseInsensitiveDuplicates.isNotEmpty()) {
-        throw GradleException(
-            "Found icon names that would collide on case-insensitive file systems (macOS/Windows) in \"$packIdentifier\": " +
-                "${caseInsensitiveDuplicates.joinToString(", ")}. " +
-                "These icons would overwrite each other during generation. " +
-                "Please rename the source files to avoid case-insensitive duplicates.",
-        )
-    }
+    checkDuplicatesInIconNames(names, "\"$packIdentifier\"")
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@tools/gradle-plugin/src/main/kotlin/io/github/composegears/valkyrie/gradle/internal/task/GenerateImageVectorsTask.kt`
around lines 422 - 455, The inline duplicate detection inside
iconsByPack.forEach duplicates the logic in checkDuplicatesInIconNames; replace
the exact- and case-insensitive checks with a call to checkDuplicatesInIconNames
by passing the computed names list and a context string built from
packIdentifier (e.g., "pack \"$packIdentifier\"" or the same format used
elsewhere for consistency). Locate the block that computes names
(IconNameFormatter.format) and remove the two duplicate-check blocks, invoking
checkDuplicatesInIconNames(names, context) instead so all duplicate handling is
centralized in that helper.

399-399: iconNames parameter is unused in the nested-packs path.

When nestedPacks.isNotEmpty(), icon names are recomputed inside validateDuplicates via IconNameFormatter.format instead of using the pre-computed iconNames. The parameter is only consumed by the two else branches. This leaks the caller's internal structure through the signature and causes double-invocation of IconNameFormatter.format for nested-pack scenarios.

Consider replacing the iconNames: List<String> parameter with a Map<File, String> (pre-computed once at the call site and looked up per file inside the function), or simply removing the parameter and always deriving names from files inside the function, which would be consistent across all branches.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@tools/gradle-plugin/src/main/kotlin/io/github/composegears/valkyrie/gradle/internal/task/GenerateImageVectorsTask.kt`
at line 399, The validateDuplicates function currently takes iconNames but
recomputes names for the nested-packs branch using IconNameFormatter.format,
causing double work and leaking caller structure; change the signature of
validateDuplicates to accept a Map<File, String> (precompute icon names once at
the call site using IconNameFormatter.format for each File) or drop the
parameter and always derive names from files inside validateDuplicates, then
update all call sites to either pass the Map<File,String> or let the function
compute names; inside validateDuplicates (and the nestedPacks branch) use the
map lookup (or the single consistent derivation path) instead of calling
IconNameFormatter.format again to eliminate the duplicate computation and remove
the unused iconNames usage.
tools/idea-plugin/src/test/kotlin/io/github/composegears/valkyrie/ui/screen/mode/iconpack/conversion/ui/util/IconValidationTest.kt (1)

167-175: Minor test redundancy with the pre-existing nested, allow duplicates for different currentNestedPack test.

Both this test and the one at line 135 assert that same-named icons in different nested packs are allowed when not using flat package — the only difference is that this one passes useFlatPackage = false explicitly. If the intent is to document the explicit API shape, that's reasonable, but you may want to drop the older test to avoid maintaining two assertions for the same invariant.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@tools/idea-plugin/src/test/kotlin/io/github/composegears/valkyrie/ui/screen/mode/iconpack/conversion/ui/util/IconValidationTest.kt`
around lines 167 - 175, The new test `nested without flatPackage, allow exact
duplicates in different nested packs` duplicates the existing `nested, allow
duplicates for different currentNestedPack` test; either remove the older
`nested, allow duplicates for different currentNestedPack` test or instead
change one of them to explicitly document the API difference (e.g., keep the new
test to assert the explicit parameter `useFlatPackage = false` and delete or
rename the older test) so you don't maintain two assertions for the same
invariant; update/remove the redundant test that uses validIcon, nestedPack, and
checkImportIssues to keep a single clear test.
tools/cli/src/main/kotlin/io/github/composegears/valkyrie/cli/command/SvgXmlToImageVectorCommand.kt (1)

221-251: Duplicate detection logic is correct; minor note on output ordering.

The two-stage validation (exact duplicates first, then case-insensitive) is well-structured. The case-insensitive check correctly excludes already-caught exact duplicates via the distinct().size > 1 guard.

One subtle point: both exactDuplicates (via groupBy {}.keys) and caseInsensitiveDuplicates (via .flatten().distinct()) produce lists whose iteration order depends on the insertion order of iconNames, which in turn depends on the filesystem-dependent order of listDirectoryEntries. The error message content is deterministic in set membership but non-deterministic in ordering. This is fine for user-facing output but makes exact string-matching in tests fragile (see test file comment).

Consider sorting the duplicate names in the error messages for deterministic output
     val exactDuplicates = iconNames
         .groupBy { it }
         .filter { it.value.size > 1 }
         .keys
-        .toList()
+        .sorted()

     ...

     val caseInsensitiveDuplicates = iconNames
         .groupBy { it.lowercase() }
         .filter { it.value.size > 1 && it.value.distinct().size > 1 }
         .values
         .flatten()
         .distinct()
+        .sorted()
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@tools/cli/src/main/kotlin/io/github/composegears/valkyrie/cli/command/SvgXmlToImageVectorCommand.kt`
around lines 221 - 251, The duplicate-name error messages are non-deterministic
because exactDuplicates and caseInsensitiveDuplicates derive order from
iconNames; modify the construction or the join step to sort the names before
reporting so output is deterministic for tests—specifically, when building
exactDuplicates and caseInsensitiveDuplicates (symbols: exactDuplicates,
caseInsensitiveDuplicates, iconNames) or immediately before calling outputError,
call a deterministic sort (e.g., sort() on the list) and then joinToString to
produce the error text for both outputError calls.
tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/mode/iconpack/conversion/IconPackConversionViewModel.kt (1)

274-332: Two-stage dedup is solid; edge case with cascading name collisions.

The sequential exact→case-insensitive resolution approach is well-structured. One edge case to be aware of: stage 1 renaming (e.g., "Test""Test1") could introduce a new collision with a pre-existing icon named "Test1". This wouldn't be caught by stage 2 since it's an exact (not case-insensitive) duplicate.

This is acceptable for an auto-resolve UI action since:

  1. checkImportIssues is called on the result (line 341), so the new collision would surface as a validation error.
  2. The user can manually rename afterward.

If you ever want to harden this, a loop-until-stable approach would handle cascading collisions:

// Pseudocode: keep resolving until no new issues arise
var current = processedIcons
do {
    val resolved = resolveOnce(current)
    val stable = resolved == current
    current = resolved
} while (!stable)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/mode/iconpack/conversion/IconPackConversionViewModel.kt`
around lines 274 - 332, Sequential exact-then-case-insensitive dedup in
resolvedIcons can produce cascading collisions (e.g., "Test" → "Test1" collides
with existing "Test1"); update the logic in the IconPackConversionViewModel
resolver to repeat the dedupe pass until stable: extract the current
per-location dedupe logic (the block producing iconsWithResolvedExactDuplicates
and the lowercase pass) into a resolveOnce function that takes a list of
processedIcons and returns resolved list, then loop do { resolved =
resolveOnce(current); } while (resolved != current) so the resolver (used where
resolvedIcons is computed) re-runs until no new renames occur; retain use of
IconName and keep checkImportIssues as the final validation step.
tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/mode/iconpack/conversion/ui/util/IconValidation.kt (1)

44-92: Pack identifier logic is duplicated between exact and case-insensitive checks.

Lines 49–55 and 72–78 contain identical packIdentifier computation. Extract it to a helper to keep the code DRY.

♻️ Extract pack identifier computation
+private fun BatchIcon.Valid.packIdentifier(useFlatPackage: Boolean): String {
+    return when (val pack = iconPack) {
+        is IconPack.Single -> pack.iconPackName
+        is IconPack.Nested -> when {
+            useFlatPackage -> pack.iconPackName
+            else -> "${pack.iconPackName}.${pack.currentNestedPack}"
+        }
+    }
+}
+
 fun List<BatchIcon>.checkImportIssues(useFlatPackage: Boolean = false): Map<ValidationError, List<IconName>> {
     return buildMap {
         // ... existing broken/empty/space checks ...

         val duplicates = this@checkImportIssues
             .filterIsInstance<BatchIcon.Valid>()
-            .groupBy {
-                val packIdentifier = when (val pack = it.iconPack) {
-                    is IconPack.Single -> pack.iconPackName
-                    is IconPack.Nested -> when {
-                        useFlatPackage -> pack.iconPackName
-                        else -> "${pack.iconPackName}.${pack.currentNestedPack}"
-                    }
-                }
-                packIdentifier to it.iconName.name
-            }
+            .groupBy { it.packIdentifier(useFlatPackage) to it.iconName.name }
             // ...

         val caseInsensitiveDuplicates = this@checkImportIssues
             .filterIsInstance<BatchIcon.Valid>()
-            .groupBy {
-                val packIdentifier = when (val pack = it.iconPack) {
-                    is IconPack.Single -> pack.iconPackName
-                    is IconPack.Nested -> when {
-                        useFlatPackage -> pack.iconPackName
-                        else -> "${pack.iconPackName}.${pack.currentNestedPack}"
-                    }
-                }
-                packIdentifier to it.iconName.name.lowercase()
-            }
+            .groupBy { it.packIdentifier(useFlatPackage) to it.iconName.name.lowercase() }
             // ...
     }
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/mode/iconpack/conversion/ui/util/IconValidation.kt`
around lines 44 - 92, The pack-identifier computation is duplicated in the
exact-duplicate and case-insensitive duplicate checks; extract it into a small
helper and call it from both groupBy blocks. Add a private helper (e.g.,
packIdentifier(pack: IconPack, useFlatPackage: Boolean): String or an extension
on BatchIcon.Valid) that returns the same string produced by the when(...) logic
used in lines with IconPack.Single / IconPack.Nested, then replace the
duplicated when blocks in both groupBy lambdas with a call to that helper and
keep the rest of the groupBy (pairing with it.iconName.name or
it.iconName.name.lowercase()) unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@tools/cli/src/test/kotlin/io/github/composegears/valkyrie/cli/command/SvgXmlToImageVectorCommandTest.kt`:
- Around line 139-177: The test is flaky because it asserts the exact ordering
"TestIcon, Testicon" while directory iteration (listDirectoryEntries) can return
files in either order; update the test in SvgXmlToImageVectorCommandTest (the
test `should throw error for case-insensitive duplicate icon names`) to be
order-independent: replace the exact equality check and strict mock `every {
outputError(mockMessage) }` with assertions that the thrown exception message
contains both "TestIcon" and "Testicon" (e.g., two contains checks or a regex)
and change the `verify { outputError(...) }` to match a string that contains
both names (use a predicate/matcher rather than exact string). Alternatively, if
you prefer production determinism, sort the duplicate icon names before building
the error message in SvgXmlToImageVectorCommand (the code path that detects
case-insensitive duplicates) so the message is always in a stable order.

In
`@tools/gradle-plugin/src/test/kotlin/io/github/composegears/valkyrie/gradle/FailedConfigurationTest.kt`:
- Around line 350-351: The assertion in FailedConfigurationTest.kt is
order-sensitive: instead of asserting the exact substring "TestIcon, Testicon"
on result.output, assert each icon name independently to avoid file-order
variations; replace the single contains("TestIcon, Testicon") check with two
assertions like assertThat(result.output).contains("TestIcon") and
assertThat(result.output).contains("Testicon") (keeping the existing
assertThat(result.output).contains("Found icon names that would collide on
case-insensitive file systems")).

In `@tools/idea-plugin/CHANGELOG.md`:
- Line 10: Update the changelog entry sentence to avoid the split infinitive:
change "Add automatic re-validation when `useFlatPackage` setting changes to
immediately detect new conflicts" to "Add automatic re-validation when
`useFlatPackage` setting changes to detect new conflicts immediately", keeping
the same intent and referencing the `useFlatPackage` setting.

---

Nitpick comments:
In
`@components/parser/unified/src/commonTest/kotlin/io/github/composegears/valkyrie/parser/unified/util/IconNameFormatterTest.kt`:
- Around line 53-65: Rename the test function currently named `check
case-insensitive collision detection` to a clearer name that reflects it asserts
formatter output, e.g. `check formatting that produces case-insensitive
collision`, and update any references; this is the test that calls
`IconNameFormatter.format(fileName)` with `IconTest` cases (the two inputs
"test-icon.svg" and "testicon.svg") and should not be confused with collision
logic exercised elsewhere (`IconValidationTest`, `FailedConfigurationTest`).

In
`@tools/cli/src/main/kotlin/io/github/composegears/valkyrie/cli/command/SvgXmlToImageVectorCommand.kt`:
- Around line 221-251: The duplicate-name error messages are non-deterministic
because exactDuplicates and caseInsensitiveDuplicates derive order from
iconNames; modify the construction or the join step to sort the names before
reporting so output is deterministic for tests—specifically, when building
exactDuplicates and caseInsensitiveDuplicates (symbols: exactDuplicates,
caseInsensitiveDuplicates, iconNames) or immediately before calling outputError,
call a deterministic sort (e.g., sort() on the list) and then joinToString to
produce the error text for both outputError calls.

In
`@tools/gradle-plugin/src/main/kotlin/io/github/composegears/valkyrie/gradle/internal/task/GenerateImageVectorsTask.kt`:
- Around line 422-455: The inline duplicate detection inside iconsByPack.forEach
duplicates the logic in checkDuplicatesInIconNames; replace the exact- and
case-insensitive checks with a call to checkDuplicatesInIconNames by passing the
computed names list and a context string built from packIdentifier (e.g., "pack
\"$packIdentifier\"" or the same format used elsewhere for consistency). Locate
the block that computes names (IconNameFormatter.format) and remove the two
duplicate-check blocks, invoking checkDuplicatesInIconNames(names, context)
instead so all duplicate handling is centralized in that helper.
- Line 399: The validateDuplicates function currently takes iconNames but
recomputes names for the nested-packs branch using IconNameFormatter.format,
causing double work and leaking caller structure; change the signature of
validateDuplicates to accept a Map<File, String> (precompute icon names once at
the call site using IconNameFormatter.format for each File) or drop the
parameter and always derive names from files inside validateDuplicates, then
update all call sites to either pass the Map<File,String> or let the function
compute names; inside validateDuplicates (and the nestedPacks branch) use the
map lookup (or the single consistent derivation path) instead of calling
IconNameFormatter.format again to eliminate the duplicate computation and remove
the unused iconNames usage.

In
`@tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/jewel/textfield/ConfirmTextField.kt`:
- Around line 46-50: The LaunchedEffect(text) block can overwrite an actively
edited value; modify the condition to only apply external text when the text
differs AND the field is not focused (i.e., add a !focused check) so updates
propagate when idle but don't clobber in-progress edits; update the
LaunchedEffect(text) condition that calls state.setTextAndPlaceCursorAtEnd(text)
to include the focused guard.

In
`@tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/mode/iconpack/conversion/IconPackConversionViewModel.kt`:
- Around line 274-332: Sequential exact-then-case-insensitive dedup in
resolvedIcons can produce cascading collisions (e.g., "Test" → "Test1" collides
with existing "Test1"); update the logic in the IconPackConversionViewModel
resolver to repeat the dedupe pass until stable: extract the current
per-location dedupe logic (the block producing iconsWithResolvedExactDuplicates
and the lowercase pass) into a resolveOnce function that takes a list of
processedIcons and returns resolved list, then loop do { resolved =
resolveOnce(current); } while (resolved != current) so the resolver (used where
resolvedIcons is computed) re-runs until no new renames occur; retain use of
IconName and keep checkImportIssues as the final validation step.

In
`@tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/mode/iconpack/conversion/ui/util/IconValidation.kt`:
- Around line 44-92: The pack-identifier computation is duplicated in the
exact-duplicate and case-insensitive duplicate checks; extract it into a small
helper and call it from both groupBy blocks. Add a private helper (e.g.,
packIdentifier(pack: IconPack, useFlatPackage: Boolean): String or an extension
on BatchIcon.Valid) that returns the same string produced by the when(...) logic
used in lines with IconPack.Single / IconPack.Nested, then replace the
duplicated when blocks in both groupBy lambdas with a call to that helper and
keep the rest of the groupBy (pairing with it.iconName.name or
it.iconName.name.lowercase()) unchanged.

In
`@tools/idea-plugin/src/test/kotlin/io/github/composegears/valkyrie/ui/screen/mode/iconpack/conversion/ui/util/IconValidationTest.kt`:
- Around line 167-175: The new test `nested without flatPackage, allow exact
duplicates in different nested packs` duplicates the existing `nested, allow
duplicates for different currentNestedPack` test; either remove the older
`nested, allow duplicates for different currentNestedPack` test or instead
change one of them to explicitly document the API difference (e.g., keep the new
test to assert the explicit parameter `useFlatPackage = false` and delete or
rename the older test) so you don't maintain two assertions for the same
invariant; update/remove the redundant test that uses validIcon, nestedPack, and
checkImportIssues to keep a single clear test.
ℹ️ Review info

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 00cdda7 and d14a118.

📒 Files selected for processing (13)
  • components/parser/unified/src/commonTest/kotlin/io/github/composegears/valkyrie/parser/unified/util/IconNameFormatterTest.kt
  • tools/cli/CHANGELOG.md
  • tools/cli/src/main/kotlin/io/github/composegears/valkyrie/cli/command/SvgXmlToImageVectorCommand.kt
  • tools/cli/src/test/kotlin/io/github/composegears/valkyrie/cli/command/SvgXmlToImageVectorCommandTest.kt
  • tools/gradle-plugin/CHANGELOG.md
  • tools/gradle-plugin/src/main/kotlin/io/github/composegears/valkyrie/gradle/internal/task/GenerateImageVectorsTask.kt
  • tools/gradle-plugin/src/test/kotlin/io/github/composegears/valkyrie/gradle/FailedConfigurationTest.kt
  • tools/idea-plugin/CHANGELOG.md
  • tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/jewel/textfield/ConfirmTextField.kt
  • tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/mode/iconpack/conversion/IconPackConversionState.kt
  • tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/mode/iconpack/conversion/IconPackConversionViewModel.kt
  • tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/mode/iconpack/conversion/ui/util/IconValidation.kt
  • tools/idea-plugin/src/test/kotlin/io/github/composegears/valkyrie/ui/screen/mode/iconpack/conversion/ui/util/IconValidationTest.kt

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (4)
tools/gradle-plugin/src/main/kotlin/io/github/composegears/valkyrie/gradle/internal/task/GenerateImageVectorsTask.kt (1)

421-455: Extract duplicate-check logic to reuse checkDuplicatesInIconNames.

The exact-duplicate and case-insensitive-duplicate checks inside this loop (lines 425–454) are a near-verbatim copy of checkDuplicatesInIconNames. You can eliminate the duplication by delegating to that method:

♻️ Proposed refactor
                 iconsByPack.forEach { (packIdentifier, filesInPack) ->
                     val names = filesInPack.map { IconNameFormatter.format(name = it.name) }
-
-                    // Check exact duplicates within this pack/group
-                    val exactDuplicates = names
-                        .groupBy { it }
-                        .filter { it.value.size > 1 }
-                        .keys
-                        .toList()
-
-                    if (exactDuplicates.isNotEmpty()) {
-                        throw GradleException(
-                            "Found duplicate icon names in \"$packIdentifier\": ${exactDuplicates.joinToString(", ")}. " +
-                                "Each icon must have a unique name. " +
-                                "Please rename the source files to avoid duplicates.",
-                        )
-                    }
-
-                    // Check case-insensitive duplicates within this pack/group
-                    val caseInsensitiveDuplicates = names
-                        .groupBy { it.lowercase() }
-                        .filter { it.value.size > 1 && it.value.distinct().size > 1 }
-                        .values
-                        .flatten()
-                        .distinct()
-
-                    if (caseInsensitiveDuplicates.isNotEmpty()) {
-                        throw GradleException(
-                            "Found icon names that would collide on case-insensitive file systems (macOS/Windows) in \"$packIdentifier\": " +
-                                "${caseInsensitiveDuplicates.joinToString(", ")}. " +
-                                "These icons would overwrite each other during generation. " +
-                                "Please rename the source files to avoid case-insensitive duplicates.",
-                        )
-                    }
+                    checkDuplicatesInIconNames(names, "\"$packIdentifier\"")
                 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@tools/gradle-plugin/src/main/kotlin/io/github/composegears/valkyrie/gradle/internal/task/GenerateImageVectorsTask.kt`
around lines 421 - 455, The existing duplicate-detection block inside the
iconsByPack.forEach loop duplicates logic already implemented in
checkDuplicatesInIconNames; replace the in-loop exact-duplicate and
case-insensitive-duplicate code with a call to that helper (pass the pack
identifier and the formatted names list produced with IconNameFormatter.format)
and remove the local grouping/filtering logic and its GradleException throws;
ensure the call is placed where the current checks run (inside the
iconsByPack.forEach) so checkDuplicatesInIconNames handles throwing on
duplicates for each packIdentifier and names.
tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/mode/iconpack/conversion/ui/util/IconValidation.kt (1)

44-89: Pack-identifier computation is duplicated verbatim — consider extracting a helper.

The block computing packIdentifier at lines 49–55 (exact duplicates) and lines 72–78 (case-insensitive duplicates) is identical. The same logic also appears a third time in IconPackConversionViewModel.resolveImportIssues (lines 276–282). Extracting a small private extension keeps future changes to the pack-grouping rule in one place.

♻️ Suggested extraction

Add a shared private helper, e.g. in a companion file or at the top of IconValidation.kt:

+private fun BatchIcon.Valid.packIdentifier(useFlatPackage: Boolean): String =
+    when (val pack = iconPack) {
+        is IconPack.Single -> pack.iconPackName
+        is IconPack.Nested -> if (useFlatPackage) pack.iconPackName
+                              else "${pack.iconPackName}.${pack.currentNestedPack}"
+    }

Then simplify both groupBy keys:

-            .groupBy {
-                val packIdentifier = when (val pack = it.iconPack) {
-                    is IconPack.Single -> pack.iconPackName
-                    is IconPack.Nested -> when {
-                        useFlatPackage -> pack.iconPackName
-                        else -> "${pack.iconPackName}.${pack.currentNestedPack}"
-                    }
-                }
-                packIdentifier to it.iconName.name
-            }
+            .groupBy { it.packIdentifier(useFlatPackage) to it.iconName.name }
-            .groupBy {
-                val packIdentifier = when (val pack = it.iconPack) {
-                    is IconPack.Single -> pack.iconPackName
-                    is IconPack.Nested -> when {
-                        useFlatPackage -> pack.iconPackName
-                        else -> "${pack.iconPackName}.${pack.currentNestedPack}"
-                    }
-                }
-                packIdentifier to it.iconName.name.lowercase()
-            }
+            .groupBy { it.packIdentifier(useFlatPackage) to it.iconName.name.lowercase() }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/mode/iconpack/conversion/ui/util/IconValidation.kt`
around lines 44 - 89, The pack-identifier computation is duplicated in
IconValidation.checkImportIssues and also in
IconPackConversionViewModel.resolveImportIssues; extract a small private helper
(e.g., fun BatchIcon.Valid.packGroupKey(useFlatPackage: Boolean): Pair<String,
String> or an extension like BatchIcon.Valid.packIdentifier(useFlatPackage))
that returns the same Pair(packIdentifier, iconName) logic using IconPack.Single
/ IconPack.Nested and useFlatPackage, then replace the two groupBy lambdas in
IconValidation (and the similar code in
IconPackConversionViewModel.resolveImportIssues) to call that helper so all
grouping logic lives in one place and future changes are centralized.
tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/mode/iconpack/conversion/IconPackConversionViewModel.kt (2)

274-282: Same pack-identifier logic triplicated here (also appears twice in IconValidation.kt).

Consider exposing the helper suggested for IconValidation.kt from a shared location so all three sites stay in sync.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/mode/iconpack/conversion/IconPackConversionViewModel.kt`
around lines 274 - 282, The pack-identification logic used in the
processedIcons.groupBy (iconsByLocation) is duplicated with code in
IconValidation.kt; extract that when/pack logic into a single shared helper
(e.g., an extension function like IconPack.getPackIdentifier(isFlatPackage:
Boolean) or a util function getPackIdentifier(iconPack: IconPack, isFlatPackage:
Boolean)) and replace the inline when in IconPackConversionViewModel (the
processedIcons.groupBy block) and the duplicated occurrences in
IconValidation.kt to call the new shared helper so all three sites use the same
implementation.

259-259: resolveImportIssues launches without Dispatchers.Default, unlike every other state-mutation function.

deleteIcon, updateIconPack, and renameIcon all use Dispatchers.Default. The reworked body of resolveImportIssues now does non-trivial grouping, mapping, and counter bookkeeping over the full icon list, making the inconsistency more pronounced.

♻️ Proposed fix
-fun resolveImportIssues() = viewModelScope.launch {
+fun resolveImportIssues() = viewModelScope.launch(Dispatchers.Default) {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/mode/iconpack/conversion/IconPackConversionViewModel.kt`
at line 259, The resolveImportIssues function performs non-trivial CPU work but
launches on the main dispatcher; update its coroutine launcher to use
Dispatchers.Default like deleteIcon, updateIconPack, and renameIcon so heavy
grouping/mapping/counter work runs off the main thread. Locate
resolveImportIssues in IconPackConversionViewModel and change
viewModelScope.launch { ... } to viewModelScope.launch(Dispatchers.Default) {
... } (or use withContext(Dispatchers.Default) inside the existing coroutine)
ensuring all state-mutating calls that must run on the main thread are switched
back to Dispatchers.Main if needed.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@tools/gradle-plugin/src/main/kotlin/io/github/composegears/valkyrie/gradle/internal/task/GenerateImageVectorsTask.kt`:
- Around line 404-419: In generateIconsForNestedPacks, ensure the same set of
files is used for validation and generation: when nestedPacks.isNotEmpty() and
useFlatPackage is false, filter the incoming files to only those whose parent
folder matches a configured nestedPack.sourceFolder before you build
fileToNestedPack and iconsByPack; update validateDuplicates to operate on that
filtered list (or pass the filtered list into validateDuplicates) so files that
won't be generated are neither validated nor grouped into the root pack.

In
`@tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/mode/iconpack/conversion/IconPackConversionViewModel.kt`:
- Around line 291-331: The two-stage rename (in IconPackConversionViewModel
where iconsWithResolvedExactDuplicates and the subsequent
lowercaseGroups/lowercaseCounters pass produce IconName("$...${counter - 1}"))
can create new collisions; change the renaming logic to check each newly
generated candidate against the full set of already-committed names (original
names plus any names already assigned in the current pass) and loop/increment
the suffix until the candidate is unique before calling icon.copy(...). Apply
this uniqueness check when assigning names in both the exact-duplicate pass
(nameCounters usage) and the case-insensitive pass (lowercaseCounters usage) so
the method always produces names not present in the current committed set (use a
mutable Set<String> of committedNames updated when you assign a new IconName);
this will ensure convergence and avoid creating new duplicates that break
checkImportIssues.

---

Nitpick comments:
In
`@tools/gradle-plugin/src/main/kotlin/io/github/composegears/valkyrie/gradle/internal/task/GenerateImageVectorsTask.kt`:
- Around line 421-455: The existing duplicate-detection block inside the
iconsByPack.forEach loop duplicates logic already implemented in
checkDuplicatesInIconNames; replace the in-loop exact-duplicate and
case-insensitive-duplicate code with a call to that helper (pass the pack
identifier and the formatted names list produced with IconNameFormatter.format)
and remove the local grouping/filtering logic and its GradleException throws;
ensure the call is placed where the current checks run (inside the
iconsByPack.forEach) so checkDuplicatesInIconNames handles throwing on
duplicates for each packIdentifier and names.

In
`@tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/mode/iconpack/conversion/IconPackConversionViewModel.kt`:
- Around line 274-282: The pack-identification logic used in the
processedIcons.groupBy (iconsByLocation) is duplicated with code in
IconValidation.kt; extract that when/pack logic into a single shared helper
(e.g., an extension function like IconPack.getPackIdentifier(isFlatPackage:
Boolean) or a util function getPackIdentifier(iconPack: IconPack, isFlatPackage:
Boolean)) and replace the inline when in IconPackConversionViewModel (the
processedIcons.groupBy block) and the duplicated occurrences in
IconValidation.kt to call the new shared helper so all three sites use the same
implementation.
- Line 259: The resolveImportIssues function performs non-trivial CPU work but
launches on the main dispatcher; update its coroutine launcher to use
Dispatchers.Default like deleteIcon, updateIconPack, and renameIcon so heavy
grouping/mapping/counter work runs off the main thread. Locate
resolveImportIssues in IconPackConversionViewModel and change
viewModelScope.launch { ... } to viewModelScope.launch(Dispatchers.Default) {
... } (or use withContext(Dispatchers.Default) inside the existing coroutine)
ensuring all state-mutating calls that must run on the main thread are switched
back to Dispatchers.Main if needed.

In
`@tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/mode/iconpack/conversion/ui/util/IconValidation.kt`:
- Around line 44-89: The pack-identifier computation is duplicated in
IconValidation.checkImportIssues and also in
IconPackConversionViewModel.resolveImportIssues; extract a small private helper
(e.g., fun BatchIcon.Valid.packGroupKey(useFlatPackage: Boolean): Pair<String,
String> or an extension like BatchIcon.Valid.packIdentifier(useFlatPackage))
that returns the same Pair(packIdentifier, iconName) logic using IconPack.Single
/ IconPack.Nested and useFlatPackage, then replace the two groupBy lambdas in
IconValidation (and the similar code in
IconPackConversionViewModel.resolveImportIssues) to call that helper so all
grouping logic lives in one place and future changes are centralized.
ℹ️ Review info

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between d14a118 and 5356322.

📒 Files selected for processing (4)
  • tools/gradle-plugin/src/main/kotlin/io/github/composegears/valkyrie/gradle/internal/task/GenerateImageVectorsTask.kt
  • tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/jewel/textfield/ConfirmTextField.kt
  • tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/mode/iconpack/conversion/IconPackConversionViewModel.kt
  • tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/mode/iconpack/conversion/ui/util/IconValidation.kt
🚧 Files skipped from review as they are similar to previous changes (1)
  • tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/jewel/textfield/ConfirmTextField.kt

@egorikftp egorikftp force-pushed the bug/case-insensitive branch from 5356322 to d36fb4f Compare February 23, 2026 09:05
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/mode/iconpack/conversion/IconPackConversionViewModel.kt (1)

259-259: ⚠️ Potential issue | 🟠 Major

resolveImportIssues runs heavy computation on the main thread.

viewModelScope.launch {} with no dispatcher argument inherits Dispatchers.Main.immediate. Every other mutating function in this ViewModel (deleteIcon, updateIconPack, renameIcon, import, processText, processFiles) explicitly uses Dispatchers.Default for CPU-bound work. The two-stage deduplication here — multiple groupBy, nested map, and unbounded while loops over all icons — should follow the same pattern.

🐛 Proposed fix
-    fun resolveImportIssues() = viewModelScope.launch {
+    fun resolveImportIssues() = viewModelScope.launch(Dispatchers.Default) {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/mode/iconpack/conversion/IconPackConversionViewModel.kt`
at line 259, resolveImportIssues currently launches on the main dispatcher and
does heavy CPU work; change its coroutine to run on Dispatchers.Default (e.g.,
viewModelScope.launch(Dispatchers.Default) or wrap heavy sections in
withContext(Dispatchers.Default)) so the groupBy/map/while loops run off the
main thread, and if you must update UI state or LiveData inside
resolveImportIssues ensure those updates are wrapped with
withContext(Dispatchers.Main); reference resolveImportIssues,
viewModelScope.launch and Dispatchers.Default when making the change.
♻️ Duplicate comments (1)
tools/gradle-plugin/src/main/kotlin/io/github/composegears/valkyrie/gradle/internal/task/GenerateImageVectorsTask.kt (1)

408-414: ⚠️ Potential issue | 🟡 Minor

Incomplete fix: unmatched files still included in validation when useFlatPackage=true.

generateIconsForNestedPacks always skips files whose parent directory isn't in sourceFolderToNestedPack (the if (nestedPack != null) guard at line 282 is unconditional). The current fix filters filesToValidate only for the useFlatPackage=false branch, so when useFlatPackage=true, files outside any configured source folder are still validated against icons that will actually be generated — which can produce a false-positive GradleException.

The filter should be applied unconditionally:

🐛 Proposed fix
-                val filesToValidate = if (useFlatPackage) {
-                    files
-                } else {
-                    files.filter { file -> sourceFolderToNestedPack.containsKey(file.parentFile.name) }
-                }
+                val filesToValidate = files.filter { file ->
+                    sourceFolderToNestedPack.containsKey(file.parentFile.name)
+                }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@tools/gradle-plugin/src/main/kotlin/io/github/composegears/valkyrie/gradle/internal/task/GenerateImageVectorsTask.kt`
around lines 408 - 414, The validation currently only filters filesToValidate
when useFlatPackage is false, which lets files outside configured nested packs
slip through when useFlatPackage=true and produce false-positive
GradleException; change the logic so the sourceFolderToNestedPack filter is
applied unconditionally when building filesToValidate (regardless of
useFlatPackage) — update the filesToValidate computation near
useFlatPackage/filesToValidate to always filter by
sourceFolderToNestedPack.containsKey(file.parentFile.name) and keep
generateIconsForNestedPacks’ existing guard (nestedPack != null) unchanged so
validation matches the actual icon generation set.
🧹 Nitpick comments (5)
tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/jewel/textfield/ConfirmTextField.kt (1)

46-50: Guard the sync against active user input by skipping when the field is focused.

focused is already tracked in this composable. Without the guard, any external text change (e.g., triggered by duplicate-name resolution in the same composition cycle) will call setTextAndPlaceCursorAtEnd while the user is mid-edit, discarding their in-progress input. The bug is unlikely in the current use-case (duplicate detection fires only after a commit), but there's no structural protection.

♻️ Proposed fix
-    // Update text field state when external text changes
-    LaunchedEffect(text) {
-        if (state.text.toString() != text) {
-            state.setTextAndPlaceCursorAtEnd(text)
-        }
-    }
+    // Update text field state when external text changes, but only when
+    // the user is not actively editing to avoid discarding in-progress input.
+    LaunchedEffect(text) {
+        if (!focused && state.text.toString() != text) {
+            state.setTextAndPlaceCursorAtEnd(text)
+        }
+    }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/jewel/textfield/ConfirmTextField.kt`
around lines 46 - 50, The LaunchedEffect(text) sync can overwrite in-progress
user edits; change it to skip applying external text updates when the field is
currently focused—i.e., inside the LaunchedEffect that compares
state.text.toString() and text, first check the composable's focused flag and
only call state.setTextAndPlaceCursorAtEnd(text) when focused is false (guard
against active user input), keeping the existing equality check and the same
setTextAndPlaceCursorAtEnd logic.
tools/gradle-plugin/src/test/kotlin/io/github/composegears/valkyrie/gradle/FailedConfigurationTest.kt (1)

337-341: svgContent is copy-pasted identically into all six new test methods.

Extract it once at the class or companion-object level.

♻️ Proposed refactor

Add a companion object (or top-level private val) in the file:

 class FailedConfigurationTest : CommonGradleTest() {
+
+    companion object {
+        private val SVG_CONTENT = """
+            <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
+                <path d="M0 0h24v24H0z" fill="none"/>
+            </svg>
+        """.trimIndent()
+    }

Then replace each inline val svgContent = """…""".trimIndent() block with SVG_CONTENT.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@tools/gradle-plugin/src/test/kotlin/io/github/composegears/valkyrie/gradle/FailedConfigurationTest.kt`
around lines 337 - 341, The six tests duplicate the same svgContent string;
extract it into a single shared constant (e.g., a companion object constant
SVG_CONTENT or a top-level private val SVG_CONTENT) and replace each inline val
svgContent = """...""".trimIndent() in the test methods with references to
SVG_CONTENT; update imports or visibility if needed so the test methods can
access the constant (put it inside the test class companion object or as a
private top-level value).
tools/gradle-plugin/src/main/kotlin/io/github/composegears/valkyrie/gradle/internal/task/GenerateImageVectorsTask.kt (2)

450-456: distinct().size > 1 guard is redundant after the exact-duplicate check.

The exact-duplicate block always throws before reaching this line, so any group with size > 1 is already guaranteed to have distinct().size > 1. The same redundancy exists in checkDuplicatesInIconNames (lines 495–501). It's harmless but may mislead future readers into thinking exact duplicates could reach this branch.

♻️ Simplification
-                    val caseInsensitiveDuplicates = names
-                        .groupBy { it.lowercase() }
-                        .filter { it.value.size > 1 && it.value.distinct().size > 1 }
+                    val caseInsensitiveDuplicates = names
+                        .groupBy { it.lowercase() }
+                        .filter { it.value.size > 1 }

Apply the same change at lines 495–497.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@tools/gradle-plugin/src/main/kotlin/io/github/composegears/valkyrie/gradle/internal/task/GenerateImageVectorsTask.kt`
around lines 450 - 456, The filter condition that checks both it.value.size > 1
and it.value.distinct().size > 1 is redundant because exact-duplicate groups are
already handled earlier; update the grouping filter used to compute
caseInsensitiveDuplicates (the names.groupBy { it.lowercase() } ... .filter {
... }) to only check it.value.size > 1, and make the identical simplification
inside the checkDuplicatesInIconNames function where the same groupBy/filter
pattern appears so both places remove the unnecessary distinct().size > 1 guard.

398-474: validateDuplicates signature is asymmetric across branches.

The nested-packs branch uses only files (and re-calls IconNameFormatter.format at line 431, duplicating the work from line 108), while the non-nested branches use only iconNames and ignore files. Consider either:

  • Removing iconNames from the signature and always deriving names from files (consistent with the nested-packs branch), or
  • Removing files and building the pack-to-name grouping externally (passing a pre-grouped Map<String, List<String>> instead).

Either change also eliminates the double call to IconNameFormatter.format for the nested case.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@tools/gradle-plugin/src/main/kotlin/io/github/composegears/valkyrie/gradle/internal/task/GenerateImageVectorsTask.kt`
around lines 398 - 474, The validateDuplicates function currently takes both
files and iconNames causing inconsistency and duplicated
IconNameFormatter.format calls; change validateDuplicates to accept only files
(remove iconNames parameter), derive formatted names from the provided files
once (use IconNameFormatter.format in one place), update the non-nested branch
to call checkDuplicatesInIconNames(formatNames, "package
\"${packageName.get()}\"") or call the existing duplicate-check logic directly
with the derived names, and remove the extra formatting at line 431 so
nested-pack grouping reuses the already-formatted names; update all callers of
validateDuplicates accordingly to pass files only.
tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/mode/iconpack/conversion/ui/util/IconValidation.kt (1)

44-93: Extract the duplicated pack-identifier lambda into a shared local val.

The pack-identifier expression (lines 49–55) is copy-pasted verbatim for the case-insensitive block (lines 73–79). Promote it to a local val packIdentifier: (BatchIcon.Valid) -> String computed once before the two groupBy calls.

♻️ Proposed refactor
+        val packIdentifier: (BatchIcon.Valid) -> String = { icon ->
+            when (val pack = icon.iconPack) {
+                is IconPack.Single -> pack.iconPackName
+                is IconPack.Nested -> when {
+                    useFlatPackage -> pack.iconPackName
+                    else -> "${pack.iconPackName}.${pack.currentNestedPack}"
+                }
+            }
+        }
+
         val duplicates = this@checkImportIssues
             .filterIsInstance<BatchIcon.Valid>()
             .groupBy {
-                val packIdentifier = when (val pack = it.iconPack) {
-                    is IconPack.Single -> pack.iconPackName
-                    is IconPack.Nested -> when {
-                        useFlatPackage -> pack.iconPackName
-                        else -> "${pack.iconPackName}.${pack.currentNestedPack}"
-                    }
-                }
-                packIdentifier to it.iconName.name
+                packIdentifier(it) to it.iconName.name
             }
             ...

         val caseInsensitiveDuplicates = this@checkImportIssues
             .filterIsInstance<BatchIcon.Valid>()
             .groupBy {
-                val packIdentifier = when (val pack = it.iconPack) {
-                    is IconPack.Single -> pack.iconPackName
-                    is IconPack.Nested -> when {
-                        useFlatPackage -> pack.iconPackName
-                        else -> "${pack.iconPackName}.${pack.currentNestedPack}"
-                    }
-                }
-                packIdentifier to it.iconName.name.lowercase()
+                packIdentifier(it) to it.iconName.name.lowercase()
             }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/mode/iconpack/conversion/ui/util/IconValidation.kt`
around lines 44 - 93, Extract the duplicated pack-identifier logic into a single
local val (e.g., val packIdentifier: (BatchIcon.Valid) -> String) defined before
computing duplicates; implement it to return the same string currently built in
both groupBy lambdas (handling IconPack.Single and IconPack.Nested with
useFlatPackage and pack.currentNestedPack), then replace the two inline
packIdentifier constructions in the groupBy calls with a call to this function
(pass the BatchIcon.Valid instance to packIdentifier) so both the
exact-duplicate and case-insensitive-duplicate groupBy use the shared logic.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@tools/gradle-plugin/src/test/kotlin/io/github/composegears/valkyrie/gradle/FailedConfigurationTest.kt`:
- Around line 582-585: This test currently asserts the general collision message
and the pack identifier (ValkyrieIcons.Outlined) but omits asserting the
individual colliding icon names; update the test that calls failTask(root,
TASK_NAME) to also assert the specific icon names that collide (same pattern as
the other tests that use assertThat(result.output).contains(...)). Add
assertions like
assertThat(result.output).contains("<first-colliding-icon-name>") and
assertThat(result.output).contains("<second-colliding-icon-name>") matching the
actual names used in this test's setup so regressions in reported icon names are
detected.

In
`@tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/mode/iconpack/conversion/IconPackConversionViewModel.kt`:
- Around line 322-353: The Stage 2 uniqueness check uses
committedNames.contains(candidateName) which is case-sensitive; change this to a
case-insensitive guard by maintaining a parallel set of lowercase committed
names (e.g., committedNamesLower = mutableSetOf<String>() seeded from
committedNames.map { it.lowercase() }) and use
committedNamesLower.contains(candidateName.lowercase()) in the while loop, and
when you add a new candidate to committedNames also add
candidateName.lowercase() to committedNamesLower; update references around
iconsWithResolvedExactDuplicates, lowercaseCounters, IconName creation to use
this new lowercase set so collisions are prevented case-insensitively.

---

Outside diff comments:
In
`@tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/mode/iconpack/conversion/IconPackConversionViewModel.kt`:
- Line 259: resolveImportIssues currently launches on the main dispatcher and
does heavy CPU work; change its coroutine to run on Dispatchers.Default (e.g.,
viewModelScope.launch(Dispatchers.Default) or wrap heavy sections in
withContext(Dispatchers.Default)) so the groupBy/map/while loops run off the
main thread, and if you must update UI state or LiveData inside
resolveImportIssues ensure those updates are wrapped with
withContext(Dispatchers.Main); reference resolveImportIssues,
viewModelScope.launch and Dispatchers.Default when making the change.

---

Duplicate comments:
In
`@tools/gradle-plugin/src/main/kotlin/io/github/composegears/valkyrie/gradle/internal/task/GenerateImageVectorsTask.kt`:
- Around line 408-414: The validation currently only filters filesToValidate
when useFlatPackage is false, which lets files outside configured nested packs
slip through when useFlatPackage=true and produce false-positive
GradleException; change the logic so the sourceFolderToNestedPack filter is
applied unconditionally when building filesToValidate (regardless of
useFlatPackage) — update the filesToValidate computation near
useFlatPackage/filesToValidate to always filter by
sourceFolderToNestedPack.containsKey(file.parentFile.name) and keep
generateIconsForNestedPacks’ existing guard (nestedPack != null) unchanged so
validation matches the actual icon generation set.

---

Nitpick comments:
In
`@tools/gradle-plugin/src/main/kotlin/io/github/composegears/valkyrie/gradle/internal/task/GenerateImageVectorsTask.kt`:
- Around line 450-456: The filter condition that checks both it.value.size > 1
and it.value.distinct().size > 1 is redundant because exact-duplicate groups are
already handled earlier; update the grouping filter used to compute
caseInsensitiveDuplicates (the names.groupBy { it.lowercase() } ... .filter {
... }) to only check it.value.size > 1, and make the identical simplification
inside the checkDuplicatesInIconNames function where the same groupBy/filter
pattern appears so both places remove the unnecessary distinct().size > 1 guard.
- Around line 398-474: The validateDuplicates function currently takes both
files and iconNames causing inconsistency and duplicated
IconNameFormatter.format calls; change validateDuplicates to accept only files
(remove iconNames parameter), derive formatted names from the provided files
once (use IconNameFormatter.format in one place), update the non-nested branch
to call checkDuplicatesInIconNames(formatNames, "package
\"${packageName.get()}\"") or call the existing duplicate-check logic directly
with the derived names, and remove the extra formatting at line 431 so
nested-pack grouping reuses the already-formatted names; update all callers of
validateDuplicates accordingly to pass files only.

In
`@tools/gradle-plugin/src/test/kotlin/io/github/composegears/valkyrie/gradle/FailedConfigurationTest.kt`:
- Around line 337-341: The six tests duplicate the same svgContent string;
extract it into a single shared constant (e.g., a companion object constant
SVG_CONTENT or a top-level private val SVG_CONTENT) and replace each inline val
svgContent = """...""".trimIndent() in the test methods with references to
SVG_CONTENT; update imports or visibility if needed so the test methods can
access the constant (put it inside the test class companion object or as a
private top-level value).

In
`@tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/jewel/textfield/ConfirmTextField.kt`:
- Around line 46-50: The LaunchedEffect(text) sync can overwrite in-progress
user edits; change it to skip applying external text updates when the field is
currently focused—i.e., inside the LaunchedEffect that compares
state.text.toString() and text, first check the composable's focused flag and
only call state.setTextAndPlaceCursorAtEnd(text) when focused is false (guard
against active user input), keeping the existing equality check and the same
setTextAndPlaceCursorAtEnd logic.

In
`@tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/mode/iconpack/conversion/ui/util/IconValidation.kt`:
- Around line 44-93: Extract the duplicated pack-identifier logic into a single
local val (e.g., val packIdentifier: (BatchIcon.Valid) -> String) defined before
computing duplicates; implement it to return the same string currently built in
both groupBy lambdas (handling IconPack.Single and IconPack.Nested with
useFlatPackage and pack.currentNestedPack), then replace the two inline
packIdentifier constructions in the groupBy calls with a call to this function
(pass the BatchIcon.Valid instance to packIdentifier) so both the
exact-duplicate and case-insensitive-duplicate groupBy use the shared logic.
ℹ️ Review info

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 5356322 and d36fb4f.

📒 Files selected for processing (7)
  • tools/cli/src/main/kotlin/io/github/composegears/valkyrie/cli/command/SvgXmlToImageVectorCommand.kt
  • tools/gradle-plugin/src/main/kotlin/io/github/composegears/valkyrie/gradle/internal/task/GenerateImageVectorsTask.kt
  • tools/gradle-plugin/src/test/kotlin/io/github/composegears/valkyrie/gradle/FailedConfigurationTest.kt
  • tools/idea-plugin/CHANGELOG.md
  • tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/jewel/textfield/ConfirmTextField.kt
  • tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/mode/iconpack/conversion/IconPackConversionViewModel.kt
  • tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/mode/iconpack/conversion/ui/util/IconValidation.kt
🚧 Files skipped from review as they are similar to previous changes (1)
  • tools/idea-plugin/CHANGELOG.md

@egorikftp egorikftp force-pushed the bug/case-insensitive branch from d36fb4f to 99df540 Compare February 23, 2026 09:24
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

♻️ Duplicate comments (3)
tools/gradle-plugin/src/main/kotlin/io/github/composegears/valkyrie/gradle/internal/task/GenerateImageVectorsTask.kt (1)

410-414: Past comment addressed — unmatched files are now filtered before grouping.

The filter at line 413 ensures files not belonging to any configured nested pack folder are excluded from validation when useFlatPackage is false, matching the generation behavior in generateIconsForNestedPacks.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@tools/gradle-plugin/src/main/kotlin/io/github/composegears/valkyrie/gradle/internal/task/GenerateImageVectorsTask.kt`
around lines 410 - 414, The previous review concern has already been addressed
by filtering unmatched files before grouping; no functional code change is
needed—confirm and remove the duplicate review comment. Specifically, verify the
logic around filesToValidate (the useFlatPackage conditional and the
files.filter using sourceFolderToNestedPack) matches generateIconsForNestedPacks
behavior, then clean up the duplicate comment or review note.
tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/mode/iconpack/conversion/IconPackConversionViewModel.kt (1)

306-310: Past comments addressed — committedNames guard is now case-insensitive.

Both Stage 1 (line 306) and Stage 2 (line 340) now use committedNames.any { it.equals(candidateName, ignoreCase = true) }, which prevents the convergence and case-collision issues raised in the previous review.

Also applies to: 340-344

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/mode/iconpack/conversion/IconPackConversionViewModel.kt`
around lines 306 - 310, The uniqueness check must be consistently
case-insensitive in both stages: update the Stage 2 loop (the code around the
second occurrence that manipulates committedNames, candidateName, originalName
and suffix) to use committedNames.any { it.equals(candidateName, ignoreCase =
true) } (matching Stage 1) when deciding to increment suffix, and then add the
chosen candidateName to committedNames; this ensures both the Stage 1 and Stage
2 uniqueness loops use the same case-insensitive comparison and prevent
case-collision.
tools/gradle-plugin/src/test/kotlin/io/github/composegears/valkyrie/gradle/FailedConfigurationTest.kt (1)

582-587: Past comment addressed — icon name assertions now present.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@tools/gradle-plugin/src/test/kotlin/io/github/composegears/valkyrie/gradle/FailedConfigurationTest.kt`
around lines 582 - 587, The reviewer note indicates the previous request was
already addressed (the test now asserts the colliding icon names), so remove the
duplicate review marker and mark the comment as resolved; keep the assertions in
FailedConfigurationTest.kt (the failTask(root, TASK_NAME) call and subsequent
assertions for "ValkyrieIcons.Outlined", "TestIcon", and "Testicon") as-is and
simply clear the duplicate "[duplicate_comment]"/resolve the review thread.
🧹 Nitpick comments (3)
tools/gradle-plugin/src/main/kotlin/io/github/composegears/valkyrie/gradle/internal/task/GenerateImageVectorsTask.kt (1)

430-466: Consider extracting duplicate-detection logic to reduce duplication.

The exact-duplicate and case-insensitive-duplicate checks in the per-group loop (lines 434–465) are nearly identical to checkDuplicatesInIconNames (lines 479–510). The only difference is how the error message context string is formatted. You could unify them by having the loop call checkDuplicatesInIconNames(names, "\"$packIdentifier\"") directly.

♻️ Suggested consolidation
             iconsByPack.forEach { (packIdentifier, filesInPack) ->
                 val names = filesInPack.map { IconNameFormatter.format(name = it.name) }
-
-                // Check exact duplicates within this pack/group
-                val exactDuplicates = names
-                    .groupBy { it }
-                    .filter { it.value.size > 1 }
-                    .keys
-                    .toList()
-                    .sorted()
-
-                if (exactDuplicates.isNotEmpty()) {
-                    throw GradleException(
-                        "Found duplicate icon names in \"$packIdentifier\": ${exactDuplicates.joinToString(", ")}. " +
-                            "Each icon must have a unique name. " +
-                            "Please rename the source files to avoid duplicates.",
-                    )
-                }
-
-                // Check case-insensitive duplicates within this pack/group
-                val caseInsensitiveDuplicates = names
-                    .groupBy { it.lowercase() }
-                    .filter { it.value.size > 1 && it.value.distinct().size > 1 }
-                    .values
-                    .flatten()
-                    .distinct()
-                    .sorted()
-
-                if (caseInsensitiveDuplicates.isNotEmpty()) {
-                    throw GradleException(
-                        "Found icon names that would collide on case-insensitive file systems (macOS/Windows) in \"$packIdentifier\": " +
-                            "${caseInsensitiveDuplicates.joinToString(", ")}. " +
-                            "These icons would overwrite each other during generation. " +
-                            "Please rename the source files to avoid case-insensitive duplicates.",
-                    )
-                }
+                checkDuplicatesInIconNames(names, "\"$packIdentifier\"")
             }

Also applies to: 477-511

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@tools/gradle-plugin/src/main/kotlin/io/github/composegears/valkyrie/gradle/internal/task/GenerateImageVectorsTask.kt`
around lines 430 - 466, The duplicate-detection code inside the
iconsByPack.forEach (where you build names = filesInPack.map {
IconNameFormatter.format(...) }) duplicates the logic implemented in
checkDuplicatesInIconNames; replace the in-loop exact and case-insensitive
checks with a single call to checkDuplicatesInIconNames(names,
"\"$packIdentifier\"") (or add a context param to checkDuplicatesInIconNames if
needed) so the existing function emits the same package-specific error messages
and avoids repeating the grouping/filtering logic.
tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/mode/iconpack/conversion/ui/util/IconValidation.kt (1)

44-93: Consider extracting the shared pack-identifier logic.

The pack-identifier resolution (lines 49–55 and 73–79) is duplicated between the exact-duplicate and case-insensitive-duplicate blocks. A small local function would keep them in sync and reduce the surface for future drift.

♻️ Suggested extraction
fun List<BatchIcon>.checkImportIssues(useFlatPackage: Boolean = false): Map<ValidationError, List<IconName>> {
    return buildMap {
        // ... existing code for brokenIcons, empty names, spaces ...

        fun BatchIcon.Valid.packIdentifier(): String = when (val pack = iconPack) {
            is IconPack.Single -> pack.iconPackName
            is IconPack.Nested -> when {
                useFlatPackage -> pack.iconPackName
                else -> "${pack.iconPackName}.${pack.currentNestedPack}"
            }
        }

        val duplicates = this@checkImportIssues
            .filterIsInstance<BatchIcon.Valid>()
            .groupBy { it.packIdentifier() to it.iconName.name }
            // ... rest unchanged

        val caseInsensitiveDuplicates = this@checkImportIssues
            .filterIsInstance<BatchIcon.Valid>()
            .groupBy { it.packIdentifier() to it.iconName.name.lowercase() }
            // ... rest unchanged
    }
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/mode/iconpack/conversion/ui/util/IconValidation.kt`
around lines 44 - 93, Extract the duplicated pack-identifier logic into a small
local function inside checkImportIssues (e.g., fun
BatchIcon.Valid.packIdentifier(): String) that returns the same string used
currently for IconPack.Single and IconPack.Nested (respecting useFlatPackage and
"${pack.iconPackName}.${pack.currentNestedPack}" when not flat); then replace
the two inline groupBy lambdas in the duplicates and caseInsensitiveDuplicates
blocks to call it (groupBy { it.packIdentifier() to it.iconName.name } and
groupBy { it.packIdentifier() to it.iconName.name.lowercase() }) so the logic is
centralized and consistent.
tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/mode/iconpack/conversion/IconPackConversionViewModel.kt (1)

274-354: Two-stage deduplication logic is correct but has O(n²) characteristic per committedNames.any call.

Each while (committedNames.any { it.equals(candidateName, ignoreCase = true) }) performs a linear scan of the committed set. With large icon sets this could add up (quadratic in the worst case with many collisions). For typical batch sizes this won't be noticeable, but if you ever expect very large icon packs, consider using a parallel HashSet of lowercased names for O(1) lookups:

♻️ Suggested optimization
         val resolvedIcons = iconsByLocation.flatMap { (_, iconsInLocation) ->
             val committedNames = mutableSetOf<String>()
+            val committedNamesLower = mutableSetOf<String>()
+
+            fun commitName(name: String) {
+                committedNames.add(name)
+                committedNamesLower.add(name.lowercase())
+            }
+
+            fun isCommitted(name: String): Boolean = name.lowercase() in committedNamesLower

             // ... in while loops:
-            while (committedNames.any { it.equals(candidateName, ignoreCase = true) }) {
+            while (isCommitted(candidateName)) {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/mode/iconpack/conversion/IconPackConversionViewModel.kt`
around lines 274 - 354, The deduplication uses committedNames.any {
it.equals(..., ignoreCase = true) } causing O(n²) behavior; replace those linear
checks by maintaining a parallel HashSet<String> (e.g., committedLowercaseNames)
of lowercase names and use
committedLowercaseNames.contains(candidateName.lowercase()) for O(1) lookups,
and whenever you add to committedNames (in the exact-duplicate pass and in the
case-insensitive pass) also add the lowercase form to committedLowercaseNames;
update logic in the block that builds iconsWithResolvedExactDuplicates and the
subsequent case-insensitive pass (references: committedNames,
iconsWithResolvedExactDuplicates, lowercaseCounters, and the candidateName
generation) to use committedLowercaseNames for membership checks and inserts.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In
`@tools/gradle-plugin/src/main/kotlin/io/github/composegears/valkyrie/gradle/internal/task/GenerateImageVectorsTask.kt`:
- Around line 410-414: The previous review concern has already been addressed by
filtering unmatched files before grouping; no functional code change is
needed—confirm and remove the duplicate review comment. Specifically, verify the
logic around filesToValidate (the useFlatPackage conditional and the
files.filter using sourceFolderToNestedPack) matches generateIconsForNestedPacks
behavior, then clean up the duplicate comment or review note.

In
`@tools/gradle-plugin/src/test/kotlin/io/github/composegears/valkyrie/gradle/FailedConfigurationTest.kt`:
- Around line 582-587: The reviewer note indicates the previous request was
already addressed (the test now asserts the colliding icon names), so remove the
duplicate review marker and mark the comment as resolved; keep the assertions in
FailedConfigurationTest.kt (the failTask(root, TASK_NAME) call and subsequent
assertions for "ValkyrieIcons.Outlined", "TestIcon", and "Testicon") as-is and
simply clear the duplicate "[duplicate_comment]"/resolve the review thread.

In
`@tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/mode/iconpack/conversion/IconPackConversionViewModel.kt`:
- Around line 306-310: The uniqueness check must be consistently
case-insensitive in both stages: update the Stage 2 loop (the code around the
second occurrence that manipulates committedNames, candidateName, originalName
and suffix) to use committedNames.any { it.equals(candidateName, ignoreCase =
true) } (matching Stage 1) when deciding to increment suffix, and then add the
chosen candidateName to committedNames; this ensures both the Stage 1 and Stage
2 uniqueness loops use the same case-insensitive comparison and prevent
case-collision.

---

Nitpick comments:
In
`@tools/gradle-plugin/src/main/kotlin/io/github/composegears/valkyrie/gradle/internal/task/GenerateImageVectorsTask.kt`:
- Around line 430-466: The duplicate-detection code inside the
iconsByPack.forEach (where you build names = filesInPack.map {
IconNameFormatter.format(...) }) duplicates the logic implemented in
checkDuplicatesInIconNames; replace the in-loop exact and case-insensitive
checks with a single call to checkDuplicatesInIconNames(names,
"\"$packIdentifier\"") (or add a context param to checkDuplicatesInIconNames if
needed) so the existing function emits the same package-specific error messages
and avoids repeating the grouping/filtering logic.

In
`@tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/mode/iconpack/conversion/IconPackConversionViewModel.kt`:
- Around line 274-354: The deduplication uses committedNames.any {
it.equals(..., ignoreCase = true) } causing O(n²) behavior; replace those linear
checks by maintaining a parallel HashSet<String> (e.g., committedLowercaseNames)
of lowercase names and use
committedLowercaseNames.contains(candidateName.lowercase()) for O(1) lookups,
and whenever you add to committedNames (in the exact-duplicate pass and in the
case-insensitive pass) also add the lowercase form to committedLowercaseNames;
update logic in the block that builds iconsWithResolvedExactDuplicates and the
subsequent case-insensitive pass (references: committedNames,
iconsWithResolvedExactDuplicates, lowercaseCounters, and the candidateName
generation) to use committedLowercaseNames for membership checks and inserts.

In
`@tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/mode/iconpack/conversion/ui/util/IconValidation.kt`:
- Around line 44-93: Extract the duplicated pack-identifier logic into a small
local function inside checkImportIssues (e.g., fun
BatchIcon.Valid.packIdentifier(): String) that returns the same string used
currently for IconPack.Single and IconPack.Nested (respecting useFlatPackage and
"${pack.iconPackName}.${pack.currentNestedPack}" when not flat); then replace
the two inline groupBy lambdas in the duplicates and caseInsensitiveDuplicates
blocks to call it (groupBy { it.packIdentifier() to it.iconName.name } and
groupBy { it.packIdentifier() to it.iconName.name.lowercase() }) so the logic is
centralized and consistent.
ℹ️ Review info

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between d36fb4f and 99df540.

📒 Files selected for processing (7)
  • tools/cli/src/main/kotlin/io/github/composegears/valkyrie/cli/command/SvgXmlToImageVectorCommand.kt
  • tools/gradle-plugin/src/main/kotlin/io/github/composegears/valkyrie/gradle/internal/task/GenerateImageVectorsTask.kt
  • tools/gradle-plugin/src/test/kotlin/io/github/composegears/valkyrie/gradle/FailedConfigurationTest.kt
  • tools/idea-plugin/CHANGELOG.md
  • tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/jewel/textfield/ConfirmTextField.kt
  • tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/mode/iconpack/conversion/IconPackConversionViewModel.kt
  • tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/mode/iconpack/conversion/ui/util/IconValidation.kt
🚧 Files skipped from review as they are similar to previous changes (2)
  • tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/jewel/textfield/ConfirmTextField.kt
  • tools/cli/src/main/kotlin/io/github/composegears/valkyrie/cli/command/SvgXmlToImageVectorCommand.kt

@egorikftp egorikftp merged commit 3296153 into main Feb 23, 2026
3 checks passed
@egorikftp egorikftp deleted the bug/case-insensitive branch February 23, 2026 09:40
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Generated kt file missing when converting icons with similar names

1 participant