Skip to content

support CJK input methods by implementing UITextInput marked text - #4

Open
sinsoul wants to merge 1 commit into
h3nock:mainfrom
sinsoul:feature/cjk-marked-text-input
Open

support CJK input methods by implementing UITextInput marked text#4
sinsoul wants to merge 1 commit into
h3nock:mainfrom
sinsoul:feature/cjk-marked-text-input

Conversation

@sinsoul

@sinsoul sinsoul commented Jul 24, 2026

Copy link
Copy Markdown

Summary

  • Implement UITextInput marked text (composing) support so CJK input methods
    (Pinyin, Wubi, etc.) can establish a composing region, display candidates,
    and commit selected characters to the terminal.
  • Previously all marked-text methods were stubbed as no-ops, which caused the
    system IME to silently discard composing state — no candidate bar appeared
    and no CJK text could be entered via the software keyboard.

What changed

GhosttyTerminalResponderView.swift

  • Added storedMarkedText and storedMarkedSelectedRange properties to track
    the active composing state across the UITextInput extension in the shim.
  • insertText now clears any active composing state before forwarding the
    committed text to the terminal.
  • deleteBackward clears composing state (instead of sending a backspace key
    event) when an IME composing region is active.

GhosttyTerminalResponderTextInputShim.swift

  • setMarkedText stores the composing string and notifies inputDelegate,
    enabling the system candidate bar.
  • unmarkText commits the stored composing text to the terminal via
    submitTextInput, then clears the composing state. This covers the
    setMarkedText → unmarkText commit path used by the system Pinyin keyboard.
  • markedTextRange returns the active composing region (previously always nil).
  • text(in:) returns the composing string content for the marked range.
  • selectedTextRange and endOfDocument reflect the dynamic virtual document
    length while composing.
  • replace(_:withText:) clears composing state before forwarding replacement
    text.
  • firstRect(for:) and caretRect(for:) return a small rect at the bottom of
    the view so the system can anchor the candidate window on screen (previously
    returned .zero).

GhosttyTerminalResponderViewTests.swift

  • Added 8 unit tests covering all marked text paths: set/clear composing,
    unmark-commits-to-terminal, insert-clears-composing, delete-during-composing,
    replace-during-composing, nil-marked-range baseline, and dynamic document
    length.

Test plan

  • All 48 unit tests pass (40 existing + 8 new), zero regressions
  • Verified on device (iPhone 12 Pro, iOS 18) with system Pinyin keyboard:
    candidate bar appears, selecting a candidate commits the character to the
    terminal
  • English keyboard input continues to work unchanged
  • Spacebar long-press floating cursor gesture continues to work unchanged

The UITextInput shim previously stubbed all marked-text methods as
no-ops, which prevented CJK input methods (Pinyin, Wubi, etc.) from
establishing a composing region. Without a valid composing region the
system IME cannot display candidates or commit selected characters.

This change:

- Tracks composing state in storedMarkedText / storedMarkedSelectedRange
  so markedTextRange reports the active composing region to UIKit.
- Implements setMarkedText to store the composing string and notify the
  input delegate, allowing the candidate bar to appear.
- Implements unmarkText to commit the composed text to the terminal,
  covering the setMarkedText-then-unmarkText commit path used by the
  system Pinyin keyboard.
- Clears composing state in insertText, deleteBackward, and replace so
  all IME commit and cancel paths behave correctly.
- Adjusts endOfDocument, text(in:), selectedTextRange, and
  position(from:offset:) to reflect the dynamic virtual document length
  while composing.
- Returns a small rect at the bottom of the view from firstRect/caretRect
  so the system can anchor the candidate window on screen.
@sinsoul sinsoul closed this Jul 25, 2026
@h3nock

h3nock commented Jul 25, 2026

Copy link
Copy Markdown
Owner

hey @sinsoul, will take a look this and support for CJK input methods.

@h3nock h3nock reopened this Jul 25, 2026
@h3nock

h3nock commented Jul 28, 2026

Copy link
Copy Markdown
Owner

@coderabbitai full review

@coderabbitai

coderabbitai Bot commented Jul 28, 2026

Copy link
Copy Markdown
✅ Action performed

Full review finished.


Your included review limit is currently reached under our Fair Usage Limits Policy. This review may still proceed through usage-based billing if eligible. Your next included review will be available in 19 minutes.

@coderabbitai

coderabbitai Bot commented Jul 28, 2026

Copy link
Copy Markdown

Warning

Review limit reached

@h3nock, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 19 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: d83b7163-1d2e-4804-8086-ade48603cd5e

📥 Commits

Reviewing files that changed from the base of the PR and between 15b531f and 46df2fc.

📒 Files selected for processing (3)
  • RemuxApp/Sources/Ghostty/GhosttyTerminalResponderTextInputShim.swift
  • RemuxApp/Sources/Ghostty/GhosttyTerminalResponderView.swift
  • RemuxAppTests/GhosttyTerminalResponderViewTests.swift
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

@h3nock

h3nock commented Jul 28, 2026

Copy link
Copy Markdown
Owner

@codex 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: 46df2fce06

ℹ️ 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 on lines +49 to +53
let loc = storedMarkedSelectedRange.location == NSNotFound
? marked.count
: min(storedMarkedSelectedRange.location, marked.count)
let pos = GhosttyVirtualTextPosition(offset: loc)
return GhosttyVirtualTextRange(from: pos, to: pos)

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 Preserve the marked selection length

When an IME supplies a nonempty selectedRange—for example, while selecting a conversion segment in a Japanese or Chinese composition—the getter collapses it to [location, location] and discards storedMarkedSelectedRange.length. UIKit therefore observes no selected segment, which can disrupt clause conversion and candidate replacement; construct the range end from the supplied length, clamped to the marked document.

Useful? React with 👍 / 👎.

Comment on lines +40 to +42
private var documentLength: Int {
storedMarkedText?.count ?? 0
}

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 Use UTF-16 offsets for the virtual document

When marked text contains a supplementary or composed character, NSRange offsets supplied by UIKit are based on the marked NSString's UTF-16 representation, while String.count counts extended grapheme clusters. For example, a caret after 𠮷 arrives at offset 2 but this document reports length 1 and clamps it there, making selection and text ranges inconsistent; use UTF-16 length and UTF-16-aware slicing throughout the virtual position implementation.

Useful? React with 👍 / 👎.

Comment on lines +347 to +352
if storedMarkedText != nil {
inputDelegate?.textWillChange(self)
storedMarkedText = nil
storedMarkedSelectedRange = NSRange(location: NSNotFound, length: 0)
inputDelegate?.textDidChange(self)
return

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 Delete only one unit from active marked text

When a keyboard routes Backspace through UIKeyInput.deleteBackward() during a multi-character composition, this branch clears the entire marked buffer and emits neither an updated marked value nor a terminal backspace. A single delete therefore cancels all preedit text instead of deleting the selected or preceding character; update the marked buffer and selection, clearing it only once it becomes empty.

Useful? React with 👍 / 👎.

@vaayne

vaayne commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

@h3nock can we review this, current there is no way to input Chinese or like.

@h3nock h3nock closed this Aug 4, 2026
@h3nock h3nock reopened this Aug 4, 2026
@h3nock

h3nock commented Aug 4, 2026

Copy link
Copy Markdown
Owner

@h3nock can we review this, current there is no way to input Chinese or like.

sorry about that. will be included in the next build.

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.

4 participants