1 /** 2 Copyright: Copyright (c) 2014-2018 Andrey Penechko. 3 License: $(WEB boost.org/LICENSE_1_0.txt, Boost License 1.0). 4 Authors: Andrey Penechko. 5 */ 6 7 module voxelman.gui.textedit.textbuffer; 8 9 import std.algorithm : equal, min; 10 import std.array : Appender, appender, empty, back, front, popFront; 11 import std.format : formattedWrite; 12 import std.exception : assumeUnique, assertThrown; 13 import std..string : format; 14 import std.range : isForwardRange, isBidirectionalRange, hasSlicing, dropExactly; 15 import std.typecons : Tuple, Yes; 16 import std.traits : isSomeString; 17 import std.uni : byGrapheme; 18 import std.utf : byDchar, count, stride, strideBack, decode, decodeFront; 19 20 import core.exception : AssertError; 21 22 bool equalDchars(S1, S2)(S1 str1, S2 str2) 23 { 24 return equal(str1.byDchar, str2.byDchar); 25 } 26 27 void* shortPtr(void* ptr) 28 { 29 return cast(void*)(cast(uint)ptr >> 4 & 0xFF); 30 //return ptr; 31 } 32 33 // Refers to a text slice in a buffer. 34 private struct Piece 35 { 36 // Offset in code points (bytes for utf-8) from the begining of the buffer. 37 size_t bufferOffset; 38 39 // length in bytes 40 size_t length; 41 42 PieceRef prev; 43 PieceRef next; 44 45 void toString(scope void delegate(const(char)[]) sink) 46 { 47 //sink.formattedWrite("P(O %s L %s P %s N %s)", 48 // bufferOffset, length, shortPtr(prev), shortPtr(next)); 49 sink.formattedWrite("P %s N %s", shortPtr(prev), shortPtr(next)); 50 } 51 } 52 alias PieceRef = Piece*; 53 54 struct PieceRestoreRange 55 { 56 PieceRef first; 57 PieceRef last; 58 59 // length of the whole sequence in bytes 60 size_t sequenceLength; 61 62 // true if need to restore link between first and last 63 // otherwise need to restore link first.prev.next -> first 64 // and last.next.prev -> last 65 bool boundary; 66 67 PieceRestoreRange apply(ref size_t currentSequenceLength) 68 { 69 if (boundary) 70 { 71 auto undoItem = PieceRestoreRange(first.next, last.prev, currentSequenceLength, false); 72 first.next = last; 73 last.prev = first; 74 currentSequenceLength = sequenceLength; 75 return undoItem; 76 } 77 else 78 { 79 PieceRestoreRange undoItem; 80 PieceRef left = first.prev; 81 PieceRef right = last.next; 82 83 if (left.next == right) 84 { 85 // we are inserting range first..last between 86 // left and right, so new sequence is 87 // left - first..last - right 88 // generate Op to connect left and right 89 undoItem = PieceRestoreRange(left, right, currentSequenceLength, true); 90 } 91 else 92 { 93 // we are replacing some sequence with first..last sequence 94 // left - curFirst..curLast - right becomes 95 // left - first..last - right 96 // generate op to insert curFirst..curLast between left and right 97 undoItem = PieceRestoreRange(left.next, right.prev, currentSequenceLength, false); 98 } 99 first.prev.next = first; 100 last.next.prev = last; 101 currentSequenceLength = sequenceLength; 102 return undoItem; 103 } 104 } 105 106 void toString()(scope void delegate(const(char)[]) sink) 107 { 108 sink.formattedWrite("F[%s]%s L[%s]%s B%s G%s", 109 shortPtr(first), *first, shortPtr(last), *last, cast(int)boundary, cast(int)group); 110 } 111 } 112 113 private struct PieceWithPos 114 { 115 PieceRef piece; 116 /// Offset into buffer in bytes 117 size_t pos; 118 119 // Can be used to continue searching for next position. 120 PieceWithPos pieceAt(size_t index) 121 { 122 PieceRef piece = piece; 123 size_t textPosition = pos; 124 125 while (index >= textPosition + piece.length) 126 { 127 textPosition += piece.length; 128 piece = piece.next; 129 } 130 131 return PieceWithPos(piece, textPosition); 132 } 133 } 134 135 private enum Previous {no, yes}; 136 137 private PieceStorage pieceStorage() 138 { 139 PieceStorage storage; 140 storage.sentinel = new Piece; // bug 17740 141 storage.sentinel.next = storage.sentinel; 142 storage.sentinel.prev = storage.sentinel; 143 return storage; 144 } 145 146 private struct PieceStorage 147 { 148 //PieceRef sentinel = new Piece; // bug 17740 149 PieceRef sentinel; 150 151 // length in bytes 152 size_t length; 153 154 void toString(scope void delegate(const(char)[]) sink) 155 { 156 auto piece = sentinel.next; 157 sink.formattedWrite("PieceStorage(S[%s]%s <-> ", shortPtr(sentinel), *sentinel); 158 while(piece != sentinel) 159 { 160 sink.formattedWrite("P[%s]%s <-> ", shortPtr(piece), *piece); 161 piece = piece.next; 162 } 163 sink.formattedWrite("S[%s]%s)", shortPtr(sentinel), *sentinel); 164 } 165 166 // test fresh storage. 167 unittest 168 { 169 PieceStorage storage = pieceStorage(); 170 171 assert((*storage.sentinel).length == 0); 172 assert(storage.length == 0); 173 } 174 175 // Find piece at position index. 176 PieceWithPos pieceAt(size_t index) 177 { 178 assert(index < length); 179 return PieceWithPos(sentinel.next, 0).pieceAt(index); 180 } 181 182 unittest 183 { 184 PieceStorage storage = pieceStorage(); 185 //storage.writeln; 186 187 auto piece1 = new Piece(10, 2); 188 storage.insertBack(piece1); 189 //writefln("piece1 %s", shortPtr(piece1)); 190 //storage.writeln; 191 192 auto piece2 = new Piece(5, 2); 193 storage.insertBack(piece2); 194 //writefln("piece1 %s", shortPtr(piece2)); 195 //storage.writeln; 196 197 auto piece3 = new Piece(1, 2); 198 storage.insertBack(piece3); 199 //writefln("piece1 %s", shortPtr(piece3)); 200 //storage.writeln; 201 202 assert(storage.pieceAt(0) == PieceWithPos(piece1, 0)); 203 assert(storage.pieceAt(1) == PieceWithPos(piece1, 0)); 204 assert(storage.pieceAt(2) == PieceWithPos(piece2, 2)); 205 assert(storage.pieceAt(3) == PieceWithPos(piece2, 2)); 206 assert(storage.pieceAt(4) == PieceWithPos(piece3, 4)); 207 assert(storage.pieceAt(5) == PieceWithPos(piece3, 4)); 208 assert(storage.pieceAt(2).pieceAt(4) == PieceWithPos(piece3, 4)); 209 assertThrown!AssertError(storage.pieceAt(6)); 210 } 211 212 PieceRestoreRange insertFront(PieceRef piece) 213 { 214 assert(piece); 215 return insertAt(piece, 0); 216 } 217 218 unittest 219 { 220 PieceStorage storage = pieceStorage(); 221 222 auto piece1 = new Piece(0, 2); 223 storage.insertFront(piece1); 224 225 assert(storage.sentinel.next == piece1); 226 assert(storage.sentinel.prev == piece1); 227 assert(storage.sentinel == storage.sentinel.next.next); 228 assert(storage.sentinel == storage.sentinel.prev.prev); 229 assert(storage.length == 2); 230 231 auto piece2 = new Piece(4, 2); 232 storage.insertFront(piece2); 233 // sentinel <-> piece2 <-> piece1 <-> sentinel 234 235 assert(storage.sentinel.next == piece2); 236 assert(storage.sentinel.next.next == piece1); 237 assert(storage.sentinel.next.next.next == storage.sentinel); 238 assert(storage.sentinel.prev == piece1); 239 assert(storage.sentinel.prev.prev == piece2); 240 assert(storage.sentinel.prev.prev.prev == storage.sentinel); 241 assert(storage.length == 4); 242 243 assert(piece1.next == storage.sentinel); 244 assert(piece1.prev == piece2); 245 assert(piece2.next == piece1); 246 assert(piece2.prev == storage.sentinel); 247 } 248 249 PieceRestoreRange insertBack(PieceRef piece) 250 { 251 assert(piece); 252 return insertAt(piece, length); 253 } 254 255 unittest 256 { 257 PieceStorage storage = pieceStorage(); 258 259 auto piece1 = new Piece(0, 2); 260 storage.insertBack(piece1); 261 262 assert(storage.sentinel.next == piece1); 263 assert(storage.sentinel.next.next == storage.sentinel); 264 assert(storage.length == 2); 265 // sentinel <-> piece1 <-> sentinel 266 267 auto piece2 = new Piece(4, 2); 268 storage.insertBack(piece2); 269 // sentinel <-> piece1 <-> piece2 <-> sentinel 270 271 assert(storage.sentinel.next == piece1); 272 assert(storage.sentinel.next.next == piece2); 273 assert(storage.length == 4); 274 275 assert(piece1.next == piece2); 276 assert(piece2.prev == piece1); 277 assert(piece2.next == storage.sentinel); 278 assert(piece1.prev == storage.sentinel); 279 } 280 281 PieceRestoreRange insertAt(PieceRef newPiece, size_t insertPos) 282 { 283 assert(newPiece); 284 assert(newPiece.length > 0); 285 286 if (insertPos >= length) // At the end of text 287 { 288 auto last = sentinel.prev; 289 290 PieceRestoreRange restoreRange = PieceRestoreRange(last, sentinel, length, true); 291 292 length += newPiece.length; 293 294 last.next = newPiece; 295 newPiece.prev = last; 296 297 newPiece.next = sentinel; 298 sentinel.prev = newPiece; 299 300 return restoreRange; 301 } 302 303 auto pair = pieceAt(insertPos); 304 305 if (insertPos == pair.pos) // At the begining of piece 306 { 307 PieceRef before = pair.piece.prev; 308 PieceRef after = pair.piece; 309 310 PieceRestoreRange restoreRange = PieceRestoreRange(pair.piece.prev, pair.piece, length, true); 311 312 before.next = newPiece; 313 newPiece.prev = before; 314 315 newPiece.next = after; 316 after.prev = newPiece; 317 318 length += newPiece.length; 319 320 return restoreRange; 321 } 322 else // In the middle of piece 323 { 324 auto restoreRange = PieceRestoreRange(pair.piece, pair.piece, length); 325 326 length += newPiece.length; 327 328 PieceRef before = pair.piece.prev; 329 PieceRef after = pair.piece.next; 330 331 auto leftPieceLength = insertPos - pair.pos; 332 auto rightPiecePos = pair.piece.bufferOffset + leftPieceLength; 333 334 PieceRef leftPiece = createPiece(pair.piece.bufferOffset, leftPieceLength, before, newPiece); 335 PieceRef rightPiece = createPiece(rightPiecePos, pair.piece.length - leftPieceLength, newPiece, after); 336 337 before.next = leftPiece; 338 339 newPiece.prev = leftPiece; 340 newPiece.next = rightPiece; 341 342 after.prev = rightPiece; 343 344 return restoreRange; 345 } 346 } 347 } 348 349 private PieceRef createPiece(size_t position = 0, size_t length = 0, PieceRef prev = null, PieceRef next = null) 350 { 351 return new Piece(position, length, prev, next); 352 } 353 354 import std.stdio; 355 struct PieceTable 356 { 357 PieceStorage pieces; 358 // Stores original text followed by inserted text. 359 Appender!(char[]) buffer; 360 361 /// Returns appended text length in bytes 362 size_t appendBuffer(S)(S text) 363 { 364 auto initialLength = buffer.data.length; 365 buffer ~= text; 366 size_t textLength = buffer.data.length - initialLength; 367 return textLength; 368 } 369 370 this(S)(S initialText) 371 { 372 pieces = pieceStorage(); 373 if (initialText.empty) return; // sequence cannot contain empty pieces 374 size_t textLength = appendBuffer(initialText); 375 pieces.insertBack(createPiece(0, textLength)); 376 } 377 378 unittest 379 { 380 PieceTable table = PieceTable("test"); 381 assert(table.length == 4); 382 table = PieceTable("абвгде"); 383 assert(table.length == 12); 384 } 385 386 Range!char opSlice() 387 { 388 if (!pieces.length) 389 return Range!char(); 390 391 PieceRef first = pieces.sentinel.next; 392 PieceRef last = pieces.sentinel.prev; 393 size_t lastOffset = last.length; 394 return Range!char(first, 0, last, lastOffset, 395 pieces.length, cast(string)buffer.data); 396 } 397 398 Range!char opSlice(size_t x, size_t y) 399 { 400 assert(x <= y, format("%s <= %s", x, y)); 401 assert(y <= length, format("%s <= %s", y, length)); 402 if (x == y) return Range!char(); 403 404 PieceWithPos first = pieces.pieceAt(x); 405 PieceWithPos last; 406 407 if (y == length) 408 last = PieceWithPos(pieces.sentinel.prev, pieces.sentinel.prev.length); 409 else 410 last = first.pieceAt(y); 411 412 return Range!char( 413 first.piece, x-first.pos, 414 last.piece, y-last.pos, 415 y - x, 416 cast(string)buffer.data); 417 } 418 419 size_t length() @property { return pieces.length; } 420 alias opDollar = length; 421 422 unittest 423 { 424 PieceTable table = PieceTable("абвгде"); 425 426 assert("абвгде".length == 12); 427 //writefln("%s", "абвгде".length); 428 assert(table.length == 12); 429 //writefln("%s", table.length); 430 assert(table[0..2].length == 2); 431 432 //writefln("%s", table[0..$]); 433 //writefln("%s", table[]); 434 assert(table[0..$].equalDchars(table[])); 435 //writefln("1: %s где", table[6..$]); 436 assert(table[6..$].equalDchars("где")); 437 assert(table[0..6].equalDchars("абв")); 438 //assertThrown!AssertError(table[6..2]); 439 assert(table[0..12].equalDchars(table[])); 440 441 auto range = table[0..$]; 442 assert(range.equalDchars(range.save[0..$])); 443 assert((table[4..8])[0..2].length == 2); 444 assert((table[4..8])[0..2].equalDchars("в")); 445 } 446 447 /// Remove sequence of text starting at index of length length 448 /* 449 * |---------| - Piece 450 * 451 * 1. |XXXXX----| - Remove from begining to the middle 452 * 453 * 2. |XXXXXXXXX| - Remove whole piece 454 * 455 * 3. |XXXXXXXXX|XXX... - Remove whole piece and past piece 456 * 457 * 4. |--XXXXX--| - Remove in the middle of piece 458 * 459 * 5. |--XXXXXXX| - Remove from middle to the end of piece 460 * 461 * 6. |--XXXXXXX|XXX... - Remove from middle and past piece 462 */ 463 PieceRestoreRange remove(size_t removePos, size_t removeLength) 464 { 465 assert(removePos < pieces.length && removeLength > 0); 466 467 if (removePos + removeLength > length) 468 { 469 removeLength = length - removePos; 470 } 471 472 size_t removeEnd = removePos + removeLength - 1; 473 474 // First piece in the sequence. 475 PieceWithPos first = pieces.pieceAt(removePos); 476 PieceWithPos last = first.pieceAt(removeEnd); 477 size_t lastEnd = last.pos + last.piece.length - 1; 478 479 PieceRef newPieces = first.piece.prev; 480 481 // handle cases 4, 5 and 6. 482 if (removePos > first.pos) 483 { 484 PieceRef subPiece = createPiece(first.piece.bufferOffset, removePos - first.pos, newPieces); 485 newPieces.next = subPiece; 486 newPieces = subPiece; 487 } 488 489 // Handle cases 1 and 4 490 if (removeEnd < lastEnd) 491 { 492 auto offset = removeEnd - last.pos + 1; 493 PieceRef subPiece = createPiece(last.piece.bufferOffset + offset, lastEnd - removeEnd, newPieces); 494 newPieces.next = subPiece; 495 newPieces = subPiece; 496 } 497 498 PieceRef after = last.piece.next; 499 newPieces.next = after; 500 after.prev = newPieces; 501 502 auto undoItem = PieceRestoreRange(first.piece, last.piece, pieces.length); 503 504 pieces.length -= removeLength; 505 506 return undoItem; 507 } 508 509 unittest 510 { 511 PieceTable table = PieceTable("абвгде"); 512 513 assert(table.length == 12); 514 auto piece1 = table.pieces.sentinel.next; 515 516 table.remove(0, 2); // case 1 517 assert(table.length == 10); 518 assert(equalDchars(table[], "бвгде")); 519 520 table.remove(0, 12); // case 2 521 assert(table.length == 0); 522 assert(equalDchars(table[], "")); 523 524 table = PieceTable("абвгде"); 525 526 table.remove(4, 2); // case 4 527 assert(table.pieces.sentinel.next.length == 4); 528 assert(table.pieces.sentinel.next.next.length == 6); 529 assert(table.pieces.sentinel.prev.length == 6); 530 assert(table.pieces.sentinel.prev.prev.length == 4); 531 assert(table.length == 10); 532 assert(equalDchars(table[], "абгде")); 533 534 table.remove(0, 10); // case 3 + case 2 535 assert(table.length == 0); 536 assert(equalDchars(table[], "")); 537 538 table = PieceTable("абвгде"); 539 540 table.remove(4, 8); // case 5 541 assert(table.length == 4); 542 assert(equalDchars(table[], "аб")); 543 544 table = PieceTable("абвгде"); 545 546 table.remove(4, 2); 547 table.remove(2, 10); // case 6 + case 2 548 assert(table.length == 2); 549 assert(equalDchars(table[], "а")); 550 551 table = PieceTable("аб"); 552 table.insert("вг"); 553 table.insert("де"); 554 table.remove(4, 4); 555 assert(table[].equalDchars("абде")); 556 } 557 558 PieceRestoreRange insert(S)(S text) 559 if (isSomeString!S || (isInputRange!S && isSomeChar!(ElementType!S))) 560 { 561 return insert(length, text); 562 } 563 564 PieceRestoreRange insert(S)(size_t insertPos, S text) 565 if (isSomeString!S || (isInputRange!S && isSomeChar!(ElementType!S))) 566 { 567 size_t bufferPos = buffer.data.length; 568 size_t textLength = appendBuffer(text); 569 570 PieceRef middlePiece = createPiece(bufferPos, textLength); 571 572 return pieces.insertAt(middlePiece, insertPos); 573 } 574 575 unittest 576 { 577 PieceTable table = PieceTable("абвгде"); 578 579 table.insert(0, "абв"); 580 assert(table.length == 18); 581 assert(equalDchars(table[], "абвабвгде")); 582 583 table.insert(18, "абв"); 584 assert(table.length == 24); 585 assert(equalDchars(table[], "абвабвгдеабв")); 586 587 table = PieceTable("абвгде"); 588 table.insert(6, "ggg"); 589 assert(table.length == 15); 590 assert(equalDchars(table[], "абвgggгде")); 591 } 592 593 unittest 594 { 595 PieceTable table = PieceTable("\ntest"); 596 table.insert(0, "d"); 597 assert(equalDchars(table[], "d\ntest")); 598 } 599 600 unittest 601 { 602 PieceTable table = PieceTable(""); 603 604 table.insert(4, "абв"); 605 table.insert(4, "абв"); 606 assert(table.length == 12); 607 assert(equalDchars(table[], "абабвв")); 608 } 609 } 610 611 private struct Range(T) 612 { 613 private PieceRef first; 614 private size_t firstOffset; 615 private PieceRef last; 616 private size_t lastOffset; 617 private size_t _length; 618 private string buffer; 619 size_t length() @property { return _length; } 620 alias opDollar = length; 621 bool empty() @property { return _length == 0; } 622 T front() @property { 623 static if (is(T == char)) 624 return buffer[first.bufferOffset + firstOffset]; 625 else // dchar 626 { 627 auto offset = first.bufferOffset + firstOffset; 628 auto str = buffer[offset..$]; 629 if (str.length == 0) 630 { 631 writefln("str [%s..%s]", offset, buffer.length); 632 } 633 return decodeFront!(Yes.useReplacementDchar)(str); 634 } 635 } 636 T back() @property { 637 static if (is(T == char)) 638 return buffer[first.bufferOffset + firstOffset]; 639 else // dchar 640 { 641 uint backLength = strideBack(buffer, last.bufferOffset + lastOffset); 642 auto pos = last.bufferOffset + lastOffset - backLength; 643 return decode!(Yes.useReplacementDchar)(buffer, pos); 644 } 645 } 646 647 void popFront() { 648 static if (is(T == char)) 649 uint frontLength = 1; 650 else // dchar 651 uint frontLength = stride(buffer, first.bufferOffset + firstOffset); 652 653 firstOffset += frontLength; 654 _length -= frontLength; 655 656 if (firstOffset == first.length) 657 { 658 first = first.next; 659 firstOffset = 0; 660 } 661 } 662 663 void popBack() 664 { 665 static if (is(T == char)) 666 uint backLength = 1; 667 else // dchar 668 uint backLength = strideBack(buffer, last.bufferOffset + lastOffset); 669 670 lastOffset -= backLength; 671 _length -= backLength; 672 673 if (lastOffset == 0) 674 { 675 last = last.prev; 676 lastOffset = last.length; 677 } 678 } 679 680 Range opSlice(size_t x, size_t y) 681 { 682 assert(x <= y); 683 assert(y <= _length); 684 if (x == y) return Range(); 685 686 PieceWithPos first = PieceWithPos(this.first, 0).pieceAt(firstOffset + x); 687 PieceWithPos last = first.pieceAt(y); 688 689 return Range( 690 first.piece, firstOffset + x - first.pos, 691 last.piece, y-last.pos, 692 y - x, 693 buffer); 694 } 695 696 Range save() { return this; } 697 698 static if (is(T == dchar)) 699 Range!char byChar() { 700 return cast(Range!char)this; 701 } 702 703 import voxelman.container.chunkedrange; 704 ChunkedRange!char toChunkedRange() 705 { 706 if (_length == 0) return ChunkedRange!char(null, 0, null, null, &ChunkedRange_popFront); 707 size_t itemsLeft = _length; 708 size_t chunkLength = min(itemsLeft, first.length-firstOffset); 709 auto from = first.bufferOffset + firstOffset; 710 auto to = from + chunkLength; 711 itemsLeft -= chunkLength; 712 auto front = buffer[from..to]; 713 return ChunkedRange!char( 714 cast(char[])front, itemsLeft, cast(void*)first.next, cast(void*)buffer.ptr, &ChunkedRange_popFront); 715 } 716 717 private static void ChunkedRange_popFront( 718 ref char[] front, 719 ref size_t itemsLeft, 720 ref void* nextPieceData, 721 ref void* bufferData) 722 { 723 PieceRef nextPiece = cast(PieceRef)nextPieceData; 724 char* buffer = cast(char*)bufferData; 725 726 if (itemsLeft == 0) 727 { 728 front = null; 729 return; 730 } 731 732 size_t chunkLength = min(itemsLeft, nextPiece.length); 733 front = buffer[nextPiece.bufferOffset..nextPiece.bufferOffset+chunkLength]; 734 nextPiece = nextPiece.next; 735 736 itemsLeft -= chunkLength; 737 738 nextPieceData = cast(void*)nextPiece; 739 } 740 741 void copyInto(char[] sink) 742 { 743 assert(sink.length == _length); 744 745 auto bytesLeft = _length; 746 747 void copyChunk(size_t from, size_t to) 748 { 749 auto chunkLen = to - from; 750 sink[0..chunkLen] = buffer[from..to]; 751 sink = sink[chunkLen..$]; 752 bytesLeft -= chunkLen; 753 } 754 755 // copy first 756 auto firstFrom = first.bufferOffset + firstOffset; 757 auto firstLen = min(first.length - firstOffset, bytesLeft); 758 auto firstTo = firstFrom + firstLen; 759 copyChunk(firstFrom, firstTo); 760 761 // copy other pieces 762 auto piece = first.next; 763 while(piece != last.next) 764 { 765 auto from = piece.bufferOffset; 766 auto to = from + min(piece.length, bytesLeft); 767 copyChunk(from, to); 768 piece = piece.next; 769 } 770 771 assert(sink.length == 0); // filled sink fully 772 } 773 } 774 775 static assert(isForwardRange!(Range!dchar)); 776 static assert(hasSlicing!(Range!dchar)); 777 static assert(isBidirectionalRange!(Range!dchar)); 778 static assert(isForwardRange!(Range!char)); 779 static assert(hasSlicing!(Range!char)); 780 static assert(isBidirectionalRange!(Range!char)); 781 782 version(unittest) string callCopy(PieceTable table, size_t from, size_t to) 783 { 784 char[] buf = new char[to-from]; 785 table[from..to].copyInto(buf); 786 return cast(string)buf; 787 } 788 789 unittest 790 { 791 PieceTable table = PieceTable("abcd"); 792 assert(table.callCopy(0, 4).equalDchars("abcd")); 793 assert(table.callCopy(1, 4).equalDchars("bcd")); 794 assert(table.callCopy(1, 3).equalDchars("bc")); 795 }