blob: 5cd17f7e3d73790ed2e80ed419ca6b947a22cdd6 [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2000, 2003 IBM Corporation and others.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Common Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/cpl-v10.html
*
* Contributors:
* IBM Corporation - initial API and implementation
*******************************************************************************/
package org.eclipse.jface.text;
import java.util.Arrays;
import java.util.Comparator;
/**
* A <code>ProjectionDocument</code> represents a projection of its master document.
* The contents of a projection document is a sequence of fragments of the master document, i.e.
* the projection document can be thought as being constructed from the master document by
* not copying the whole master document by omitting serveral ranges of the master document. <p>
* The projection document utilizes its master document as <code>ITextStore</code>.<p>
* This class if for internal use only.
*
* @since 2.1
*/
public final class ProjectionDocument extends AbstractDocument {
/** The position category used by <code>ProjectionDocument</code>s to manage the fragments they consist of. */
final public static String FRAGMENT_CATEGORY= "__fragment_category"; //$NON-NLS-1$
/** The parent document */
private IDocument fParentDocument;
/** The parent document as document extension */
private IDocumentExtension fExtension;
/** The position category defining the projection */
private String fProjectionCategory;
/** The document event issued by the parent document */
private DocumentEvent fParentEvent;
/** The document event issued and to be issued by the projection document */
private SlaveDocumentEvent fEvent;
/** Indicates whether the projection document initiated a parent document update or not */
private boolean fIsUpdating= false;
/** The position updater for the positions managing the fragments */
private FragmentUpdater fFragmentUpdater= new FragmentUpdater(FRAGMENT_CATEGORY);
/**
* Creates a projection document for the given parent document.
*
* @param parentDocument the parent Document
* @param projectionCategory the document position category whose positions define the projection of the parent document
*/
public ProjectionDocument(IDocument parentDocument, String projectionCategory) {
super();
fParentDocument= parentDocument;
if (fParentDocument instanceof IDocumentExtension)
fExtension= (IDocumentExtension) fParentDocument;
ITextStore s= new ProjectionTextStore(this);
ILineTracker tracker= new DefaultLineTracker();
setTextStore(s);
setLineTracker(tracker);
completeInitialization();
initializeProjection(projectionCategory);
tracker.set(s.get(0, s.getLength()));
}
/**
* Initializes the projection document from the parent document based on the given projection category.
*
* @param projectionCategory the document position category whose positions define the projection of the parent document
*/
private void initializeProjection(String projectionCategory) {
fProjectionCategory= projectionCategory;
try {
addPositionCategory(FRAGMENT_CATEGORY);
addPositionUpdater(fFragmentUpdater);
int offset= 0;
Position[] patch= fParentDocument.getPositions(fProjectionCategory);
for (int i= 0; i < patch.length; i++) {
Position p= patch[i];
addPosition(FRAGMENT_CATEGORY, new Fragment(offset, p.length, p));
offset += p.length;
}
} catch (BadPositionCategoryException x) {
} catch (BadLocationException x) {
}
}
/**
* Creates a fragment from a postion of the parent document.
*
* @param parentPosition a position of the parent document
* @return the fragment representing the range given by the parent position
*/
public Fragment createFragment(Position parentPosition) {
try {
int index= fParentDocument.computeIndexInCategory(fProjectionCategory, parentPosition.offset);
if (index <= 0)
return new Fragment(0, parentPosition.length, parentPosition);
Position[] fragments= getPositions(FRAGMENT_CATEGORY);
Position p= fragments[index -1];
return new Fragment(p.offset + p.length, parentPosition.length, parentPosition);
} catch (BadPositionCategoryException e) {
} catch (BadLocationException e) {
}
return null;
}
/**
* Returns the index of the position of the given category of the given document that includes the
* given offset. <code>direction</code> indicates the direction into which the algorithm should search.
*
* @param document the document
* @param category the position category of <code>document</code>
* @param offset the offset into <code>document</code>
* @param direction the search direction
* @return the index of the position
* @throws BadPositionCategoryException if <code>category</code> is not valid in <code>document</code>
* @throws BadLocationException if <code>offset</code> is not valid in <code>document</code>
*/
private int getPositionOfOffset(IDocument document, String category, int offset, int direction ) throws BadPositionCategoryException, BadLocationException{
Position[] positions= document.getPositions(category);
if (positions != null && positions.length > 0) {
// test for inclusion
int index= document.computeIndexInCategory(category, offset);
if (index < positions.length && positions[index].includes(offset))
return index;
if (index > 0 && positions[index -1].includes(offset))
return index -1;
// find next accorrding to direction
if (direction != 0) {
if (direction > 0) {
if (index < positions.length && positions[index].overlapsWith(offset, direction))
return index;
} else {
if (index > 0 && positions[index -1].overlapsWith(offset + direction, -direction))
return index -1;
}
}
}
return -1;
}
/**
* Returns the position which is used to manage a parent
* document range represented in this projection document and that
* includes or is close to the given parent document offset. The distance
* is computed based on the given direction hint.
*
* @param offsetInParent the parent document offset
* @param direction the direction hint used for computing the distance
* @return position the parent document position including or near to the parent document offset
*/
private Position getParentDocumentPositionOfOffset(int offsetInParent, int direction ) {
try {
int index= getPositionOfOffset(fParentDocument, fProjectionCategory, offsetInParent, direction);
if (index > -1) {
Position[] positions= fParentDocument.getPositions(fProjectionCategory);
return positions[index];
}
} catch (BadPositionCategoryException x) {
} catch (BadLocationException x) {
}
return null;
}
/**
* Returns the offset in the projection document corresponding to the
* given parent document offset.
*
* @param offsetInParent the parent document offset
* @return the projection document offset corresponding to the given parent document offset
*/
private int toProjectionDocumentOffset(int offsetInParent, int direction) {
Position p= getParentDocumentPositionOfOffset(offsetInParent, direction);
if (p == null)
return -1;
int relative= offsetInParent - p.offset;
if (direction > 0) {
if (relative < 0)
relative= 0;
} else if (direction < 0) {
if (relative >= p.length)
relative= p.length -1;
}
Fragment f= findCorrespondingFragment(p);
return f.offset + relative;
}
/**
* Creates a position describing the projection document range corresponding to
* the given parent document range.
*
* @param offsetInParent the parent document offset
* @param lengthInParent the parent document lengh
* @return position describing the projection document range corresponding to the given parent document range
*/
public Position computeProjectionDocumentPosition(int offsetInParent, int lengthInParent) {
Position p= getParentDocumentCoverage();
if (p != null) {
if (p.overlapsWith(offsetInParent, lengthInParent)) {
int o1= toProjectionDocumentOffset(offsetInParent, lengthInParent);
if (o1 == -1)
return null;
if (lengthInParent == 0)
return new Position(o1, 0);
int o2= toProjectionDocumentOffset(offsetInParent + lengthInParent -1, 1 - lengthInParent);
if (o2 == -1)
return null;
return new Position(o1, o2 - o1 + 1);
} else if (p.getOffset() + p.getLength() == offsetInParent + lengthInParent) {
Position[] fragments= getFragmentation();
if (fragments != null && fragments.length > 0) {
Position last= fragments[fragments.length -1];
return new Position(last.getOffset() + last.getLength());
}
}
}
return null;
}
/**
* Returns the offset in the parent document that corresponds to the given offset in this
* projection document.
*
* @param offset the offset in the projection document
* @return the corresponding parent document offset
* @throws BadLocationException if <code>offset</code> is not valid in this projection document
*/
public int toParentDocumentOffset(int offset) throws BadLocationException {
Fragment fragment= getFragmentOfOffset(offset);
if (fragment == null) {
// if (offset == 0)
// return 0;
// throw new BadLocationException();
Position[] fragmentation= getFragmentation();
if (fragmentation != null && fragmentation.length > 0) {
Fragment last= (Fragment) fragmentation[fragmentation.length -1];
if (last.offset + last.length == offset) {
Position origin= last.getOrigin();
return origin.offset + origin.length;
}
}
throw new BadLocationException();
}
int relative= offset - fragment.offset;
return fragment.getOrigin().offset + relative;
}
/**
* Computes and returns the region of the parent document that corresponds to the given region of the
* projection document.
*
* @param offset the offset of the projection document region
* @param length the length of the projection document region
* @return the corresponding region of the parent document
* @throws BadLocationException if the given projection document region is not valid
*/
public IRegion computeParentDocumentRegion(int offset, int length) throws BadLocationException {
if (length == 0) {
if (offset == 0 && length == getLength())
return new Region(0, fParentDocument.getLength());
return new Region(toParentDocumentOffset(offset), 0);
}
int o1= toParentDocumentOffset(offset);
int o2= toParentDocumentOffset(offset + length -1);
return new Region(o1, o2 - o1 + 1);
}
/**
* Removes all fragments and thereby clears this projection document.
*/
public void removeAllFragments() {
Position[] projection= getProjection();
if (projection == null)
return;
for (int i= 0; i < projection.length; i++) {
try {
removeFragment(projection[i]);
} catch (BadLocationException e) {
}
}
}
/**
* Add a new fragment of the parent document to this projection document.
*
* @param offsetInParent offset of the parent document range
* @param lengthInParent length of the parent document range
* @return returns the position representing the parent document range in this projection document
* @throws BadLocationException
*/
public void addFragment(int offsetInParent, int lengthInParent) throws BadLocationException {
if (lengthInParent == 0)
return;
try {
ProjectionPosition p= new ProjectionPosition(this, offsetInParent, lengthInParent);
fParentDocument.addPosition(fProjectionCategory, p);
Fragment fragment= createFragment(p);
p.setFragment(fragment);
fireDocumentProjectionChanged(new DocumentEvent(this, fragment.offset, 0, fParentDocument.get(offsetInParent, lengthInParent)));
addPosition(FRAGMENT_CATEGORY, fragment);
getTracker().set(getStore().get(0, getStore().getLength()));
} catch (BadPositionCategoryException x) {
}
}
/**
* Joins all fragments that represent neighboring regions in the parent document.
*/
public void joinFragments() {
try {
while (joinTwoFragments()) {}
} catch (BadPositionCategoryException x) {
}
}
/**
* Joins the first two fragments that represent neighboring regions of the parent document.
* @return <code>true</code> if two segments have been joined, <code>false</code> otherwise
* @throws BadPositionCategoryException
*/
private boolean joinTwoFragments() throws BadPositionCategoryException {
Position[] projection= getProjection();
if (projection != null && projection.length > 0) {
Position previous= projection[0];
for (int i= 1; i < projection.length; i++) {
Position current= projection[i];
if (previous.offset + previous.length == current.offset) {
join(previous, current);
return true;
}
previous= current;
}
}
return false;
}
/**
* Joins the fragments of this projection document that correspond to the two given,
* neighboring ranges of the parent document.
*
* @param p1 lower range in the parent document
* @param p2 higher range of the parent document
* @throws BadPositionCategoryException if the fragment position category is not defined in this projection document
*/
private void join(Position p1, Position p2) throws BadPositionCategoryException {
// remove p2
Fragment fragment= findCorrespondingFragment(p2);
removePosition(FRAGMENT_CATEGORY, fragment);
fParentDocument.removePosition(fProjectionCategory, p2);
// extend p1 by length of p2
fragment= findCorrespondingFragment(p1);
fragment.length += p2.length;
p1.length += p2.length;
}
/**
* Removes the fragment that corresponds to the given parent document range.
*
* @param parentPosition the position representing the parent document range
* @throws BadLocationException if the fragment position category is not defined in this projection document
*/
public void removeFragment(Position parentPosition) throws BadLocationException {
try {
Fragment fragment= findCorrespondingFragment(parentPosition);
if (fragment != null) {
removePosition(FRAGMENT_CATEGORY, fragment);
fParentDocument.removePosition(fProjectionCategory, parentPosition);
fireDocumentProjectionChanged(new DocumentEvent(this, fragment.offset, fragment.length, null));
getTracker().set(getStore().get(0, getStore().getLength()));
}
} catch (BadPositionCategoryException x) {
}
}
/**
* Returns the list of fragments whose corresponding ranges in the parent document overlap with
* the specifed range of the parent document.
*
* @param offsetInParent the offset of the parent document range
* @param lengthInParent the length of the parent document range
* @return the list of affected fragments
*/
public Position[] getAffectedFragments(int offsetInParent, int lengthInParent) {
Position p= computeProjectionDocumentPosition(offsetInParent, lengthInParent);
if (p == null)
return null;
Fragment[] f= getFragmentsOfRange(p.offset, p.length);
if (f == null)
return null;
Position[] result= new Position[f.length];
for (int i= 0; i < f.length; i++)
result[i]= f[i].getOrigin();
return result;
}
/**
* Finds the fragment that represents the given parent document range in this projection document.
*
* @param parentPosition the parent document range
* @return the fragment representing the given parent document range
*/
private Fragment findCorrespondingFragment(Position parentPosition) {
try {
Position[] fragments= getPositions(FRAGMENT_CATEGORY);
for (int i= 0; i < fragments.length; i++) {
Fragment f= (Fragment) fragments[i];
if (parentPosition.equals(f.getOrigin()))
return f;
}
} catch (BadPositionCategoryException x) {
}
return null;
}
/**
* Returns the fragment that contains the given offset.
*
* @param offset the offset
* @return the fragment that contains the given offset
* @throws BadLocationException if <code>offset</code> is not a valid offset
*/
protected Fragment getFragmentOfOffset(int offset) throws BadLocationException {
try {
int index= getPositionOfOffset(this, FRAGMENT_CATEGORY, offset, 0);
if (index > -1) {
Position[] fragments= getPositions(FRAGMENT_CATEGORY);
return (Fragment) fragments[index];
}
} catch (BadPositionCategoryException x) {
}
return null;
}
/**
* Returns the minimal consecutive list of fragments that completely covers the given range.
*
* @param offset the offset of the range
* @param length the length of the range
* @return the minimal consecutive list of fragments convering the given range
*/
protected Fragment[] getFragmentsOfRange(int offset, int length) {
try {
int start= getPositionOfOffset(this, FRAGMENT_CATEGORY, offset, length);
int end= getPositionOfOffset(this, FRAGMENT_CATEGORY, offset + length -1, 1 - length);
if (start > -1 && end > -1) {
Position[] positions= getPositions(FRAGMENT_CATEGORY);
if (start == end)
return new Fragment[] { (Fragment) positions[start] };
Fragment[] result= new Fragment[end - start + 1];
for (int i= start; i <= end; i++)
result[i - start]= (Fragment) positions[i];
sortFragments(result);
return result;
}
} catch (BadPositionCategoryException e) {
} catch (BadLocationException e) {
}
return new Fragment[0];
}
/**
* Sorts a list of fragments based on the offsets of their corresponding ranges in the parent document.
*
* @param result the list for fragments
*/
private void sortFragments(Object[] result) {
Comparator comparator= new Comparator() {
public int compare(Object o1, Object o2) {
Fragment f1= (Fragment) o1;
Fragment f2= (Fragment) o2;
return f1.getOrigin().getOffset() - f2.getOrigin().getOffset();
}
public boolean equals(Object obj) {
return false;
}
};
Arrays.sort(result, comparator);
}
/**
* Returns the minimal range of the parent document that covers all ranges that
* correspond to the fragments of this projection document.
*
* @return a position describing the minimal parent document range covering all fragments
*/
public Position getParentDocumentCoverage() {
Position[] projection= getProjection();
if (projection != null && projection.length > 0) {
Position first=projection[0];
Position last= projection[projection.length -1];
return new Position(first.offset, last.offset - first.offset + last.length);
}
return new Position(0, 0);
}
/**
* The projection of the parent document has been changed by inserting or removing
* new fragments into this projection document. The projection change is described in
* the given <code>DocumentEvent</code>. All positions managed by this projection
* document must be adapted accordingly.
*
* @param event the document event
*/
private void fireDocumentProjectionChanged(DocumentEvent event) {
fFragmentUpdater.enableShiftMode(true);
try {
updatePositions(event);
} finally {
fFragmentUpdater.enableShiftMode(false);
}
}
/**
* Returns parent document.
*
* @return the parent document
*/
public IDocument getParentDocument() {
return fParentDocument;
}
/**
* Returns the ranges of the parent document that correspond to the fragments of this
* projection document.
*
* @return the ranges of the parent document corresponding to the fragments
*/
public Position[] getProjection() {
try {
return fParentDocument.getPositions(fProjectionCategory);
} catch (BadPositionCategoryException x) {
}
return null;
}
/**
* Returns the list of all fragments of this projection document.
*
* @return the list of all fragments of this projection document
*/
public Position[] getFragmentation() {
try {
Position[] fragmentation= getPositions(FRAGMENT_CATEGORY);
sortFragments(fragmentation);
return fragmentation;
} catch (BadPositionCategoryException x) {
}
return null;
}
/**
* Transforms a document event of the parent document into a projection document
* based document event.
*
* @param e the parent document event
* @return the slave document event
*/
private SlaveDocumentEvent normalize(DocumentEvent e) {
Position c= computeProjectionDocumentPosition(e.getOffset(), e.getLength());
if (c != null) {
if (c.length == 0) {
int insertLength= e.getText() == null ? 0 : e.getText().length();
if (insertLength == 0)
return null;
}
return new SlaveDocumentEvent(this, c.offset, c.length, e.getText(), e);
}
return null;
}
/**
* When called, this projection document is informed about a forthcoming change
* of its parent document. This projection document checks whether the parent
* document change affects it and if so informs all document listeners.
*
* @param event the parent document event
*/
public void parentDocumentAboutToBeChanged(DocumentEvent event) {
fParentEvent= event;
fEvent= normalize(event);
if (fEvent != null)
delayedFireDocumentAboutToBeChanged();
}
/**
* When called, this projection document is informed about a change of its parent document.
* If this projection document is affected it informs all of its document listeners.
*
* @param event the parent document event
*/
public void parentDocumentChanged(DocumentEvent event) {
if ( !fIsUpdating && event == fParentEvent && fEvent != null) {
try {
getTracker().replace(fEvent.getOffset(), fEvent.getLength(), fEvent.getText());
fireDocumentChanged(fEvent);
} catch (BadLocationException x) {
Assert.isLegal(false);
}
}
}
/*
* @see AbstractDocument#fireDocumentAboutToBeChanged(DocumentEvent)
*/
protected void fireDocumentAboutToBeChanged(DocumentEvent event) {
// delay it until there is a notification from the parent document
// otherwise there it is expensive to construct the parent document information
}
/**
* Fires the slave document event as about-to-be-changed event to all registed listeners.
*/
private void delayedFireDocumentAboutToBeChanged() {
super.fireDocumentAboutToBeChanged(fEvent);
}
/**
* Ignores the given event and sends the semantically equal slave document event instead.
*
* @param event the event to be ignored
*/
protected void fireDocumentChanged(DocumentEvent event) {
super.fireDocumentChanged(fEvent);
}
/*
* @see IDocument#replace(int, int, String)
*/
public void replace(int offset, int length, String text) throws BadLocationException {
try {
fIsUpdating= true;
if (fExtension != null)
fExtension.stopPostNotificationProcessing();
super.replace(offset, length, text);
} finally {
fIsUpdating= false;
if (fExtension != null)
fExtension.resumePostNotificationProcessing();
}
}
/*
* @see IDocument#set(String)
*/
public void set(String text) {
try {
fIsUpdating= true;
if (fExtension != null)
fExtension.stopPostNotificationProcessing();
super.set(text);
} finally {
fIsUpdating= false;
if (fExtension != null)
fExtension.resumePostNotificationProcessing();
}
}
/*
* @see IDocumentExtension#registerPostNotificationReplace(IDocumentListener, IDocumentExtension.IReplace)
*/
public void registerPostNotificationReplace(IDocumentListener owner, IDocumentExtension.IReplace replace) {
if (!fIsUpdating)
throw new UnsupportedOperationException();
super.registerPostNotificationReplace(owner, replace);
}
/**
* Convenience method for removing and adapting the fragments whose corresponding
* ranges in the parent document are included or overlap with the given range of the
* parent document.
*
* @param offsetInParent the offset of the parent document range
* @param lengthInParent the length of the parent document range
*/
public void hide(int offsetInParent, int lengthInParent) {
IDocument parent= getParentDocument();
Position[] effected= getAffectedFragments(offsetInParent, lengthInParent);
try {
if (effected == null) {
// populate new document with two new fragments, the left and the right of the hidden region
int end= offsetInParent + lengthInParent;
addFragment(0, offsetInParent);
addFragment(end, parent.getLength() - end);
} else if (effected.length == 1) {
// the only affected fragment must be splitted into two
Position fragment= effected[0];
removeFragment(fragment);
addFragment(fragment.offset, offsetInParent - fragment.offset);
int secondOffset= offsetInParent + lengthInParent;
addFragment(secondOffset, fragment.offset + fragment.length - secondOffset);
} else {
// first expand and than collapse
internalShow(offsetInParent, lengthInParent, effected);
hide(offsetInParent, lengthInParent);
}
joinFragments();
} catch (BadLocationException x) {
}
}
/**
* Convenience method for adding fragments or adapting existing fragments so that their corresponding
* ranges in the parent document include the given range of the parent document.
*
* @param offsetInParent the offset of the parent document range
* @param lengthInParent the length of the parent document range
*/
public void show(int offsetInParent, int lengthInParent) {
Position[] effected= getAffectedFragments(offsetInParent, lengthInParent);
if (effected == null || effected.length == 0) {
try {
addFragment(offsetInParent, lengthInParent);
joinFragments();
} catch (BadLocationException x) {
}
return;
}
internalShow(offsetInParent, lengthInParent, effected);
joinFragments();
}
/**
* Removes the given fragments and inserts a new fragment whose parent document
* range corresponds the given range of the parent document.
*
* @param offsetInParent the offset of the parent document range
* @param lengthInParent the length of the parent document range
* @param effected the list for fragments to be removed
*/
private void internalShow(int offsetInParent, int lengthInParent, Position[] effected) {
try {
int size= effected.length;
for (int i= 0; i < size; i++)
removeFragment(effected[i]);
int offset= Math.min(offsetInParent, effected[0].offset);
int end= Math.max(offsetInParent + lengthInParent, effected[size -1].offset + effected[size -1].length);
addFragment(offset, end - offset);
} catch (BadLocationException x) {
}
}
}