diff --git a/content/base/public/Makefile.in b/content/base/public/Makefile.in index 0ab521ef2bc0..41facbfcc681 100644 --- a/content/base/public/Makefile.in +++ b/content/base/public/Makefile.in @@ -89,6 +89,7 @@ XPIDLSRCS = \ nsIDocumentEncoder.idl \ nsIDOMParser.idl \ nsIDOMSerializer.idl \ + nsISelection2.idl \ nsISelectionController.idl \ nsISelectionDisplay.idl \ nsISelectionListener.idl \ diff --git a/content/base/public/nsISelection2.idl b/content/base/public/nsISelection2.idl new file mode 100644 index 000000000000..050940c30d29 --- /dev/null +++ b/content/base/public/nsISelection2.idl @@ -0,0 +1,65 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Selection code. + * + * The Initial Developer of the Original Code is + * Google Inc. + * Portions created by the Initial Developer are Copyright (C) 2006 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Brett Wilson + * + * Alternatively, the contents of this file may be used under the terms of + * either of the GNU General Public License Version 2 or later (the "GPL"), + * or the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +#include "nsISelection.idl" + +interface nsIDOMNode; +interface nsIDOMRange; + +%{C++ +#include "nsTArray.h" +%} + +[ptr] native RangeArray(nsCOMArray); + +[scriptable, uuid(eab4ae76-efdc-4e09-828c-bd921e9a662f)] +interface nsISelection2 : nsISelection +{ + void GetRangesForInterval( + in nsIDOMNode beginNode, in PRInt32 beginOffset, + in nsIDOMNode endNode, in PRInt32 endOffset, + in PRBool allowAdjacent, + out PRUint32 resultCount, + [retval, array, size_is(resultCount)] out nsIDOMRange results); + + [noscript] void GetRangesForIntervalCOMArray( + in nsIDOMNode beginNode, in PRInt32 beginOffset, + in nsIDOMNode endNode, in PRInt32 endOffset, + in PRBool allowAdjacent, + in RangeArray results); +}; diff --git a/layout/generic/nsSelection.cpp b/layout/generic/nsSelection.cpp index 4568af8b8e80..e5fbf77f05eb 100644 --- a/layout/generic/nsSelection.cpp +++ b/layout/generic/nsSelection.cpp @@ -50,6 +50,7 @@ #include "nsIDOMRange.h" #include "nsFrameSelection.h" #include "nsISelection.h" +#include "nsISelection2.h" #include "nsISelectionPrivate.h" #include "nsISelectionListener.h" #include "nsIComponentManager.h" @@ -182,7 +183,16 @@ struct CachedOffsetForFrame { PRPackedBool mCanCacheFrameOffset; // cached frame offset is valid? }; -class nsTypedSelection : public nsISelection, +struct RangeData +{ + RangeData(nsIDOMRange* aRange, PRInt32 aEndIndex) : + mRange(aRange), mEndIndex(aEndIndex) {} + + nsCOMPtr mRange; + PRInt32 mEndIndex; // index into mRangeEndings of this item +}; + +class nsTypedSelection : public nsISelection2, public nsISelectionPrivate, public nsSupportsWeakReference { @@ -193,6 +203,7 @@ public: NS_DECL_ISUPPORTS NS_DECL_NSISELECTION + NS_DECL_NSISELECTION2 NS_DECL_NSISELECTIONPRIVATE // utility methods for scrolling the selection into view @@ -208,8 +219,8 @@ public: NS_IMETHOD ScrollIntoView(SelectionRegion aRegion=nsISelectionController::SELECTION_FOCUS_REGION, PRBool aIsSynchronous=PR_TRUE); nsresult AddItem(nsIDOMRange *aRange); nsresult RemoveItem(nsIDOMRange *aRange); - nsresult Clear(nsPresContext* aPresContext); + // methods for convenience. Note, these don't addref nsIDOMNode* FetchAnchorNode(); //where did the selection begin PRInt32 FetchAnchorOffset(); @@ -300,7 +311,55 @@ private: NS_IMETHOD FixupSelectionPoints(nsIDOMRange *aRange, nsDirection *aDir, PRBool *aFixupState); #endif //OLD_SELECTION - nsCOMArray mRangeArray; + // These are the ranges inside this selection. They are kept sorted in order + // of DOM position of start and end, respectively (both of these arrays + // should have the same contents, but possibly in different orders). + // + // This data structure is sorted by the range beginnings and the range + // endings. When searching for a range, we can discard ranges whose ends + // are before the point in question, or whose beginnings are after. We can + // find these two sets of ranges on O(log n) time. + // + // Merging these two result sets takes O(n) time, so a a full query is O(log + // n + n) time. This looks worse than brute force O(n) searching, but in + // practice it is much faster. The DOM comparisons used in a brute force + // search are VERY expensive because they actually walk the DOM tree. We only + // do log(n) of these comparisons. + // + // Our O(n) merging step uses very fast integer comparisons, and, since + // we store the range ending index in the structure, we have to merge at + // most half of the results. Timing shows that this algorithm is nearly + // twice as fast doing intersections for 9 ranges, and 18 times faster for + // 250 ranges. + // + // An interval tree would give us O(log n) time lookups, which would be + // better. However, this approach gets us most of the way, and doesn't + // require rebalancing and other overhead. If this algorithm is found to be + // a bottleneck, it should be replaced with an interval tree. + // + // It has been discussed requiring selections to be disjoint in the future. + // If this requirement is added, the current design makes the most sense, we + // can just remove the array sorted by endings. + + nsTArray mRanges; + nsTArray mRangeEndings; // references info mRanges +#ifdef DEBUG + PRBool ValidateRanges(); +#endif + + nsresult FindInsertionPoint( + const nsTArray* aRemappingArray, + nsIDOMNode* aPointNode, PRInt32 aPointOffset, + nsresult (*aComparator)(nsIDOMNode*,PRInt32,nsIDOMRange*,PRInt32*), + PRInt32* aInsertionPoint); + nsresult MoveIndexToFirstMatch(PRInt32* aIndex, nsIDOMNode* aNode, + PRInt32 aOffset, + const nsTArray* aArray, + PRBool aUseBeginning); + nsresult MoveIndexToNextMismatch(PRInt32* aIndex, nsIDOMNode* aNode, + PRInt32 aOffset, + const nsTArray* aRemappingArray, + PRBool aUseBeginning); nsCOMPtr mAnchorFocusRange; nsCOMPtr mOriginalAnchorRange; //used as a point with range gravity for security @@ -657,7 +716,7 @@ NS_IMETHODIMP nsSelectionIterator::Next() { mIndex++; - PRInt32 cnt = mDomSelection->mRangeArray.Count(); + PRInt32 cnt = mDomSelection->mRanges.Length(); if (mIndex < cnt) return NS_OK; return NS_ERROR_FAILURE; @@ -692,7 +751,7 @@ nsSelectionIterator::Last() { if (!mDomSelection) return NS_ERROR_NULL_POINTER; - mIndex = mDomSelection->mRangeArray.Count() - 1; + mIndex = mDomSelection->mRanges.Length() - 1; return NS_OK; } @@ -704,11 +763,11 @@ nsSelectionIterator::CurrentItem(nsISupports **aItem) if (!aItem) return NS_ERROR_NULL_POINTER; - if (mIndex < 0 || mIndex >= mDomSelection->mRangeArray.Count()) { + if (mIndex < 0 || mIndex >= (PRInt32)mDomSelection->mRanges.Length()) { return NS_ERROR_FAILURE; } - return CallQueryInterface(mDomSelection->mRangeArray[mIndex], + return CallQueryInterface(mDomSelection->mRanges[mIndex].mRange, aItem); } @@ -718,11 +777,11 @@ nsSelectionIterator::CurrentItem(nsIDOMRange **aItem) { if (!aItem) return NS_ERROR_NULL_POINTER; - if (mIndex < 0 || mIndex >= mDomSelection->mRangeArray.Count()) { + if (mIndex < 0 || mIndex >= (PRInt32)mDomSelection->mRanges.Length()) { return NS_ERROR_FAILURE; } - *aItem = mDomSelection->mRangeArray[mIndex]; + *aItem = mDomSelection->mRanges[mIndex].mRange; NS_IF_ADDREF(*aItem); return NS_OK; } @@ -732,7 +791,7 @@ nsSelectionIterator::CurrentItem(nsIDOMRange **aItem) NS_IMETHODIMP nsSelectionIterator::IsDone() { - PRInt32 cnt = mDomSelection->mRangeArray.Count(); + PRInt32 cnt = mDomSelection->mRanges.Length(); if (mIndex >= 0 && mIndex < cnt) { return NS_ENUMERATOR_FALSE; } @@ -2488,26 +2547,13 @@ nsFrameSelection::LookUpSelection(nsIContent *aContent, PRInt32 aContentOffset, if (!aContent || !mShell) return nsnull; - // if any of the additional selections are set, always do a slow check. The - // issue is that the NS_FRAME_SELECTED_CONTENT bit is set when any of the - // selections are used, and the 'non-slow' code assumes that this means that - // there exists something selected for all selection types. - PRInt8 j; - for (j = (PRInt8) 1; j < (PRInt8)nsISelectionController::NUM_SELECTIONTYPES; j++){ - if (mDomSelections[j]){ - PRBool iscollapsed; - mDomSelections[j]->GetIsCollapsed(&iscollapsed); - if (!iscollapsed){ - aSlowCheck = PR_TRUE; - } - } - } - SelectionDetails* details = nsnull; - for (j = (PRInt8) 0; j < (PRInt8)nsISelectionController::NUM_SELECTIONTYPES; j++){ - if (mDomSelections[j]) - mDomSelections[j]->LookUpSelection(aContent, aContentOffset, aContentLength, &details, (SelectionType)(1<LookUpSelection(aContent, aContentOffset, + aContentLength, &details, (SelectionType)(1<mSelectingTableCellMode == TABLESELECTION_NONE) mFrameSelection->mSelectingTableCellMode = tableMode; - PRInt32 count = mRangeArray.Count(); - - if (count > 0) - { - // Adding a cell range to existing list of cell ranges - PRInt32 index; - PRInt32 row, col; - // Insert range at appropriate location - for (index = 0; index < count; index++) - { - nsIDOMRange* range = mRangeArray[index]; - if (!range) return NS_ERROR_FAILURE; - - PRInt32 selectionMode; - result = getTableCellLocationFromRange(range, &selectionMode, &row, &col); - if (NS_FAILED(result)) return result; - - // Don't proceed if range not a table cell - if (selectionMode != nsISelectionPrivate::TABLESELECTION_CELL) - return NS_OK; - - if (row > newRow || - (row == newRow && col > newCol)) - { - // Existing selected cell is after cell to add, - // so insert at this index - *aDidAddRange = mRangeArray.InsertObjectAt(aRange, index); - return (*aDidAddRange) ? NS_OK : NS_ERROR_FAILURE; - } - } - } - // If here, we are adding a selected cell range - // to end of range array or it's the first selected range - *aDidAddRange = mRangeArray.AppendObject(aRange); - return (*aDidAddRange) ? NS_OK : NS_ERROR_FAILURE; + return AddItem(aRange); } //TODO: Figure out TABLESELECTION_COLUMN and TABLESELECTION_ALLCELLS @@ -4035,6 +4047,7 @@ void nsTypedSelection::DetachFromPresentation() { // QueryInterface implementation for nsRange NS_INTERFACE_MAP_BEGIN(nsTypedSelection) NS_INTERFACE_MAP_ENTRY(nsISelection) + NS_INTERFACE_MAP_ENTRY(nsISelection2) NS_INTERFACE_MAP_ENTRY(nsISelectionPrivate) NS_INTERFACE_MAP_ENTRY(nsISupportsWeakReference) NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, nsISelection) @@ -4135,14 +4148,14 @@ NS_IMETHODIMP nsTypedSelection::GetFocusOffset(PRInt32* aFocusOffset) void nsTypedSelection::setAnchorFocusRange(PRInt32 indx) { - if (indx >= mRangeArray.Count()) + if (indx >= (PRInt32)mRanges.Length()) return; if (indx < 0) //release all { mAnchorFocusRange = nsnull; } else{ - mAnchorFocusRange = mRangeArray[indx]; + mAnchorFocusRange = mRanges[indx].mRange; } } @@ -4261,14 +4274,206 @@ nsTypedSelection::FetchEndOffset(nsIDOMRange *aRange) return 0; } +static nsresult +CompareToRangeStart(nsIDOMNode* aCompareNode, PRInt32 aCompareOffset, + nsIDOMRange* aRange, PRInt32* cmp) +{ + nsCOMPtr startNode; + nsresult rv = aRange->GetStartContainer(getter_AddRefs(startNode)); + NS_ENSURE_SUCCESS(rv, rv); + + PRInt32 startOffset; + rv = aRange->GetStartOffset(&startOffset); + NS_ENSURE_SUCCESS(rv, rv); + + *cmp = nsRange::ComparePoints(aCompareNode, aCompareOffset, + startNode, startOffset); + return NS_OK; +} + +static nsresult +CompareToRangeEnd(nsIDOMNode* aCompareNode, PRInt32 aCompareOffset, + nsIDOMRange* aRange, PRInt32* cmp) +{ + nsCOMPtr endNode; + nsresult rv = aRange->GetEndContainer(getter_AddRefs(endNode)); + NS_ENSURE_SUCCESS(rv, rv); + + PRInt32 endOffset; + rv = aRange->GetEndOffset(&endOffset); + NS_ENSURE_SUCCESS(rv, rv); + + *cmp = nsRange::ComparePoints(aCompareNode, aCompareOffset, + endNode, endOffset); + return NS_OK; +} + +#ifdef DEBUG +// checks the indices to make sure the range bookkeeping is up-to-date, useful +// for debugging to make sure everything is consistent +PRBool +nsTypedSelection::ValidateRanges() +{ + if (mRanges.Length() != mRangeEndings.Length()) { + NS_NOTREACHED("Lengths don't match"); + return PR_FALSE; + } + + // check the main array and make sure they refer to the correct sorted ones + PRUint32 i; + for (i = 0; i < mRanges.Length(); i ++) { + if (mRanges[i].mEndIndex < 0 || + mRanges[i].mEndIndex >= (PRInt32)mRangeEndings.Length()) { + NS_NOTREACHED("Sorted index is out of bounds"); + return PR_FALSE; + } + if (mRangeEndings[mRanges[i].mEndIndex] != (PRInt32)i) { + NS_NOTREACHED("Beginning or ending isn't correct"); + return PR_FALSE; + } + } + + // check the other array to make sure they all refer to valid indices + for (i = 0; i < mRangeEndings.Length(); i ++) { + if (mRangeEndings[i] < 0 || + mRangeEndings[i] >= (PRInt32)mRanges.Length()) { + NS_NOTREACHED("Ending refers to invalid index"); + return PR_FALSE; + } + } + + return PR_TRUE; +} +#endif + +// nsTypedSelection::FindInsertionPoint +// +// Binary searches the sorted array of ranges for the insertion point for +// the given node/offset. The given comparator is used, and the index where +// the point should appear in the array is placed in *aInsertionPoint; +// +// If the remapping array is given, we'll find the position in that array +// for the insertion, where that array contains indices that reference +// the range array. This can be NULL to not use a remapping array. +// +// If there is item(s) in the array equal to the input point, we will return +// a random index between or adjacent to this item(s). + +nsresult +nsTypedSelection::FindInsertionPoint( + const nsTArray* aRemappingArray, + nsIDOMNode* aPointNode, PRInt32 aPointOffset, + nsresult (*aComparator)(nsIDOMNode*,PRInt32,nsIDOMRange*,PRInt32*), + PRInt32* aInsertionPoint) +{ + nsresult rv; + NS_ASSERTION(!aRemappingArray || aRemappingArray->Length() == mRanges.Length(), + "Remapping array must have the same entries as the range array"); + + PRInt32 beginSearch = 0; + PRInt32 endSearch = mRanges.Length(); // one beyond what to check + while (endSearch - beginSearch > 0) { + PRInt32 center = (endSearch - beginSearch) / 2 + beginSearch; + + nsIDOMRange* range; + if (aRemappingArray) + range = mRanges[(*aRemappingArray)[center]].mRange; + else + range = mRanges[center].mRange; + + PRInt32 cmp; + rv = aComparator(aPointNode, aPointOffset, range, &cmp); + NS_ENSURE_SUCCESS(rv, rv); + + if (cmp < 0) { // point < cur + endSearch = center; + } else if (cmp > 0) { // point > cur + beginSearch = center + 1; + } else { // found match, done + beginSearch = center; + break; + } + } + *aInsertionPoint = beginSearch; + return NS_OK; +} + nsresult nsTypedSelection::AddItem(nsIDOMRange *aItem) { + nsresult rv; if (!aItem) return NS_ERROR_NULL_POINTER; - return mRangeArray.AppendObject(aItem) ? NS_OK : NS_ERROR_FAILURE; -} + NS_ASSERTION(ValidateRanges(), "Ranges out of sync"); + + // a common case is that we have no ranges yet + if (mRanges.Length() == 0) { + if (! mRanges.AppendElement(RangeData(aItem, 0))) + return NS_ERROR_OUT_OF_MEMORY; + if (! mRangeEndings.AppendElement(0)) { + mRanges.Clear(); + return NS_ERROR_OUT_OF_MEMORY; + } + return NS_OK; + } + + nsCOMPtr itemNode; + PRInt32 itemOffset; + rv = aItem->GetStartContainer(getter_AddRefs(itemNode)); + NS_ENSURE_SUCCESS(rv, rv); + rv = aItem->GetStartOffset(&itemOffset); + NS_ENSURE_SUCCESS(rv, rv); + + PRInt32 beginInsertionPoint; + rv = FindInsertionPoint(nsnull, itemNode, itemOffset, + CompareToRangeStart, &beginInsertionPoint); + NS_ENSURE_SUCCESS(rv, rv); + + // XXX Performance: 99% of the time, the beginning array and the ending array + // will be the same because the ranges do not overlap. We could save a few + // compares (which can be expensive) in this common case by special casing + // this. + + rv = aItem->GetEndContainer(getter_AddRefs(itemNode)); + NS_ENSURE_SUCCESS(rv, rv); + rv = aItem->GetEndOffset(&itemOffset); + NS_ENSURE_SUCCESS(rv, rv); + + PRInt32 endInsertionPoint; + rv = FindInsertionPoint(&mRangeEndings, itemNode, itemOffset, + CompareToRangeEnd, &endInsertionPoint); + NS_ENSURE_SUCCESS(rv, rv); + + // insert the range, being careful to revert everything on error to keep + // consistency + if (! mRanges.InsertElementAt(beginInsertionPoint, + RangeData(aItem, endInsertionPoint))) { + return NS_ERROR_OUT_OF_MEMORY; + } + if (! mRangeEndings.InsertElementAt(endInsertionPoint, beginInsertionPoint)) { + mRanges.RemoveElementAt(beginInsertionPoint); + return NS_ERROR_OUT_OF_MEMORY; + } + + // adjust the end indices that point to the main list + PRUint32 i; + for (i = 0; i < mRangeEndings.Length(); i ++) { + if (mRangeEndings[i] >= beginInsertionPoint) + mRangeEndings[i] ++; + } + + // the last loop updated the inserted index as well, so we need to put it + // back (this saves a comparison in that loop) + mRangeEndings[endInsertionPoint] = beginInsertionPoint; + + // adjust the begin/end indices + for (i = endInsertionPoint + 1; i < mRangeEndings.Length(); i ++) + mRanges[mRangeEndings[i]].mEndIndex = i; + + NS_ASSERTION(ValidateRanges(), "Ranges out of sync"); + return NS_OK; +} nsresult @@ -4276,26 +4481,57 @@ nsTypedSelection::RemoveItem(nsIDOMRange *aItem) { if (!aItem) return NS_ERROR_NULL_POINTER; - mRangeArray.RemoveObject(aItem); - return NS_OK; -} + NS_ASSERTION(ValidateRanges(), "Ranges out of sync"); + // Find the range's index & remove it. We could use FindInsertionPoint to + // get O(log n) time, but that requires many expensive DOM comparisons. + // For even several thousand items, this is probably faster because the + // comparisons are so fast. + PRInt32 idx = -1; + PRUint32 i; + for (i = 0; i < mRanges.Length(); i ++) { + if (mRanges[i].mRange == aItem) { + idx = (PRInt32)i; + break; + } + } + if (index < 0) + return NS_ERROR_INVALID_ARG; + mRanges.RemoveElementAt(idx); + + // need to update the range ending list to reflect the removed item + PRInt32 endingIndex = -1; + for (i = 0; i < mRangeEndings.Length(); i ++) { + if (mRangeEndings[i] == idx) + endingIndex = i; + if (mRangeEndings[i] > idx) + mRangeEndings[i] --; + } + NS_ASSERTION(endingIndex >= 0 && endingIndex < (PRInt32)mRangeEndings.Length(), + "Index not found in ending list"); + + // remove from the sorted lists + mRangeEndings.RemoveElementAt(endingIndex); + + // adjust indices of the RangeData structures + for (i = endingIndex; i < mRangeEndings.Length(); i ++) + mRanges[mRangeEndings[i]].mEndIndex = i; + + NS_ASSERTION(ValidateRanges(), "Ranges out of sync"); + return NS_OK; +} nsresult nsTypedSelection::Clear(nsPresContext* aPresContext) { setAnchorFocusRange(-1); - // Get an iterator - while (PR_TRUE) - { - PRInt32 cnt = mRangeArray.Count(); - if (cnt == 0) - break; - nsCOMPtr range = mRangeArray[0]; - mRangeArray.RemoveObjectAt(0); - selectFrames(aPresContext, range, 0); - } + + for (PRInt32 i = 0; i < (PRInt32)mRanges.Length(); i ++) + selectFrames(aPresContext, mRanges[i].mRange, 0); + mRanges.Clear(); + mRangeEndings.Clear(); + // Reset direction so for more dependable table selection range handling SetDirection(eDirNext); @@ -4308,6 +4544,257 @@ nsTypedSelection::Clear(nsPresContext* aPresContext) return NS_OK; } +// nsTypedSelection::MoveIndexToFirstMatch +// +// This adjusts the given index backwards in the range array so that it +// points to the first match of (node,offset). There might be any number +// of identical sequencial items in the array and the index can initially +// point to any one of them. When complete, the index will be moved to +// point to the first one of these. If the index does not point to a +// match of (node,offset) or there is only one match, the index will be +// untouched. +// +// If there are multiple ranges beginning at the requested position, we'll +// get a random index in between those from FindInsertionPoint. We want the +// index to be at the first match in the array. +// +// If the remapping array (sorted list containing indices into mRanges) +// is given, we'll use that for sorting and consider the index into +// that array. If NULL, we'll just use mRanges directly. +// +// If aUseBeginning is set we'll compare to the range beginnings in the +// selection. If false, we'll compare to range endings. + +nsresult +nsTypedSelection::MoveIndexToFirstMatch(PRInt32* aIndex, nsIDOMNode* aNode, + PRInt32 aOffset, + const nsTArray* aRemappingArray, + PRBool aUseBeginning) +{ + nsresult rv; + nsCOMPtr curNode; + PRInt32 curOffset; + while (*aIndex > 0) { + nsIDOMRange* range; + if (aRemappingArray) + range = mRanges[(*aRemappingArray)[(*aIndex) - 1]].mRange; + else + range = mRanges[(*aIndex) - 1].mRange; + + if (aUseBeginning) { + rv = range->GetStartContainer(getter_AddRefs(curNode)); + NS_ENSURE_SUCCESS(rv, rv); + rv = range->GetStartOffset(&curOffset); + NS_ENSURE_SUCCESS(rv, rv); + } else { + rv = range->GetEndContainer(getter_AddRefs(curNode)); + NS_ENSURE_SUCCESS(rv, rv); + rv = range->GetEndOffset(&curOffset); + NS_ENSURE_SUCCESS(rv, rv); + } + + if (curNode != aNode) + break; // not a match + if (curOffset != aOffset) + break; // not a match + + // the previous node matches, go back one + (*aIndex) --; + } + return NS_OK; +} + +// nsTypedSelection::MoveIndexToNextMismatch +// +// The same as MoveIndexToFirstMatch but increments the index until it +// points to something that doesn't match (node,offset). +// +// If there is one or more range ending at the requested position, we +// want to start checking the first one that ends inside the range. This +// moves the given index to point to the first index inside the range. +// It may be one past the last item in the array. +// +// If the requested DOM position is '9', and the caller got index 3 +// from FindInsertionPoint with points in the range array: +// 6 8 9 9 9 13 19 +// ^ +// input +// The input point will point to a random '9' in the array, or possibly +// the '13', which are all valid points for a new '9' to live in this array. +// This function will move the index to point to the '13' in this case, +// which is the first offset not matching the input position. +// +// See MoveIndexToFirstMatch for the meaning of aRemappingArray and +// aUseBeginning. + +nsresult +nsTypedSelection::MoveIndexToNextMismatch(PRInt32* aIndex, nsIDOMNode* aNode, + PRInt32 aOffset, + const nsTArray* aRemappingArray, + PRBool aUseBeginning) +{ + nsresult rv; + nsCOMPtr curNode; + PRInt32 curOffset; + while (*aIndex < (PRInt32)mRanges.Length()) { + nsIDOMRange* range; + if (aRemappingArray) + range = mRanges[(*aRemappingArray)[*aIndex]].mRange; + else + range = mRanges[*aIndex].mRange; + + if (aUseBeginning) { + rv = range->GetStartContainer(getter_AddRefs(curNode)); + NS_ENSURE_SUCCESS(rv, rv); + rv = range->GetStartOffset(&curOffset); + NS_ENSURE_SUCCESS(rv, rv); + } else { + rv = range->GetEndContainer(getter_AddRefs(curNode)); + NS_ENSURE_SUCCESS(rv, rv); + rv = range->GetEndOffset(&curOffset); + NS_ENSURE_SUCCESS(rv, rv); + } + + if (curNode != aNode) + break; // mismatch + if (curOffset != aOffset) + break; // mismatch + + // this node matches, go to the next one + (*aIndex) ++; + } + return NS_OK; +} + +// nsTypedSelection::GetRangesForInterval +// +// XPCOM wrapper for the COMArray version + +NS_IMETHODIMP +nsTypedSelection::GetRangesForInterval(nsIDOMNode* aBeginNode, PRInt32 aBeginOffset, + nsIDOMNode* aEndNode, PRInt32 aEndOffset, + PRBool aAllowAdjacent, + PRUint32 *aResultCount, + nsIDOMRange ***aResults) +{ + nsresult rv; + *aResultCount = 0; + *aResults = nsnull; + if (! aBeginNode || ! aEndNode || ! aResultCount || ! aResults) + return NS_ERROR_NULL_POINTER; + + nsCOMArray results; + rv = GetRangesForIntervalCOMArray(aBeginNode, aBeginOffset, + aEndNode, aEndOffset, + aAllowAdjacent, + &results); + NS_ENSURE_SUCCESS(rv, rv); + if (results.Count() == 0) + return NS_OK; + + *aResults = NS_STATIC_CAST(nsIDOMRange**, + nsMemory::Alloc(sizeof(nsIDOMRange*) * results.Count())); + NS_ENSURE_TRUE(*aResults, NS_ERROR_OUT_OF_MEMORY); + + *aResultCount = results.Count(); + for (PRInt32 i = 0; i < results.Count(); i ++) + NS_ADDREF((*aResults)[i] = results[i]); + return NS_OK; +} + +// nsTypedSelection::GetRangesForIntervalCOMArray +// +// Fills a COM array with the ranges overlapping the range specified by +// the given endpoints. Ranges in the selection exactly adjacent to the +// input range are not returned unless aAllowAdjacent is set. + +NS_IMETHODIMP +nsTypedSelection::GetRangesForIntervalCOMArray(nsIDOMNode* aBeginNode, PRInt32 aBeginOffset, + nsIDOMNode* aEndNode, PRInt32 aEndOffset, + PRBool aAllowAdjacent, + nsCOMArray* aRanges) +{ + nsresult rv; + NS_ASSERTION(ValidateRanges(), "Ranges out of sync"); + aRanges->Clear(); + if (mRanges.Length() == 0) + return NS_OK; + + // Ranges that begin after the checked range, and ranges that end before + // the checked range can be discarded. The beginning index is the offset + // into the beginning array of the first item we DON'T have to check. + + // ...index into the beginning array that is the FIRST ITEM OUTSIDE + // OUR RANGE + PRInt32 beginningIndex; + rv = FindInsertionPoint(nsnull, aEndNode, aEndOffset, + &CompareToRangeStart, &beginningIndex); + NS_ENSURE_SUCCESS(rv, rv); + if (beginningIndex == 0) + return NS_OK; // optimization: all ranges are after us + + // ...index into the ending array that is the FIRST ITEM WE WANT TO CHECK + PRInt32 endingIndex; + rv = FindInsertionPoint(&mRangeEndings, aBeginNode, aBeginOffset, + &CompareToRangeEnd, &endingIndex); + NS_ENSURE_SUCCESS(rv, rv); + if (endingIndex == (PRInt32)mRangeEndings.Length()) + return NS_OK; // optimization: all ranges are before us + + // adjust the indices in case of exact matches, FindInsertionPoint will + // give us a random index that would still be sorted if there is a match + if (aAllowAdjacent) { + // include adjacent points + // + // Recall that we want all things in mRangeEndings (indexed by endingIndex) + // before the requested beginning, and everything in mRanges (indexed by + // beginningIndex) after the requested ending. + // + // 1 3 5 5 5 8 9 10 10 10 11 12 <-- imaginary DOM positions of ranges + // ^ ^ <-- we have this + // ^ ^ <-- compute this range + // endingIndex beginningIndex + rv = MoveIndexToFirstMatch(&endingIndex, aBeginNode, aBeginOffset, + &mRangeEndings, PR_FALSE); + NS_ENSURE_SUCCESS(rv, rv); + rv = MoveIndexToNextMismatch(&beginningIndex, aEndNode, aEndOffset, + nsnull, PR_TRUE); + NS_ENSURE_SUCCESS(rv, rv); + } else { + // exclude adjacent points, see previous case + // 1 3 5 5 5 8 9 10 10 10 11 12 <-- imaginary DOM positions of ranges + // ^ ^ <-- we have this + // ^ ^ <-- compute this range + // endingIndex beginningIndex + rv = MoveIndexToNextMismatch(&endingIndex, aBeginNode, aBeginOffset, + &mRangeEndings, PR_FALSE); + NS_ENSURE_SUCCESS(rv, rv); + rv = MoveIndexToFirstMatch(&beginningIndex, aEndNode, aEndOffset, + nsnull, PR_TRUE); + NS_ENSURE_SUCCESS(rv, rv); + } + + // check for overlaps in the two ranges by linearly searching the smallest + // set of matches + if (beginningIndex > (PRInt32)mRangeEndings.Length() - endingIndex) { + // check ending array because its smaller + for (PRInt32 i = endingIndex; i < (PRInt32)mRangeEndings.Length(); i ++) { + if (mRangeEndings[i] < beginningIndex) { + if (! aRanges->AppendObject(mRanges[mRangeEndings[i]].mRange)) + return NS_ERROR_OUT_OF_MEMORY; + } + } + } else { + for (PRInt32 i = 0; i < beginningIndex; i ++) { + if (mRanges[i].mEndIndex >= endingIndex) { + if (! aRanges->AppendObject(mRanges[i].mRange)) + return NS_ERROR_OUT_OF_MEMORY; + } + } + } + return NS_OK; +} + //utility method to get the primary frame of node or use the offset to get frame of child node #if 0 @@ -4590,141 +5077,117 @@ nsTypedSelection::selectFrames(nsPresContext* aPresContext, nsIDOMRange *aRange, return result; } +// nsTypedSelection::LookUpSelection +// +// This function is called when a node wants to know where the selection is +// over itself. +// +// Usually, this is called when we already know there is a selection over +// the node in question, and we only need to find the boundaries of it on +// that node. This is when slowCheck is false--a strict test is not needed. +// Other times, the caller has no idea, and wants us to test everything, +// so we are supposed to determine whether there is a selection over the +// node at all. +// +// A previous version of this code used this flag to do less work when +// inclusion was already known (slowCheck=false). However, our tree +// structure allows us to quickly determine ranges overlapping the node, +// so we just ignore the slowCheck flag and do the full test every time. +// +// PERFORMANCE: a common case is that we are doing a fast check with exactly +// one range in the selection. In this case, this function is slower than +// brute force because of the overhead of checking the tree. We can optimize +// this case to make it faster by doing the same thing the previous version +// of this function did in the case of 1 range. This would also mean that +// the aSlowCheck flag would have meaning again. NS_IMETHODIMP -nsTypedSelection::LookUpSelection(nsIContent *aContent, PRInt32 aContentOffset, PRInt32 aContentLength, - SelectionDetails **aReturnDetails, SelectionType aType, PRBool aSlowCheck) +nsTypedSelection::LookUpSelection(nsIContent *aContent, PRInt32 aContentOffset, + PRInt32 aContentLength, + SelectionDetails **aReturnDetails, + SelectionType aType, PRBool aSlowCheck) { - PRInt32 cnt; - nsresult rv = GetRangeCount(&cnt); - if (NS_FAILED(rv)) - return rv; - PRInt32 i; + nsresult rv; + if (! aContent || ! aReturnDetails) + return NS_ERROR_NULL_POINTER; - nsCOMPtr passedInNode; - passedInNode = do_QueryInterface(aContent); - if (!passedInNode) - return NS_ERROR_FAILURE; + // it is common to have no ranges, to optimize that + if (mRanges.Length() == 0) + return NS_OK; - SelectionDetails *details = nsnull; - SelectionDetails *newDetails = details; + nsCOMPtr node = do_QueryInterface(aContent, &rv); + NS_ENSURE_SUCCESS(rv, rv); - for (i =0; i range = mRangeArray[i]; - if (range){ - nsCOMPtr startNode; - nsCOMPtr endNode; - PRInt32 startOffset; - PRInt32 endOffset; - range->GetStartContainer(getter_AddRefs(startNode)); - range->GetStartOffset(&startOffset); - range->GetEndContainer(getter_AddRefs(endNode)); - range->GetEndOffset(&endOffset); - if (passedInNode == startNode && passedInNode == endNode){ - if (startOffset < (aContentOffset + aContentLength) && - endOffset > aContentOffset){ - if (!details){ - details = new SelectionDetails; + nsCOMArray overlappingRanges; + rv = GetRangesForIntervalCOMArray(node, aContentOffset, + node, aContentOffset + aContentLength, + PR_FALSE, + &overlappingRanges); + NS_ENSURE_SUCCESS(rv, rv); + if (overlappingRanges.Count() == 0) + return NS_OK; - newDetails = details; - } - else{ - newDetails->mNext = new SelectionDetails; - newDetails = newDetails->mNext; - } - if (!newDetails) - return NS_ERROR_OUT_OF_MEMORY; - newDetails->mNext = nsnull; - newDetails->mStart = PR_MAX(0,startOffset - aContentOffset); - newDetails->mEnd = PR_MIN(aContentLength, endOffset - aContentOffset); - newDetails->mType = aType; - } + for (PRInt32 i = 0; i < overlappingRanges.Count(); i ++) { + nsCOMPtr startNode, endNode; + PRInt32 startOffset, endOffset; + nsIDOMRange* range = overlappingRanges[i]; + range->GetStartContainer(getter_AddRefs(startNode)); + range->GetStartOffset(&startOffset); + range->GetEndContainer(getter_AddRefs(endNode)); + range->GetEndOffset(&endOffset); + + PRInt32 start = -1, end = -1; + if (startNode == node && endNode == node) { + if (startOffset < (aContentOffset + aContentLength) && + endOffset > aContentOffset) { + // this range is totally inside the requested content range + start = PR_MAX(0, startOffset - aContentOffset); + end = PR_MIN(aContentLength, endOffset - aContentOffset); } - else if (passedInNode == startNode){ //are we at the beginning? - if (startOffset < (aContentOffset + aContentLength)){ - if (!details){ - details = new SelectionDetails; - newDetails = details; - } - else{ - newDetails->mNext = new SelectionDetails; - newDetails = newDetails->mNext; - } - if (!newDetails) - return NS_ERROR_OUT_OF_MEMORY; - newDetails->mNext = nsnull; - newDetails->mStart = PR_MAX(0,startOffset - aContentOffset); - newDetails->mEnd = aContentLength; - newDetails->mType = aType; - } + // otherwise, range is inside the requested node, but does not intersect + // the requested content range, so ignore it + } else if (startNode == node) { + if (startOffset < (aContentOffset + aContentLength)) { + // the beginning of the range is inside the requested node, but the + // end is outside, select everything from there to the end + start = PR_MAX(0, startOffset - aContentOffset); + end = aContentLength; } - else if (passedInNode == endNode){//are we at the end? - if (endOffset > aContentOffset){ - if (!details){ - details = new SelectionDetails; - newDetails = details; - } - else{ - newDetails->mNext = new SelectionDetails; - newDetails = newDetails->mNext; - } - if (!newDetails) - return NS_ERROR_OUT_OF_MEMORY; - newDetails->mNext = nsnull; - newDetails->mStart = 0; - newDetails->mEnd = PR_MIN(aContentLength, endOffset - aContentOffset); - newDetails->mType = aType; - } + } else if (endNode == node) { + if (endOffset > aContentOffset) { + // the end of the range is inside the requested node, but the beginning + // is outside, select everything from the beginning to there + start = 0; + end = PR_MIN(aContentLength, endOffset - aContentOffset); } - else { //then we MUST be completely selected! unless someone needs us to check to make sure with slowcheck - - if (cnt > 1 || aSlowCheck){ //if more than 1 selection or we need to do - //slow check see if farther than start or - //less than end. - //we only have to look at start offset because anything else would - //have been in the range - PRInt32 resultnum = nsRange::ComparePoints(startNode, - startOffset, - passedInNode, - aContentOffset); - if (resultnum > 0) - continue; - resultnum = nsRange::ComparePoints(endNode, endOffset, - passedInNode, aContentOffset); - if (resultnum <0) - continue; - } - - if (!details){ - details = new SelectionDetails; - newDetails = details; - } - else{ - newDetails->mNext = new SelectionDetails; - newDetails = newDetails->mNext; - } - if (!newDetails) - return NS_ERROR_OUT_OF_MEMORY; - newDetails->mNext = nsnull; - newDetails->mStart = 0; - newDetails->mEnd = aContentLength; - newDetails->mType = aType; - } + } else { + // this range does not begin or end in the requested node, but since + // GetRangesForInterval returned this range, we know it overlaps. + // Therefore, this node is enclosed in the range, and we select all + // of it. + start = 0; + end = aContentLength; } - else - continue; - } - if (*aReturnDetails && newDetails) - newDetails->mNext = *aReturnDetails; - if (details) + if (start < 0) + continue; // the ranges do not overlap the input range + + SelectionDetails* details = new SelectionDetails; + if (! details) + return NS_ERROR_OUT_OF_MEMORY; + + details->mNext = *aReturnDetails; + details->mStart = start; + details->mEnd = end; + details->mType = aType; *aReturnDetails = details; + } return NS_OK; } NS_IMETHODIMP nsTypedSelection::Repaint(nsPresContext* aPresContext) { - PRInt32 arrCount = mRangeArray.Count(); + PRInt32 arrCount = (PRInt32)mRanges.Length(); if (arrCount < 1) return NS_OK; @@ -4734,7 +5197,7 @@ nsTypedSelection::Repaint(nsPresContext* aPresContext) for (i = 0; i < arrCount; i++) { - range = mRangeArray[i]; + range = mRanges[i].mRange; if (!range) return NS_ERROR_UNEXPECTED; @@ -5271,7 +5734,7 @@ nsTypedSelection::RemoveRange(nsIDOMRange* aRange) if (mType != nsISelectionController::SELECTION_SPELLCHECK && aRange == mAnchorFocusRange.get()) { - PRInt32 cnt = mRangeArray.Count(); + PRInt32 cnt = mRanges.Length(); if (cnt > 0) { setAnchorFocusRange(cnt - 1);//reset anchor to LAST range. @@ -5364,7 +5827,7 @@ nsTypedSelection::CollapseToStart() return NS_ERROR_FAILURE; // Get the first range - nsIDOMRange* firstRange = mRangeArray[0]; + nsIDOMRange* firstRange = mRanges[0].mRange; if (!firstRange) return NS_ERROR_FAILURE; @@ -5398,7 +5861,7 @@ nsTypedSelection::CollapseToEnd() return NS_ERROR_FAILURE; // Get the last range - nsIDOMRange* lastRange = mRangeArray[cnt-1]; + nsIDOMRange* lastRange = mRanges[cnt-1].mRange; if (!lastRange) return NS_ERROR_FAILURE; @@ -5428,7 +5891,7 @@ nsTypedSelection::GetIsCollapsed(PRBool* aIsCollapsed) if (!aIsCollapsed) return NS_ERROR_NULL_POINTER; - PRInt32 cnt = mRangeArray.Count();; + PRInt32 cnt = (PRInt32)mRanges.Length();; if (cnt == 0) { *aIsCollapsed = PR_TRUE; @@ -5441,7 +5904,7 @@ nsTypedSelection::GetIsCollapsed(PRBool* aIsCollapsed) return NS_OK; } - return mRangeArray[0]->GetCollapsed(aIsCollapsed); + return mRanges[0].mRange->GetCollapsed(aIsCollapsed); } NS_IMETHODIMP @@ -5450,7 +5913,8 @@ nsTypedSelection::GetRangeCount(PRInt32* aRangeCount) if (!aRangeCount) return NS_ERROR_NULL_POINTER; - *aRangeCount = mRangeArray.Count(); + *aRangeCount = (PRInt32)mRanges.Length(); + NS_ASSERTION(ValidateRanges(), "Ranges out of sync"); return NS_OK; } @@ -5460,12 +5924,13 @@ nsTypedSelection::GetRangeAt(PRInt32 aIndex, nsIDOMRange** aReturn) { if (!aReturn) return NS_ERROR_NULL_POINTER; + NS_ASSERTION(ValidateRanges(), "Ranges out of sync"); - PRInt32 cnt = mRangeArray.Count(); + PRInt32 cnt = (PRInt32)mRanges.Length(); if (aIndex < 0 || aIndex >= cnt) return NS_ERROR_INVALID_ARG; - *aReturn = mRangeArray[aIndex]; + *aReturn = mRanges[aIndex].mRange; NS_IF_ADDREF(*aReturn); return NS_OK; @@ -6179,56 +6644,65 @@ nsTypedSelection::SelectAllChildren(nsIDOMNode* aParentNode) } NS_IMETHODIMP -nsTypedSelection::ContainsNode(nsIDOMNode* aNode, PRBool aRecursive, PRBool* aYes) +nsTypedSelection::ContainsNode(nsIDOMNode* aNode, PRBool aAllowPartial, + PRBool* aYes) { + nsresult rv; if (!aYes) return NS_ERROR_NULL_POINTER; + NS_ASSERTION(ValidateRanges(), "Ranges out of sync"); *aYes = PR_FALSE; - // Iterate over the ranges in the selection checking for the content: - PRInt32 cnt = mRangeArray.Count(); - for (PRInt32 i = 0; i < cnt; ++i) - { - nsIDOMRange* range = mRangeArray[i]; - if (!range) - return NS_ERROR_UNEXPECTED; + if (mRanges.Length() == 0) + return NS_OK; + + PRUint16 nodeType; + aNode->GetNodeType(&nodeType); + PRUint32 nodeLength; + if (nodeType == nsIDOMNode::TEXT_NODE) { + nsAutoString nodeValue; + rv = aNode->GetNodeValue(nodeValue); + NS_ENSURE_SUCCESS(rv, rv); + nodeLength = nodeValue.Length(); + } else { + nsCOMPtr aChildNodes; + rv = aNode->GetChildNodes(getter_AddRefs(aChildNodes)); + NS_ENSURE_SUCCESS(rv, rv); + rv = aChildNodes->GetLength(&nodeLength); + NS_ENSURE_SUCCESS(rv, rv); + } - nsCOMPtr content (do_QueryInterface(aNode)); - if (content) - { - if (nsRange::IsNodeIntersectsRange(content, range)) - { - // If recursive, then we're done -- IsNodeIntersectsRange does the right thing - if (aRecursive) - { - *aYes = PR_TRUE; - return NS_OK; - } + nsCOMArray overlappingRanges; + rv = GetRangesForIntervalCOMArray(aNode, 0, aNode, nodeLength, + PR_FALSE, &overlappingRanges); + NS_ENSURE_SUCCESS(rv, rv); + if (overlappingRanges.Count() == 0) + return NS_OK; // no ranges overlap + + // if the caller said partial intersections are OK, we're done + if (aAllowPartial) { + *aYes = PR_TRUE; + return NS_OK; + } - // else not recursive -- node itself must be contained, - // so we need to do more checking - PRBool nodeStartsBeforeRange, nodeEndsAfterRange; - if (NS_SUCCEEDED(nsRange::CompareNodeToRange(content, range, - &nodeStartsBeforeRange, - &nodeEndsAfterRange))) - { -#ifdef DEBUG_ContainsNode - nsAutoString name, value; - aNode->GetNodeName(name); - aNode->GetNodeValue(value); - printf("%s [%s]: %d, %d\n", NS_LossyConvertUTF16toASCII(name).get(), - NS_LossyConvertUTF16toASCII(value).get(), - nodeStartsBeforeRange, nodeEndsAfterRange); -#endif - PRUint16 nodeType; - aNode->GetNodeType(&nodeType); - if ((!nodeStartsBeforeRange && !nodeEndsAfterRange) - || (nodeType == nsIDOMNode::TEXT_NODE)) - { - *aYes = PR_TRUE; - return NS_OK; - } - } + // text nodes always count as inside + if (nodeType == nsIDOMNode::TEXT_NODE) { + *aYes = PR_TRUE; + return NS_OK; + } + + // The caller wants to know if the node is entirely within the given range, + // so we have to check all intersecting ranges. + nsCOMPtr content (do_QueryInterface(aNode, &rv)); + NS_ENSURE_SUCCESS(rv, rv); + for (PRInt32 i = 0; i < overlappingRanges.Count(); i ++) { + PRBool nodeStartsBeforeRange, nodeEndsAfterRange; + if (NS_SUCCEEDED(nsRange::CompareNodeToRange(content, overlappingRanges[i], + &nodeStartsBeforeRange, + &nodeEndsAfterRange))) { + if (!nodeStartsBeforeRange && !nodeEndsAfterRange) { + *aYes = PR_TRUE; + return NS_OK; } } }