Skip to content
73 changes: 71 additions & 2 deletions doc/skkelua.txt
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ INTERFACE |skkelua-interface|
AUTOCMDS |skkelua-autocmds|
OPTIONS |skkelua-options|
COMPLETION |skkelua-completion|
EXTENSION |skkelua-completion-extension|
MACRO |skkelua-macro|
INDICATOR |skkelua-indicator|
SOURCES |skkelua-sources|
Expand Down Expand Up @@ -175,6 +176,11 @@ AUTOCMDS *skkelua-autocmds*
参照できます。
skkelua-handled
キー処理の完了後。
skkelua-pre-edit-restored
辞書登録のキャンセルなど、タイプを伴わずに pre-edit が復元された
後。タイプを伴わないので補完 UI の自動表示は働きません。UI の補完を
再要求するにはこのイベントで呼んでください (組み込みの補完は
skkelua が再要求します)。
skkelua-persistent-enable / skkelua-persistent-disable
persistent mode の有効化・無効化時。

Expand Down Expand Up @@ -310,8 +316,10 @@ pureSpace (|skkelua-options|) も合わせて有効にすると快適です。
- 候補の選択挿入中に > を押すと、その候補で確定して接尾辞入力 (▽>) に
入ります (▼ 選択中の > と同じ動作)。接尾辞入力中 (▽>けい) も
補完メニューに候補が出ます
- 候補は |v:completed_item| 確定時 (CTRL-Y など) に pre-edit ごと置換され、
ユーザー辞書へ登録されます (以後の候補順に反映されます)
- 候補は確定時 (CTRL-Y など) に pre-edit ごと置換され、ユーザー辞書へ
登録されます (以後の候補順に反映されます)。登録は候補の command
(|skkelua-completion-extension|) として実行されるため、確定せず閉じた
(CTRL-E や <Esc>) 場合は登録されません
- 候補を未選択のまま CTRL-Y を押すと、変換せずかなのまま確定します
(デフォルトマップの kakuteiPassThrough、|skkelua-kanatable| 参照)
- 送りなし入力の候補は、ユーザー辞書で確定した候補 (ランク持ち) が
Expand Down Expand Up @@ -356,6 +364,67 @@ Note: |vim.lsp.completion.enable()| は最初にバッファの補完状態を
他の設定・プラグインが opts 無しで enable(true) を呼ぶと、skkelua の
autotrigger が失われて候補が自動表示されなくなります。

==============================================================================
EXTENSION *skkelua-completion-extension*

外部補完との接続 ~

Neovim 組み込み以外の補完 UI でも、skkelua の候補を LSP 経由で受け取れます。
候補の生成・辞書の学習・[辞書登録] は skkelua の in-process LSP サーバーが担い、
UI 側は通常の LSP クライアントとして接続します。

require("skkelua.lsp").start({bufnr}?) *skkelua.lsp.start()*
{bufnr} (省略時は現在のバッファ) を共有 LSP クライアントへ attach し、
client id を返します (起動できなければ nil)。クライアントはバッファ間で
共有され、SKK の有効・無効に関係なく残ります。組み込みの補完 UI は
有効化しません (|skkelua-completion| の completion.enabled とは
独立です)。
候補は補完要求の位置 (UTF-8 のバイトオフセット) と、読み込み済み
バッファ名 (= URI) から引きます。名前の無いバッファは全て同じ URI に
なるため、現在のバッファが要求元として扱われます。仮想ドキュメントを
使う場合は、バッファ名を要求の URI に合わせてください。
返す候補はスニペット化などの UI 固有の整形をしていません。
>lua
vim.api.nvim_create_autocmd("InsertEnter", {
callback = function()
require("skkelua.lsp").start()
end,
})
<
候補には command "skkelua.accept" が付いており、サーバーが
`executeCommandProvider` で宣言しています。クライアントが候補の確定時
(キャンセル時ではなく) に |vim.lsp.Client:exec_cmd()| 相当で command を
実行すると、ユーザー辞書への学習と [辞書登録] の登録プロンプトが動きます。
command を実行しない UI では、これらは動きません (pum.vim と nvim-cmp を
自動検出する場合も同じです)。

require("skkelua.completion").set_adapter({adapter}?)
*skkelua.completion.set_adapter()*
補完メニューを持つ UI を skkelua のキー処理 (CTRL-Y, <CR> など) へ
接続します。nil で自動検出に戻ります。{adapter} のフィールド:
- selected: `fun(): table?`
メニューで選択中の LSP CompletionItem (data を含む)。メニューが無い
か未選択なら nil を返します。item を持てない UI は空のテーブルで
「選択中」だけを伝えられます。例外を投げた場合は未選択として扱います
- confirm: `fun(): string`
選択中の候補を確定するキー列 (|nvim_replace_termcodes()| 済み)。
文字列が "<Cmd>" で始まる場合は、skkelua が <CR> を補って実行します。
同期的にテキストを変更しないでください
>lua
require("skkelua.completion").set_adapter({
selected = function()
return my_menu.selected_item()
end,
confirm = function()
return vim.keycode("<Cmd>lua my_menu.confirm()<CR>")
end,
})
<
明示的に設定したアダプターは自動検出より優先されます。アダプターを設定
している間は、組み込みの補完メニューや pum.vim / nvim-cmp の表示状態は
参照されません。アダプターを設定しない場合は、表示中の pum.vim と nvim-cmp
を自動検出して CTRL-Y などの確定を連携します。

