Skip to content

fix(engine): ⌫ xoá dấu cách mở lại từ vừa chốt - issue #40 - #42

Merged
ptrinh merged 1 commit into
ptrinh:mainfrom
J2TEAM:fix/40-reopen-word-on-backspace
Aug 13, 2026
Merged

fix(engine): ⌫ xoá dấu cách mở lại từ vừa chốt - issue #40#42
ptrinh merged 1 commit into
ptrinh:mainfrom
J2TEAM:fix/40-reopen-word-on-backspace

Conversation

@J2TEAM

@J2TEAM J2TEAM commented Aug 12, 2026

Copy link
Copy Markdown
Contributor

thasytháy, space, ⌫ xoá dấu cách, rồi gõ a để bù dấu: ra tháya thay vì thấy. Space chốt từ và reset engine sạch; ⌫ với buffer rỗng chỉ resetContext rồi thả phím qua, không có đường nào dựng lại buffer nên a là ký tự mới. reEditWord không cứu được: nó chỉ bắt phím dấu THUẦN (s/f/r/x/j/z/w), a là chữ cái, và mặc định còn OFF. Mọi bộ gõ khác (iOS/macOS, UniKey, EVKey, OpenKey) đều khôi phục buffer khi ⌫ lùi vào từ đã chốt.

ENGINE — snapshot re-open, buffer cố định, zero-alloc:

commitBoundary/commitText nhớ đúng chuỗi phím vừa chốt, CHỈ khi màn hình đang hiển thị composed: không auto-restore về raw (google → screen là "google", buffer sẽ là "gôgle" ⇒ desync), không overflow (view 32 ký tự là prefix cũ của màn hình). reopenLastCommit() replay lại chuỗi đó qua feed() nên đặt thanh, lan ươ, ánh xạ ⌫ và quyết định boundary hành xử y như từ chưa từng bị chốt — không có code path thứ hai phải giữ đồng bộ (khác seed(), vốn dựng lại từ TEXT).

Replay được VERIFY như round-trip của seed(): settings đổi giữa commit và ⌫ mà từ không compose ra đúng như cũ thì reset + trả nil, không bao giờ compose đè lên text không tái tạo được. previousWordEnglish cũng khôi phục về giá trị TRƯỚC commit — từ mở lại phải nhìn ngữ cảnh của từ đứng trước nó, không phải ngữ cảnh chính nó vừa sinh ra.

Snapshot hẹp có chủ đích: sống qua đúng MỘT lần commit, chết ngay khi phím đầu của từ mới vào feed(), khi reset() (mất focus, click chuột, đổi app), hoặc khi caller gọi forgetLastCommit(). reset() tách làm hai — resetWord() cho hai đường commit (chúng vừa chụp snapshot xong), reset() công khai xoá cả hai.

CONTROLLERS — cùng một luật ở cả IMKit lẫn tap (máy người báo lỗi đi tap):

  1. Chỉ mở lại khi phím kết thúc từ để lại ĐÚNG 1 ký tự ASCII in được (space/dấu câu/số). Return/Tab/Esc/mũi tên/phím chức năng/dead key → forgetLastCommit(): Enter gửi tin nhắn trong app chat, Tab đổi focus, Esc không chèn gì ⇒ ⌫ sau đó không xoá ký tự ranh giới. commitComposition (app force-commit giữa từ) cũng vậy.

  2. Trước khi dùng, ĐỌC LẠI text trên màn hình đúng vị trí từ đó và so khớp — IMKit qua attributedSubstring, tap qua AX. Lệch (app nuốt phím ranh giới, autocorrect/text-substitution đổi từ, caret nhảy, field NFD, AX trễ) → bỏ qua, ⌫ hành xử y như trước. Đây là khác biệt cốt lõi với reEditWord vốn ĐOÁN từ trên màn hình rồi seed (regression 1.4.28 → default OFF): ở đây engine biết chính xác từ nào, đọc màn hình chỉ để xác nhận.

  3. Loại trừ per-FIELD giống re-edit: omnibox/search bar (inline autocomplete tự viết đè) và app marked-text. Tap thêm điều kiện emitMode == .backspace.

Kèm theo: ⌘/⌃/⌥ và nhánh imeActive trong tap trước bị gate !engine.isEmpty nên snapshot sống sót qua ⌘A + ⌫ (⌫ đó xoá SELECTION, không phải ký tự ranh giới) — nay test thêm canReopenLastCommit, vẫn là một phép so Int nên máy đang dùng ABC không trả phí gì mỗi phím.

TelexCore 233 test pass (debug + release), AppTests 167 pass. Zero-allocation invariant giữ nguyên, 0.1545 µs/phím (ceiling 0.40).

