Skip to content

fix(studio): IME 조합 삭제 count 를 char(scalar) 단위로 정정 (#2548) - #2549

Closed
kevin9327 wants to merge 1 commit into
edwardkim:develfrom
kevin9327:task/m100-2548-char-offset-scalar
Closed

fix(studio): IME 조합 삭제 count 를 char(scalar) 단위로 정정 (#2548)#2549
kevin9327 wants to merge 1 commit into
edwardkim:develfrom
kevin9327:task/m100-2548-char-offset-scalar

Conversation

@kevin9327

Copy link
Copy Markdown
Contributor

이슈 #2548 처리.
처리결과 문서: mydocs/report/task_m100_2548_report.md

문제

[#2337-review] 에서 확립된 계약 — WASM 삭제/조회 count 는 Rust Paragraph::delete_text_at 의 char(Unicode scalar) 단위이며 JS String.length(UTF-16)를 넘기면 astral 문자(😀 등)에서 실제보다 많이 지운다. 당시 InsertTextCommand.undo 와 HF/FN 경로에는 charCount() 가 적용됐으나 IME 조합 경로는 누락돼 있었습니다.

this.compositionLength = text.length;               // UTF-16 code unit
this.deleteTextAt(anchor, this.compositionLength);  // scalar count 로 전달

deleteTextAt(pos, count)count 를 그대로 wasm.deleteText / deleteTextInCell / deleteTextInCellByPath / deleteTextInHeaderFooter / deleteTextInFootnote 에 넘깁니다. iOS 조합 폴백 _iosLength 도 동일합니다.

영향: 조합 중 후보에 astral 문자가 포함되면(일본어·중국어 IME 의 이모지 후보 등) 다음 조합 업데이트가 뒤따르는 실제 문자까지 지우고, 조합 종료 시 getTextAt 도 이웃 문자를 포함해 읽어 undo 레코드가 부정확해집니다.

분석 — 오탐 배제 (중요)

최초 이슈에는 InsertTextCommand.execute()charOffset + this.text.length 도 함께 적었으나 검증 결과 오탐이라 철회했습니다(이슈 본문에 정정 고지). 그 값은 커서 오프셋 이고 저장소가 UTF-16 관례로 의도적으로 고정해 둔 것입니다:

  • command.ts:922 charCount 주석 — "커서 오프셋은 studio 의 UTF-16 관례를 유지하므로 여기서만 char 단위를 쓴다."
  • tests/undo-delete-char-count.test.ts — "커서 오프셋(charOffset + text.length)은 studio 의 UTF-16 관례를 유지하므로 제외하고, …"

따라서 본 PR 은 삭제/조회 count 에만 적용하고 커서 오프셋은 손대지 않았습니다(:497, :615, :634 불변).

변경

rhwp-studio/src/engine/input-handler-text.tscharCount() 헬퍼 추가(계약 주석 포함), compositionLength/_iosLength 를 scalar 로 계산.

검증

  • 신규 정적 소스 가드 3건 tests/ime-composition-char-count.test.ts — 선례 undo-delete-char-count.test.ts 와 동일 방식.
  • red→green 실증: 수정을 되돌리면 2건 실패, 복원하면 3건 통과.
  • 회귀: node --test tests/*.test.ts436 pass / 1 fail. 유일한 실패 cell-flow-boundary.test.ts 는 변경을 git stash 로 제거한 깨끗한 devel 에서도 pass 0 / fail 1 로 동일 실패함을 확인 — 사전 실패이며 본 변경과 무관.

미실행 (투명 고지)

  • 행위 증명(브라우저 왕복) 미실행 — 선례 테스트도 "행위 증명은 브라우저 왕복(PR 검증)" 방침을 주석에 명시하고 있어 그에 따랐습니다.
  • PR CI 전체 검증(cargo test --verbose, cargo clippy -- -D warnings) 미실행 — 저장소 규약상 작업지시자 별도 승인 사항입니다. 본 PR 은 Rust 코드를 변경하지 않습니다.

잔여

이슈에 함께 적은 각주 커서 경계·단어 경계 탐색(cursor.ts)은 커서 오프셋 계열이라 위 UTF-16 관례와의 관계를 먼저 확정해야 해서 분리했습니다(머리말/꼬리말은 WASM info.charCount 를 쓰는데 각주판은 JS .length 인 비대칭이 있어 별도 판단 필요).

[edwardkim#2337-review] 계약: WASM 삭제/조회 count 는 Rust Paragraph::delete_text_at 의
char(Unicode scalar) 단위이며, JS String.length(UTF-16)를 넘기면 astral 문자에서
실제보다 많이 지운다. 당시 undo/HF/FN 경로에는 charCount() 가 적용됐지만 IME
조합 경로는 누락돼 있었다.

compositionLength(:488)와 iOS 폴백 _iosLength(:553)는 deleteTextAt 의 삭제
count 로 전달되므로(deleteTextAt → wasm.deleteText*) scalar 여야 한다. 조합 중
후보에 astral 문자가 있으면 다음 업데이트가 뒤따르는 실제 문자까지 지우고,
조합 종료 시 getTextAt 도 이웃 문자를 포함해 읽어 undo 레코드가 부정확해진다.

커서 오프셋(anchor.charOffset + text.length 등)은 studio 의 UTF-16 관례를
유지하므로 손대지 않았다 — command.ts charCount 주석과
tests/undo-delete-char-count.test.ts 가 그 관례를 명시한다.

검증:
- 신규 정적 소스 가드 3건(tests/ime-composition-char-count.test.ts). 수정을
  되돌리면 2건 실패, 복원하면 3건 통과(red→green 실증).
- 전체 studio 테스트 436 pass / 1 fail. 유일한 실패 cell-flow-boundary 는
  깨끗한 devel 에서도 동일하게 실패하는 사전 실패임을 stash 로 확인.

처리결과 문서: mydocs/report/task_m100_2548_report.md
@edwardkim
edwardkim self-requested a review July 21, 2026 07:08
@edwardkim edwardkim added the bug Something isn't working label Jul 21, 2026
@edwardkim edwardkim added this to the v1.0.0 milestone Jul 21, 2026
@edwardkim

Copy link
Copy Markdown
Owner

통합 PR #2676 에 원 커밋을 cherry-pick(-x)해 merge 되었습니다 (819785c). red→green 재실증(소스 원복 시 신규 테스트 3건 FAIL) + tsc·npm 463/463 green. IME 조합 삭제를 char scalar 단위로 정정한 처리 확인. 기여 감사합니다. @kevin9327

@edwardkim edwardkim closed this Jul 21, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants