/* * Copyright 2013-2014, Stephan Aßmus . * Copyright 2021, Andrew Lindesay . * All rights reserved. Distributed under the terms of the MIT License. */ #include "TextDocument.h" #include #include #include TextDocument::TextDocument() : fParagraphs(), fEmptyLastParagraph(), fDefaultCharacterStyle() { } TextDocument::TextDocument(CharacterStyle characterStyle, ParagraphStyle paragraphStyle) : fParagraphs(), fEmptyLastParagraph(paragraphStyle), fDefaultCharacterStyle(characterStyle) { } TextDocument::TextDocument(const TextDocument& other) : fParagraphs(other.fParagraphs), fEmptyLastParagraph(other.fEmptyLastParagraph), fDefaultCharacterStyle(other.fDefaultCharacterStyle) { } TextDocument& TextDocument::operator=(const TextDocument& other) { fParagraphs = other.fParagraphs; fEmptyLastParagraph = other.fEmptyLastParagraph; fDefaultCharacterStyle = other.fDefaultCharacterStyle; return *this; } bool TextDocument::operator==(const TextDocument& other) const { if (this == &other) return true; return fEmptyLastParagraph == other.fEmptyLastParagraph && fDefaultCharacterStyle == other.fDefaultCharacterStyle && fParagraphs == other.fParagraphs; } bool TextDocument::operator!=(const TextDocument& other) const { return !(*this == other); } // #pragma mark - status_t TextDocument::Insert(int32 textOffset, const BString& text) { return Replace(textOffset, 0, text); } status_t TextDocument::Insert(int32 textOffset, const BString& text, CharacterStyle style) { return Replace(textOffset, 0, text, style); } status_t TextDocument::Insert(int32 textOffset, const BString& text, CharacterStyle characterStyle, ParagraphStyle paragraphStyle) { return Replace(textOffset, 0, text, characterStyle, paragraphStyle); } // #pragma mark - status_t TextDocument::Remove(int32 textOffset, int32 length) { return Replace(textOffset, length, BString()); } // #pragma mark - status_t TextDocument::Replace(int32 textOffset, int32 length, const BString& text) { return Replace(textOffset, length, text, CharacterStyleAt(textOffset)); } status_t TextDocument::Replace(int32 textOffset, int32 length, const BString& text, CharacterStyle style) { return Replace(textOffset, length, text, style, ParagraphStyleAt(textOffset)); } status_t TextDocument::Replace(int32 textOffset, int32 length, const BString& text, CharacterStyle characterStyle, ParagraphStyle paragraphStyle) { TextDocumentRef document = NormalizeText(text, characterStyle, paragraphStyle); if (!document.IsSet() || document->Length() != text.CountChars()) return B_NO_MEMORY; return Replace(textOffset, length, document); } status_t TextDocument::Replace(int32 textOffset, int32 length, TextDocumentRef document) { int32 firstParagraph = 0; int32 paragraphCount = 0; // TODO: Call _NotifyTextChanging() before any change happened status_t ret = _Remove(textOffset, length, firstParagraph, paragraphCount); if (ret != B_OK) return ret; ret = _Insert(textOffset, document, firstParagraph, paragraphCount); _NotifyTextChanged(TextChangedEvent(firstParagraph, paragraphCount)); return ret; } // #pragma mark - const CharacterStyle& TextDocument::CharacterStyleAt(int32 textOffset) const { int32 paragraphOffset; const Paragraph& paragraph = ParagraphAt(textOffset, paragraphOffset); textOffset -= paragraphOffset; int32 index; int32 count = paragraph.CountTextSpans(); for (index = 0; index < count; index++) { const TextSpan& span = paragraph.TextSpanAtIndex(index); if (textOffset - span.CountChars() < 0) return span.Style(); textOffset -= span.CountChars(); } return fDefaultCharacterStyle; } const BMessage* TextDocument::ClickMessageAt(int32 textOffset) const { int32 paragraphOffset; const Paragraph& paragraph = ParagraphAt(textOffset, paragraphOffset); textOffset -= paragraphOffset; int32 index; int32 count = paragraph.CountTextSpans(); for (index = 0; index < count; index++) { const TextSpan& span = paragraph.TextSpanAtIndex(index); if (textOffset - span.CountChars() < 0) return span.ClickMessage(); textOffset -= span.CountChars(); } return NULL; } BCursor TextDocument::CursorAt(int32 textOffset) const { int32 paragraphOffset; const Paragraph& paragraph = ParagraphAt(textOffset, paragraphOffset); textOffset -= paragraphOffset; int32 index; int32 count = paragraph.CountTextSpans(); for (index = 0; index < count; index++) { const TextSpan& span = paragraph.TextSpanAtIndex(index); if (textOffset - span.CountChars() < 0) return span.Cursor(); textOffset -= span.CountChars(); } return BCursor((BMessage*)NULL); } const ParagraphStyle& TextDocument::ParagraphStyleAt(int32 textOffset) const { int32 paragraphOffset; return ParagraphAt(textOffset, paragraphOffset).Style(); } // #pragma mark - int32 TextDocument::CountParagraphs() const { return fParagraphs.size(); } const Paragraph& TextDocument::ParagraphAtIndex(int32 index) const { return fParagraphs[index]; } int32 TextDocument::ParagraphIndexFor(int32 textOffset, int32& paragraphOffset) const { // TODO: Could binary search the Paragraphs if they were wrapped in classes // that knew there text offset in the document. int32 textLength = 0; paragraphOffset = 0; int32 count = fParagraphs.size(); for (int32 i = 0; i < count; i++) { const Paragraph& paragraph = fParagraphs[i]; int32 paragraphLength = paragraph.Length(); textLength += paragraphLength; if (textLength > textOffset || (i == count - 1 && textLength == textOffset)) { return i; } paragraphOffset += paragraphLength; } return -1; } const Paragraph& TextDocument::ParagraphAt(int32 textOffset, int32& paragraphOffset) const { int32 index = ParagraphIndexFor(textOffset, paragraphOffset); if (index >= 0) return fParagraphs[index]; return fEmptyLastParagraph; } const Paragraph& TextDocument::ParagraphAt(int32 index) const { if (index >= 0 && index < static_cast(fParagraphs.size())) return fParagraphs[index]; return fEmptyLastParagraph; } bool TextDocument::Append(const Paragraph& paragraph) { try { fParagraphs.push_back(paragraph); } catch (std::bad_alloc& ba) { fprintf(stderr, "bad_alloc when adding a paragraph to a text " "document\n"); return false; } return true; } int32 TextDocument::Length() const { // TODO: Could be O(1) if the Paragraphs were wrapped in classes that // knew their text offset in the document. int32 textLength = 0; int32 count = fParagraphs.size(); for (int32 i = 0; i < count; i++) { const Paragraph& paragraph = fParagraphs[i]; textLength += paragraph.Length(); } return textLength; } BString TextDocument::Text() const { return Text(0, Length()); } BString TextDocument::Text(int32 start, int32 length) const { if (start < 0) start = 0; BString text; int32 count = fParagraphs.size(); for (int32 i = 0; i < count; i++) { const Paragraph& paragraph = fParagraphs[i]; int32 paragraphLength = paragraph.Length(); if (paragraphLength == 0) continue; if (start > paragraphLength) { // Skip paragraph if its before start start -= paragraphLength; continue; } // Remaining paragraph length after start paragraphLength -= start; int32 copyLength = std::min(paragraphLength, length); text << paragraph.Text(start, copyLength); length -= copyLength; if (length == 0) break; // Next paragraph is copied from its beginning start = 0; } return text; } TextDocumentRef TextDocument::SubDocument(int32 start, int32 length) const { TextDocumentRef result(new(std::nothrow) TextDocument( fDefaultCharacterStyle, fEmptyLastParagraph.Style()), true); if (!result.IsSet()) return result; if (start < 0) start = 0; int32 count = fParagraphs.size(); for (int32 i = 0; i < count; i++) { const Paragraph& paragraph = fParagraphs[i]; int32 paragraphLength = paragraph.Length(); if (paragraphLength == 0) continue; if (start > paragraphLength) { // Skip paragraph if its before start start -= paragraphLength; continue; } // Remaining paragraph length after start paragraphLength -= start; int32 copyLength = std::min(paragraphLength, length); result->Append(paragraph.SubParagraph(start, copyLength)); length -= copyLength; if (length == 0) break; // Next paragraph is copied from its beginning start = 0; } return result; } // #pragma mark - void TextDocument::PrintToStream() const { int32 paragraphCount = fParagraphs.size(); if (paragraphCount == 0) { printf("\n"); return; } printf("\n"); for (int32 i = 0; i < paragraphCount; i++) { fParagraphs[i].PrintToStream(); } printf("\n"); } /*static*/ TextDocumentRef TextDocument::NormalizeText(const BString& text, CharacterStyle characterStyle, ParagraphStyle paragraphStyle) { TextDocumentRef document(new(std::nothrow) TextDocument(characterStyle, paragraphStyle), true); if (!document.IsSet()) throw B_NO_MEMORY; Paragraph paragraph(paragraphStyle); // Append TextSpans, splitting 'text' into Paragraphs at line breaks. int32 length = text.CountChars(); int32 chunkStart = 0; while (chunkStart < length) { int32 chunkEnd = text.FindFirst('\n', chunkStart); bool foundLineBreak = chunkEnd >= chunkStart; if (foundLineBreak) chunkEnd++; else chunkEnd = length; BString chunk; text.CopyCharsInto(chunk, chunkStart, chunkEnd - chunkStart); TextSpan span(chunk, characterStyle); if (!paragraph.Append(span)) throw B_NO_MEMORY; if (paragraph.Length() > 0 && !document->Append(paragraph)) throw B_NO_MEMORY; paragraph = Paragraph(paragraphStyle); chunkStart = chunkEnd + 1; } return document; } // #pragma mark - bool TextDocument::AddListener(TextListenerRef listener) { try { fTextListeners.push_back(listener); } catch (std::bad_alloc& ba) { fprintf(stderr, "bad_alloc when adding a listener to a text " "document\n"); return false; } return true; } bool TextDocument::RemoveListener(TextListenerRef listener) { fTextListeners.erase(std::remove(fTextListeners.begin(), fTextListeners.end(), listener), fTextListeners.end()); return true; } bool TextDocument::AddUndoListener(UndoableEditListenerRef listener) { try { fUndoListeners.push_back(listener); } catch (std::bad_alloc& ba) { fprintf(stderr, "bad_alloc when adding an undo listener to a text " "document\n"); return false; } return true; } bool TextDocument::RemoveUndoListener(UndoableEditListenerRef listener) { fUndoListeners.erase(std::remove(fUndoListeners.begin(), fUndoListeners.end(), listener), fUndoListeners.end()); return true; } // #pragma mark - private status_t TextDocument::_Insert(int32 textOffset, TextDocumentRef document, int32& index, int32& paragraphCount) { int32 paragraphOffset; index = ParagraphIndexFor(textOffset, paragraphOffset); if (index < 0) return B_BAD_VALUE; if (document->Length() == 0) return B_OK; textOffset -= paragraphOffset; bool hasLineBreaks; if (document->CountParagraphs() > 1) { hasLineBreaks = true; } else { const Paragraph& paragraph = document->ParagraphAt(0); hasLineBreaks = paragraph.EndsWith("\n"); } if (hasLineBreaks) { // Split paragraph at textOffset Paragraph paragraph1(ParagraphAt(index).Style()); Paragraph paragraph2(document->ParagraphAt( document->CountParagraphs() - 1).Style()); { const Paragraph& paragraphAtIndex = ParagraphAt(index); int32 spanCount = paragraphAtIndex.CountTextSpans(); for (int32 i = 0; i < spanCount; i++) { const TextSpan& span = paragraphAtIndex.TextSpanAtIndex(i); int32 spanLength = span.CountChars(); if (textOffset >= spanLength) { if (!paragraph1.Append(span)) return B_NO_MEMORY; textOffset -= spanLength; } else if (textOffset > 0) { if (!paragraph1.Append( span.SubSpan(0, textOffset)) || !paragraph2.Append( span.SubSpan(textOffset, spanLength - textOffset))) { return B_NO_MEMORY; } textOffset = 0; } else { if (!paragraph2.Append(span)) return B_NO_MEMORY; } } } fParagraphs.erase(fParagraphs.begin() + index); // Append first paragraph in other document to first part of // paragraph at insert position { const Paragraph& otherParagraph = document->ParagraphAt(0); int32 spanCount = otherParagraph.CountTextSpans(); for (int32 i = 0; i < spanCount; i++) { const TextSpan& span = otherParagraph.TextSpanAtIndex(i); // TODO: Import/map CharacterStyles if (!paragraph1.Append(span)) return B_NO_MEMORY; } } // Insert the first paragraph-part again to the document try { fParagraphs.insert(fParagraphs.begin() + index, paragraph1); } catch (std::bad_alloc& ba) { return B_NO_MEMORY; } paragraphCount++; // Insert the other document's paragraph save for the last one for (int32 i = 1; i < document->CountParagraphs() - 1; i++) { const Paragraph& otherParagraph = document->ParagraphAt(i); // TODO: Import/map CharacterStyles and ParagraphStyle index++; try { fParagraphs.insert(fParagraphs.begin() + index, otherParagraph); } catch (std::bad_alloc& ba) { return B_NO_MEMORY; } paragraphCount++; } int32 lastIndex = document->CountParagraphs() - 1; if (lastIndex > 0) { const Paragraph& otherParagraph = document->ParagraphAt(lastIndex); if (otherParagraph.EndsWith("\n")) { // TODO: Import/map CharacterStyles and ParagraphStyle index++; try { fParagraphs.insert(fParagraphs.begin() + index, otherParagraph); } catch (std::bad_alloc& ba) { return B_NO_MEMORY; } } else { int32 spanCount = otherParagraph.CountTextSpans(); for (int32 i = 0; i < spanCount; i++) { const TextSpan& span = otherParagraph.TextSpanAtIndex(i); // TODO: Import/map CharacterStyles if (!paragraph2.Prepend(span)) return B_NO_MEMORY; } } } // Insert back the second paragraph-part if (paragraph2.IsEmpty()) { // Make sure Paragraph has at least one TextSpan, even // if its empty. This handles the case of inserting a // line-break at the end of the document. It than needs to // have a new, empty paragraph at the end. const int32 indexLastSpan = paragraph1.CountTextSpans() - 1; const TextSpan& span = paragraph1.TextSpanAtIndex(indexLastSpan); if (!paragraph2.Append(TextSpan("", span.Style()))) return B_NO_MEMORY; } index++; try { fParagraphs.insert(fParagraphs.begin() + index, paragraph2); } catch (std::bad_alloc& ba) { return B_NO_MEMORY; } paragraphCount++; } else { Paragraph paragraph(ParagraphAt(index)); const Paragraph& otherParagraph = document->ParagraphAt(0); int32 spanCount = otherParagraph.CountTextSpans(); for (int32 i = 0; i < spanCount; i++) { const TextSpan& span = otherParagraph.TextSpanAtIndex(i); paragraph.Insert(textOffset, span); textOffset += span.CountChars(); } fParagraphs[index] = paragraph; paragraphCount++; } return B_OK; } status_t TextDocument::_Remove(int32 textOffset, int32 length, int32& index, int32& paragraphCount) { if (length == 0) return B_OK; int32 paragraphOffset; index = ParagraphIndexFor(textOffset, paragraphOffset); if (index < 0) return B_BAD_VALUE; textOffset -= paragraphOffset; paragraphCount++; // The paragraph at the text offset remains, even if the offset is at // the beginning of that paragraph. The idea is that the selection start // stays visually in the same place. Therefore, the paragraph at that // offset has to keep the paragraph style from that paragraph. Paragraph resultParagraph(ParagraphAt(index)); int32 paragraphLength = resultParagraph.Length(); if (textOffset == 0 && length > paragraphLength) { length -= paragraphLength; paragraphLength = 0; resultParagraph.Clear(); } else { int32 removeLength = std::min(length, paragraphLength - textOffset); resultParagraph.Remove(textOffset, removeLength); paragraphLength -= removeLength; length -= removeLength; } if (textOffset == paragraphLength && length == 0 && index + 1 < static_cast(fParagraphs.size())) { // Line break between paragraphs got removed. Shift the next // paragraph's text spans into the resulting one. const Paragraph& paragraph = ParagraphAt(index + 1); int32 spanCount = paragraph.CountTextSpans(); for (int32 i = 0; i < spanCount; i++) { const TextSpan& span = paragraph.TextSpanAtIndex(i); resultParagraph.Append(span); } fParagraphs.erase(fParagraphs.begin() + (index + 1)); paragraphCount++; } textOffset = 0; while (length > 0 && index + 1 < static_cast(fParagraphs.size())) { paragraphCount++; const Paragraph& paragraph = ParagraphAt(index + 1); paragraphLength = paragraph.Length(); // Remove paragraph in any case. If some of it remains, the last // paragraph to remove is reached, and the remaining spans are // transfered to the result parahraph. if (length >= paragraphLength) { length -= paragraphLength; fParagraphs.erase(fParagraphs.begin() + index); } else { // Last paragraph reached int32 removedLength = std::min(length, paragraphLength); Paragraph newParagraph(paragraph); fParagraphs.erase(fParagraphs.begin() + (index + 1)); if (!newParagraph.Remove(0, removedLength)) return B_NO_MEMORY; // Transfer remaining spans to resultParagraph int32 spanCount = newParagraph.CountTextSpans(); for (int32 i = 0; i < spanCount; i++) { const TextSpan& span = newParagraph.TextSpanAtIndex(i); resultParagraph.Append(span); } break; } } fParagraphs[index] = resultParagraph; return B_OK; } // #pragma mark - notifications void TextDocument::_NotifyTextChanging(TextChangingEvent& event) const { // Copy listener list to have a stable list in case listeners // are added/removed from within the notification hook. std::vector listeners(fTextListeners); int32 count = listeners.size(); for (int32 i = 0; i < count; i++) { const TextListenerRef& listener = listeners[i]; if (!listener.IsSet()) continue; listener->TextChanging(event); if (event.IsCanceled()) break; } } void TextDocument::_NotifyTextChanged(const TextChangedEvent& event) const { // Copy listener list to have a stable list in case listeners // are added/removed from within the notification hook. std::vector listeners(fTextListeners); int32 count = listeners.size(); for (int32 i = 0; i < count; i++) { const TextListenerRef& listener = listeners[i]; if (!listener.IsSet()) continue; listener->TextChanged(event); } } void TextDocument::_NotifyUndoableEditHappened(const UndoableEditRef& edit) const { // Copy listener list to have a stable list in case listeners // are added/removed from within the notification hook. std::vector listeners(fUndoListeners); int32 count = listeners.size(); for (int32 i = 0; i < count; i++) { const UndoableEditListenerRef& listener = listeners[i]; if (!listener.IsSet()) continue; listener->UndoableEditHappened(this, edit); } }