@@ -10,6 +10,136 @@
1010#include <string.h>
1111#include <ctype.h>
121213+#ifdef MRB_UTF8_STRING
14+/*
15+ * UTF-8 helper functions
16+ * These are only compiled when MRB_UTF8_STRING is defined
17+ */
18+19+/* Check if byte is a UTF-8 lead byte (not a continuation byte) */
20+static mrb_bool
21+utf8_islead(unsigned char c)
22+{
23+return (c & 0xC0) != 0x80;
24+}
25+26+/* UTF-8 character length table indexed by (first_byte >> 3) */
27+static const char utf8_len_table[] = {
28+1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, /* 0x00-0x7F: ASCII */
29+0, 0, 0, 0, 0, 0, 0, 0, /* 0x80-0xBF: continuation (invalid start) */
30+2, 2, 2, 2, /* 0xC0-0xDF: 2-byte sequences */
31+3, 3, /* 0xE0-0xEF: 3-byte sequences */
32+4, /* 0xF0-0xF7: 4-byte sequences */
33+0 /* 0xF8-0xFF: invalid */
34+};
35+36+/*
37+ * Get byte length of UTF-8 character at position
38+ * Returns 1 for invalid sequences (safe fallback)
39+ */
40+static size_t
41+utf8_char_len(const char *p, const char *end)
42+{
43+size_t len;
44+if (p >= end) return 0;
45+46+len = (size_t)utf8_len_table[(unsigned char)p[0] >> 3];
47+if (len == 0 || len > (size_t)(end - p)) return 1;
48+49+/* Validate continuation bytes */
50+switch (len) {
51+case 4:
52+if (!utf8_islead((unsigned char)p[3])) break; /* continuation expected */
53+return 1;
54+case 3:
55+if (!utf8_islead((unsigned char)p[2])) break;
56+return 1;
57+case 2:
58+if (!utf8_islead((unsigned char)p[1])) break;
59+return 1;
60+ }
61+return len;
62+}
63+64+/*
65+ * Find start of previous UTF-8 character
66+ * Returns byte offset from start of string to the previous character
67+ * If at position 0, returns 0
68+ */
69+static size_t
70+utf8_prev_char_start(const char *str, size_t pos)
71+{
72+size_t i;
73+if (pos == 0) return 0;
74+75+/* Scan back to find a lead byte (max 4 bytes back) */
76+for (i = 1; i <= 4 && i <= pos; i++) {
77+if (utf8_islead((unsigned char)str[pos - i])) {
78+return pos - i;
79+ }
80+ }
81+/* No lead byte found, assume single byte */
82+return pos - 1;
83+}
84+85+/*
86+ * Calculate display width for a UTF-8 character
87+ * Returns 2 for CJK/wide characters, 1 for others
88+ *
89+ * This is a simplified version - proper implementation would use wcwidth()
90+ * We detect East Asian Wide characters by their code point ranges:
91+ * - CJK Unified Ideographs: U+4E00-U+9FFF (3-byte UTF-8: E4-E9)
92+ * - Hiragana/Katakana: U+3040-U+30FF (3-byte UTF-8: E3 81-83)
93+ * - Full-width forms: U+FF00-U+FFEF (3-byte UTF-8: EF BC-BF)
94+ */
95+static int
96+utf8_char_width(const char *p, const char *end)
97+{
98+unsigned char c = (unsigned char)p[0];
99+100+if (c < 0x80) return 1; /* ASCII */
101+if (c < 0xE0) return 1; /* 2-byte (Latin extended, etc.) */
102+103+/* 3-byte sequences - check for wide characters */
104+if (c >= 0xE3 && c <= 0xE9 && (end - p) >= 3) {
105+/* CJK and Japanese ranges are typically double-width */
106+return 2;
107+ }
108+if (c == 0xEF && (end - p) >= 3) {
109+unsigned char c2 = (unsigned char)p[1];
110+if (c2 >= 0xBC && c2 <= 0xBF) {
111+/* Full-width ASCII and symbols */
112+return 2;
113+ }
114+ }
115+116+/* 4-byte sequences (emoji, etc.) - typically double-width */
117+if (c >= 0xF0) return 2;
118+119+return 1;
120+}
121+122+/*
123+ * Calculate display column from byte position
124+ * Sums up the display width of all characters before the byte position
125+ */
126+static size_t
127+utf8_display_col(const char *str, size_t byte_pos)
128+{
129+size_t col = 0;
130+const char *p = str;
131+const char *end = str + byte_pos;
132+133+while (p < end) {
134+size_t char_len = utf8_char_len(p, str + byte_pos + 4); /* +4 for safety */
135+if (char_len == 0) break;
136+col += (size_t)utf8_char_width(p, end);
137+p += char_len;
138+ }
139+return col;
140+}
141+#endif /* MRB_UTF8_STRING */
142+13143/*
14144 * Helper: Initialize a single line
15145 */
@@ -76,6 +206,7 @@ line_insert_at(mirb_line *line, size_t pos, char c)
76206/*
77207 * Helper: Delete character at position in line
78208 */
209+#ifndef MRB_UTF8_STRING
79210static mrb_bool
80211line_delete_at(mirb_line *line, size_t pos)
81212{
@@ -85,6 +216,23 @@ line_delete_at(mirb_line *line, size_t pos)
85216line->len--;
86217return TRUE;
87218}
219+#endif
220+221+#ifdef MRB_UTF8_STRING
222+/*
223+ * Helper: Delete N bytes at position in line (for UTF-8 multibyte chars)
224+ */
225+static mrb_bool
226+line_delete_bytes_at(mirb_line *line, size_t pos, size_t count)
227+{
228+if (pos >= line->len || count == 0) return FALSE;
229+if (pos + count > line->len) count = line->len - pos;
230+231+memmove(line->data + pos, line->data + pos + count, line->len - pos - count + 1);
232+line->len -= count;
233+return TRUE;
234+}
235+#endif
8823689237/*
90238 * Helper: Set line content
@@ -336,11 +484,22 @@ mirb_buffer_delete_back(mirb_buffer *buf)
336484if (buf->cursor_col > 0) {
337485/* Delete within line */
338486mirb_line *line = &buf->lines[buf->cursor_line];
487+#ifdef MRB_UTF8_STRING
488+/* Find start of previous UTF-8 character and delete entire character */
489+size_t prev_pos = utf8_prev_char_start(line->data, buf->cursor_col);
490+size_t char_len = buf->cursor_col - prev_pos;
491+if (line_delete_bytes_at(line, prev_pos, char_len)) {
492+buf->cursor_col = prev_pos;
493+buf->modified = TRUE;
494+return TRUE;
495+ }
496+#else
339497if (line_delete_at(line, buf->cursor_col - 1)) {
340498buf->cursor_col--;
341499buf->modified = TRUE;
342500return TRUE;
343501 }
502+#endif
344503 }
345504else if (buf->cursor_line > 0) {
346505/* Join with previous line */
@@ -378,10 +537,20 @@ mirb_buffer_delete_forward(mirb_buffer *buf)
378537379538if (buf->cursor_col < line->len) {
380539/* Delete within line */
540+#ifdef MRB_UTF8_STRING
541+/* Delete entire UTF-8 character at cursor */
542+size_t char_len = utf8_char_len(line->data + buf->cursor_col,
543+line->data + line->len);
544+if (line_delete_bytes_at(line, buf->cursor_col, char_len)) {
545+buf->modified = TRUE;
546+return TRUE;
547+ }
548+#else
381549if (line_delete_at(line, buf->cursor_col)) {
382550buf->modified = TRUE;
383551return TRUE;
384552 }
553+#endif
385554 }
386555else if (buf->cursor_line < buf->line_count - 1) {
387556/* Join with next line */
@@ -497,7 +666,13 @@ mrb_bool
497666mirb_buffer_cursor_left(mirb_buffer *buf)
498667{
499668if (buf->cursor_col > 0) {
669+#ifdef MRB_UTF8_STRING
670+/* Move back to start of previous UTF-8 character */
671+mirb_line *line = &buf->lines[buf->cursor_line];
672+buf->cursor_col = utf8_prev_char_start(line->data, buf->cursor_col);
673+#else
500674buf->cursor_col--;
675+#endif
501676return TRUE;
502677 }
503678else if (buf->cursor_line > 0) {
@@ -517,7 +692,14 @@ mirb_buffer_cursor_right(mirb_buffer *buf)
517692mirb_line *line = &buf->lines[buf->cursor_line];
518693519694if (buf->cursor_col < line->len) {
695+#ifdef MRB_UTF8_STRING
696+/* Skip entire UTF-8 character */
697+size_t char_len = utf8_char_len(line->data + buf->cursor_col,
698+line->data + line->len);
699+buf->cursor_col += char_len;
700+#else
520701buf->cursor_col++;
702+#endif
521703return TRUE;
522704 }
523705else if (buf->cursor_line < buf->line_count - 1) {
@@ -835,3 +1017,19 @@ mirb_buffer_line_len(mirb_buffer *buf, size_t index)
8351017if (index >= buf->line_count) return 0;
8361018return buf->lines[index].len;
8371019}
1020+1021+/*
1022+ * Get cursor display column (visual column for terminal positioning)
1023+ * When MRB_UTF8_STRING is defined, calculates display width considering
1024+ * multibyte characters. Otherwise, returns the byte position directly.
1025+ */
1026+size_t
1027+mirb_buffer_cursor_display_col(mirb_buffer *buf)
1028+{
1029+#ifdef MRB_UTF8_STRING
1030+mirb_line *line = &buf->lines[buf->cursor_line];
1031+return utf8_display_col(line->data, buf->cursor_col);
1032+#else
1033+return buf->cursor_col;
1034+#endif
1035+}