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