local acquisition_service_factory = require("clever_f.acquisition_service") local domain = require("clever_f.domain") local feedback_service_factory = require("clever_f.feedback_service") local motion_executor_factory = require("clever_f.motion_executor") local motion_plan_factory = require("clever_f.motion_plan") local policy = require("clever_f.policy") local repeat_resolver_factory = require("clever_f.repeat_resolver") local sequence_state = require("clever_f.sequence_state") local state_transitions = require("clever_f.state_transitions") local target_plan_factory = require("clever_f.target_plan") local text_topology = require("clever_f.text_topology") local M = {} local SequenceCoordinator = {} SequenceCoordinator.__index = SequenceCoordinator M.SequenceCoordinator = SequenceCoordinator local coordinator_records = setmetatable({}, { __mode = "k" }) local function fail(message, level) error(message, (level or 1) + 1) end local function descriptor_text(value) if domain.Descriptor.is(value) then return value.value end return tostring(value) end function M.validate_primary_descriptor(value) local descriptor = domain.Descriptor.try_from_string(value) if descriptor == nil then fail("clever-f: Invalid mapping '" .. descriptor_text(value) .. "'", 2) end return descriptor end function SequenceCoordinator.new(options) if SequenceCoordinator.is(options) then return options end if type(options) ~= "table" then fail("SequenceCoordinator options must be a table", 2) end local host = options.host or options local state = options.state or sequence_state.get() if not sequence_state.is(state) then fail("SequenceCoordinator state must be the plugin-global SequenceState", 2) end local transitions = options.transitions or options.state_transitions or state_transitions.new(state) local policy_service = options.policy or options.policy_service or policy.new(host) local resolver = options.repeat_resolver or options.resolver or repeat_resolver_factory.new({ state = state, transitions = transitions, policy = policy_service, clock = host, }) if type(resolver) ~= "table" or type(resolver.decide) ~= "function" then fail("SequenceCoordinator repeat resolver must provide decide", 2) end local feedback = options.feedback or options.feedback_service or feedback_service_factory.new({ host = host, state = state, transitions = transitions, policy = policy_service, }) local target_factory = options.target_factory or options.target_plan_factory or target_plan_factory.new({ policy = policy_service }) if type(target_factory) ~= "table" or type(target_factory.build) ~= "function" then fail("SequenceCoordinator target factory must provide build", 2) end local motion_factory = options.motion_factory or options.motion_plan_factory or motion_plan_factory.new({ policy = policy_service }) if type(motion_factory) ~= "table" or type(motion_factory.build_for_context) ~= "function" then fail("SequenceCoordinator motion factory must build contextual plans", 2) end local acquisition = options.acquisition or options.acquisition_service or acquisition_service_factory.new({ host = host, state = state, transitions = transitions, policy = policy_service, feedback = feedback, target_factory = target_factory, motion_factory = motion_factory, }) if type(acquisition) ~= "table" or type(acquisition.acquire) ~= "function" then fail("SequenceCoordinator acquisition service must provide acquire", 2) end local executor = options.motion_executor or options.executor or motion_executor_factory.new({ host = host, state = state, transitions = transitions, feedback = feedback, }) if type(executor) ~= "table" or type(executor.execute) ~= "function" then fail("SequenceCoordinator motion executor must provide execute", 2) end local coordinator = setmetatable({}, SequenceCoordinator) coordinator_records[coordinator] = { host = host, state = state, transitions = transitions, policy = policy_service, repeat_resolver = resolver, feedback = feedback, target_factory = target_factory, motion_factory = motion_factory, acquisition = acquisition, motion_executor = executor, last_primary_resolution = nil, last_explicit_resolution = nil, } return coordinator end function SequenceCoordinator.is(value) return type(value) == "table" and coordinator_records[value] ~= nil end function SequenceCoordinator:validate_primary_descriptor(value) return M.validate_primary_descriptor(value) end local function require_primary_reader(host) if type(host) ~= "table" or type(host.read_mode) ~= "function" or type(host.read_cursor) ~= "function" or type(host.read_count) ~= "function" or type(host.read_macro_state) ~= "function" then fail("SequenceCoordinator host must provide primary action state", 3) end return host end function SequenceCoordinator:read_primary_invocation() local host = require_primary_reader(coordinator_records[self].host) local context = domain.ModeContext.from_full_mode(host:read_mode()) local position = domain.Position.coerce(host:read_cursor()) local count = domain.Count.new(host:read_count()) local macro_state = domain.MacroState.new(host:read_macro_state()) return { context = context, position = position, origin = position, count = count, macro_state = macro_state, } end function SequenceCoordinator:inspect_fold_open_policy(invocation) if type(invocation) ~= "table" or not domain.Position.is(invocation.position) then fail("fold preflight requires primary invocation state", 2) end local host = coordinator_records[self].host if type(host.read_fold_state) ~= "function" then fail("SequenceCoordinator host must provide fold state", 2) end local fold_state = host:read_fold_state() if not domain.FoldState.is(fold_state) then fail("SequenceCoordinator host must return FoldState", 2) end return fold_state end local function fold_open_enabled(fold_state) return fold_state:opens("horizontal") or fold_state:opens("all") end function SequenceCoordinator:open_enclosing_folds(invocation, fold_state) if type(invocation) ~= "table" or not domain.Position.is(invocation.position) then fail("fold opening requires primary invocation state", 2) end if not domain.FoldState.is(fold_state) then fail("fold opening requires FoldState", 2) end if not fold_open_enabled(fold_state) then return 0 end local host = coordinator_records[self].host if type(host.open_fold) ~= "function" then fail("SequenceCoordinator host must open folds", 2) end local opened = 0 while fold_state.closed_levels > 0 do if host:open_fold(invocation.position) ~= true then break end opened = opened + 1 fold_state = self:inspect_fold_open_policy(invocation) end return opened end function SequenceCoordinator:decide_primary(invocation) if type(invocation) ~= "table" or not domain.ModeContext.is(invocation.context) or not domain.Position.is(invocation.position) or not domain.MacroState.is(invocation.macro_state) then fail("primary decision requires invocation state", 2) end return coordinator_records[self].repeat_resolver:decide( invocation.context, invocation.position, invocation.macro_state ) end function SequenceCoordinator:acquire_primary(descriptor, invocation) descriptor = self:validate_primary_descriptor(descriptor) if type(invocation) ~= "table" then fail("primary acquisition requires invocation state", 2) end return coordinator_records[self].acquisition:acquire( descriptor, invocation.context, invocation.position, invocation.count, invocation.macro_state ) end function SequenceCoordinator:fresh_primary_resolution( initiating_descriptor, acquisition_result, invocation ) initiating_descriptor = self:validate_primary_descriptor(initiating_descriptor) if type(acquisition_result) ~= "table" or acquisition_result.resolved ~= true or not domain.TargetValue.is(acquisition_result.target) or not domain.TargetPlan.is(acquisition_result.target_plan) or not domain.ResolvedMotionPlan.is(acquisition_result.motion_plan) then fail("fresh primary resolution requires acquired motion plans", 2) end return { kind = "fresh", invocation = invocation, acquisition_result = acquisition_result, target = acquisition_result.target, target_plan = acquisition_result.target_plan, motion_plan = acquisition_result.motion_plan, effective_descriptor = initiating_descriptor, first_move = coordinator_records[self].state:get_first_move( invocation.context ) == true, skip_destination = acquisition_result.target_plan.kind == domain.TargetPlanKind.EMPTY, } end function SequenceCoordinator:resolve_acquisition(descriptor, invocation) local result = self:acquire_primary(descriptor, invocation) if type(result.has_outcome) ~= "function" then fail("AcquisitionService must return an AcquisitionResult", 2) end if result:has_outcome() then return result.outcome end return self:fresh_primary_resolution(descriptor, result, invocation) end function SequenceCoordinator:evaluate_repeat_timeout(invocation) if type(invocation) ~= "table" then fail("repeat timeout requires primary invocation state", 2) end local record = coordinator_records[self] if type(record.repeat_resolver.evaluate_timeout) ~= "function" then fail("SequenceCoordinator repeat resolver must evaluate timeout", 2) end local window = record.host:read_window() local decision, elapsed_ms, cleanup = record.repeat_resolver:evaluate_timeout(window) return { decision = decision, elapsed_ms = elapsed_ms, cleanup = cleanup, window = window, } end function SequenceCoordinator:build_live_target_plan(target, invocation) if not domain.TargetValue.is(target) then fail("primary target planning requires a TargetValue", 2) end if type(invocation) ~= "table" or not domain.Position.is(invocation.position) then fail("primary target planning requires invocation state", 2) end local record = coordinator_records[self] local view = text_topology.from_host(record.host) local sampled_search = record.policy:sample_search() local target_plan = record.target_factory:build(target, nil, { text_view = view, origin = invocation.position, search_scope = sampled_search.search_scope, effective_encoding = view.effective_encoding, }) if not domain.TargetPlan.is(target_plan) then fail("TargetPlanFactory must return a TargetPlan", 2) end return target_plan, view, sampled_search.search_scope end function SequenceCoordinator:build_movement_plan( target_plan, effective_descriptor, invocation, search_scope ) if not domain.TargetPlan.is(target_plan) then fail("movement planning requires a TargetPlan", 2) end if type(invocation) ~= "table" or not domain.ModeContext.is(invocation.context) then fail("movement planning requires invocation state", 2) end local record = coordinator_records[self] local selection = invocation.context.visual and record.host:read_selection() or nil local motion_plan = record.motion_factory:build_for_context( target_plan, effective_descriptor, invocation.context, selection, search_scope ) if not domain.ResolvedMotionPlan.is(motion_plan) then fail("MotionPlanFactory must return a ResolvedMotionPlan", 2) end return motion_plan end function SequenceCoordinator:restore_repeated_feedback(resolution) if type(resolution) ~= "table" or not domain.TargetPlan.is(resolution.target_plan) or not domain.ResolvedMotionPlan.is(resolution.motion_plan) then fail("feedback restoration requires a repeated primary resolution", 2) end local feedback = coordinator_records[self].feedback if type(feedback.restore_primary) ~= "function" then fail("FeedbackService must restore primary feedback", 2) end return feedback:restore_primary({ context = resolution.invocation.context, anchor = resolution.invocation.position, target_plan = resolution.target_plan, motion_plan = resolution.motion_plan, stored_descriptor = resolution.stored_descriptor, endpoint_policy = resolution.motion_plan.endpoint_policy, text_view = resolution.text_view, window = resolution.timeout.window, }) end function SequenceCoordinator:stored_primary_resolution( invocation, pressed_descriptor, timeout ) if type(invocation) ~= "table" or not domain.ModeContext.is(invocation.context) then fail("stored primary resolution requires invocation state", 2) end pressed_descriptor = self:validate_primary_descriptor(pressed_descriptor) local state = coordinator_records[self].state local stored_descriptor = state:get_previous_descriptor(invocation.context) local stored_target = state:get_previous_target(invocation.context) if stored_descriptor == nil or stored_target == nil then fail("repeat-eligible primary state must contain descriptor and target", 2) end local resolver = coordinator_records[self].repeat_resolver if type(resolver.resolve_primary_direction) ~= "function" then fail("SequenceCoordinator repeat resolver must resolve primary direction", 2) end local effective_descriptor = domain.Descriptor.from_string( resolver:resolve_primary_direction(stored_descriptor, pressed_descriptor) ) if effective_descriptor.family ~= stored_descriptor.family then fail("primary repetition must preserve the stored motion family", 2) end local target_plan, text_view, search_scope = self:build_live_target_plan( stored_target, invocation ) local motion_plan = self:build_movement_plan( target_plan, effective_descriptor, invocation, search_scope ) local resolution = { kind = "repeat", invocation = invocation, pressed_descriptor = pressed_descriptor, timeout = timeout, stored_descriptor = stored_descriptor, target = stored_target, target_plan = target_plan, motion_plan = motion_plan, text_view = text_view, search_scope = search_scope, effective_descriptor = effective_descriptor, first_move = state:get_first_move(invocation.context) == true, } resolution.restored_feedback = self:restore_repeated_feedback(resolution) return resolution end function SequenceCoordinator:refresh_primary_feedback(resolution) if type(resolution) ~= "table" or not domain.TargetValue.is(resolution.target) then fail("primary feedback refresh requires a resolved target", 2) end local record = coordinator_records[self] if type(record.feedback.refresh_primary) ~= "function" then fail("FeedbackService must refresh primary feedback", 2) end local window = resolution.timeout and resolution.timeout.window or record.host:read_window() return record.feedback:refresh_primary(resolution.target, window) end function SequenceCoordinator:execute_resolved_motion(resolution, execution_options) if type(resolution) ~= "table" or not domain.ModeContext.is(resolution.invocation.context) or not domain.ResolvedMotionPlan.is(resolution.motion_plan) then fail("motion execution requires a resolved motion", 2) end if resolution.skip_destination then return domain.ActionOutcome.empty(resolution.invocation.position) end local record = coordinator_records[self] local view = resolution.text_view or text_topology.from_host(record.host) local outcome = record.motion_executor:execute( view, resolution.invocation.context, resolution.motion_plan, resolution.invocation.count, resolution.first_move, execution_options ) if not domain.ActionOutcome.is(outcome) then fail("MotionExecutor must return an ActionOutcome", 2) end return outcome end function SequenceCoordinator:execute_primary_resolution(resolution) local record = coordinator_records[self] record.last_primary_resolution = resolution local outcome = self:execute_resolved_motion(resolution) resolution.highlight_timer = self:refresh_primary_feedback(resolution) return outcome end function SequenceCoordinator:last_primary_resolution() return coordinator_records[self].last_primary_resolution end function SequenceCoordinator:last_explicit_resolution() return coordinator_records[self].last_explicit_resolution end function SequenceCoordinator:reset() local record = coordinator_records[self] local position = domain.Position.coerce(record.host:read_cursor()) local cleanup = record.transitions:PublicReset(record.host:read_window()) if type(record.feedback.release_transition_cleanup) ~= "function" then fail("FeedbackService must release reset cleanup", 2) end record.feedback:release_transition_cleanup(cleanup) return domain.ActionOutcome.neutral(position) end SequenceCoordinator.Reset = SequenceCoordinator.reset function SequenceCoordinator:read_explicit_invocation() local host = coordinator_records[self].host if type(host) ~= "table" or type(host.read_mode) ~= "function" or type(host.read_cursor) ~= "function" or type(host.read_count) ~= "function" then fail("SequenceCoordinator host must provide explicit action state", 2) end local position = domain.Position.coerce(host:read_cursor()) return { context = domain.ModeContext.from_full_mode(host:read_mode()), position = position, origin = position, count = domain.Count.new(host:read_count()), } end function SequenceCoordinator:resolve_explicit(kind, resolver_method) if type(kind) ~= "string" or kind == "" then fail("explicit repeat kind must be a nonempty string", 2) end if type(resolver_method) ~= "string" or resolver_method == "" then fail("explicit repeat resolver method must be a nonempty string", 2) end local invocation = self:read_explicit_invocation() local resolver = coordinator_records[self].repeat_resolver if type(resolver[resolver_method]) ~= "function" then fail("RepeatResolver must build " .. kind .. " requests", 2) end local request = resolver[resolver_method](resolver, invocation.context) local resolution = { kind = kind, invocation = invocation, request = request, } if request.neutral then return resolution end local target_plan, text_view, search_scope = self:build_live_target_plan( request.target, invocation ) local motion_plan = self:build_movement_plan( target_plan, request.descriptor, invocation, search_scope ) resolution.target = request.target resolution.target_plan = target_plan resolution.motion_plan = motion_plan resolution.text_view = text_view resolution.search_scope = search_scope resolution.effective_descriptor = request.descriptor resolution.first_move = coordinator_records[self].state:get_first_move( invocation.context ) == true resolution.skip_destination = target_plan.kind == domain.TargetPlanKind.EMPTY return resolution end function SequenceCoordinator:resolve_explicit_same() return self:resolve_explicit("explicit_same", "same_direction_request") end function SequenceCoordinator:resolve_explicit_opposite() return self:resolve_explicit( "explicit_opposite", "opposite_direction_request" ) end function SequenceCoordinator:primary(value) local descriptor = self:validate_primary_descriptor(value) local invocation = self:read_primary_invocation() invocation.fold_state = self:inspect_fold_open_policy(invocation) invocation.opened_folds = self:open_enclosing_folds( invocation, invocation.fold_state ) invocation.repeat_decision = self:decide_primary(invocation) if invocation.repeat_decision == repeat_resolver_factory.Decision.ACQUIRE then local acquired = self:resolve_acquisition(descriptor, invocation) if domain.ActionOutcome.is(acquired) then return acquired end return self:execute_primary_resolution(acquired) end local timeout = self:evaluate_repeat_timeout(invocation) if timeout.decision == repeat_resolver_factory.Decision.ACQUIRE then local record = coordinator_records[self] if timeout.cleanup ~= nil then if type(record.feedback.release_transition_cleanup) ~= "function" then fail("FeedbackService must release reset cleanup", 2) end record.feedback:release_transition_cleanup(timeout.cleanup) end local acquired = self:resolve_acquisition(descriptor, invocation) if domain.ActionOutcome.is(acquired) then return acquired end return self:execute_primary_resolution(acquired) end return self:execute_primary_resolution( self:stored_primary_resolution(invocation, descriptor, timeout) ) end function SequenceCoordinator:execute_explicit_resolution(resolution) if type(resolution) ~= "table" or not domain.ExplicitRepeatRequest.is(resolution.request) then fail("explicit execution requires a resolved repeat request", 2) end coordinator_records[self].last_explicit_resolution = resolution if resolution.request.neutral then return domain.ActionOutcome.empty(resolution.invocation.position) end return self:execute_resolved_motion(resolution) end function SequenceCoordinator:repeat_same_direction() return self:execute_explicit_resolution(self:resolve_explicit_same()) end SequenceCoordinator.RepeatSameDirection = SequenceCoordinator.repeat_same_direction function SequenceCoordinator:repeat_opposite_direction() return self:execute_explicit_resolution(self:resolve_explicit_opposite()) end SequenceCoordinator.RepeatOppositeDirection = SequenceCoordinator.repeat_opposite_direction function M.new(options) return SequenceCoordinator.new(options) end setmetatable(M, { __call = function(_, options) return SequenceCoordinator.new(options) end, }) return M