Skip to content

Commit bcb8044

Browse files
committed
fix bug when extmark positions are inverted/beyond EOF
1 parent 9a73e51 commit bcb8044

2 files changed

Lines changed: 81 additions & 5 deletions

File tree

lua/morph.lua

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -493,11 +493,10 @@ function Extmark._from_raw(bufnr, ns, id, start_row0, start_col0, details)
493493

494494
-- Clamp extmark bounds to actual buffer size (extmarks can overshoot after deletions)
495495
local last_line_idx = math.max(0, vim.api.nvim_buf_line_count(bufnr) - 1)
496-
if extmark.stop[1] > last_line_idx then
497-
local last_line = vim.api.nvim_buf_get_lines(bufnr, last_line_idx, last_line_idx + 1, true)[1]
498-
or ''
499-
extmark.stop = Pos00.new(last_line_idx, #last_line)
500-
end
496+
local last_line = vim.api.nvim_buf_get_lines(bufnr, last_line_idx, last_line_idx + 1, true)[1]
497+
or ''
498+
if extmark.start[1] > last_line_idx then extmark.start = Pos00.new(last_line_idx, #last_line) end
499+
if extmark.stop[1] > last_line_idx then extmark.stop = Pos00.new(last_line_idx, #last_line) end
501500

502501
return extmark
503502
end
@@ -534,6 +533,10 @@ function Extmark:_text()
534533
local start, stop = self.start, self.stop
535534
if start == stop then return '' end
536535

536+
-- Handle inverted positions (start > stop), which can occur after buffer
537+
-- deletions. Return empty string as there's no valid content to extract.
538+
if start > stop then return '' end
539+
537540
-- Handle edge case: if stop is at column 0, we need to include the newline
538541
-- from the previous line, which getregion doesn't handle well
539542
local needs_trailing_newline = false
@@ -1433,6 +1436,7 @@ Morph.Pos00 = Pos00
14331436
if vim.env.NVIM_TEST then
14341437
Morph._is_textlock = is_textlock
14351438
Morph._levenshtein = levenshtein
1439+
Morph.Extmark = Extmark
14361440
end
14371441

14381442
return Morph

spec/morph_spec.lua

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ vim.env.NVIM_TEST = 'true'
88
local Morph = require 'morph'
99
local h = Morph.h
1010
local 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

Comments
 (0)