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 std::remove(fTextListeners.begin(), fTextListeners.end(), listener); 447 return true; 448 } 449 450 451 bool 452 TextDocument::AddUndoListener(UndoableEditListenerRef listener) 453 { 454 try { 455 fUndoListeners.push_back(listener); 456 } 457 catch (std::bad_alloc& ba) { 458 fprintf(stderr, "bad_alloc when adding an undo listener to a text " 459 "document\n"); 460 return false; 461 } 462 return true; 463 } 464 465 466 bool 467 TextDocument::RemoveUndoListener(UndoableEditListenerRef listener) 468 { 469 std::remove(fUndoListeners.begin(), fUndoListeners.end(), listener); 470 return true; 471 } 472 473 474 // #pragma mark - private 475 476 477 status_t 478 TextDocument::_Insert(int32 textOffset, TextDocumentRef document, 479 int32& index, int32& paragraphCount) 480 { 481 int32 paragraphOffset; 482 index = ParagraphIndexFor(textOffset, paragraphOffset); 483 if (index < 0) 484 return B_BAD_VALUE; 485 486 if (document->Length() == 0) 487 return B_OK; 488 489 textOffset -= paragraphOffset; 490 491 bool hasLineBreaks; 492 if (document->CountParagraphs() > 1) { 493 hasLineBreaks = true; 494 } else { 495 const Paragraph& paragraph = document->ParagraphAt(0); 496 hasLineBreaks = paragraph.EndsWith("\n"); 497 } 498 499 if (hasLineBreaks) { 500 // Split paragraph at textOffset 501 Paragraph paragraph1(ParagraphAt(index).Style()); 502 Paragraph paragraph2(document->ParagraphAt( 503 document->CountParagraphs() - 1).Style()); 504 { 505 const Paragraph& paragraphAtIndex = ParagraphAt(index); 506 int32 spanCount = paragraphAtIndex.CountTextSpans(); 507 for (int32 i = 0; i < spanCount; i++) { 508 const TextSpan& span = paragraphAtIndex.TextSpanAtIndex(i); 509 int32 spanLength = span.CountChars(); 510 if (textOffset >= spanLength) { 511 if (!paragraph1.Append(span)) 512 return B_NO_MEMORY; 513 textOffset -= spanLength; 514 } else if (textOffset > 0) { 515 if (!paragraph1.Append( 516 span.SubSpan(0, textOffset)) 517 || !paragraph2.Append( 518 span.SubSpan(textOffset, 519 spanLength - textOffset))) { 520 return B_NO_MEMORY; 521 } 522 textOffset = 0; 523 } else { 524 if (!paragraph2.Append(span)) 525 return B_NO_MEMORY; 526 } 527 } 528 } 529 530 fParagraphs.erase(fParagraphs.begin() + index); 531 532 // Append first paragraph in other document to first part of 533 // paragraph at insert position 534 { 535 const Paragraph& otherParagraph = document->ParagraphAt(0); 536 int32 spanCount = otherParagraph.CountTextSpans(); 537 for (int32 i = 0; i < spanCount; i++) { 538 const TextSpan& span = otherParagraph.TextSpanAtIndex(i); 539 // TODO: Import/map CharacterStyles 540 if (!paragraph1.Append(span)) 541 return B_NO_MEMORY; 542 } 543 } 544 545 // Insert the first paragraph-part again to the document 546 try { 547 fParagraphs.insert(fParagraphs.begin() + index, paragraph1); 548 } 549 catch (std::bad_alloc& ba) { 550 return B_NO_MEMORY; 551 } 552 paragraphCount++; 553 554 // Insert the other document's paragraph save for the last one 555 for (int32 i = 1; i < document->CountParagraphs() - 1; i++) { 556 const Paragraph& otherParagraph = document->ParagraphAt(i); 557 // TODO: Import/map CharacterStyles and ParagraphStyle 558 index++; 559 try { 560 fParagraphs.insert(fParagraphs.begin() + index, otherParagraph); 561 } 562 catch (std::bad_alloc& ba) { 563 return B_NO_MEMORY; 564 } 565 paragraphCount++; 566 } 567 568 int32 lastIndex = document->CountParagraphs() - 1; 569 if (lastIndex > 0) { 570 const Paragraph& otherParagraph = document->ParagraphAt(lastIndex); 571 if (otherParagraph.EndsWith("\n")) { 572 // TODO: Import/map CharacterStyles and ParagraphStyle 573 index++; 574 try { 575 fParagraphs.insert(fParagraphs.begin() + index, otherParagraph); 576 } 577 catch (std::bad_alloc& ba) { 578 return B_NO_MEMORY; 579 } 580 } else { 581 int32 spanCount = otherParagraph.CountTextSpans(); 582 for (int32 i = 0; i < spanCount; i++) { 583 const TextSpan& span = otherParagraph.TextSpanAtIndex(i); 584 // TODO: Import/map CharacterStyles 585 if (!paragraph2.Prepend(span)) 586 return B_NO_MEMORY; 587 } 588 } 589 } 590 591 // Insert back the second paragraph-part 592 if (paragraph2.IsEmpty()) { 593 // Make sure Paragraph has at least one TextSpan, even 594 // if its empty. This handles the case of inserting a 595 // line-break at the end of the document. It than needs to 596 // have a new, empty paragraph at the end. 597 const int32 indexLastSpan = paragraph1.CountTextSpans() - 1; 598 const TextSpan& span = paragraph1.TextSpanAtIndex(indexLastSpan); 599 if (!paragraph2.Append(TextSpan("", span.Style()))) 600 return B_NO_MEMORY; 601 } 602 603 index++; 604 try { 605 fParagraphs.insert(fParagraphs.begin() + index, paragraph2); 606 } 607 catch (std::bad_alloc& ba) { 608 return B_NO_MEMORY; 609 } 610 611 paragraphCount++; 612 } else { 613 Paragraph paragraph(ParagraphAt(index)); 614 const Paragraph& otherParagraph = document->ParagraphAt(0); 615 616 int32 spanCount = otherParagraph.CountTextSpans(); 617 for (int32 i = 0; i < spanCount; i++) { 618 const TextSpan& span = otherParagraph.TextSpanAtIndex(i); 619 paragraph.Insert(textOffset, span); 620 textOffset += span.CountChars(); 621 } 622 623 fParagraphs[index] = paragraph; 624 paragraphCount++; 625 } 626 627 return B_OK; 628 } 629 630 631 status_t 632 TextDocument::_Remove(int32 textOffset, int32 length, int32& index, 633 int32& paragraphCount) 634 { 635 if (length == 0) 636 return B_OK; 637 638 int32 paragraphOffset; 639 index = ParagraphIndexFor(textOffset, paragraphOffset); 640 if (index < 0) 641 return B_BAD_VALUE; 642 643 textOffset -= paragraphOffset; 644 paragraphCount++; 645 646 // The paragraph at the text offset remains, even if the offset is at 647 // the beginning of that paragraph. The idea is that the selection start 648 // stays visually in the same place. Therefore, the paragraph at that 649 // offset has to keep the paragraph style from that paragraph. 650 651 Paragraph resultParagraph(ParagraphAt(index)); 652 int32 paragraphLength = resultParagraph.Length(); 653 if (textOffset == 0 && length > paragraphLength) { 654 length -= paragraphLength; 655 paragraphLength = 0; 656 resultParagraph.Clear(); 657 } else { 658 int32 removeLength = std::min(length, paragraphLength - textOffset); 659 resultParagraph.Remove(textOffset, removeLength); 660 paragraphLength -= removeLength; 661 length -= removeLength; 662 } 663 664 if (textOffset == paragraphLength && length == 0 665 && index + 1 < static_cast<int32>(fParagraphs.size())) { 666 // Line break between paragraphs got removed. Shift the next 667 // paragraph's text spans into the resulting one. 668 669 const Paragraph& paragraph = ParagraphAt(index + 1); 670 int32 spanCount = paragraph.CountTextSpans(); 671 for (int32 i = 0; i < spanCount; i++) { 672 const TextSpan& span = paragraph.TextSpanAtIndex(i); 673 resultParagraph.Append(span); 674 } 675 fParagraphs.erase(fParagraphs.begin() + (index + 1)); 676 paragraphCount++; 677 } 678 679 textOffset = 0; 680 681 while (length > 0 && index + 1 < static_cast<int32>(fParagraphs.size())) { 682 paragraphCount++; 683 const Paragraph& paragraph = ParagraphAt(index + 1); 684 paragraphLength = paragraph.Length(); 685 // Remove paragraph in any case. If some of it remains, the last 686 // paragraph to remove is reached, and the remaining spans are 687 // transfered to the result parahraph. 688 if (length >= paragraphLength) { 689 length -= paragraphLength; 690 fParagraphs.erase(fParagraphs.begin() + index); 691 } else { 692 // Last paragraph reached 693 int32 removedLength = std::min(length, paragraphLength); 694 Paragraph newParagraph(paragraph); 695 fParagraphs.erase(fParagraphs.begin() + (index + 1)); 696 697 if (!newParagraph.Remove(0, removedLength)) 698 return B_NO_MEMORY; 699 700 // Transfer remaining spans to resultParagraph 701 int32 spanCount = newParagraph.CountTextSpans(); 702 for (int32 i = 0; i < spanCount; i++) { 703 const TextSpan& span = newParagraph.TextSpanAtIndex(i); 704 resultParagraph.Append(span); 705 } 706 707 break; 708 } 709 } 710 711 fParagraphs[index] = resultParagraph; 712 713 return B_OK; 714 } 715 716 717 // #pragma mark - notifications 718 719 720 void 721 TextDocument::_NotifyTextChanging(TextChangingEvent& event) const 722 { 723 // Copy listener list to have a stable list in case listeners 724 // are added/removed from within the notification hook. 725 std::vector<TextListenerRef> listeners(fTextListeners); 726 727 int32 count = listeners.size(); 728 for (int32 i = 0; i < count; i++) { 729 const TextListenerRef& listener = listeners[i]; 730 if (!listener.IsSet()) 731 continue; 732 listener->TextChanging(event); 733 if (event.IsCanceled()) 734 break; 735 } 736 } 737 738 739 void 740 TextDocument::_NotifyTextChanged(const TextChangedEvent& event) const 741 { 742 // Copy listener list to have a stable list in case listeners 743 // are added/removed from within the notification hook. 744 std::vector<TextListenerRef> listeners(fTextListeners); 745 int32 count = listeners.size(); 746 for (int32 i = 0; i < count; i++) { 747 const TextListenerRef& listener = listeners[i]; 748 if (!listener.IsSet()) 749 continue; 750 listener->TextChanged(event); 751 } 752 } 753 754 755 void 756 TextDocument::_NotifyUndoableEditHappened(const UndoableEditRef& edit) const 757 { 758 // Copy listener list to have a stable list in case listeners 759 // are added/removed from within the notification hook. 760 std::vector<UndoableEditListenerRef> listeners(fUndoListeners); 761 int32 count = listeners.size(); 762 for (int32 i = 0; i < count; i++) { 763 const UndoableEditListenerRef& listener = listeners[i]; 764 if (!listener.IsSet()) 765 continue; 766 listener->UndoableEditHappened(this, edit); 767 } 768 } 769