Skip to content

Commit 7d2fb93

Browse files
committed
Merge nab-mark-undo (mark undo lifecycle fixes)
2 parents ce26265 + 5ebfc19 commit 7d2fb93

5 files changed

Lines changed: 349 additions & 54 deletions

File tree

generic/tkText.c

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5594,7 +5594,10 @@ TextUndoRedoCallback(
55945594
*/
55955595
((TkTextUndoSubAtom *) subAtom)->item = NULL;
55965596
}
5597-
TkTextPushUndoToken(sharedTextPtr, redoInfo.token, redoInfo.byteSize);
5597+
if (redoInfo.token) {
5598+
/* A replay may be a no-op (the mark it acts on is gone). */
5599+
TkTextPushUndoToken(sharedTextPtr, redoInfo.token, redoInfo.byteSize);
5600+
}
55985601
}
55995602
if (!isDelete && sharedTextPtr->triggerWatchCmd) {
56005603
TriggerWatchUndoRedo(sharedTextPtr, token, subAtom->redo, i == 0, peers, countPeers);

generic/tkText.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1694,6 +1694,7 @@ typedef enum {
16941694
#define DELETE_INCLUSIVE (1 << 3)
16951695
#define DELETE_CLEANUP (1 << 4)
16961696
#define DELETE_LASTLINE (1 << 5)
1697+
#define DELETE_RELEASE (1 << 6)
16971698

16981699
/*
16991700
* Flags for sorting.

generic/tkTextBTree.c

Lines changed: 35 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -1768,7 +1768,14 @@ UndoDeleteDestroy(
17681768
assert(segPtr->typePtr->deleteProc);
17691769

17701770
nextPtr = (segPtr->nextPtr && !segPtr->sectionPtr) ? segPtr->nextPtr : NULL;
1771-
segPtr->typePtr->deleteProc(sharedTextPtr, segPtr, DELETE_BRANCHES | DELETE_MARKS);
1771+
/*
1772+
* The chain may hold references to segments still living
1773+
* inside the tree (relocated marks): DELETE_RELEASE makes
1774+
* the delete procs release the reference instead of
1775+
* deleting the segment behind the back of the tree.
1776+
*/
1777+
segPtr->typePtr->deleteProc(sharedTextPtr, segPtr,
1778+
DELETE_BRANCHES | DELETE_MARKS | DELETE_RELEASE);
17721779
segPtr = nextPtr;
17731780
}
17741781
}
@@ -5296,38 +5303,28 @@ TkBTreeInsertChars(
52965303

52975304
static void
52985305
MakeUndoIndex(
5299-
const TkSharedText *sharedTextPtr,
5306+
TCL_UNUSED(const TkSharedText *),
53005307
const TkTextIndex *indexPtr, /* Convert this index. */
53015308
TkTextUndoIndex *undoIndexPtr, /* Pointer to resulting index. */
5302-
int gravity) /* +1 = right gravity, -1 = left gravity */
5309+
TCL_UNUSED(int)) /* +1 = right gravity, -1 = left gravity */
53035310
{
5304-
TkTextSegment *segPtr;
5305-
53065311
assert(indexPtr);
5307-
assert(gravity == GRAVITY_LEFT || gravity == GRAVITY_RIGHT);
53085312

53095313
/*
5310-
* At first, try to find a neighboring mark segment at the same byte
5311-
* index, but we cannot use the special marks "insert" and "current",
5312-
* and we cannot not use private marks.
5314+
* A neighboring mark used to be recorded as the anchor of this index
5315+
* ("right behind this mark"), because a mark is stable enough to act
5316+
* as a predecessor. But the anchor was a bare pointer: nothing kept
5317+
* the mark alive nor linked, and any interleaving which unlinks it
5318+
* (deletion of the marks of a range, unset, an homonym superseding
5319+
* it) left the index dangling - the resolution then answered with a
5320+
* wrong position, or dereferenced an unlinked segment. The positional
5321+
* form below is the one the widget uses without steady marks, and it
5322+
* is valid at replay time as well: the stack is replayed in reverse
5323+
* order, so the line and byte index recorded here still designate the
5324+
* same place. Only the order among zero-sized segments sharing that
5325+
* position is not preserved anymore.
53135326
*/
53145327

5315-
if (sharedTextPtr->steadyMarks
5316-
&& (segPtr = TkTextIndexGetSegment(indexPtr))
5317-
&& segPtr->typePtr->group == SEG_GROUP_MARK) {
5318-
TkTextSegment *searchPtr = (gravity == GRAVITY_LEFT) ? segPtr->prevPtr : segPtr->nextPtr;
5319-
5320-
while (searchPtr && TkTextIsSpecialOrPrivateMark(searchPtr)) {
5321-
searchPtr = (gravity == GRAVITY_LEFT) ? searchPtr->prevPtr : searchPtr->nextPtr;
5322-
}
5323-
5324-
if (searchPtr && TkTextIsStableMark(searchPtr)) {
5325-
undoIndexPtr->u.markPtr = searchPtr;
5326-
undoIndexPtr->lineIndex = -1;
5327-
return;
5328-
}
5329-
}
5330-
53315328
undoIndexPtr->lineIndex = TkTextIndexGetLineNumber(indexPtr, NULL);
53325329
undoIndexPtr->u.byteIndex = TkTextIndexGetByteIndex(indexPtr);
53335330
}
@@ -8527,28 +8524,19 @@ DeleteIndexRange(
85278524
redoToken->startIndex = undoToken->startIndex;
85288525
redoToken->endIndex = undoToken->endIndex;
85298526
} else {
8530-
if (sharedTextPtr->steadyMarks
8531-
&& segPtr1
8532-
&& TkTextIsStableMark(segPtr1)
8533-
&& !(flags & DELETE_MARKS)) {
8534-
redoToken->startIndex.u.markPtr = segPtr1;
8535-
redoToken->startIndex.lineIndex = -1;
8536-
} else {
8537-
TkTextIndex index = *indexPtr1;
8538-
TkTextIndexSetSegment(&index, firstPtr);
8539-
MakeUndoIndex(sharedTextPtr, &index, &redoToken->startIndex, GRAVITY_LEFT);
8540-
}
8541-
if (sharedTextPtr->steadyMarks
8542-
&& segPtr2
8543-
&& TkTextIsStableMark(segPtr2)
8544-
&& !(flags & DELETE_MARKS)) {
8545-
redoToken->endIndex.u.markPtr = segPtr2;
8546-
redoToken->endIndex.lineIndex = -1;
8547-
} else {
8548-
TkTextIndex index = *indexPtr2;
8549-
TkTextIndexSetSegment(&index, lastPtr);
8550-
MakeUndoIndex(sharedTextPtr, &index, &redoToken->endIndex, GRAVITY_RIGHT);
8551-
}
8527+
TkTextIndex index = *indexPtr1;
8528+
8529+
/*
8530+
* The boundary marks were recorded as anchors here, see the
8531+
* remarks in MakeUndoIndex: a bare pointer to a mark does not
8532+
* survive the interleavings of the undo stack.
8533+
*/
8534+
8535+
TkTextIndexSetSegment(&index, firstPtr);
8536+
MakeUndoIndex(sharedTextPtr, &index, &redoToken->startIndex, GRAVITY_LEFT);
8537+
index = *indexPtr2;
8538+
TkTextIndexSetSegment(&index, lastPtr);
8539+
MakeUndoIndex(sharedTextPtr, &index, &redoToken->endIndex, GRAVITY_RIGHT);
85528540
}
85538541
redoInfo->token = (TkTextUndoToken *) redoToken;
85548542
redoInfo->byteSize = 0;

