Skip to content

fix: preserve uncased character positions#736

Merged
azat-io merged 1 commit into
azat-io:mainfrom
aarcamp:fix-base16-scheme-palette-sorting
Apr 17, 2026
Merged

fix: preserve uncased character positions#736
azat-io merged 1 commit into
azat-io:mainfrom
aarcamp:fix-base16-scheme-palette-sorting

Conversation

@aarcamp
Copy link
Copy Markdown
Contributor

@aarcamp aarcamp commented Apr 17, 2026

Description

The previous implementation of placeAllWithCaseBeforeAllWithOtherCase collected every cased-character slot across the whole alphabet and refilled them uppercase-first. This meant a single outlier cased character — e.g. the Greek combining mark ͅ(U+0345), which has a case mapping to Ι and lands at an early position under en-US locale collation — created a "cased slot" thousands of positions ahead of the Latin block. The refill then placed 'A' into that distant slot, putting uppercase letters ahead of digits and breaking the expected ordering (e.g. base0A sorting before base09).

Rewrite the method as a stable partition: characters in the "other" case move to the end; all other characters (including uncased ones like digits and punctuation) keep their relative positions. This preserves the documented uppercase-first / lowercase-first semantics on purely cased alphabets while leaving uncased characters untouched.

Add regression tests covering the partition semantics, the recommended alphabet under en-US locale, and a realistic base16 palette sort that reproduces the original user-facing failure.

Reviewer notes

Here is a concrete demonstration of what I ran into:

aaron@bow:~/src/honeymux$ bun eslint

/home/aaron/src/honeymux/src/util/config.ts
  132:7  error  Expected "base0A" to come before "base09"  perfectionist/sort-objects

error: "eslint" exited with code 1
aaron@bow:~/src/honeymux$ grep -A 5 -B 10 base0A src/util/config.ts
      base00: "#000000",
      base01: "#111111",
      base02: "#222222",
      base03: "#444444",
      base04: "#888888",
      base05: "#bbbbbb",
      base06: "#dddddd",
      base07: "#ffffff",
      base08: "#555555",
      base09: "#999999",
      base0A: "#cccccc",
      base0B: "#666666",
      base0C: "#777777",
      base0D: "#aaaaaa",
      base0E: "#eeeeee",
      base0F: "#333333",
