| /******************************************************************************* |
| * 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.link; |
| |
| import java.util.ArrayList; |
| import java.util.HashMap; |
| import java.util.Iterator; |
| import java.util.LinkedList; |
| import java.util.List; |
| import java.util.Map; |
| |
| import org.eclipse.text.edits.MultiTextEdit; |
| import org.eclipse.text.edits.ReplaceEdit; |
| import org.eclipse.text.edits.TextEdit; |
| |
| import org.eclipse.jface.text.Assert; |
| import org.eclipse.jface.text.BadLocationException; |
| import org.eclipse.jface.text.DocumentEvent; |
| import org.eclipse.jface.text.IDocument; |
| import org.eclipse.jface.text.IRegion; |
| import org.eclipse.jface.text.Position; |
| |
| /** |
| * A group of positions in multiple documents that are simultaneously modified - |
| * if one gets edited, all other positions in a <code>PositionGroup</code> |
| * are edited the same way. All linked positions in a group have the same |
| * content. |
| * <p> |
| * Normally, new positions are given a tab stop weight which can be used by |
| * clients, e.g. the UI. If no weight is given, a position will not be visited. |
| * If no weights are used at all, the first position in a document is taken as |
| * the only stop as to comply with the behaviour of the old linked position |
| * infrastructure. |
| * </p> |
| * |
| * @since 3.0 |
| */ |
| public class LinkedPositionGroup { |
| |
| /** Sequence constant declaring that a position should not be stopped by. */ |
| public static final int NO_STOP= -1; |
| |
| /* members */ |
| |
| /** The linked positions of this group. */ |
| private final List fPositions= new LinkedList(); |
| /** Whether we are sealed or not. */ |
| private boolean fIsSealed= false; |
| |
| /* |
| * iteration variables, set to communicate state between isLegalEvent and |
| * handleEvent |
| */ |
| /** The position including the most recent <code>DocumentEvent</code>. */ |
| private LinkedPosition fLastPosition; |
| /** The offset of <code>fLastPosition</code>. */ |
| private int fLastPositionOffset; |
| /** |
| * <code>true</code> if there are custom iteration weights. For backward |
| * compatibility. |
| */ |
| private boolean fHasCustomIteration= false; |
| |
| /** |
| * Adds a position to this group. If the position overlaps with another in |
| * this group, or if the group is already part of an environment, an |
| * exception is thrown. |
| * |
| * @param document the document of the position |
| * @param offset the offset of the position |
| * @param length the length of the position |
| * @throws BadLocationException if the position is invalid or conflicts |
| * with other positions in the group |
| * @throws IllegalStateException if the group has alreay been added to an |
| * environment |
| */ |
| public void createPosition(IDocument document, int offset, int length) throws BadLocationException { |
| createPosition(document, offset, length, LinkedPositionGroup.NO_STOP); |
| } |
| |
| /** |
| * Adds a position to this group. If the position overlaps with another in |
| * this group, or if the group is already part of an environment, an |
| * exception is thrown. |
| * |
| * @param document the document of the position |
| * @param offset the offset of the position |
| * @param length the length of the position |
| * @param sequence the tab stop number of the position |
| * @throws BadLocationException if the position is invalid or conflicts |
| * with other positions in the group |
| * @throws IllegalStateException if the group has alreay been added to an |
| * environment |
| */ |
| public void createPosition(IDocument document, int offset, int length, int sequence) throws BadLocationException { |
| addPosition(new LinkedPosition(document, offset, length, sequence)); |
| } |
| |
| /** |
| * Adds a position to this group. If the position overlaps with another in |
| * this group, or if the group is already part of an environment, an |
| * exception is thrown. |
| * |
| * @param document the document of the position |
| * @param region a region describing the new position (it is not |
| * stored, only its values are used) |
| * @throws BadLocationException if the position is invalid or conflicts |
| * with other positions in the group |
| * @throws IllegalStateException if the group has alreay been added to an |
| * environment |
| */ |
| public void createPosition(IDocument document, IRegion region) throws BadLocationException { |
| createPosition(document, region, LinkedPositionGroup.NO_STOP); |
| } |
| |
| /** |
| * Adds a position to this group. If the position overlaps with another in |
| * this group, or if the group is already part of an environment, an |
| * exception is thrown. |
| * |
| * @param document the document of the position |
| * @param region a region describing the new position (it is not |
| * stored, only its values are used) |
| * @param sequence the tab stop number of the position |
| * @throws BadLocationException if the position is invalid or conflicts |
| * with other positions in the group |
| * @throws IllegalStateException if the group has alreay been added to an |
| * environment |
| */ |
| public void createPosition(IDocument document, IRegion region, int sequence) throws BadLocationException { |
| createPosition(document, region.getOffset(), region.getLength(), sequence); |
| } |
| |
| // /** |
| // * Adds a position to this group. If the position overlaps with another in |
| // * this group, or if the group is already part of an environment, an |
| // * exception is thrown. |
| // * |
| // * @param document the document of the position |
| // * @param region a region describing the new position (it is not |
| // * stored, only its values are used) |
| // * @param proposals the completion proposals to be shown when a position of |
| // * this type comes up |
| // * @throws BadLocationException if the position is invalid or conflicts |
| // * with other positions in the group |
| // * @throws IllegalStateException if the group has alreay been added to an |
| // * environment |
| // */ |
| // public void createPosition(IDocument document, IRegion region, ICompletionProposal[] proposals) throws BadLocationException { |
| // createPosition(document, region, LinkedPositionGroup.NO_STOP, proposals); |
| // } |
| // |
| // /** |
| // * Adds a position to this group. If the position overlaps with another in |
| // * this group, or if the group is already part of an environment, an |
| // * exception is thrown. |
| // * |
| // * @param document the document of the position |
| // * @param offset the offset of the position |
| // * @param length the length of the position |
| // * @param proposals the completion proposals to be shown when a position of |
| // * this type comes up |
| // * @param sequence the tab stop number of the position |
| // * @throws BadLocationException if the position is invalid or conflicts |
| // * with other positions in the group |
| // * @throws IllegalStateException if the group has alreay been added to an |
| // * environment |
| // */ |
| // public void createPosition(IDocument document, int offset, int length, int sequence, ICompletionProposal[] proposals) throws BadLocationException { |
| // addPosition(new ProposalPosition(document, offset, length, sequence, proposals)); |
| // } |
| // |
| // /** |
| // * Adds a position to this group. If the position overlaps with another in |
| // * this group, or if the group is already part of an environment, an |
| // * exception is thrown. |
| // * |
| // * @param document the document of the position |
| // * @param offset the offset of the position |
| // * @param length the length of the position |
| // * @param proposals the completion proposals to be shown when a position of |
| // * this type comes up |
| // * @throws BadLocationException if the position is invalid or conflicts |
| // * with other positions in the group |
| // * @throws IllegalStateException if the group has alreay been added to an |
| // * environment |
| // */ |
| // public void createPosition(IDocument document, int offset, int length, ICompletionProposal[] proposals) throws BadLocationException { |
| // createPosition(document, offset, length, LinkedPositionGroup.NO_STOP, proposals); |
| // } |
| // |
| // /** |
| // * Adds a position to this group. If the position overlaps with another in |
| // * this group, or if the group is already part of an environment, an |
| // * exception is thrown. |
| // * |
| // * @param document the document of the position |
| // * @param region a region describing the new position (it is not |
| // * stored, only its values are used) |
| // * @param proposals the completion proposals to be shown when a position of |
| // * this type comes up |
| // * @param sequence the tab stop number of the position |
| // * @throws BadLocationException if the position is invalid or conflicts |
| // * with other positions in the group |
| // * @throws IllegalStateException if the group has alreay been added to an |
| // * environment |
| // */ |
| // public void createPosition(IDocument document, IRegion region, int sequence, ICompletionProposal[] proposals) throws BadLocationException { |
| // createPosition(document, region.getOffset(), region.getLength(), sequence, proposals); |
| // } |
| // |
| // /** |
| // * Adds a position to this group. If the position overlaps with another in |
| // * this group, or if the group is already part of an environment, an |
| // * exception is thrown. |
| // * <p> |
| // * This method is not implemented yet and will throw an <code>UnsupportedOperationException</code>. |
| // * |
| // * @param document the document of the position |
| // * @param region a region describing the new position (it is not stored, |
| // * only its values are used) |
| // * @param sequence the tab stop number of the position |
| // * @param processor the content assist processor that will be called to |
| // * compute proposals for this position. |
| // * @throws BadLocationException if the position is invalid or conflicts |
| // * with other positions in the group |
| // * @throws IllegalStateException if the group has alreay been added to an |
| // * environment |
| // */ |
| // public void createPosition(IDocument document, IRegion region, int sequence, IContentAssistProcessor processor) throws BadLocationException { |
| // // TODO implement |
| // throw new UnsupportedOperationException(); |
| // } |
| // |
| /** |
| * Implementation of all the <code>createPosition</code> methods. Enforces |
| * constraints and sets the custom iteration flag. If the position is |
| * already in this group, nothing happens. |
| * <p> |
| * Positions added using this method are owned by this group afterwards any may |
| * not be updated or modified thereafter. |
| * </p> |
| * |
| * @param position the position to add |
| * @throws BadLocationException if the position is invalid or conflicts |
| * with other positions in the group |
| * @throws IllegalStateException if the group has alreay been added to an |
| * environment |
| */ |
| public void addPosition(LinkedPosition position) throws BadLocationException { |
| Assert.isNotNull(position); |
| // don't add positions after it is installed. |
| if (fIsSealed) |
| throw new IllegalStateException("cannot add positions after the group is added to an environment"); //$NON-NLS-1$ |
| |
| if (!fPositions.contains(position)) { |
| enforceDisjoint(position); |
| enforceEqualContent(position); |
| fPositions.add(position); |
| fHasCustomIteration |= position.getSequenceNumber() != LinkedPositionGroup.NO_STOP; |
| } else |
| return; // nothing happens |
| } |
| |
| /** |
| * Enforces the invariant that all positions must contain the same string. |
| * |
| * @param position the position to check |
| * @throws BadLocationException if the equal content check fails |
| */ |
| private void enforceEqualContent(LinkedPosition position) throws BadLocationException { |
| if (fPositions.size() > 0) { |
| String groupContent= ((LinkedPosition) fPositions.get(0)).getContent(); |
| String positionContent= position.getContent(); |
| if (!groupContent.equals(positionContent)) |
| throw new BadLocationException(); |
| } |
| } |
| |
| /** |
| * Enforces the invariant that all positions must be disjoint. |
| * |
| * @param position the position to check |
| * @throws BadLocationException if the disjointness check fails |
| */ |
| private void enforceDisjoint(LinkedPosition position) throws BadLocationException { |
| for (Iterator it= fPositions.iterator(); it.hasNext(); ) { |
| LinkedPosition p= (LinkedPosition) it.next(); |
| if (p.overlapsWith(position)) |
| throw new BadLocationException(); |
| } |
| } |
| |
| /** |
| * Enforces the disjointness for another group |
| * |
| * @param group the group to check |
| * @throws BadLocationException if the disjointness check fails |
| */ |
| void enforceDisjoint(LinkedPositionGroup group) throws BadLocationException { |
| Assert.isNotNull(group); |
| for (Iterator it= group.fPositions.iterator(); it.hasNext(); ) { |
| LinkedPosition p= (LinkedPosition) it.next(); |
| enforceDisjoint(p); |
| } |
| } |
| |
| /** |
| * Checks whether <code>event</code> fits in any of the positions of this |
| * group. |
| * |
| * @param event the document event to check |
| * @return <code>true</code> if <code>event</code> fits in any position |
| */ |
| boolean isLegalEvent(DocumentEvent event) { |
| for (Iterator it= fPositions.iterator(); it.hasNext(); ) { |
| LinkedPosition pos= (LinkedPosition) it.next(); |
| if (pos.includes(event)) { |
| fLastPosition= pos; |
| fLastPositionOffset= pos.getOffset(); |
| return true; |
| } |
| } |
| fLastPosition= null; |
| fLastPositionOffset= -1; |
| return false; |
| } |
| |
| /** |
| * Creates an edition of a document change that will forward any |
| * modification in one position to all linked siblings. The return value is |
| * a map from <code>IDocument</code> to <code>TextEdit</code>. |
| * |
| * @param event the document event to check |
| * @return a map of edits, grouped by edited document |
| */ |
| Map handleEvent(DocumentEvent event) { |
| |
| if (fLastPosition != null) { |
| |
| Map map= new HashMap(); |
| |
| int relOffset= event.getOffset() - fLastPositionOffset; |
| int length= event.getLength(); |
| String text= event.getText(); |
| |
| for (Iterator it2= fPositions.iterator(); it2.hasNext(); ) { |
| LinkedPosition p= (LinkedPosition) it2.next(); |
| if (p == fLastPosition) |
| continue; // don't re-update the origin of the change |
| |
| List edits= (List) map.get(p.getDocument()); |
| if (edits == null) { |
| edits= new ArrayList(); |
| map.put(p.getDocument(), edits); |
| } |
| |
| edits.add(new ReplaceEdit(p.getOffset() + relOffset, length, text)); |
| } |
| |
| for (Iterator it2= map.keySet().iterator(); it2.hasNext(); ) { |
| IDocument d= (IDocument) it2.next(); |
| TextEdit edit= new MultiTextEdit(0, d.getLength()); |
| edit.addChildren((TextEdit[]) ((List) map.get(d)).toArray(new TextEdit[0])); |
| map.put(d, edit); |
| } |
| |
| return map; |
| |
| } |
| |
| return null; |
| } |
| |
| /** |
| * Sets the environment of this group. Once an environment has been set, no |
| * more positions can be added and the environment cannot be changed. |
| * |
| * @param environment the environment |
| */ |
| void seal() { |
| Assert.isTrue(!fIsSealed); |
| fIsSealed= true; |
| |
| if (fHasCustomIteration == false && fPositions.size() > 0) { |
| ((LinkedPosition) fPositions.get(0)).setSequenceNumber(0); |
| } |
| } |
| |
| IDocument[] getDocuments() { |
| IDocument[] docs= new IDocument[fPositions.size()]; |
| int i= 0; |
| for (Iterator it= fPositions.iterator(); it.hasNext(); i++) { |
| LinkedPosition pos= (LinkedPosition) it.next(); |
| docs[i]= pos.getDocument(); |
| } |
| return docs; |
| } |
| |
| void register(LinkedEnvironment environment) throws BadLocationException { |
| for (Iterator it= fPositions.iterator(); it.hasNext(); ) { |
| LinkedPosition pos= (LinkedPosition) it.next(); |
| environment.register(pos); |
| } |
| } |
| |
| /** |
| * Returns the position in this group that encompasses all positions in |
| * <code>group</code>. |
| * |
| * @param group the group to be adopted |
| * @return a position in the receiver that contains all positions in <code>group</code>, |
| * or <code>null</code> if none can be found |
| * @throws BadLocationException if more than one position are affected by |
| * <code>group</code> |
| */ |
| LinkedPosition adopt(LinkedPositionGroup group) throws BadLocationException { |
| LinkedPosition found= null; |
| for (Iterator it= group.fPositions.iterator(); it.hasNext(); ) { |
| LinkedPosition pos= (LinkedPosition) it.next(); |
| LinkedPosition localFound= null; |
| for (Iterator it2= fPositions.iterator(); it2.hasNext(); ) { |
| LinkedPosition myPos= (LinkedPosition) it2.next(); |
| if (myPos.includes(pos)) { |
| if (found == null) |
| found= myPos; |
| else if (found != myPos) |
| throw new BadLocationException(); |
| if (localFound == null) |
| localFound= myPos; |
| } |
| } |
| |
| if (localFound != found) |
| throw new BadLocationException(); |
| } |
| return found; |
| } |
| |
| /** |
| * Finds the closest position to <code>toFind</code>. |
| */ |
| LinkedPosition getPosition(LinkedPosition toFind) { |
| for (Iterator it= fPositions.iterator(); it.hasNext(); ) { |
| LinkedPosition p= (LinkedPosition) it.next(); |
| if (p.includes(toFind)) |
| return p; |
| } |
| return null; |
| } |
| |
| /** |
| * Returns <code>true</code> if <code>offset</code> is contained in any |
| * position in this group. |
| * |
| * @param offset the offset to check |
| * @return <code>true</code> if offset is contained by this group |
| */ |
| boolean contains(int offset) { |
| for (Iterator it= fPositions.iterator(); it.hasNext(); ) { |
| LinkedPosition pos= (LinkedPosition) it.next(); |
| if (pos.includes(offset)) { |
| return true; |
| } |
| } |
| return false; |
| } |
| |
| /** |
| * Returns whether this group contains any positions. |
| * |
| * @return <code>true</code> if this group is empty, <code>false</code> if it is not |
| */ |
| public boolean isEmtpy() { |
| return fPositions.size() == 0; |
| } |
| |
| /** |
| * Returns the positions contained in the receiver as an array. The |
| * positions are the actual positions and must not be modified; the array |
| * is a copy of internal structures. |
| * |
| * @return the positions of this group in no particular order |
| */ |
| public LinkedPosition[] getPositions() { |
| return (LinkedPosition[]) fPositions.toArray(new LinkedPosition[0]); |
| } |
| |
| /** |
| * Returns <code>true</code> if the receiver contains <code>position</code>. |
| * |
| * @param position the position to check |
| * @return <code>true</code> if the receiver contains <code>position</code> |
| */ |
| boolean contains(Position position) { |
| for (Iterator it= fPositions.iterator(); it.hasNext(); ) { |
| LinkedPosition p= (LinkedPosition) it.next(); |
| if (position.equals(p)) |
| return true; |
| } |
| return false; |
| } |
| } |