Skip to content

🐛 fix: handle non-ASCII character input - #23

Open
kqnade wants to merge 12 commits into
mainfrom
fix/japanese-charactor-render
Open

🐛 fix: handle non-ASCII character input#23
kqnade wants to merge 12 commits into
mainfrom
fix/japanese-charactor-render

Conversation

@kqnade

@kqnade kqnade commented Aug 9, 2026

Copy link
Copy Markdown
Owner

日本語のレンダーが正常に文字幅をレンダーできていなかった問題を修正

Summary by CodeRabbit

  • 新機能

    • 絵文字やワイド文字を含むテキストの端末表示幅に対応しました。
    • ANSI装飾やタブを考慮した正確な描画、切り詰め、カーソル位置計算に対応しました。
    • 矢印・編集・ファンクションキーの入力処理を改善しました。
  • 改善

    • 絵文字やサロゲートペアを1文字として移動・削除・Undoできるようになりました。
    • 上下行への移動でUnicode文字の列位置を正しく維持します。
    • Unicode文字の入力処理を改善しました。

@kqnade kqnade self-assigned this Aug 9, 2026
@coderabbitai

coderabbitai Bot commented Aug 9, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Unicodeの表示幅API、特殊キーコード、コードポイント単位の編集処理、端末セル幅による描画処理を追加・変更した。ANSIシーケンス、ワイド文字、絵文字、サロゲートペアのテストも追加した。

Changes

Unicode端末動作

Layer / File(s) Summary
Unicode表示幅API
rust/ecro-core/Cargo.toml, rust/ecro-core/ecro_core.h, rust/ecro-core/src/lib.rs, src/ecro/native.clj
UTF-8文字列の端末表示幅APIと、指定幅内の書記素単位UTF-16プレフィックス長APIを追加した。ClojureからネイティブAPIを呼び出し、失敗時はnilを返す。
特殊キーコード体系
rust/ecro-core/src/lib.rs, src/ecro/key.clj, test/ecro/key_test.clj, test/ecro/main_test.clj
特殊キーとファンクションキーにUnicode範囲外のコードを割り当てた。固定キーコード判定を共有定数へ置き換えた。
Unicode入力と編集境界
src/ecro/key.clj, src/ecro/buffer.clj, src/ecro/core.clj, src/ecro/undo.clj, test/ecro/buffer_test.clj, test/ecro/core_test.clj
入力、移動、削除、Undo/RedoをコードポイントまたはUTF-16文字列長で処理するよう変更した。補助文字の内部を指さないテストを追加した。
端末セル幅による描画
src/ecro/render.clj, test/ecro/render_test.clj
ANSIシーケンスを保持しながら、ワイド文字、絵文字、タブの表示幅を使って切り詰め、パディング、カーソル位置を計算するよう変更した。

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Renderer as render.clj
  participant NativeBridge as native.clj
  participant UnicodeCore as ecro_core
  Renderer->>NativeBridge: テキストの表示幅を要求
  NativeBridge->>UnicodeCore: UTF-8バイト列を渡す
  UnicodeCore-->>NativeBridge: 端末セル幅を返す
  NativeBridge-->>Renderer: 表示幅を返す
  Renderer->>Renderer: ANSIと書記素境界を維持して描画幅を調整
Loading

Poem

うさぎが跳ねて、幅を測る
絵文字ひとつ、境界を守る
ANSIの色はそのままに
キーも安全に空へ舞う
Unicodeの野を、軽やかに進む

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed タイトルは、非ASCII文字の入力と表示幅を含むUnicode対応の主な変更内容を簡潔に示しています。
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/japanese-charactor-render

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.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: eee4e0a905

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/ecro/key.clj
Comment on lines +222 to +223
(buffer/insert-text (:current-buffer editor-state)
(key-code->string key-code)))

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Keep supplementary characters atomic during editing

When the terminal supplies a valid code point above U+FFFF, such as an emoji, key-code->string produces a two-unit UTF-16 string and insert-text leaves the point after both units. Existing cursor and deletion operations in ecro.buffer advance or remove only one UTF-16 unit, so pressing Backspace immediately afterward leaves an unpaired surrogate and corrupts the buffer contents. Supplementary input must either be rejected or made atomic across cursor, deletion, and region operations.

Useful? React with 👍 / 👎.

Comment thread src/ecro/key.clj Outdated
Comment on lines +20 to +22
(defn- function-key-code?
[key-code]
(<= 2000 key-code 2255))

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Move function-key codes outside the Unicode scalar range