Gõ `thasy` → `tháy`, space, ⌫ xoá dấu cách, rồi gõ `a` để bù dấu: ra `tháya`
thay vì `thấy`. Space chốt từ và reset engine sạch; ⌫ với buffer rỗng chỉ
resetContext rồi thả phím qua, không có đường nào dựng lại buffer nên `a` là ký
tự mới. reEditWord không cứu được: nó chỉ bắt phím dấu THUẦN (s/f/r/x/j/z/w),
`a` là chữ cái, và mặc định còn OFF. Mọi bộ gõ khác (iOS/macOS, UniKey, EVKey,
OpenKey) đều khôi phục buffer khi ⌫ lùi vào từ đã chốt.

ENGINE — snapshot re-open, buffer cố định, zero-alloc:

commitBoundary/commitText nhớ đúng chuỗi phím vừa chốt, CHỈ khi màn hình đang
hiển thị `composed`: không auto-restore về raw (`google` → screen là "google",
buffer sẽ là "gôgle" ⇒ desync), không overflow (view 32 ký tự là prefix cũ của
màn hình). reopenLastCommit() replay lại chuỗi đó qua feed() nên đặt thanh, lan
ươ, ánh xạ ⌫ và quyết định boundary hành xử y như từ chưa từng bị chốt — không
có code path thứ hai phải giữ đồng bộ (khác seed(), vốn dựng lại từ TEXT).

Replay được VERIFY như round-trip của seed(): settings đổi giữa commit và ⌫ mà
từ không compose ra đúng như cũ thì reset + trả nil, không bao giờ compose đè
lên text không tái tạo được. previousWordEnglish cũng khôi phục về giá trị
TRƯỚC commit — từ mở lại phải nhìn ngữ cảnh của từ đứng trước nó, không phải
ngữ cảnh chính nó vừa sinh ra.

Snapshot hẹp có chủ đích: sống qua đúng MỘT lần commit, chết ngay khi phím đầu
của từ mới vào feed(), khi reset() (mất focus, click chuột, đổi app), hoặc khi
caller gọi forgetLastCommit(). reset() tách làm hai — resetWord() cho hai đường
commit (chúng vừa chụp snapshot xong), reset() công khai xoá cả hai.

CONTROLLERS — cùng một luật ở cả IMKit lẫn tap (máy người báo lỗi đi tap):

1. Chỉ mở lại khi phím kết thúc từ để lại ĐÚNG 1 ký tự ASCII in được
   (space/dấu câu/số). Return/Tab/Esc/mũi tên/phím chức năng/dead key →
   forgetLastCommit(): Enter gửi tin nhắn trong app chat, Tab đổi focus, Esc
   không chèn gì ⇒ ⌫ sau đó không xoá ký tự ranh giới. commitComposition (app
   force-commit giữa từ) cũng vậy.

2. Trước khi dùng, ĐỌC LẠI text trên màn hình đúng vị trí từ đó và so khớp —
   IMKit qua attributedSubstring, tap qua AX. Lệch (app nuốt phím ranh giới,
   autocorrect/text-substitution đổi từ, caret nhảy, field NFD, AX trễ) →
   bỏ qua, ⌫ hành xử y như trước. Đây là khác biệt cốt lõi với reEditWord vốn
   ĐOÁN từ trên màn hình rồi seed (regression 1.4.28 → default OFF): ở đây
   engine biết chính xác từ nào, đọc màn hình chỉ để xác nhận.

3. Loại trừ per-FIELD giống re-edit: omnibox/search bar (inline autocomplete tự
   viết đè) và app marked-text. Tap thêm điều kiện emitMode == .backspace.

Kèm theo: ⌘/⌃/⌥ và nhánh imeActive trong tap trước bị gate `!engine.isEmpty`
nên snapshot sống sót qua ⌘A + ⌫ (⌫ đó xoá SELECTION, không phải ký tự ranh
giới) — nay test thêm canReopenLastCommit, vẫn là một phép so Int nên máy đang
dùng ABC không trả phí gì mỗi phím.

TelexCore 233 test pass (debug + release), AppTests 167 pass. Zero-allocation
invariant giữ nguyên, 0.1545 µs/phím (ceiling 0.40).
@ptrinh
ptrinh merged commit 391b168 into ptrinh:main Aug 13, 2026
2 checks passed
ptrinh added a commit that referenced this pull request Aug 13, 2026
Quyết định maintainer 13/08: PR #42 merge always-on, nhưng cả re-edit lẫn ⌫
re-open đều là hành vi "với ngược vào từ trước con trỏ" — một opt-in gác cả
đôi, default OFF (bài học 1.4.28). Guard 1 dòng ở cả hai entry point (IMKit +
tap, đặt trước cả phép so Int canReopen để máy không opt-in không trả thêm phí);
description toggle cập nhật cả ba nơi (code + en + vi): "Xóa dấu cách mở lại
từ vừa chốt". Test pin default-OFF của gate (giờ gác hai tính năng).

