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