summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJackson Moore <jacksonmoore@tuta.io>2026-09-04 12:32:44 +0200
committerJackson Moore <jacksonmoore@tuta.io>2026-09-04 12:32:44 +0200
commitc6f2c9941742c84a43faee9e6426786bc1e8f13c (patch)
treecd14010eb2d37aed874e2395f7b707acfbe4cbd4
parenta32ac60fad13db062f23478e8da74d5dbba0edc8 (diff)
Apply persistent match behavior
-rw-r--r--lua/clever_f/feedback_service.lua100
-rw-r--r--tests/run.lua61
2 files changed, 154 insertions, 7 deletions
diff --git a/lua/clever_f/feedback_service.lua b/lua/clever_f/feedback_service.lua
index 9703502..037c7fd 100644
--- a/lua/clever_f/feedback_service.lua
+++ b/lua/clever_f/feedback_service.lua
@@ -1,6 +1,7 @@
local domain = require("clever_f.domain")
local policy = require("clever_f.policy")
local state_transitions = require("clever_f.state_transitions")
+local text_topology = require("clever_f.text_topology")
local M = {}
local FeedbackService = {}
@@ -307,6 +308,78 @@ function M.persistent_context_eligible(context)
or context.key == "cvr"
end
+function M.persistent_destination(
+ view,
+ target_position,
+ descriptor,
+ endpoint_policy
+)
+ if not text_topology.TextView.is(view) then
+ fail("persistent feedback requires a TextView", 2)
+ end
+ target_position = domain.Position.coerce(target_position)
+ descriptor = domain.Descriptor.from_string(descriptor)
+ endpoint_policy = domain.EndpointPolicy.from_string(endpoint_policy)
+
+ if endpoint_policy == domain.EndpointPolicy.VISUAL_EXCLUSIVE
+ and descriptor.direction == domain.Direction.FORWARD
+ then
+ if descriptor.family == domain.Family.FIND then
+ return view:successor(target_position)
+ end
+ return target_position
+ end
+ if descriptor.family == domain.Family.FIND then
+ return target_position
+ end
+ if descriptor.direction == domain.Direction.FORWARD then
+ return view:predecessor(target_position)
+ end
+ return view:successor(target_position)
+end
+
+function M.persistent_match_positions(
+ view,
+ match_start_line,
+ target_plan,
+ descriptor,
+ endpoint_policy
+)
+ if not text_topology.TextView.is(view) then
+ fail("persistent feedback requires a TextView", 2)
+ end
+ if not domain.TargetPlan.is(target_plan) then
+ fail("persistent feedback requires a TargetPlan", 2)
+ end
+ local positions = {}
+ local seen = {}
+ local candidates = view:iter_line_forward(match_start_line)
+ while true do
+ local position, character = candidates()
+ if position == nil then
+ break
+ end
+ if target_plan:matches(character, position, view) then
+ local destination = M.persistent_destination(
+ view,
+ position,
+ descriptor,
+ endpoint_policy
+ )
+ if destination ~= nil then
+ local key = tostring(destination.line)
+ .. ":"
+ .. tostring(destination.byte_column)
+ if not seen[key] then
+ seen[key] = true
+ positions[#positions + 1] = destination
+ end
+ end
+ end
+ end
+ return positions
+end
+
function FeedbackService:build_persistent(specification)
if type(specification) ~= "table" then
fail("persistent feedback request must be a table", 2)
@@ -324,17 +397,30 @@ function FeedbackService:build_persistent(specification)
if specification.motion_plan.target_plan ~= specification.target_plan then
fail("persistent feedback must reuse the movement TargetPlan", 2)
end
- local descriptor = specification.descriptor
- or specification.motion_plan.descriptor
- local endpoint_policy = specification.endpoint_policy
- or specification.motion_plan.endpoint_policy
+ local descriptor = domain.Descriptor.from_string(
+ specification.descriptor or specification.motion_plan.descriptor
+ )
+ local endpoint_policy = domain.EndpointPolicy.from_string(
+ specification.endpoint_policy or specification.motion_plan.endpoint_policy
+ )
+ local anchor = domain.Position.coerce(specification.anchor)
+ local view = specification.text_view
+ or text_topology.from_host(service_records[self].host)
return {
context = context,
- anchor = domain.Position.coerce(specification.anchor),
+ anchor = anchor,
target_plan = specification.target_plan,
motion_plan = specification.motion_plan,
- descriptor = domain.Descriptor.from_string(descriptor),
- endpoint_policy = domain.EndpointPolicy.from_string(endpoint_policy),
+ descriptor = descriptor,
+ endpoint_policy = endpoint_policy,
+ positions = M.persistent_match_positions(
+ view,
+ anchor.line,
+ specification.target_plan,
+ descriptor,
+ endpoint_policy
+ ),
+ text_view = view,
window = specification.window,
}
end
diff --git a/tests/run.lua b/tests/run.lua
index 57f4988..b044379 100644
--- a/tests/run.lua
+++ b/tests/run.lua
@@ -5872,6 +5872,67 @@ test("Persistent feedback uses its selected descriptor and endpoint policy", fun
same(domain.EndpointPolicy.VISUAL_EXCLUSIVE, selected.endpoint_policy)
end)
+test("Persistent positions apply target predicates and endpoint transforms", function()
+ local view = text_topology.new({ "aA!\\\227\129\130" }, "utf-8")
+ local insensitive = target_plan.build(target("a"), {
+ ignore_case = true,
+ smart_case = false,
+ use_migemo = false,
+ chars_match_any_signs = "",
+ })
+ local symbols = target_plan.build(target("!"), {
+ ignore_case = false,
+ smart_case = false,
+ use_migemo = false,
+ chars_match_any_signs = "!",
+ })
+ local backslash = target_plan.build(target("\\"), matching_policy())
+ local migemo = domain.TargetPlan.new({
+ target = target("a"),
+ kind = domain.TargetPlanKind.MIGEMO,
+ case_mode = domain.CaseMode.SENSITIVE,
+ matcher = function(character)
+ return character == "\227\129\130"
+ end,
+ })
+ local function columns(plan)
+ local result = {}
+ for index, position in ipairs(feedback_service.persistent_match_positions(
+ view,
+ 1,
+ plan,
+ "f",
+ domain.EndpointPolicy.REGULAR
+ )) do
+ result[index] = position.byte_column
+ end
+ return result
+ end
+
+ list_same({ 1, 2 }, columns(insensitive))
+ list_same({ 3, 4 }, columns(symbols))
+ list_same({ 4 }, columns(backslash))
+ list_same({ 5 }, columns(migemo))
+ same(
+ domain.Position.new(1, 1),
+ feedback_service.persistent_destination(
+ view,
+ domain.Position.new(1, 2),
+ "t",
+ domain.EndpointPolicy.REGULAR
+ )
+ )
+ same(
+ domain.Position.new(1, 2),
+ feedback_service.persistent_destination(
+ view,
+ domain.Position.new(1, 2),
+ "t",
+ domain.EndpointPolicy.VISUAL_EXCLUSIVE
+ )
+ )
+end)
+
for _, item in ipairs(tests) do
local ok, failure = xpcall(item.body, debug.traceback)
if not ok then