generic/tkTextMark.c

Lines changed: 129 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -289,6 +289,50 @@ UndoToggleGravityInspect(
289289
return AppendName(UndoToggleGravityGetCommand(sharedTextPtr, item), sharedTextPtr, token->markPtr);
290290
}
291291

292+
/*
293+
*----------------------------------------------------------------------
294+
*
295+
* ReplayTarget --
296+
*
297+
* Find the mark a token has to act on. The referenced mark may have
298+
* been unlinked meanwhile (deleted by [delete -marks], unset, or
299+
* superseded by a living homonym): in this case the living mark of
300+
* the same name is the target, replaying the operation on it - this
301+
* is the same adoption the redo of a [mark set] performs.
302+
*
303+
* Results:
304+
* The mark to act on, or NULL if the name is not alive anymore; in
305+
* the latter case the token has to be a no-op.
306+
*
307+
* Side effects:
308+
* None.
309+
*
310+
*----------------------------------------------------------------------
311+
*/
312+
313+
static TkTextSegment *
314+
ReplayTarget(
315+
const TkSharedText *sharedTextPtr, /* Handle to shared text resource. */
316+
TkTextSegment *markPtr) /* The mark referenced by the token. */
317+
{
318+
Tcl_HashEntry *hPtr;
319+
320+
assert(markPtr);
321+
assert(TkTextIsNormalMark(markPtr));
322+
323+
if (markPtr->sectionPtr) {
324+
return markPtr;
325+
}
326+
if (!IS_PRESERVED(markPtr)) {
327+
return NULL;
328+
}
329+
if (!(hPtr = Tcl_FindHashEntry(&((TkSharedText *) sharedTextPtr)->markTable,
330+
GET_NAME(markPtr)))) {
331+
return NULL;
332+
}
333+
return (TkTextSegment *)Tcl_GetHashValue(hPtr);
334+
}
335+
292336
static void
293337
UndoToggleGravityPerform(
294338
TkSharedText *sharedTextPtr,
@@ -299,12 +343,17 @@ UndoToggleGravityPerform(
299343
UndoTokenToggleGravity *token = (UndoTokenToggleGravity *) undoInfo->token;
300344
const Tk_SegType *newTypePtr;
301345
const Tk_SegType *oldTypePtr;
346+
TkTextSegment *markPtr;
302347

303348
assert(!token->markPtr->body.mark.changePtr);
304349

305-
oldTypePtr = token->markPtr->typePtr;
350+
if (!(markPtr = ReplayTarget(sharedTextPtr, token->markPtr))) {
351+
return; /* the mark is gone, nothing to toggle */
352+
}
353+
354+
oldTypePtr = markPtr->typePtr;
306355
newTypePtr = (oldTypePtr == &tkTextRightMarkType) ? &tkTextLeftMarkType : &tkTextRightMarkType;
307-
ChangeGravity(sharedTextPtr, NULL, token->markPtr, newTypePtr, NULL);
356+
ChangeGravity(sharedTextPtr, NULL, markPtr, newTypePtr, NULL);
308357

309358
if (redoInfo) {
310359
redoInfo->token = undoInfo->token;
@@ -335,24 +384,29 @@ UndoMoveMarkPerform(
335384
{
336385
UndoTokenMoveMark *token = (UndoTokenMoveMark *) undoInfo->token;
337386
TkTextUndoIndex index = token->index;
387+
TkTextSegment *markPtr;
338388

339389
assert(!token->markPtr->body.mark.changePtr);
340390

391+
if (!(markPtr = ReplayTarget(sharedTextPtr, token->markPtr))) {
392+
return; /* the mark is gone, nothing to move */
393+
}
394+
341395
if (redoInfo) {
342396
TkTextUndoIndex redoIndex;
343397

344398
/*
345399
* Don't clobber 'index': the mark must be re-inserted at the saved
346400
* position, whereas the redo token receives the current one.
347401
*/
348-
TkBTreeMakeUndoIndex(sharedTextPtr, token->markPtr, &redoIndex);
402+
TkBTreeMakeUndoIndex(sharedTextPtr, markPtr, &redoIndex);
349403
token->index = redoIndex;
350404
redoInfo->token = undoInfo->token;
351405
redoInfo->token->undoType = isRedo ? &undoTokenMoveMarkType : &redoTokenMoveMarkType;
352406
}
353407

354-
TkBTreeUnlinkSegment(sharedTextPtr, token->markPtr);
355-
TkBTreeReInsertSegment(sharedTextPtr, &index, token->markPtr);
408+
TkBTreeUnlinkSegment(sharedTextPtr, markPtr);
409+
TkBTreeReInsertSegment(sharedTextPtr, &index, markPtr);
356410
}
357411

358412
static void
@@ -412,6 +466,10 @@ UndoSetMarkPerform(
412466
const UndoTokenSetMark *token = (const UndoTokenSetMark *) undoInfo->token;
413467
TkTextSegment *markPtr = (TkTextSegment *)GET_POINTER(token->markPtr);
414468

469+
if (!(markPtr = ReplayTarget(sharedTextPtr, markPtr))) {
470+
return; /* the name is gone, nothing to unset */
471+
}
472+
415473
assert(!markPtr->body.mark.changePtr);
416474
UnsetMark(sharedTextPtr, markPtr, redoInfo);
417475
if (redoInfo && !isRedo) {
@@ -447,8 +505,35 @@ RedoSetMarkPerform(
447505
assert(TkTextIsNormalMark(markPtr));
448506

449507
if (IS_PRESERVED(markPtr)) {
508+
Tcl_HashEntry *hPtr = Tcl_FindHashEntry(&sharedTextPtr->markTable, GET_NAME(markPtr));
509+
510+
if (hPtr) {
511+
/*
512+
* The name is alive again, undo/redo interleavings can revive an
513+
* homonym (the redo of a deletion does not delete the marks
514+
* again). The mark of the token owns the name back, with its
515+
* recorded gravity and position: unset the homonym, without
516+
* recording - the further undoes revive it through the chain of
517+
* the deletion which preserved it, and its restoration then
518+
* finds the name taken (MarkRestoreProc drops the duplicate).
519+
*/
520+
521+
TkTextSegment *livingPtr = (TkTextSegment *)Tcl_GetHashValue(hPtr);
522+
523+
assert(livingPtr != markPtr);
524+
assert(TkTextIsNormalMark(livingPtr));
525+
UnsetMark(sharedTextPtr, livingPtr, NULL);
526+
}
527+
450528
ReactivateMark(sharedTextPtr, markPtr);
451529
sharedTextPtr->numMarks += 1;
530+
} else if (markPtr->sectionPtr) {
531+
/*
532+
* The mark is alive and linked again (an undone deletion has
533+
* restored it): setting an existing mark is a move, so unlink it
534+
* before it is re-inserted at the recorded position.
535+
*/
536+
TkBTreeUnlinkSegment(sharedTextPtr, markPtr);
452537
}
453538

454539
TkBTreeReInsertSegment(sharedTextPtr, &token->index, markPtr);
@@ -1834,6 +1919,24 @@ SetMark(
18341919
}
18351920

18361921
if ((segPtr = TkTextIndexGetSegment(indexPtr)) == markPtr) {
1922+
if (typePtr && typePtr != markPtr->typePtr) {
1923+
/*
1924+
* The index resolves to the mark itself (e.g. [mark set m
1925+
* end right] with the mark heading the last line, or [mark
1926+
* set m m right]), but an explicit direction still has to
1927+
* be applied - it used to be skipped on this path.
1928+
*/
1929+
1930+
TkTextUndoInfo undoInfo;
1931+
TkTextUndoInfo *undoInfoPtr = NULL;
1932+
1933+
if (sharedTextPtr->steadyMarks
1934+
&& TkTextIsNormalMark(markPtr)
1935+
&& !TkTextUndoUndoStackIsFull(sharedTextPtr->undoStack)) {
1936+
undoInfoPtr = &undoInfo;
1937+
}
1938+
ChangeGravity(sharedTextPtr, textPtr, markPtr, typePtr, undoInfoPtr);
1939+
}
18371940
return markPtr;
18381941
}
18391942

@@ -2281,6 +2384,18 @@ MarkDeleteProc(
22812384
return 0;
22822385
}
22832386

2387+
if ((flags & DELETE_RELEASE) && segPtr->sectionPtr) {
2388+
/*
2389+
* The caller destroys a token whose chain referenced this mark, but
2390+
* the mark is still alive inside the tree: release the reference,
2391+
* the mark is not ours to delete - preserving it here would remove
2392+
* its hash entry and leave a preserved segment inside the tree.
2393+
*/
2394+
assert(segPtr->refCount > 1);
2395+
segPtr->refCount -= 1;
2396+
return 1;
2397+
}
2398+
22842399
assert(segPtr->body.mark.ptr);
22852400

22862401
if (segPtr->body.mark.changePtr) {
@@ -2446,7 +2561,15 @@ MarkRestoreProc(
24462561
}
24472562

24482563
hPtr = Tcl_CreateHashEntry(&sharedTextPtr->markTable, GET_NAME(segPtr), &isNew);
2449-
assert(isNew);
2564+
if (!isNew) {
2565+
/*
2566+
* A mark of this name is alive again (undo/redo interleavings
2567+
* can revive an homonym): the living one wins, discard the
2568+
* preserved duplicate like the branch above does.
2569+
*/
2570+
MarkDeleteProc(sharedTextPtr, segPtr, DELETE_CLEANUP);
2571+
return 0;
2572+
}
24502573
Tcl_SetHashValue(hPtr, segPtr);
24512574
sharedTextPtr->numMarks += 1;
24522575
Tcl_Free(GET_NAME(segPtr));

0 commit comments

Comments
 (0)