local domain = require("clever_f.domain") local sequence_state = require("clever_f.sequence_state") local M = {} local StateTransitions = {} StateTransitions.__index = StateTransitions M.StateTransitions = StateTransitions local function fail(message, level) error(message, (level or 1) + 1) end local function is_integer(value) return type(value) == "number" and value > -math.huge and value < math.huge and value == math.floor(value) end local function require_time(value, name) if type(value) ~= "number" or value ~= value or value <= -math.huge or value >= math.huge then fail((name or "time") .. " must be a finite number", 2) end return value end local function require_identity(identity, name) if identity == nil then fail((name or "resource identity") .. " must be active", 2) end return identity end local function require_location(location, name) if location == nil then fail((name or "resource location") .. " must identify its host location", 2) end return location end local function require_target(target) if not domain.TargetValue.is(target) then fail("acquired target must be a TargetValue", 2) end return target end local function require_position(position) return domain.Position.coerce(position) end local function moved_forward_value(direction) if type(direction) == "boolean" then return direction end if domain.Direction.is(direction) then return direction == domain.Direction.FORWARD end if domain.Descriptor.is(direction) then return direction.direction == domain.Direction.FORWARD end if direction == "forward" or direction == "backward" then return direction == "forward" end fail("movement direction must be a Boolean, Direction, or Descriptor", 2) end local function copy_resource(resource) local result = {} for key, value in pairs(resource) do result[key] = value end return result end local function resource_matches(resource, identity, location_field, location) return resource.identity == identity and (location == nil or resource[location_field] == location) end local function add_unique_resource(resources, resource, location_field, field_name) for _, active in ipairs(resources) do if resource_matches( active, resource.identity, location_field, resource[location_field] ) then fail(field_name .. " resource is already active at this host location", 3) end end resources[#resources + 1] = resource return copy_resource(resource) end local function remove_resources(data, field, predicate) local removed = {} local retained = {} for _, resource in ipairs(data[field]) do if predicate(resource) then removed[#removed + 1] = copy_resource(resource) else retained[#retained + 1] = resource end end data[field] = retained return removed end local function clear_target_overlays(data, window) return remove_resources(data, "target_overlays", function(resource) return window == nil or resource.window == window end) end local function clear_temporary_overlays(data, window) return remove_resources(data, "temporary_overlays", function(resource) return window == nil or resource.window == window end) end local function clear_finalizers(data, buffer) return remove_resources(data, "finalizers", function(resource) return buffer == nil or resource.buffer == buffer end) end local function clear_highlight_timer(data) local identity = data.highlight_timer data.highlight_timer = nil return identity end local function clear_all_landings_and_direction(data) data.previous_landing = {} data.moved_forward = false end local function public_reset(data, current_window) local cleanup = { highlight_timer = clear_highlight_timer(data), target_overlays = clear_target_overlays(data, current_window), finalizers = {}, temporary_overlays = {}, } data.previous_descriptor = {} data.previous_landing = {} data.first_move = {} data.migemo_cache = {} data.repeat_timestamp_ms = 0 return cleanup end function StateTransitions.new(state) state = state or sequence_state.get() if not sequence_state.is(state) then fail("StateTransitions requires the plugin-global SequenceState", 2) end return setmetatable({ _state = state }, StateTransitions) end function M.new(state) return StateTransitions.new(state) end setmetatable(M, { __call = function(_, state) return StateTransitions.new(state) end, }) function StateTransitions:state() return self._state end function StateTransitions:_mutate(mutation) return sequence_state._mutate(self._state, mutation) end function StateTransitions:BeginAcquisition(context, descriptor) context = sequence_state._normalize_context(context) descriptor = domain.Descriptor.from_string(descriptor) return self:_mutate(function(state) state.known_contexts[context] = true state.previous_descriptor[context] = descriptor state.first_move[context] = true return self._state:get_context(context) end) end function StateTransitions:CommitAcquiredTarget(context, target, time_ms) context = sequence_state._normalize_context(context) target = require_target(target) if time_ms ~= nil then time_ms = require_time(time_ms, "acquisition time") end return self:_mutate(function(state) state.known_contexts[context] = true state.previous_target[context] = target state.last_input_context = context if time_ms ~= nil then state.repeat_timestamp_ms = time_ms end return self._state:get_context(context) end) end function StateTransitions:CommitCommandSuccess(context, destination, direction) context = sequence_state._normalize_context(context) destination = require_position(destination) local forward = moved_forward_value(direction) return self:_mutate(function(state) state.known_contexts[context] = true state.moved_forward = forward state.moved_forward_initialized = true state.previous_landing[context] = destination state.first_move[context] = false return self._state:get_context(context) end) end function StateTransitions:CommitVisualSuccess(context, destination) context = sequence_state._normalize_context(context) destination = require_position(destination) return self:_mutate(function(state) state.known_contexts[context] = true state.previous_landing[context] = destination state.first_move[context] = false return self._state:get_context(context) end) end function StateTransitions:ClearAllLandingsAndDirection() return self:_mutate(function(state) clear_all_landings_and_direction(state) end) end function StateTransitions:SetRepeatTimestamp(time_ms) time_ms = require_time(time_ms, "repeat timestamp") return self:_mutate(function(state) local previous = state.repeat_timestamp_ms state.repeat_timestamp_ms = time_ms return previous end) end function StateTransitions:CacheMigemo(encoding, dictionary) if type(encoding) ~= "string" or encoding == "" then fail("Migemo cache encoding must be a nonempty string", 2) end if dictionary == nil then fail("Migemo cache dictionary must be present", 2) end return self:_mutate(function(state) local previous = state.migemo_cache[encoding] state.migemo_cache[encoding] = dictionary return previous end) end function StateTransitions:RemoveMigemo(encoding) if type(encoding) ~= "string" or encoding == "" then fail("Migemo cache encoding must be a nonempty string", 2) end return self:_mutate(function(state) local previous = state.migemo_cache[encoding] state.migemo_cache[encoding] = nil return previous end) end function StateTransitions:ClearMigemoCache() return self:_mutate(function(state) local previous = state.migemo_cache state.migemo_cache = {} return previous end) end function StateTransitions:SetHighlightTimer(identity) return self:_mutate(function(state) local previous = state.highlight_timer state.highlight_timer = identity return previous end) end function StateTransitions:ClearHighlightTimer(expected_identity) return self:_mutate(function(state) local current = state.highlight_timer if current == nil then return nil, false end if expected_identity ~= nil and current ~= expected_identity then return nil, false end state.highlight_timer = nil return current, true end) end function StateTransitions:AddTargetOverlay(identity, window, anchor_line) if type(identity) == "table" and window == nil and identity.identity ~= nil then local resource = identity identity = resource.identity window = resource.window anchor_line = resource.anchor_line end require_identity(identity, "target overlay identity") require_location(window, "target overlay window") if anchor_line ~= nil and (not is_integer(anchor_line) or anchor_line < 1) then fail("target overlay anchor_line must be a positive integer", 2) end local resource = { identity = identity, window = window, group = "CleverFChar", anchor_line = anchor_line, } return self:_mutate(function(state) return add_unique_resource( state.target_overlays, resource, "window", "target overlay" ) end) end function StateTransitions:RemoveTargetOverlay(identity, window) require_identity(identity, "target overlay identity") return self:_mutate(function(state) return remove_resources(state, "target_overlays", function(resource) return resource_matches(resource, identity, "window", window) end) end) end function StateTransitions:ClearTargetOverlays(window) return self:_mutate(function(state) return clear_target_overlays(state, window) end) end function StateTransitions:AddTemporaryOverlay(identity, window, group) if type(identity) == "table" and window == nil and identity.identity ~= nil then local resource = identity identity = resource.identity window = resource.window group = resource.group end require_identity(identity, "temporary overlay identity") require_location(window, "temporary overlay window") group = group or "CleverFCursor" if group ~= "CleverFCursor" and group ~= "CleverFDirect" then fail("temporary overlay group must be CleverFCursor or CleverFDirect", 2) end local resource = { identity = identity, window = window, group = group, } return self:_mutate(function(state) return add_unique_resource( state.temporary_overlays, resource, "window", "temporary overlay" ) end) end function StateTransitions:RemoveTemporaryOverlay(identity, window) require_identity(identity, "temporary overlay identity") return self:_mutate(function(state) return remove_resources(state, "temporary_overlays", function(resource) return resource_matches(resource, identity, "window", window) end) end) end function StateTransitions:ClearTemporaryOverlays(window) return self:_mutate(function(state) return clear_temporary_overlays(state, window) end) end function StateTransitions:AddFinalizer(identity, buffer) if type(identity) == "table" and buffer == nil and identity.identity ~= nil then local resource = identity identity = resource.identity buffer = resource.buffer end require_identity(identity, "finalizer identity") require_location(buffer, "finalizer buffer") local resource = { identity = identity, buffer = buffer, } return self:_mutate(function(state) return add_unique_resource(state.finalizers, resource, "buffer", "finalizer") end) end function StateTransitions:RemoveFinalizer(identity, buffer) require_identity(identity, "finalizer identity") return self:_mutate(function(state) return remove_resources(state, "finalizers", function(resource) return resource_matches(resource, identity, "buffer", buffer) end) end) end function StateTransitions:ClearFinalizers(buffer) return self:_mutate(function(state) return clear_finalizers(state, buffer) end) end function StateTransitions:ClearTargetFeedback(current_window) return self:_mutate(function(state) return { highlight_timer = clear_highlight_timer(state), target_overlays = clear_target_overlays(state, current_window), finalizers = {}, temporary_overlays = {}, } end) end function StateTransitions:FullFinalization(current_window) return self:_mutate(function(state) local cleanup = { highlight_timer = clear_highlight_timer(state), target_overlays = clear_target_overlays(state, current_window), finalizers = clear_finalizers(state), temporary_overlays = {}, } clear_all_landings_and_direction(state) return cleanup end) end function StateTransitions:PublicReset(current_window) return self:_mutate(function(state) return public_reset(state, current_window) end) end function StateTransitions:DiagnosticFullReset(current_window) return self:_mutate(function(state) local cleanup = public_reset(state, current_window) cleanup.finalizers = clear_finalizers(state) state.previous_target = {} state.last_input_context = nil state.moved_forward = false state.moved_forward_initialized = false return cleanup end) end StateTransitions.begin_acquisition = StateTransitions.BeginAcquisition StateTransitions.commit_acquired_target = StateTransitions.CommitAcquiredTarget StateTransitions.commit_command_success = StateTransitions.CommitCommandSuccess StateTransitions.commit_visual_success = StateTransitions.CommitVisualSuccess StateTransitions.clear_all_landings_and_direction = StateTransitions.ClearAllLandingsAndDirection StateTransitions.set_repeat_timestamp = StateTransitions.SetRepeatTimestamp StateTransitions.cache_migemo = StateTransitions.CacheMigemo StateTransitions.remove_migemo = StateTransitions.RemoveMigemo StateTransitions.clear_migemo_cache = StateTransitions.ClearMigemoCache StateTransitions.set_highlight_timer = StateTransitions.SetHighlightTimer StateTransitions.clear_highlight_timer = StateTransitions.ClearHighlightTimer StateTransitions.add_target_overlay = StateTransitions.AddTargetOverlay StateTransitions.remove_target_overlay = StateTransitions.RemoveTargetOverlay StateTransitions.clear_target_overlays = StateTransitions.ClearTargetOverlays StateTransitions.add_temporary_overlay = StateTransitions.AddTemporaryOverlay StateTransitions.remove_temporary_overlay = StateTransitions.RemoveTemporaryOverlay StateTransitions.clear_temporary_overlays = StateTransitions.ClearTemporaryOverlays StateTransitions.add_finalizer = StateTransitions.AddFinalizer StateTransitions.remove_finalizer = StateTransitions.RemoveFinalizer StateTransitions.clear_finalizers = StateTransitions.ClearFinalizers StateTransitions.clear_target_feedback = StateTransitions.ClearTargetFeedback StateTransitions.full_finalization = StateTransitions.FullFinalization StateTransitions.public_reset = StateTransitions.PublicReset StateTransitions.diagnostic_full_reset = StateTransitions.DiagnosticFullReset return M