xref: /haiku/src/apps/haikudepot/textview/TextDocument.cpp (revision 410ed2fbba58819ac21e27d3676739728416761d)
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