diff options
| -rw-r--r-- | Makefile | 2 | ||||
| -rw-r--r-- | lua/clever_f/host_adapter.lua | 411 | ||||
| -rw-r--r-- | lua/clever_f/init.lua | 118 | ||||
| -rw-r--r-- | plugin/clever_f.lua | 6 | ||||
| -rw-r--r-- | tests/host_adapter_contract.lua | 179 | ||||
| -rw-r--r-- | tests/plugin_smoke.lua | 104 | ||||
| -rw-r--r-- | tests/run.lua | 2 |
7 files changed, 803 insertions, 19 deletions
@@ -4,3 +4,5 @@ NVIM ?= nvim test: NVIM_LOG_FILE=/dev/null $(NVIM) --headless -u NONE -i NONE -l tests/run.lua + NVIM_LOG_FILE=/dev/null $(NVIM) --headless -u NONE -i NONE -l tests/host_adapter_contract.lua + NVIM_LOG_FILE=/dev/null $(NVIM) --headless -u NONE -i NONE -l tests/plugin_smoke.lua diff --git a/lua/clever_f/host_adapter.lua b/lua/clever_f/host_adapter.lua index a81a44c..724e9da 100644 --- a/lua/clever_f/host_adapter.lua +++ b/lua/clever_f/host_adapter.lua @@ -2,6 +2,7 @@ local capabilities = require("clever_f.capabilities") local domain = require("clever_f.domain") local M = {} +local unpack_values = table.unpack or unpack local HostAdapter = {} HostAdapter.__index = HostAdapter M.HostAdapter = HostAdapter @@ -11,6 +12,24 @@ M.ActionEffect = { ESCAPE = "escape", ERROR = "error", } +M.CONFIGURATION_PREFIX = "clever_f_" +M.CONFIGURATION_GLOBALS = { + suppress_default_mappings = "clever_f_not_overwrites_standard_mappings", +} + +local BOOLEAN_CONFIGURATION = { + search_current_line_only = true, + ignore_case = true, + smart_case = true, + use_migemo = true, + fix_key_direction = true, + show_prompt = true, + mark_cursor = true, + hide_cursor_on_cmdline = true, + mark_char = true, + mark_direct = true, + clean_labels_eagerly = true, +} local adapter_records = setmetatable({}, { __mode = "k" }) @@ -19,8 +38,14 @@ local function fail(message, level) end local function current_runtime(options) - if type(options) == "table" and options.runtime ~= nil then - return options.runtime + if type(options) == "table" then + if options.runtime ~= nil then + return options.runtime + end + if options.api ~= nil then + return options + end + return rawget(_G, "vim") end if options ~= nil and options ~= HostAdapter then return options @@ -47,9 +72,12 @@ function HostAdapter.new(options) timers = {}, cursor_presentations = {}, events = {}, + actions = {}, + mappings = {}, dot_repeat = nil, dot_bridge = nil, event_order = {}, + action_diagnostics = nil, augroup = nil, } record.event_queue = capabilities.EventQueue.new(function(_, delivery) @@ -67,6 +95,108 @@ function HostAdapter:runtime() return adapter_records[self].runtime end +function HostAdapter:read_text() + local lines = self:runtime().api.nvim_buf_get_lines(0, 0, -1, true) + if #lines == 0 then + lines = { "" } + end + return domain.TextSnapshot.new(lines) +end + +function HostAdapter:read_buffer() + return self:runtime().api.nvim_get_current_buf() +end + +function HostAdapter:read_window() + return self:runtime().api.nvim_get_current_win() +end + +function HostAdapter:read_cursor() + local position = self:runtime().api.nvim_win_get_cursor(0) + return domain.Position.new(position[1], position[2] + 1) +end + +function HostAdapter:read_mode() + return self:runtime().api.nvim_get_mode().mode +end + +local function selection_option(runtime) + local value = runtime.api.nvim_get_option_value( + "selection", + { scope = "global" } + ) + if value == "exclusive" then + return domain.SelectionOption.EXCLUSIVE + end + return domain.SelectionOption.INCLUSIVE +end + +function HostAdapter:read_selection() + local runtime = self:runtime() + local context = domain.ModeContext.from_full_mode(self:read_mode()) + local kind = context.visual_kind or context.select_kind + local option = selection_option(runtime) + if kind == nil then + return domain.Selection.inactive(option) + end + local raw_anchor = runtime.fn.getpos("v") + local focus = self:read_cursor() + local anchor + if type(raw_anchor) == "table" + and type(raw_anchor[2]) == "number" + and raw_anchor[2] > 0 + and type(raw_anchor[3]) == "number" + and raw_anchor[3] > 0 + then + anchor = domain.Position.new(raw_anchor[2], raw_anchor[3]) + else + anchor = focus + end + return domain.Selection.active(kind, anchor, focus, option) +end + +function HostAdapter:read_count() + local runtime = self:runtime() + local count = runtime.v.count1 + if type(count) ~= "number" or count < 1 then + count = 1 + end + return domain.Count.new(count) +end + +local function configuration_global(name) + if type(name) ~= "string" or name == "" then + fail("configuration name must be a nonempty string", 3) + end + return M.CONFIGURATION_GLOBALS[name] or M.CONFIGURATION_PREFIX .. name +end + +function M.configuration_global(name) + return configuration_global(name) +end + +function HostAdapter:configuration_present(name) + local global = configuration_global(name) + return self:runtime().fn.exists("g:" .. global) == 1 +end + +local function normalize_configuration(name, value) + if BOOLEAN_CONFIGURATION[name] and type(value) == "number" then + return value ~= 0 + end + return value +end + +function HostAdapter:read_configuration(name) + local global = configuration_global(name) + return normalize_configuration(name, self:runtime().g[global]) +end + +function HostAdapter:write_configuration(name, value) + local global = configuration_global(name) + self:runtime().g[global] = value +end + function HostAdapter:read_encoding() local runtime = self:runtime() return runtime.api.nvim_get_option_value( @@ -86,6 +216,45 @@ function HostAdapter:lowercase(value) return result end +function HostAdapter:read_macro_state() + local register = self:runtime().fn.reg_executing() + return domain.MacroState.new(register ~= "" and register or nil) +end + +local function fold_open_policies(value) + local result = {} + for item in tostring(value):gmatch("[^,]+") do + if item == "hor" then + item = "horizontal" + end + result[#result + 1] = item + end + return result +end + +function HostAdapter:read_fold_state() + local runtime = self:runtime() + local foldopen = runtime.api.nvim_get_option_value( + "foldopen", + { scope = "global" } + ) + local line = self:read_cursor().line + local closed_levels = runtime.fn.foldclosed(line) == -1 and 0 or 1 + return domain.FoldState.new( + fold_open_policies(foldopen), + closed_levels + ) +end + +function HostAdapter:read_time_ms() + local runtime = self:runtime() + local uv = runtime.uv or runtime.loop + if type(uv) ~= "table" or type(uv.hrtime) ~= "function" then + fail("HostAdapter runtime must provide a monotonic clock", 2) + end + return uv.hrtime() / 1000000 +end + function HostAdapter:read_pending_operator() local operator = self:runtime().v.operator if operator == nil then @@ -125,6 +294,101 @@ function HostAdapter:set_operator_inclusive(enabled) end end +local function string_bytes(value) + local bytes = {} + for index = 1, #value do + bytes[index] = string.byte(value, index) + end + return bytes +end + +function HostAdapter:read_input() + local runtime = self:runtime() + local value = runtime.fn.getcharstr() + if type(value) ~= "string" or value == "" then + fail("Nvim target input must be a nonempty string", 2) + end + local bytes = string_bytes(value) + if #bytes == 3 + and bytes[1] == 0x80 + and bytes[2] == 0xfd + and bytes[3] == 0x60 + then + return domain.InputPacket.raw_bytes(bytes) + end + if #bytes == 1 and bytes[1] == 27 then + return domain.InputPacket.special_key("Escape", bytes) + end + if bytes[1] == 0x80 then + local name = type(runtime.fn.keytrans) == "function" + and runtime.fn.keytrans(value) + or "Special" + return domain.InputPacket.special_key(name, value) + end + return domain.InputPacket.text(value) +end + +function HostAdapter:open_fold(position) + position = position and domain.Position.coerce(position) or self:read_cursor() + local runtime = self:runtime() + if runtime.fn.foldclosed(position.line) == -1 then + return false + end + runtime.api.nvim_cmd({ + cmd = "normal", + bang = true, + args = { "zo" }, + }, {}) + return true +end + +function HostAdapter:show_prompt(text) + if type(text) ~= "string" then + fail("prompt must be a string", 2) + end + self:runtime().api.nvim_echo({ { text } }, false, {}) +end + +function HostAdapter:redraw(kind) + if kind == "suppressed" then + return false + end + if kind ~= "screen" and kind ~= "full" then + fail("redraw kind must be screen, full, or suppressed", 2) + end + self:runtime().api.nvim_cmd({ + cmd = "redraw", + bang = kind == "full", + }, {}) + return true +end + +local DIAGNOSTIC_LEVELS = { + error = "ERROR", + warning = "WARN", + info = "INFO", +} + +function HostAdapter:emit_diagnostic(level, text) + local level_name = DIAGNOSTIC_LEVELS[level] + if level_name == nil then + fail("diagnostic level must be error, warning, or info", 2) + end + if type(text) ~= "string" or text == "" then + fail("diagnostic text must be a nonempty string", 2) + end + local record = adapter_records[self] + local runtime = record.runtime + if type(runtime.notify) ~= "function" then + fail("HostAdapter runtime must provide notify", 2) + end + local levels = type(runtime.log) == "table" and runtime.log.levels or {} + runtime.notify(text, levels[level_name], { title = "clever-f" }) + if record.action_diagnostics ~= nil then + record.action_diagnostics[level .. "\0" .. text] = true + end +end + local install_dot_bridge function HostAdapter:register_dot_repeat(payload, callback) @@ -356,7 +620,7 @@ function HostAdapter:start_timer(delay_ms, callback) if resource == nil or not resource.active then return end - resource.active = false + record.timers[identity] = nil callback(identity) end) if type(timer_id) ~= "number" or timer_id < 0 then @@ -375,7 +639,7 @@ function HostAdapter:stop_timer(identity) if resource == nil or not resource.active then return false end - resource.active = false + record.timers[identity] = nil record.runtime.fn.timer_stop(resource.timer_id) return true end @@ -544,13 +808,9 @@ install_dot_bridge = function(adapter) if registration == nil or registration.callback == nil then return end - local count = runtime.v.count1 - if type(count) ~= "number" or count < 1 then - count = 1 - end local outcome = registration.callback( registration.payload, - domain.Count.new(count) + adapter:read_count() ) if domain.ActionOutcome.is(outcome) then adapter:translate_action_outcome(outcome) @@ -627,11 +887,16 @@ function HostAdapter:deliver_event(name, payload) end function HostAdapter:begin_action_transition() - return adapter_records[self].event_queue:begin_transition() + local record = adapter_records[self] + record.action_diagnostics = {} + return record.event_queue:begin_transition() end function HostAdapter:commit_action_transition(token) - return adapter_records[self].event_queue:commit_transition(token) + local record = adapter_records[self] + local result = record.event_queue:commit_transition(token) + record.action_diagnostics = nil + return result end local function terminal_cursor_option(runtime) @@ -724,12 +989,7 @@ function HostAdapter:return_escape() end function HostAdapter:emit_action_error(text) - local runtime = self:runtime() - if type(runtime.notify) ~= "function" then - fail("HostAdapter runtime must provide notify", 2) - end - local levels = type(runtime.log) == "table" and runtime.log.levels or nil - runtime.notify(text, levels and levels.ERROR or nil, { title = "clever-f" }) + return self:emit_diagnostic("error", text) end function HostAdapter:translate_action_outcome(outcome) @@ -741,12 +1001,127 @@ function HostAdapter:translate_action_outcome(outcome) return M.ActionEffect.ESCAPE end if outcome.kind == domain.ActionKind.ERROR then - self:emit_action_error(outcome.diagnostic) + local diagnostics = adapter_records[self].action_diagnostics + local key = "error\0" .. outcome.diagnostic + if diagnostics == nil or not diagnostics[key] then + self:emit_action_error(outcome.diagnostic) + end return M.ActionEffect.ERROR end return M.ActionEffect.NONE end +local function packed(...) + return { n = select("#", ...), ... } +end + +local function invoke_callback(adapter, callback, ...) + local arguments = packed(...) + local token = adapter:begin_action_transition() + local results = packed(pcall(function() + local values = packed(callback(unpack_values(arguments, 1, arguments.n))) + if domain.ActionOutcome.is(values[1]) then + adapter:translate_action_outcome(values[1]) + end + return unpack_values(values, 1, values.n) + end)) + local commit = packed(pcall(adapter.commit_action_transition, adapter, token)) + if not results[1] then + error(results[2], 0) + end + if not commit[1] then + error(commit[2], 0) + end + return unpack_values(results, 2, results.n) +end + +function HostAdapter:register_action(name, callback) + if type(name) ~= "string" or name == "" then + fail("action name must be a nonempty string", 2) + end + if type(callback) ~= "function" then + fail("action callback must be a function", 2) + end + local actions = adapter_records[self].actions + if actions[name] ~= nil then + fail("action is already registered", 2) + end + actions[name] = callback + return name +end + +function HostAdapter:invoke_action(name, ...) + local callback = adapter_records[self].actions[name] + if callback == nil then + fail("action is not registered", 2) + end + return invoke_callback(self, callback, ...) +end + +function HostAdapter:invoke_callback(callback, ...) + if type(callback) ~= "function" then + fail("action callback must be a function", 2) + end + return invoke_callback(self, callback, ...) +end + +local function mapping_modes(value) + if type(value) == "string" then + value = { value } + end + if type(value) ~= "table" or #value == 0 then + fail("mapping modes must be a nonempty list", 3) + end + local result = {} + for index, mode in ipairs(value) do + if type(mode) ~= "string" or mode == "" then + fail("mapping mode must be a nonempty string", 3) + end + result[index] = mode + end + return result +end + +function HostAdapter:register_mapping(modes, lhs, action, options) + modes = mapping_modes(modes) + if type(lhs) ~= "string" or lhs == "" then + fail("mapping lhs must be a nonempty string", 2) + end + if type(action) ~= "string" and type(action) ~= "function" then + fail("mapping action must be an action name or function", 2) + end + options = options or {} + if type(options) ~= "table" then + fail("mapping options must be a table", 2) + end + local callback + if type(action) == "string" then + callback = function() + return self:invoke_action(action) + end + else + callback = function(...) + return invoke_callback(self, action, ...) + end + end + local native_options = { + silent = options.silent == true, + remap = options.remap == true, + desc = options.desc + or ("clever-f " .. (type(action) == "string" and action or lhs)), + } + self:runtime().keymap.set(modes, lhs, callback, native_options) + local identity = next_identity(self, "mapping") + adapter_records[self].mappings[identity] = { + modes = modes, + lhs = lhs, + action = action, + options = options, + callback = callback, + } + return identity +end + function M.new(options) return HostAdapter.new(options) end diff --git a/lua/clever_f/init.lua b/lua/clever_f/init.lua new file mode 100644 index 0000000..a1272fc --- /dev/null +++ b/lua/clever_f/init.lua @@ -0,0 +1,118 @@ +local composition_root = require("clever_f.composition_root") +local host_adapter = require("clever_f.host_adapter") + +local M = {} +local active_root +local active_activation + +local function fail(message, level) + error(message, (level or 1) + 1) +end + +local function build_root(options) + if composition_root.CompositionRoot.is(options) then + return options + end + if host_adapter.HostAdapter.is(options) then + return composition_root.new({ host = options }) + end + options = options or {} + if type(options) ~= "table" then + fail("clever-f activation options must be a table", 3) + end + if options.host == nil then + local adapter_options = options.runtime ~= nil + and { runtime = options.runtime } + or nil + options = { host = host_adapter.new(adapter_options) } + end + return composition_root.new(options) +end + +function M.activate(options) + if active_root == nil then + active_root = build_root(options) + active_activation = active_root:activate() + end + return active_activation +end + +local function root() + M.activate() + return active_root +end + +function M.root() + return root() +end + +function M.state() + return root():state() +end + +local function invoke(name) + local instance = root() + return instance:host():invoke_action(name) +end + +function M.StartFindForward() + return invoke("StartFindForward") +end + +function M.StartFindBackward() + return invoke("StartFindBackward") +end + +function M.StartTillForward() + return invoke("StartTillForward") +end + +function M.StartTillBackward() + return invoke("StartTillBackward") +end + +function M.Reset() + return invoke("Reset") +end + +function M.RepeatSameDirection() + return invoke("RepeatSameDirection") +end + +function M.RepeatOppositeDirection() + return invoke("RepeatOppositeDirection") +end + +local function invoke_direct(callback) + local instance = root() + local host = instance:host() + if type(host.invoke_callback) ~= "function" then + fail("clever-f host must invoke direct action callbacks", 2) + end + return host:invoke_callback(function() + return callback(instance) + end) +end + +function M.invoke_descriptor(value) + return invoke_direct(function(instance) + return instance:invoke_descriptor(value) + end) +end + +function M._diagnostic_full_reset() + return invoke_direct(function(instance) + return instance:diagnostic_full_reset() + end) +end + +M.start_find_forward = M.StartFindForward +M.start_find_backward = M.StartFindBackward +M.start_till_forward = M.StartTillForward +M.start_till_backward = M.StartTillBackward +M.reset = M.Reset +M.repeat_same_direction = M.RepeatSameDirection +M.repeat_opposite_direction = M.RepeatOppositeDirection +M.free_form = M.invoke_descriptor + +return M diff --git a/plugin/clever_f.lua b/plugin/clever_f.lua new file mode 100644 index 0000000..1a8cadd --- /dev/null +++ b/plugin/clever_f.lua @@ -0,0 +1,6 @@ +if vim.g.loaded_clever_f_lua then + return +end + +vim.g.loaded_clever_f_lua = 1 +require("clever_f").activate() diff --git a/tests/host_adapter_contract.lua b/tests/host_adapter_contract.lua new file mode 100644 index 0000000..ff369d5 --- /dev/null +++ b/tests/host_adapter_contract.lua @@ -0,0 +1,179 @@ +local script = debug.getinfo(1, "S").source:sub(2) +local root = script:match("^(.*)/tests/host_adapter_contract%.lua$") or "." +package.path = table.concat({ + root .. "/lua/?.lua", + root .. "/lua/?/init.lua", + package.path, +}, ";") + +local capabilities = require("clever_f.capabilities") +local composition_root = require("clever_f.composition_root") +local domain = require("clever_f.domain") +local host_adapter = require("clever_f.host_adapter") + +local function same(expected, actual, message) + if expected ~= actual then + error((message or "values differ") + .. ": expected " .. tostring(expected) + .. ", got " .. tostring(actual), 2) + end +end + +local function truthy(value, message) + if not value then + error(message or "value must be true", 2) + end +end + +local exercised = {} +local host = host_adapter.new() +capabilities.assert_implements(host) + +local function call(name, ...) + exercised[name] = true + return host[name](host, ...) +end + +vim.api.nvim_buf_set_lines(0, 0, -1, true, { "ababa" }) +vim.api.nvim_win_set_cursor(0, { 1, 0 }) + +same("ababa", call("read_text"):line(1)) +same(vim.api.nvim_get_current_buf(), call("read_buffer")) +same(vim.api.nvim_get_current_win(), call("read_window")) +same(domain.Position.new(1, 1), call("read_cursor")) +same("n", call("read_mode")) +truthy(not call("read_selection").active) +same(1, call("read_count").value) + +local ignore_case_global = host_adapter.configuration_global("ignore_case") +vim.g[ignore_case_global] = 0 +truthy(call("configuration_present", "ignore_case")) +same(false, call("read_configuration", "ignore_case")) +call("write_configuration", "ignore_case", true) +same(true, call("read_configuration", "ignore_case")) +same("utf-8", call("read_encoding")) +same(vim.fn.tolower("AbC"), call("lowercase", "AbC")) +truthy(not call("read_macro_state").executing) +truthy(domain.FoldState.is(call("read_fold_state"))) +truthy(call("read_time_ms") > 0) + +same("", call("read_pending_operator")) +call("apply_cursor", domain.Position.new(1, 2)) +same(domain.Position.new(1, 2), call("read_cursor")) +call("apply_selection", domain.Position.new(1, 3), domain.SelectionKind.CHARACTER) +same(domain.Position.new(1, 3), call("read_cursor")) +call("set_operator_inclusive", false) + +vim.api.nvim_feedkeys("z", "n", false) +local packet = call("read_input") +same(domain.InputPacketKind.TEXT, packet.kind) +same("z", packet.text) +same(false, call("open_fold", domain.Position.new(1, 3))) +call("show_prompt", "clever-f: ") +call("redraw", "screen") +call("redraw", "full") +same(false, call("redraw", "suppressed")) + +local notifications = {} +vim.notify = function(text, level, options) + notifications[#notifications + 1] = { + text = text, + level = level, + title = options.title, + } +end +call("emit_diagnostic", "info", "adapter contract") +same("adapter contract", notifications[1].text) +same("clever-f", notifications[1].title) + +call("define_highlight_group", "CleverFContract", { + guifg = "red", + guibg = "NONE", + gui = { bold = true }, +}, { force = true }) +truthy(call("read_highlight_group", "CleverFContract") ~= nil) +local highlight = call("create_highlight", { + group = "CleverFContract", + window = call("read_window"), + positions = { domain.Position.new(1, 1), domain.Position.new(1, 3) }, + priority = "high", +}) +truthy(call("remove_highlight", highlight)) + +truthy(call("supports_timers")) +local timer = call("start_timer", 100000, function() end) +truthy(timer ~= nil) +truthy(call("stop_timer", timer)) + +local prior_guicursor = vim.o.guicursor +local prior_terminal_cursor = vim.fn.eval("&t_ve") +local presentation_supported = call("supports_cursor_presentation") +local lease = call("suppress_cursor_presentation") +if presentation_supported then + truthy(lease ~= nil) + truthy(call("restore_cursor_presentation", lease)) + same(prior_guicursor, vim.o.guicursor) + same(prior_terminal_cursor, vim.fn.eval("&t_ve")) +else + same(nil, lease) + same(false, call("restore_cursor_presentation", lease)) +end + +local delivered +local registration = call("register_events", "CursorMoved", function(name, payload) + delivered = { name = name, payload = payload } +end) +local transition = call("begin_action_transition") +call("deliver_event", "CursorMoved", { + buffer = call("read_buffer"), + window = call("read_window"), +}) +same(nil, delivered) +call("commit_action_transition", transition) +same("CursorMoved", delivered.name) +truthy(call("remove_event_registration", registration)) + +local neutral_position = call("read_cursor") +call("register_action", "ContractNeutral", function() + return domain.ActionOutcome.neutral(neutral_position) +end) +local mapping = call( + "register_mapping", + { "n" }, + "<Plug>(CleverFContract)", + "ContractNeutral", + { silent = true, remap = false, preserve_count = true } +) +truthy(mapping ~= nil) +local mapping_info = vim.fn.maparg("<Plug>(CleverFContract)", "n", false, true) +same(1, mapping_info.silent) +same(1, mapping_info.noremap) + +local target = domain.TargetValue.character("a", string.byte("a")) +local payload = domain.DotPayload.new("f", target) +same(payload, call("register_dot_repeat", payload, function() + return domain.ActionOutcome.neutral(neutral_position) +end)) + +for _, method_name in ipairs(capabilities.required_methods()) do + truthy(exercised[method_name], "adapter capability was not exercised: " .. method_name) +end + +local suppression_global = host_adapter.configuration_global( + "suppress_default_mappings" +) +same("clever_f_not_overwrites_standard_mappings", suppression_global) +for _, value in ipairs({ false, 0 }) do + vim.g[suppression_global] = value + local suppressed = composition_root.new({ + host = host_adapter.new(), + }):activate() + same(false, suppressed.setup.install_default_mappings) + same(nil, next(suppressed.mappings)) +end +vim.g[suppression_global] = nil + +io.stdout:write(string.format( + "Phase 15 adapter contract: %d capabilities passed\n", + #capabilities.required_methods() +)) diff --git a/tests/plugin_smoke.lua b/tests/plugin_smoke.lua new file mode 100644 index 0000000..3028723 --- /dev/null +++ b/tests/plugin_smoke.lua @@ -0,0 +1,104 @@ +local script = debug.getinfo(1, "S").source:sub(2) +local root = script:match("^(.*)/tests/plugin_smoke%.lua$") or "." +vim.opt.runtimepath:prepend(root) + +local function same(expected, actual, message) + if expected ~= actual then + error((message or "values differ") + .. ": expected " .. tostring(expected) + .. ", got " .. tostring(actual), 2) + end +end + +local function truthy(value, message) + if not value then + error(message or "value must be true", 2) + end +end + +local function feed(keys) + vim.api.nvim_feedkeys(vim.keycode(keys), "xt", false) + vim.cmd("redraw") +end + +vim.g.clever_f_mark_cursor = 0 +vim.g.clever_f_mark_char = 0 +vim.g.clever_f_mark_direct = 0 +vim.g.clever_f_hide_cursor_on_cmdline = 0 +vim.g.clever_f_clean_labels_eagerly = 0 +vim.api.nvim_buf_set_lines(0, 0, -1, true, { "ababa" }) +vim.api.nvim_win_set_cursor(0, { 1, 0 }) + +vim.cmd.runtime("plugin/clever_f.lua") +same(1, vim.g.loaded_clever_f_lua) +local clever_f = require("clever_f") +local first_root = clever_f.root() +truthy(first_root ~= nil) + +for _, mode in ipairs({ "n", "x", "o" }) do + for _, lhs in ipairs({ "f", "F", "t", "T" }) do + local mapping = vim.fn.maparg(lhs, mode, false, true) + same(1, mapping.silent, mode .. lhs) + same(1, mapping.noremap, mode .. lhs) + end +end + +feed("2fb") +same(1, vim.api.nvim_win_get_cursor(0)[1]) +same(3, vim.api.nvim_win_get_cursor(0)[2]) +same( + 4, + clever_f.state():to_table().contexts.n.previous_landing.byte_column +) +clever_f.Reset() + +vim.api.nvim_buf_set_lines(0, 0, -1, true, { "ababa" }) +vim.api.nvim_win_set_cursor(0, { 1, 0 }) +feed("vfb") +same("v", vim.api.nvim_get_mode().mode) +same(1, vim.api.nvim_win_get_cursor(0)[2]) +feed("<Esc>") +clever_f.Reset() + +vim.api.nvim_buf_set_lines(0, 0, -1, true, { "hoge fuge piye poye" }) +vim.api.nvim_win_set_cursor(0, { 1, 0 }) +feed("dfe") +same(" fuge piye poye", vim.api.nvim_get_current_line()) +truthy(clever_f.root():host():dot_repeat_payload() ~= nil) +feed(".") +same(" piye poye", vim.api.nvim_get_current_line()) +feed(".") +same(" poye", vim.api.nvim_get_current_line()) +feed(".") +same("", vim.api.nvim_get_current_line()) + +local ok, diagnostic = pcall(clever_f.invoke_descriptor, "X") +same(false, ok) +same("clever-f: Invalid mapping 'X'", diagnostic) + +vim.g.clever_f_mark_char = 1 +vim.g.clever_f_mark_char_color = "Search" +vim.api.nvim_exec_autocmds("ColorScheme", {}) +same( + "Search", + vim.api.nvim_get_hl(0, { name = "CleverFChar", link = true }).link +) + +for _, name in ipairs({ + "StartFindForward", + "StartFindBackward", + "StartTillForward", + "StartTillBackward", + "Reset", + "RepeatSameDirection", + "RepeatOppositeDirection", +}) do + same("function", type(clever_f[name]), name) +end +same("function", type(clever_f.invoke_descriptor)) +same("function", type(clever_f._diagnostic_full_reset)) + +vim.cmd.runtime("plugin/clever_f.lua") +same(first_root, clever_f.root()) + +io.stdout:write("Phase 15 plugin smoke: startup, mappings, actions, and dot passed\n") diff --git a/tests/run.lua b/tests/run.lua index fb84dd3..03aa725 100644 --- a/tests/run.lua +++ b/tests/run.lua @@ -8756,4 +8756,4 @@ for _, item in ipairs(tests) do passed = passed + 1 end -io.stdout:write(string.format("Phase 14: %d tests passed\n", passed)) +io.stdout:write(string.format("Phase 15 core: %d tests passed\n", passed)) |
