Skip to content

Commit 230494b

Browse files
authored
fix(skk-mode): fix candidate cycling accumulation bug (#2095)
* fix(skk-mode): fix candidate cycling accumulation bug The show-candidate function was incorrectly accumulating candidates instead of replacing them when cycling through conversion options. Root cause: The henkan-start point (created with :left-inserting) was moving after text insertion, causing incorrect deletion range for subsequent candidate changes. Fix: Save the start position as an integer before operations and restore henkan-start to its original position after insertion. * add .serena/ into .gitignroe * test(skk-mode): add candidate cycling replacement tests Add comprehensive tests for the candidate cycling bug fix: - test-candidate-cycling-replacement: Basic replacement test - test-candidate-cycling-with-okurigana: Tests with okurigana - test-next-prev-candidate-functions: Tests next/prev cycling - test-candidate-cycling-with-existing-text: Tests with pre-existing buffer content All tests verify that candidates are replaced, not accumulated.
1 parent db14672 commit 230494b

File tree

3 files changed

+202
-8
lines changed

3 files changed

+202
-8
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,3 +41,4 @@ build/*
4141
TODO
4242
artifacts/
4343
logs/
44+
.serena/

extensions/skk-mode/conversion.lisp

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -225,18 +225,22 @@ Appends okurigana-kana to the candidate if present."
225225
(let ((candidates (skk-candidates state))
226226
(start (skk-henkan-start state)))
227227
(when (and candidates start (<= 0 index (1- (length candidates))))
228-
;; Delete from henkan-start to current point
229-
(delete-between-points start (current-point))
230-
;; Insert the candidate with okurigana if present
231228
(let* ((candidate (nth index candidates))
232229
(okurigana (skk-okurigana-kana state))
233230
(text (if (plusp (length okurigana))
234231
(concatenate 'string candidate okurigana)
235-
candidate)))
236-
(insert-string start text)
237-
;; Move cursor to end of inserted text
238-
(move-point (current-point) start)
239-
(character-offset (current-point) (length text))))))
232+
candidate))
233+
;; Save the start position as an integer before any operations
234+
(start-pos (lem:position-at-point start)))
235+
;; Delete from henkan-start to current point
236+
(delete-between-points start (current-point))
237+
;; Move current-point back to the saved start position for insertion
238+
(lem:move-to-position (current-point) start-pos)
239+
;; Insert new candidate
240+
(insert-string (current-point) text)
241+
;; IMPORTANT: Restore henkan-start to its original position AFTER insertion
242+
;; This ensures it's ready for the next candidate change
243+
(lem:move-to-position start start-pos)))))
240244

241245
(defun next-candidate (state)
242246
"Show the next candidate."

extensions/skk-mode/tests/main.lisp

Lines changed: 189 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,16 @@
1212
:skk-input-mode
1313
:skk-preedit
1414
:skk-henkan-mode-p
15+
:skk-henkan-start
1516
:skk-henkan-key
1617
:skk-okurigana-consonant
1718
:skk-okurigana-kana
1819
:skk-candidates
1920
:skk-candidate-index
2021
:clear-skk-state)
22+
(:import-from :lem-skk-mode/conversion
23+
:next-candidate
24+
:prev-candidate)
2125
(:import-from :lem-fake-interface
2226
:with-fake-interface))
2327
(in-package :lem-skk-mode/tests/main)
@@ -1173,3 +1177,188 @@ Uppercase starts henkan, Space would trigger conversion (simulated as henkan bou
11731177

11741178
;; Cleanup
11751179
(kill-buffer buf)))))
1180+
1181+
;;; ============================================================
1182+
;;; Candidate Cycling Tests
1183+
;;; ============================================================
1184+
;;; These tests verify the fix for the candidate cycling bug where
1185+
;;; candidates were accumulating instead of being replaced.
1186+
;;; Bug: Space/x would show "漢字幹事" instead of replacing "漢字" with "幹事"
1187+
1188+
(deftest test-candidate-cycling-replacement
1189+
(testing "Candidate cycling replaces instead of accumulates"
1190+
(with-fake-interface ()
1191+
(let ((buf (make-buffer "*skk-candidate-test*" :temporary t)))
1192+
(switch-to-buffer buf)
1193+
(erase-buffer buf)
1194+
1195+
;; Set up SKK state with candidates
1196+
(let ((state (make-instance 'skk-state)))
1197+
(setf (buffer-value buf 'lem-skk-mode/state::skk-state) state)
1198+
1199+
;; Set henkan start point
1200+
(setf (skk-henkan-start state)
1201+
(copy-point (buffer-point buf) :left-inserting))
1202+
(setf (skk-henkan-mode-p state) t)
1203+
(setf (skk-henkan-key state) "かんじ")
1204+
(setf (skk-candidates state) '("漢字" "幹事" "監事"))
1205+
(setf (skk-candidate-index state) 0)
1206+
1207+
;; Show first candidate
1208+
(lem-skk-mode/conversion::show-candidate state 0)
1209+
(ok (equal (buffer-text buf) "漢字")
1210+
"First candidate should be '漢字'")
1211+
1212+
;; Show second candidate - should REPLACE, not accumulate
1213+
(lem-skk-mode/conversion::show-candidate state 1)
1214+
(ok (equal (buffer-text buf) "幹事")
1215+
"Second candidate should replace to '幹事' (not '漢字幹事')")
1216+
1217+
;; Show third candidate - should REPLACE again
1218+
(lem-skk-mode/conversion::show-candidate state 2)
1219+
(ok (equal (buffer-text buf) "監事")
1220+
"Third candidate should replace to '監事'")
1221+
1222+
;; Go back to first - should still replace
1223+
(lem-skk-mode/conversion::show-candidate state 0)
1224+
(ok (equal (buffer-text buf) "漢字")
1225+
"Going back to first candidate should replace to '漢字'")
1226+
1227+
;; Cleanup point
1228+
(when (skk-henkan-start state)
1229+
(delete-point (skk-henkan-start state))))
1230+
1231+
(kill-buffer buf)))))
1232+
1233+
(deftest test-candidate-cycling-with-okurigana
1234+
(testing "Candidate cycling with okurigana replaces correctly"
1235+
(with-fake-interface ()
1236+
(let ((buf (make-buffer "*skk-okurigana-test*" :temporary t)))
1237+
(switch-to-buffer buf)
1238+
(erase-buffer buf)
1239+
1240+
(let ((state (make-instance 'skk-state)))
1241+
(setf (buffer-value buf 'lem-skk-mode/state::skk-state) state)
1242+
1243+
;; Set up okurigana conversion: かk -> 書く, 描く
1244+
(setf (skk-henkan-start state)
1245+
(copy-point (buffer-point buf) :left-inserting))
1246+
(setf (skk-henkan-mode-p state) t)
1247+
(setf (skk-henkan-key state) "")
1248+
(setf (skk-okurigana-consonant state) "k")
1249+
(setf (skk-okurigana-kana state) "")
1250+
(setf (skk-candidates state) '("" "" ""))
1251+
(setf (skk-candidate-index state) 0)
1252+
1253+
;; Show first candidate with okurigana
1254+
(lem-skk-mode/conversion::show-candidate state 0)
1255+
(ok (equal (buffer-text buf) "書く")
1256+
"First candidate with okurigana: '書く'")
1257+
1258+
;; Second candidate - should replace entire text
1259+
(lem-skk-mode/conversion::show-candidate state 1)
1260+
(ok (equal (buffer-text buf) "描く")
1261+
"Second candidate should replace to '描く' (not '書く描く')")
1262+
1263+
;; Third candidate
1264+
(lem-skk-mode/conversion::show-candidate state 2)
1265+
(ok (equal (buffer-text buf) "掻く")
1266+
"Third candidate should replace to '掻く'")
1267+
1268+
(when (skk-henkan-start state)
1269+
(delete-point (skk-henkan-start state))))
1270+
1271+
(kill-buffer buf)))))
1272+
1273+
(deftest test-next-prev-candidate-functions
1274+
(testing "next-candidate and prev-candidate cycle correctly"
1275+
(with-fake-interface ()
1276+
(let ((buf (make-buffer "*skk-next-prev-test*" :temporary t)))
1277+
(switch-to-buffer buf)
1278+
(erase-buffer buf)
1279+
1280+
(let ((state (make-instance 'skk-state)))
1281+
(setf (buffer-value buf 'lem-skk-mode/state::skk-state) state)
1282+
1283+
;; Set up state
1284+
(setf (skk-henkan-start state)
1285+
(copy-point (buffer-point buf) :left-inserting))
1286+
(setf (skk-henkan-mode-p state) t)
1287+
(setf (skk-henkan-key state) "かんじ")
1288+
(setf (skk-candidates state) '("漢字" "幹事" "監事"))
1289+
(setf (skk-candidate-index state) 0)
1290+
1291+
;; Show initial candidate
1292+
(lem-skk-mode/conversion::show-candidate state 0)
1293+
(ok (equal (buffer-text buf) "漢字") "Initial: 漢字")
1294+
1295+
;; next-candidate
1296+
(next-candidate state)
1297+
(ok (equal (buffer-text buf) "幹事") "After next: 幹事")
1298+
(ok (= (skk-candidate-index state) 1) "Index should be 1")
1299+
1300+
;; next-candidate again
1301+
(next-candidate state)
1302+
(ok (equal (buffer-text buf) "監事") "After next: 監事")
1303+
(ok (= (skk-candidate-index state) 2) "Index should be 2")
1304+
1305+
;; next-candidate wraps around
1306+
(next-candidate state)
1307+
(ok (equal (buffer-text buf) "漢字") "After wrap: 漢字")
1308+
(ok (= (skk-candidate-index state) 0) "Index should wrap to 0")
1309+
1310+
;; prev-candidate
1311+
(prev-candidate state)
1312+
(ok (equal (buffer-text buf) "監事") "After prev: 監事")
1313+
(ok (= (skk-candidate-index state) 2) "Index should wrap to 2")
1314+
1315+
;; prev-candidate again
1316+
(prev-candidate state)
1317+
(ok (equal (buffer-text buf) "幹事") "After prev: 幹事")
1318+
(ok (= (skk-candidate-index state) 1) "Index should be 1")
1319+
1320+
(when (skk-henkan-start state)
1321+
(delete-point (skk-henkan-start state))))
1322+
1323+
(kill-buffer buf)))))
1324+
1325+
(deftest test-candidate-cycling-with-existing-text
1326+
(testing "Candidate cycling works when buffer has existing text"
1327+
(with-fake-interface ()
1328+
(let ((buf (make-buffer "*skk-existing-text-test*" :temporary t)))
1329+
(switch-to-buffer buf)
1330+
(erase-buffer buf)
1331+
1332+
;; Insert some existing text first
1333+
(insert-string (buffer-point buf) "これは")
1334+
1335+
(let ((state (make-instance 'skk-state)))
1336+
(setf (buffer-value buf 'lem-skk-mode/state::skk-state) state)
1337+
1338+
;; Set henkan start point at current position (after "これは")
1339+
(setf (skk-henkan-start state)
1340+
(copy-point (buffer-point buf) :left-inserting))
1341+
(setf (skk-henkan-mode-p state) t)
1342+
(setf (skk-henkan-key state) "ほん")
1343+
(setf (skk-candidates state) '("" "" ""))
1344+
(setf (skk-candidate-index state) 0)
1345+
1346+
;; Show first candidate
1347+
(lem-skk-mode/conversion::show-candidate state 0)
1348+
(ok (equal (buffer-text buf) "これは本")
1349+
"Buffer should have existing text + first candidate")
1350+
1351+
;; Cycle to second - only candidate part should be replaced
1352+
(lem-skk-mode/conversion::show-candidate state 1)
1353+
(ok (equal (buffer-text buf) "これは翻")
1354+
"Only candidate should be replaced, existing text preserved")
1355+
1356+
;; Cycle to third
1357+
(lem-skk-mode/conversion::show-candidate state 2)
1358+
(ok (equal (buffer-text buf) "これは奔")
1359+
"Candidate replacement should work correctly")
1360+
1361+
(when (skk-henkan-start state)
1362+
(delete-point (skk-henkan-start state))))
1363+
1364+
(kill-buffer buf)))))

0 commit comments

Comments
 (0)