For characters whose code points fall from 2000 through 2255, this predicate silently treats ordinary input as a function key and discards it; for example, U+0800 (Samaritan Letter Alaf) arrives as integer 2048. The Rust adapter's encode_key_event uses the same integer space for KeyCode::Char(c) and KeyCode::F(n), so these cases cannot be distinguished here; function keys need disjoint sentinel values above U+10FFFF or an explicit event-kind field.

Useful? React with 👍 / 👎.

@kqnade

kqnade commented Aug 9, 2026

Copy link
Copy Markdown
Owner Author

@codex re-review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 03269abb56

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/ecro/buffer.clj
(if (< point (count text))
(let [deleted-char (get text point)
new-buf (assoc buf :text (str (subs text 0 point) (subs text (inc point))))]
(let [end (next-character-offset text point)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Keep vertical movement out of surrogate pairs

When a supplementary character starts a destination line, C-n/C-p can still put point between its UTF-16 units: for example, moving from column 1 in "a\n😀x" computes the destination as the second line's start plus 1. At that point this call treats the low surrogate as a standalone character, so forward deletion removes only that unit and corrupts the buffer. Fresh evidence for the previously reported supplementary-character issue is the unchanged UTF-16 column arithmetic in core/next-line and core/previous-line, which the new boundary helpers do not normalize; vertical movement must also select a code-point boundary.

Useful? React with 👍 / 👎.

Comment thread src/ecro/render.clj Outdated

(defn- wide-code-point?
[code-point]
(or (Character/isEmojiPresentation code-point)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Honor emoji presentation selectors when measuring width

This checks each code point's default presentation independently, so an emoji-style sequence such as ❤️ is measured as one cell: U+2764 has text presentation by default, while U+FE0F is assigned zero width. Terminals render the selected emoji presentation as two cells, causing the cursor to be placed one cell left and causing truncation or padding to use the wrong boundary. Width calculation needs to account for variation selectors or operate on emoji grapheme sequences.

Useful? React with 👍 / 👎.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@rust/ecro-core/src/lib.rs`:
- Around line 152-204: Add Rust tests for ecro_display_width and
ecro_prefix_utf16_length_for_width covering Japanese display widths,
supplementary Unicode characters, grapheme-aligned truncation, invalid UTF-8,
negative lengths, and null pointers, including each function’s documented -1
error result. Follow the project’s existing test conventions, write the tests
before implementation changes, and run lint after the tests pass.

In `@src/ecro/render.clj`:
- Around line 71-98: Update truncate-to-width so the loop’s normal end-of-input
path appends "\033[0m" when sgr-active? is true, including exact-width inputs
such as "\033[31mabc"; preserve the existing truncation reset behavior. Add a
render test covering this exact-width trailing-SGR case in the existing render
test suite.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: e0d9b06b-23ed-4701-a1a3-69667c672c9d

📥 Commits

Reviewing files that changed from the base of the PR and between 3e9a016 and f823e0f.

📒 Files selected for processing (14)
  • rust/ecro-core/Cargo.toml
  • rust/ecro-core/ecro_core.h
  • rust/ecro-core/src/lib.rs
  • src/ecro/buffer.clj
  • src/ecro/core.clj
  • src/ecro/key.clj
  • src/ecro/native.clj
  • src/ecro/render.clj
  • src/ecro/undo.clj
  • test/ecro/buffer_test.clj
  • test/ecro/core_test.clj
  • test/ecro/key_test.clj
  • test/ecro/main_test.clj
  • test/ecro/render_test.clj
📜 Review details
⏰ Context from checks skipped due to timeout. (1)
  • GitHub Check: clojure
🧰 Additional context used
📓 Path-based instructions (6)
**/*.{clj,cljs,cljc}

📄 CodeRabbit inference engine (AGENTS.md)

**/*.{clj,cljs,cljc}: Clojure code must hold all editor meaning and core editor state.
Run cljstyle check for Clojure linting.

Files:

  • test/ecro/core_test.clj
  • test/ecro/buffer_test.clj
  • src/ecro/undo.clj
  • test/ecro/main_test.clj
  • src/ecro/native.clj
  • src/ecro/core.clj
  • test/ecro/key_test.clj
  • src/ecro/buffer.clj
  • test/ecro/render_test.clj
  • src/ecro/key.clj
  • src/ecro/render.clj
**/*.{clj,cljs,cljc,rs}

📄 CodeRabbit inference engine (AGENTS.md)

**/*.{clj,cljs,cljc,rs}: The FFI boundary must use a polling event queue rather than callbacks.
Follow test-driven development: write the test first, implement the minimum needed to pass, refactor afterward, and run lint before declaring completion.

Files:

  • test/ecro/core_test.clj
  • test/ecro/buffer_test.clj
  • src/ecro/undo.clj
  • test/ecro/main_test.clj
  • src/ecro/native.clj
  • src/ecro/core.clj
  • test/ecro/key_test.clj
  • src/ecro/buffer.clj
  • test/ecro/render_test.clj
  • src/ecro/key.clj
  • src/ecro/render.clj
  • rust/ecro-core/src/lib.rs
test/ecro/**/*.{clj,cljs,cljc}

📄 CodeRabbit inference engine (AGENTS.md)

Use clojure.test for Clojure tests.

Files:

  • test/ecro/core_test.clj
  • test/ecro/buffer_test.clj
  • test/ecro/main_test.clj
  • test/ecro/key_test.clj
  • test/ecro/render_test.clj
**/*.clj

📄 CodeRabbit inference engine (AGENTS.md)

Use SCI-evaluated Clojure for the documented ~/.ecro/init.clj configuration file.

Files:

  • test/ecro/core_test.clj
  • test/ecro/buffer_test.clj
  • src/ecro/undo.clj
  • test/ecro/main_test.clj
  • src/ecro/native.clj
  • src/ecro/core.clj
  • test/ecro/key_test.clj
  • src/ecro/buffer.clj
  • test/ecro/render_test.clj
  • src/ecro/key.clj
  • src/ecro/render.clj
**/*.rs

📄 CodeRabbit inference engine (AGENTS.md)

Rust terminal-adapter code must not hold editor state; it must remain a stateless terminal adapter.

Files:

  • rust/ecro-core/src/lib.rs
rust/**/*.rs

📄 CodeRabbit inference engine (AGENTS.md)

Use cargo test for Rust tests, cargo clippy -- -D warnings for linting, and cargo fmt --check for formatting.

Files:

  • rust/ecro-core/src/lib.rs
🔇 Additional comments (5)
rust/ecro-core/Cargo.toml (1)

11-12: LGTM!

rust/ecro-core/ecro_core.h (1)

32-48: LGTM!

src/ecro/native.clj (1)

7-9: LGTM!

Also applies to: 44-45, 112-129

src/ecro/render.clj (1)

13-68: LGTM!

Also applies to: 114-150, 194-195, 240-240

test/ecro/render_test.clj (1)

44-47: LGTM!

Also applies to: 68-78, 92-107

Comment thread rust/ecro-core/src/lib.rs
Comment on lines +152 to +204
#[unsafe(no_mangle)]
/// Return the terminal-cell width of a UTF-8 byte slice.
///
/// # Safety
///
/// `text` must point to `length` readable bytes. The bytes must remain valid for the duration of
/// the call.
pub unsafe extern "C" fn ecro_display_width(text: *const u8, length: i32) -> i32 {
if text.is_null() || length < 0 {
return -1;
}

let bytes = unsafe { std::slice::from_raw_parts(text, length as usize) };
match std::str::from_utf8(bytes) {
Ok(text) => i32::try_from(text.width()).unwrap_or(-1),
Err(_) => -1,
}
}

#[unsafe(no_mangle)]
/// Return the UTF-16 length of the longest grapheme-aligned prefix within `maximum_width` cells.
///
/// # Safety
///
/// `text` must point to `length` readable bytes. The bytes must remain valid for the duration of
/// the call.
pub unsafe extern "C" fn ecro_prefix_utf16_length_for_width(
text: *const u8,
length: i32,
maximum_width: i32,
) -> i32 {
if text.is_null() || length < 0 || maximum_width < 0 {
return -1;
}

let bytes = unsafe { std::slice::from_raw_parts(text, length as usize) };
let Ok(text) = std::str::from_utf8(bytes) else {
return -1;
};

let mut current_width = 0;
let mut utf16_length = 0;
for grapheme in text.graphemes(true) {
let grapheme_width = grapheme.width();
if current_width + grapheme_width > maximum_width as usize {
break;
}
current_width += grapheme_width;
utf16_length += grapheme.encode_utf16().count();
}

i32::try_from(utf16_length).unwrap_or(-1)
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

新しい C ABI の境界ケースを Rust テストで固定してください。

ecro_display_widthecro_prefix_utf16_length_for_width を呼び出すテストがありません。日本語の幅、補助文字、書記素境界、無効 UTF-8、負の長さ、null ポインタの戻り値をテストしてください。これにより Clojure 側の描画契約を固定できます。

As per coding guidelines, “Follow test-driven development: write the test first, implement the minimum needed to pass, refactor afterward, and run lint before declaring completion.”

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@rust/ecro-core/src/lib.rs` around lines 152 - 204, Add Rust tests for
ecro_display_width and ecro_prefix_utf16_length_for_width covering Japanese
display widths, supplementary Unicode characters, grapheme-aligned truncation,
invalid UTF-8, negative lengths, and null pointers, including each function’s
documented -1 error result. Follow the project’s existing test conventions,
write the tests before implementation changes, and run lint after the tests
pass.

Source: Coding guidelines

Comment thread src/ecro/render.clj
Comment on lines +71 to +98
(defn- truncate-to-width
[^String text width]
(let [result (StringBuilder.)]
(loop [offset 0
current-width 0
sgr-active? false]
(if (< offset (count text))
(if-let [ansi-end (ansi-csi-end text offset)]
(do
(.append result (subs text offset ansi-end))
(recur ansi-end
current-width
(sgr-active-after text offset ansi-end sgr-active?)))
(let [segment-end (plain-segment-end text offset false)
segment (subs text offset segment-end)
segment-width (terminal-width segment)
next-width (+ current-width segment-width)]
(if (<= next-width width)
(do
(.append result segment)
(recur segment-end next-width sgr-active?))
(do
(let [prefix-length (terminal-prefix-length segment (- width current-width))]
(.append result (subs segment 0 prefix-length)))
(when sgr-active?
(.append result "\033[0m"))
(str result)))))
(str result)))))

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

末尾の有効な SGR をリセットしてください。

入力が幅をちょうど満たし、その末尾に有効な SGR がある場合、Line 98 はリセットを出力しません。
例えば、幅 3 の "\033[31mabc""\033[31mabc" のまま返ります。後続の描画に色または属性が継続します。

ループ終端でも sgr-active? が真の場合は "\033[0m" を追加してください。test/ecro/render_test.clj にこの境界ケースを追加してください。

修正案
-        (str result)))))
+        (do
+          (when sgr-active?
+            (.append result "\033[0m"))
+          (str result))))))
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
(defn- truncate-to-width
[^String text width]
(let [result (StringBuilder.)]
(loop [offset 0
current-width 0
sgr-active? false]
(if (< offset (count text))
(if-let [ansi-end (ansi-csi-end text offset)]
(do
(.append result (subs text offset ansi-end))
(recur ansi-end
current-width
(sgr-active-after text offset ansi-end sgr-active?)))
(let [segment-end (plain-segment-end text offset false)
segment (subs text offset segment-end)
segment-width (terminal-width segment)
next-width (+ current-width segment-width)]
(if (<= next-width width)
(do
(.append result segment)
(recur segment-end next-width sgr-active?))
(do
(let [prefix-length (terminal-prefix-length segment (- width current-width))]
(.append result (subs segment 0 prefix-length)))
(when sgr-active?
(.append result "\033[0m"))
(str result)))))
(str result)))))
(defn- truncate-to-width
[^String text width]
(let [result (StringBuilder.)]
(loop [offset 0
current-width 0
sgr-active? false]
(if (< offset (count text))
(if-let [ansi-end (ansi-csi-end text offset)]
(do
(.append result (subs text offset ansi-end))
(recur ansi-end
current-width
(sgr-active-after text offset ansi-end sgr-active?)))
(let [segment-end (plain-segment-end text offset false)
segment (subs text offset segment-end)
segment-width (terminal-width segment)
next-width (+ current-width segment-width)]
(if (<= next-width width)
(do
(.append result segment)
(recur segment-end next-width sgr-active?))
(do
(let [prefix-length (terminal-prefix-length segment (- width current-width))]
(.append result (subs segment 0 prefix-length)))
(when sgr-active?
(.append result "\033[0m"))
(str result)))))
(do
(when sgr-active?
(.append result "\033[0m"))
(str result))))))
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/ecro/render.clj` around lines 71 - 98, Update truncate-to-width so the
loop’s normal end-of-input path appends "\033[0m" when sgr-active? is true,
including exact-width inputs such as "\033[31mabc"; preserve the existing
truncation reset behavior. Add a render test covering this exact-width
trailing-SGR case in the existing render test suite.

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.

1 participant