local capabilities = require("clever_f.capabilities") local domain = require("clever_f.domain") local text_topology = require("clever_f.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", _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: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) for _, identity in ipairs(order) do local registration = self._event_registrations[identity] if registration.active and registration.name_set[name] 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 "", 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