diff options
Diffstat (limited to 'lua/clever_tee/testing/memory_host.lua')
| -rw-r--r-- | lua/clever_tee/testing/memory_host.lua | 1075 |
1 files changed, 1075 insertions, 0 deletions
diff --git a/lua/clever_tee/testing/memory_host.lua b/lua/clever_tee/testing/memory_host.lua new file mode 100644 index 0000000..4479da7 --- /dev/null +++ b/lua/clever_tee/testing/memory_host.lua @@ -0,0 +1,1075 @@ +local capabilities = require("clever_tee.capabilities") +local domain = require("clever_tee.domain") +local text_topology = require("clever_tee.text_topology") + +local M = {} +local MemoryHost = {} +MemoryHost.__index = MemoryHost +M.MemoryHost = MemoryHost +local unpack_values = table.unpack or unpack + +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 copy(value, seen) + if type(value) ~= "table" or domain.type_of(value) ~= nil then + return value + end + seen = seen or {} + if seen[value] ~= nil then + return seen[value] + end + local result = {} + seen[value] = result + for key, item in pairs(value) do + result[copy(key, seen)] = copy(item, seen) + end + return result +end + +local function list_copy(values) + local result = {} + for index = 1, #values do + result[index] = values[index] + end + return result +end + +local function text_snapshot(value) + if domain.TextSnapshot.is(value) then + return value + end + if type(value) == "table" and value.lines ~= nil then + value = value.lines + end + return domain.TextSnapshot.new(value) +end + +local function selection_value(value) + if value == nil then + return domain.Selection.inactive() + end + return domain.Selection.new(value) +end + +local function macro_state(value) + if type(value) == "table" and not domain.MacroState.is(value) then + value = value.register + end + return domain.MacroState.new(value) +end + +local function fold_state(options) + if domain.FoldState.is(options.fold_state) then + return options.fold_state + end + return domain.FoldState.new( + options.fold_open_policy or {}, + options.closed_fold_levels or 0 + ) +end + +local function input_packet(value) + return domain.InputPacket.from_table(value) +end + +local function normalize_event_names(event_names) + if type(event_names) == "string" then + event_names = { event_names } + end + if type(event_names) ~= "table" or #event_names < 1 then + error("event names must be a nonempty list", 3) + end + local result = {} + local seen = {} + for index = 1, #event_names do + local name = event_names[index] + if type(name) ~= "string" or name == "" then + error("event name must be a nonempty string", 3) + end + if not seen[name] then + seen[name] = true + result[#result + 1] = name + end + end + return result, seen +end + +local function normalize_modes(modes) + if type(modes) == "string" then + modes = { modes } + end + if type(modes) ~= "table" or #modes < 1 then + error("mapping modes must be a nonempty list", 3) + end + local result = {} + for index = 1, #modes do + if type(modes[index]) ~= "string" or modes[index] == "" then + error("mapping mode must be a nonempty string", 3) + end + result[index] = modes[index] + end + return result +end + +function MemoryHost.new(options) + options = options or {} + if type(options) ~= "table" then + error("memory host options must be a table", 2) + end + + local cursor_presentation_support = options.cursor_presentation_support + if cursor_presentation_support == nil then + cursor_presentation_support = options.cmdline_cursor_support + end + if cursor_presentation_support == nil then + cursor_presentation_support = true + end + + local raw_mode = options.mode or "n" + if domain.ModeContext.is(raw_mode) then + raw_mode = raw_mode.full_mode + end + domain.ModeContext.from_full_mode(raw_mode) + + local self = setmetatable({ + _text = text_snapshot(options.text or options.buffer_lines or { "" }), + _buffer = options.buffer or "buffer-1", + _cursor = domain.Position.coerce(options.cursor or { line = 1, byte_column = 1 }), + _window = options.window or "window-1", + _mode = raw_mode, + _selection = selection_value(options.selection), + _count = domain.Count.new(options.count), + _configuration = copy(options.configuration or {}), + _encoding = options.encoding or options.effective_encoding or "utf-8", + _lowercase = options.lowercase or vim.fn.tolower, + _macro_state = macro_state(options.macro_state or options.macro_register), + _fold_state = fold_state(options), + _pending_operator = options.pending_operator, + _time_values = list_copy(options.time_values_ms or {}), + _time_index = 1, + _current_time = options.time_ms or 0, + _input_packets = {}, + _input_index = 1, + _timer_support = options.timer_support ~= false, + _cursor_presentation_support = cursor_presentation_support, + _cursor_presentation = copy(options.cursor_presentation or { + hidden = false, + }), + _emit_movement_events = options.emit_movement_events ~= false, + _operator_inclusive = false, + _operations = {}, + _prompts = {}, + _redraws = {}, + _diagnostics = {}, + _highlight_groups = copy(options.highlight_groups or {}), + _highlights = {}, + _timers = {}, + _event_registrations = {}, + _event_registration_order = {}, + _actions = {}, + _mappings = {}, + _cursor_leases = {}, + _dot_repeat = nil, + _identity_counters = {}, + }, MemoryHost) + + for index, packet in ipairs(options.input_packets or {}) do + self._input_packets[index] = input_packet(packet) + end + + self._event_queue = capabilities.EventQueue.new(function(name, payload) + self:_deliver_event_now(name, payload) + end) + + return capabilities.assert_implements(self) +end + +function M.new(options) + return MemoryHost.new(options) +end + +setmetatable(M, { + __call = function(_, options) + return MemoryHost.new(options) + end, +}) + +function MemoryHost:_next_identity(prefix) + local next_value = (self._identity_counters[prefix] or 0) + 1 + self._identity_counters[prefix] = next_value + return prefix .. "-" .. tostring(next_value) +end + +function MemoryHost:_record(operation, details) + local entry = { operation = operation } + for key, value in pairs(details or {}) do + entry[key] = copy(value) + end + self._operations[#self._operations + 1] = entry +end + +function MemoryHost:operations() + return copy(self._operations) +end + +function MemoryHost:clear_operations() + self._operations = {} +end + +function MemoryHost:read_text() + self:_record("read_text") + return self._text +end + +function MemoryHost:read_cursor() + self:_record("read_cursor") + return self._cursor +end + +function MemoryHost:read_buffer() + self:_record("read_buffer", { buffer = self._buffer }) + return self._buffer +end + +function MemoryHost:read_window() + self:_record("read_window", { window = self._window }) + return self._window +end + +function MemoryHost:read_mode() + self:_record("read_mode", { mode = self._mode }) + return self._mode +end + +function MemoryHost:read_mode_context() + return domain.ModeContext.from_full_mode(self:read_mode()) +end + +function MemoryHost:read_pending_operator() + self:_record("read_pending_operator", { operator = self._pending_operator }) + return self._pending_operator +end + +function MemoryHost:read_selection() + self:_record("read_selection") + return self._selection +end + +function MemoryHost:read_count() + self:_record("read_count", { count = self._count.value }) + return self._count +end + +function MemoryHost:configuration_present(name) + if type(name) ~= "string" or name == "" then + error("configuration name must be a nonempty string", 2) + end + local present = self._configuration[name] ~= nil + self:_record("configuration_present", { name = name, present = present }) + return present +end + +function MemoryHost:read_configuration(name) + if type(name) ~= "string" or name == "" then + error("configuration name must be a nonempty string", 2) + end + local value = copy(self._configuration[name]) + self:_record("read_configuration", { name = name, value = value }) + return value +end + +function MemoryHost:write_configuration(name, value) + if type(name) ~= "string" or name == "" then + error("configuration name must be a nonempty string", 2) + end + self._configuration[name] = copy(value) + self:_record("write_configuration", { name = name, value = value }) +end + +function MemoryHost:read_encoding() + self:_record("read_encoding", { encoding = self._encoding }) + return self._encoding +end + +function MemoryHost:lowercase(value) + if type(value) ~= "string" then + error("case conversion value must be a string", 2) + end + local result = self._lowercase(value) + if type(result) ~= "string" then + error("case converter must return a string", 2) + end + self:_record("lowercase", { value = value, result = result }) + return result +end + +function MemoryHost:read_macro_state() + self:_record("read_macro_state", { executing = self._macro_state.executing }) + return self._macro_state +end + +function MemoryHost:read_fold_state() + self:_record("read_fold_state", { closed_levels = self._fold_state.closed_levels }) + return self._fold_state +end + +function MemoryHost:read_time_ms() + local value = self._time_values[self._time_index] + if value ~= nil then + self._time_index = self._time_index + 1 + self._current_time = value + else + value = self._current_time + end + if type(value) ~= "number" then + error("time value must be a number", 2) + end + self:_record("read_time_ms", { value = value }) + return value +end + +function MemoryHost:set_text(value) + self._text = text_snapshot(value) +end + +function MemoryHost:set_cursor(position) + self._cursor = domain.Position.coerce(position) +end + +function MemoryHost:set_buffer(buffer) + if buffer == nil then + error("buffer identity must be present", 2) + end + self._buffer = buffer +end + +function MemoryHost:set_window(window) + if window == nil then + error("window identity must be present", 2) + end + self._window = window +end + +function MemoryHost:set_mode(full_mode) + if domain.ModeContext.is(full_mode) then + full_mode = full_mode.full_mode + end + domain.ModeContext.from_full_mode(full_mode) + self._mode = full_mode +end + +function MemoryHost:set_selection(selection) + self._selection = selection_value(selection) +end + +function MemoryHost:set_count(count) + self._count = domain.Count.new(count) +end + +function MemoryHost:set_configuration(name, value) + if type(name) ~= "string" or name == "" then + error("configuration name must be a nonempty string", 2) + end + self._configuration[name] = copy(value) +end + +function MemoryHost:unset_configuration(name) + self._configuration[name] = nil +end + +function MemoryHost:set_encoding(encoding) + if type(encoding) ~= "string" or encoding == "" then + error("encoding must be a nonempty string", 2) + end + self._encoding = encoding +end + +function MemoryHost:set_macro_state(state) + self._macro_state = macro_state(state) +end + +function MemoryHost:set_fold_state(state, closed_levels) + if domain.FoldState.is(state) then + self._fold_state = state + else + self._fold_state = domain.FoldState.new(state, closed_levels) + end +end + +function MemoryHost:set_pending_operator(operator) + self._pending_operator = operator +end + +function MemoryHost:push_time_ms(value) + if type(value) ~= "number" then + error("time value must be a number", 2) + end + self._time_values[#self._time_values + 1] = value +end + +function MemoryHost:push_input(packet) + self._input_packets[#self._input_packets + 1] = input_packet(packet) +end + +function MemoryHost:_emit_movement_event(previous) + if self._emit_movement_events and not domain.Position.equal(previous, self._cursor) then + self:deliver_event("CursorMoved", { + cursor = self._cursor, + }) + end +end + +local function motion_descriptor(motion) + if domain.Descriptor.is(motion) then + return motion + end + if type(motion) == "table" and motion.descriptor ~= nil then + return domain.Descriptor.from_string(motion.descriptor) + end + return nil +end + +local function character_boundary(view, position) + if view:line_is_empty(position.line) then + return 1 + end + return view:character_index_for_position(position) +end + +local function character_lines(snapshot) + local result = {} + for line_number, line in ipairs(snapshot:lines()) do + result[line_number] = text_topology.split_editor_characters(line) + end + return result +end + +local function joined_range(characters, first, last) + local result = {} + for index = first, last do + result[#result + 1] = characters[index] + end + return table.concat(result) +end + +local function delete_character_range( + snapshot, + start_line, + start_index, + finish_line, + finish_index +) + local source = character_lines(snapshot) + local lines = snapshot:lines() + local result = {} + + for line_number = 1, start_line - 1 do + result[#result + 1] = lines[line_number] + end + + local prefix = joined_range(source[start_line], 1, start_index - 1) + if start_line == finish_line then + result[#result + 1] = prefix + .. joined_range( + source[start_line], + finish_index, + #source[start_line] + ) + else + result[#result + 1] = prefix + .. joined_range( + source[finish_line], + finish_index, + #source[finish_line] + ) + end + + for line_number = finish_line + 1, #lines do + result[#result + 1] = lines[line_number] + end + return domain.TextSnapshot.new(result) +end + +local function normalized_cursor(snapshot, encoding, position) + local line_number = math.min(position.line, snapshot.line_count) + local view = text_topology.new(snapshot, encoding) + return view:normalize_endpoint(line_number, position.byte_column) +end + +function MemoryHost:_apply_pending_delete(origin, destination, descriptor) + if self._pending_operator ~= "delete" and self._pending_operator ~= "d" then + return false + end + + local view = text_topology.new(self._text, self._encoding) + local origin_index = character_boundary(view, origin) + local destination_index = character_boundary(view, destination) + local start_line + local start_index + local finish_line + local finish_index + local final_cursor + + if descriptor.direction == domain.Direction.FORWARD then + start_line = origin.line + start_index = origin_index + finish_line = destination.line + finish_index = destination_index + (self._operator_inclusive and 1 or 0) + final_cursor = origin + elseif descriptor.family == domain.Family.FIND then + start_line = destination.line + start_index = destination_index + 1 + finish_line = origin.line + finish_index = origin_index + 1 + final_cursor = destination + else + start_line = destination.line + start_index = destination_index + finish_line = origin.line + finish_index = origin_index + final_cursor = view:predecessor(destination) or destination + end + + self._text = delete_character_range( + self._text, + start_line, + start_index, + finish_line, + finish_index + ) + self._cursor = normalized_cursor(self._text, self._encoding, final_cursor) + self:_record("apply_operator", { + operator = self._pending_operator, + descriptor = descriptor.value, + origin = origin, + endpoint = destination, + position = self._cursor, + }) + return true +end + +function MemoryHost:apply_cursor(position, motion) + position = domain.Position.coerce(position) + local previous = self._cursor + local descriptor = motion_descriptor(motion) + self._cursor = position + self:_record("apply_cursor", { + position = position, + descriptor = descriptor and descriptor.value or nil, + }) + if descriptor ~= nil then + local origin = type(motion) == "table" and motion.origin or previous + self:_apply_pending_delete(domain.Position.coerce(origin), position, descriptor) + end + self:_emit_movement_event(previous) +end + +function MemoryHost:apply_selection(position, kind) + local previous = self._cursor + local next_selection + if domain.Selection.is(position) then + next_selection = position + position = next_selection.focus + else + position = domain.Position.coerce(position) + if kind == nil then + kind = self._selection.kind + end + kind = domain.SelectionKind.from_string(kind) + if kind == domain.SelectionKind.NONE then + error("selection movement requires a Visual selection kind", 2) + end + local anchor = self._selection.active and self._selection.anchor or previous + next_selection = domain.Selection.active( + kind, + anchor, + position, + self._selection.option + ) + end + + self._selection = next_selection + self._cursor = position + self:_record("apply_selection", { + position = position, + kind = next_selection.kind.value, + }) + self:_emit_movement_event(previous) +end + +function MemoryHost:set_operator_inclusive(enabled) + if type(enabled) ~= "boolean" then + error("operator inclusivity must be a Boolean", 2) + end + self._operator_inclusive = enabled + self:_record("set_operator_inclusive", { enabled = enabled }) +end + +function MemoryHost:operator_inclusive() + return self._operator_inclusive +end + +function MemoryHost:read_input() + local packet = self._input_packets[self._input_index] + if packet == nil then + error("in-memory input queue is empty", 2) + end + self._input_index = self._input_index + 1 + self:_record("read_input", { packet = packet:to_table() }) + if packet.kind == domain.InputPacketKind.ERROR then + error(packet.message, 0) + end + return packet +end + +function MemoryHost:open_fold(position) + position = position and domain.Position.coerce(position) or self._cursor + local closed_levels = self._fold_state.closed_levels + if closed_levels == 0 then + self:_record("open_fold", { position = position, opened = false }) + return false + end + self:_record("open_fold", { + position = position, + fold_level = closed_levels, + opened = true, + }) + self._fold_state = domain.FoldState.new( + self._fold_state:policies(), + closed_levels - 1 + ) + return true +end + +function MemoryHost:show_prompt(text) + if type(text) ~= "string" then + error("prompt must be a string", 2) + end + self._prompts[#self._prompts + 1] = text + self:_record("show_prompt", { text = text }) +end + +function MemoryHost:prompts() + return list_copy(self._prompts) +end + +function MemoryHost:redraw(kind) + if kind ~= "screen" and kind ~= "full" and kind ~= "suppressed" then + error("redraw kind must be screen, full, or suppressed", 2) + end + self._redraws[#self._redraws + 1] = kind + self:_record("redraw", { kind = kind }) +end + +function MemoryHost:redraws() + return list_copy(self._redraws) +end + +function MemoryHost:emit_diagnostic(level, text) + if level ~= "error" and level ~= "warning" and level ~= "info" then + error("diagnostic level must be error, warning, or info", 2) + end + if type(text) ~= "string" or text == "" then + error("diagnostic text must be a nonempty string", 2) + end + local diagnostic = { level = level, text = text } + self._diagnostics[#self._diagnostics + 1] = diagnostic + self:_record("emit_diagnostic", diagnostic) +end + +function MemoryHost:diagnostics() + return copy(self._diagnostics) +end + +function MemoryHost:read_highlight_group(name) + if type(name) ~= "string" or name == "" then + error("highlight group name must be a nonempty string", 2) + end + local definition = self._highlight_groups[name] + self:_record("read_highlight_group", { + name = name, + defined = definition ~= nil, + }) + return copy(definition) +end + +function MemoryHost:highlight_groups() + return copy(self._highlight_groups) +end + +function MemoryHost:define_highlight_group(name, definition, options) + if type(name) ~= "string" or name == "" then + error("highlight group name must be a nonempty string", 2) + end + if type(definition) ~= "table" then + error("highlight group definition must be a table", 2) + end + options = options or {} + if type(options) ~= "table" then + error("highlight group options must be a table", 2) + end + if options.default ~= nil and type(options.default) ~= "boolean" then + error("highlight group default option must be a Boolean", 2) + end + if options.force ~= nil and type(options.force) ~= "boolean" then + error("highlight group force option must be a Boolean", 2) + end + + local exists = self._highlight_groups[name] ~= nil + local applied = not (exists and options.default) + if applied then + self._highlight_groups[name] = copy(definition) + end + self:_record("define_highlight_group", { + name = name, + definition = definition, + options = options, + applied = applied, + }) + return applied +end + +function MemoryHost:create_highlight(specification) + if type(specification) ~= "table" then + error("highlight specification must be a table", 2) + end + if type(specification.group) ~= "string" or specification.group == "" then + error("highlight group must be a nonempty string", 2) + end + local identity = specification.identity or self:_next_identity("highlight") + if self._highlights[identity] ~= nil then + error("highlight identity is already active", 2) + end + local stored = copy(specification) + stored.identity = identity + self._highlights[identity] = stored + self:_record("create_highlight", stored) + return identity +end + +function MemoryHost:remove_highlight(identity) + if type(identity) ~= "string" or identity == "" then + error("highlight identity must be a nonempty string", 2) + end + local removed = self._highlights[identity] ~= nil + self._highlights[identity] = nil + self:_record("remove_highlight", { identity = identity, removed = removed }) + return removed +end + +function MemoryHost:highlights() + return copy(self._highlights) +end + +function MemoryHost:supports_cursor_presentation() + self:_record("supports_cursor_presentation", { + supported = self._cursor_presentation_support, + }) + return self._cursor_presentation_support +end + +function MemoryHost:suppress_cursor_presentation() + if not self._cursor_presentation_support then + self:_record("suppress_cursor_presentation", { supported = false }) + return nil + end + local identity = self:_next_identity("cursor-presentation") + self._cursor_leases[identity] = copy(self._cursor_presentation) + local suppressed = copy(self._cursor_presentation) + suppressed.hidden = true + self._cursor_presentation = suppressed + self:_record("suppress_cursor_presentation", { + identity = identity, + supported = true, + }) + return identity +end + +function MemoryHost:restore_cursor_presentation(identity) + if identity == nil then + self:_record("restore_cursor_presentation", { restored = false }) + return false + end + local saved = self._cursor_leases[identity] + if saved == nil then + error("cursor presentation lease is inactive", 2) + end + self._cursor_presentation = saved + self._cursor_leases[identity] = nil + self:_record("restore_cursor_presentation", { + identity = identity, + restored = true, + }) + return true +end + +function MemoryHost:cursor_presentation() + return copy(self._cursor_presentation) +end + +function MemoryHost:supports_timers() + self:_record("supports_timers", { supported = self._timer_support }) + return self._timer_support +end + +function MemoryHost:start_timer(delay_ms, callback) + if not is_integer(delay_ms) or delay_ms < 0 then + error("timer delay must be a nonnegative integer", 2) + end + if type(callback) ~= "function" then + error("timer callback must be a function", 2) + end + if not self._timer_support then + self:_record("start_timer", { delay_ms = delay_ms, supported = false }) + return nil + end + local identity = self:_next_identity("timer") + self._timers[identity] = { + identity = identity, + delay_ms = delay_ms, + callback = callback, + active = true, + } + self:_record("start_timer", { + identity = identity, + delay_ms = delay_ms, + supported = true, + }) + return identity +end + +function MemoryHost:stop_timer(identity) + if type(identity) ~= "string" or identity == "" then + error("timer identity must be a nonempty string", 2) + end + local timer = self._timers[identity] + local stopped = timer ~= nil and timer.active + if timer ~= nil then + timer.active = false + end + self:_record("stop_timer", { identity = identity, stopped = stopped }) + return stopped +end + +function MemoryHost:fire_timer(identity) + local timer = self._timers[identity] + if timer == nil then + error("timer identity is unknown", 2) + end + if not timer.active then + self:_record("ignore_timer", { identity = identity }) + return false + end + timer.active = false + self:_record("fire_timer", { identity = identity }) + timer.callback(identity) + return true +end + +function MemoryHost:timers() + local result = {} + for identity, timer in pairs(self._timers) do + result[identity] = { + identity = identity, + delay_ms = timer.delay_ms, + active = timer.active, + } + end + return result +end + +function MemoryHost:register_events(event_names, callback, options) + local names, name_set = normalize_event_names(event_names) + if type(callback) ~= "function" then + error("event callback must be a function", 2) + end + local identity = self:_next_identity("event-registration") + self._event_registrations[identity] = { + identity = identity, + names = names, + name_set = name_set, + callback = callback, + options = copy(options or {}), + active = true, + } + self._event_registration_order[#self._event_registration_order + 1] = identity + self:_record("register_events", { + identity = identity, + names = names, + options = options or {}, + }) + return identity +end + +function MemoryHost:remove_event_registration(identity) + local registration = self._event_registrations[identity] + local removed = registration ~= nil and registration.active + if registration ~= nil then + registration.active = false + end + self:_record("remove_event_registration", { + identity = identity, + removed = removed, + }) + return removed +end + +function MemoryHost:_deliver_event_now(name, payload) + self:_record("event", { name = name, payload = payload }) + local order = list_copy(self._event_registration_order) + local event_buffer = payload.buffer or self._buffer + for _, identity in ipairs(order) do + local registration = self._event_registrations[identity] + local registration_buffer = registration.options.buffer + if registration.active + and registration.name_set[name] + and (registration_buffer == nil or registration_buffer == event_buffer) + then + registration.callback(name, payload) + end + end +end + +function MemoryHost:deliver_event(name, payload) + if type(name) ~= "string" or name == "" then + error("event name must be a nonempty string", 2) + end + payload = copy(payload or {}) + local queued = self._event_queue:is_transition_active() + self:_record(queued and "queue_event" or "deliver_event", { + name = name, + payload = payload, + }) + return self._event_queue:emit(name, payload) +end + +function MemoryHost:begin_action_transition() + local token = self._event_queue:begin_transition() + self:_record("begin_action_transition", { identity = token }) + return token +end + +function MemoryHost:commit_action_transition(token) + self:_record("commit_action_transition", { identity = token }) + self._event_queue:commit_transition(token) +end + +function MemoryHost:pending_event_count() + return self._event_queue:pending_count() +end + +function MemoryHost:event_registrations() + local result = {} + for identity, registration in pairs(self._event_registrations) do + result[identity] = { + identity = identity, + names = list_copy(registration.names), + options = copy(registration.options), + active = registration.active, + } + end + return result +end + +function MemoryHost:register_action(name, callback) + if type(name) ~= "string" or name == "" then + error("action name must be a nonempty string", 2) + end + if type(callback) ~= "function" then + error("action callback must be a function", 2) + end + if self._actions[name] ~= nil then + error("action is already registered", 2) + end + self._actions[name] = callback + self:_record("register_action", { name = name }) + return name +end + +function MemoryHost:invoke_action(name, ...) + local callback = self._actions[name] + if callback == nil then + error("action is not registered", 2) + end + local arguments = { ... } + local argument_count = select("#", ...) + local token = self:begin_action_transition() + local results = { + pcall(function() + return callback(unpack_values(arguments, 1, argument_count)) + end), + } + self:commit_action_transition(token) + local succeeded = table.remove(results, 1) + if not succeeded then + error(results[1], 0) + end + return unpack_values(results) +end + +function MemoryHost:register_mapping(modes, lhs, action, options) + modes = normalize_modes(modes) + if type(lhs) ~= "string" or lhs == "" then + error("mapping lhs must be a nonempty string", 2) + end + if type(action) ~= "string" and type(action) ~= "function" then + error("mapping action must be an action name or function", 2) + end + local identity = self:_next_identity("mapping") + self._mappings[identity] = { + identity = identity, + modes = modes, + lhs = lhs, + action = action, + options = copy(options or {}), + } + self:_record("register_mapping", { + identity = identity, + modes = modes, + lhs = lhs, + action = type(action) == "string" and action or "<function>", + options = options or {}, + }) + return identity +end + +function MemoryHost:mappings() + return copy(self._mappings) +end + +function MemoryHost:register_dot_repeat(payload, callback) + if not domain.DotPayload.is(payload) then + error("dot-repeat payload must be a DotPayload", 2) + end + if callback ~= nil and type(callback) ~= "function" then + error("dot-repeat callback must be a function", 2) + end + self._dot_repeat = { + payload = payload, + callback = callback, + operator = self._pending_operator, + mode = self._mode, + } + self:_record("register_dot_repeat", { payload = payload:to_table() }) + return payload +end + +function MemoryHost:dot_repeat_payload() + return self._dot_repeat and self._dot_repeat.payload or nil +end + +function MemoryHost:replay_dot(count) + if self._dot_repeat == nil or self._dot_repeat.callback == nil then + error("dot repeat is not executable", 2) + end + self._pending_operator = self._dot_repeat.operator + self._mode = self._dot_repeat.mode + return self._dot_repeat.callback(self._dot_repeat.payload, domain.Count.new(count)) +end + +return M |
