summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJackson Moore <jacksonmoore@tuta.io>2026-09-04 14:29:26 +0200
committerJackson Moore <jacksonmoore@tuta.io>2026-09-04 14:29:26 +0200
commit80f3025ed00f8c35ae0c83f4ed754a45d108ea1b (patch)
treea755dd3af74541dbfaa4f20ab5b1b00f823ce1e3
parent890cedb3003d45ded2327e483675c42fdfe024f1 (diff)
Connect encoding and case conversion
-rw-r--r--lua/clever_f/capabilities.lua1
-rw-r--r--lua/clever_f/host_adapter.lua19
-rw-r--r--lua/clever_f/sequence_coordinator.lua19
-rw-r--r--lua/clever_f/testing/memory_host.lua13
-rw-r--r--tests/run.lua56
5 files changed, 107 insertions, 1 deletions
diff --git a/lua/clever_f/capabilities.lua b/lua/clever_f/capabilities.lua
index ff92f8f..325d015 100644
--- a/lua/clever_f/capabilities.lua
+++ b/lua/clever_f/capabilities.lua
@@ -9,6 +9,7 @@ M.read_methods = {
count = { "read_count" },
configuration = { "read_configuration", "configuration_present" },
encoding = { "read_encoding" },
+ case_conversion = { "lowercase" },
macro_state = { "read_macro_state" },
fold_state = { "read_fold_state" },
time = { "read_time_ms" },
diff --git a/lua/clever_f/host_adapter.lua b/lua/clever_f/host_adapter.lua
index 51591a9..7a71fb8 100644
--- a/lua/clever_f/host_adapter.lua
+++ b/lua/clever_f/host_adapter.lua
@@ -65,6 +65,25 @@ function HostAdapter:runtime()
return adapter_records[self].runtime
end
+function HostAdapter:read_encoding()
+ local runtime = self:runtime()
+ return runtime.api.nvim_get_option_value(
+ "encoding",
+ { scope = "global" }
+ )
+end
+
+function HostAdapter:lowercase(value)
+ if type(value) ~= "string" then
+ fail("case conversion value must be a string", 2)
+ end
+ local result = self:runtime().fn.tolower(value)
+ if type(result) ~= "string" then
+ fail("Nvim case conversion must return a string", 2)
+ end
+ return result
+end
+
local function next_identity(adapter, prefix)
local record = adapter_records[adapter]
local identity = prefix .. "-" .. tostring(record.next_identity)
diff --git a/lua/clever_f/sequence_coordinator.lua b/lua/clever_f/sequence_coordinator.lua
index 0f233cf..6d2d31b 100644
--- a/lua/clever_f/sequence_coordinator.lua
+++ b/lua/clever_f/sequence_coordinator.lua
@@ -1,4 +1,6 @@
local acquisition_service_factory = require("clever_f.acquisition_service")
+local case_policy = require("clever_f.case_policy")
+local direct_preview_planner = require("clever_f.direct_preview_planner")
local domain = require("clever_f.domain")
local feedback_service_factory = require("clever_f.feedback_service")
local motion_executor_factory = require("clever_f.motion_executor")
@@ -73,9 +75,20 @@ function SequenceCoordinator.new(options)
transitions = transitions,
policy = policy_service,
})
+ local lowercase = options.lowercase
+ if lowercase == nil and type(host.lowercase) == "function" then
+ lowercase = function(value)
+ return host:lowercase(value)
+ end
+ end
+ local case_resolver = options.case_resolver
+ or case_policy.new({ lowercase = lowercase })
local target_factory = options.target_factory
or options.target_plan_factory
- or target_plan_factory.new({ policy = policy_service })
+ or target_plan_factory.new({
+ policy = policy_service,
+ case_resolver = case_resolver,
+ })
if type(target_factory) ~= "table" or type(target_factory.build) ~= "function" then
fail("SequenceCoordinator target factory must provide build", 2)
end
@@ -87,6 +100,9 @@ function SequenceCoordinator.new(options)
then
fail("SequenceCoordinator motion factory must build contextual plans", 2)
end
+ local direct_planner = options.direct_planner
+ or options.direct_preview_planner
+ or direct_preview_planner.new({ case_resolver = case_resolver })
local acquisition = options.acquisition
or options.acquisition_service
or acquisition_service_factory.new({
@@ -95,6 +111,7 @@ function SequenceCoordinator.new(options)
transitions = transitions,
policy = policy_service,
feedback = feedback,
+ direct_planner = direct_planner,
target_factory = target_factory,
motion_factory = motion_factory,
})
diff --git a/lua/clever_f/testing/memory_host.lua b/lua/clever_f/testing/memory_host.lua
index 4c95721..6147d2c 100644
--- a/lua/clever_f/testing/memory_host.lua
+++ b/lua/clever_f/testing/memory_host.lua
@@ -146,6 +146,7 @@ function MemoryHost.new(options)
_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,
@@ -295,6 +296,18 @@ function MemoryHost:read_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
diff --git a/tests/run.lua b/tests/run.lua
index 5ca4f34..871d977 100644
--- a/tests/run.lua
+++ b/tests/run.lua
@@ -361,6 +361,30 @@ test("ActionOutcome and DotPayload retain resolved result data", function()
same("problem", domain.ActionOutcome.error(destination, "problem").diagnostic)
end)
+test("Host adapter reads encoding and uses Nvim case conversion", function()
+ local lowered = {}
+ local runtime = {
+ api = {
+ nvim_get_option_value = function(name, options)
+ same("encoding", name)
+ same("global", options.scope)
+ return "utf-8"
+ end,
+ },
+ fn = {
+ tolower = function(value)
+ lowered[#lowered + 1] = value
+ return string.lower(value)
+ end,
+ },
+ }
+ local adapter = host_adapter.new({ runtime = runtime })
+
+ same("utf-8", adapter:read_encoding())
+ same("abc", adapter:lowercase("AbC"))
+ list_same({ "AbC" }, lowered)
+end)
+
test("Host adapter restores exact cursor presentation values", function()
local options = {
guicursor = "n:block,i:ver37-blinkon123",
@@ -7382,6 +7406,38 @@ test("Eager cleanup preserves history and finalizer registrations", function()
same(nil, state.highlight_timer)
end)
+test("Core composition injects host case conversion into matching", function()
+ fresh_sequence_state()
+ local conversions = {}
+ local host = MemoryHost.new({
+ buffer_lines = { "xA" },
+ cursor = { line = 1, byte_column = 1 },
+ lowercase = function(value)
+ conversions[#conversions + 1] = value
+ if value == "a" or value == "A" then
+ return "folded-a"
+ end
+ return value
+ end,
+ input_packets = { { kind = "text", text = "a" } },
+ configuration = {
+ ignore_case = true,
+ mark_cursor = false,
+ mark_char = false,
+ },
+ emit_movement_events = false,
+ })
+
+ local outcome = composition_root.new({ host = host })
+ :invoke_descriptor("f")
+
+ same(domain.ActionKind.MOVEMENT, outcome.kind)
+ same(domain.Position.new(1, 2), outcome.position)
+ truthy(#conversions >= 2)
+ same("a", conversions[1])
+ same("A", conversions[#conversions])
+end)
+
test("Core activation owns the plugin-global sequence state", function()
local state = fresh_sequence_state()
local host = MemoryHost.new()