summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--lua/clever_f/host_adapter.lua129
-rw-r--r--tests/run.lua86
2 files changed, 214 insertions, 1 deletions
diff --git a/lua/clever_f/host_adapter.lua b/lua/clever_f/host_adapter.lua
index 17c29ef..a81a44c 100644
--- a/lua/clever_f/host_adapter.lua
+++ b/lua/clever_f/host_adapter.lua
@@ -48,6 +48,7 @@ function HostAdapter.new(options)
cursor_presentations = {},
events = {},
dot_repeat = nil,
+ dot_bridge = nil,
event_order = {},
augroup = nil,
}
@@ -124,6 +125,8 @@ function HostAdapter:set_operator_inclusive(enabled)
end
end
+local install_dot_bridge
+
function HostAdapter:register_dot_repeat(payload, callback)
if not domain.DotPayload.is(payload) then
fail("dot-repeat payload must be a DotPayload", 2)
@@ -131,10 +134,18 @@ function HostAdapter:register_dot_repeat(payload, callback)
if callback ~= nil and type(callback) ~= "function" then
fail("dot-repeat callback must be a function", 2)
end
- adapter_records[self].dot_repeat = {
+ local record = adapter_records[self]
+ record.dot_repeat = {
payload = payload,
callback = callback,
+ operator = self:read_pending_operator(),
}
+ if install_dot_bridge ~= nil then
+ install_dot_bridge(self)
+ end
+ if record.dot_bridge ~= nil then
+ record.dot_bridge.awaiting_change = true
+ end
return payload
end
@@ -479,6 +490,122 @@ function HostAdapter:remove_event_registration(identity)
return true
end
+local DOT_MOTION_MAPPING = "<Plug>(clever-f-dot-motion)"
+
+local function dot_bridge_supported(runtime)
+ return type(runtime.keymap) == "table"
+ and type(runtime.keymap.set) == "function"
+ and type(runtime.keymap.del) == "function"
+ and type(runtime.fn.maparg) == "function"
+ and type(runtime.fn.mapset) == "function"
+ and type(runtime.api.nvim_feedkeys) == "function"
+end
+
+local function restore_dot_mapping(record)
+ local bridge = record.dot_bridge
+ if bridge == nil or not bridge.active then
+ return false
+ end
+ bridge.active = false
+ pcall(record.runtime.keymap.del, "n", ".")
+ if type(bridge.previous_mapping) == "table"
+ and next(bridge.previous_mapping) ~= nil
+ then
+ record.runtime.fn.mapset("n", false, bridge.previous_mapping)
+ end
+ return true
+end
+
+local function dot_replay_keys(runtime, count, operator)
+ local prefix = count > 0 and tostring(count) or ""
+ local keys = prefix .. operator .. DOT_MOTION_MAPPING
+ if type(runtime.keycode) == "function" then
+ return runtime.keycode(keys)
+ end
+ return runtime.api.nvim_replace_termcodes(keys, true, false, true)
+end
+
+install_dot_bridge = function(adapter)
+ local record = adapter_records[adapter]
+ local runtime = record.runtime
+ if not dot_bridge_supported(runtime) then
+ return nil
+ end
+ local bridge = record.dot_bridge
+ if bridge == nil then
+ bridge = {
+ active = false,
+ awaiting_change = false,
+ previous_mapping = nil,
+ }
+ record.dot_bridge = bridge
+ runtime.keymap.set("o", DOT_MOTION_MAPPING, function()
+ local registration = record.dot_repeat
+ if registration == nil or registration.callback == nil then
+ return
+ end
+ local count = runtime.v.count1
+ if type(count) ~= "number" or count < 1 then
+ count = 1
+ end
+ local outcome = registration.callback(
+ registration.payload,
+ domain.Count.new(count)
+ )
+ if domain.ActionOutcome.is(outcome) then
+ adapter:translate_action_outcome(outcome)
+ end
+ end, {
+ silent = true,
+ remap = false,
+ desc = "clever-f dot motion",
+ })
+ local has_cmd_atom = type(runtime.fn.exists) == "function"
+ and runtime.fn.exists("##CmdAtom") == 1
+ local ownership_events = has_cmd_atom
+ and "CmdAtom"
+ or { "TextChanged", "TextChangedI", "TextChangedP" }
+ runtime.api.nvim_create_autocmd(ownership_events, {
+ group = event_augroup(record),
+ desc = "clever-f dot ownership",
+ callback = function(event)
+ if has_cmd_atom and not (event.data and event.data.changed) then
+ return
+ end
+ if bridge.awaiting_change then
+ bridge.awaiting_change = false
+ return
+ end
+ restore_dot_mapping(record)
+ end,
+ })
+ end
+ if not bridge.active then
+ bridge.previous_mapping = runtime.fn.maparg(".", "n", false, true)
+ runtime.keymap.set("n", ".", function()
+ local registration = record.dot_repeat
+ if registration == nil or registration.operator == "" then
+ restore_dot_mapping(record)
+ runtime.api.nvim_feedkeys(".", "n", false)
+ return
+ end
+ bridge.awaiting_change = true
+ local count = runtime.v.count or 0
+ runtime.api.nvim_feedkeys(
+ dot_replay_keys(runtime, count, registration.operator),
+ "n",
+ false
+ )
+ end, {
+ silent = true,
+ remap = false,
+ desc = "clever-f dot repeat",
+ })
+ bridge.active = true
+ end
+ return bridge
+end
+
function HostAdapter:deliver_event(name, payload)
if type(name) ~= "string" or name == "" then
fail("event name must be a nonempty string", 2)
diff --git a/tests/run.lua b/tests/run.lua
index d4535ef..fb84dd3 100644
--- a/tests/run.lua
+++ b/tests/run.lua
@@ -417,6 +417,92 @@ test("Host adapter connects motion endpoints and dot payloads", function()
same(3, replayed_count.value)
end)
+test("Host adapter bridges native dot to resolved payload replay", function()
+ local mappings = {}
+ local deleted = {}
+ local restored
+ local ownership_autocmd
+ local fed
+ local runtime = {
+ api = {
+ nvim_create_augroup = function()
+ return 9
+ end,
+ nvim_create_autocmd = function(_, options)
+ ownership_autocmd = options.callback
+ return 12
+ end,
+ nvim_feedkeys = function(keys, mode, escape_csi)
+ fed = { keys = keys, mode = mode, escape_csi = escape_csi }
+ end,
+ },
+ fn = {
+ exists = function(name)
+ same("##CmdAtom", name)
+ return 1
+ end,
+ maparg = function(lhs, mode, abbreviation, dictionary)
+ same(".", lhs)
+ same("n", mode)
+ falsy(abbreviation)
+ truthy(dictionary)
+ return { lhs = ".", rhs = "prior-dot" }
+ end,
+ mapset = function(mode, abbreviation, mapping)
+ restored = { mode = mode, abbreviation = abbreviation, mapping = mapping }
+ end,
+ },
+ keymap = {
+ set = function(mode, lhs, callback, options)
+ mappings[mode .. "\0" .. lhs] = {
+ callback = callback,
+ options = options,
+ }
+ end,
+ del = function(mode, lhs)
+ deleted[#deleted + 1] = mode .. "\0" .. lhs
+ mappings[mode .. "\0" .. lhs] = nil
+ end,
+ },
+ keycode = function(keys)
+ return keys
+ end,
+ v = { operator = "d", count = 2, count1 = 2 },
+ }
+ local adapter = host_adapter.new({ runtime = runtime })
+ local payload = domain.DotPayload.new(
+ "f",
+ domain.TargetValue.character("e", string.byte("e"))
+ )
+ local replayed
+ adapter:register_dot_repeat(payload, function(value, count)
+ replayed = { payload = value, count = count }
+ return domain.ActionOutcome.neutral(domain.Position.new(1, 1))
+ end)
+
+ local dot = mappings["n\0."]
+ local motion = mappings["o\0<Plug>(clever-f-dot-motion)"]
+ truthy(dot ~= nil)
+ truthy(motion ~= nil)
+ dot.callback()
+ same("2d<Plug>(clever-f-dot-motion)", fed.keys)
+ same("n", fed.mode)
+ falsy(fed.escape_csi)
+
+ motion.callback()
+ same(payload, replayed.payload)
+ same(2, replayed.count.value)
+
+ ownership_autocmd({ data = { changed = true } })
+ truthy(mappings["n\0."] ~= nil)
+ ownership_autocmd({ data = { changed = true } })
+ same(nil, mappings["n\0."])
+ list_same({ "n\0." }, deleted)
+ same("n", restored.mode)
+ falsy(restored.abbreviation)
+ same("prior-dot", restored.mapping.rhs)
+end)
+
test("Host adapter reads encoding and uses Nvim case conversion", function()
local lowered = {}
local runtime = {