summaryrefslogtreecommitdiff
path: root/lua/clever_f
diff options
context:
space:
mode:
Diffstat (limited to 'lua/clever_f')
-rw-r--r--lua/clever_f/sequence_state.lua302
-rw-r--r--lua/clever_f/state_transitions.lua493
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