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 14TextDocument::TextDocument() 15 : 16 fParagraphs(), 17 fEmptyLastParagraph(), 18 fDefaultCharacterStyle() 19{ 20} 21 22 23TextDocument::TextDocument(CharacterStyle characterStyle, 24 ParagraphStyle paragraphStyle) 25 : 26 fParagraphs(), 27 fEmptyLastParagraph(paragraphStyle), 28 fDefaultCharacterStyle(characterStyle) 29{ 30} 31 32 33TextDocument::TextDocument(const TextDocument& other) 34 : 35 fParagraphs(other.fParagraphs), 36 fEmptyLastParagraph(other.fEmptyLastParagraph), 37 fDefaultCharacterStyle(other.fDefaultCharacterStyle) 38{ 39} 40 41 42TextDocument& 43TextDocument::operator=(const TextDocument& other) 44{ 45 fParagraphs = other.fParagraphs; 46 fEmptyLastParagraph = other.fEmptyLastParagraph; 47 fDefaultCharacterStyle = other.fDefaultCharacterStyle; 48 49 return *this; 50} 51 52 53bool 54TextDocument::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 65bool 66TextDocument::operator!=(const TextDocument& other) const 67{ 68 return !(*this == other); 69} 70 71 72// #pragma mark - 73 74 75status_t 76TextDocument::Insert(int32 textOffset, const BString& text) 77{ 78 return Replace(textOffset, 0, text); 79} 80 81 82status_t 83TextDocument::Insert(int32 textOffset, const BString& text, 84 CharacterStyle style) 85{ 86 return Replace(textOffset, 0, text, style); 87} 88 89 90status_t 91TextDocument::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 101status_t 102TextDocument::Remove(int32 textOffset, int32 length) 103{ 104 return Replace(textOffset, length, BString()); 105} 106 107 108// #pragma mark - 109 110 111status_t 112TextDocument::Replace(int32 textOffset, int32 length, const BString& text) 113{ 114 return Replace(textOffset, length, text, CharacterStyleAt(textOffset)); 115} 116 117 118status_t 119TextDocument::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 127status_t 128TextDocument::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 139status_t 140TextDocument::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 162const CharacterStyle& 163TextDocument::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 183const BMessage* 184TextDocument::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 204BCursor 205TextDocument::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 225const ParagraphStyle& 226TextDocument::ParagraphStyleAt(int32 textOffset) const 227{ 228 int32 paragraphOffset; 229 return ParagraphAt(textOffset, paragraphOffset).Style(); 230} 231 232 233// #pragma mark - 234 235 236int32 237TextDocument::CountParagraphs() const 238{ 239 return fParagraphs.size(); 240} 241 242 243const Paragraph& 244TextDocument::ParagraphAtIndex(int32 index) const 245{ 246 return fParagraphs[index]; 247} 248 249 250int32 251TextDocument::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 272const Paragraph& 273TextDocument::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 283const Paragraph& 284TextDocument::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 292bool 293TextDocument::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 307int32 308TextDocument::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 322BString 323TextDocument::Text() const 324{ 325 return Text(0, Length()); 326} 327 328 329BString 330TextDocument::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 367TextDocumentRef 368TextDocument::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 412void 413TextDocument::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 429TextDocument::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 470bool 471TextDocument::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 485bool 486TextDocument::RemoveListener(TextListenerRef listener) 487{ 488 fTextListeners.erase(std::remove(fTextListeners.begin(), fTextListeners.end(), 489 listener), fTextListeners.end()); 490 return true; 491} 492 493 494bool 495TextDocument::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 509bool 510TextDocument::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 521status_t 522TextDocument::_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 675status_t 676TextDocument::_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 764void 765TextDocument::_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 783void 784TextDocument::_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 799void 800TextDocument::_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