Đồng thời bỏ 3 commit local chưa push của bản seed-from-screen cũ (backup tại
branch backup/my-backspace-restore-40) — PR #42 thay thế hoàn toàn với kiến
trúc tốt hơn (engine nhớ đúng keystroke + verify màn hình từng ký tự).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@J2TEAM
J2TEAM deleted the fix/40-reopen-word-on-backspace branch August 13, 2026 03:27
ptrinh added a commit that referenced this pull request Aug 13, 2026
…hật ký (PR #43)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@ptrinh

ptrinh commented Aug 13, 2026

Copy link
Copy Markdown
Owner

Ship trong v1.5.8 🙏 Follow-up nhỏ (0b51336): gate vào chung toggle thử nghiệm với re-edit (quyết định maintainer — mọi hành vi "với ngược vào từ trước" đi qua một opt-in, default OFF theo bài học 1.4.28) + cập nhật description toggle. Kiến trúc snapshot+verify rất chuẩn — cảm ơn bạn!

xkhanhs pushed a commit to xkhanhs/vtx that referenced this pull request Aug 13, 2026
… hiện có

Quyết định maintainer 13/08: PR ptrinh#42 merge always-on, nhưng cả re-edit lẫn ⌫
re-open đều là hành vi "với ngược vào từ trước con trỏ" — một opt-in gác cả
đôi, default OFF (bài học 1.4.28). Guard 1 dòng ở cả hai entry point (IMKit +
tap, đặt trước cả phép so Int canReopen để máy không opt-in không trả thêm phí);
description toggle cập nhật cả ba nơi (code + en + vi): "Xóa dấu cách mở lại
từ vừa chốt". Test pin default-OFF của gate (giờ gác hai tính năng).

Đồng thời bỏ 3 commit local chưa push của bản seed-from-screen cũ (backup tại
branch backup/my-backspace-restore-40) — PR ptrinh#42 thay thế hoàn toàn với kiến
trúc tốt hơn (engine nhớ đúng keystroke + verify màn hình từng ký tự).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
ptrinh added a commit that referenced this pull request Aug 14, 2026
Maintainer 14/08 sau một ngày field-test (Discord/iTerm/Chrome/TextEdit).
Doc ghi rõ lịch sử: 03/08 bật → 04/08 revert cùng ngày vì lớp lỗi "seed ĐOÁN
từ trên màn hình rồi transform bậy"; PR #42 đã thay đoán bằng snapshot chuỗi
phím + đối chiếu màn hình từng ký tự, gate keystream #38 chặn seed xuyên
boundary, poke AX (1.5.11) cho Electron — đường gây regression cũ không còn.
Test pin cả hai chiều (default ON + explicit OFF vẫn dính, là escape hatch).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
ptrinh pushed a commit that referenced this pull request Aug 15, 2026
Trước đây cập nhật chỉ nói "Có bản mới: 1.6.3" — người dùng phải cài trong khi
không biết bản mới có gì. Release body đã nằm sẵn trong response của
/releases/latest nhưng bị bỏ đi, chỉ lấy tag_name và html_url.

Kênh manual lấy changelog miễn phí: body có sẵn trong response vừa parse, không
thêm request nào. Kênh weekly (stable) thì stable.json chỉ có version, nên phải
hỏi thêm /releases/tags/v{version} — đổi lại stable.json vẫn giữ hai dòng
maintainer sửa tay, không phải nhớ cập nhật changelog lúc promote. Notes của
kênh weekly lưu vào pendingUpdateNotes cạnh pendingUpdateVersion (ghi/xoá cùng
nhau) để sống sót qua lần IME khởi động lại giữa lúc check và lúc mở tab.

Fetch notes là best-effort: mọi lỗi trả nil chứ không throw, vì thiếu changelog
không bao giờ được biến một bản update thật thành .failed — số phiên bản là tin
chính, changelog là phần thêm. Có test pin 404.

Body đến từ mạng nên không tin nó ngắn hay đúng định dạng: lọc git trailer
(Co-Authored-By, Signed-off-by — plumbing của commit, không phải tin cho người
dùng), gộp các dòng trống thừa, cap 4000 ký tự. Quá cap thì link "Xem đầy đủ
thay đổi" dẫn ra trang release, URL suy ra từ version nên kênh weekly cũng có
link mà không phải lưu thêm key.

Render Markdown tối giản: Text chỉ hiểu inline markdown, nên tách ## và - thành
block riêng rồi giao phần inline cho AttributedString — không thêm dependency.
Heading theo đúng luật ATX (phải có dấu cách sau #), nếu không thì dòng mở đầu
bằng "#42 sửa lỗi" sẽ bị ăn mất # và in đậm.

Cửa sổ Settings mặc định cao 560 → 660: tab Giới thiệu không có scroll container
và phần cố định của nó đã chiếm gần hết chiều cao, nên hộp changelog là thành
phần duy nhất co được và sẽ bị ép còn một vệt. 660 chỉ áp dụng lần tạo đầu, ai
đã chỉnh kích thước vẫn giữ nguyên.

Blocks parse một lần lúc gán vào @State thay vì parse trong ForEach: body chạy
lại mỗi lần model publish hoặc checking/installing/status đổi.

193 test pass. Layout chưa kiểm tra trực quan — mới đúng về mặt code.
xkhanhs pushed a commit to xkhanhs/vtx that referenced this pull request Aug 17, 2026
Trước đây cập nhật chỉ nói "Có bản mới: 1.6.3" — người dùng phải cài trong khi
không biết bản mới có gì. Release body đã nằm sẵn trong response của
/releases/latest nhưng bị bỏ đi, chỉ lấy tag_name và html_url.

Kênh manual lấy changelog miễn phí: body có sẵn trong response vừa parse, không
thêm request nào. Kênh weekly (stable) thì stable.json chỉ có version, nên phải
hỏi thêm /releases/tags/v{version} — đổi lại stable.json vẫn giữ hai dòng
maintainer sửa tay, không phải nhớ cập nhật changelog lúc promote. Notes của
kênh weekly lưu vào pendingUpdateNotes cạnh pendingUpdateVersion (ghi/xoá cùng
nhau) để sống sót qua lần IME khởi động lại giữa lúc check và lúc mở tab.

Fetch notes là best-effort: mọi lỗi trả nil chứ không throw, vì thiếu changelog
không bao giờ được biến một bản update thật thành .failed — số phiên bản là tin
chính, changelog là phần thêm. Có test pin 404.

Body đến từ mạng nên không tin nó ngắn hay đúng định dạng: lọc git trailer
(Co-Authored-By, Signed-off-by — plumbing của commit, không phải tin cho người
dùng), gộp các dòng trống thừa, cap 4000 ký tự. Quá cap thì link "Xem đầy đủ
thay đổi" dẫn ra trang release, URL suy ra từ version nên kênh weekly cũng có
link mà không phải lưu thêm key.

Render Markdown tối giản: Text chỉ hiểu inline markdown, nên tách ## và - thành
block riêng rồi giao phần inline cho AttributedString — không thêm dependency.
Heading theo đúng luật ATX (phải có dấu cách sau #), nếu không thì dòng mở đầu
bằng "ptrinh#42 sửa lỗi" sẽ bị ăn mất # và in đậm.

Cửa sổ Settings mặc định cao 560 → 660: tab Giới thiệu không có scroll container
và phần cố định của nó đã chiếm gần hết chiều cao, nên hộp changelog là thành
phần duy nhất co được và sẽ bị ép còn một vệt. 660 chỉ áp dụng lần tạo đầu, ai
đã chỉnh kích thước vẫn giữ nguyên.

Blocks parse một lần lúc gán vào @State thay vì parse trong ForEach: body chạy
lại mỗi lần model publish hoặc checking/installing/status đổi.

193 test pass. Layout chưa kiểm tra trực quan — mới đúng về mặt code.

(cherry picked from commit f8ca1d1)
xkhanhs pushed a commit to xkhanhs/vtx that referenced this pull request Aug 17, 2026
Maintainer 14/08 sau một ngày field-test (Discord/iTerm/Chrome/TextEdit).
Doc ghi rõ lịch sử: 03/08 bật → 04/08 revert cùng ngày vì lớp lỗi "seed ĐOÁN
từ trên màn hình rồi transform bậy"; PR ptrinh#42 đã thay đoán bằng snapshot chuỗi
phím + đối chiếu màn hình từng ký tự, gate keystream ptrinh#38 chặn seed xuyên
boundary, poke AX (1.5.11) cho Electron — đường gây regression cũ không còn.
Test pin cả hai chiều (default ON + explicit OFF vẫn dính, là escape hatch).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
(cherry picked from commit f3a7ccd)
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.

2 participants