From c6f2c9941742c84a43faee9e6426786bc1e8f13c Mon Sep 17 00:00:00 2001 From: Jackson Moore Date: Fri, 4 Sep 2026 12:32:44 +0200 Subject: Apply persistent match behavior --- lua/clever_f/feedback_service.lua | 100 +++++++++++++++++++++++++++++++++++--- tests/run.lua | 61 +++++++++++++++++++++++ 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 -- cgit v1.2.3