diff options
Diffstat (limited to 'lua/clever_tee/state_transitions.lua')
| -rw-r--r-- | lua/clever_tee/state_transitions.lua | 493 |
1 files changed, 493 insertions, 0 deletions
diff --git a/lua/clever_tee/state_transitions.lua b/lua/clever_tee/state_transitions.lua new file mode 100644 index 0000000..377f04f --- /dev/null +++ b/lua/clever_tee/state_transitions.lua @@ -0,0 +1,493 @@ +local domain = require("clever_tee.domain") +local sequence_state = require("clever_tee.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 = "CleverTeeChar", + 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 "CleverTeeCursor" + if group ~= "CleverTeeCursor" and group ~= "CleverTeeDirect" then + fail("temporary overlay group must be CleverTeeCursor or CleverTeeDirect", 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 |
