Skip to content

Commit c67924a

Browse files
unhappychoiceclaude
andcommitted
fix: resolve multibyte character input issues in typing core
Fix input handling issues when code contains multibyte characters such as ✓, ❌, 🔥, Japanese, Chinese, and other Unicode symbols. Previously, users experienced unresponsive input or incorrect character matching when typing these characters. Changes: - Replace byte-based position calculations with character-based calculations - Fix create_typing_text() to use chars().enumerate() instead of char_indices() - Fix create_display_text() to use character positions consistently - Fix boundary condition checks in is_completed() and can_accept_input() - Ensure proper completion detection for text containing multibyte characters Resolves issue where last character in multibyte text could not be input, and eliminates input freezing with Unicode content. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent 21a63fe commit c67924a

File tree

1 file changed

+40
-36
lines changed

1 file changed

+40
-36
lines changed

src/game/typing_core.rs

Lines changed: 40 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -310,18 +310,19 @@ impl TypingCore {
310310
let lines: Vec<&str> = original.lines().collect();
311311
let mut processed_lines = Vec::new();
312312
let mut position_mapping = Vec::new();
313-
let mut original_pos = 0;
313+
let mut original_char_pos = 0;
314314
let mut _processed_pos = 0;
315315

316316
for (line_idx, line) in lines.iter().enumerate() {
317-
let line_start_pos = original_pos;
317+
let line_start_char_pos = original_char_pos;
318318

319319
// Process this line, removing comments
320320
let mut line_result = String::new();
321321
let mut line_mapping = Vec::new();
322322

323-
for (char_idx, ch) in line.char_indices() {
324-
let char_pos = line_start_pos + char_idx;
323+
// Use enumerate() to get character position, not byte position
324+
for (char_idx_in_line, ch) in line.chars().enumerate() {
325+
let char_pos = line_start_char_pos + char_idx_in_line;
325326

326327
// Check if this character is in a comment
327328
let in_comment = comment_ranges
@@ -335,14 +336,16 @@ impl TypingCore {
335336
let is_leading = line_result.chars().all(|c| c.is_whitespace());
336337

337338
// Check if this is trailing whitespace or followed by comment
338-
let remaining_line = &line[char_idx..];
339-
let is_trailing = remaining_line.chars().all(|c| {
340-
c.is_whitespace()
341-
|| comment_ranges.iter().any(|&(start, end)| {
342-
let pos =
343-
line_start_pos + char_idx + remaining_line.find(c).unwrap_or(0);
344-
pos >= start && pos < end
345-
})
339+
let remaining_chars: Vec<char> = line.chars().skip(char_idx_in_line).collect();
340+
let is_trailing = remaining_chars.iter().all(|&c| {
341+
c.is_whitespace() || {
342+
// Calculate the absolute char position of this character
343+
let check_pos = char_pos
344+
+ remaining_chars.iter().position(|&rc| rc == c).unwrap_or(0);
345+
comment_ranges
346+
.iter()
347+
.any(|&(start, end)| check_pos >= start && check_pos < end)
348+
}
346349
});
347350

348351
// Skip leading and trailing whitespace, preserve internal spaces
@@ -366,15 +369,15 @@ impl TypingCore {
366369

367370
// Add line mappings to main mapping
368371
position_mapping.extend(line_mapping);
369-
original_pos += line.len();
372+
original_char_pos += line.chars().count();
370373

371374
// Handle newline character if not the last line
372375
if line_idx < lines.len() - 1 {
373376
if !is_empty_line {
374-
position_mapping.push(line_start_pos + line.len());
377+
position_mapping.push(line_start_char_pos + line.chars().count());
375378
_processed_pos += 1;
376379
}
377-
original_pos += 1; // Account for \n
380+
original_char_pos += 1; // Account for \n
378381
}
379382
}
380383

@@ -391,19 +394,19 @@ impl TypingCore {
391394
let mut position_mapping = Vec::new();
392395

393396
let lines: Vec<&str> = original_text.lines().collect();
394-
let mut original_pos = 0;
397+
let mut original_char_pos = 0;
395398

396399
for (line_idx, line) in lines.iter().enumerate() {
397-
let line_start = original_pos;
400+
let line_start_char_pos = original_char_pos;
398401

399402
// Check if this line has any content that needs typing (not just comments)
400403
let line_has_typeable_content = {
401404
let mut has_content = false;
402-
for (char_idx, line_ch) in line.char_indices() {
403-
let absolute_pos = line_start + char_idx;
405+
for (char_idx_in_line, line_ch) in line.chars().enumerate() {
406+
let absolute_char_pos = line_start_char_pos + char_idx_in_line;
404407
let in_comment = comment_ranges
405408
.iter()
406-
.any(|&(start, end)| absolute_pos >= start && absolute_pos < end);
409+
.any(|&(start, end)| absolute_char_pos >= start && absolute_char_pos < end);
407410

408411
if !in_comment && !line_ch.is_whitespace() {
409412
has_content = true;
@@ -414,16 +417,16 @@ impl TypingCore {
414417
};
415418

416419
// Find the position of the last typeable character in this line
417-
let last_typeable_pos = if line_has_typeable_content {
420+
let last_typeable_char_idx = if line_has_typeable_content {
418421
let mut last_pos = None;
419-
for (char_idx, line_ch) in line.char_indices() {
420-
let absolute_pos = line_start + char_idx;
422+
for (char_idx_in_line, line_ch) in line.chars().enumerate() {
423+
let absolute_char_pos = line_start_char_pos + char_idx_in_line;
421424
let in_comment = comment_ranges
422425
.iter()
423-
.any(|&(start, end)| absolute_pos >= start && absolute_pos < end);
426+
.any(|&(start, end)| absolute_char_pos >= start && absolute_char_pos < end);
424427

425428
if !in_comment && !line_ch.is_whitespace() {
426-
last_pos = Some(char_idx);
429+
last_pos = Some(char_idx_in_line);
427430
}
428431
}
429432
last_pos
@@ -432,8 +435,8 @@ impl TypingCore {
432435
};
433436

434437
// Process each character in the line
435-
for (char_idx, ch) in line.char_indices() {
436-
let char_original_pos = line_start + char_idx;
438+
for (char_idx_in_line, ch) in line.chars().enumerate() {
439+
let char_original_pos = line_start_char_pos + char_idx_in_line;
437440
position_mapping.push(char_original_pos);
438441

439442
if options.highlight_special_chars && ch == '\t' {
@@ -447,20 +450,20 @@ impl TypingCore {
447450
}
448451

449452
// Insert ↵ right after the last typeable character
450-
if options.add_newline_symbols && Some(char_idx) == last_typeable_pos {
451-
position_mapping.push(line_start + line.len()); // Position for ↵
453+
if options.add_newline_symbols && Some(char_idx_in_line) == last_typeable_char_idx {
454+
position_mapping.push(line_start_char_pos + line.chars().count()); // Position for ↵
452455
display_text.push('↵');
453456
}
454457
}
455458

456459
// Handle newline
457460
if line_idx < lines.len() - 1 {
458-
position_mapping.push(line_start + line.len()); // Position of \n
461+
position_mapping.push(line_start_char_pos + line.chars().count()); // Position of \n
459462
display_text.push('\n');
460463

461-
original_pos += line.len() + 1; // +1 for \n
464+
original_char_pos += line.chars().count() + 1; // +1 for \n
462465
} else {
463-
original_pos += line.len();
466+
original_char_pos += line.chars().count();
464467
}
465468
}
466469

@@ -489,12 +492,13 @@ impl TypingCore {
489492

490493
// Helper methods for typing logic
491494
pub fn is_position_at_line_end(&self, type_pos: usize) -> bool {
492-
if type_pos >= self.text_to_type.len() {
495+
let chars: Vec<char> = self.text_to_type.chars().collect();
496+
497+
if type_pos >= chars.len() {
493498
return true;
494499
}
495500

496501
// Check if current position is newline or if all following chars on line are whitespace
497-
let chars: Vec<char> = self.text_to_type.chars().collect();
498502
if chars.get(type_pos) == Some(&'\n') {
499503
return true;
500504
}
@@ -519,11 +523,11 @@ impl TypingCore {
519523

520524
// Helper methods for typing logic
521525
pub fn is_completed(&self) -> bool {
522-
self.current_position_to_type >= self.text_to_type.len()
526+
self.current_position_to_type >= self.text_to_type.chars().count()
523527
}
524528

525529
pub fn can_accept_input(&self) -> bool {
526-
self.current_position_to_type < self.text_to_type.len()
530+
self.current_position_to_type < self.text_to_type.chars().count()
527531
}
528532

529533
pub fn check_character_match(&self, input_char: char) -> bool {

0 commit comments

Comments
 (0)