aaron@bow:~/src/honeymux$ bun eslint --fix src/util/config.ts
aaron@bow:~/src/honeymux$ git diff src/util/config.ts
diff --git a/src/util/config.ts b/src/util/config.ts
index adc9f1a..19ff6b4 100644
--- a/src/util/config.ts
+++ b/src/util/config.ts
@@ -119,6 +119,7 @@ export function defaultConfig(): HoneymuxConfig {
     screenshotMaxHeightPixels: 65535,
     themeBuiltin: DEFAULT_SCHEME,
     themeCustom: {
+      base0A: "#cccccc",
       base00: "#000000",
       base01: "#111111",
       base02: "#222222",
@@ -129,8 +130,6 @@ export function defaultConfig(): HoneymuxConfig {
       base07: "#ffffff",
       base08: "#555555",
       base09: "#999999",
-      base0A: "#cccccc",
       base0B: "#666666",
       base0C: "#777777",
       base0D: "#aaaaaa",
aaron@bow:~/src/honeymux$

i.e. it was incorrectly re-ordering:

base00, ... base09, base0A, base0B, ...

to

base0A, base00, ..., base09, base0B, ...

which is clearly not what someone would want.


Pre-merge checklist

  • [✓] No similar open PR exists
  • [✓] Description explains problem/solution or links an issue
  • [✓] Tests added/updated when needed

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 17, 2026

📝 Walkthrough

Walkthrough

Refactors Alphabet.placeAllWithCaseBeforeAllWithOtherCase to a single-pass stable partition and adds unit tests validating uncased-character position retention and locale-sorted alphabet ordering constraints.

Changes

Cohort / File(s) Summary
Test Suite Additions
test/utils/alphabet.test.ts, test/utils/compare/compare-by-custom-sort.test.ts
Appended assertions: alphabet.test.ts now checks that uncased/symbol characters keep relative positions between surrounding cased characters and that locale-sorted ordering keeps digits before uppercase and uppercase before lowercase; compare-by-custom-sort.test.ts adds a sort-order test using a custom base16Options alphabet.
Alphabet Method Refactoring
utils/alphabet.ts
Rewrote Alphabet.placeAllWithCaseBeforeAllWithOtherCase from an index-based reassignment approach to a single-pass stable partition (collect keep and move lists then set this.characters = keep.concat(move)).

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description check ✅ Passed The description thoroughly explains the problem (outlier cased characters breaking ordering), the solution (stable partition), and includes concrete examples. All template sections are completed with comprehensive detail.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Title check ✅ Passed The title 'fix: preserve uncased character positions' accurately summarizes the main change—a bug fix to the placeAllWithCaseBeforeAllWithOtherCase method that now preserves uncased character positions through a stable partition approach.

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


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
Copy Markdown

@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.

🧹 Nitpick comments (1)
utils/alphabet.ts (1)

250-256: Clean fix — stable partition correctly preserves uncased positions.

The new implementation cleanly expresses the intent: keep retains uppercase + uncased characters in order (when caseToComeFirst === 'uppercase'), move collects lowercase. Since uppercaseCharacterCodePoint is only set when the character itself has an uppercase mapping (i.e. it is lowercase), isOtherCase correctly identifies the characters to move to the end, and the outlier-cased-character bug described in the PR (e.g. U+0345 creating a distant slot that gets refilled with an uppercase letter) is avoided because the relative positions of non-moved characters are preserved by filter.

♻️ Optional: single-pass partition

Minor readability/perf nit — avoid calling isOtherCase twice per character:

-    let isOtherCase = (character: Character): boolean =>
-      caseToComeFirst === 'uppercase' ?
-        character.uppercaseCharacterCodePoint !== undefined
-      : character.lowercaseCharacterCodePoint !== undefined
-    let keep = this.characters.filter(character => !isOtherCase(character))
-    let move = this.characters.filter(character => isOtherCase(character))
-    this.characters = [...keep, ...move]
+    let otherCaseKey =
+      caseToComeFirst === 'uppercase' ?
+        'uppercaseCharacterCodePoint'
+      : 'lowercaseCharacterCodePoint'
+    let keep: Character[] = []
+    let move: Character[] = []
+    for (let character of this.characters) {
+      ;(character[otherCaseKey] === undefined ? keep : move).push(character)
+    }
+    this.characters = [...keep, ...move]
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@utils/alphabet.ts` around lines 250 - 256, The current implementation calls
isOtherCase twice per character via two filters (keep and move), which is a
minor perf/readability issue; replace the double-filter with a single-pass
stable partition: iterate once over this.characters, call isOtherCase(character)
a single time and push each character into either keep or move accordingly, then
set this.characters = [...keep, ...move]; keep references to caseToComeFirst and
isOtherCase so the same selection logic and preservation of uncased positions
remain intact.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@utils/alphabet.ts`:
- Around line 250-256: The current implementation calls isOtherCase twice per
character via two filters (keep and move), which is a minor perf/readability
issue; replace the double-filter with a single-pass stable partition: iterate
once over this.characters, call isOtherCase(character) a single time and push
each character into either keep or move accordingly, then set this.characters =
[...keep, ...move]; keep references to caseToComeFirst and isOtherCase so the
same selection logic and preservation of uncased positions remain intact.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 03f49245-0079-4156-b680-2c65e7ed25c4

📥 Commits

Reviewing files that changed from the base of the PR and between f03c6ad and 8c91778.

📒 Files selected for processing (3)
  • test/utils/alphabet.test.ts
  • test/utils/compare/compare-by-custom-sort.test.ts
  • utils/alphabet.ts

@codecov
Copy link
Copy Markdown

codecov Bot commented Apr 17, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 100.00%. Comparing base (f03c6ad) to head (1dcb734).
⚠️ Report is 1 commits behind head on main.

Additional details and impacted files
@@            Coverage Diff            @@
##              main      #736   +/-   ##
=========================================
  Coverage   100.00%   100.00%           
=========================================
  Files          224       224           
  Lines         4402      4400    -2     
  Branches      1375      1375           
=========================================
- Hits          4402      4400    -2     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Copy link
Copy Markdown
Owner

@azat-io azat-io left a comment

Choose a reason for hiding this comment

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

Thank you for your PR!

Looks good to me. Could you fix ESLint and Prettier errors?

The previous implementation of placeAllWithCaseBeforeAllWithOtherCase
collected every cased-character slot across the whole alphabet and
refilled them uppercase-first. This meant a single outlier cased
character — e.g. the Greek combining mark ͅ(U+0345), which has a case
mapping to Ι and lands at an early position under en-US locale collation
— created a "cased slot" thousands of positions ahead of the Latin
block. The refill then placed 'A' into that distant slot, putting
uppercase letters ahead of digits and breaking the expected ordering
(e.g. base0A sorting before base09).

Rewrite the method as a stable partition: characters in the "other" case
move to the end; all other characters (including uncased ones like
digits and punctuation) keep their relative positions. This preserves
the documented uppercase-first / lowercase-first semantics on purely
cased alphabets while leaving uncased characters untouched.

Add regression tests covering the partition semantics, the recommended
alphabet under en-US locale, and a realistic base16 palette sort that
reproduces the original user-facing failure.

Signed-off-by: Aaron Campbell <aaron@monkey.org>
@aarcamp aarcamp force-pushed the fix-base16-scheme-palette-sorting branch from 8c91778 to 1dcb734 Compare April 17, 2026 14:08
@aarcamp
Copy link
Copy Markdown
Contributor Author

aarcamp commented Apr 17, 2026

Thank you for your PR!

Looks good to me. Could you fix ESLint and Prettier errors?

Yes sorry, done. I also updated the code to retain a single-pass implementation based on a suggestion from the bot here.

@azat-io azat-io changed the title fix(alphabet): preserve uncased character positions fix: preserve uncased character positions Apr 17, 2026
@aarcamp
Copy link
Copy Markdown
Contributor Author

aarcamp commented Apr 17, 2026

@azat-io Thanks for the approval! It’s not merged yet, but perhaps you batch them prior to release (I’m new here). Love this project by the way, it was a lifesaver to sort everything alphabetically in my project after the LLM just placed everything randomly during heavy dev haha.

@azat-io azat-io merged commit 3d75ce2 into azat-io:main Apr 17, 2026
13 checks passed
@azat-io
Copy link
Copy Markdown
Owner

azat-io commented Apr 18, 2026

Released in v5.9.0.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants