1 /* 2 * Copyright 2013-2014, Stephan Aßmus <superstippi@gmx.de>. 3 * Copyright 2021, Andrew Lindesay <apl@lindesay.co.nz>. 4 * All rights reserved. Distributed under the terms of the MIT License. 5 */ 6 7 #include "TextDocument.h" 8 9 #include <algorithm> 10 #include <stdio.h> 11 #include <vector> 12 13 14 TextDocument::TextDocument() 15 : 16 fParagraphs(), 17 fEmptyLastParagraph(), 18 fDefaultCharacterStyle() 19 { 20 } 21 22 23 TextDocument::TextDocument(CharacterStyle characterStyle, 24 ParagraphStyle paragraphStyle) 25 : 26 fParagraphs(), 27 fEmptyLastParagraph(paragraphStyle), 28 fDefaultCharacterStyle(characterStyle) 29 { 30 } 31 32 33 TextDocument::TextDocument(const TextDocument& other) 34 : 35 fParagraphs(other.fParagraphs), 36 fEmptyLastParagraph(other.fEmptyLastParagraph), 37 fDefaultCharacterStyle(other.fDefaultCharacterStyle) 38 { 39 } 40 41 42 TextDocument& 43 TextDocument::operator=(const TextDocument& other) 44 { 45 fParagraphs = other.fParagraphs; 46 fEmptyLastParagraph = other.fEmptyLastParagraph; 47 fDefaultCharacterStyle = other.fDefaultCharacterStyle; 48 49 return *this; 50 } 51 52 53 bool 54 TextDocument::operator==(const TextDocument& other) const 55 { 56 if (this == &other) 57 return true; 58 59 return fEmptyLastParagraph == other.fEmptyLastParagraph 60 && fDefaultCharacterStyle == other.fDefaultCharacterStyle 61 && fParagraphs == other.fParagraphs; 62 } 63 64 65 bool 66 TextDocument::operator!=(const TextDocument& other) const 67 { 68 return !(*this == other); 69 } 70 71 72 // #pragma mark - 73 74 75 status_t 76 TextDocument::Insert(int32 textOffset, const BString& text) 77 { 78 return Replace(textOffset, 0, text); 79 } 80 81 82 status_t 83 TextDocument::Insert(int32 textOffset, const BString& text, 84 CharacterStyle style) 85 { 86 return Replace(textOffset, 0, text, style); 87 } 88 89 90 status_t 91 TextDocument::Insert(int32 textOffset, const BString& text, 92 CharacterStyle characterStyle, ParagraphStyle paragraphStyle) 93 { 94 return Replace(textOffset, 0, text, characterStyle, paragraphStyle); 95 } 96 97 98 // #pragma mark - 99 100 101 status_t 102 TextDocument::Remove(int32 textOffset, int32 length) 103 { 104 return Replace(textOffset, length, BString()); 105 } 106 107 108 // #pragma mark - 109 110 111 status_t 112 TextDocument::Replace(int32 textOffset, int32 length, const BString& text) 113 { 114 return Replace(textOffset, length, text, CharacterStyleAt(textOffset)); 115 } 116 117 118 status_t 119 TextDocument::Replace(int32 textOffset, int32 length, const BString& text, 120 CharacterStyle style) 121 { 122 return Replace(textOffset, length, text, style, 123 ParagraphStyleAt(textOffset)); 124 } 125 126 127 status_t 128 TextDocument::Replace(int32 textOffset, int32 length, const BString& text, 129 CharacterStyle characterStyle, ParagraphStyle paragraphStyle) 130 { 131 TextDocumentRef document = NormalizeText(text, characterStyle, 132 paragraphStyle); 133 if (!document.IsSet() || document->Length() != text.CountChars()) 134 return B_NO_MEMORY; 135 return Replace(textOffset, length, document); 136 } 137 138 139 status_t 140 TextDocument::Replace(int32 textOffset, int32 length, TextDocumentRef document) 141 { 142 int32 firstParagraph = 0; 143 int32 paragraphCount = 0; 144 145 // TODO: Call _NotifyTextChanging() before any change happened 146 147 status_t ret = _Remove(textOffset, length, firstParagraph, paragraphCount); 148 if (ret != B_OK) 149 return ret; 150 151 ret = _Insert(textOffset, document, firstParagraph, paragraphCount); 152 153 _NotifyTextChanged(TextChangedEvent(firstParagraph, paragraphCount)); 154 155 return ret; 156 } 157 158 159 // #pragma mark - 160 161 162 const CharacterStyle& 163 TextDocument::CharacterStyleAt(int32 textOffset) const 164 { 165 int32 paragraphOffset; 166 const Paragraph& paragraph = ParagraphAt(textOffset, paragraphOffset); 167 168 textOffset -= paragraphOffset; 169 int32 index; 170 int32 count = paragraph.CountTextSpans(); 171 172 for (index = 0; index < count; index++) { 173 const TextSpan& span = paragraph.TextSpanAtIndex(index); 174 if (textOffset - span.CountChars() < 0) 175 return span.Style(); 176 textOffset -= span.CountChars(); 177 } 178 179 return fDefaultCharacterStyle; 180 } 181 182 183 const BMessage* 184 TextDocument::ClickMessageAt(int32 textOffset) const 185 { 186 int32 paragraphOffset; 187 const Paragraph& paragraph = ParagraphAt(textOffset, paragraphOffset); 188 189 textOffset -= paragraphOffset; 190 int32 index; 191 int32 count = paragraph.CountTextSpans(); 192 193 for (index = 0; index < count; index++) { 194 const TextSpan& span = paragraph.TextSpanAtIndex(index); 195 if (textOffset - span.CountChars() < 0) 196 return span.ClickMessage(); 197 textOffset -= span.CountChars(); 198 } 199 200 return NULL; 201 } 202 203 204 BCursor 205 TextDocument::CursorAt(int32 textOffset) const 206 { 207 int32 paragraphOffset; 208 const Paragraph& paragraph = ParagraphAt(textOffset, paragraphOffset); 209 210 textOffset -= paragraphOffset; 211 int32 index; 212 int32 count = paragraph.CountTextSpans(); 213 214 for (index = 0; index < count; index++) { 215 const TextSpan& span = paragraph.TextSpanAtIndex(index); 216 if (textOffset - span.CountChars() < 0) 217 return span.Cursor(); 218 textOffset -= span.CountChars(); 219 } 220 221 return BCursor((BMessage*)NULL); 222 } 223 224 225 const ParagraphStyle& 226 TextDocument::ParagraphStyleAt(int32 textOffset) const 227 { 228 int32 paragraphOffset; 229 return ParagraphAt(textOffset, paragraphOffset).Style(); 230 } 231 232 233 // #pragma mark - 234 235 236 int32 237 TextDocument::CountParagraphs() const 238 { 239 return fParagraphs.size(); 240 } 241 242 243 const Paragraph& 244 TextDocument::ParagraphAtIndex(int32 index) const 245 { 246 return fParagraphs[index]; 247 } 248 249 250 int32 251 TextDocument::ParagraphIndexFor(int32 textOffset, int32& paragraphOffset) const 252 { 253 // TODO: Could binary search the Paragraphs if they were wrapped in classes 254 // that knew there text offset in the document. 255 int32 textLength = 0; 256 paragraphOffset = 0; 257 int32 count = fParagraphs.size(); 258 for (int32 i = 0; i < count; i++) { 259 const Paragraph& paragraph = fParagraphs[i]; 260 int32 paragraphLength = paragraph.Length(); 261 textLength += paragraphLength; 262 if (textLength > textOffset 263 || (i == count - 1 && textLength == textOffset)) { 264 return i; 265 } 266 paragraphOffset += paragraphLength; 267 } 268 return -1; 269 } 270 271 272 const Paragraph& 273 TextDocument::ParagraphAt(int32 textOffset, int32& paragraphOffset) const 274 { 275 int32 index = ParagraphIndexFor(textOffset, paragraphOffset); 276 if (index >= 0) 277 return fParagraphs[index]; 278 279 return fEmptyLastParagraph; 280 } 281 282 283 const Paragraph& 284 TextDocument::ParagraphAt(int32 index) const 285 { 286 if (index >= 0 && index < static_cast<int32>(fParagraphs.size())) 287 return fParagraphs[index]; 288 return fEmptyLastParagraph; 289 } 290 291 292 bool 293 TextDocument::Append(const Paragraph& paragraph) 294 { 295 try { 296 fParagraphs.push_back(paragraph); 297 } 298 catch (std::bad_alloc& ba) { 299 fprintf(stderr, "bad_alloc when adding a paragraph to a text " 300 "document\n"); 301 return false; 302 } 303 return true; 304 } 305 306 307 int32 308 TextDocument::Length() const 309 { 310 // TODO: Could be O(1) if the Paragraphs were wrapped in classes that 311 // knew their text offset in the document. 312 int32 textLength = 0; 313 int32 count = fParagraphs.size(); 314 for (int32 i = 0; i < count; i++) { 315 const Paragraph& paragraph = fParagraphs[i]; 316 textLength += paragraph.Length(); 317 } 318 return textLength; 319 } 320 321 322 BString 323 TextDocument::Text() const 324 { 325 return Text(0, Length()); 326 } 327 328 329 BString 330 TextDocument::Text(int32 start, int32 length) const 331 { 332 if (start < 0) 333 start = 0; 334 335 BString text; 336 337 int32 count = fParagraphs.size(); 338 for (int32 i = 0; i < count; i++) { 339 const Paragraph& paragraph = fParagraphs[i]; 340 int32 paragraphLength = paragraph.Length(); 341 if (paragraphLength == 0) 342 continue; 343 if (start > paragraphLength) { 344 // Skip paragraph if its before start 345 start -= paragraphLength; 346 continue; 347 } 348 349 // Remaining paragraph length after start 350 paragraphLength -= start; 351 int32 copyLength = std::min(paragraphLength, length); 352 353 text << paragraph.Text(start, copyLength); 354 355 length -= copyLength; 356 if (length == 0) 357 break; 358 359 // Next paragraph is copied from its beginning 360 start = 0; 361 } 362 363 return text; 364 } 365 366 367 TextDocumentRef 368 TextDocument::SubDocument(int32 start, int32 length) const 369 { 370 TextDocumentRef result(new(std::nothrow) TextDocument( 371 fDefaultCharacterStyle, fEmptyLastParagraph.Style()), true); 372 373 if (!result.IsSet()) 374 return result; 375 376 if (start < 0) 377 start = 0; 378 379 int32 count = fParagraphs.size(); 380 for (int32 i = 0; i < count; i++) { 381 const Paragraph& paragraph = fParagraphs[i]; 382 int32 paragraphLength = paragraph.Length(); 383 if (paragraphLength == 0) 384 continue; 385 if (start > paragraphLength) { 386 // Skip paragraph if its before start 387 start -= paragraphLength; 388 continue; 389 } 390 391 // Remaining paragraph length after start 392 paragraphLength -= start; 393 int32 copyLength = std::min(paragraphLength, length); 394 395 result->Append(paragraph.SubParagraph(start, copyLength)); 396 397 length -= copyLength; 398 if (length == 0) 399 break; 400 401 // Next paragraph is copied from its beginning 402 start = 0; 403 } 404 405 return result; 406 } 407 408 409 // #pragma mark - 410 411 412 void 413 TextDocument::PrintToStream() const 414 { 415 int32 paragraphCount = fParagraphs.size(); 416 if (paragraphCount == 0) { 417 printf("<document/>\n"); 418 return; 419 } 420 printf("<document>\n"); 421 for (int32 i = 0; i < paragraphCount; i++) { 422 fParagraphs[i].PrintToStream(); 423 } 424 printf("</document>\n"); 425 } 426 427 428 /*static*/ TextDocumentRef 429 TextDocument::NormalizeText(const BString& text, 430 CharacterStyle characterStyle, ParagraphStyle paragraphStyle) 431 { 432 TextDocumentRef document(new(std::nothrow) TextDocument(characterStyle, 433 paragraphStyle), true); 434 if (!document.IsSet()) 435 throw B_NO_MEMORY; 436 437 Paragraph paragraph(paragraphStyle); 438 439 // Append TextSpans, splitting 'text' into Paragraphs at line breaks. 440 int32 length = text.CountChars(); 441 int32 chunkStart = 0; 442 while (chunkStart < length) { 443 int32 chunkEnd = text.FindFirst('\n', chunkStart); 444 bool foundLineBreak = chunkEnd >= chunkStart; 445 if (foundLineBreak) 446 chunkEnd++; 447 else 448 chunkEnd = length; 449 450 BString chunk; 451 text.CopyCharsInto(chunk, chunkStart, chunkEnd - chunkStart); 452 TextSpan span(chunk, characterStyle); 453 454 if (!paragraph.Append(span)) 455 throw B_NO_MEMORY; 456 if (paragraph.Length() > 0 && !document->Append(paragraph)) 457 throw B_NO_MEMORY; 458 459 paragraph = Paragraph(paragraphStyle); 460 chunkStart = chunkEnd + 1; 461 } 462 463 return document; 464 } 465 466 467 // #pragma mark - 468 469 470 bool 471 TextDocument::AddListener(TextListenerRef listener) 472 { 473 try { 474 fTextListeners.push_back(listener); 475 } 476 catch (std::bad_alloc& ba) { 477 fprintf(stderr, "bad_alloc when adding a listener to a text " 478 "document\n"); 479 return false; 480 } 481 return true; 482 } 483 484 485 bool 486 TextDocument::RemoveListener(TextListenerRef listener) 487 { 488 fTextListeners.erase(std::remove(fTextListeners.begin(), fTextListeners.end(), 489 listener), fTextListeners.end()); 490 return true; 491 } 492 493 494 bool 495 TextDocument::AddUndoListener(UndoableEditListenerRef listener) 496 { 497 try { 498 fUndoListeners.push_back(listener); 499 } 500 catch (std::bad_alloc& ba) { 501 fprintf(stderr, "bad_alloc when adding an undo listener to a text " 502 "document\n"); 503 return false; 504 } 505 return true; 506 } 507 508 509 bool 510 TextDocument::RemoveUndoListener(UndoableEditListenerRef listener) 511 { 512 fUndoListeners.erase(std::remove(fUndoListeners.begin(), fUndoListeners.end(), 513 listener), fUndoListeners.end()); 514 return true; 515 } 516 517 518 // #pragma mark - private 519 520 521 status_t 522 TextDocument::_Insert(int32 textOffset, TextDocumentRef document, 523 int32& index, int32& paragraphCount) 524 { 525 int32 paragraphOffset; 526 index = ParagraphIndexFor(textOffset, paragraphOffset); 527 if (index < 0) 528 return B_BAD_VALUE; 529 530 if (document->Length() == 0) 531 return B_OK; 532 533 textOffset -= paragraphOffset; 534 535 bool hasLineBreaks; 536 if (document->CountParagraphs() > 1) { 537 hasLineBreaks = true; 538 } else { 539 const Paragraph& paragraph = document->ParagraphAt(0); 540 hasLineBreaks = paragraph.EndsWith("\n"); 541 } 542 543 if (hasLineBreaks) { 544 // Split paragraph at textOffset 545 Paragraph paragraph1(ParagraphAt(index).Style()); 546 Paragraph paragraph2(document->ParagraphAt( 547 document->CountParagraphs() - 1).Style()); 548 { 549 const Paragraph& paragraphAtIndex = ParagraphAt(index); 550 int32 spanCount = paragraphAtIndex.CountTextSpans(); 551 for (int32 i = 0; i < spanCount; i++) { 552 const TextSpan& span = paragraphAtIndex.TextSpanAtIndex(i); 553 int32 spanLength = span.CountChars(); 554 if (textOffset >= spanLength) { 555 if (!paragraph1.Append(span)) 556 return B_NO_MEMORY; 557 textOffset -= spanLength; 558 } else if (textOffset > 0) { 559 if (!paragraph1.Append( 560 span.SubSpan(0, textOffset)) 561 || !paragraph2.Append( 562 span.SubSpan(textOffset, 563 spanLength - textOffset))) { 564 return B_NO_MEMORY; 565 } 566 textOffset = 0; 567 } else { 568 if (!paragraph2.Append(span)) 569 return B_NO_MEMORY; 570 } 571 } 572 } 573 574 fParagraphs.erase(fParagraphs.begin() + index); 575 576 // Append first paragraph in other document to first part of 577 // paragraph at insert position 578 { 579 const Paragraph& otherParagraph = document->ParagraphAt(0); 580 int32 spanCount = otherParagraph.CountTextSpans(); 581 for (int32 i = 0; i < spanCount; i++) { 582 const TextSpan& span = otherParagraph.TextSpanAtIndex(i); 583 // TODO: Import/map CharacterStyles 584 if (!paragraph1.Append(span)) 585 return B_NO_MEMORY; 586 } 587 } 588 589 // Insert the first paragraph-part again to the document 590 try { 591 fParagraphs.insert(fParagraphs.begin() + index, paragraph1); 592 } 593 catch (std::bad_alloc& ba) { 594 return B_NO_MEMORY; 595 } 596 paragraphCount++; 597 598 // Insert the other document's paragraph save for the last one 599 for (int32 i = 1; i < document->CountParagraphs() - 1; i++) { 600 const Paragraph& otherParagraph = document->ParagraphAt(i); 601 // TODO: Import/map CharacterStyles and ParagraphStyle 602 index++; 603 try { 604 fParagraphs.insert(fParagraphs.begin() + index, otherParagraph); 605 } 606 catch (std::bad_alloc& ba) { 607 return B_NO_MEMORY; 608 } 609 paragraphCount++; 610 } 611 612 int32 lastIndex = document->CountParagraphs() - 1; 613 if (lastIndex > 0) { 614 const Paragraph& otherParagraph = document->ParagraphAt(lastIndex); 615 if (otherParagraph.EndsWith("\n")) { 616 // TODO: Import/map CharacterStyles and ParagraphStyle 617 index++; 618 try { 619 fParagraphs.insert(fParagraphs.begin() + index, otherParagraph); 620 } 621 catch (std::bad_alloc& ba) { 622 return B_NO_MEMORY; 623 } 624 } else { 625 int32 spanCount = otherParagraph.CountTextSpans(); 626 for (int32 i = 0; i < spanCount; i++) { 627 const TextSpan& span = otherParagraph.TextSpanAtIndex(i); 628 // TODO: Import/map CharacterStyles 629 if (!paragraph2.Prepend(span)) 630 return B_NO_MEMORY; 631 } 632 } 633 } 634 635 // Insert back the second paragraph-part 636 if (paragraph2.IsEmpty()) { 637 // Make sure Paragraph has at least one TextSpan, even 638 // if its empty. This handles the case of inserting a 639 // line-break at the end of the document. It than needs to 640 // have a new, empty paragraph at the end. 641 const int32 indexLastSpan = paragraph1.CountTextSpans() - 1; 642 const TextSpan& span = paragraph1.TextSpanAtIndex(indexLastSpan); 643 if (!paragraph2.Append(TextSpan("", span.Style()))) 644 return B_NO_MEMORY; 645 } 646 647 index++; 648 try { 649 fParagraphs.insert(fParagraphs.begin() + index, paragraph2); 650 } 651 catch (std::bad_alloc& ba) { 652 return B_NO_MEMORY; 653 } 654 655 paragraphCount++; 656 } else { 657 Paragraph paragraph(ParagraphAt(index)); 658 const Paragraph& otherParagraph = document->ParagraphAt(0); 659 660 int32 spanCount = otherParagraph.CountTextSpans(); 661 for (int32 i = 0; i < spanCount; i++) { 662 const TextSpan& span = otherParagraph.TextSpanAtIndex(i); 663 paragraph.Insert(textOffset, span); 664 textOffset += span.CountChars(); 665 } 666 667 fParagraphs[index] = paragraph; 668 paragraphCount++; 669 } 670 671 return B_OK; 672 } 673 674 675 status_t 676 TextDocument::_Remove(int32 textOffset, int32 length, int32& index, 677 int32& paragraphCount) 678 { 679 if (length == 0) 680 return B_OK; 681 682 int32 paragraphOffset; 683 index = ParagraphIndexFor(textOffset, paragraphOffset); 684 if (index < 0) 685 return B_BAD_VALUE; 686 687 textOffset -= paragraphOffset; 688 paragraphCount++; 689 690 // The paragraph at the text offset remains, even if the offset is at 691 // the beginning of that paragraph. The idea is that the selection start 692 // stays visually in the same place. Therefore, the paragraph at that 693 // offset has to keep the paragraph style from that paragraph. 694 695 Paragraph resultParagraph(ParagraphAt(index)); 696 int32 paragraphLength = resultParagraph.Length(); 697 if (textOffset == 0 && length > paragraphLength) { 698 length -= paragraphLength; 699 paragraphLength = 0; 700 resultParagraph.Clear(); 701 } else { 702 int32 removeLength = std::min(length, paragraphLength - textOffset); 703 resultParagraph.Remove(textOffset, removeLength); 704 paragraphLength -= removeLength; 705 length -= removeLength; 706 } 707 708 if (textOffset == paragraphLength && length == 0 709 && index + 1 < static_cast<int32>(fParagraphs.size())) { 710 // Line break between paragraphs got removed. Shift the next 711 // paragraph's text spans into the resulting one. 712 713 const Paragraph& paragraph = ParagraphAt(index + 1); 714 int32 spanCount = paragraph.CountTextSpans(); 715 for (int32 i = 0; i < spanCount; i++) { 716 const TextSpan& span = paragraph.TextSpanAtIndex(i); 717 resultParagraph.Append(span); 718 } 719 fParagraphs.erase(fParagraphs.begin() + (index + 1)); 720 paragraphCount++; 721 } 722 723 textOffset = 0; 724 725 while (length > 0 && index + 1 < static_cast<int32>(fParagraphs.size())) { 726 paragraphCount++; 727 const Paragraph& paragraph = ParagraphAt(index + 1); 728 paragraphLength = paragraph.Length(); 729 // Remove paragraph in any case. If some of it remains, the last 730 // paragraph to remove is reached, and the remaining spans are 731 // transfered to the result parahraph. 732 if (length >= paragraphLength) { 733 length -= paragraphLength; 734 fParagraphs.erase(fParagraphs.begin() + index); 735 } else { 736 // Last paragraph reached 737 int32 removedLength = std::min(length, paragraphLength); 738 Paragraph newParagraph(paragraph); 739 fParagraphs.erase(fParagraphs.begin() + (index + 1)); 740 741 if (!newParagraph.Remove(0, removedLength)) 742 return B_NO_MEMORY; 743 744 // Transfer remaining spans to resultParagraph 745 int32 spanCount = newParagraph.CountTextSpans(); 746 for (int32 i = 0; i < spanCount; i++) { 747 const TextSpan& span = newParagraph.TextSpanAtIndex(i); 748 resultParagraph.Append(span); 749 } 750 751 break; 752 } 753 } 754 755 fParagraphs[index] = resultParagraph; 756 757 return B_OK; 758 } 759 760 761 // #pragma mark - notifications 762 763 764 void 765 TextDocument::_NotifyTextChanging(TextChangingEvent& event) const 766 { 767 // Copy listener list to have a stable list in case listeners 768 // are added/removed from within the notification hook. 769 std::vector<TextListenerRef> listeners(fTextListeners); 770 771 int32 count = listeners.size(); 772 for (int32 i = 0; i < count; i++) { 773 const TextListenerRef& listener = listeners[i]; 774 if (!listener.IsSet()) 775 continue; 776 listener->TextChanging(event); 777 if (event.IsCanceled()) 778 break; 779 } 780 } 781 782 783 void 784 TextDocument::_NotifyTextChanged(const TextChangedEvent& event) const 785 { 786 // Copy listener list to have a stable list in case listeners 787 // are added/removed from within the notification hook. 788 std::vector<TextListenerRef> listeners(fTextListeners); 789 int32 count = listeners.size(); 790 for (int32 i = 0; i < count; i++) { 791 const TextListenerRef& listener = listeners[i]; 792 if (!listener.IsSet()) 793 continue; 794 listener->TextChanged(event); 795 } 796 } 797 798 799 void 800 TextDocument::_NotifyUndoableEditHappened(const UndoableEditRef& edit) const 801 { 802 // Copy listener list to have a stable list in case listeners 803 // are added/removed from within the notification hook. 804 std::vector<UndoableEditListenerRef> listeners(fUndoListeners); 805 int32 count = listeners.size(); 806 for (int32 i = 0; i < count; i++) { 807 const UndoableEditListenerRef& listener = listeners[i]; 808 if (!listener.IsSet()) 809 continue; 810 listener->UndoableEditHappened(this, edit); 811 } 812 } 813