summaryrefslogtreecommitdiff
path: root/tests
diff options
context:
space:
mode:
Diffstat (limited to 'tests')
-rw-r--r--tests/run.lua288
1 files changed, 287 insertions, 1 deletions
diff --git a/tests/run.lua b/tests/run.lua
index 2eb49b3..1cf9053 100644
--- a/tests/run.lua
+++ b/tests/run.lua
@@ -11,6 +11,7 @@ local capabilities = require("clever_f.capabilities")
local policy = require("clever_f.policy")
local sequence_state = require("clever_f.sequence_state")
local state_transitions = require("clever_f.state_transitions")
+local text_topology = require("clever_f.text_topology")
local MemoryHost = require("clever_f.testing.memory_host")
local tests = {}
@@ -1197,6 +1198,291 @@ test("ClearAllLandingsAndDirection retains movement initialization", function()
same(domain.Descriptor.TILL_FORWARD, state:get_previous_descriptor("v"))
end)
+local function collect_iteration(iterator)
+ local positions = {}
+ local characters = {}
+ local spans = {}
+ while true do
+ local position, character, span = iterator()
+ if position == nil then
+ break
+ end
+ positions[#positions + 1] = position
+ characters[#characters + 1] = character
+ spans[#spans + 1] = span
+ end
+ return positions, characters, spans
+end
+
+local function position_strings(positions)
+ local result = {}
+ for index, position in ipairs(positions) do
+ result[index] = tostring(position)
+ end
+ return result
+end
+
+local MIXED_JAPANESE = "A\227\129\130\239\189\178\230\188\162B"
+local MIXED_CHARACTERS = {
+ "A",
+ "\227\129\130",
+ "\239\189\178",
+ "\230\188\162",
+ "B",
+}
+
+test("TextView indexes editor characters in each required encoding", function()
+ local cases = {
+ {
+ encoding = "utf-8",
+ starts = { 1, 2, 5, 8, 11 },
+ lengths = { 1, 3, 3, 3, 1 },
+ line_length = 11,
+ },
+ {
+ encoding = "cp932",
+ starts = { 1, 2, 4, 5, 7 },
+ lengths = { 1, 2, 1, 2, 1 },
+ line_length = 7,
+ },
+ {
+ encoding = "euc-jp",
+ starts = { 1, 2, 4, 6, 8 },
+ lengths = { 1, 2, 2, 2, 1 },
+ line_length = 8,
+ },
+ }
+
+ for _, case in ipairs(cases) do
+ local view = text_topology.new({ MIXED_JAPANESE }, case.encoding)
+ same(case.encoding, view.encoding)
+ same(1, view.line_count)
+ same(5, view:line_character_count(1))
+ same(case.line_length, view:line_byte_length(1))
+ same(case.line_length, #view:line_encoded_text(1))
+
+ for character_index = 1, #MIXED_CHARACTERS do
+ local column = view:byte_column_for_character_index(1, character_index)
+ same(case.starts[character_index], column)
+ same(
+ character_index,
+ view:character_index_for_byte_column(1, column)
+ )
+ same(MIXED_CHARACTERS[character_index], view:character_at(1, column))
+ local span = view:byte_span_for_character_index(1, character_index)
+ same(case.lengths[character_index], span.byte_length)
+ same(column, span.byte_start)
+ same(column + span.byte_length - 1, span.byte_end)
+ truthy(view:is_character_start(span.position))
+ end
+ end
+
+ local ascii = text_topology.new({ "plain" }, "utf8")
+ same("utf-8", ascii.encoding)
+ same(5, ascii:line_byte_length(1))
+ same(5, ascii:line_character_count(1))
+ for index = 1, 5 do
+ same(index, ascii:character_index_to_byte_column(1, index))
+ same(index, ascii:byte_column_to_character_index(1, index))
+ end
+end)
+
+test("TextView rejects interior and boundary bytes as character starts", function()
+ local view = text_topology.new({ MIXED_JAPANESE }, "utf-8")
+ local valid = {
+ [1] = true,
+ [2] = true,
+ [5] = true,
+ [8] = true,
+ [11] = true,
+ }
+
+ for column = 1, view:line_byte_length(1) do
+ same(valid[column] == true, view:is_character_start(1, column))
+ if not valid[column] then
+ same(nil, view:try_character_index_for_byte_column(1, column))
+ fails(function()
+ view:character_index_for_byte_column(1, column)
+ end, "inside an editor character")
+ end
+ end
+
+ falsy(view:is_character_start(1, 12))
+ fails(function()
+ view:character_index_for_byte_column(1, 12)
+ end, "does not identify")
+ fails(function()
+ view:byte_column_for_character_index(1, 0)
+ end, "character_index")
+end)
+
+test("TextView treats Nvim grapheme clusters as editor characters", function()
+ local combining = "e\204\129x"
+ local view = text_topology.new({ combining }, "utf-8")
+
+ same(2, view:line_character_count(1))
+ same(4, view:line_byte_length(1))
+ same(1, view:byte_column_for_character_index(1, 1))
+ same(4, view:byte_column_for_character_index(1, 2))
+ same("e\204\129", view:character_at_index(1, 1))
+ falsy(view:is_character_start(1, 2))
+ falsy(view:is_character_start(1, 3))
+end)
+
+test("Forward and backward iteration enumerate reverse character starts", function()
+ local view = text_topology.new({
+ "a\227\129\130",
+ "",
+ "\239\189\178z",
+ }, "utf-8")
+
+ local forward, characters, spans = collect_iteration(view:iter_buffer_forward())
+ list_same({ "(1,1)", "(1,2)", "(3,1)", "(3,4)" }, position_strings(forward))
+ list_same({ "a", "\227\129\130", "\239\189\178", "z" }, characters)
+ for index, position in ipairs(forward) do
+ truthy(view:is_character_start(position))
+ same(position, spans[index].position)
+ end
+
+ local backward = collect_iteration(view:iter_buffer_backward())
+ list_same({ "(3,4)", "(3,1)", "(1,2)", "(1,1)" }, position_strings(backward))
+
+ local line_forward = collect_iteration(view:iter_line_forward(1))
+ local line_backward = collect_iteration(view:iter_line_backward(1))
+ list_same({ "(1,1)", "(1,2)" }, position_strings(line_forward))
+ list_same({ "(1,2)", "(1,1)" }, position_strings(line_backward))
+end)
+
+test("Bounded iterators include and stop at their selected boundaries", function()
+ local view = text_topology.new({ "ab", "cd", "ef" }, "utf-8")
+ local first = domain.Position.new(1, 2)
+ local last = domain.Position.new(3, 1)
+
+ local forward = collect_iteration(view:iter_forward(first, last))
+ list_same({ "(1,2)", "(2,1)", "(2,2)", "(3,1)" }, position_strings(forward))
+ local backward = collect_iteration(view:iter_backward(last, first))
+ list_same({ "(3,1)", "(2,2)", "(2,1)", "(1,2)" }, position_strings(backward))
+
+ fails(function()
+ view:iter_forward(last, first)
+ end, "must not follow")
+ fails(function()
+ view:iter_backward(first, last)
+ end, "must not precede")
+ fails(function()
+ view:iter_forward(domain.Position.new(1, 3), last)
+ end, "must start an editor character")
+end)
+
+test("Strict-side iteration obeys line and buffer match-start bounds", function()
+ local view = text_topology.new({ "abc", "", "def" }, "utf-8")
+ local origin = domain.Position.new(1, 2)
+
+ local line_forward = collect_iteration(view:iter_strict_forward(
+ origin,
+ domain.SearchScope.CURRENT_LINE
+ ))
+ list_same({ "(1,3)" }, position_strings(line_forward))
+
+ local buffer_forward = collect_iteration(view:iter_strict_forward(
+ origin,
+ domain.SearchScope.BUFFER
+ ))
+ list_same({ "(1,3)", "(3,1)", "(3,2)", "(3,3)" }, position_strings(buffer_forward))
+
+ local line_backward = collect_iteration(view:iter_strict_backward(
+ origin,
+ "current_line"
+ ))
+ list_same({ "(1,1)" }, position_strings(line_backward))
+
+ local buffer_backward = collect_iteration(view:iter_strict_backward(
+ domain.Position.new(3, 2),
+ "buffer"
+ ))
+ list_same({ "(3,1)", "(1,3)", "(1,2)", "(1,1)" }, position_strings(buffer_backward))
+
+ local empty_line_bounds = view:line_match_start_bounds(2)
+ truthy(empty_line_bounds.empty)
+ same(nil, empty_line_bounds.first)
+ same(nil, empty_line_bounds.last)
+ same(0, #position_strings(collect_iteration(view:iter_line_forward(2))))
+ same(0, #position_strings(collect_iteration(view:iter_strict_forward(
+ domain.Position.new(2, 1),
+ "current_line"
+ ))))
+
+ local buffer_bounds = view:match_start_bounds("buffer")
+ falsy(buffer_bounds.empty)
+ same(domain.Position.new(1, 1), buffer_bounds.first)
+ same(domain.Position.new(3, 3), buffer_bounds.last)
+ truthy(buffer_bounds:contains(domain.Position.new(2, 1)))
+end)
+
+test("Endpoint adjacency crosses lines and normalizes cursor boundaries", function()
+ local view = text_topology.new({
+ "a\227\129\130",
+ "",
+ "\239\189\178z",
+ }, "utf-8")
+
+ same(nil, view:predecessor(domain.Position.new(1, 1)))
+ same(domain.Position.new(1, 1), view:predecessor(domain.Position.new(1, 2)))
+ same(domain.Position.new(2, 1), view:successor(domain.Position.new(1, 2)))
+ same(domain.Position.new(1, 2), view:predecessor(domain.Position.new(2, 1)))
+ same(domain.Position.new(3, 1), view:successor(domain.Position.new(2, 1)))
+ same(domain.Position.new(2, 1), view:predecessor(domain.Position.new(3, 1)))
+ same(domain.Position.new(3, 4), view:successor(domain.Position.new(3, 1)))
+ same(nil, view:successor(domain.Position.new(3, 4)))
+
+ same(domain.Position.new(1, 2), view:normalize_endpoint(1, 3))
+ same(domain.Position.new(1, 2), view:normalize_endpoint(1, 5))
+ same(domain.Position.new(2, 1), view:normalize_endpoint(2, 7))
+ truthy(view:is_valid_cursor_position(view:normalize_endpoint(1, 3)))
+ truthy(view:is_valid_cursor_position(view:normalize_endpoint(2, 7)))
+
+ fails(function()
+ view:successor(domain.Position.new(1, 3))
+ end, "inside an editor character")
+ fails(function()
+ view:predecessor(domain.Position.new(2, 2))
+ end, "empty line")
+end)
+
+test("TextView creation reads one operation-local snapshot", function()
+ local host = MemoryHost.new({
+ buffer_lines = { "ab" },
+ effective_encoding = "utf-8",
+ })
+ host:clear_operations()
+ local first = text_topology.from_host(host)
+ local operations = host:operations()
+ same(2, #operations)
+ same("read_text", operations[1].operation)
+ same("read_encoding", operations[2].operation)
+
+ host:set_text({ "xyz" })
+ local second = text_topology.from_host(host)
+ same("ab", first:line_text(1))
+ same(2, first:line_character_count(1))
+ same("xyz", second:line_text(1))
+ same(3, second:line_character_count(1))
+ falsy(first == second)
+ fails(function()
+ first.encoding = "cp932"
+ end, "immutable")
+end)
+
+test("All-empty buffers expose empty full-buffer iteration", function()
+ local view = text_topology.new({ "", "", "" }, "utf-8")
+ local bounds = view:buffer_match_start_bounds()
+ truthy(bounds:is_empty())
+ same(nil, bounds.first)
+ same(nil, bounds.last)
+ same(0, #position_strings(collect_iteration(view:iter_buffer_forward())))
+ same(0, #position_strings(collect_iteration(view:iter_buffer_backward())))
+end)
+
for _, item in ipairs(tests) do
local ok, failure = xpcall(item.body, debug.traceback)
if not ok then
@@ -1206,4 +1492,4 @@ for _, item in ipairs(tests) do
passed = passed + 1
end
-io.stdout:write(string.format("Phase 4: %d tests passed\n", passed))
+io.stdout:write(string.format("Phase 5: %d tests passed\n", passed))