diff options
| author | Jackson Moore <jacksonmoore@tuta.io> | 2026-09-04 11:47:14 +0200 |
|---|---|---|
| committer | Jackson Moore <jacksonmoore@tuta.io> | 2026-09-04 11:47:14 +0200 |
| commit | 71c815b9794f19df9451f59fac6dd0368d91e1f8 (patch) | |
| tree | 94313ecb6dd68f1d9264c4c87351773529fa6e03 | |
| parent | 881a6714c6bfd4d28c5b0470ad54041e5399a06c (diff) | |
Return valid direct marker positions
| -rw-r--r-- | lua/clever_f/direct_preview_planner.lua | 37 | ||||
| -rw-r--r-- | lua/clever_f/feedback_service.lua | 55 | ||||
| -rw-r--r-- | tests/run.lua | 76 |
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)) |
