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