summaryrefslogtreecommitdiff
path: root/lua/clever_tee/sequence_state.lua
diff options
context:
space:
mode:
Diffstat (limited to 'lua/clever_tee/sequence_state.lua')
-rw-r--r--lua/clever_tee/sequence_state.lua302
1 files changed, 302 insertions, 0 deletions
diff --git a/lua/clever_tee/sequence_state.lua b/lua/clever_tee/sequence_state.lua
new file mode 100644
index 0000000..5c66a42
--- /dev/null
+++ b/lua/clever_tee/sequence_state.lua
@@ -0,0 +1,302 @@
+local domain = require("clever_tee.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_tee.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