==============================================================================
MACRO *skkelua-macro*

Expand Down
144 changes: 144 additions & 0 deletions lua/skkelua/completion.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
-- 組み込み以外の補完 UI (pum.vim / nvim-cmp / 外部 UI) の選択取得と確定。
-- 補完候補そのものは lsp.lua の in-process サーバーが返し、
-- ここは「いま何が選択されているか」と
-- 「選択中の候補を確定するキー」だけを UI ごとの差異として吸収する。

local M = {}

---@class skkelua.CompletionAdapter
---@field selected fun(): table? メニューで選択中の LSP CompletionItem (data を含む)。メニューが無い・未選択なら nil。item を持てない UI は空のテーブルで「選択中」だけを伝える
---@field confirm fun(): string 選択中の候補を確定するキー列 (termcode 済み。"<Cmd>" で始まる文字列なら <CR> を補って実行する)。同期的にテキストを変更しない

---@type skkelua.CompletionAdapter?
local adapter

local function is_truthy(v)
return v == true or v == 1
end

--- pum.vim / cmp が持つ候補から LSP の CompletionItem を取り出す
---@param value any
---@return table
local function decode_item(value)
if type(value) == "string" then
local ok, decoded = pcall(vim.json.decode, value)
value = ok and decoded or nil
end
return type(value) == "table" and value or {}
end

local pum_adapter = {
selected = function()
local info = vim.fn["pum#complete_info"]({ "selected" })
if (info.selected or -1) < 0 then
return nil
end
return decode_item(vim.tbl_get(vim.fn["pum#current_item"]() or {}, "user_data", "lspitem"))
end,
confirm = function()
return "<Cmd>call pum#map#confirm()"
end,
}

local cmp_adapter = {
selected = function()
local entry = package.loaded["cmp"].get_active_entry()
return entry and entry:get_completion_item() or nil
end,
confirm = function()
return "<Cmd>lua require('cmp').confirm({select = true})"
end,
}

--- 補完 UI のアダプターを明示的に設定する。nil で自動検出に戻す。
--- アダプターは UI 自身の寿命・候補の変換・確定イベントを担当する
---@param value? skkelua.CompletionAdapter
function M.set_adapter(value)
adapter = value
end

--- 表示中のメニューを持つ UI を検出する。組み込みの補完メニューなら nil。
--- 明示的に設定されたアダプターは自動検出より優先する
local function detect()
if adapter then
return adapter
end
if vim.fn.exists("*pum#visible") == 1 and is_truthy(vim.fn["pum#visible"]()) then
return pum_adapter
end
local cmp = package.loaded["cmp"]
if cmp then
local ok, visible = pcall(cmp.visible)
if ok and visible then
return cmp_adapter
end
end
return nil
end

--- アダプターの呼び出しで例外が出てもキー処理を止めない。
--- state() は打鍵ごとに呼ばれるため、壊れたアダプターが全ての入力を巻き込む
---@param what string
---@param fn function
---@return boolean ok
---@return any result
local function call_adapter(what, fn)
local ok, result = pcall(fn)
if not ok then
vim.notify_once(("skkelua: completion adapter %s() failed: %s"):format(what, result), vim.log.levels.WARN)
end
return ok, result
end

--- 選択中の候補。組み込みの補完メニューが担当するときは external が false
---@return boolean external 組み込み以外の UI が担当するか
---@return table? item 選択中の CompletionItem
function M.selection()
local active = detect()
if not active then
return false
end
local ok, item = call_adapter("selected", active.selected)
-- 状態が分からない間は、未選択として扱う
return true, ok and type(item) == "table" and item or nil
end

--- 選択中の候補を確定するキー列
---@return string?
function M.confirm()
local active = detect()
if not active then
return nil
end
local ok, keys = call_adapter("confirm", active.confirm)
return ok and type(keys) == "string" and keys or nil
end

--- skkelua の候補が選択時にバッファへ挿入する文字列
---@param item? table
---@return string?
function M.inserted_text(item)
local data = M.item_data(item)
return data and data.text or nil
end

--- CompletionItem が skkelua の候補なら、その data を返す
---@param item? table
---@return table?
function M.item_data(item)
local data = item and item.data
if type(data) == "table" and data.skkelua == true then
return data
end
return nil
end

