1/*
2 * Copyright 2013-2014, Stephan Aßmus <superstippi@gmx.de>.
3 * All rights reserved. Distributed under the terms of the MIT License.
4 */
5
6#include "TextDocument.h"
7
8#include <algorithm>
9#include <stdio.h>
10
11
12TextDocument::TextDocument()
13	:
14	fParagraphs(),
15	fEmptyLastParagraph(),
16	fDefaultCharacterStyle()
17{
18}
19
20
21TextDocument::TextDocument(CharacterStyle characterStyle,
22	ParagraphStyle paragraphStyle)
23	:
24	fParagraphs(),
25	fEmptyLastParagraph(paragraphStyle),
26	fDefaultCharacterStyle(characterStyle)
27{
28}
29
30
31TextDocument::TextDocument(const TextDocument& other)
32	:
33	fParagraphs(other.fParagraphs),
34	fEmptyLastParagraph(other.fEmptyLastParagraph),
35	fDefaultCharacterStyle(other.fDefaultCharacterStyle)
36{
37}
38
39
40TextDocument&
41TextDocument::operator=(const TextDocument& other)
42{
43	fParagraphs = other.fParagraphs;
44	fEmptyLastParagraph = other.fEmptyLastParagraph;
45	fDefaultCharacterStyle = other.fDefaultCharacterStyle;
46
47	return *this;
48}
49
50
51bool
52TextDocument::operator==(const TextDocument& other) const
53{
54	if (this == &other)
55		return true;
56
57	return fEmptyLastParagraph == other.fEmptyLastParagraph
58		&& fDefaultCharacterStyle == other.fDefaultCharacterStyle
59		&& fParagraphs == other.fParagraphs;
60}
61
62
63bool
64TextDocument::operator!=(const TextDocument& other) const
65{
66	return !(*this == other);
67}
68
69
70// #pragma mark -
71
72
73status_t
74TextDocument::Insert(int32 textOffset, const BString& text)
75{
76	return Replace(textOffset, 0, text);
77}
78
79
80status_t
81TextDocument::Insert(int32 textOffset, const BString& text,
82	CharacterStyle style)
83{
84	return Replace(textOffset, 0, text, style);
85}
86
87
88status_t
89TextDocument::Insert(int32 textOffset, const BString& text,
90	CharacterStyle characterStyle, ParagraphStyle paragraphStyle)
91{
92	return Replace(textOffset, 0, text, characterStyle, paragraphStyle);
93}
94
95
96// #pragma mark -
97
98
99status_t
100TextDocument::Remove(int32 textOffset, int32 length)
101{
102	return Replace(textOffset, length, BString());
103}
104
105
106// #pragma mark -
107
108
109status_t
110TextDocument::Replace(int32 textOffset, int32 length, const BString& text)
111{
112	return Replace(textOffset, length, text, CharacterStyleAt(textOffset));
113}
114
115
116status_t
117TextDocument::Replace(int32 textOffset, int32 length, const BString& text,
118	CharacterStyle style)
119{
120	return Replace(textOffset, length, text, style,
121		ParagraphStyleAt(textOffset));
122}
123
124
125status_t
126TextDocument::Replace(int32 textOffset, int32 length, const BString& text,
127	CharacterStyle characterStyle, ParagraphStyle paragraphStyle)
128{
129	TextDocumentRef document = NormalizeText(text, characterStyle,
130		paragraphStyle);
131	if (document.Get() == NULL || document->Length() != text.CountChars())
132		return B_NO_MEMORY;
133	return Replace(textOffset, length, document);
134}
135
136
137status_t
138TextDocument::Replace(int32 textOffset, int32 length, TextDocumentRef document)
139{
140	int32 firstParagraph = 0;
141	int32 paragraphCount = 0;
142
143	// TODO: Call _NotifyTextChanging() before any change happened
144
145	status_t ret = _Remove(textOffset, length, firstParagraph, paragraphCount);
146	if (ret != B_OK)
147		return ret;
148
149	ret = _Insert(textOffset, document, firstParagraph, paragraphCount);
150
151	_NotifyTextChanged(TextChangedEvent(firstParagraph, paragraphCount));
152
153	return ret;
154}
155
156
157// #pragma mark -
158
159
160const CharacterStyle&
161TextDocument::CharacterStyleAt(int32 textOffset) const
162{
163	int32 paragraphOffset;
164	const Paragraph& paragraph = ParagraphAt(textOffset, paragraphOffset);
165
166	textOffset -= paragraphOffset;
167	const TextSpanList& spans = paragraph.TextSpans();
168
169	int32 index = 0;
170	while (index < spans.CountItems()) {
171		const TextSpan& span = spans.ItemAtFast(index);
172		if (textOffset - span.CountChars() < 0)
173			return span.Style();
174		textOffset -= span.CountChars();
175		index++;
176	}
177
178	return fDefaultCharacterStyle;
179}
180
181
182const ParagraphStyle&
183TextDocument::ParagraphStyleAt(int32 textOffset) const
184{
185	int32 paragraphOffset;
186	return ParagraphAt(textOffset, paragraphOffset).Style();
187}
188
189
190// #pragma mark -
191
192
193int32
194TextDocument::CountParagraphs() const
195{
196	return fParagraphs.CountItems();
197}
198
199
200int32
201TextDocument::ParagraphIndexFor(int32 textOffset, int32& paragraphOffset) const
202{
203	// TODO: Could binary search the Paragraphs if they were wrapped in classes
204	// that knew there text offset in the document.
205	int32 textLength = 0;
206	paragraphOffset = 0;
207	int32 count = fParagraphs.CountItems();
208	for (int32 i = 0; i < count; i++) {
209		const Paragraph& paragraph = fParagraphs.ItemAtFast(i);
210		int32 paragraphLength = paragraph.Length();
211		textLength += paragraphLength;
212		if (textLength > textOffset
213			|| (i == count - 1 && textLength == textOffset)) {
214			return i;
215		}
216		paragraphOffset += paragraphLength;
217	}
218	return -1;
219}
220
221
222const Paragraph&
223TextDocument::ParagraphAt(int32 textOffset, int32& paragraphOffset) const
224{
225	int32 index = ParagraphIndexFor(textOffset, paragraphOffset);
226	if (index >= 0)
227		return fParagraphs.ItemAtFast(index);
228
229	return fEmptyLastParagraph;
230}
231
232
233const Paragraph&
234TextDocument::ParagraphAt(int32 index) const
235{
236	if (index >= 0 && index < fParagraphs.CountItems())
237		return fParagraphs.ItemAtFast(index);
238	return fEmptyLastParagraph;
239}
240
241
242bool
243TextDocument::Append(const Paragraph& paragraph)
244{
245	return fParagraphs.Add(paragraph);
246}
247
248
249int32
250TextDocument::Length() const
251{
252	// TODO: Could be O(1) if the Paragraphs were wrapped in classes that
253	// knew their text offset in the document.
254	int32 textLength = 0;
255	int32 count = fParagraphs.CountItems();
256	for (int32 i = 0; i < count; i++) {
257		const Paragraph& paragraph = fParagraphs.ItemAtFast(i);
258		textLength += paragraph.Length();
259	}
260	return textLength;
261}
262
263
264BString
265TextDocument::Text() const
266{
267	return Text(0, Length());
268}
269
270
271BString
272TextDocument::Text(int32 start, int32 length) const
273{
274	if (start < 0)
275		start = 0;
276
277	BString text;
278
279	int32 count = fParagraphs.CountItems();
280	for (int32 i = 0; i < count; i++) {
281		const Paragraph& paragraph = fParagraphs.ItemAtFast(i);
282		int32 paragraphLength = paragraph.Length();
283		if (paragraphLength == 0)
284			continue;
285		if (start > paragraphLength) {
286			// Skip paragraph if its before start
287			start -= paragraphLength;
288			continue;
289		}
290
291		// Remaining paragraph length after start
292		paragraphLength -= start;
293		int32 copyLength = std::min(paragraphLength, length);
294
295		text << paragraph.Text(start, copyLength);
296
297		length -= copyLength;
298		if (length == 0)
299			break;
300
301		// Next paragraph is copied from its beginning
302		start = 0;
303	}
304
305	return text;
306}
307
308
309TextDocumentRef
310TextDocument::SubDocument(int32 start, int32 length) const
311{
312	TextDocumentRef result(new(std::nothrow) TextDocument(
313		fDefaultCharacterStyle, fEmptyLastParagraph.Style()), true);
314
315	if (result.Get() == NULL)
316		return result;
317
318	if (start < 0)
319		start = 0;
320
321	int32 count = fParagraphs.CountItems();
322	for (int32 i = 0; i < count; i++) {
323		const Paragraph& paragraph = fParagraphs.ItemAtFast(i);
324		int32 paragraphLength = paragraph.Length();
325		if (paragraphLength == 0)
326			continue;
327		if (start > paragraphLength) {
328			// Skip paragraph if its before start
329			start -= paragraphLength;
330			continue;
331		}
332
333		// Remaining paragraph length after start
334		paragraphLength -= start;
335		int32 copyLength = std::min(paragraphLength, length);
336
337		result->Append(paragraph.SubParagraph(start, copyLength));
338
339		length -= copyLength;
340		if (length == 0)
341			break;
342
343		// Next paragraph is copied from its beginning
344		start = 0;
345	}
346
347	return result;
348}
349
350
351// #pragma mark -
352
353
354void
355TextDocument::PrintToStream() const
356{
357	int32 paragraphCount = fParagraphs.CountItems();
358	if (paragraphCount == 0) {
359		printf("<document/>\n");
360		return;
361	}
362	printf("<document>\n");
363	for (int32 i = 0; i < paragraphCount; i++) {
364		fParagraphs.ItemAtFast(i).PrintToStream();
365	}
366	printf("</document>\n");
367}
368
369
370/*static*/ TextDocumentRef
371TextDocument::NormalizeText(const BString& text,
372	CharacterStyle characterStyle, ParagraphStyle paragraphStyle)
373{
374	TextDocumentRef document(new(std::nothrow) TextDocument(characterStyle,
375			paragraphStyle), true);
376	if (document.Get() == NULL)
377		throw B_NO_MEMORY;
378
379	Paragraph paragraph(paragraphStyle);
380
381	// Append TextSpans, splitting 'text' into Paragraphs at line breaks.
382	int32 length = text.CountChars();
383	int32 chunkStart = 0;
384	while (chunkStart < length) {
385		int32 chunkEnd = text.FindFirst('\n', chunkStart);
386		bool foundLineBreak = chunkEnd >= chunkStart;
387		if (foundLineBreak)
388			chunkEnd++;
389		else
390			chunkEnd = length;
391
392		BString chunk;
393		text.CopyCharsInto(chunk, chunkStart, chunkEnd - chunkStart);
394		TextSpan span(chunk, characterStyle);
395
396		if (!paragraph.Append(span))
397			throw B_NO_MEMORY;
398		if (paragraph.Length() > 0 && !document->Append(paragraph))
399			throw B_NO_MEMORY;
400
401		paragraph = Paragraph(paragraphStyle);
402		chunkStart = chunkEnd + 1;
403	}
404
405	return document;
406}
407
408
409// #pragma mark -
410
411
412bool
413TextDocument::AddListener(TextListenerRef listener)
414{
415	return fTextListeners.Add(listener);
416}
417
418
419bool
420TextDocument::RemoveListener(TextListenerRef listener)
421{
422	return fTextListeners.Remove(listener);
423}
424
425
426bool
427TextDocument::AddUndoListener(UndoableEditListenerRef listener)
428{
429	return fUndoListeners.Add(listener);
430}
431
432
433bool
434TextDocument::RemoveUndoListener(UndoableEditListenerRef listener)
435{
436	return fUndoListeners.Remove(listener);
437}
438
439
440// #pragma mark - private
441
442
443status_t
444TextDocument::_Insert(int32 textOffset, TextDocumentRef document,
445	int32& index, int32& paragraphCount)
446{
447	int32 paragraphOffset;
448	index = ParagraphIndexFor(textOffset, paragraphOffset);
449	if (index < 0)
450		return B_BAD_VALUE;
451
452	if (document->Length() == 0)
453		return B_OK;
454
455	textOffset -= paragraphOffset;
456
457	bool hasLineBreaks;
458	if (document->CountParagraphs() > 1) {
459		hasLineBreaks = true;
460	} else {
461		const Paragraph& paragraph = document->ParagraphAt(0);
462		hasLineBreaks = paragraph.EndsWith("\n");
463	}
464
465	if (hasLineBreaks) {
466		// Split paragraph at textOffset
467		Paragraph paragraph1(ParagraphAt(index).Style());
468		Paragraph paragraph2(document->ParagraphAt(
469			document->CountParagraphs() - 1).Style());
470		{
471			const TextSpanList& textSpans = ParagraphAt(index).TextSpans();
472			int32 spanCount = textSpans.CountItems();
473			for (int32 i = 0; i < spanCount; i++) {
474				const TextSpan& span = textSpans.ItemAtFast(i);
475				int32 spanLength = span.CountChars();
476				if (textOffset >= spanLength) {
477					if (!paragraph1.Append(span))
478						return B_NO_MEMORY;
479					textOffset -= spanLength;
480				} else if (textOffset > 0) {
481					if (!paragraph1.Append(
482							span.SubSpan(0, textOffset))
483						|| !paragraph2.Append(
484							span.SubSpan(textOffset,
485								spanLength - textOffset))) {
486						return B_NO_MEMORY;
487					}
488					textOffset = 0;
489				} else {
490					if (!paragraph2.Append(span))
491						return B_NO_MEMORY;
492				}
493			}
494		}
495
496		fParagraphs.Remove(index);
497
498		// Append first paragraph in other document to first part of
499		// paragraph at insert position
500		{
501			const Paragraph& otherParagraph = document->ParagraphAt(0);
502			const TextSpanList& textSpans = otherParagraph.TextSpans();
503			int32 spanCount = textSpans.CountItems();
504			for (int32 i = 0; i < spanCount; i++) {
505				const TextSpan& span = textSpans.ItemAtFast(i);
506				// TODO: Import/map CharacterStyles
507				if (!paragraph1.Append(span))
508					return B_NO_MEMORY;
509			}
510		}
511
512		// Insert the first paragraph-part again to the document
513		if (!fParagraphs.Add(paragraph1, index))
514			return B_NO_MEMORY;
515		paragraphCount++;
516
517		// Insert the other document's paragraph save for the last one
518		for (int32 i = 1; i < document->CountParagraphs() - 1; i++) {
519			const Paragraph& otherParagraph = document->ParagraphAt(i);
520			// TODO: Import/map CharacterStyles and ParagraphStyle
521			if (!fParagraphs.Add(otherParagraph, ++index))
522				return B_NO_MEMORY;
523			paragraphCount++;
524		}
525
526		int32 lastIndex = document->CountParagraphs() - 1;
527		if (lastIndex > 0) {
528			const Paragraph& otherParagraph = document->ParagraphAt(lastIndex);
529			if (otherParagraph.EndsWith("\n")) {
530				// TODO: Import/map CharacterStyles and ParagraphStyle
531				if (!fParagraphs.Add(otherParagraph, ++index))
532					return B_NO_MEMORY;
533			} else {
534				const TextSpanList& textSpans = otherParagraph.TextSpans();
535				int32 spanCount = textSpans.CountItems();
536				for (int32 i = 0; i < spanCount; i++) {
537					const TextSpan& span = textSpans.ItemAtFast(i);
538					// TODO: Import/map CharacterStyles
539					if (!paragraph2.Prepend(span))
540						return B_NO_MEMORY;
541				}
542			}
543		}
544
545		// Insert back the second paragraph-part
546		if (paragraph2.IsEmpty()) {
547			// Make sure Paragraph has at least one TextSpan, even
548			// if its empty. This handles the case of inserting a
549			// line-break at the end of the document. It than needs to
550			// have a new, empty paragraph at the end.
551			const TextSpanList& spans = paragraph1.TextSpans();
552			const TextSpan& span = spans.LastItem();
553			if (!paragraph2.Append(TextSpan("", span.Style())))
554				return B_NO_MEMORY;
555		}
556
557		if (!fParagraphs.Add(paragraph2, ++index))
558			return B_NO_MEMORY;
559
560		paragraphCount++;
561	} else {
562		Paragraph paragraph(ParagraphAt(index));
563		const Paragraph& otherParagraph = document->ParagraphAt(0);
564
565		const TextSpanList& textSpans = otherParagraph.TextSpans();
566		int32 spanCount = textSpans.CountItems();
567		for (int32 i = 0; i < spanCount; i++) {
568			const TextSpan& span = textSpans.ItemAtFast(i);
569			paragraph.Insert(textOffset, span);
570			textOffset += span.CountChars();
571		}
572
573		if (!fParagraphs.Replace(index, paragraph))
574			return B_NO_MEMORY;
575
576		paragraphCount++;
577	}
578
579	return B_OK;
580}
581
582
583status_t
584TextDocument::_Remove(int32 textOffset, int32 length, int32& index,
585	int32& paragraphCount)
586{
587	if (length == 0)
588		return B_OK;
589
590	int32 paragraphOffset;
591	index = ParagraphIndexFor(textOffset, paragraphOffset);
592	if (index < 0)
593		return B_BAD_VALUE;
594
595	textOffset -= paragraphOffset;
596	paragraphCount++;
597
598	// The paragraph at the text offset remains, even if the offset is at
599	// the beginning of that paragraph. The idea is that the selection start
600	// stays visually in the same place. Therefore, the paragraph at that
601	// offset has to keep the paragraph style from that paragraph.
602
603	Paragraph resultParagraph(ParagraphAt(index));
604	int32 paragraphLength = resultParagraph.Length();
605	if (textOffset == 0 && length > paragraphLength) {
606		length -= paragraphLength;
607		paragraphLength = 0;
608		resultParagraph.Clear();
609	} else {
610		int32 removeLength = std::min(length, paragraphLength - textOffset);
611		resultParagraph.Remove(textOffset, removeLength);
612		paragraphLength -= removeLength;
613		length -= removeLength;
614	}
615
616	if (textOffset == paragraphLength && length == 0
617		&& index + 1 < fParagraphs.CountItems()) {
618		// Line break between paragraphs got removed. Shift the next
619		// paragraph's text spans into the resulting one.
620
621		const TextSpanList&	textSpans = ParagraphAt(index + 1).TextSpans();
622		int32 spanCount = textSpans.CountItems();
623		for (int32 i = 0; i < spanCount; i++) {
624			const TextSpan& span = textSpans.ItemAtFast(i);
625			resultParagraph.Append(span);
626		}
627		fParagraphs.Remove(index + 1);
628		paragraphCount++;
629	}
630
631	textOffset = 0;
632
633	while (length > 0 && index + 1 < fParagraphs.CountItems()) {
634		paragraphCount++;
635		const Paragraph& paragraph = ParagraphAt(index + 1);
636		paragraphLength = paragraph.Length();
637		// Remove paragraph in any case. If some of it remains, the last
638		// paragraph to remove is reached, and the remaining spans are
639		// transfered to the result parahraph.
640		if (length >= paragraphLength) {
641			length -= paragraphLength;
642			fParagraphs.Remove(index);
643		} else {
644			// Last paragraph reached
645			int32 removedLength = std::min(length, paragraphLength);
646			Paragraph newParagraph(paragraph);
647			fParagraphs.Remove(index + 1);
648
649			if (!newParagraph.Remove(0, removedLength))
650				return B_NO_MEMORY;
651
652			// Transfer remaining spans to resultParagraph
653			const TextSpanList&	textSpans = newParagraph.TextSpans();
654			int32 spanCount = textSpans.CountItems();
655			for (int32 i = 0; i < spanCount; i++) {
656				const TextSpan& span = textSpans.ItemAtFast(i);
657				resultParagraph.Append(span);
658			}
659
660			break;
661		}
662	}
663
664	fParagraphs.Replace(index, resultParagraph);
665
666	return B_OK;
667}
668
669
670// #pragma mark - notifications
671
672
673void
674TextDocument::_NotifyTextChanging(TextChangingEvent& event) const
675{
676	// Copy listener list to have a stable list in case listeners
677	// are added/removed from within the notification hook.
678	TextListenerList listeners(fTextListeners);
679	int32 count = listeners.CountItems();
680	for (int32 i = 0; i < count; i++) {
681		const TextListenerRef& listener = listeners.ItemAtFast(i);
682		if (listener.Get() == NULL)
683			continue;
684		listener->TextChanging(event);
685		if (event.IsCanceled())
686			break;
687	}
688}
689
690
691void
692TextDocument::_NotifyTextChanged(const TextChangedEvent& event) const
693{
694	// Copy listener list to have a stable list in case listeners
695	// are added/removed from within the notification hook.
696	TextListenerList listeners(fTextListeners);
697	int32 count = listeners.CountItems();
698	for (int32 i = 0; i < count; i++) {
699		const TextListenerRef& listener = listeners.ItemAtFast(i);
700		if (listener.Get() == NULL)
701			continue;
702		listener->TextChanged(event);
703	}
704}
705
706
707void
708TextDocument::_NotifyUndoableEditHappened(const UndoableEditRef& edit) const
709{
710	// Copy listener list to have a stable list in case listeners
711	// are added/removed from within the notification hook.
712	UndoListenerList listeners(fUndoListeners);
713	int32 count = listeners.CountItems();
714	for (int32 i = 0; i < count; i++) {
715		const UndoableEditListenerRef& listener = listeners.ItemAtFast(i);
716		if (listener.Get() == NULL)
717			continue;
718		listener->UndoableEditHappened(this, edit);
719	}
720}
721