summaryrefslogtreecommitdiff
path: root/lua/clever_f/domain.lua
diff options
context:
space:
mode:
Diffstat (limited to 'lua/clever_f/domain.lua')
-rw-r--r--lua/clever_f/domain.lua1285
1 files changed, 1285 insertions, 0 deletions
diff --git a/lua/clever_f/domain.lua b/lua/clever_f/domain.lua
new file mode 100644
index 0000000..a5b8d39
--- /dev/null
+++ b/lua/clever_f/domain.lua
@@ -0,0 +1,1285 @@
+local M = {}
+
+local records = setmetatable({}, { __mode = "k" })
+local record_types = setmetatable({}, { __mode = "k" })
+local methods = {}
+local formatters = {}
+local equalities = {}
+local metatables = {}
+
+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 register_type(type_name, type_methods, formatter, equality)
+ methods[type_name] = type_methods or {}
+ formatters[type_name] = formatter
+ equalities[type_name] = equality
+
+ local mt = {
+ __index = function(value, key)
+ local field = records[value][key]
+ if field ~= nil then
+ return field
+ end
+ return methods[type_name][key]
+ end,
+ __newindex = function()
+ fail(type_name .. " values are immutable", 2)
+ end,
+ __tostring = function(value)
+ local format = formatters[type_name]
+ if format then
+ return format(records[value])
+ end
+ return type_name
+ end,
+ __eq = function(left, right)
+ if record_types[left] ~= type_name or record_types[right] ~= type_name then
+ return false
+ end
+ local equal = equalities[type_name]
+ if equal then
+ return equal(records[left], records[right])
+ end
+ return rawequal(left, right)
+ end,
+ __metatable = "clever_f.domain." .. type_name,
+ }
+ metatables[type_name] = mt
+end
+
+local function new_record(type_name, fields)
+ local value = {}
+ records[value] = fields
+ record_types[value] = type_name
+ return setmetatable(value, metatables[type_name])
+end
+
+local function is_record(value, type_name)
+ return record_types[value] == type_name
+end
+
+local function require_record(value, type_name, name)
+ if not is_record(value, type_name) then
+ fail((name or "value") .. " must be a " .. type_name, 2)
+ end
+ return value
+end
+
+local function require_string(value, name, allow_empty)
+ if type(value) ~= "string" or (not allow_empty and value == "") then
+ fail(name .. " must be " .. (allow_empty and "a string" or "a nonempty string"), 2)
+ end
+ return value
+end
+
+local function require_boolean(value, name)
+ if type(value) ~= "boolean" then
+ fail(name .. " must be a Boolean", 2)
+ end
+ return value
+end
+
+local function require_nonnegative_integer(value, name)
+ if not is_integer(value) or value < 0 then
+ fail(name .. " must be a nonnegative integer", 2)
+ end
+ return value
+end
+
+function M.type_of(value)
+ return record_types[value]
+end
+
+local function define_enum(type_name, entries)
+ local enum_methods = {}
+ local namespace = {}
+ local by_value = {}
+
+ register_type(type_name, enum_methods, function(data)
+ return data.value
+ end)
+
+ for constant, serialized in pairs(entries) do
+ local value = new_record(type_name, {
+ name = constant,
+ value = serialized,
+ })
+ namespace[constant] = value
+ by_value[serialized] = value
+ end
+
+ function namespace.from_string(value)
+ if is_record(value, type_name) then
+ return value
+ end
+ local result = by_value[value]
+ if result == nil then
+ fail("value must be a valid " .. type_name, 2)
+ end
+ return result
+ end
+
+ function namespace.is(value)
+ return is_record(value, type_name)
+ end
+
+ function enum_methods:to_string()
+ return records[self].value
+ end
+
+ return namespace
+end
+
+M.Family = define_enum("Family", {
+ FIND = "FIND",
+ TILL = "TILL",
+})
+
+M.Direction = define_enum("Direction", {
+ FORWARD = "forward",
+ BACKWARD = "backward",
+})
+
+M.SelectionKind = define_enum("SelectionKind", {
+ NONE = "none",
+ CHARACTER = "character",
+ LINE = "line",
+ BLOCK = "block",
+})
+
+M.SelectionOption = define_enum("SelectionOption", {
+ INCLUSIVE = "inclusive",
+ EXCLUSIVE = "exclusive",
+})
+
+M.TargetKind = define_enum("TargetKind", {
+ CHARACTER = "character",
+ SPECIAL_KEY = "special_key",
+ CODE_FALLBACK = "code_fallback",
+})
+
+M.CaseMode = define_enum("CaseMode", {
+ SENSITIVE = "sensitive",
+ INSENSITIVE = "insensitive",
+})
+
+M.TargetPlanKind = define_enum("TargetPlanKind", {
+ EMPTY = "empty",
+ LITERAL = "literal",
+ BACKSLASH = "backslash",
+ SYMBOL = "symbol",
+ MIGEMO = "migemo",
+})
+
+M.SearchScope = define_enum("SearchScope", {
+ BUFFER = "buffer",
+ CURRENT_LINE = "current_line",
+})
+
+M.EndpointPolicy = define_enum("EndpointPolicy", {
+ REGULAR = "regular",
+ VISUAL_EXCLUSIVE = "visual_exclusive",
+})
+
+M.SearchStatus = define_enum("SearchStatus", {
+ COMPLETE = "complete",
+ BOUNDARY_AFTER_PARTIAL = "boundary_after_partial",
+ BOUNDARY_BEFORE_ANY = "boundary_before_any",
+})
+
+M.ActionKind = define_enum("ActionKind", {
+ MOVEMENT = "movement",
+ NEUTRAL = "neutral",
+ ESCAPE = "escape",
+ FAILED_SEARCH = "failed_search",
+ ERROR = "error",
+ EMPTY = "empty",
+})
+
+local Position = {}
+M.Position = Position
+
+register_type("Position", Position, function(data)
+ return string.format("(%d,%d)", data.line, data.byte_column)
+end, function(left, right)
+ return left.line == right.line and left.byte_column == right.byte_column
+end)
+
+function Position.new(line, byte_column)
+ if not is_integer(line) or line < 1 then
+ fail("line must be a positive one-based integer", 2)
+ end
+ if not is_integer(byte_column) or byte_column < 1 then
+ fail("byte_column must be a positive one-based integer", 2)
+ end
+ return new_record("Position", {
+ line = line,
+ byte_column = byte_column,
+ })
+end
+
+function Position.coerce(value)
+ if Position.is(value) then
+ return value
+ end
+ if type(value) ~= "table" then
+ fail("position must be a Position or position table", 2)
+ end
+ return Position.new(value.line, value.byte_column)
+end
+
+function Position.is(value)
+ return is_record(value, "Position")
+end
+
+function Position.compare(left, right)
+ require_record(left, "Position", "left")
+ require_record(right, "Position", "right")
+ if left.line < right.line then
+ return -1
+ end
+ if left.line > right.line then
+ return 1
+ end
+ if left.byte_column < right.byte_column then
+ return -1
+ end
+ if left.byte_column > right.byte_column then
+ return 1
+ end
+ return 0
+end
+
+function Position.equal(left, right)
+ return Position.compare(left, right) == 0
+end
+
+function Position.stationary(left, right)
+ return Position.equal(left, right)
+end
+
+function Position.is_forward(candidate, origin)
+ return Position.compare(candidate, origin) > 0
+end
+
+function Position.is_backward(candidate, origin)
+ return Position.compare(candidate, origin) < 0
+end
+
+function Position:to_table()
+ return {
+ line = self.line,
+ byte_column = self.byte_column,
+ }
+end
+
+local Descriptor = {}
+M.Descriptor = Descriptor
+
+register_type("Descriptor", Descriptor, function(data)
+ return data.value
+end)
+
+local descriptors_by_string = {}
+local descriptors_by_parts = {}
+
+local function descriptor_key(family, direction)
+ return family.value .. ":" .. direction.value
+end
+
+local function define_descriptor(name, serialized, family, direction)
+ local descriptor = new_record("Descriptor", {
+ name = name,
+ value = serialized,
+ family = family,
+ direction = direction,
+ uppercase = serialized:match("%u") ~= nil,
+ })
+ descriptors_by_string[serialized] = descriptor
+ descriptors_by_parts[descriptor_key(family, direction)] = descriptor
+ Descriptor[name] = descriptor
+ Descriptor[serialized] = descriptor
+ return descriptor
+end
+
+Descriptor.FIND_FORWARD = define_descriptor(
+ "FIND_FORWARD",
+ "f",
+ M.Family.FIND,
+ M.Direction.FORWARD
+)
+Descriptor.FIND_BACKWARD = define_descriptor(
+ "FIND_BACKWARD",
+ "F",
+ M.Family.FIND,
+ M.Direction.BACKWARD
+)
+Descriptor.TILL_FORWARD = define_descriptor(
+ "TILL_FORWARD",
+ "t",
+ M.Family.TILL,
+ M.Direction.FORWARD
+)
+Descriptor.TILL_BACKWARD = define_descriptor(
+ "TILL_BACKWARD",
+ "T",
+ M.Family.TILL,
+ M.Direction.BACKWARD
+)
+
+function Descriptor.is(value)
+ return is_record(value, "Descriptor")
+end
+
+function Descriptor.is_valid(value)
+ return Descriptor.is(value) or descriptors_by_string[value] ~= nil
+end
+
+function Descriptor.from_string(value)
+ if Descriptor.is(value) then
+ return value
+ end
+ local descriptor = descriptors_by_string[value]
+ if descriptor == nil then
+ fail("descriptor must be one of f, F, t, or T", 2)
+ end
+ return descriptor
+end
+
+function Descriptor.try_from_string(value)
+ if Descriptor.is(value) then
+ return value
+ end
+ return descriptors_by_string[value]
+end
+
+function Descriptor.from_parts(family, direction)
+ family = M.Family.from_string(family)
+ direction = M.Direction.from_string(direction)
+ return descriptors_by_parts[descriptor_key(family, direction)]
+end
+
+function Descriptor.to_string(value)
+ return Descriptor.from_string(value).value
+end
+
+function Descriptor.is_uppercase(value)
+ return Descriptor.from_string(value).uppercase
+end
+
+function Descriptor.is_lowercase(value)
+ return not Descriptor.is_uppercase(value)
+end
+
+function Descriptor.swap(value)
+ local descriptor = Descriptor.from_string(value)
+ local direction = descriptor.direction == M.Direction.FORWARD
+ and M.Direction.BACKWARD
+ or M.Direction.FORWARD
+ return Descriptor.from_parts(descriptor.family, direction)
+end
+
+function Descriptor.lowercase(value)
+ local descriptor = Descriptor.from_string(value)
+ return Descriptor.from_parts(descriptor.family, M.Direction.FORWARD)
+end
+
+function Descriptor.uppercase(value)
+ local descriptor = Descriptor.from_string(value)
+ return Descriptor.from_parts(descriptor.family, M.Direction.BACKWARD)
+end
+
+local Count = {}
+M.Count = Count
+
+register_type("Count", Count, function(data)
+ return tostring(data.value)
+end, function(left, right)
+ return left.value == right.value
+end)
+
+local count_one
+
+function Count.new(value)
+ if Count.is(value) then
+ return value
+ end
+ if value == nil then
+ value = 1
+ end
+ if not is_integer(value) or value < 1 then
+ fail("count must be a positive integer", 2)
+ end
+ if value == 1 and count_one ~= nil then
+ return count_one
+ end
+ local count = new_record("Count", { value = value })
+ if value == 1 then
+ count_one = count
+ end
+ return count
+end
+
+function Count.is(value)
+ return is_record(value, "Count")
+end
+
+function Count.to_number(value)
+ return Count.new(value).value
+end
+
+Count.ONE = Count.new(1)
+
+local ModeContext = {}
+M.ModeContext = ModeContext
+
+register_type("ModeContext", ModeContext, function(data)
+ return data.key
+end)
+
+local mode_contexts = {}
+local CTRL_V = string.char(0x16)
+local CTRL_S = string.char(0x13)
+
+local operator_modes = {
+ no = true,
+ nov = true,
+ noV = true,
+ ["no" .. CTRL_V] = true,
+}
+
+local function mode_traits(full_mode)
+ local operator = operator_modes[full_mode] == true
+ local visual_kind
+ local select_kind
+
+ if not operator then
+ local lead = full_mode:sub(1, 1)
+ if lead == "v" then
+ visual_kind = M.SelectionKind.CHARACTER
+ elseif lead == "V" then
+ visual_kind = M.SelectionKind.LINE
+ elseif lead == CTRL_V then
+ visual_kind = M.SelectionKind.BLOCK
+ elseif lead == "s" then
+ select_kind = M.SelectionKind.CHARACTER
+ elseif lead == "S" then
+ select_kind = M.SelectionKind.LINE
+ elseif lead == CTRL_S then
+ select_kind = M.SelectionKind.BLOCK
+ end
+ end
+
+ return operator, visual_kind, select_kind
+end
+
+function ModeContext.from_full_mode(full_mode)
+ if ModeContext.is(full_mode) then
+ return full_mode
+ end
+ require_string(full_mode, "full_mode", false)
+
+ local operator, visual_kind, select_kind = mode_traits(full_mode)
+ local key = operator and "no" or full_mode
+ local context = mode_contexts[key]
+ if context ~= nil then
+ return context
+ end
+
+ context = new_record("ModeContext", {
+ key = key,
+ full_mode = key,
+ operator = operator,
+ visual_kind = visual_kind,
+ select_kind = select_kind,
+ visual = visual_kind ~= nil,
+ select = select_kind ~= nil,
+ command_path = visual_kind == nil,
+ })
+ mode_contexts[key] = context
+ return context
+end
+
+function ModeContext.is(value)
+ return is_record(value, "ModeContext")
+end
+
+function ModeContext.equal(left, right)
+ require_record(left, "ModeContext", "left")
+ require_record(right, "ModeContext", "right")
+ return left.key == right.key
+end
+
+function ModeContext:to_key()
+ return self.key
+end
+
+local Selection = {}
+M.Selection = Selection
+
+register_type("Selection", Selection, function(data)
+ return data.active and ("selection:" .. data.kind.value) or "selection:none"
+end, function(left, right)
+ return left.active == right.active
+ and left.kind == right.kind
+ and left.anchor == right.anchor
+ and left.focus == right.focus
+ and left.option == right.option
+end)
+
+function Selection.new(options)
+ if Selection.is(options) then
+ return options
+ end
+ if type(options) ~= "table" then
+ fail("selection options must be a table", 2)
+ end
+
+ local active = require_boolean(options.active, "selection.active")
+ local kind = M.SelectionKind.from_string(options.kind)
+ local option = M.SelectionOption.from_string(options.option or "inclusive")
+ local anchor = options.anchor
+ local focus = options.focus
+
+ if active then
+ if kind == M.SelectionKind.NONE then
+ fail("an active selection must have a selection kind", 2)
+ end
+ anchor = Position.coerce(anchor)
+ focus = Position.coerce(focus)
+ else
+ if kind ~= M.SelectionKind.NONE then
+ fail("an inactive selection must use the none kind", 2)
+ end
+ if anchor ~= nil or focus ~= nil then
+ fail("an inactive selection must have empty endpoints", 2)
+ end
+ end
+
+ return new_record("Selection", {
+ active = active,
+ kind = kind,
+ anchor = anchor,
+ focus = focus,
+ option = option,
+ })
+end
+
+function Selection.inactive(option)
+ return Selection.new({
+ active = false,
+ kind = M.SelectionKind.NONE,
+ option = option or M.SelectionOption.INCLUSIVE,
+ })
+end
+
+function Selection.active(kind, anchor, focus, option)
+ return Selection.new({
+ active = true,
+ kind = kind,
+ anchor = anchor,
+ focus = focus,
+ option = option or M.SelectionOption.INCLUSIVE,
+ })
+end
+
+function Selection.is(value)
+ return is_record(value, "Selection")
+end
+
+function Selection:with_focus(focus, kind)
+ if not self.active then
+ fail("selection must be active", 2)
+ end
+ return Selection.active(kind or self.kind, self.anchor, focus, self.option)
+end
+
+function Selection:to_table()
+ return {
+ active = self.active,
+ kind = self.kind.value,
+ anchor = self.anchor and self.anchor:to_table() or nil,
+ focus = self.focus and self.focus:to_table() or nil,
+ option = self.option.value,
+ }
+end
+
+local TextSnapshot = {}
+M.TextSnapshot = TextSnapshot
+local text_lines = setmetatable({}, { __mode = "k" })
+
+register_type("TextSnapshot", TextSnapshot, function(data)
+ return "text:" .. tostring(data.line_count) .. " lines"
+end, function(left, right)
+ if left.line_count ~= right.line_count then
+ return false
+ end
+ local left_lines = text_lines[left.identity]
+ local right_lines = text_lines[right.identity]
+ for index = 1, left.line_count do
+ if left_lines[index] ~= right_lines[index] then
+ return false
+ end
+ end
+ return true
+end)
+
+function TextSnapshot.new(lines)
+ if TextSnapshot.is(lines) then
+ return lines
+ end
+ if type(lines) ~= "table" or #lines < 1 then
+ fail("text lines must be a nonempty list", 2)
+ end
+ local copy = {}
+ for index = 1, #lines do
+ if type(lines[index]) ~= "string" then
+ fail("each text line must be a string", 2)
+ end
+ copy[index] = lines[index]
+ end
+ local identity = {}
+ text_lines[identity] = copy
+ return new_record("TextSnapshot", {
+ identity = identity,
+ line_count = #copy,
+ })
+end
+
+function TextSnapshot.is(value)
+ return is_record(value, "TextSnapshot")
+end
+
+function TextSnapshot:line(line_number)
+ if not is_integer(line_number) or line_number < 1 or line_number > self.line_count then
+ fail("line_number must identify a line in the text snapshot", 2)
+ end
+ return text_lines[self.identity][line_number]
+end
+
+function TextSnapshot:lines()
+ local result = {}
+ local source = text_lines[self.identity]
+ for index = 1, self.line_count do
+ result[index] = source[index]
+ end
+ return result
+end
+
+function TextSnapshot:to_table()
+ return { lines = self:lines() }
+end
+
+local MacroState = {}
+M.MacroState = MacroState
+
+register_type("MacroState", MacroState, function(data)
+ return data.executing and ("macro:" .. data.register) or "macro:inactive"
+end, function(left, right)
+ return left.register == right.register
+end)
+
+function MacroState.new(register)
+ if MacroState.is(register) then
+ return register
+ end
+ if register == "" then
+ register = nil
+ end
+ if register ~= nil then
+ require_string(register, "macro register", false)
+ end
+ return new_record("MacroState", {
+ register = register,
+ executing = register ~= nil,
+ })
+end
+
+function MacroState.is(value)
+ return is_record(value, "MacroState")
+end
+
+local FoldState = {}
+M.FoldState = FoldState
+local fold_policies = setmetatable({}, { __mode = "k" })
+
+register_type("FoldState", FoldState, function(data)
+ return "folds:" .. tostring(data.closed_levels)
+end)
+
+function FoldState.new(open_policy, closed_levels)
+ if FoldState.is(open_policy) and closed_levels == nil then
+ return open_policy
+ end
+ if type(open_policy) ~= "table" then
+ fail("fold open policy must be a list", 2)
+ end
+ require_nonnegative_integer(closed_levels, "closed fold levels")
+
+ local identity = {}
+ local policies = {}
+ local seen = {}
+ for index = 1, #open_policy do
+ local policy = require_string(open_policy[index], "fold policy item", false)
+ if not seen[policy] then
+ seen[policy] = true
+ policies[#policies + 1] = policy
+ end
+ end
+ fold_policies[identity] = {
+ list = policies,
+ set = seen,
+ }
+ return new_record("FoldState", {
+ identity = identity,
+ closed_levels = closed_levels,
+ })
+end
+
+function FoldState.is(value)
+ return is_record(value, "FoldState")
+end
+
+function FoldState:opens(policy)
+ require_string(policy, "fold policy", false)
+ return fold_policies[self.identity].set[policy] == true
+end
+
+function FoldState:policies()
+ local result = {}
+ local source = fold_policies[self.identity].list
+ for index = 1, #source do
+ result[index] = source[index]
+ end
+ return result
+end
+
+local InputPacket = {}
+M.InputPacket = InputPacket
+local packet_bytes = setmetatable({}, { __mode = "k" })
+
+M.InputPacketKind = define_enum("InputPacketKind", {
+ TEXT = "text",
+ RAW_BYTES = "raw_bytes",
+ SPECIAL_KEY = "special_key",
+ ERROR = "error",
+})
+
+register_type("InputPacket", InputPacket, function(data)
+ return "input:" .. data.kind.value
+end)
+
+function InputPacket.text(text)
+ require_string(text, "input text", false)
+ return new_record("InputPacket", {
+ kind = M.InputPacketKind.TEXT,
+ text = text,
+ })
+end
+
+function InputPacket.raw_bytes(bytes)
+ if type(bytes) ~= "table" or #bytes < 1 then
+ fail("raw input bytes must be a nonempty list", 2)
+ end
+ local copy = {}
+ for index = 1, #bytes do
+ local byte = bytes[index]
+ if not is_integer(byte) or byte < 0 or byte > 255 then
+ fail("raw input bytes must contain byte values", 2)
+ end
+ copy[index] = byte
+ end
+ local identity = {}
+ packet_bytes[identity] = copy
+ return new_record("InputPacket", {
+ kind = M.InputPacketKind.RAW_BYTES,
+ identity = identity,
+ })
+end
+
+function InputPacket.special_key(name, encoded)
+ require_string(name, "special key name", false)
+ if encoded ~= nil then
+ require_string(encoded, "encoded special key", false)
+ end
+ return new_record("InputPacket", {
+ kind = M.InputPacketKind.SPECIAL_KEY,
+ name = name,
+ encoded = encoded,
+ })
+end
+
+function InputPacket.error(message)
+ require_string(message, "input error message", false)
+ return new_record("InputPacket", {
+ kind = M.InputPacketKind.ERROR,
+ message = message,
+ })
+end
+
+function InputPacket.from_table(packet)
+ if InputPacket.is(packet) then
+ return packet
+ end
+ if type(packet) ~= "table" then
+ fail("input packet must be an InputPacket or packet table", 2)
+ end
+ local kind = M.InputPacketKind.from_string(packet.kind)
+ if kind == M.InputPacketKind.TEXT then
+ return InputPacket.text(packet.text)
+ end
+ if kind == M.InputPacketKind.RAW_BYTES then
+ return InputPacket.raw_bytes(packet.bytes)
+ end
+ if kind == M.InputPacketKind.SPECIAL_KEY then
+ return InputPacket.special_key(packet.name, packet.encoded)
+ end
+ return InputPacket.error(packet.message)
+end
+
+function InputPacket.is(value)
+ return is_record(value, "InputPacket")
+end
+
+function InputPacket:bytes()
+ if self.kind ~= M.InputPacketKind.RAW_BYTES then
+ return nil
+ end
+ local source = packet_bytes[self.identity]
+ local result = {}
+ for index = 1, #source do
+ result[index] = source[index]
+ end
+ return result
+end
+
+function InputPacket:to_table()
+ local result = { kind = self.kind.value }
+ if self.kind == M.InputPacketKind.TEXT then
+ result.text = self.text
+ elseif self.kind == M.InputPacketKind.RAW_BYTES then
+ result.bytes = self:bytes()
+ elseif self.kind == M.InputPacketKind.SPECIAL_KEY then
+ result.name = self.name
+ result.encoded = self.encoded
+ else
+ result.message = self.message
+ end
+ return result
+end
+
+local TargetValue = {}
+M.TargetValue = TargetValue
+
+register_type("TargetValue", TargetValue, function(data)
+ return "target:" .. data.kind.value .. ":" .. tostring(data.first_code)
+end, function(left, right)
+ return left.kind == right.kind
+ and left.value == right.value
+ and left.first_code == right.first_code
+end)
+
+function TargetValue.character(value, first_code)
+ require_string(value, "target character", false)
+ require_nonnegative_integer(first_code, "target first code")
+ return new_record("TargetValue", {
+ kind = M.TargetKind.CHARACTER,
+ value = value,
+ first_code = first_code,
+ })
+end
+
+function TargetValue.special_key(value, first_code)
+ require_string(value, "encoded special key", false)
+ first_code = first_code or string.byte(value, 1)
+ require_nonnegative_integer(first_code, "target first code")
+ if first_code ~= 0x80 then
+ fail("an encoded special key must start with hexadecimal 80", 2)
+ end
+ return new_record("TargetValue", {
+ kind = M.TargetKind.SPECIAL_KEY,
+ value = value,
+ first_code = first_code,
+ })
+end
+
+function TargetValue.code_fallback(first_code)
+ first_code = first_code or 0
+ require_nonnegative_integer(first_code, "fallback character code")
+ return new_record("TargetValue", {
+ kind = M.TargetKind.CODE_FALLBACK,
+ value = "",
+ first_code = first_code,
+ })
+end
+
+function TargetValue.from_table(target)
+ if TargetValue.is(target) then
+ return target
+ end
+ if type(target) ~= "table" then
+ fail("target must be a TargetValue or target table", 2)
+ end
+ local kind = M.TargetKind.from_string(target.kind)
+ if kind == M.TargetKind.CHARACTER then
+ return TargetValue.character(target.value, target.first_code)
+ end
+ if kind == M.TargetKind.SPECIAL_KEY then
+ return TargetValue.special_key(target.value, target.first_code)
+ end
+ return TargetValue.code_fallback(target.first_code)
+end
+
+function TargetValue.is(value)
+ return is_record(value, "TargetValue")
+end
+
+function TargetValue:to_table()
+ return {
+ kind = self.kind.value,
+ value = self.value,
+ first_code = self.first_code,
+ }
+end
+
+local TargetPlan = {}
+M.TargetPlan = TargetPlan
+
+register_type("TargetPlan", TargetPlan, function(data)
+ return "target-plan:" .. data.kind.value
+end)
+
+function TargetPlan.new(options)
+ if TargetPlan.is(options) then
+ return options
+ end
+ if type(options) ~= "table" then
+ fail("target plan options must be a table", 2)
+ end
+ local target = require_record(options.target, "TargetValue", "target plan target")
+ local kind = M.TargetPlanKind.from_string(options.kind)
+ local case_mode = M.CaseMode.from_string(options.case_mode)
+ if type(options.matcher) ~= "function" then
+ fail("target plan matcher must be a function", 2)
+ end
+ return new_record("TargetPlan", {
+ target = target,
+ kind = kind,
+ case_mode = case_mode,
+ matcher = options.matcher,
+ })
+end
+
+function TargetPlan.is(value)
+ return is_record(value, "TargetPlan")
+end
+
+function TargetPlan:matches(...)
+ return self.matcher(...)
+end
+
+function TargetPlan:to_table()
+ return {
+ target = self.target:to_table(),
+ kind = self.kind.value,
+ case_mode = self.case_mode.value,
+ }
+end
+
+local ResolvedMotionPlan = {}
+M.ResolvedMotionPlan = ResolvedMotionPlan
+
+register_type("ResolvedMotionPlan", ResolvedMotionPlan, function(data)
+ return "motion-plan:" .. data.descriptor.value
+end)
+
+function ResolvedMotionPlan.new(options)
+ if ResolvedMotionPlan.is(options) then
+ return options
+ end
+ if type(options) ~= "table" then
+ fail("resolved motion plan options must be a table", 2)
+ end
+ return new_record("ResolvedMotionPlan", {
+ target_plan = require_record(options.target_plan, "TargetPlan", "target plan"),
+ descriptor = Descriptor.from_string(options.descriptor),
+ search_scope = M.SearchScope.from_string(options.search_scope),
+ endpoint_policy = M.EndpointPolicy.from_string(options.endpoint_policy),
+ })
+end
+
+function ResolvedMotionPlan.is(value)
+ return is_record(value, "ResolvedMotionPlan")
+end
+
+function ResolvedMotionPlan:to_table()
+ return {
+ target_plan = self.target_plan:to_table(),
+ descriptor = self.descriptor.value,
+ search_scope = self.search_scope.value,
+ endpoint_policy = self.endpoint_policy.value,
+ }
+end
+
+local MotionRequest = {}
+M.MotionRequest = MotionRequest
+
+register_type("MotionRequest", MotionRequest, function(data)
+ return "motion-request:" .. data.descriptor.value
+end)
+
+function MotionRequest.new(options)
+ if MotionRequest.is(options) then
+ return options
+ end
+ if type(options) ~= "table" then
+ fail("motion request options must be a table", 2)
+ end
+ if options.policy == nil then
+ fail("motion request policy is required", 2)
+ end
+ return new_record("MotionRequest", {
+ context = require_record(options.context, "ModeContext", "motion context"),
+ origin = Position.coerce(options.origin),
+ descriptor = Descriptor.from_string(options.descriptor),
+ target = require_record(options.target, "TargetValue", "motion target"),
+ count = Count.new(options.count),
+ policy = options.policy,
+ first_move = require_boolean(options.first_move, "first_move"),
+ })
+end
+
+function MotionRequest.is(value)
+ return is_record(value, "MotionRequest")
+end
+
+local SearchOutcome = {}
+M.SearchOutcome = SearchOutcome
+
+register_type("SearchOutcome", SearchOutcome, function(data)
+ return "search:" .. data.status.value
+end)
+
+local function new_search_outcome(status, endpoint, successful_steps)
+ status = M.SearchStatus.from_string(status)
+ endpoint = Position.coerce(endpoint)
+ require_nonnegative_integer(successful_steps, "successful_steps")
+
+ if status == M.SearchStatus.COMPLETE and successful_steps < 1 then
+ fail("a complete search must contain a successful step", 3)
+ end
+ if status == M.SearchStatus.BOUNDARY_AFTER_PARTIAL and successful_steps < 1 then
+ fail("a partial search must contain a successful step", 3)
+ end
+ if status == M.SearchStatus.BOUNDARY_BEFORE_ANY and successful_steps ~= 0 then
+ fail("a boundary-before-any search must contain zero successful steps", 3)
+ end
+
+ return new_record("SearchOutcome", {
+ status = status,
+ endpoint = endpoint,
+ successful_steps = successful_steps,
+ complete = status == M.SearchStatus.COMPLETE,
+ })
+end
+
+function SearchOutcome.new(options)
+ if SearchOutcome.is(options) then
+ return options
+ end
+ if type(options) ~= "table" then
+ fail("search outcome options must be a table", 2)
+ end
+ return new_search_outcome(options.status, options.endpoint, options.successful_steps)
+end
+
+function SearchOutcome.complete(endpoint, successful_steps)
+ return new_search_outcome(M.SearchStatus.COMPLETE, endpoint, successful_steps)
+end
+
+function SearchOutcome.boundary_after_partial(endpoint, successful_steps)
+ return new_search_outcome(
+ M.SearchStatus.BOUNDARY_AFTER_PARTIAL,
+ endpoint,
+ successful_steps
+ )
+end
+
+function SearchOutcome.boundary_before_any(origin)
+ return new_search_outcome(M.SearchStatus.BOUNDARY_BEFORE_ANY, origin, 0)
+end
+
+function SearchOutcome.is(value)
+ return is_record(value, "SearchOutcome")
+end
+
+function SearchOutcome:to_table()
+ return {
+ status = self.status.value,
+ endpoint = self.endpoint:to_table(),
+ successful_steps = self.successful_steps,
+ complete = self.complete,
+ }
+end
+
+local DotPayload = {}
+M.DotPayload = DotPayload
+
+register_type("DotPayload", DotPayload, function(data)
+ return "dot:" .. data.descriptor.value
+end, function(left, right)
+ return left.descriptor == right.descriptor and left.target == right.target
+end)
+
+function DotPayload.new(descriptor, target)
+ if DotPayload.is(descriptor) and target == nil then
+ return descriptor
+ end
+ return new_record("DotPayload", {
+ descriptor = Descriptor.from_string(descriptor),
+ target = require_record(target, "TargetValue", "dot target"),
+ })
+end
+
+function DotPayload.is(value)
+ return is_record(value, "DotPayload")
+end
+
+function DotPayload:to_table()
+ return {
+ descriptor = self.descriptor.value,
+ target = self.target:to_table(),
+ }
+end
+
+local ActionOutcome = {}
+M.ActionOutcome = ActionOutcome
+
+register_type("ActionOutcome", ActionOutcome, function(data)
+ return "action:" .. data.kind.value
+end)
+
+local function new_action_outcome(options)
+ local kind = M.ActionKind.from_string(options.kind)
+ local position = Position.coerce(options.position)
+ local search_outcome = options.search_outcome
+ local descriptor = options.effective_descriptor
+ local dot_payload = options.dot_payload
+
+ if search_outcome ~= nil then
+ require_record(search_outcome, "SearchOutcome", "search outcome")
+ end
+ if descriptor ~= nil then
+ descriptor = Descriptor.from_string(descriptor)
+ end
+ if dot_payload ~= nil then
+ require_record(dot_payload, "DotPayload", "dot payload")
+ end
+ if options.diagnostic ~= nil then
+ require_string(options.diagnostic, "diagnostic", false)
+ end
+
+ if kind == M.ActionKind.MOVEMENT then
+ if search_outcome == nil or not search_outcome.complete then
+ fail("a movement action requires a complete search outcome", 3)
+ end
+ elseif kind == M.ActionKind.FAILED_SEARCH then
+ if search_outcome == nil or search_outcome.complete then
+ fail("a failed-search action requires an incomplete search outcome", 3)
+ end
+ elseif search_outcome ~= nil then
+ fail("only movement and failed-search actions can contain a search outcome", 3)
+ end
+
+ if kind == M.ActionKind.ERROR and options.diagnostic == nil then
+ fail("an error action requires a diagnostic", 3)
+ end
+
+ local complete
+ if search_outcome ~= nil then
+ complete = search_outcome.complete
+ end
+
+ return new_record("ActionOutcome", {
+ kind = kind,
+ position = position,
+ search_outcome = search_outcome,
+ complete = complete,
+ successful_steps = search_outcome and search_outcome.successful_steps or 0,
+ effective_descriptor = descriptor,
+ dot_payload = dot_payload,
+ diagnostic = options.diagnostic,
+ })
+end
+
+function ActionOutcome.new(options)
+ if ActionOutcome.is(options) then
+ return options
+ end
+ if type(options) ~= "table" then
+ fail("action outcome options must be a table", 2)
+ end
+ return new_action_outcome(options)
+end
+
+function ActionOutcome.from_search(search_outcome, descriptor, dot_payload)
+ require_record(search_outcome, "SearchOutcome", "search outcome")
+ return new_action_outcome({
+ kind = search_outcome.complete and M.ActionKind.MOVEMENT or M.ActionKind.FAILED_SEARCH,
+ position = search_outcome.endpoint,
+ search_outcome = search_outcome,
+ effective_descriptor = descriptor,
+ dot_payload = dot_payload,
+ })
+end
+
+local function simple_action(kind, position, diagnostic)
+ return new_action_outcome({
+ kind = kind,
+ position = position,
+ diagnostic = diagnostic,
+ })
+end
+
+function ActionOutcome.neutral(position)
+ return simple_action(M.ActionKind.NEUTRAL, position)
+end
+
+function ActionOutcome.escape(position)
+ return simple_action(M.ActionKind.ESCAPE, position)
+end
+
+function ActionOutcome.empty(position)
+ return simple_action(M.ActionKind.EMPTY, position)
+end
+
+function ActionOutcome.error(position, diagnostic)
+ return simple_action(M.ActionKind.ERROR, position, diagnostic)
+end
+
+function ActionOutcome.is(value)
+ return is_record(value, "ActionOutcome")
+end
+
+function ActionOutcome:to_table()
+ return {
+ kind = self.kind.value,
+ position = self.position:to_table(),
+ complete = self.complete,
+ successful_steps = self.successful_steps,
+ effective_descriptor = self.effective_descriptor and self.effective_descriptor.value or nil,
+ dot_payload = self.dot_payload and self.dot_payload:to_table() or nil,
+ diagnostic = self.diagnostic,
+ }
+end
+
+return M