diff options
Diffstat (limited to 'lua/clever_tee/sequence_coordinator.lua')
| -rw-r--r-- | lua/clever_tee/sequence_coordinator.lua | 675 |
1 files changed, 675 insertions, 0 deletions
diff --git a/lua/clever_tee/sequence_coordinator.lua b/lua/clever_tee/sequence_coordinator.lua new file mode 100644 index 0000000..1437d6c --- /dev/null +++ b/lua/clever_tee/sequence_coordinator.lua @@ -0,0 +1,675 @@ +local acquisition_service_factory = require("clever_tee.acquisition_service") +local case_policy = require("clever_tee.case_policy") +local direct_preview_planner = require("clever_tee.direct_preview_planner") +local domain = require("clever_tee.domain") +local feedback_service_factory = require("clever_tee.feedback_service") +local motion_executor_factory = require("clever_tee.motion_executor") +local motion_plan_factory = require("clever_tee.motion_plan") +local policy = require("clever_tee.policy") +local repeat_resolver_factory = require("clever_tee.repeat_resolver") +local sequence_state = require("clever_tee.sequence_state") +local state_transitions = require("clever_tee.state_transitions") +local target_plan_factory = require("clever_tee.target_plan") +local text_topology = require("clever_tee.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 + error("clever-tee: Invalid mapping '" .. descriptor_text(value) .. "'", 0) + 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 lowercase = options.lowercase + if lowercase == nil and type(host.lowercase) == "function" then + lowercase = function(value) + return host:lowercase(value) + end + end + local case_resolver = options.case_resolver + or case_policy.new({ lowercase = lowercase }) + local target_factory = options.target_factory + or options.target_plan_factory + or target_plan_factory.new({ + policy = policy_service, + case_resolver = case_resolver, + }) + 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 direct_planner = options.direct_planner + or options.direct_preview_planner + or direct_preview_planner.new({ case_resolver = case_resolver }) + local acquisition = options.acquisition + or options.acquisition_service + or acquisition_service_factory.new({ + host = host, + state = state, + transitions = transitions, + policy = policy_service, + feedback = feedback, + direct_planner = direct_planner, + 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:diagnostic_full_reset() + local record = coordinator_records[self] + local position = domain.Position.coerce(record.host:read_cursor()) + local cleanup = record.transitions:DiagnosticFullReset( + record.host:read_window() + ) + if type(record.feedback.release_transition_cleanup) ~= "function" then + fail("FeedbackService must release diagnostic cleanup", 2) + end + record.feedback:release_transition_cleanup(cleanup) + return domain.ActionOutcome.neutral(position) +end + +SequenceCoordinator.DiagnosticFullReset = + SequenceCoordinator.diagnostic_full_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 |
