summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJackson Moore <jacksonmoore@tuta.io>2026-09-04 11:47:14 +0200
committerJackson Moore <jacksonmoore@tuta.io>2026-09-04 11:47:14 +0200
commit71c815b9794f19df9451f59fac6dd0368d91e1f8 (patch)
tree94313ecb6dd68f1d9264c4c87351773529fa6e03
parent881a6714c6bfd4d28c5b0470ad54041e5399a06c (diff)
Return valid direct marker positions
-rw-r--r--lua/clever_f/direct_preview_planner.lua37
-rw-r--r--lua/clever_f/feedback_service.lua55
-rw-r--r--tests/run.lua76
3 files changed, 166 insertions, 2 deletions
diff --git a/lua/clever_f/direct_preview_planner.lua b/lua/clever_f/direct_preview_planner.lua
index e771b15..6735924 100644
--- a/lua/clever_f/direct_preview_planner.lua
+++ b/lua/clever_f/direct_preview_planner.lua
@@ -149,6 +149,41 @@ function M.case_grouping_settings(settings)
}
end
+function M.validate_marker_positions(view, positions)
+ require_view(view)
+ if type(positions) ~= "table" then
+ fail("direct preview markers must be a list of positions", 2)
+ end
+
+ local result = {}
+ local seen = {}
+ local item_count = 0
+ for key, position in pairs(positions) do
+ if type(key) ~= "number"
+ or key ~= math.floor(key)
+ or key < 1
+ or key > #positions
+ then
+ fail("direct preview markers must be a list of positions", 2)
+ end
+ position = domain.Position.coerce(position)
+ if not view:is_character_start(position) then
+ fail("direct preview marker must start an editor character", 2)
+ end
+ local identity = tostring(position.line) .. ":" .. tostring(position.byte_column)
+ if seen[identity] then
+ fail("direct preview marker positions must be unique", 2)
+ end
+ seen[identity] = true
+ result[key] = position
+ item_count = item_count + 1
+ end
+ if item_count ~= #positions then
+ fail("direct preview markers must be a list of positions", 2)
+ end
+ return result
+end
+
function DirectPreviewPlanner:plan(view, origin, descriptor, count, settings)
local grouping = M.case_grouping_settings(settings)
local ignore_case = grouping.ignore_case
@@ -181,7 +216,7 @@ function DirectPreviewPlanner:plan(view, origin, descriptor, count, settings)
positions[#positions + 1] = M.marker_position(descriptor, record.position)
end
end
- return positions
+ return M.validate_marker_positions(view, positions)
end
function M.new(options)
diff --git a/lua/clever_f/feedback_service.lua b/lua/clever_f/feedback_service.lua
index e8c387d..b1a4233 100644
--- a/lua/clever_f/feedback_service.lua
+++ b/lua/clever_f/feedback_service.lua
@@ -201,6 +201,54 @@ function FeedbackService:create_cursor_presentation_lease(enabled)
return new_cursor_presentation_lease(record.host, supported == true)
end
+local function position_list(positions)
+ if type(positions) ~= "table" then
+ fail("direct marker positions must be a list", 3)
+ end
+ local result = {}
+ local item_count = 0
+ for key, position in pairs(positions) do
+ if type(key) ~= "number"
+ or key ~= math.floor(key)
+ or key < 1
+ or key > #positions
+ then
+ fail("direct marker positions must be a list", 3)
+ end
+ result[key] = domain.Position.coerce(position)
+ item_count = item_count + 1
+ end
+ if item_count ~= #positions then
+ fail("direct marker positions must be a list", 3)
+ end
+ return result
+end
+
+function FeedbackService:create_direct_markers(positions, window)
+ positions = position_list(positions)
+ if #positions == 0 then
+ return nil
+ end
+ if window == nil then
+ fail("direct marker window must identify its host window", 2)
+ end
+
+ local record = service_records[self]
+ local identity = record.host:create_highlight({
+ group = "CleverFDirect",
+ window = window,
+ positions = positions,
+ priority = M.overlay_priority("CleverFDirect"),
+ })
+ record.transitions:AddTemporaryOverlay(identity, window, "CleverFDirect")
+ return {
+ identity = identity,
+ window = window,
+ group = "CleverFDirect",
+ positions = positions,
+ }
+end
+
function FeedbackService:create_cursor_marker(position, window)
position = domain.Position.coerce(position)
if window == nil then
@@ -307,6 +355,13 @@ function FeedbackService:ensure_default_label()
}
end
+function FeedbackService:evaluate_highlights()
+ return {
+ default_label = self:ensure_default_label(),
+ feature_links = self:evaluate_feature_links(),
+ }
+end
+
function M.new(options)
return FeedbackService.new(options)
end
diff --git a/tests/run.lua b/tests/run.lua
index 3c5150a..31c223e 100644
--- a/tests/run.lua
+++ b/tests/run.lua
@@ -4782,6 +4782,80 @@ test("Direct preview counters ignore Migemo and symbol merging", function()
same(nil, grouping.chars_match_any_signs)
end)
+test("Direct previews return unique valid multibyte byte positions", function()
+ local line = "x\227\129\130x\227\129\132x"
+ local view = text_topology.new({ line }, "utf-8")
+ local planner = direct_preview_planner.new()
+
+ local forward = planner:plan(view, domain.Position.new(1, 1), "f", nil)
+ local backward = planner:plan(view, domain.Position.new(1, 9), "F", nil)
+ local function assert_columns(expected, positions)
+ same(#expected, #positions)
+ local seen = {}
+ for index, position in ipairs(positions) do
+ same(expected[index], position.byte_column)
+ truthy(view:is_character_start(position))
+ falsy(seen[position.byte_column])
+ seen[position.byte_column] = true
+ end
+ end
+ assert_columns({ 2, 5, 6 }, forward)
+ assert_columns({ 6, 5, 2 }, backward)
+
+ local empty_view = text_topology.new({ "" }, "utf-8")
+ same(
+ 0,
+ #planner:plan(empty_view, domain.Position.new(1, 1), "f", nil)
+ )
+ fails(function()
+ direct_preview_planner.validate_marker_positions(view, {
+ domain.Position.new(1, 3),
+ })
+ end, "start an editor character")
+ fails(function()
+ direct_preview_planner.validate_marker_positions(view, {
+ domain.Position.new(1, 2),
+ domain.Position.new(1, 2),
+ })
+ end, "must be unique")
+end)
+
+test("Feedback creates one ordinary direct marker set", function()
+ local state, transitions = fresh_sequence_state()
+ local host = MemoryHost.new({
+ configuration = { mark_direct = true },
+ })
+ local feedback = feedback_service.new({
+ host = host,
+ transitions = transitions,
+ })
+ local positions = {
+ domain.Position.new(1, 2),
+ domain.Position.new(1, 5),
+ domain.Position.new(1, 6),
+ }
+
+ local marker = feedback:create_direct_markers(positions, "window-1")
+ local highlight = host:highlights()[marker.identity]
+ same("CleverFDirect", highlight.group)
+ same(feedback_service.Priority.ORDINARY, highlight.priority)
+ same(3, #highlight.positions)
+ for index = 1, #positions do
+ same(positions[index], highlight.positions[index])
+ end
+ same(marker.identity, state:temporary_overlay_identities()[1])
+ truthy(feedback:remove_temporary_overlay(marker))
+ same(0, #state:temporary_overlay_identities())
+ same(nil, feedback:create_direct_markers({}, "window-1"))
+
+ local evaluation = feedback:evaluate_highlights()
+ same("fallback", evaluation.default_label.source)
+ same(
+ "CleverFDefaultLabel",
+ evaluation.feature_links.CleverFDirect.target
+ )
+end)
+
for _, item in ipairs(tests) do
local ok, failure = xpcall(item.body, debug.traceback)
if not ok then
@@ -4791,4 +4865,4 @@ for _, item in ipairs(tests) do
passed = passed + 1
end
-io.stdout:write(string.format("Phase 10: %d tests passed\n", passed))
+io.stdout:write(string.format("Phase 11: %d tests passed\n", passed))