xref: /haiku/src/apps/haikudepot/textview/TextDocument.cpp (revision c4e439b5a548ad2c125ca39e24653043133cf2e5)
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 
TextDocument()14 TextDocument::TextDocument()
15 	:
16 	fParagraphs(),
17 	fEmptyLastParagraph(),
18 	fDefaultCharacterStyle()
19 {
20 }
21 
22 
TextDocument(CharacterStyle characterStyle,ParagraphStyle paragraphStyle)23 TextDocument::TextDocument(CharacterStyle characterStyle,
24 	ParagraphStyle paragraphStyle)
25 	:
26 	fParagraphs(),
27 	fEmptyLastParagraph(paragraphStyle),
28 	fDefaultCharacterStyle(characterStyle)
29 {
30 }
31 
32 
TextDocument(const TextDocument & other)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&
operator =(const TextDocument & other)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
operator ==(const TextDocument & other) const54 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
operator !=(const TextDocument & other) const66 TextDocument::operator!=(const TextDocument& other) const
67 {
68 	return !(*this == other);
69 }
70 
71 
72 // #pragma mark -
73 
74 
75 status_t
Insert(int32 textOffset,const BString & text)76 TextDocument::Insert(int32 textOffset, const BString& text)
77 {
78 	return Replace(textOffset, 0, text);
79 }
80 
81 
82 status_t
Insert(int32 textOffset,const BString & text,CharacterStyle style)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
Insert(int32 textOffset,const BString & text,CharacterStyle characterStyle,ParagraphStyle paragraphStyle)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
Remove(int32 textOffset,int32 length)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
Replace(int32 textOffset,int32 length,const BString & text)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
Replace(int32 textOffset,int32 length,const BString & text,CharacterStyle style)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
Replace(int32 textOffset,int32 length,const BString & text,CharacterStyle characterStyle,ParagraphStyle paragraphStyle)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
Replace(int32 textOffset,int32 length,TextDocumentRef document)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&
CharacterStyleAt(int32 textOffset) const168 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*
ClickMessageAt(int32 textOffset) const189 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
CursorAt(int32 textOffset) const210 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&
ParagraphStyleAt(int32 textOffset) const231 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
CountParagraphs() const242 TextDocument::CountParagraphs() const
243 {
244 	return fParagraphs.size();
245 }
246 
247 
248 const Paragraph&
ParagraphAtIndex(int32 index) const249 TextDocument::ParagraphAtIndex(int32 index) const
250 {
251 	return fParagraphs[index];
252 }
253 
254 
255 int32
ParagraphIndexFor(int32 textOffset,int32 & paragraphOffset) const256 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&
ParagraphAt(int32 textOffset,int32 & paragraphOffset) const278 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&
ParagraphAt(int32 index) const289 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
Append(const Paragraph & paragraph)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
Length() const313 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
Text() const328 TextDocument::Text() const
329 {
330 	return Text(0, Length());
331 }
332 
333 
334 BString
Text(int32 start,int32 length) const335 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
SubDocument(int32 start,int32 length) const373 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
PrintToStream() const418 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
NormalizeText(const BString & text,CharacterStyle characterStyle,ParagraphStyle paragraphStyle)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
AddListener(TextListenerRef listener)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
RemoveListener(TextListenerRef listener)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
AddUndoListener(UndoableEditListenerRef listener)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
RemoveUndoListener(UndoableEditListenerRef listener)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
_Insert(int32 textOffset,TextDocumentRef document,int32 & index,int32 & paragraphCount)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
_Remove(int32 textOffset,int32 length,int32 & index,int32 & paragraphCount)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
_NotifyTextChanging(TextChangingEvent & event) const770 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
_NotifyTextChanged(const TextChangedEvent & event) const789 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
_NotifyUndoableEditHappened(const UndoableEditRef & edit) const805 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