diff options
| -rw-r--r-- | lua/clever_f/motion_executor.lua | 6 | ||||
| -rw-r--r-- | lua/clever_f/testing/memory_host.lua | 142 | ||||
| -rw-r--r-- | tests/run.lua | 69 |
3 files changed, 214 insertions, 3 deletions
diff --git a/lua/clever_f/motion_executor.lua b/lua/clever_f/motion_executor.lua index 7a0755b..4e972cd 100644 --- a/lua/clever_f/motion_executor.lua +++ b/lua/clever_f/motion_executor.lua @@ -222,7 +222,11 @@ function MotionExecutor:_execute_command(request) then host:set_operator_inclusive(true) end - host:apply_cursor(outcome.endpoint) + host:apply_cursor(outcome.endpoint, { + context = request.context, + descriptor = request.plan.descriptor, + origin = origin, + }) end if not outcome.complete then return domain.ActionOutcome.from_search(outcome, request.plan.descriptor) diff --git a/lua/clever_f/testing/memory_host.lua b/lua/clever_f/testing/memory_host.lua index 57af1f1..8b473ad 100644 --- a/lua/clever_f/testing/memory_host.lua +++ b/lua/clever_f/testing/memory_host.lua @@ -1,5 +1,6 @@ local capabilities = require("clever_f.capabilities") local domain = require("clever_f.domain") +local text_topology = require("clever_f.text_topology") local M = {} local MemoryHost = {} @@ -383,11 +384,148 @@ function MemoryHost:_emit_movement_event(previous) end end -function MemoryHost:apply_cursor(position) +local function motion_descriptor(motion) + if domain.Descriptor.is(motion) then + return motion + end + if type(motion) == "table" and motion.descriptor ~= nil then + return domain.Descriptor.from_string(motion.descriptor) + end + return nil +end + +local function character_boundary(view, position) + if view:line_is_empty(position.line) then + return 1 + end + return view:character_index_for_position(position) +end + +local function character_lines(snapshot) + local result = {} + for line_number, line in ipairs(snapshot:lines()) do + result[line_number] = text_topology.split_editor_characters(line) + end + return result +end + +local function joined_range(characters, first, last) + local result = {} + for index = first, last do + result[#result + 1] = characters[index] + end + return table.concat(result) +end + +local function delete_character_range( + snapshot, + start_line, + start_index, + finish_line, + finish_index +) + local source = character_lines(snapshot) + local lines = snapshot:lines() + local result = {} + + for line_number = 1, start_line - 1 do + result[#result + 1] = lines[line_number] + end + + local prefix = joined_range(source[start_line], 1, start_index - 1) + if start_line == finish_line then + result[#result + 1] = prefix + .. joined_range( + source[start_line], + finish_index, + #source[start_line] + ) + else + result[#result + 1] = prefix + .. joined_range( + source[finish_line], + finish_index, + #source[finish_line] + ) + end + + for line_number = finish_line + 1, #lines do + result[#result + 1] = lines[line_number] + end + return domain.TextSnapshot.new(result) +end + +local function normalized_cursor(snapshot, encoding, position) + local line_number = math.min(position.line, snapshot.line_count) + local view = text_topology.new(snapshot, encoding) + return view:normalize_endpoint(line_number, position.byte_column) +end + +function MemoryHost:_apply_pending_delete(origin, destination, descriptor) + if self._pending_operator ~= "delete" and self._pending_operator ~= "d" then + return false + end + + local view = text_topology.new(self._text, self._encoding) + local origin_index = character_boundary(view, origin) + local destination_index = character_boundary(view, destination) + local start_line + local start_index + local finish_line + local finish_index + local final_cursor + + if descriptor.direction == domain.Direction.FORWARD then + start_line = origin.line + start_index = origin_index + finish_line = destination.line + finish_index = destination_index + (self._operator_inclusive and 1 or 0) + final_cursor = origin + elseif descriptor.family == domain.Family.FIND then + start_line = destination.line + start_index = destination_index + 1 + finish_line = origin.line + finish_index = origin_index + 1 + final_cursor = destination + else + start_line = destination.line + start_index = destination_index + finish_line = origin.line + finish_index = origin_index + final_cursor = view:predecessor(destination) or destination + end + + self._text = delete_character_range( + self._text, + start_line, + start_index, + finish_line, + finish_index + ) + self._cursor = normalized_cursor(self._text, self._encoding, final_cursor) + self:_record("apply_operator", { + operator = self._pending_operator, + descriptor = descriptor.value, + origin = origin, + endpoint = destination, + position = self._cursor, + }) + return true +end + +function MemoryHost:apply_cursor(position, motion) position = domain.Position.coerce(position) local previous = self._cursor + local descriptor = motion_descriptor(motion) self._cursor = position - self:_record("apply_cursor", { position = position }) + self:_record("apply_cursor", { + position = position, + descriptor = descriptor and descriptor.value or nil, + }) + if descriptor ~= nil then + local origin = type(motion) == "table" and motion.origin or previous + self:_apply_pending_delete(domain.Position.coerce(origin), position, descriptor) + end self:_emit_movement_event(previous) end diff --git a/tests/run.lua b/tests/run.lua index 393d51a..1c07824 100644 --- a/tests/run.lua +++ b/tests/run.lua @@ -3537,6 +3537,75 @@ test("Forward operator motions toggle characterwise inclusivity", function() end end) +test("Operator execution preserves FIND and TILL endpoint conventions", function() + local source = "hoge fuge piye poye" + local cases = { + { + descriptor = "f", + origin = 1, + expected = " fuge piye poye", + cursor = 1, + }, + { + descriptor = "t", + origin = 1, + expected = "e fuge piye poye", + cursor = 1, + }, + { + descriptor = "F", + origin = 19, + expected = "hoge fuge piye", + cursor = 14, + }, + { + descriptor = "T", + origin = 19, + expected = "hoge fuge piyee", + cursor = 14, + }, + } + + for _, case in ipairs(cases) do + fresh_sequence_state() + local host = MemoryHost.new({ + buffer_lines = { source }, + cursor = { line = 1, byte_column = case.origin }, + mode = "no", + pending_operator = "delete", + }) + local plan = motion_plan.build( + target_plan.build(target("e"), matching_policy()), + case.descriptor + ) + local outcome = motion_executor.new(host):execute( + text_topology.from_host(host), + "no", + plan, + 1, + true + ) + + same(domain.ActionKind.MOVEMENT, outcome.kind, case.descriptor) + list_same({ case.expected }, host:read_text():lines()) + same( + domain.Position.new(1, case.cursor), + host:read_cursor(), + case.descriptor + ) + if case.descriptor == "F" or case.descriptor == "T" then + falsy(host:operator_inclusive(), case.descriptor) + local toggles = 0 + for _, operation in ipairs(host:operations()) do + if operation.operation == "set_operator_inclusive" then + toggles = toggles + 1 + end + end + same(0, toggles, case.descriptor) + end + end +end) + for _, item in ipairs(tests) do local ok, failure = xpcall(item.body, debug.traceback) if not ok then |
