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