diff options
Diffstat (limited to 'lua')
| -rw-r--r-- | lua/clever_f/sequence_state.lua | 302 | ||||
| -rw-r--r-- | lua/clever_f/state_transitions.lua | 493 |
2 files changed, 795 insertions, 0 deletions
diff --git a/lua/clever_f/sequence_state.lua b/lua/clever_f/sequence_state.lua new file mode 100644 index 0000000..e31752d --- /dev/null +++ b/lua/clever_f/sequence_state.lua @@ -0,0 +1,302 @@ +local domain = require("clever_f.domain") + +local M = {} +local State = {} +M.State = State + +local MAP_FIELDS = { + "previous_descriptor", + "previous_landing", + "first_move", + "previous_target", +} + +local MAP_FIELD_SET = {} +for _, field in ipairs(MAP_FIELDS) do + MAP_FIELD_SET[field] = true +end + +local data = { + previous_descriptor = {}, + previous_landing = {}, + first_move = {}, + previous_target = {}, + known_contexts = {}, + last_input_context = nil, + moved_forward = false, + moved_forward_initialized = false, + migemo_cache = {}, + repeat_timestamp_ms = 0, + highlight_timer = nil, + target_overlays = {}, + temporary_overlays = {}, + finalizers = {}, +} + +local function fail(message, level) + error(message, (level or 1) + 1) +end + +local function normalize_context(context, name) + if domain.ModeContext.is(context) then + return domain.ModeContext.from_full_mode(context.full_mode) + end + if type(context) ~= "string" or context == "" then + fail((name or "context") .. " must be a ModeContext or full mode string", 2) + end + return domain.ModeContext.from_full_mode(context) +end + +local function copy_map(source) + local result = {} + for key, value in pairs(source) do + result[key] = value + end + return result +end + +local function copy_resource(resource) + local result = {} + for key, value in pairs(resource) do + result[key] = value + end + return result +end + +local function copy_resources(resources) + local result = {} + for index, resource in ipairs(resources) do + result[index] = copy_resource(resource) + end + return result +end + +local function sorted_contexts() + local result = {} + for context in pairs(data.known_contexts) do + result[#result + 1] = context + end + table.sort(result, function(left, right) + return left.key < right.key + end) + return result +end + +local function context_record(context) + return { + context = context, + previous_descriptor = data.previous_descriptor[context], + previous_landing = data.previous_landing[context], + first_move = data.first_move[context], + previous_target = data.previous_target[context], + } +end + +local function context_records() + local result = {} + for _, context in ipairs(sorted_contexts()) do + result[context.key] = context_record(context) + end + return result +end + +function State:get_previous_descriptor(context) + context = normalize_context(context) + return data.previous_descriptor[context] +end + +function State:get_previous_landing(context) + context = normalize_context(context) + return data.previous_landing[context] +end + +function State:get_first_move(context) + context = normalize_context(context) + return data.first_move[context] +end + +function State:get_previous_target(context) + context = normalize_context(context) + return data.previous_target[context] +end + +function State:get_context(context) + context = normalize_context(context) + return context_record(context) +end + +State.context = State.get_context + +function State:has_previous_landing(context) + return self:get_previous_landing(context) ~= nil +end + +function State:get_migemo(encoding) + if type(encoding) ~= "string" or encoding == "" then + fail("encoding must be a nonempty string", 2) + end + return data.migemo_cache[encoding] +end + +function State:target_overlay_identities() + local result = {} + for index, resource in ipairs(data.target_overlays) do + result[index] = resource.identity + end + return result +end + +function State:temporary_overlay_identities() + local result = {} + for index, resource in ipairs(data.temporary_overlays) do + result[index] = resource.identity + end + return result +end + +function State:finalizer_identities() + local result = {} + for index, resource in ipairs(data.finalizers) do + result[index] = resource.identity + end + return result +end + +function State:resources() + return { + highlight_timer = data.highlight_timer, + target_overlays = copy_resources(data.target_overlays), + temporary_overlays = copy_resources(data.temporary_overlays), + finalizers = copy_resources(data.finalizers), + } +end + +function State:snapshot() + local snapshot = { + contexts = context_records(), + last_input_context = data.last_input_context, + moved_forward = data.moved_forward, + moved_forward_initialized = data.moved_forward_initialized, + migemo_cache = copy_map(data.migemo_cache), + repeat_timestamp_ms = data.repeat_timestamp_ms, + highlight_timer = data.highlight_timer, + target_overlays = copy_resources(data.target_overlays), + temporary_overlays = copy_resources(data.temporary_overlays), + finalizers = copy_resources(data.finalizers), + } + for _, field in ipairs(MAP_FIELDS) do + snapshot[field] = copy_map(data[field]) + end + return snapshot +end + +local function target_to_table(target) + return target and target:to_table() or nil +end + +function State:to_table() + local contexts = {} + for _, context in ipairs(sorted_contexts()) do + local record = context_record(context) + contexts[context.key] = { + previous_descriptor = record.previous_descriptor + and record.previous_descriptor.value + or nil, + previous_landing = record.previous_landing + and record.previous_landing:to_table() + or nil, + first_move = record.first_move, + previous_target = target_to_table(record.previous_target), + } + end + + local cache_keys = {} + for encoding in pairs(data.migemo_cache) do + cache_keys[#cache_keys + 1] = encoding + end + table.sort(cache_keys) + + return { + contexts = contexts, + last_input_context = data.last_input_context and data.last_input_context.key or nil, + moved_forward = data.moved_forward, + moved_forward_initialized = data.moved_forward_initialized, + migemo_cache = cache_keys, + repeat_timestamp_ms = data.repeat_timestamp_ms, + highlight_timer = data.highlight_timer, + target_overlays = self:target_overlay_identities(), + temporary_overlays = self:temporary_overlay_identities(), + finalizers = self:finalizer_identities(), + } +end + +local state +local state_metatable = { + __index = function(_, key) + local method = State[key] + if method ~= nil then + return method + end + if MAP_FIELD_SET[key] then + return copy_map(data[key]) + end + if key == "contexts" then + return context_records() + end + if key == "last_input_context" + or key == "moved_forward" + or key == "moved_forward_initialized" + or key == "highlight_timer" + then + return data[key] + end + if key == "repeat_timestamp" or key == "repeat_timestamp_ms" then + return data.repeat_timestamp_ms + end + if key == "migemo_cache" then + return copy_map(data.migemo_cache) + end + if key == "target_overlays" + or key == "temporary_overlays" + or key == "finalizers" + then + return copy_resources(data[key]) + end + return nil + end, + __newindex = function() + fail("SequenceState is read-only; use StateTransitions", 2) + end, + __metatable = "clever_f.sequence_state.State", +} +state = setmetatable({}, state_metatable) + +function M.get() + return state +end + +function M.new() + return state +end + +function M.is(value) + return value == state +end + +M.global = state + +function M._mutate(target, mutation) + if target ~= state then + fail("StateTransitions must use the plugin-global SequenceState", 2) + end + if type(mutation) ~= "function" then + fail("state mutation must be a function", 2) + end + return mutation(data) +end + +function M._normalize_context(context) + return normalize_context(context) +end + +return M diff --git a/lua/clever_f/state_transitions.lua b/lua/clever_f/state_transitions.lua new file mode 100644 index 0000000..426bddb --- /dev/null +++ b/lua/clever_f/state_transitions.lua @@ -0,0 +1,493 @@ +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 |
