Skip to content

Commit cfa40b7

Browse files
krobelusmawww
authored andcommitted
Stop apply_diff() from creating non-monotonic changes
Commit ba41928 (Remove spurious newline when | replaces at buffer end, 2024-11-28) also tried to fix a "DiffOp::Add" code path. Unless I'm misremembering, I had run into a crash and extracted this unit test. When apply_diff() decides to erase the last line, and then insert something at the end, buffer changes might look like: {.type=Erase, .begin={2, 0}, .end={3, 0}} {.type=Insert, .begin={1, 5}, .end={5, 0}} The "{1, 5}" is caused by "pos = buffer.prev(pos);" added by the aforementioned commit. It's problematic because it breaks the caller's "ForwardChangesTracker" invariant: the previous Erase leaves the cursor at {2, 0}, so the insert cannot start before that. Additionally, the inserted range returned by apply_diff() would be "{ {2, 0}, {4, 0} }" whose end position is weirdly different from the Insert's end position. It questionable that we are passing on this weird state. The "pos = buffer.prev(pos);" statement was added so we actually delete the current final newline. We still leave "tried_to_erase_final_newline" set, meaning we'll also delete the the final newline after we're done. But deleting the "current final newline" the way we do it is confusing, because we do it at every step DiffOp::Add step. This would blow up if there are multiple successive contiguous DiffOp::Add events. I doubt this ever happens but it's still confusing. Remove this logic, and restore historical behavior in case we append at the buffer end. This does not break the system test added by ba41928 because in that case, we 1. first delete the entire buffer, setting pos={0, 0}, but also restoring the EOL invariant, so the end position would be {0, 1}. 2. then we insert at pos={0, 0}, and since "buffer.is_end(pos)" is false we don't run into this bad code path. While at it add an assertion to clarify that we already assume that the diff algorithm never gives us empty DiffOp::Keep, and always monotonically increasing positions, except for the very special case where we deleted until buffer end, which should already be handled. The unit test is not perfect because it depends on the current behavior of the diff algorithm.
1 parent 7919240 commit cfa40b7

File tree

2 files changed

+75
-2
lines changed

2 files changed

+75
-2
lines changed

src/buffer.hh

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -224,6 +224,10 @@ public:
224224
Type type;
225225
BufferCoord begin;
226226
BufferCoord end;
227+
228+
#ifdef KAK_DEBUG
229+
bool operator==(const Change&) const = default;
230+
#endif
227231
};
228232
ConstArrayView<Change> changes_since(size_t timestamp) const;
229233

src/normal.cc

Lines changed: 71 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
#include "normal.hh"
22

3+
#include "array_view.hh"
4+
#include "assert.hh"
35
#include "buffer.hh"
46
#include "buffer_manager.hh"
57
#include "buffer_utils.hh"
@@ -20,6 +22,7 @@
2022
#include "selectors.hh"
2123
#include "shell_manager.hh"
2224
#include "string.hh"
25+
#include "units.hh"
2326
#include "user_interface.hh"
2427
#include "unit_tests.hh"
2528
#include "window.hh"
@@ -555,19 +558,21 @@ BufferRange apply_diff(Buffer& buffer, BufferCoord pos, ArrayView<StringView> li
555558
switch (op)
556559
{
557560
case DiffOp::Keep:
561+
kak_assert(not tried_to_erase_final_newline);
558562
pos = buffer.advance(pos, byte_count(lines_before, posA, len));
559563
posA += len;
560564
posB += len;
561565
break;
562566
case DiffOp::Add:
563-
if (buffer.is_end(pos) and tried_to_erase_final_newline)
564-
pos = buffer.prev(pos);
567+
if (buffer.is_end(pos))
568+
tried_to_erase_final_newline = false;
565569
pos = buffer.insert(pos, {lines_after[posB].begin(),
566570
lines_after[posB + len - 1].end()}).end;
567571
posB += len;
568572
break;
569573
case DiffOp::Remove:
570574
{
575+
kak_assert(not tried_to_erase_final_newline);
571576
BufferCoord end = buffer.advance(pos, byte_count(lines_before, posA, len));
572577
tried_to_erase_final_newline |= buffer.is_end(end);
573578
pos = buffer.erase(pos, end);
@@ -584,6 +589,70 @@ BufferRange apply_diff(Buffer& buffer, BufferCoord pos, ArrayView<StringView> li
584589
return {first, pos};
585590
}
586591

592+
UnitTest test_apply_diff{[] {
593+
using Change = Buffer::Change;
594+
auto validate = [&](LineCount line,
595+
StringView new_text,
596+
BufferRange expected_new_range,
597+
ConstArrayView<Change> expected_buffer_changes)
598+
{
599+
Buffer buffer{"", Buffer::Flags::None, {
600+
StringData::create("line1\n"),
601+
StringData::create("line2\n"),
602+
StringData::create("line3\n"),
603+
}};
604+
const size_t timestamp = buffer.timestamp();
605+
Vector<StringView> old_text;
606+
for (auto i = line; i < buffer.line_count(); i++)
607+
old_text.push_back(buffer[i]);
608+
BufferRange new_text_range = apply_diff(buffer, BufferCoord{line, 0}, old_text, new_text);
609+
kak_assert(new_text_range == expected_new_range);
610+
kak_assert(buffer.changes_since(timestamp) == expected_buffer_changes);
611+
};
612+
// When appending at end, we add any missing newline
613+
validate(
614+
/*line=*/3,
615+
"added-line3-missing-eol",
616+
BufferRange{{3, 0}, {4, 0}},
617+
{
618+
Change{Change::Insert, {3, 0}, {4, 0}},
619+
}
620+
);
621+
// Special case: erasing until buffer end also erases the final newline.
622+
validate(
623+
/*line=*/2,
624+
"",
625+
BufferRange{{1, 5}, {1, 5}},
626+
{
627+
Change{Change::Erase, {2, 0}, {3, 0}},
628+
}
629+
);
630+
// Erasing and appending at end still produces forward-only changes.
631+
validate(
632+
/*line=*/2,
633+
"changed-line3\n"
634+
"added-line4\n"
635+
"added-line5\n",
636+
BufferRange{{2, 0}, {5, 0}},
637+
{
638+
Change{Change::Erase, {2, 0}, {3, 0}},
639+
Change{Change::Insert, {2, 0}, {5, 0}},
640+
}
641+
);
642+
// Same result when append is missing newline.
643+
validate(
644+
/*line=*/2,
645+
"changed-line3\n"
646+
"added-line4\n"
647+
"added-line5-missing-eol",
648+
BufferRange{{2, 0}, {5, 0}},
649+
{
650+
Change{Change::Erase, {2, 0}, {3, 0}},
651+
Change{Change::Insert, {2, 0}, {5, 0}},
652+
}
653+
);
654+
}};
655+
587656
template<bool replace>
588657
void pipe(Context& context, NormalParams params)
589658
{

0 commit comments

Comments
 (0)