@@ -8,6 +8,7 @@ vim.env.NVIM_TEST = 'true'
88local Morph = require ' morph'
99local h = Morph .h
1010local Pos00 = Morph .Pos00
11+ local Extmark = Morph .Extmark
1112
1213---- ----------------------------------------------------------------------------
1314-- TEST HELPERS
@@ -1280,6 +1281,77 @@ describe('Morph', function()
12801281 end )
12811282 end )
12821283 end )
1284+
1285+ it (' handles inverted extmark positions gracefully' , function ()
1286+ -- This test verifies that extmark positions becoming inverted
1287+ -- (start > stop) is handled gracefully by returning empty string.
1288+ -- The error message was:
1289+ -- (morph.nvim:getregion:invalid-pos) { start, end } = { { 9, 4, 1 }, { 9, 3, 9 } }
1290+ -- Note how line 4 > line 3, which is inverted.
1291+ with_buf ({ ' line1' , ' line2' , ' line3' }, function ()
1292+ -- Create an Extmark directly with inverted positions (start > stop)
1293+ -- This simulates what happens when extmarks get corrupted
1294+ local extmark = {
1295+ bufnr = 0 ,
1296+ ns = vim .api .nvim_create_namespace ' test' ,
1297+ -- Inverted: start at row 2, stop at row 1 (0-based)
1298+ -- This is the bug condition
1299+ start = Pos00 .new (2 , 0 ),
1300+ stop = Pos00 .new (1 , 5 ),
1301+ }
1302+ setmetatable (extmark , { __index = Extmark })
1303+
1304+ -- Calling _text() with inverted positions should return empty string
1305+ local result = extmark :_text ()
1306+
1307+ assert .are .equal (
1308+ ' ' ,
1309+ result ,
1310+ ' Extmark:_text() should handle inverted positions gracefully (return empty string), but got: '
1311+ .. vim .inspect (result )
1312+ )
1313+ end )
1314+ end )
1315+ end )
1316+
1317+ -- Bug: Deleting trailing blank line causes getregion invalid-pos error
1318+ -- Error: (morph.nvim:getregion:invalid-pos) { start, end } = { { 9, 4, 1 }, { 9, 3, 9 } }
1319+ --
1320+ -- Root cause: Extmark._from_raw clamps stop but NOT start.
1321+ -- When an extmark's start is past buffer end after deletion, we get start > stop.
1322+
1323+ it (' clamps extmark start position to buffer bounds' , function ()
1324+ with_buf ({}, function ()
1325+ local ns = vim .api .nvim_create_namespace ' test_extmark_clamp'
1326+
1327+ -- Buffer: 4 lines (0-3)
1328+ vim .api .nvim_buf_set_lines (0 , 0 , - 1 , true , { ' line 0' , ' line 1' , ' line 2' , ' ' })
1329+ assert .are .same (4 , line_count ())
1330+
1331+ -- Extmark at line 3 (the last line)
1332+ local ext_id = vim .api .nvim_buf_set_extmark (0 , ns , 3 , 0 , { end_row = 3 , end_col = 0 })
1333+
1334+ -- Delete the last line → buffer now has 3 lines (0-2)
1335+ vim .api .nvim_buf_set_lines (0 , 3 , 4 , true , {})
1336+ assert .are .same (3 , line_count ())
1337+
1338+ -- Neovim returns extmark with stale position (line 3 doesn't exist)
1339+ local raw = vim .api .nvim_buf_get_extmark_by_id (0 , ns , ext_id , { details = true })
1340+ local start_row , start_col , details = raw [1 ], raw [2 ], raw [3 ]
1341+
1342+ -- BUG: _from_raw clamps stop to buffer bounds but NOT start
1343+ local extmark = Extmark ._from_raw (0 , ns , ext_id , start_row , start_col , details )
1344+
1345+ -- start should be clamped to valid buffer line
1346+ local last_line_idx = line_count () - 1
1347+ assert .is_true (
1348+ extmark .start [1 ] <= last_line_idx ,
1349+ (' extmark.start[1] (%d) should be <= last_line_idx (%d)' ):format (
1350+ extmark .start [1 ],
1351+ last_line_idx
1352+ )
1353+ )
1354+ end )
12831355 end )
12841356
12851357 ---- --------------------------------------------------------------------------
0 commit comments