--- CompletionItem が skkelua の [辞書登録] 項目かどうか
---@param item? table
---@return boolean
function M.is_register_item(item)
local data = M.item_data(item)
return data ~= nil and data.register == true
end

return M
4 changes: 2 additions & 2 deletions lua/skkelua/function/dictionary.lua
Original file line number Diff line number Diff line change
Expand Up @@ -182,11 +182,11 @@ local function register_word_float(context)
replace_pre_edit(ctx:to_string())
ctx.preEdit:sync(ctx:to_string())
-- タイプを伴わない復元では補完の autotrigger が働かないため、
-- キー処理が終わって insert に入ったところで明示的にトリガーする
-- キー処理が終わって insert に入ったところで補完 UI に知らせる
vim.api.nvim_create_autocmd("SafeState", {
once = true,
callback = function()
require("skkelua.lsp").trigger()
require("skkelua.lsp").pre_edit_restored()
end,
})
end
Expand Down
42 changes: 19 additions & 23 deletions lua/skkelua/init.lua
Original file line number Diff line number Diff line change
Expand Up @@ -91,18 +91,11 @@ end

---@return string, table
local function complete_info()
if vim.fn.exists("*pum#visible") == 1 and is_truthy(vim.fn["pum#visible"]()) then
return "pum.vim", vim.fn["pum#complete_info"]({ "pum_visible", "selected" })
end
local cmp = package.loaded["cmp"]
if cmp then
local ok, visible = pcall(function()
return cmp.visible()
end)
if ok and visible then
local selected = cmp.get_active_entry() ~= nil
return "cmp", { pum_visible = true, selected = selected and 1 or -1 }
end
local external, item = require("skkelua.completion").selection()
if external then
-- 外部 UI は選択中の候補だけを伝える。未選択のメニューはキー処理に
-- 影響しないので、メニューが無いのと同じに扱う
return "external", { pum_visible = item ~= nil, selected = item and 0 or -1, completion_item = item }
end
-- items は選択中の [辞書登録] 項目の判定 (handle_impl) に使う
return "native", vim.fn.complete_info({ "pum_visible", "selected", "items" })
Expand Down Expand Up @@ -148,10 +141,8 @@ end
local function native_confirm_key(complete_type)
if complete_type == "native" then
return require("skkelua.notation").notation_to_key["<c-y>"]
elseif complete_type == "pum.vim" then
return "<Cmd>call pum#map#confirm()"
elseif complete_type == "cmp" then
return "<Cmd>lua require('cmp').confirm({select = true})"
elseif complete_type == "external" then
return require("skkelua.completion").confirm()
end
return nil
end
Expand Down Expand Up @@ -216,8 +207,8 @@ end
--- 消して候補を入れるキー列を出力する。マクロには「pre-edit の削除 + 候補」
--- だけが記録され、再生時は補完に頼らず同じ文字列が入る (<C-e> は <Cmd> の
--- 中で pum が見えている時だけ送るので、再生時には何もしない)。
--- 確定した候補の辞書登録は native の確定 (CompleteDone) を通らないため
--- ここで行う
--- 確定した候補の辞書登録は、native の確定 (クライアントが item.command を
--- 実行する経路) を通らないためここで行う
---@param context skkelua.Context
---@param data table 候補の data (lsp.lua)
---@param word string pum の item.word (data.text が無い場合の挿入文字列)
Expand Down Expand Up @@ -273,8 +264,12 @@ local function handle_impl(opts, vim_status)
if completed and type(info.items) == "table" then
sel_item = info.items[info.selected + 1]
end
local lsp = require("skkelua.lsp")
local data = lsp.item_data(sel_item)
-- 選択中の候補を LSP の CompletionItem として扱う
-- (native は complete_info の items から、外部 UI はアダプターから取る)
local completion = require("skkelua.completion")
local completion_item = info.completion_item
or vim.tbl_get(sel_item or {}, "user_data", "nvim", "lsp", "completion_item")
local data = completion.item_data(completion_item)
if
vim_status.completeType == "native"
and data
Expand All @@ -289,9 +284,10 @@ local function handle_impl(opts, vim_status)
else
local handled = handle_complete_key(completed, vim_status.completeType, notation_str)
if type(handled) == "string" then
-- [辞書登録] 項目の確定はバッファを変えず、CompleteDone からの
-- registerWord が変換入力の続きとして実行されるため状態を保つ
if not lsp.is_register_item(sel_item) then
-- [辞書登録] 項目の確定はバッファを変えず、候補の command
-- (skkelua.accept) からの registerWord が変換入力の続きとして
-- 実行されるため状態を保つ
if not completion.is_register_item(completion_item) then
require("skkelua.mode").initialize_state_with_abbrev(context, { "converter" })
context.preEdit:output("")
end
Expand Down
Loading
Loading