diff --git a/autoload/skkeleton.vim b/autoload/skkeleton.vim index ef21fef55..d672f4341 100644 --- a/autoload/skkeleton.vim +++ b/autoload/skkeleton.vim @@ -268,6 +268,15 @@ function! skkeleton#handle(func, opts) abort let result = "\" .. result[5:] .. "\" endif + " Put the cursor back where |skkeleton-functions-kakuteiUndo| walked away + " from. This has to happen after the text has landed, so it rides along at + " the end of the same feedkeys() rather than being called from here. + let restore_col = get(ret, 'restoreCol', 0) + if restore_col > 0 + let result ..= printf("\call skkeleton#restore_point(%d, %d)\", + \ get(ret, 'restoreLnum', 0), restore_col) + endif + call skkeleton#doautocmd() if get(a:opts, 'expr', v:false) @@ -446,6 +455,19 @@ function! skkeleton#locate_kakutei(bufnr, lnum, before, kakutei) abort return v:true endfunction +" Put the cursor at {col}, a byte column of {lnum}, once whatever replaced the +" taken back kakutei has been written. Fed at the end of the same keys as that +" kakutei, so the line is already the final one by the time this runs. +" {lnum} is where the undo happened: a kakutei followed by a newline leaves the +" cursor on another line, and a column of the line it came from means nothing +" there. +function! skkeleton#restore_point(lnum, col) abort + if mode() !=# 'i' || line('.') != a:lnum + return + endif + call cursor(a:lnum, a:col) +endfunction + function! skkeleton#initialize() abort call skkeleton#notify_async('initialize', []) endfunction diff --git a/denops/skkeleton/completefunc_test.ts b/denops/skkeleton/completefunc_test.ts index 1e3f937af..c71495b43 100644 --- a/denops/skkeleton/completefunc_test.ts +++ b/denops/skkeleton/completefunc_test.ts @@ -179,3 +179,68 @@ test({ } }, }); + +// Note: the bundled completefunc reports a confirmation from CompleteDone, +// which Vim fires after the mapping of the key that ended the completion +// (measured). So the same ordering a completion engine of its own can +// produce applies here too, and the cursor of +// |skkeleton-functions-kakuteiUndo| has to survive it +test({ + mode: "nvim", // can input mode test only in nvim + name: "complete_done leaves the cursor to what a key has started since", + async fn(denops: Denops) { + const lib = await currentLibrary.get(); + await lib.registerHenkanResult("okurinasi", "かく", "各"); + await lib.registerHenkanResult("okurinasi", "かくい", "各位"); + await denops.cmd( + 'call skkeleton#register_keymap("input", "", "kakuteiUndo")', + ); + await denops.cmd("call skkeleton#config(#{immediatelyCancel: v:false})"); + await denops.cmd("set virtualedit=onemore"); + await denops.cmd("startinsert"); + + const done = async (midasi: string, word: string) => { + await denops.cmd( + "let v:completed_item = #{word: word, user_data: userData}", + { + word, + userData: JSON.stringify({ + tag: "skkeleton", + midasi, + word, + type: "okurinasi", + }), + }, + ); + await denops.cmd("call skkeleton#complete_done()"); + }; + + // the completion confirms 各位 out of ▽かく + for (const key of ["K", "a", "k", "u"]) { + await denops.cmd(`call skkeleton#handle("handleKey", {"key": "${key}"})`); + } + await denops.call("setline", 1, "各位"); + await denops.call("cursor", 1, 7); + await done("かくい", "各位"); + assertEquals(await denops.call("getline", 1), "各位"); + + // take it back, go to the reading and shorten it + for (const key of ["", "", ""]) { + await denops.cmd(`call skkeleton#handle("handleKey", {"key": "${key}"})`); + } + assertEquals(await denops.call("getline", 1), "▽かく"); + + // 各 is only selected, and an uppercase key starts a new henkan point + // instead of confirming it. CompleteDone comes after that mapping + await denops.call("setline", 1, "各"); + await denops.call("cursor", 1, 4); + await denops.cmd('call skkeleton#handle("handleKey", {"key": "J"})'); + assertEquals(await denops.call("getline", 1), "各▽j"); + const col = await denops.call("col", "."); + await done("かく", "各"); + + // the cursor belongs to the ▽ that key has started, not to the column the + // undo remembered (which would be 4) + assertEquals(await denops.call("col", "."), col); + }, +}); diff --git a/denops/skkeleton/context.ts b/denops/skkeleton/context.ts index f375ecfb9..05dad259b 100644 --- a/denops/skkeleton/context.ts +++ b/denops/skkeleton/context.ts @@ -48,6 +48,30 @@ type PendingKakuteiResult = Omit & { tail: string; }; +// where the cursor was when |skkeleton-functions-kakuteiUndo| walked away from +// it, so that the kakutei which picks another candidate can put it back +// columns are byte ones, the way Vim counts them +type RememberedPoint = { + // the line it was on: the cursor is not sent back to a column of a line it + // has since left, as it does when a kakutei is followed by a newline + lnum: number; + col: number; + // the byte length of the taken back kakutei, so that whatever is confirmed + // instead shifts the cursor by the difference + bytes: number; +}; + +// where Vim has to put the cursor back once it has applied a key handling +type CursorPoint = { + lnum: number; + col: number; +}; + +// Vim counts columns in bytes +function byteLength(str: string): number { + return new TextEncoder().encode(str).length; +} + export class Context { denops?: Denops; state: State = initializeState({}); @@ -68,6 +92,56 @@ export class Context { }; lastKakutei: KakuteiResult | undefined; pendingKakutei: PendingKakuteiResult | undefined; + #rememberedPoint: RememberedPoint | undefined; + // where Vim has to put the cursor once it has applied the output of this key + // handling, or undefined for leaving it where the output ends + restorePoint: CursorPoint | undefined; + + // remember where the cursor is standing, before kakuteiUndo() walks it back + // to the kakutei it is about to delete + recordPointRestore(kakutei: string, bufferText: string) { + const col = byteLength(this.prevInput) + 1; + // Note: the cursor was inside the kakutei, so the column it was at means + // nothing once something of another length has replaced it + this.#rememberedPoint = col >= byteLength(bufferText) + 1 + ? { lnum: this.lnum, col, bytes: byteLength(kakutei) } + : void 0; + } + + // the byte column to put the cursor back at, now that {kakutei} is what has + // been written where the undo walked away from, or 0 when there is nothing to + // put back + // the column is shifted by however much longer or shorter {kakutei} is than + // what was taken back, so the cursor keeps its place in the text + // an undo which never moved the cursor gives the column the kakutei ends at + // anyway, so nothing visibly happens for it + usePointRestore(kakutei: string): CursorPoint | undefined { + const restore = this.#rememberedPoint; + this.#rememberedPoint = void 0; + return restore + ? { + lnum: restore.lnum, + col: restore.col + byteLength(kakutei) - restore.bytes, + } + : void 0; + } + + // give up on putting the cursor back + // called once skkeleton has nothing pending where the undo happened: the + // reading may have been taken all the way back with cancel, or skkeleton + // disabled, and a column remembered for a spot nobody is editing any more + // would move the cursor on some unrelated kakutei later on + forgetPointRestore() { + this.#rememberedPoint = void 0; + } + + // whether skkeleton is still writing where the undo left off: a henkan, a + // reading being typed, or a pre-edit waiting to become one + get hasPendingInput(): boolean { + const state = this.state; + return state.type !== "input" || state.mode !== "direct" || + state.feed !== "" || this.preEdit.current !== ""; + } // remember a kakutei so that |skkeleton-functions-kakuteiUndo| can take it // back diff --git a/denops/skkeleton/function/common.ts b/denops/skkeleton/function/common.ts index 321be23a3..d1b98bf5d 100644 --- a/denops/skkeleton/function/common.ts +++ b/denops/skkeleton/function/common.ts @@ -37,6 +37,13 @@ export async function kakutei(context: Context) { context.kakuteiWithUndoPoint(ret); // Note: remember what is needed to take this kakutei back context.recordKakutei("henkan", ret, snapshot); + // Note: a henkan which an undo has restored owes the cursor its place + // back, now that the replacement is known and its length with it + // the text is written by the keys this handling returns, so the + // point rides along with them (see |skkeleton#restore_point()|) + // whether it is used at all is decided once the handling is over: + // the same key can confirm a henkan and open a new one after it + context.restorePoint = context.usePointRestore(ret); break; } case "input": { @@ -80,6 +87,25 @@ export async function completionKakutei( ) { return; } + // Note: the engine has already written, so unlike a kakutei out of a henkan + // there are no keys for the point to ride along with and Vim is told + // right away + // asked before the lookup below, because the buffer holds the text even + // when the candidate turns out not to be in the dictionary + const restore = context.usePointRestore(inserted); + // Note: the report can arrive after a key handling has started something else + // where the cursor now is -- a new henkan point, or a reading being + // typed on -- and the cursor belongs to that + const onThisReading = state.mode !== "direct" && state.henkanFeed === midasi; + if ( + restore && (!context.hasPendingInput || onThisReading) && context.denops + ) { + await context.denops.call( + "skkeleton#restore_point", + restore.lnum, + restore.col, + ); + } // Note: the completion has never been in a henkan state, so the candidates // are looked up instead of being restored from a snapshot // this happens after the learning, hence the confirmed candidate comes @@ -163,6 +189,10 @@ export async function kakuteiUndo(context: Context) { context.kakutei("\b".repeat(graphemeLength(last.kakutei))); const restored = { ...last.state }; context.state = restored; + // Note: prevInput still tells where the cursor was when the undo was asked + // for, which is where the kakutei picking another candidate puts it + // back + context.recordPointRestore(last.kakutei, last.bufferText); context.invalidateKakutei(); if (context.mode !== last.mode) { await modeChange(context, last.mode); diff --git a/denops/skkeleton/function/common_test.ts b/denops/skkeleton/function/common_test.ts index aacd36471..fef882114 100644 --- a/denops/skkeleton/function/common_test.ts +++ b/denops/skkeleton/function/common_test.ts @@ -827,3 +827,315 @@ Deno.test({ assertEquals(context.mode, "hira"); }, }); + +test({ + mode: "nvim", // can input mode test only in nvim + name: "kakutei undo puts the cursor back where it was asked from", + async fn(denops: Denops) { + const l = await currentLibrary.get(); + // Note: 試 is a byte shorter than the others, so the column it is put back + // to cannot be the one it was saved at + await l.registerHenkanResult("okurinasi", "てすと", "試"); + await l.registerHenkanResult("okurinasi", "てすと", "手酢戸"); + await l.registerHenkanResult("okurinasi", "てすと", "テスト"); + await denops.cmd( + 'call skkeleton#register_keymap("input", "", "kakuteiUndo")', + ); + await denops.cmd("startinsert"); + + for (const key of ["T", "e", "s", "u", "t", "o", " ", ""]) { + await denops.cmd(`call skkeleton#handle("handleKey", {"key": "${key}"})`); + } + for (const key of ["s", "u", "r", "u"]) { + await denops.cmd(`call skkeleton#handle("handleKey", {"key": "${key}"})`); + } + assertEquals(await fn.getline(denops, "."), "テストする"); + // the cursor is at the end of the line, which is where the undo is asked + // from and where it has to come back to + assertEquals(await fn.col(denops, "."), 16); + + await denops.cmd('call skkeleton#handle("handleKey", {"key": ""})'); + assertEquals(await fn.getline(denops, "."), "▼テストする"); + + // pick 試, which is 6 bytes shorter than テスト + await denops.cmd('call skkeleton#handle("handleKey", {"key": ""})'); + await denops.cmd('call skkeleton#handle("handleKey", {"key": ""})'); + await denops.cmd('call skkeleton#handle("handleKey", {"key": ""})'); + assertEquals(await fn.getline(denops, "."), "試する"); + // back to the end of the line, not to where 試 ends (which would be 4) + assertEquals(await fn.col(denops, "."), 10); + }, +}); + +test({ + mode: "nvim", // can input mode test only in nvim + name: "kakutei undo puts the cursor back after a completion has confirmed", + async fn(denops: Denops) { + const l = await currentLibrary.get(); + await l.registerHenkanResult("okurinasi", "てすと", "手酢戸"); + await l.registerHenkanResult("okurinasi", "てすと", "テスト"); + await denops.cmd( + 'call skkeleton#register_keymap("input", "", "kakuteiUndo")', + ); + // Note: cancel has to stop at the reading for the completion to have + // something to complete + await denops.cmd("call skkeleton#config(#{immediatelyCancel: v:false})"); + await denops.cmd("set virtualedit=onemore"); + await denops.cmd("startinsert"); + + for (const key of ["T", "e", "s", "u", "t", "o", " ", ""]) { + await denops.cmd(`call skkeleton#handle("handleKey", {"key": "${key}"})`); + } + for (const key of ["s", "u", "r", "u"]) { + await denops.cmd(`call skkeleton#handle("handleKey", {"key": "${key}"})`); + } + assertEquals(await fn.getline(denops, "."), "テストする"); + assertEquals(await fn.col(denops, "."), 16); + + // take it back, then go from the henkan back to the reading with cancel + await denops.cmd('call skkeleton#handle("handleKey", {"key": ""})'); + await denops.cmd('call skkeleton#handle("handleKey", {"key": ""})'); + assertEquals(await fn.getline(denops, "."), "▽てすとする"); + + // mimics a completion engine replacing the pre-edit with the item it + // confirms and reporting it back through the completion source + await denops.cmd("call setline('.', '手酢戸する')"); + await denops.cmd("call cursor(1, 10)"); + await denops.dispatcher.completeCallback( + "てすと", + "手酢戸", + "okurinasi", + "手酢戸", + ); + assertEquals(await fn.getline(denops, "."), "手酢戸する"); + // back to the end of the line, not to where 手酢戸 ends (which would be 10) + assertEquals(await fn.col(denops, "."), 16); + }, +}); + +test({ + mode: "nvim", // can input mode test only in nvim + name: "kakutei undo forgets the cursor once nothing is left to confirm", + async fn(denops: Denops) { + const l = await currentLibrary.get(); + await l.registerHenkanResult("okurinasi", "てすと", "手酢戸"); + await l.registerHenkanResult("okurinasi", "てすと", "テスト"); + await denops.cmd( + 'call skkeleton#register_keymap("input", "", "kakuteiUndo")', + ); + // Note: spelled out because config is shared between the tests in this + // file, and cancelling all the way at once is what this one needs + await denops.cmd("call skkeleton#config(#{immediatelyCancel: v:true})"); + await denops.cmd("startinsert"); + + for (const key of ["T", "e", "s", "u", "t", "o", " ", ""]) { + await denops.cmd(`call skkeleton#handle("handleKey", {"key": "${key}"})`); + } + for (const key of ["s", "u", "r", "u"]) { + await denops.cmd(`call skkeleton#handle("handleKey", {"key": "${key}"})`); + } + assertEquals(await fn.getline(denops, "."), "テストする"); + + // take it back and then give it up, which leaves nothing to be confirmed + // where the undo happened + for (const key of ["", ""]) { + await denops.cmd(`call skkeleton#handle("handleKey", {"key": "${key}"})`); + } + assertEquals(await fn.getline(denops, "."), "する"); + + // an unrelated kakutei has to end where it ends, not at the column the + // abandoned undo remembered (which would be 16) + for (const key of ["T", "e", "s", "u", "t", "o", " ", ""]) { + await denops.cmd(`call skkeleton#handle("handleKey", {"key": "${key}"})`); + } + assertEquals(await fn.getline(denops, "."), "テストする"); + assertEquals(await fn.col(denops, "."), 10); + }, +}); + +test({ + mode: "nvim", // can input mode test only in nvim + name: "kakutei undo puts the cursor back after a completion of a completion", + async fn(denops: Denops) { + const l = await currentLibrary.get(); + await l.registerHenkanResult("okurinasi", "ほかん", "補間"); + await l.registerHenkanResult("okurinasi", "ほかん", "補完"); + await denops.cmd( + 'call skkeleton#register_keymap("input", "", "kakuteiUndo")', + ); + await denops.cmd("call skkeleton#config(#{immediatelyCancel: v:false})"); + await denops.cmd("set virtualedit=onemore"); + await denops.cmd("startinsert"); + + // a completion confirms 補完, then the rest is typed on + for (const key of ["H", "o"]) { + await denops.cmd(`call skkeleton#handle("handleKey", {"key": "${key}"})`); + } + await denops.cmd("call setline('.', '補完')"); + await denops.cmd("call cursor(1, 7)"); + await denops.dispatcher.completeCallback( + "ほかん", + "補完", + "okurinasi", + "補完", + ); + for (const key of ["s", "u", "r", "u"]) { + await denops.cmd(`call skkeleton#handle("handleKey", {"key": "${key}"})`); + } + assertEquals(await fn.getline(denops, "."), "補完する"); + assertEquals(await fn.col(denops, "."), 13); + + // take it back and go from the henkan to the reading + await denops.cmd('call skkeleton#handle("handleKey", {"key": ""})'); + assertEquals(await fn.getline(denops, "."), "▼補完する"); + await denops.cmd('call skkeleton#handle("handleKey", {"key": ""})'); + assertEquals(await fn.getline(denops, "."), "▽ほかんする"); + + // an engine with auto_insert writes its preview into the buffer while the + // item is only being selected. The next key handling sees that as a + // mismatch against the pre-edit and resets the state over it, which must + // not be taken for the reading having been given up on + await denops.cmd("call setline('.', '補間する')"); + await denops.cmd("call cursor(1, 7)"); + await denops.cmd('call skkeleton#handle("handleKey", {"key": ""})'); + + // the selected item is then confirmed + await denops.dispatcher.completeCallback( + "ほかん", + "補間", + "okurinasi", + "補間", + ); + assertEquals(await fn.getline(denops, "."), "補間する"); + assertEquals(await fn.col(denops, "."), 13); + }, +}); + +test({ + mode: "nvim", // can input mode test only in nvim + name: "kakutei undo leaves the cursor to what a key has started since", + async fn(denops: Denops) { + const l = await currentLibrary.get(); + await l.registerHenkanResult("okurinasi", "かく", "各"); + await l.registerHenkanResult("okurinasi", "かくい", "各位"); + await denops.cmd( + 'call skkeleton#register_keymap("input", "", "kakuteiUndo")', + ); + await denops.cmd("call skkeleton#config(#{immediatelyCancel: v:false})"); + await denops.cmd("set virtualedit=onemore"); + await denops.cmd("startinsert"); + + // a completion confirms 各位 out of ▽かく + for (const key of ["K", "a", "k", "u"]) { + await denops.cmd(`call skkeleton#handle("handleKey", {"key": "${key}"})`); + } + await denops.cmd("call setline('.', '各位')"); + await denops.cmd("call cursor(1, 7)"); + await denops.dispatcher.completeCallback( + "かくい", + "各位", + "okurinasi", + "各位", + ); + assertEquals(await fn.getline(denops, "."), "各位"); + + // take it back, go to the reading and shorten it + for (const key of ["", "", ""]) { + await denops.cmd(`call skkeleton#handle("handleKey", {"key": "${key}"})`); + } + assertEquals(await fn.getline(denops, "."), "▽かく"); + + // 各 is only selected, and an uppercase key starts a new henkan point + // instead of confirming it + await denops.cmd("call setline('.', '各')"); + await denops.cmd("call cursor(1, 4)"); + await denops.cmd('call skkeleton#handle("handleKey", {"key": "J"})'); + assertEquals(await fn.getline(denops, "."), "各▽j"); + const col = await fn.col(denops, "."); + + // the engine reports the selected candidate afterwards, from a scheduled + // callback. The cursor belongs to the ▽ that key has started, not to the + // column the undo remembered (which would be 4) + await denops.dispatcher.completeCallback("かく", "各", "okurinasi", "各"); + assertEquals(await fn.col(denops, "."), col); + }, +}); + +test({ + mode: "nvim", // can input mode test only in nvim + name: "kakutei undo leaves the cursor to a henkan the same key has opened", + async fn(denops: Denops) { + const l = await currentLibrary.get(); + await l.registerHenkanResult("okurinasi", "かく", "各"); + await l.registerHenkanResult("okurinasi", "かく", "核"); + await l.registerHenkanResult("okurinasi", "かくい", "各位"); + await denops.cmd( + 'call skkeleton#register_keymap("input", "", "kakuteiUndo")', + ); + await denops.cmd( + "call skkeleton#config(#{immediatelyCancel: v:false, eggLikeNewline: v:true})", + ); + await denops.cmd("set virtualedit=onemore"); + await denops.cmd("startinsert"); + + // 各位 is confirmed and the rest is typed on + for (const key of ["K", "a", "k", "u", "i", " ", ""]) { + await denops.cmd(`call skkeleton#handle("handleKey", {"key": "${key}"})`); + } + for (const key of ["h", "o", "g", "e", "h", "o", "g", "e"]) { + await denops.cmd(`call skkeleton#handle("handleKey", {"key": "${key}"})`); + } + assertEquals(await fn.getline(denops, "."), "各位ほげほげ"); + assertEquals(await fn.col(denops, "."), 19); + + // take it back, shorten the reading and convert again + for (const key of ["", "", "", " "]) { + await denops.cmd(`call skkeleton#handle("handleKey", {"key": "${key}"})`); + } + assertEquals(await fn.getline(denops, "."), "▼核ほげほげ"); + + // an uppercase key confirms 核 and opens a henkan after it in one go. The + // cursor belongs to the one it has opened, not to the column the undo + // remembered (which would be 16, in the middle of ほげほげ) + await denops.cmd('call skkeleton#handle("handleKey", {"key": "J"})'); + assertEquals(await fn.getline(denops, "."), "核▽jほげほげ"); + assertEquals(await fn.col(denops, "."), 8); + }, +}); + +test({ + mode: "nvim", // can input mode test only in nvim + name: "kakutei undo does not follow the cursor onto a new line", + async fn(denops: Denops) { + const l = await currentLibrary.get(); + await l.registerHenkanResult("okurinasi", "かく", "核"); + await denops.cmd( + 'call skkeleton#register_keymap("input", "", "kakuteiUndo")', + ); + // Note: a kakutei out of inserts a newline without this + await denops.cmd("call skkeleton#config(#{eggLikeNewline: v:false})"); + await denops.cmd("set virtualedit=onemore"); + await denops.cmd("startinsert"); + + for (const key of ["K", "a", "k", "u", " ", ""]) { + await denops.cmd(`call skkeleton#handle("handleKey", {"key": "${key}"})`); + } + for (const key of ["h", "o", "g", "e"]) { + await denops.cmd(`call skkeleton#handle("handleKey", {"key": "${key}"})`); + } + assertEquals(await fn.getline(denops, "."), "核ほげ"); + + // the undo walks back, and confirming with both writes the candidate + // and breaks the line + await denops.cmd('call skkeleton#handle("handleKey", {"key": ""})'); + assertEquals(await fn.getline(denops, "."), "▼核ほげ"); + await denops.cmd('call skkeleton#handle("handleKey", {"key": ""})'); + + // the cursor is on the line the newline has started, so the column the undo + // remembered means nothing and is not used + assertEquals(await fn.getline(denops, 1), "核"); + assertEquals(await fn.getline(denops, 2), "ほげ"); + assertEquals(await fn.line(denops, "."), 2); + assertEquals(await fn.col(denops, "."), 1); + }, +}); diff --git a/denops/skkeleton/main.ts b/denops/skkeleton/main.ts index 49650a9cb..3cf230422 100644 --- a/denops/skkeleton/main.ts +++ b/denops/skkeleton/main.ts @@ -44,6 +44,11 @@ type HandleResult = { phase: string; }; result: string; + // where to put the cursor once {result} has been applied, or 0 for leaving it + // where the applied text ends + // |skkeleton-functions-kakuteiUndo| is the only thing asking for this so far + restoreLnum: number; + restoreCol: number; }; const isOpts = is.ObjectOf({ @@ -233,7 +238,15 @@ async function handle( } function buildResult(result: string): HandleResult { - const state = currentContext.get().state; + const context = currentContext.get(); + const state = context.state; + // Note: asked for once and then forgotten, so that it does not travel along + // with every following key handling + // dropped when the handling has left something pending: an uppercase + // key confirms a henkan and opens a new one after it in one go, and + // the cursor belongs to the one it has opened + const restore = context.hasPendingInput ? void 0 : context.restorePoint; + context.restorePoint = void 0; let phase = ""; if (state.type === "input") { if (state.mode === "okurinasi") { @@ -252,6 +265,8 @@ function buildResult(result: string): HandleResult { phase, }, result, + restoreLnum: restore?.lnum ?? 0, + restoreCol: restore?.col ?? 0, }; } @@ -301,6 +316,18 @@ export const main: Entrypoint = async (denops) => { context.prevInput = prevInput; context.bufnr = bufnr; context.lnum = lnum; + // Note: a column |skkeleton-functions-kakuteiUndo| has remembered is only + // good while skkeleton is still writing where the undo happened. + // Once there is nothing pending -- the reading taken all the way + // back with cancel, say -- nobody is going to confirm anything + // there, and the column must not move the cursor on some unrelated + // kakutei later on. + // asked before the mismatch below resets the state: a completion + // engine writing its preview into the buffer is a mismatch, and + // the reading it is about to complete is very much still pending + if (!context.hasPendingInput) { + context.forgetPointRestore(); + } // only now is it known where a kakutei has been written to the buffer const resolved = context.resolvePendingKakutei(); // 補完の後などpreEditとバッファが不一致している状態の時にリセットする diff --git a/doc/skkeleton-functions.jax b/doc/skkeleton-functions.jax index bf93756eb..4a573ab39 100644 --- a/doc/skkeleton-functions.jax +++ b/doc/skkeleton-functions.jax @@ -22,6 +22,9 @@ kakuteiUndo *skkeleton-functions-kakuteiUndo* もしません。 カーソルを戻すのは挿入モードのみです。コマンドラインモードと端末モー ドでは、従来どおりカーソルの直前に残っている場合のみ動作します。 + 選び直した候補を確定すると、カーソルは確定アンドゥを実行した位置に + 戻ります。選び直した候補の長さが元と違う場合は、その差だけずれた位置 + になります。 |skkeleton-function-purgeCandidate| で候補を削除した後と、辞書登録 による確定も取り消せません。skkeletonを無効化した場合も忘れます。 ユーザー辞書への学習は取り消されません。