summaryrefslogtreecommitdiffstats
path: root/editor/spellchecker/TextServicesDocument.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'editor/spellchecker/TextServicesDocument.cpp')
-rw-r--r--editor/spellchecker/TextServicesDocument.cpp2787
1 files changed, 2787 insertions, 0 deletions
diff --git a/editor/spellchecker/TextServicesDocument.cpp b/editor/spellchecker/TextServicesDocument.cpp
new file mode 100644
index 0000000000..1417cf79ae
--- /dev/null
+++ b/editor/spellchecker/TextServicesDocument.cpp
@@ -0,0 +1,2787 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "TextServicesDocument.h"
+
+#include "EditorBase.h" // for EditorBase
+#include "EditorUtils.h" // for AutoTransactionBatchExternal
+#include "FilteredContentIterator.h" // for FilteredContentIterator
+#include "HTMLEditHelpers.h" // for BlockInlineCheck
+#include "HTMLEditUtils.h" // for HTMLEditUtils
+
+#include "mozilla/Assertions.h" // for MOZ_ASSERT, etc
+#include "mozilla/IntegerRange.h" // for IntegerRange
+#include "mozilla/mozalloc.h" // for operator new, etc
+#include "mozilla/OwningNonNull.h"
+#include "mozilla/UniquePtr.h" // for UniquePtr
+#include "mozilla/dom/AbstractRange.h" // for AbstractRange
+#include "mozilla/dom/Element.h"
+#include "mozilla/dom/Selection.h"
+#include "mozilla/dom/StaticRange.h" // for StaticRange
+#include "mozilla/dom/Text.h"
+#include "mozilla/intl/WordBreaker.h" // for WordRange, WordBreaker
+
+#include "nsAString.h" // for nsAString::Length, etc
+#include "nsContentUtils.h" // for nsContentUtils
+#include "nsComposeTxtSrvFilter.h"
+#include "nsDebug.h" // for NS_ENSURE_TRUE, etc
+#include "nsDependentSubstring.h" // for Substring
+#include "nsError.h" // for NS_OK, NS_ERROR_FAILURE, etc
+#include "nsGenericHTMLElement.h" // for nsGenericHTMLElement
+#include "nsIContent.h" // for nsIContent, etc
+#include "nsID.h" // for NS_GET_IID
+#include "nsIEditor.h" // for nsIEditor, etc
+#include "nsIEditorSpellCheck.h" // for nsIEditorSpellCheck, etc
+#include "nsINode.h" // for nsINode
+#include "nsISelectionController.h" // for nsISelectionController, etc
+#include "nsISupports.h" // for nsISupports
+#include "nsISupportsUtils.h" // for NS_IF_ADDREF, NS_ADDREF, etc
+#include "nsRange.h" // for nsRange
+#include "nsString.h" // for nsString, nsAutoString
+#include "nscore.h" // for nsresult, NS_IMETHODIMP, etc
+
+namespace mozilla {
+
+using namespace dom;
+
+/**
+ * OffsetEntry manages a range in a text node. It stores 2 offset values,
+ * one is offset in the text node, the other is offset in all text in
+ * the ancestor block of the text node. And the length is managing length
+ * in the text node, starting from the offset in text node.
+ * In other words, a text node may be managed by multiple instances of this
+ * class.
+ */
+class OffsetEntry final {
+ public:
+ OffsetEntry() = delete;
+
+ /**
+ * @param aTextNode The text node which will be manged by the instance.
+ * @param aOffsetInTextInBlock
+ * Start offset in the text node which will be managed by
+ * the instance.
+ * @param aLength Length in the text node which will be managed by the
+ * instance.
+ */
+ OffsetEntry(Text& aTextNode, uint32_t aOffsetInTextInBlock, uint32_t aLength)
+ : mTextNode(aTextNode),
+ mOffsetInTextNode(0),
+ mOffsetInTextInBlock(aOffsetInTextInBlock),
+ mLength(aLength),
+ mIsInsertedText(false),
+ mIsValid(true) {}
+
+ /**
+ * EndOffsetInTextNode() returns end offset in the text node, which is
+ * managed by the instance.
+ */
+ uint32_t EndOffsetInTextNode() const { return mOffsetInTextNode + mLength; }
+
+ /**
+ * OffsetInTextNodeIsInRangeOrEndOffset() checks whether the offset in
+ * the text node is managed by the instance or not.
+ */
+ bool OffsetInTextNodeIsInRangeOrEndOffset(uint32_t aOffsetInTextNode) const {
+ return aOffsetInTextNode >= mOffsetInTextNode &&
+ aOffsetInTextNode <= EndOffsetInTextNode();
+ }
+
+ /**
+ * EndOffsetInTextInBlock() returns end offset in the all text in ancestor
+ * block of the text node, which is managed by the instance.
+ */
+ uint32_t EndOffsetInTextInBlock() const {
+ return mOffsetInTextInBlock + mLength;
+ }
+
+ /**
+ * OffsetInTextNodeIsInRangeOrEndOffset() checks whether the offset in
+ * the all text in ancestor block of the text node is managed by the instance
+ * or not.
+ */
+ bool OffsetInTextInBlockIsInRangeOrEndOffset(
+ uint32_t aOffsetInTextInBlock) const {
+ return aOffsetInTextInBlock >= mOffsetInTextInBlock &&
+ aOffsetInTextInBlock <= EndOffsetInTextInBlock();
+ }
+
+ OwningNonNull<Text> mTextNode;
+ uint32_t mOffsetInTextNode;
+ // Offset in all text in the closest ancestor block of mTextNode.
+ uint32_t mOffsetInTextInBlock;
+ uint32_t mLength;
+ bool mIsInsertedText;
+ bool mIsValid;
+};
+
+template <typename ElementType>
+struct MOZ_STACK_CLASS ArrayLengthMutationGuard final {
+ ArrayLengthMutationGuard() = delete;
+ explicit ArrayLengthMutationGuard(const nsTArray<ElementType>& aArray)
+ : mArray(aArray), mOldLength(aArray.Length()) {}
+ ~ArrayLengthMutationGuard() {
+ if (mArray.Length() != mOldLength) {
+ MOZ_CRASH("The array length was changed unexpectedly");
+ }
+ }
+
+ private:
+ const nsTArray<ElementType>& mArray;
+ size_t mOldLength;
+};
+
+#define LockOffsetEntryArrayLengthInDebugBuild(aName, aArray) \
+ DebugOnly<ArrayLengthMutationGuard<UniquePtr<OffsetEntry>>> const aName = \
+ ArrayLengthMutationGuard<UniquePtr<OffsetEntry>>(aArray);
+
+TextServicesDocument::TextServicesDocument()
+ : mTxtSvcFilterType(0), mIteratorStatus(IteratorStatus::eDone) {}
+
+NS_IMPL_CYCLE_COLLECTING_ADDREF(TextServicesDocument)
+NS_IMPL_CYCLE_COLLECTING_RELEASE(TextServicesDocument)
+
+NS_INTERFACE_MAP_BEGIN(TextServicesDocument)
+ NS_INTERFACE_MAP_ENTRY(nsIEditActionListener)
+ NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, nsIEditActionListener)
+ NS_INTERFACE_MAP_ENTRIES_CYCLE_COLLECTION(TextServicesDocument)
+NS_INTERFACE_MAP_END
+
+NS_IMPL_CYCLE_COLLECTION(TextServicesDocument, mDocument, mSelCon, mEditorBase,
+ mFilteredIter, mPrevTextBlock, mNextTextBlock, mExtent)
+
+nsresult TextServicesDocument::InitWithEditor(nsIEditor* aEditor) {
+ nsCOMPtr<nsISelectionController> selCon;
+
+ NS_ENSURE_TRUE(aEditor, NS_ERROR_NULL_POINTER);
+
+ // Check to see if we already have an mSelCon. If we do, it
+ // better be the same one the editor uses!
+
+ nsresult rv = aEditor->GetSelectionController(getter_AddRefs(selCon));
+
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+
+ if (!selCon || (mSelCon && selCon != mSelCon)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ if (!mSelCon) {
+ mSelCon = selCon;
+ }
+
+ // Check to see if we already have an mDocument. If we do, it
+ // better be the same one the editor uses!
+
+ RefPtr<Document> doc = aEditor->AsEditorBase()->GetDocument();
+ if (!doc || (mDocument && doc != mDocument)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ if (!mDocument) {
+ mDocument = doc;
+
+ rv = CreateDocumentContentIterator(getter_AddRefs(mFilteredIter));
+
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+
+ mIteratorStatus = IteratorStatus::eDone;
+
+ rv = FirstBlock();
+
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+ }
+
+ mEditorBase = aEditor->AsEditorBase();
+
+ rv = aEditor->AddEditActionListener(this);
+
+ return rv;
+}
+
+nsresult TextServicesDocument::SetExtent(const AbstractRange* aAbstractRange) {
+ MOZ_ASSERT(aAbstractRange);
+
+ if (NS_WARN_IF(!mDocument)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ // We need to store a copy of aAbstractRange since we don't know where it
+ // came from.
+ mExtent = nsRange::Create(aAbstractRange, IgnoreErrors());
+ if (NS_WARN_IF(!mExtent)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ // Create a new iterator based on our new extent range.
+ nsresult rv =
+ CreateFilteredContentIterator(mExtent, getter_AddRefs(mFilteredIter));
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ // Now position the iterator at the start of the first block
+ // in the range.
+ mIteratorStatus = IteratorStatus::eDone;
+
+ rv = FirstBlock();
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "FirstBlock() failed");
+ return rv;
+}
+
+nsresult TextServicesDocument::ExpandRangeToWordBoundaries(
+ StaticRange* aStaticRange) {
+ MOZ_ASSERT(aStaticRange);
+
+ // Get the end points of the range.
+
+ nsCOMPtr<nsINode> rngStartNode, rngEndNode;
+ uint32_t rngStartOffset, rngEndOffset;
+
+ nsresult rv = GetRangeEndPoints(aStaticRange, getter_AddRefs(rngStartNode),
+ &rngStartOffset, getter_AddRefs(rngEndNode),
+ &rngEndOffset);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ // Create a content iterator based on the range.
+ RefPtr<FilteredContentIterator> filteredIter;
+ rv =
+ CreateFilteredContentIterator(aStaticRange, getter_AddRefs(filteredIter));
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ // Find the first text node in the range.
+ IteratorStatus iterStatus = IteratorStatus::eDone;
+ rv = FirstTextNode(filteredIter, &iterStatus);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ if (iterStatus == IteratorStatus::eDone) {
+ // No text was found so there's no adjustment necessary!
+ return NS_OK;
+ }
+
+ nsINode* firstText = filteredIter->GetCurrentNode();
+ if (NS_WARN_IF(!firstText)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ // Find the last text node in the range.
+
+ rv = LastTextNode(filteredIter, &iterStatus);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ if (iterStatus == IteratorStatus::eDone) {
+ // We should never get here because a first text block
+ // was found above.
+ NS_ASSERTION(false, "Found a first without a last!");
+ return NS_ERROR_FAILURE;
+ }
+
+ nsINode* lastText = filteredIter->GetCurrentNode();
+ if (NS_WARN_IF(!lastText)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ // Now make sure our end points are in terms of text nodes in the range!
+
+ if (rngStartNode != firstText) {
+ // The range includes the start of the first text node!
+ rngStartNode = firstText;
+ rngStartOffset = 0;
+ }
+
+ if (rngEndNode != lastText) {
+ // The range includes the end of the last text node!
+ rngEndNode = lastText;
+ rngEndOffset = lastText->Length();
+ }
+
+ // Create a doc iterator so that we can scan beyond
+ // the bounds of the extent range.
+
+ RefPtr<FilteredContentIterator> docFilteredIter;
+ rv = CreateDocumentContentIterator(getter_AddRefs(docFilteredIter));
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ // Grab all the text in the block containing our
+ // first text node.
+ rv = docFilteredIter->PositionAt(firstText);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ iterStatus = IteratorStatus::eValid;
+
+ OffsetEntryArray offsetTable;
+ nsAutoString blockStr;
+ Result<IteratorStatus, nsresult> result = offsetTable.Init(
+ *docFilteredIter, IteratorStatus::eValid, nullptr, &blockStr);
+ if (result.isErr()) {
+ return result.unwrapErr();
+ }
+
+ Result<EditorDOMRangeInTexts, nsresult> maybeWordRange =
+ offsetTable.FindWordRange(
+ blockStr, EditorRawDOMPoint(rngStartNode, rngStartOffset));
+ offsetTable.Clear();
+ if (maybeWordRange.isErr()) {
+ NS_WARNING(
+ "TextServicesDocument::OffsetEntryArray::FindWordRange() failed");
+ return maybeWordRange.unwrapErr();
+ }
+ rngStartNode = maybeWordRange.inspect().StartRef().GetContainerAs<Text>();
+ rngStartOffset = maybeWordRange.inspect().StartRef().Offset();
+
+ // Grab all the text in the block containing our
+ // last text node.
+
+ rv = docFilteredIter->PositionAt(lastText);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ result = offsetTable.Init(*docFilteredIter, IteratorStatus::eValid, nullptr,
+ &blockStr);
+ if (result.isErr()) {
+ return result.unwrapErr();
+ }
+
+ maybeWordRange = offsetTable.FindWordRange(
+ blockStr, EditorRawDOMPoint(rngEndNode, rngEndOffset));
+ offsetTable.Clear();
+ if (maybeWordRange.isErr()) {
+ NS_WARNING(
+ "TextServicesDocument::OffsetEntryArray::FindWordRange() failed");
+ return maybeWordRange.unwrapErr();
+ }
+
+ // To prevent expanding the range too much, we only change
+ // rngEndNode and rngEndOffset if it isn't already at the start of the
+ // word and isn't equivalent to rngStartNode and rngStartOffset.
+
+ if (rngEndNode !=
+ maybeWordRange.inspect().StartRef().GetContainerAs<Text>() ||
+ rngEndOffset != maybeWordRange.inspect().StartRef().Offset() ||
+ (rngEndNode == rngStartNode && rngEndOffset == rngStartOffset)) {
+ rngEndNode = maybeWordRange.inspect().EndRef().GetContainerAs<Text>();
+ rngEndOffset = maybeWordRange.inspect().EndRef().Offset();
+ }
+
+ // Now adjust the range so that it uses our new end points.
+ rv = aStaticRange->SetStartAndEnd(rngStartNode, rngStartOffset, rngEndNode,
+ rngEndOffset);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "Failed to update the given range");
+ return rv;
+}
+
+nsresult TextServicesDocument::SetFilterType(uint32_t aFilterType) {
+ mTxtSvcFilterType = aFilterType;
+
+ return NS_OK;
+}
+
+nsresult TextServicesDocument::GetCurrentTextBlock(nsAString& aStr) {
+ aStr.Truncate();
+
+ NS_ENSURE_TRUE(mFilteredIter, NS_ERROR_FAILURE);
+
+ Result<IteratorStatus, nsresult> result =
+ mOffsetTable.Init(*mFilteredIter, mIteratorStatus, mExtent, &aStr);
+ if (result.isErr()) {
+ NS_WARNING("OffsetEntryArray::Init() failed");
+ return result.unwrapErr();
+ }
+ mIteratorStatus = result.unwrap();
+ return NS_OK;
+}
+
+nsresult TextServicesDocument::FirstBlock() {
+ NS_ENSURE_TRUE(mFilteredIter, NS_ERROR_FAILURE);
+
+ nsresult rv = FirstTextNode(mFilteredIter, &mIteratorStatus);
+
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+
+ // Keep track of prev and next blocks, just in case
+ // the text service blows away the current block.
+
+ if (mIteratorStatus == IteratorStatus::eValid) {
+ mPrevTextBlock = nullptr;
+ rv = GetFirstTextNodeInNextBlock(getter_AddRefs(mNextTextBlock));
+ } else {
+ // There's no text block in the document!
+
+ mPrevTextBlock = nullptr;
+ mNextTextBlock = nullptr;
+ }
+
+ // XXX Result of FirstTextNode() or GetFirstTextNodeInNextBlock().
+ return rv;
+}
+
+nsresult TextServicesDocument::LastSelectedBlock(
+ BlockSelectionStatus* aSelStatus, uint32_t* aSelOffset,
+ uint32_t* aSelLength) {
+ NS_ENSURE_TRUE(aSelStatus && aSelOffset && aSelLength, NS_ERROR_NULL_POINTER);
+
+ mIteratorStatus = IteratorStatus::eDone;
+
+ *aSelStatus = BlockSelectionStatus::eBlockNotFound;
+ *aSelOffset = *aSelLength = UINT32_MAX;
+
+ if (!mSelCon || !mFilteredIter) {
+ return NS_ERROR_FAILURE;
+ }
+
+ RefPtr<Selection> selection =
+ mSelCon->GetSelection(nsISelectionController::SELECTION_NORMAL);
+ if (NS_WARN_IF(!selection)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ RefPtr<const nsRange> range;
+ nsCOMPtr<nsINode> parent;
+
+ if (selection->IsCollapsed()) {
+ // We have a caret. Check if the caret is in a text node.
+ // If it is, make the text node's block the current block.
+ // If the caret isn't in a text node, search forwards in
+ // the document, till we find a text node.
+
+ range = selection->GetRangeAt(0);
+ if (!range) {
+ return NS_ERROR_FAILURE;
+ }
+
+ parent = range->GetStartContainer();
+ if (!parent) {
+ return NS_ERROR_FAILURE;
+ }
+
+ nsresult rv;
+ if (parent->IsText()) {
+ // The caret is in a text node. Find the beginning
+ // of the text block containing this text node and
+ // return.
+
+ rv = mFilteredIter->PositionAt(parent->AsText());
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+
+ rv = FirstTextNodeInCurrentBlock(mFilteredIter);
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+
+ Result<IteratorStatus, nsresult> result =
+ mOffsetTable.Init(*mFilteredIter, IteratorStatus::eValid, mExtent);
+ if (result.isErr()) {
+ NS_WARNING("OffsetEntryArray::Init() failed");
+ mIteratorStatus = IteratorStatus::eValid; // XXX
+ return result.unwrapErr();
+ }
+ mIteratorStatus = result.unwrap();
+
+ rv = GetSelection(aSelStatus, aSelOffset, aSelLength);
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+
+ if (*aSelStatus == BlockSelectionStatus::eBlockContains) {
+ rv = SetSelectionInternal(*aSelOffset, *aSelLength, false);
+ }
+ } else {
+ // The caret isn't in a text node. Create an iterator
+ // based on a range that extends from the current caret
+ // position to the end of the document, then walk forwards
+ // till you find a text node, then find the beginning of it's block.
+
+ range = CreateDocumentContentRootToNodeOffsetRange(
+ parent, range->StartOffset(), false);
+ if (NS_WARN_IF(!range)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ if (range->Collapsed()) {
+ // If we get here, the range is collapsed because there is nothing after
+ // the caret! Just return NS_OK;
+ return NS_OK;
+ }
+
+ RefPtr<FilteredContentIterator> filteredIter;
+ rv = CreateFilteredContentIterator(range, getter_AddRefs(filteredIter));
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+
+ filteredIter->First();
+
+ Text* textNode = nullptr;
+ for (; !filteredIter->IsDone(); filteredIter->Next()) {
+ nsINode* currentNode = filteredIter->GetCurrentNode();
+ if (currentNode->IsText()) {
+ textNode = currentNode->AsText();
+ break;
+ }
+ }
+
+ if (!textNode) {
+ return NS_OK;
+ }
+
+ rv = mFilteredIter->PositionAt(textNode);
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+
+ rv = FirstTextNodeInCurrentBlock(mFilteredIter);
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+
+ Result<IteratorStatus, nsresult> result = mOffsetTable.Init(
+ *mFilteredIter, IteratorStatus::eValid, mExtent, nullptr);
+ if (result.isErr()) {
+ NS_WARNING("OffsetEntryArray::Init() failed");
+ mIteratorStatus = IteratorStatus::eValid; // XXX
+ return result.unwrapErr();
+ }
+ mIteratorStatus = result.inspect();
+
+ rv = GetSelection(aSelStatus, aSelOffset, aSelLength);
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+ }
+
+ // Result of SetSelectionInternal() in the |if| block or NS_OK.
+ return rv;
+ }
+
+ // If we get here, we have an uncollapsed selection!
+ // Look backwards through each range in the selection till you
+ // find the first text node. If you find one, find the
+ // beginning of its text block, and make it the current
+ // block.
+
+ const uint32_t rangeCount = selection->RangeCount();
+ MOZ_ASSERT(
+ rangeCount,
+ "Selection is not collapsed, so, the range count should be 1 or larger");
+
+ // XXX: We may need to add some code here to make sure
+ // the ranges are sorted in document appearance order!
+
+ for (const uint32_t i : Reversed(IntegerRange(rangeCount))) {
+ MOZ_ASSERT(selection->RangeCount() == rangeCount);
+ range = selection->GetRangeAt(i);
+ if (MOZ_UNLIKELY(!range)) {
+ return NS_OK; // XXX Really?
+ }
+
+ // Create an iterator for the range.
+
+ RefPtr<FilteredContentIterator> filteredIter;
+ nsresult rv =
+ CreateFilteredContentIterator(range, getter_AddRefs(filteredIter));
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+
+ filteredIter->Last();
+
+ // Now walk through the range till we find a text node.
+
+ for (; !filteredIter->IsDone(); filteredIter->Prev()) {
+ if (filteredIter->GetCurrentNode()->NodeType() == nsINode::TEXT_NODE) {
+ // We found a text node, so position the document's
+ // iterator at the beginning of the block, then get
+ // the selection in terms of the string offset.
+
+ nsresult rv = mFilteredIter->PositionAt(filteredIter->GetCurrentNode());
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+
+ rv = FirstTextNodeInCurrentBlock(mFilteredIter);
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+
+ mIteratorStatus = IteratorStatus::eValid;
+
+ Result<IteratorStatus, nsresult> result =
+ mOffsetTable.Init(*mFilteredIter, IteratorStatus::eValid, mExtent);
+ if (result.isErr()) {
+ NS_WARNING("OffsetEntryArray::Init() failed");
+ mIteratorStatus = IteratorStatus::eValid; // XXX
+ return result.unwrapErr();
+ }
+ mIteratorStatus = result.unwrap();
+
+ return GetSelection(aSelStatus, aSelOffset, aSelLength);
+ }
+ }
+ }
+
+ // If we get here, we didn't find any text node in the selection!
+ // Create a range that extends from the end of the selection,
+ // to the end of the document, then iterate forwards through
+ // it till you find a text node!
+ range = rangeCount > 0 ? selection->GetRangeAt(rangeCount - 1) : nullptr;
+ if (!range) {
+ return NS_ERROR_FAILURE;
+ }
+
+ parent = range->GetEndContainer();
+ if (!parent) {
+ return NS_ERROR_FAILURE;
+ }
+
+ range = CreateDocumentContentRootToNodeOffsetRange(parent, range->EndOffset(),
+ false);
+ if (NS_WARN_IF(!range)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ if (range->Collapsed()) {
+ // If we get here, the range is collapsed because there is nothing after
+ // the current selection! Just return NS_OK;
+ return NS_OK;
+ }
+
+ RefPtr<FilteredContentIterator> filteredIter;
+ nsresult rv =
+ CreateFilteredContentIterator(range, getter_AddRefs(filteredIter));
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+
+ filteredIter->First();
+
+ for (; !filteredIter->IsDone(); filteredIter->Next()) {
+ if (filteredIter->GetCurrentNode()->NodeType() == nsINode::TEXT_NODE) {
+ // We found a text node! Adjust the document's iterator to point
+ // to the beginning of its text block, then get the current selection.
+ nsresult rv = mFilteredIter->PositionAt(filteredIter->GetCurrentNode());
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+
+ rv = FirstTextNodeInCurrentBlock(mFilteredIter);
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+
+ Result<IteratorStatus, nsresult> result =
+ mOffsetTable.Init(*mFilteredIter, IteratorStatus::eValid, mExtent);
+ if (result.isErr()) {
+ NS_WARNING("OffsetEntryArray::Init() failed");
+ mIteratorStatus = IteratorStatus::eValid; // XXX
+ return result.unwrapErr();
+ }
+ mIteratorStatus = result.unwrap();
+
+ rv = GetSelection(aSelStatus, aSelOffset, aSelLength);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "TextServicesDocument::GetSelection() failed");
+ return rv;
+ }
+ }
+
+ // If we get here, we didn't find any block before or inside
+ // the selection! Just return OK.
+ return NS_OK;
+}
+
+nsresult TextServicesDocument::PrevBlock() {
+ NS_ENSURE_TRUE(mFilteredIter, NS_ERROR_FAILURE);
+
+ if (mIteratorStatus == IteratorStatus::eDone) {
+ return NS_OK;
+ }
+
+ switch (mIteratorStatus) {
+ case IteratorStatus::eValid:
+ case IteratorStatus::eNext: {
+ nsresult rv = FirstTextNodeInPrevBlock(mFilteredIter);
+
+ if (NS_FAILED(rv)) {
+ mIteratorStatus = IteratorStatus::eDone;
+ return rv;
+ }
+
+ if (mFilteredIter->IsDone()) {
+ mIteratorStatus = IteratorStatus::eDone;
+ return NS_OK;
+ }
+
+ mIteratorStatus = IteratorStatus::eValid;
+ break;
+ }
+ case IteratorStatus::ePrev:
+
+ // The iterator already points to the previous
+ // block, so don't do anything.
+
+ mIteratorStatus = IteratorStatus::eValid;
+ break;
+
+ default:
+
+ mIteratorStatus = IteratorStatus::eDone;
+ break;
+ }
+
+ // Keep track of prev and next blocks, just in case
+ // the text service blows away the current block.
+ nsresult rv = NS_OK;
+ if (mIteratorStatus == IteratorStatus::eValid) {
+ GetFirstTextNodeInPrevBlock(getter_AddRefs(mPrevTextBlock));
+ rv = GetFirstTextNodeInNextBlock(getter_AddRefs(mNextTextBlock));
+ } else {
+ // We must be done!
+ mPrevTextBlock = nullptr;
+ mNextTextBlock = nullptr;
+ }
+
+ // XXX The result of GetFirstTextNodeInNextBlock() or NS_OK.
+ return rv;
+}
+
+nsresult TextServicesDocument::NextBlock() {
+ NS_ENSURE_TRUE(mFilteredIter, NS_ERROR_FAILURE);
+
+ if (mIteratorStatus == IteratorStatus::eDone) {
+ return NS_OK;
+ }
+
+ switch (mIteratorStatus) {
+ case IteratorStatus::eValid: {
+ // Advance the iterator to the next text block.
+
+ nsresult rv = FirstTextNodeInNextBlock(mFilteredIter);
+
+ if (NS_FAILED(rv)) {
+ mIteratorStatus = IteratorStatus::eDone;
+ return rv;
+ }
+
+ if (mFilteredIter->IsDone()) {
+ mIteratorStatus = IteratorStatus::eDone;
+ return NS_OK;
+ }
+
+ mIteratorStatus = IteratorStatus::eValid;
+ break;
+ }
+ case IteratorStatus::eNext:
+
+ // The iterator already points to the next block,
+ // so don't do anything to it!
+
+ mIteratorStatus = IteratorStatus::eValid;
+ break;
+
+ case IteratorStatus::ePrev:
+
+ // If the iterator is pointing to the previous block,
+ // we know that there is no next text block! Just
+ // fall through to the default case!
+
+ default:
+
+ mIteratorStatus = IteratorStatus::eDone;
+ break;
+ }
+
+ // Keep track of prev and next blocks, just in case
+ // the text service blows away the current block.
+ nsresult rv = NS_OK;
+ if (mIteratorStatus == IteratorStatus::eValid) {
+ GetFirstTextNodeInPrevBlock(getter_AddRefs(mPrevTextBlock));
+ rv = GetFirstTextNodeInNextBlock(getter_AddRefs(mNextTextBlock));
+ } else {
+ // We must be done.
+ mPrevTextBlock = nullptr;
+ mNextTextBlock = nullptr;
+ }
+
+ // The result of GetFirstTextNodeInNextBlock() or NS_OK.
+ return rv;
+}
+
+nsresult TextServicesDocument::IsDone(bool* aIsDone) {
+ NS_ENSURE_TRUE(aIsDone, NS_ERROR_NULL_POINTER);
+
+ *aIsDone = false;
+
+ NS_ENSURE_TRUE(mFilteredIter, NS_ERROR_FAILURE);
+
+ *aIsDone = mIteratorStatus == IteratorStatus::eDone;
+
+ return NS_OK;
+}
+
+nsresult TextServicesDocument::SetSelection(uint32_t aOffset,
+ uint32_t aLength) {
+ NS_ENSURE_TRUE(mSelCon, NS_ERROR_FAILURE);
+
+ return SetSelectionInternal(aOffset, aLength, true);
+}
+
+nsresult TextServicesDocument::ScrollSelectionIntoView() {
+ NS_ENSURE_TRUE(mSelCon, NS_ERROR_FAILURE);
+
+ // After ScrollSelectionIntoView(), the pending notifications might be flushed
+ // and PresShell/PresContext/Frames may be dead. See bug 418470.
+ nsresult rv = mSelCon->ScrollSelectionIntoView(
+ nsISelectionController::SELECTION_NORMAL,
+ nsISelectionController::SELECTION_FOCUS_REGION,
+ nsISelectionController::SCROLL_SYNCHRONOUS);
+
+ return rv;
+}
+
+nsresult TextServicesDocument::OffsetEntryArray::WillDeleteSelection() {
+ MOZ_ASSERT(mSelection.IsSet());
+ MOZ_ASSERT(!mSelection.IsCollapsed());
+
+ for (size_t i = mSelection.StartIndex(); i <= mSelection.EndIndex(); i++) {
+ OffsetEntry* entry = ElementAt(i).get();
+ if (i == mSelection.StartIndex()) {
+ // Calculate the length of the selection. Note that the
+ // selection length can be zero if the start of the selection
+ // is at the very end of a text node entry.
+ uint32_t selLength;
+ if (entry->mIsInsertedText) {
+ // Inserted text offset entries have no width when
+ // talking in terms of string offsets! If the beginning
+ // of the selection is in an inserted text offset entry,
+ // the caret is always at the end of the entry!
+ selLength = 0;
+ } else {
+ selLength = entry->EndOffsetInTextInBlock() -
+ mSelection.StartOffsetInTextInBlock();
+ }
+
+ if (selLength > 0) {
+ if (mSelection.StartOffsetInTextInBlock() >
+ entry->mOffsetInTextInBlock) {
+ // Selection doesn't start at the beginning of the
+ // text node entry. We need to split this entry into
+ // two pieces, the piece before the selection, and
+ // the piece inside the selection.
+ nsresult rv = SplitElementAt(i, selLength);
+ if (NS_FAILED(rv)) {
+ NS_WARNING("selLength was invalid for the OffsetEntry");
+ return rv;
+ }
+
+ // Adjust selection indexes to account for new entry:
+ MOZ_DIAGNOSTIC_ASSERT(mSelection.StartIndex() + 1 < Length());
+ MOZ_DIAGNOSTIC_ASSERT(mSelection.EndIndex() + 1 < Length());
+ mSelection.SetIndexes(mSelection.StartIndex() + 1,
+ mSelection.EndIndex() + 1);
+ entry = ElementAt(++i).get();
+ }
+
+ if (mSelection.StartIndex() < mSelection.EndIndex()) {
+ // The entire entry is contained in the selection. Mark the
+ // entry invalid.
+ entry->mIsValid = false;
+ }
+ }
+ }
+
+ if (i == mSelection.EndIndex()) {
+ if (entry->mIsInsertedText) {
+ // Inserted text offset entries have no width when
+ // talking in terms of string offsets! If the end
+ // of the selection is in an inserted text offset entry,
+ // the selection includes the entire entry!
+ entry->mIsValid = false;
+ } else {
+ // Calculate the length of the selection. Note that the
+ // selection length can be zero if the end of the selection
+ // is at the very beginning of a text node entry.
+
+ const uint32_t selLength =
+ mSelection.EndOffsetInTextInBlock() - entry->mOffsetInTextInBlock;
+ if (selLength) {
+ if (mSelection.EndOffsetInTextInBlock() <
+ entry->EndOffsetInTextInBlock()) {
+ // mOffsetInTextInBlock is guaranteed to be inside the selection,
+ // even when mSelection.IsInSameElement() is true.
+ nsresult rv = SplitElementAt(i, entry->mLength - selLength);
+ if (NS_FAILED(rv)) {
+ NS_WARNING(
+ "entry->mLength - selLength was invalid for the OffsetEntry");
+ return rv;
+ }
+
+ // Update the entry fields:
+ ElementAt(i + 1)->mOffsetInTextNode = entry->mOffsetInTextNode;
+ }
+
+ if (mSelection.EndOffsetInTextInBlock() ==
+ entry->EndOffsetInTextInBlock()) {
+ // The entire entry is contained in the selection. Mark the
+ // entry invalid.
+ entry->mIsValid = false;
+ }
+ }
+ }
+ }
+
+ if (i != mSelection.StartIndex() && i != mSelection.EndIndex()) {
+ // The entire entry is contained in the selection. Mark the
+ // entry invalid.
+ entry->mIsValid = false;
+ }
+ }
+
+ return NS_OK;
+}
+
+nsresult TextServicesDocument::DeleteSelection() {
+ if (NS_WARN_IF(!mEditorBase) ||
+ NS_WARN_IF(!mOffsetTable.mSelection.IsSet())) {
+ return NS_ERROR_FAILURE;
+ }
+
+ if (mOffsetTable.mSelection.IsCollapsed()) {
+ return NS_OK;
+ }
+
+ // If we have an mExtent, save off its current set of
+ // end points so we can compare them against mExtent's
+ // set after the deletion of the content.
+
+ nsCOMPtr<nsINode> origStartNode, origEndNode;
+ uint32_t origStartOffset = 0, origEndOffset = 0;
+
+ if (mExtent) {
+ nsresult rv = GetRangeEndPoints(
+ mExtent, getter_AddRefs(origStartNode), &origStartOffset,
+ getter_AddRefs(origEndNode), &origEndOffset);
+
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+ }
+
+ if (NS_FAILED(mOffsetTable.WillDeleteSelection())) {
+ NS_WARNING(
+ "TextServicesDocument::OffsetEntryTable::WillDeleteSelection() failed");
+ return NS_ERROR_FAILURE;
+ }
+
+ // Make sure mFilteredIter always points to something valid!
+ AdjustContentIterator();
+
+ // Now delete the actual content!
+ OwningNonNull<EditorBase> editorBase = *mEditorBase;
+ nsresult rv = editorBase->DeleteSelectionAsAction(nsIEditor::ePrevious,
+ nsIEditor::eStrip);
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+
+ // Now that we've actually deleted the selected content,
+ // check to see if our mExtent has changed, if so, then
+ // we have to create a new content iterator!
+
+ if (origStartNode && origEndNode) {
+ nsCOMPtr<nsINode> curStartNode, curEndNode;
+ uint32_t curStartOffset = 0, curEndOffset = 0;
+
+ rv = GetRangeEndPoints(mExtent, getter_AddRefs(curStartNode),
+ &curStartOffset, getter_AddRefs(curEndNode),
+ &curEndOffset);
+
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+
+ if (origStartNode != curStartNode || origEndNode != curEndNode) {
+ // The range has changed, so we need to create a new content
+ // iterator based on the new range.
+ nsCOMPtr<nsIContent> curContent;
+ if (mIteratorStatus != IteratorStatus::eDone) {
+ // The old iterator is still pointing to something valid,
+ // so get its current node so we can restore it after we
+ // create the new iterator!
+ curContent = mFilteredIter->GetCurrentNode()
+ ? mFilteredIter->GetCurrentNode()->AsContent()
+ : nullptr;
+ }
+
+ // Create the new iterator.
+ rv =
+ CreateFilteredContentIterator(mExtent, getter_AddRefs(mFilteredIter));
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+
+ // Now make the new iterator point to the content node
+ // the old one was pointing at.
+ if (curContent) {
+ rv = mFilteredIter->PositionAt(curContent);
+ if (NS_FAILED(rv)) {
+ mIteratorStatus = IteratorStatus::eDone;
+ } else {
+ mIteratorStatus = IteratorStatus::eValid;
+ }
+ }
+ }
+ }
+
+ OffsetEntry* entry = mOffsetTable.DidDeleteSelection();
+ if (entry) {
+ SetSelection(mOffsetTable.mSelection.StartOffsetInTextInBlock(), 0);
+ }
+
+ // Now remove any invalid entries from the offset table.
+ mOffsetTable.RemoveInvalidElements();
+ return NS_OK;
+}
+
+OffsetEntry* TextServicesDocument::OffsetEntryArray::DidDeleteSelection() {
+ MOZ_ASSERT(mSelection.IsSet());
+
+ // Move the caret to the end of the first valid entry.
+ // Start with SelectionStartIndex() since it may still be valid.
+ OffsetEntry* entry = nullptr;
+ for (size_t i = mSelection.StartIndex() + 1; !entry && i > 0; i--) {
+ entry = ElementAt(i - 1).get();
+ if (!entry->mIsValid) {
+ entry = nullptr;
+ } else {
+ MOZ_DIAGNOSTIC_ASSERT(i - 1 < Length());
+ mSelection.Set(i - 1, entry->EndOffsetInTextInBlock());
+ }
+ }
+
+ // If we still don't have a valid entry, move the caret
+ // to the next valid entry after the selection:
+ for (size_t i = mSelection.EndIndex(); !entry && i < Length(); i++) {
+ entry = ElementAt(i).get();
+ if (!entry->mIsValid) {
+ entry = nullptr;
+ } else {
+ MOZ_DIAGNOSTIC_ASSERT(i < Length());
+ mSelection.Set(i, entry->mOffsetInTextInBlock);
+ }
+ }
+
+ if (!entry) {
+ // Uuughh we have no valid offset entry to place our
+ // caret ... just mark the selection invalid.
+ mSelection.Reset();
+ }
+
+ return entry;
+}
+
+nsresult TextServicesDocument::InsertText(const nsAString& aText) {
+ if (NS_WARN_IF(!mEditorBase) ||
+ NS_WARN_IF(!mOffsetTable.mSelection.IsSet())) {
+ return NS_ERROR_FAILURE;
+ }
+
+ // If the selection is not collapsed, we need to save
+ // off the selection offsets so we can restore the
+ // selection and delete the selected content after we've
+ // inserted the new text. This is necessary to try and
+ // retain as much of the original style of the content
+ // being deleted.
+
+ const bool wasSelectionCollapsed = mOffsetTable.mSelection.IsCollapsed();
+ const uint32_t savedSelOffset =
+ mOffsetTable.mSelection.StartOffsetInTextInBlock();
+ const uint32_t savedSelLength = mOffsetTable.mSelection.LengthInTextInBlock();
+
+ if (!wasSelectionCollapsed) {
+ // Collapse to the start of the current selection
+ // for the insert!
+ nsresult rv =
+ SetSelection(mOffsetTable.mSelection.StartOffsetInTextInBlock(), 0);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ // AutoTransactionBatchExternal grabs mEditorBase, so, we don't need to grab
+ // the instance with local variable here.
+ OwningNonNull<EditorBase> editorBase = *mEditorBase;
+ AutoTransactionBatchExternal treatAsOneTransaction(editorBase);
+
+ nsresult rv = editorBase->InsertTextAsAction(aText);
+ if (NS_FAILED(rv)) {
+ NS_WARNING("InsertTextAsAction() failed");
+ return rv;
+ }
+
+ RefPtr<Selection> selection =
+ mSelCon->GetSelection(nsISelectionController::SELECTION_NORMAL);
+ rv = mOffsetTable.DidInsertText(selection, aText);
+ if (NS_FAILED(rv)) {
+ NS_WARNING("TextServicesDocument::OffsetEntry::DidInsertText() failed");
+ return rv;
+ }
+
+ if (!wasSelectionCollapsed) {
+ nsresult rv = SetSelection(savedSelOffset, savedSelLength);
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+
+ rv = DeleteSelection();
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+ }
+
+ return NS_OK;
+}
+
+nsresult TextServicesDocument::OffsetEntryArray::DidInsertText(
+ dom::Selection* aSelection, const nsAString& aInsertedString) {
+ MOZ_ASSERT(mSelection.IsSet());
+
+ // When you touch this method, please make sure that the entry instance
+ // won't be deleted. If you know it'll be deleted, you should set it to
+ // `nullptr`.
+ OffsetEntry* entry = ElementAt(mSelection.StartIndex()).get();
+ OwningNonNull<Text> const textNodeAtStartEntry = entry->mTextNode;
+
+ NS_ASSERTION((entry->mIsValid), "Invalid insertion point!");
+
+ if (entry->mOffsetInTextInBlock == mSelection.StartOffsetInTextInBlock()) {
+ if (entry->mIsInsertedText) {
+ // If the caret is in an inserted text offset entry,
+ // we simply insert the text at the end of the entry.
+ entry->mLength += aInsertedString.Length();
+ } else {
+ // Insert an inserted text offset entry before the current
+ // entry!
+ UniquePtr<OffsetEntry> newInsertedTextEntry =
+ MakeUnique<OffsetEntry>(entry->mTextNode, entry->mOffsetInTextInBlock,
+ aInsertedString.Length());
+ newInsertedTextEntry->mIsInsertedText = true;
+ newInsertedTextEntry->mOffsetInTextNode = entry->mOffsetInTextNode;
+ // XXX(Bug 1631371) Check if this should use a fallible operation as it
+ // pretended earlier.
+ InsertElementAt(mSelection.StartIndex(), std::move(newInsertedTextEntry));
+ }
+ } else if (entry->EndOffsetInTextInBlock() ==
+ mSelection.EndOffsetInTextInBlock()) {
+ // We are inserting text at the end of the current offset entry.
+ // Look at the next valid entry in the table. If it's an inserted
+ // text entry, add to its length and adjust its node offset. If
+ // it isn't, add a new inserted text entry.
+ uint32_t nextIndex = mSelection.StartIndex() + 1;
+ OffsetEntry* insertedTextEntry = nullptr;
+ if (Length() > nextIndex) {
+ insertedTextEntry = ElementAt(nextIndex).get();
+ if (!insertedTextEntry) {
+ return NS_ERROR_FAILURE;
+ }
+
+ // Check if the entry is a match. If it isn't, set
+ // iEntry to zero.
+ if (!insertedTextEntry->mIsInsertedText ||
+ insertedTextEntry->mOffsetInTextInBlock !=
+ mSelection.StartOffsetInTextInBlock()) {
+ insertedTextEntry = nullptr;
+ }
+ }
+
+ if (!insertedTextEntry) {
+ // We didn't find an inserted text offset entry, so
+ // create one.
+ UniquePtr<OffsetEntry> newInsertedTextEntry = MakeUnique<OffsetEntry>(
+ entry->mTextNode, mSelection.StartOffsetInTextInBlock(), 0);
+ newInsertedTextEntry->mOffsetInTextNode = entry->EndOffsetInTextNode();
+ newInsertedTextEntry->mIsInsertedText = true;
+ // XXX(Bug 1631371) Check if this should use a fallible operation as it
+ // pretended earlier.
+ insertedTextEntry =
+ InsertElementAt(nextIndex, std::move(newInsertedTextEntry))->get();
+ }
+
+ // We have a valid inserted text offset entry. Update its
+ // length, adjust the selection indexes, and make sure the
+ // caret is properly placed!
+
+ insertedTextEntry->mLength += aInsertedString.Length();
+
+ MOZ_DIAGNOSTIC_ASSERT(nextIndex < Length());
+ mSelection.SetIndex(nextIndex);
+
+ if (!aSelection) {
+ return NS_OK;
+ }
+
+ OwningNonNull<Text> textNode = insertedTextEntry->mTextNode;
+ nsresult rv = aSelection->CollapseInLimiter(
+ textNode, insertedTextEntry->EndOffsetInTextNode());
+ if (NS_FAILED(rv)) {
+ NS_WARNING("Selection::CollapseInLimiter() failed");
+ return rv;
+ }
+ } else if (entry->EndOffsetInTextInBlock() >
+ mSelection.StartOffsetInTextInBlock()) {
+ // We are inserting text into the middle of the current offset entry.
+ // split the current entry into two parts, then insert an inserted text
+ // entry between them!
+ nsresult rv = SplitElementAt(mSelection.StartIndex(),
+ entry->EndOffsetInTextInBlock() -
+ mSelection.StartOffsetInTextInBlock());
+ if (NS_FAILED(rv)) {
+ NS_WARNING(
+ "entry->EndOffsetInTextInBlock() - "
+ "mSelection.StartOffsetInTextInBlock() was invalid for the "
+ "OffsetEntry");
+ return rv;
+ }
+
+ // XXX(Bug 1631371) Check if this should use a fallible operation as it
+ // pretended earlier.
+ UniquePtr<OffsetEntry>& insertedTextEntry = *InsertElementAt(
+ mSelection.StartIndex() + 1,
+ MakeUnique<OffsetEntry>(entry->mTextNode,
+ mSelection.StartOffsetInTextInBlock(),
+ aInsertedString.Length()));
+ LockOffsetEntryArrayLengthInDebugBuild(observer, *this);
+ insertedTextEntry->mIsInsertedText = true;
+ insertedTextEntry->mOffsetInTextNode = entry->EndOffsetInTextNode();
+ MOZ_DIAGNOSTIC_ASSERT(mSelection.StartIndex() + 1 < Length());
+ mSelection.SetIndex(mSelection.StartIndex() + 1);
+ }
+
+ // We've just finished inserting an inserted text offset entry.
+ // update all entries with the same mTextNode pointer that follow
+ // it in the table!
+
+ for (size_t i = mSelection.StartIndex() + 1; i < Length(); i++) {
+ const UniquePtr<OffsetEntry>& entry = ElementAt(i);
+ LockOffsetEntryArrayLengthInDebugBuild(observer, *this);
+ if (entry->mTextNode != textNodeAtStartEntry) {
+ break;
+ }
+ if (entry->mIsValid) {
+ entry->mOffsetInTextNode += aInsertedString.Length();
+ }
+ }
+
+ return NS_OK;
+}
+
+void TextServicesDocument::DidDeleteContent(const nsIContent& aChildContent) {
+ if (NS_WARN_IF(!mFilteredIter) || !aChildContent.IsText()) {
+ return;
+ }
+
+ Maybe<size_t> maybeNodeIndex =
+ mOffsetTable.FirstIndexOf(*aChildContent.AsText());
+ if (maybeNodeIndex.isNothing()) {
+ // It's okay if the node isn't in the offset table, the
+ // editor could be cleaning house.
+ return;
+ }
+
+ nsINode* node = mFilteredIter->GetCurrentNode();
+ if (node && node == &aChildContent &&
+ mIteratorStatus != IteratorStatus::eDone) {
+ // XXX: This should never really happen because
+ // AdjustContentIterator() should have been called prior
+ // to the delete to try and position the iterator on the
+ // next valid text node in the offset table, and if there
+ // wasn't a next, it would've set mIteratorStatus to eIsDone.
+
+ NS_ERROR("DeleteNode called for current iterator node.");
+ }
+
+ for (size_t nodeIndex = *maybeNodeIndex; nodeIndex < mOffsetTable.Length();
+ nodeIndex++) {
+ const UniquePtr<OffsetEntry>& entry = mOffsetTable[nodeIndex];
+ LockOffsetEntryArrayLengthInDebugBuild(observer, mOffsetTable);
+ if (!entry) {
+ return;
+ }
+
+ if (entry->mTextNode == &aChildContent) {
+ entry->mIsValid = false;
+ }
+ }
+}
+
+void TextServicesDocument::DidJoinContents(
+ const EditorRawDOMPoint& aJoinedPoint, const nsIContent& aRemovedContent) {
+ // Make sure that both nodes are text nodes -- otherwise we don't care.
+ if (!aJoinedPoint.IsInTextNode() || !aRemovedContent.IsText()) {
+ return;
+ }
+
+ // Note: The editor merges the contents of the left node into the
+ // contents of the right.
+
+ Maybe<size_t> maybeRemovedIndex =
+ mOffsetTable.FirstIndexOf(*aRemovedContent.AsText());
+ if (maybeRemovedIndex.isNothing()) {
+ // It's okay if the node isn't in the offset table, the
+ // editor could be cleaning house.
+ return;
+ }
+
+ Maybe<size_t> maybeJoinedIndex =
+ mOffsetTable.FirstIndexOf(*aJoinedPoint.ContainerAs<Text>());
+ if (maybeJoinedIndex.isNothing()) {
+ // It's okay if the node isn't in the offset table, the
+ // editor could be cleaning house.
+ return;
+ }
+
+ const size_t removedIndex = *maybeRemovedIndex;
+ const size_t joinedIndex = *maybeJoinedIndex;
+
+ if (MOZ_UNLIKELY(joinedIndex > removedIndex)) {
+ NS_ASSERTION(joinedIndex < removedIndex, "Indexes out of order.");
+ return;
+ }
+ NS_ASSERTION(mOffsetTable[removedIndex]->mOffsetInTextNode == 0,
+ "Unexpected offset value for rightIndex.");
+
+ // Run through the table and change all entries referring to
+ // the removed node so that they now refer to the joined node,
+ // and adjust offsets if necessary.
+ const uint32_t movedTextDataLength =
+ aJoinedPoint.ContainerAs<Text>()->TextDataLength() -
+ aJoinedPoint.Offset();
+ for (uint32_t i = removedIndex; i < mOffsetTable.Length(); i++) {
+ const UniquePtr<OffsetEntry>& entry = mOffsetTable[i];
+ LockOffsetEntryArrayLengthInDebugBuild(observer, mOffsetTable);
+ if (entry->mTextNode != aRemovedContent.AsText()) {
+ break;
+ }
+ if (entry->mIsValid) {
+ entry->mTextNode = aJoinedPoint.ContainerAs<Text>();
+ // The text was moved from aRemovedContent to end of the container of
+ // aJoinedPoint.
+ entry->mOffsetInTextNode += movedTextDataLength;
+ }
+ }
+
+ // Now check to see if the iterator is pointing to the
+ // left node. If it is, make it point to the joined node!
+ if (mFilteredIter->GetCurrentNode() == aRemovedContent.AsText()) {
+ mFilteredIter->PositionAt(aJoinedPoint.ContainerAs<Text>());
+ }
+}
+
+nsresult TextServicesDocument::CreateFilteredContentIterator(
+ const AbstractRange* aAbstractRange,
+ FilteredContentIterator** aFilteredIter) {
+ if (NS_WARN_IF(!aAbstractRange) || NS_WARN_IF(!aFilteredIter)) {
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ *aFilteredIter = nullptr;
+
+ UniquePtr<nsComposeTxtSrvFilter> composeFilter;
+ switch (mTxtSvcFilterType) {
+ case nsIEditorSpellCheck::FILTERTYPE_NORMAL:
+ composeFilter = nsComposeTxtSrvFilter::CreateNormalFilter();
+ break;
+ case nsIEditorSpellCheck::FILTERTYPE_MAIL:
+ composeFilter = nsComposeTxtSrvFilter::CreateMailFilter();
+ break;
+ }
+
+ // Create a FilteredContentIterator
+ // This class wraps the ContentIterator in order to give itself a chance
+ // to filter out certain content nodes
+ RefPtr<FilteredContentIterator> filter =
+ new FilteredContentIterator(std::move(composeFilter));
+ nsresult rv = filter->Init(aAbstractRange);
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+
+ filter.forget(aFilteredIter);
+ return NS_OK;
+}
+
+Element* TextServicesDocument::GetDocumentContentRootNode() const {
+ if (NS_WARN_IF(!mDocument)) {
+ return nullptr;
+ }
+
+ if (mDocument->IsHTMLOrXHTML()) {
+ Element* rootElement = mDocument->GetRootElement();
+ if (rootElement && rootElement->IsXULElement()) {
+ // HTML documents with root XUL elements should eventually be transitioned
+ // to a regular document structure, but for now the content root node will
+ // be the document element.
+ return mDocument->GetDocumentElement();
+ }
+ // For HTML documents, the content root node is the body.
+ return mDocument->GetBody();
+ }
+
+ // For non-HTML documents, the content root node will be the document element.
+ return mDocument->GetDocumentElement();
+}
+
+already_AddRefed<nsRange> TextServicesDocument::CreateDocumentContentRange() {
+ nsCOMPtr<nsINode> node = GetDocumentContentRootNode();
+ if (NS_WARN_IF(!node)) {
+ return nullptr;
+ }
+
+ RefPtr<nsRange> range = nsRange::Create(node);
+ IgnoredErrorResult ignoredError;
+ range->SelectNodeContents(*node, ignoredError);
+ NS_WARNING_ASSERTION(!ignoredError.Failed(), "SelectNodeContents() failed");
+ return range.forget();
+}
+
+already_AddRefed<nsRange>
+TextServicesDocument::CreateDocumentContentRootToNodeOffsetRange(
+ nsINode* aParent, uint32_t aOffset, bool aToStart) {
+ if (NS_WARN_IF(!aParent)) {
+ return nullptr;
+ }
+
+ nsCOMPtr<nsINode> bodyNode = GetDocumentContentRootNode();
+ if (NS_WARN_IF(!bodyNode)) {
+ return nullptr;
+ }
+
+ nsCOMPtr<nsINode> startNode;
+ nsCOMPtr<nsINode> endNode;
+ uint32_t startOffset, endOffset;
+
+ if (aToStart) {
+ // The range should begin at the start of the document
+ // and extend up until (aParent, aOffset).
+ startNode = bodyNode;
+ startOffset = 0;
+ endNode = aParent;
+ endOffset = aOffset;
+ } else {
+ // The range should begin at (aParent, aOffset) and
+ // extend to the end of the document.
+ startNode = aParent;
+ startOffset = aOffset;
+ endNode = bodyNode;
+ endOffset = endNode ? endNode->GetChildCount() : 0;
+ }
+
+ RefPtr<nsRange> range = nsRange::Create(startNode, startOffset, endNode,
+ endOffset, IgnoreErrors());
+ NS_WARNING_ASSERTION(range,
+ "nsRange::Create() failed to create new valid range");
+ return range.forget();
+}
+
+nsresult TextServicesDocument::CreateDocumentContentIterator(
+ FilteredContentIterator** aFilteredIter) {
+ NS_ENSURE_TRUE(aFilteredIter, NS_ERROR_NULL_POINTER);
+
+ RefPtr<nsRange> range = CreateDocumentContentRange();
+ if (NS_WARN_IF(!range)) {
+ *aFilteredIter = nullptr;
+ return NS_ERROR_FAILURE;
+ }
+
+ return CreateFilteredContentIterator(range, aFilteredIter);
+}
+
+nsresult TextServicesDocument::AdjustContentIterator() {
+ NS_ENSURE_TRUE(mFilteredIter, NS_ERROR_FAILURE);
+
+ nsCOMPtr<nsINode> node = mFilteredIter->GetCurrentNode();
+ NS_ENSURE_TRUE(node, NS_ERROR_FAILURE);
+
+ Text* prevValidTextNode = nullptr;
+ Text* nextValidTextNode = nullptr;
+ bool foundEntry = false;
+
+ const size_t tableLength = mOffsetTable.Length();
+ for (size_t i = 0; i < tableLength && !nextValidTextNode; i++) {
+ UniquePtr<OffsetEntry>& entry = mOffsetTable[i];
+ LockOffsetEntryArrayLengthInDebugBuild(observer, mOffsetTable);
+ if (entry->mTextNode == node) {
+ if (entry->mIsValid) {
+ // The iterator is still pointing to something valid!
+ // Do nothing!
+ return NS_OK;
+ }
+ // We found an invalid entry that points to
+ // the current iterator node. Stop looking for
+ // a previous valid node!
+ foundEntry = true;
+ }
+
+ if (entry->mIsValid) {
+ if (!foundEntry) {
+ prevValidTextNode = entry->mTextNode;
+ } else {
+ nextValidTextNode = entry->mTextNode;
+ }
+ }
+ }
+
+ Text* validTextNode = nullptr;
+ if (prevValidTextNode) {
+ validTextNode = prevValidTextNode;
+ } else if (nextValidTextNode) {
+ validTextNode = nextValidTextNode;
+ }
+
+ if (validTextNode) {
+ nsresult rv = mFilteredIter->PositionAt(validTextNode);
+ if (NS_FAILED(rv)) {
+ mIteratorStatus = IteratorStatus::eDone;
+ } else {
+ mIteratorStatus = IteratorStatus::eValid;
+ }
+ return rv;
+ }
+
+ // If we get here, there aren't any valid entries
+ // in the offset table! Try to position the iterator
+ // on the next text block first, then previous if
+ // one doesn't exist!
+
+ if (mNextTextBlock) {
+ nsresult rv = mFilteredIter->PositionAt(mNextTextBlock);
+ if (NS_FAILED(rv)) {
+ mIteratorStatus = IteratorStatus::eDone;
+ return rv;
+ }
+
+ mIteratorStatus = IteratorStatus::eNext;
+ } else if (mPrevTextBlock) {
+ nsresult rv = mFilteredIter->PositionAt(mPrevTextBlock);
+ if (NS_FAILED(rv)) {
+ mIteratorStatus = IteratorStatus::eDone;
+ return rv;
+ }
+
+ mIteratorStatus = IteratorStatus::ePrev;
+ } else {
+ mIteratorStatus = IteratorStatus::eDone;
+ }
+ return NS_OK;
+}
+
+// static
+bool TextServicesDocument::DidSkip(FilteredContentIterator* aFilteredIter) {
+ return aFilteredIter && aFilteredIter->DidSkip();
+}
+
+// static
+void TextServicesDocument::ClearDidSkip(
+ FilteredContentIterator* aFilteredIter) {
+ // Clear filter's skip flag
+ if (aFilteredIter) {
+ aFilteredIter->ClearDidSkip();
+ }
+}
+
+// static
+bool TextServicesDocument::HasSameBlockNodeParent(Text& aTextNode1,
+ Text& aTextNode2) {
+ // XXX How about the case that both text nodes are orphan nodes?
+ if (aTextNode1.GetParent() == aTextNode2.GetParent()) {
+ return true;
+ }
+
+ // I think that spellcheck should be available only in editable nodes.
+ // So, we also need to check whether they are in same editing host.
+ const Element* editableBlockElementOrInlineEditingHost1 =
+ HTMLEditUtils::GetAncestorElement(
+ aTextNode1,
+ HTMLEditUtils::ClosestEditableBlockElementOrInlineEditingHost,
+ BlockInlineCheck::UseHTMLDefaultStyle);
+ const Element* editableBlockElementOrInlineEditingHost2 =
+ HTMLEditUtils::GetAncestorElement(
+ aTextNode2,
+ HTMLEditUtils::ClosestEditableBlockElementOrInlineEditingHost,
+ BlockInlineCheck::UseHTMLDefaultStyle);
+ return editableBlockElementOrInlineEditingHost1 &&
+ editableBlockElementOrInlineEditingHost1 ==
+ editableBlockElementOrInlineEditingHost2;
+}
+
+Result<EditorRawDOMRangeInTexts, nsresult>
+TextServicesDocument::OffsetEntryArray::WillSetSelection(
+ uint32_t aOffsetInTextInBlock, uint32_t aLength) {
+ // Find start of selection in node offset terms:
+ EditorRawDOMPointInText newStart;
+ for (size_t i = 0; !newStart.IsSet() && i < Length(); i++) {
+ const UniquePtr<OffsetEntry>& entry = ElementAt(i);
+ LockOffsetEntryArrayLengthInDebugBuild(observer, *this);
+ if (entry->mIsValid) {
+ if (entry->mIsInsertedText) {
+ // Caret can only be placed at the end of an
+ // inserted text offset entry, if the offsets
+ // match exactly!
+ if (entry->mOffsetInTextInBlock == aOffsetInTextInBlock) {
+ newStart.Set(entry->mTextNode, entry->EndOffsetInTextNode());
+ }
+ } else if (aOffsetInTextInBlock >= entry->mOffsetInTextInBlock) {
+ bool foundEntry = false;
+ if (aOffsetInTextInBlock < entry->EndOffsetInTextInBlock()) {
+ foundEntry = true;
+ } else if (aOffsetInTextInBlock == entry->EndOffsetInTextInBlock()) {
+ // Peek after this entry to see if we have any
+ // inserted text entries belonging to the same
+ // entry->mTextNode. If so, we have to place the selection
+ // after it!
+ if (i + 1 < Length()) {
+ const UniquePtr<OffsetEntry>& nextEntry = ElementAt(i + 1);
+ LockOffsetEntryArrayLengthInDebugBuild(observer, *this);
+ if (!nextEntry->mIsValid ||
+ nextEntry->mOffsetInTextInBlock != aOffsetInTextInBlock) {
+ // Next offset entry isn't an exact match, so we'll
+ // just use the current entry.
+ foundEntry = true;
+ }
+ }
+ }
+
+ if (foundEntry) {
+ newStart.Set(entry->mTextNode, entry->mOffsetInTextNode +
+ aOffsetInTextInBlock -
+ entry->mOffsetInTextInBlock);
+ }
+ }
+
+ if (newStart.IsSet()) {
+ MOZ_DIAGNOSTIC_ASSERT(i < Length());
+ mSelection.Set(i, aOffsetInTextInBlock);
+ }
+ }
+ }
+
+ if (NS_WARN_IF(!newStart.IsSet())) {
+ return Err(NS_ERROR_FAILURE);
+ }
+
+ if (!aLength) {
+ mSelection.CollapseToStart();
+ return EditorRawDOMRangeInTexts(newStart);
+ }
+
+ // Find the end of the selection in node offset terms:
+ EditorRawDOMPointInText newEnd;
+ const uint32_t endOffset = aOffsetInTextInBlock + aLength;
+ for (uint32_t i = Length(); !newEnd.IsSet() && i > 0; i--) {
+ const UniquePtr<OffsetEntry>& entry = ElementAt(i - 1);
+ LockOffsetEntryArrayLengthInDebugBuild(observer, *this);
+ if (entry->mIsValid) {
+ if (entry->mIsInsertedText) {
+ if (entry->mOffsetInTextInBlock ==
+ (newEnd.IsSet() ? newEnd.Offset() : 0)) {
+ // If the selection ends on an inserted text offset entry,
+ // the selection includes the entire entry!
+ newEnd.Set(entry->mTextNode, entry->EndOffsetInTextNode());
+ }
+ } else if (entry->OffsetInTextInBlockIsInRangeOrEndOffset(endOffset)) {
+ newEnd.Set(entry->mTextNode, entry->mOffsetInTextNode + endOffset -
+ entry->mOffsetInTextInBlock);
+ }
+
+ if (newEnd.IsSet()) {
+ MOZ_DIAGNOSTIC_ASSERT(mSelection.StartIndex() < Length());
+ MOZ_DIAGNOSTIC_ASSERT(i - 1 < Length());
+ mSelection.Set(mSelection.StartIndex(), i - 1,
+ mSelection.StartOffsetInTextInBlock(), endOffset);
+ }
+ }
+ }
+
+ return newEnd.IsSet() ? EditorRawDOMRangeInTexts(newStart, newEnd)
+ : EditorRawDOMRangeInTexts(newStart);
+}
+
+nsresult TextServicesDocument::SetSelectionInternal(
+ uint32_t aOffsetInTextInBlock, uint32_t aLength, bool aDoUpdate) {
+ if (NS_WARN_IF(!mSelCon)) {
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ Result<EditorRawDOMRangeInTexts, nsresult> newSelectionRange =
+ mOffsetTable.WillSetSelection(aOffsetInTextInBlock, aLength);
+ if (newSelectionRange.isErr()) {
+ NS_WARNING(
+ "TextServicesDocument::OffsetEntryArray::WillSetSelection() failed");
+ return newSelectionRange.unwrapErr();
+ }
+
+ if (!aDoUpdate) {
+ return NS_OK;
+ }
+
+ // XXX: If we ever get a SetSelection() method in nsIEditor, we should
+ // use it.
+ RefPtr<Selection> selection =
+ mSelCon->GetSelection(nsISelectionController::SELECTION_NORMAL);
+ if (NS_WARN_IF(!selection)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ if (newSelectionRange.inspect().Collapsed()) {
+ nsresult rv =
+ selection->CollapseInLimiter(newSelectionRange.inspect().StartRef());
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "Selection::CollapseInLimiter() failed");
+ return rv;
+ }
+
+ ErrorResult error;
+ selection->SetStartAndEndInLimiter(newSelectionRange.inspect().StartRef(),
+ newSelectionRange.inspect().EndRef(),
+ error);
+ NS_WARNING_ASSERTION(!error.Failed(),
+ "Selection::SetStartAndEndInLimiter() failed");
+ return error.StealNSResult();
+}
+
+nsresult TextServicesDocument::GetSelection(BlockSelectionStatus* aSelStatus,
+ uint32_t* aSelOffset,
+ uint32_t* aSelLength) {
+ NS_ENSURE_TRUE(aSelStatus && aSelOffset && aSelLength, NS_ERROR_NULL_POINTER);
+
+ *aSelStatus = BlockSelectionStatus::eBlockNotFound;
+ *aSelOffset = UINT32_MAX;
+ *aSelLength = UINT32_MAX;
+
+ NS_ENSURE_TRUE(mDocument && mSelCon, NS_ERROR_FAILURE);
+
+ if (mIteratorStatus == IteratorStatus::eDone) {
+ return NS_OK;
+ }
+
+ RefPtr<Selection> selection =
+ mSelCon->GetSelection(nsISelectionController::SELECTION_NORMAL);
+ NS_ENSURE_TRUE(selection, NS_ERROR_FAILURE);
+
+ if (selection->IsCollapsed()) {
+ return GetCollapsedSelection(aSelStatus, aSelOffset, aSelLength);
+ }
+
+ return GetUncollapsedSelection(aSelStatus, aSelOffset, aSelLength);
+}
+
+nsresult TextServicesDocument::GetCollapsedSelection(
+ BlockSelectionStatus* aSelStatus, uint32_t* aSelOffset,
+ uint32_t* aSelLength) {
+ RefPtr<Selection> selection =
+ mSelCon->GetSelection(nsISelectionController::SELECTION_NORMAL);
+ NS_ENSURE_TRUE(selection, NS_ERROR_FAILURE);
+
+ // The calling function should have done the GetIsCollapsed()
+ // check already. Just assume it's collapsed!
+ *aSelStatus = BlockSelectionStatus::eBlockOutside;
+ *aSelOffset = *aSelLength = UINT32_MAX;
+
+ const uint32_t tableCount = mOffsetTable.Length();
+ if (!tableCount) {
+ return NS_OK;
+ }
+
+ // Get pointers to the first and last offset entries
+ // in the table.
+
+ UniquePtr<OffsetEntry>& eStart = mOffsetTable[0];
+ UniquePtr<OffsetEntry>& eEnd =
+ tableCount > 1 ? mOffsetTable[tableCount - 1] : eStart;
+ LockOffsetEntryArrayLengthInDebugBuild(observer, mOffsetTable);
+
+ const uint32_t eStartOffset = eStart->mOffsetInTextNode;
+ const uint32_t eEndOffset = eEnd->EndOffsetInTextNode();
+
+ RefPtr<const nsRange> range = selection->GetRangeAt(0);
+ NS_ENSURE_STATE(range);
+
+ nsCOMPtr<nsINode> parent = range->GetStartContainer();
+ MOZ_ASSERT(parent);
+
+ uint32_t offset = range->StartOffset();
+
+ const Maybe<int32_t> e1s1 = nsContentUtils::ComparePoints(
+ eStart->mTextNode, eStartOffset, parent, offset);
+ const Maybe<int32_t> e2s1 = nsContentUtils::ComparePoints(
+ eEnd->mTextNode, eEndOffset, parent, offset);
+
+ if (MOZ_UNLIKELY(NS_WARN_IF(!e1s1) || NS_WARN_IF(!e2s1))) {
+ return NS_ERROR_FAILURE;
+ }
+
+ if (*e1s1 > 0 || *e2s1 < 0) {
+ // We're done if the caret is outside the current text block.
+ return NS_OK;
+ }
+
+ if (parent->IsText()) {
+ // Good news, the caret is in a text node. Look
+ // through the offset table for the entry that
+ // matches its parent and offset.
+
+ for (uint32_t i = 0; i < tableCount; i++) {
+ const UniquePtr<OffsetEntry>& entry = mOffsetTable[i];
+ LockOffsetEntryArrayLengthInDebugBuild(observer, mOffsetTable);
+ if (entry->mTextNode == parent->AsText() &&
+ entry->OffsetInTextNodeIsInRangeOrEndOffset(offset)) {
+ *aSelStatus = BlockSelectionStatus::eBlockContains;
+ *aSelOffset =
+ entry->mOffsetInTextInBlock + (offset - entry->mOffsetInTextNode);
+ *aSelLength = 0;
+ return NS_OK;
+ }
+ }
+
+ // If we get here, we didn't find a text node entry
+ // in our offset table that matched.
+ return NS_ERROR_FAILURE;
+ }
+
+ // The caret is in our text block, but it's positioned in some
+ // non-text node (ex. <b>). Create a range based on the start
+ // and end of the text block, then create an iterator based on
+ // this range, with its initial position set to the closest
+ // child of this non-text node. Then look for the closest text
+ // node.
+
+ range = nsRange::Create(eStart->mTextNode, eStartOffset, eEnd->mTextNode,
+ eEndOffset, IgnoreErrors());
+ if (NS_WARN_IF(!range)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ RefPtr<FilteredContentIterator> filteredIter;
+ nsresult rv =
+ CreateFilteredContentIterator(range, getter_AddRefs(filteredIter));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsIContent* saveNode;
+ if (parent->HasChildren()) {
+ // XXX: We need to make sure that all of parent's
+ // children are in the text block.
+
+ // If the parent has children, position the iterator
+ // on the child that is to the left of the offset.
+
+ nsIContent* content = range->GetChildAtStartOffset();
+ if (content && parent->GetFirstChild() != content) {
+ content = content->GetPreviousSibling();
+ }
+ NS_ENSURE_TRUE(content, NS_ERROR_FAILURE);
+
+ nsresult rv = filteredIter->PositionAt(content);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ saveNode = content;
+ } else {
+ // The parent has no children, so position the iterator
+ // on the parent.
+ NS_ENSURE_TRUE(parent->IsContent(), NS_ERROR_FAILURE);
+ nsCOMPtr<nsIContent> content = parent->AsContent();
+
+ nsresult rv = filteredIter->PositionAt(content);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ saveNode = content;
+ }
+
+ // Now iterate to the left, towards the beginning of
+ // the text block, to find the first text node you
+ // come across.
+
+ Text* textNode = nullptr;
+ for (; !filteredIter->IsDone(); filteredIter->Prev()) {
+ nsINode* current = filteredIter->GetCurrentNode();
+ if (current->IsText()) {
+ textNode = current->AsText();
+ break;
+ }
+ }
+
+ if (textNode) {
+ // We found a node, now set the offset to the end
+ // of the text node.
+ offset = textNode->TextLength();
+ } else {
+ // We should never really get here, but I'm paranoid.
+
+ // We didn't find a text node above, so iterate to
+ // the right, towards the end of the text block, looking
+ // for a text node.
+
+ nsresult rv = filteredIter->PositionAt(saveNode);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ textNode = nullptr;
+ for (; !filteredIter->IsDone(); filteredIter->Next()) {
+ nsINode* current = filteredIter->GetCurrentNode();
+ if (current->IsText()) {
+ textNode = current->AsText();
+ break;
+ }
+ }
+ NS_ENSURE_TRUE(textNode, NS_ERROR_FAILURE);
+
+ // We found a text node, so set the offset to
+ // the beginning of the node.
+ offset = 0;
+ }
+
+ for (size_t i = 0; i < tableCount; i++) {
+ const UniquePtr<OffsetEntry>& entry = mOffsetTable[i];
+ LockOffsetEntryArrayLengthInDebugBuild(observer, mOffsetTable);
+ if (entry->mTextNode == textNode &&
+ entry->OffsetInTextNodeIsInRangeOrEndOffset(offset)) {
+ *aSelStatus = BlockSelectionStatus::eBlockContains;
+ *aSelOffset =
+ entry->mOffsetInTextInBlock + (offset - entry->mOffsetInTextNode);
+ *aSelLength = 0;
+
+ // Now move the caret so that it is actually in the text node.
+ // We do this to keep things in sync.
+ //
+ // In most cases, the user shouldn't see any movement in the caret
+ // on screen.
+ return SetSelectionInternal(*aSelOffset, *aSelLength, true);
+ }
+ }
+
+ return NS_ERROR_FAILURE;
+}
+
+nsresult TextServicesDocument::GetUncollapsedSelection(
+ BlockSelectionStatus* aSelStatus, uint32_t* aSelOffset,
+ uint32_t* aSelLength) {
+ RefPtr<const nsRange> range;
+ RefPtr<Selection> selection =
+ mSelCon->GetSelection(nsISelectionController::SELECTION_NORMAL);
+ NS_ENSURE_TRUE(selection, NS_ERROR_FAILURE);
+
+ // It is assumed that the calling function has made sure that the
+ // selection is not collapsed, and that the input params to this
+ // method are initialized to some defaults.
+
+ nsCOMPtr<nsINode> startContainer, endContainer;
+
+ const size_t tableCount = mOffsetTable.Length();
+
+ // Get pointers to the first and last offset entries
+ // in the table.
+
+ UniquePtr<OffsetEntry>& eStart = mOffsetTable[0];
+ UniquePtr<OffsetEntry>& eEnd =
+ tableCount > 1 ? mOffsetTable[tableCount - 1] : eStart;
+ LockOffsetEntryArrayLengthInDebugBuild(observer, mOffsetTable);
+
+ const uint32_t eStartOffset = eStart->mOffsetInTextNode;
+ const uint32_t eEndOffset = eEnd->EndOffsetInTextNode();
+
+ const uint32_t rangeCount = selection->RangeCount();
+ MOZ_ASSERT(rangeCount);
+
+ // Find the first range in the selection that intersects
+ // the current text block.
+ Maybe<int32_t> e1s2;
+ Maybe<int32_t> e2s1;
+ uint32_t startOffset, endOffset;
+ for (const uint32_t i : IntegerRange(rangeCount)) {
+ MOZ_ASSERT(selection->RangeCount() == rangeCount);
+ range = selection->GetRangeAt(i);
+ if (MOZ_UNLIKELY(NS_WARN_IF(!range))) {
+ return NS_ERROR_FAILURE;
+ }
+
+ nsresult rv =
+ GetRangeEndPoints(range, getter_AddRefs(startContainer), &startOffset,
+ getter_AddRefs(endContainer), &endOffset);
+
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ e1s2 = nsContentUtils::ComparePoints(eStart->mTextNode, eStartOffset,
+ endContainer, endOffset);
+ if (NS_WARN_IF(!e1s2)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ e2s1 = nsContentUtils::ComparePoints(eEnd->mTextNode, eEndOffset,
+ startContainer, startOffset);
+ if (NS_WARN_IF(!e2s1)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ // Break out of the loop if the text block intersects the current range.
+
+ if (*e1s2 <= 0 && *e2s1 >= 0) {
+ break;
+ }
+ }
+
+ // We're done if we didn't find an intersecting range.
+
+ if (rangeCount < 1 || *e1s2 > 0 || *e2s1 < 0) {
+ *aSelStatus = BlockSelectionStatus::eBlockOutside;
+ *aSelOffset = *aSelLength = UINT32_MAX;
+ return NS_OK;
+ }
+
+ // Now that we have an intersecting range, find out more info:
+ const Maybe<int32_t> e1s1 = nsContentUtils::ComparePoints(
+ eStart->mTextNode, eStartOffset, startContainer, startOffset);
+ if (NS_WARN_IF(!e1s1)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ const Maybe<int32_t> e2s2 = nsContentUtils::ComparePoints(
+ eEnd->mTextNode, eEndOffset, endContainer, endOffset);
+ if (NS_WARN_IF(!e2s2)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ if (rangeCount > 1) {
+ // There are multiple selection ranges, we only deal
+ // with the first one that intersects the current,
+ // text block, so mark this a as a partial.
+ *aSelStatus = BlockSelectionStatus::eBlockPartial;
+ } else if (*e1s1 > 0 && *e2s2 < 0) {
+ // The range extends beyond the start and
+ // end of the current text block.
+ *aSelStatus = BlockSelectionStatus::eBlockInside;
+ } else if (*e1s1 <= 0 && *e2s2 >= 0) {
+ // The current text block contains the entire
+ // range.
+ *aSelStatus = BlockSelectionStatus::eBlockContains;
+ } else {
+ // The range partially intersects the block.
+ *aSelStatus = BlockSelectionStatus::eBlockPartial;
+ }
+
+ // Now create a range based on the intersection of the
+ // text block and range:
+
+ nsCOMPtr<nsINode> p1, p2;
+ uint32_t o1, o2;
+
+ // The start of the range will be the rightmost
+ // start node.
+
+ if (*e1s1 >= 0) {
+ p1 = eStart->mTextNode;
+ o1 = eStartOffset;
+ } else {
+ p1 = startContainer;
+ o1 = startOffset;
+ }
+
+ // The end of the range will be the leftmost
+ // end node.
+
+ if (*e2s2 <= 0) {
+ p2 = eEnd->mTextNode;
+ o2 = eEndOffset;
+ } else {
+ p2 = endContainer;
+ o2 = endOffset;
+ }
+
+ range = nsRange::Create(p1, o1, p2, o2, IgnoreErrors());
+ if (NS_WARN_IF(!range)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ // Now iterate over this range to figure out the selection's
+ // block offset and length.
+
+ RefPtr<FilteredContentIterator> filteredIter;
+ nsresult rv =
+ CreateFilteredContentIterator(range, getter_AddRefs(filteredIter));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Find the first text node in the range.
+ nsCOMPtr<nsIContent> content;
+ filteredIter->First();
+ if (!p1->IsText()) {
+ bool found = false;
+ for (; !filteredIter->IsDone(); filteredIter->Next()) {
+ nsINode* node = filteredIter->GetCurrentNode();
+ if (node->IsText()) {
+ p1 = node->AsText();
+ o1 = 0;
+ found = true;
+ break;
+ }
+ }
+ NS_ENSURE_TRUE(found, NS_ERROR_FAILURE);
+ }
+
+ // Find the last text node in the range.
+ filteredIter->Last();
+ if (!p2->IsText()) {
+ bool found = false;
+ for (; !filteredIter->IsDone(); filteredIter->Prev()) {
+ nsINode* node = filteredIter->GetCurrentNode();
+ if (node->IsText()) {
+ p2 = node->AsText();
+ o2 = p2->AsText()->Length();
+ found = true;
+
+ break;
+ }
+ }
+ NS_ENSURE_TRUE(found, NS_ERROR_FAILURE);
+ }
+
+ bool found = false;
+ *aSelLength = 0;
+
+ for (size_t i = 0; i < tableCount; i++) {
+ const UniquePtr<OffsetEntry>& entry = mOffsetTable[i];
+ LockOffsetEntryArrayLengthInDebugBuild(observer, mOffsetTable);
+ if (!found) {
+ if (entry->mTextNode == p1.get() &&
+ entry->OffsetInTextNodeIsInRangeOrEndOffset(o1)) {
+ *aSelOffset =
+ entry->mOffsetInTextInBlock + (o1 - entry->mOffsetInTextNode);
+ if (p1 == p2 && entry->OffsetInTextNodeIsInRangeOrEndOffset(o2)) {
+ // The start and end of the range are in the same offset
+ // entry. Calculate the length of the range then we're done.
+ *aSelLength = o2 - o1;
+ break;
+ }
+ // Add the length of the sub string in this offset entry
+ // that follows the start of the range.
+ *aSelLength = entry->EndOffsetInTextNode() - o1;
+ found = true;
+ }
+ } else { // Found.
+ if (entry->mTextNode == p2.get() &&
+ entry->OffsetInTextNodeIsInRangeOrEndOffset(o2)) {
+ // We found the end of the range. Calculate the length of the
+ // sub string that is before the end of the range, then we're done.
+ *aSelLength += o2 - entry->mOffsetInTextNode;
+ break;
+ }
+ // The entire entry must be in the range.
+ *aSelLength += entry->mLength;
+ }
+ }
+
+ return NS_OK;
+}
+
+// static
+nsresult TextServicesDocument::GetRangeEndPoints(
+ const AbstractRange* aAbstractRange, nsINode** aStartContainer,
+ uint32_t* aStartOffset, nsINode** aEndContainer, uint32_t* aEndOffset) {
+ if (NS_WARN_IF(!aAbstractRange) || NS_WARN_IF(!aStartContainer) ||
+ NS_WARN_IF(!aEndContainer) || NS_WARN_IF(!aEndOffset)) {
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ nsCOMPtr<nsINode> startContainer = aAbstractRange->GetStartContainer();
+ if (NS_WARN_IF(!startContainer)) {
+ return NS_ERROR_FAILURE;
+ }
+ nsCOMPtr<nsINode> endContainer = aAbstractRange->GetEndContainer();
+ if (NS_WARN_IF(!endContainer)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ startContainer.forget(aStartContainer);
+ endContainer.forget(aEndContainer);
+ *aStartOffset = aAbstractRange->StartOffset();
+ *aEndOffset = aAbstractRange->EndOffset();
+ return NS_OK;
+}
+
+// static
+nsresult TextServicesDocument::FirstTextNode(
+ FilteredContentIterator* aFilteredIter, IteratorStatus* aIteratorStatus) {
+ if (aIteratorStatus) {
+ *aIteratorStatus = IteratorStatus::eDone;
+ }
+
+ for (aFilteredIter->First(); !aFilteredIter->IsDone();
+ aFilteredIter->Next()) {
+ if (aFilteredIter->GetCurrentNode()->NodeType() == nsINode::TEXT_NODE) {
+ if (aIteratorStatus) {
+ *aIteratorStatus = IteratorStatus::eValid;
+ }
+ break;
+ }
+ }
+
+ return NS_OK;
+}
+
+// static
+nsresult TextServicesDocument::LastTextNode(
+ FilteredContentIterator* aFilteredIter, IteratorStatus* aIteratorStatus) {
+ if (aIteratorStatus) {
+ *aIteratorStatus = IteratorStatus::eDone;
+ }
+
+ for (aFilteredIter->Last(); !aFilteredIter->IsDone(); aFilteredIter->Prev()) {
+ if (aFilteredIter->GetCurrentNode()->NodeType() == nsINode::TEXT_NODE) {
+ if (aIteratorStatus) {
+ *aIteratorStatus = IteratorStatus::eValid;
+ }
+ break;
+ }
+ }
+
+ return NS_OK;
+}
+
+// static
+nsresult TextServicesDocument::FirstTextNodeInCurrentBlock(
+ FilteredContentIterator* aFilteredIter) {
+ NS_ENSURE_TRUE(aFilteredIter, NS_ERROR_NULL_POINTER);
+
+ ClearDidSkip(aFilteredIter);
+
+ // Walk backwards over adjacent text nodes until
+ // we hit a block boundary:
+ RefPtr<Text> lastTextNode;
+ while (!aFilteredIter->IsDone()) {
+ nsCOMPtr<nsIContent> content =
+ aFilteredIter->GetCurrentNode()->IsContent()
+ ? aFilteredIter->GetCurrentNode()->AsContent()
+ : nullptr;
+ // We don't observe layout updates, therefore, we should consider whether
+ // block or inline only with the default definition of the element.
+ if (lastTextNode && content &&
+ (HTMLEditUtils::IsBlockElement(*content,
+ BlockInlineCheck::UseHTMLDefaultStyle) ||
+ content->IsHTMLElement(nsGkAtoms::br))) {
+ break;
+ }
+ if (content && content->IsText()) {
+ if (lastTextNode && !TextServicesDocument::HasSameBlockNodeParent(
+ *content->AsText(), *lastTextNode)) {
+ // We're done, the current text node is in a
+ // different block.
+ break;
+ }
+ lastTextNode = content->AsText();
+ }
+
+ aFilteredIter->Prev();
+
+ if (DidSkip(aFilteredIter)) {
+ break;
+ }
+ }
+
+ if (lastTextNode) {
+ aFilteredIter->PositionAt(lastTextNode);
+ }
+
+ // XXX: What should we return if last is null?
+
+ return NS_OK;
+}
+
+// static
+nsresult TextServicesDocument::FirstTextNodeInPrevBlock(
+ FilteredContentIterator* aFilteredIter) {
+ NS_ENSURE_TRUE(aFilteredIter, NS_ERROR_NULL_POINTER);
+
+ // XXX: What if mFilteredIter is not currently on a text node?
+
+ // Make sure mFilteredIter is pointing to the first text node in the
+ // current block:
+
+ nsresult rv = FirstTextNodeInCurrentBlock(aFilteredIter);
+
+ NS_ENSURE_SUCCESS(rv, NS_ERROR_FAILURE);
+
+ // Point mFilteredIter to the first node before the first text node:
+
+ aFilteredIter->Prev();
+
+ if (aFilteredIter->IsDone()) {
+ return NS_ERROR_FAILURE;
+ }
+
+ // Now find the first text node of the next block:
+
+ return FirstTextNodeInCurrentBlock(aFilteredIter);
+}
+
+// static
+nsresult TextServicesDocument::FirstTextNodeInNextBlock(
+ FilteredContentIterator* aFilteredIter) {
+ bool crossedBlockBoundary = false;
+
+ NS_ENSURE_TRUE(aFilteredIter, NS_ERROR_NULL_POINTER);
+
+ ClearDidSkip(aFilteredIter);
+
+ RefPtr<Text> previousTextNode;
+ while (!aFilteredIter->IsDone()) {
+ if (nsCOMPtr<nsIContent> content =
+ aFilteredIter->GetCurrentNode()->IsContent()
+ ? aFilteredIter->GetCurrentNode()->AsContent()
+ : nullptr) {
+ if (content->IsText()) {
+ if (crossedBlockBoundary ||
+ (previousTextNode && !TextServicesDocument::HasSameBlockNodeParent(
+ *previousTextNode, *content->AsText()))) {
+ break;
+ }
+ previousTextNode = content->AsText();
+ }
+ // We don't observe layout updates, therefore, we should consider whether
+ // block or inline only with the default definition of the element.
+ else if (!crossedBlockBoundary &&
+ (HTMLEditUtils::IsBlockElement(
+ *content, BlockInlineCheck::UseHTMLDefaultStyle) ||
+ content->IsHTMLElement(nsGkAtoms::br))) {
+ crossedBlockBoundary = true;
+ }
+ }
+
+ aFilteredIter->Next();
+
+ if (!crossedBlockBoundary && DidSkip(aFilteredIter)) {
+ crossedBlockBoundary = true;
+ }
+ }
+
+ return NS_OK;
+}
+
+nsresult TextServicesDocument::GetFirstTextNodeInPrevBlock(
+ nsIContent** aContent) {
+ NS_ENSURE_TRUE(aContent, NS_ERROR_NULL_POINTER);
+
+ *aContent = 0;
+
+ // Save the iterator's current content node so we can restore
+ // it when we are done:
+
+ nsINode* node = mFilteredIter->GetCurrentNode();
+
+ nsresult rv = FirstTextNodeInPrevBlock(mFilteredIter);
+
+ if (NS_FAILED(rv)) {
+ // Try to restore the iterator before returning.
+ mFilteredIter->PositionAt(node);
+ return rv;
+ }
+
+ if (!mFilteredIter->IsDone()) {
+ nsCOMPtr<nsIContent> current =
+ mFilteredIter->GetCurrentNode()->IsContent()
+ ? mFilteredIter->GetCurrentNode()->AsContent()
+ : nullptr;
+ current.forget(aContent);
+ }
+
+ // Restore the iterator:
+
+ return mFilteredIter->PositionAt(node);
+}
+
+nsresult TextServicesDocument::GetFirstTextNodeInNextBlock(
+ nsIContent** aContent) {
+ NS_ENSURE_TRUE(aContent, NS_ERROR_NULL_POINTER);
+
+ *aContent = 0;
+
+ // Save the iterator's current content node so we can restore
+ // it when we are done:
+
+ nsINode* node = mFilteredIter->GetCurrentNode();
+
+ nsresult rv = FirstTextNodeInNextBlock(mFilteredIter);
+
+ if (NS_FAILED(rv)) {
+ // Try to restore the iterator before returning.
+ mFilteredIter->PositionAt(node);
+ return rv;
+ }
+
+ if (!mFilteredIter->IsDone()) {
+ nsCOMPtr<nsIContent> current =
+ mFilteredIter->GetCurrentNode()->IsContent()
+ ? mFilteredIter->GetCurrentNode()->AsContent()
+ : nullptr;
+ current.forget(aContent);
+ }
+
+ // Restore the iterator:
+ return mFilteredIter->PositionAt(node);
+}
+
+Result<TextServicesDocument::IteratorStatus, nsresult>
+TextServicesDocument::OffsetEntryArray::Init(
+ FilteredContentIterator& aFilteredIter, IteratorStatus aIteratorStatus,
+ nsRange* aIterRange, nsAString* aAllTextInBlock /* = nullptr */) {
+ Clear();
+
+ if (aAllTextInBlock) {
+ aAllTextInBlock->Truncate();
+ }
+
+ if (aIteratorStatus == IteratorStatus::eDone) {
+ return IteratorStatus::eDone;
+ }
+
+ // If we have an aIterRange, retrieve the endpoints so
+ // they can be used in the while loop below to trim entries
+ // for text nodes that are partially selected by aIterRange.
+
+ nsCOMPtr<nsINode> rngStartNode, rngEndNode;
+ uint32_t rngStartOffset = 0, rngEndOffset = 0;
+ if (aIterRange) {
+ nsresult rv = TextServicesDocument::GetRangeEndPoints(
+ aIterRange, getter_AddRefs(rngStartNode), &rngStartOffset,
+ getter_AddRefs(rngEndNode), &rngEndOffset);
+ if (NS_FAILED(rv)) {
+ NS_WARNING("TextServicesDocument::GetRangeEndPoints() failed");
+ return Err(rv);
+ }
+ }
+
+ // The text service could have added text nodes to the beginning
+ // of the current block and called this method again. Make sure
+ // we really are at the beginning of the current block:
+
+ nsresult rv =
+ TextServicesDocument::FirstTextNodeInCurrentBlock(&aFilteredIter);
+ if (NS_FAILED(rv)) {
+ NS_WARNING("TextServicesDocument::FirstTextNodeInCurrentBlock() failed");
+ return Err(rv);
+ }
+
+ TextServicesDocument::ClearDidSkip(&aFilteredIter);
+
+ uint32_t offset = 0;
+ RefPtr<Text> firstTextNode, previousTextNode;
+ while (!aFilteredIter.IsDone()) {
+ if (nsCOMPtr<nsIContent> content =
+ aFilteredIter.GetCurrentNode()->IsContent()
+ ? aFilteredIter.GetCurrentNode()->AsContent()
+ : nullptr) {
+ // We don't observe layout updates, therefore, we should consider whether
+ // block or inline only with the default definition of the element.
+ if (HTMLEditUtils::IsBlockElement(
+ *content, BlockInlineCheck::UseHTMLDefaultStyle) ||
+ content->IsHTMLElement(nsGkAtoms::br)) {
+ break;
+ }
+ if (content->IsText()) {
+ if (previousTextNode && !TextServicesDocument::HasSameBlockNodeParent(
+ *previousTextNode, *content->AsText())) {
+ break;
+ }
+
+ nsString str;
+ content->AsText()->GetNodeValue(str);
+
+ // Add an entry for this text node into the offset table:
+
+ UniquePtr<OffsetEntry>& entry = *AppendElement(
+ MakeUnique<OffsetEntry>(*content->AsText(), offset, str.Length()));
+ LockOffsetEntryArrayLengthInDebugBuild(observer, *this);
+
+ // If one or both of the endpoints of the iteration range
+ // are in the text node for this entry, make sure the entry
+ // only accounts for the portion of the text node that is
+ // in the range.
+
+ uint32_t startOffset = 0;
+ uint32_t endOffset = str.Length();
+ bool adjustStr = false;
+
+ if (entry->mTextNode == rngStartNode) {
+ entry->mOffsetInTextNode = startOffset = rngStartOffset;
+ adjustStr = true;
+ }
+
+ if (entry->mTextNode == rngEndNode) {
+ endOffset = rngEndOffset;
+ adjustStr = true;
+ }
+
+ if (adjustStr) {
+ entry->mLength = endOffset - startOffset;
+ str = Substring(str, startOffset, entry->mLength);
+ }
+
+ offset += str.Length();
+
+ if (aAllTextInBlock) {
+ // Append the text node's string to the output string:
+ if (!firstTextNode) {
+ *aAllTextInBlock = str;
+ } else {
+ *aAllTextInBlock += str;
+ }
+ }
+
+ previousTextNode = content->AsText();
+
+ if (!firstTextNode) {
+ firstTextNode = content->AsText();
+ }
+ }
+ }
+
+ aFilteredIter.Next();
+
+ if (TextServicesDocument::DidSkip(&aFilteredIter)) {
+ break;
+ }
+ }
+
+ if (firstTextNode) {
+ // Always leave the iterator pointing at the first
+ // text node of the current block!
+ aFilteredIter.PositionAt(firstTextNode);
+ return aIteratorStatus;
+ }
+
+ // If we never ran across a text node, the iterator
+ // might have been pointing to something invalid to
+ // begin with.
+ return IteratorStatus::eDone;
+}
+
+void TextServicesDocument::OffsetEntryArray::RemoveInvalidElements() {
+ for (size_t i = 0; i < Length();) {
+ if (ElementAt(i)->mIsValid) {
+ i++;
+ continue;
+ }
+
+ RemoveElementAt(i);
+ if (!mSelection.IsSet()) {
+ continue;
+ }
+ if (mSelection.StartIndex() == i) {
+ NS_ASSERTION(false, "What should we do in this case?");
+ mSelection.Reset();
+ } else if (mSelection.StartIndex() > i) {
+ MOZ_DIAGNOSTIC_ASSERT(mSelection.StartIndex() - 1 < Length());
+ MOZ_DIAGNOSTIC_ASSERT(mSelection.EndIndex() - 1 < Length());
+ mSelection.SetIndexes(mSelection.StartIndex() - 1,
+ mSelection.EndIndex() - 1);
+ } else if (mSelection.EndIndex() >= i) {
+ MOZ_DIAGNOSTIC_ASSERT(mSelection.EndIndex() - 1 < Length());
+ mSelection.SetIndexes(mSelection.StartIndex(), mSelection.EndIndex() - 1);
+ }
+ }
+}
+
+nsresult TextServicesDocument::OffsetEntryArray::SplitElementAt(
+ size_t aIndex, uint32_t aOffsetInTextNode) {
+ OffsetEntry* leftEntry = ElementAt(aIndex).get();
+ MOZ_ASSERT(leftEntry);
+ NS_ASSERTION((aOffsetInTextNode > 0), "aOffsetInTextNode == 0");
+ NS_ASSERTION((aOffsetInTextNode < leftEntry->mLength),
+ "aOffsetInTextNode >= mLength");
+
+ if (aOffsetInTextNode < 1 || aOffsetInTextNode >= leftEntry->mLength) {
+ return NS_ERROR_FAILURE;
+ }
+
+ const uint32_t oldLength = leftEntry->mLength - aOffsetInTextNode;
+
+ // XXX(Bug 1631371) Check if this should use a fallible operation as it
+ // pretended earlier.
+ UniquePtr<OffsetEntry>& rightEntry = *InsertElementAt(
+ aIndex + 1,
+ MakeUnique<OffsetEntry>(leftEntry->mTextNode,
+ leftEntry->mOffsetInTextInBlock + oldLength,
+ aOffsetInTextNode));
+ LockOffsetEntryArrayLengthInDebugBuild(observer, *this);
+ leftEntry->mLength = oldLength;
+ rightEntry->mOffsetInTextNode = leftEntry->mOffsetInTextNode + oldLength;
+
+ return NS_OK;
+}
+
+Maybe<size_t> TextServicesDocument::OffsetEntryArray::FirstIndexOf(
+ const Text& aTextNode) const {
+ for (size_t i = 0; i < Length(); i++) {
+ if (ElementAt(i)->mTextNode == &aTextNode) {
+ return Some(i);
+ }
+ }
+ return Nothing();
+}
+
+// Spellchecker code has this. See bug 211343
+#define IS_NBSP_CHAR(c) (((unsigned char)0xa0) == (c))
+
+Result<EditorDOMRangeInTexts, nsresult>
+TextServicesDocument::OffsetEntryArray::FindWordRange(
+ nsAString& aAllTextInBlock, const EditorRawDOMPoint& aStartPointToScan) {
+ MOZ_ASSERT(aStartPointToScan.IsInTextNode());
+ // It's assumed that aNode is a text node. The first thing
+ // we do is get its index in the offset table so we can
+ // calculate the dom point's string offset.
+ Maybe<size_t> maybeEntryIndex =
+ FirstIndexOf(*aStartPointToScan.ContainerAs<Text>());
+ if (NS_WARN_IF(maybeEntryIndex.isNothing())) {
+ NS_WARNING(
+ "TextServicesDocument::OffsetEntryArray::FirstIndexOf() didn't find "
+ "entries");
+ return Err(NS_ERROR_FAILURE);
+ }
+
+ // Next we map offset into a string offset.
+
+ const UniquePtr<OffsetEntry>& entry = ElementAt(*maybeEntryIndex);
+ LockOffsetEntryArrayLengthInDebugBuild(observer, *this);
+ uint32_t strOffset = entry->mOffsetInTextInBlock +
+ aStartPointToScan.Offset() - entry->mOffsetInTextNode;
+
+ // Now we use the word breaker to find the beginning and end
+ // of the word from our calculated string offset.
+
+ const char16_t* str = aAllTextInBlock.BeginReading();
+ MOZ_ASSERT(strOffset <= aAllTextInBlock.Length(),
+ "The string offset shouldn't be greater than the string length!");
+
+ intl::WordRange res = intl::WordBreaker::FindWord(aAllTextInBlock, strOffset);
+
+ // Strip out the NBSPs at the ends
+ while (res.mBegin <= res.mEnd && IS_NBSP_CHAR(str[res.mBegin])) {
+ res.mBegin++;
+ }
+ if (str[res.mEnd] == static_cast<char16_t>(0x20)) {
+ uint32_t realEndWord = res.mEnd - 1;
+ while (realEndWord > res.mBegin && IS_NBSP_CHAR(str[realEndWord])) {
+ realEndWord--;
+ }
+ if (realEndWord < res.mEnd - 1) {
+ res.mEnd = realEndWord + 1;
+ }
+ }
+
+ // Now that we have the string offsets for the beginning
+ // and end of the word, run through the offset table and
+ // convert them back into dom points.
+
+ EditorDOMPointInText wordStart, wordEnd;
+ size_t lastIndex = Length() - 1;
+ for (size_t i = 0; i <= lastIndex; i++) {
+ // Check to see if res.mBegin is within the range covered
+ // by this entry. Note that if res.mBegin is after the last
+ // character covered by this entry, we will use the next
+ // entry if there is one.
+ const UniquePtr<OffsetEntry>& entry = ElementAt(i);
+ LockOffsetEntryArrayLengthInDebugBuild(observer, *this);
+ if (entry->mOffsetInTextInBlock <= res.mBegin &&
+ (res.mBegin < entry->EndOffsetInTextInBlock() ||
+ (res.mBegin == entry->EndOffsetInTextInBlock() && i == lastIndex))) {
+ wordStart.Set(entry->mTextNode, entry->mOffsetInTextNode + res.mBegin -
+ entry->mOffsetInTextInBlock);
+ }
+
+ // Check to see if res.mEnd is within the range covered
+ // by this entry.
+ if (entry->mOffsetInTextInBlock <= res.mEnd &&
+ res.mEnd <= entry->EndOffsetInTextInBlock()) {
+ if (res.mBegin == res.mEnd &&
+ res.mEnd == entry->EndOffsetInTextInBlock() && i != lastIndex) {
+ // Wait for the next round so that we use the same entry
+ // we did for aWordStartNode.
+ continue;
+ }
+
+ wordEnd.Set(entry->mTextNode, entry->mOffsetInTextNode + res.mEnd -
+ entry->mOffsetInTextInBlock);
+ break;
+ }
+ }
+
+ return EditorDOMRangeInTexts(wordStart, wordEnd);
+}
+
+/**
+ * nsIEditActionListener implementation:
+ * Don't implement the behavior directly here. The methods won't be called
+ * if the instance is created for inline spell checker created for editor.
+ * If you need to listen a new edit action, you need to add similar
+ * non-virtual method and you need to call it from EditorBase directly.
+ */
+
+NS_IMETHODIMP
+TextServicesDocument::DidDeleteNode(nsINode* aChild, nsresult aResult) {
+ if (NS_WARN_IF(NS_FAILED(aResult)) || NS_WARN_IF(!aChild) ||
+ !aChild->IsContent()) {
+ return NS_OK;
+ }
+ DidDeleteContent(*aChild->AsContent());
+ return NS_OK;
+}
+
+NS_IMETHODIMP TextServicesDocument::DidJoinContents(
+ const EditorRawDOMPoint& aJoinedPoint, const nsINode* aRemovedNode) {
+ if (MOZ_UNLIKELY(NS_WARN_IF(!aJoinedPoint.IsSetAndValid()) ||
+ NS_WARN_IF(!aRemovedNode->IsContent()))) {
+ return NS_OK;
+ }
+ DidJoinContents(aJoinedPoint, *aRemovedNode->AsContent());
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+TextServicesDocument::DidInsertText(CharacterData* aTextNode, int32_t aOffset,
+ const nsAString& aString,
+ nsresult aResult) {
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+TextServicesDocument::WillDeleteText(CharacterData* aTextNode, int32_t aOffset,
+ int32_t aLength) {
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+TextServicesDocument::WillDeleteRanges(
+ const nsTArray<RefPtr<nsRange>>& aRangesToDelete) {
+ return NS_OK;
+}
+
+#undef LockOffsetEntryArrayLengthInDebugBuild
+
+} // namespace mozilla