| /******************************************************************************* |
| * 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.Arrays; |
| import java.util.HashSet; |
| import java.util.Iterator; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.Set; |
| |
| import org.eclipse.text.edits.MalformedTreeException; |
| import org.eclipse.text.edits.TextEdit; |
| |
| import org.eclipse.jface.text.Assert; |
| import org.eclipse.jface.text.BadLocationException; |
| import org.eclipse.jface.text.BadPositionCategoryException; |
| import org.eclipse.jface.text.DocumentEvent; |
| import org.eclipse.jface.text.IDocument; |
| import org.eclipse.jface.text.IDocumentExtension; |
| import org.eclipse.jface.text.IDocumentListener; |
| import org.eclipse.jface.text.IPositionUpdater; |
| import org.eclipse.jface.text.Position; |
| import org.eclipse.jface.text.IDocumentExtension.IReplace; |
| |
| /** |
| * A <code> LinkedEnvironment</code> umbrellas several <code>LinkedPositionGroup</code>s. |
| * Responsible for updating the siblings of a linked position when a change |
| * occurs. |
| * |
| * @since 3.0 |
| */ |
| public class LinkedEnvironment { |
| |
| /** |
| * Checks whether there is alreay a linked environment installed on <code>document</code>. |
| * |
| * @param document the <code>IDocument</code> of interest |
| * @return <code>true</code> if there is an existing environment, <code>false</code> |
| * otherwise |
| */ |
| public static boolean hasEnvironment(IDocument document) { |
| // if there is a manager, there also is an enviroment |
| return LinkedManager.hasManager(document); |
| } |
| |
| /** |
| * Checks whether there is alreay a linked environment installed on any of |
| * the <code>documents</code>. |
| * |
| * @param documents the <code>IDocument</code> s of interest |
| * @return <code>true</code> if there is an existing environment, <code>false</code> |
| * otherwise |
| */ |
| public static boolean hasEnvironment(IDocument[] documents) { |
| // if there is a manager, there also is an enviroment |
| return LinkedManager.hasManager(documents); |
| } |
| |
| /** |
| * Cancels any linked environment on the specified document. If there is no |
| * environment, nothing happens. |
| * |
| * @param document the document whose <code>LinkedEnvironment</code> should |
| * be cancelled |
| */ |
| public static void closeEnvironment(IDocument document) { |
| LinkedManager.cancelManager(document); |
| } |
| |
| /** |
| * Returns the environment currently active on <code>document</code> at |
| * <code>offset</code>, or <code>null</code> if there is none. |
| * |
| * @param document the document for which the caller asks for an |
| * environment |
| * @param offset the offset into <code>document</code>, as there may be |
| * several environments on a document |
| * @return the environment currently active on <code>document</code>, or |
| * <code>null</code> |
| */ |
| public static LinkedEnvironment getEnvironment(IDocument document, int offset) { |
| LinkedManager mgr= LinkedManager.getLinkedManager(new IDocument[] {document}, false); |
| if (mgr != null) |
| return mgr.getTopEnvironment(); |
| else |
| return null; |
| } |
| |
| /** |
| * Encapsulates the edition triggered by a change to a linked position. Can |
| * be applied to a document as a whole. |
| */ |
| private class Replace implements IReplace { |
| |
| /** The edition to apply on a document. */ |
| private TextEdit fEdit; |
| |
| /** |
| * Creates a new instance. |
| * |
| * @param edit the edition to apply to a document. |
| */ |
| public Replace(TextEdit edit) { |
| fEdit= edit; |
| } |
| |
| /* |
| * @see org.eclipse.jface.text.IDocumentExtension.IReplace#perform(org.eclipse.jface.text.IDocument, org.eclipse.jface.text.IDocumentListener) |
| */ |
| public void perform(IDocument document, IDocumentListener owner) throws RuntimeException, MalformedTreeException { |
| document.removeDocumentListener(owner); |
| fIsChanging= true; |
| try { |
| fEdit.apply(document, TextEdit.UPDATE_REGIONS | TextEdit.CREATE_UNDO); |
| } catch (BadLocationException e) { |
| /* perform should really throw a BadLocationException |
| * TODO see https://bugs.eclipse.org/bugs/show_bug.cgi?id=52950 |
| */ |
| throw new RuntimeException(e); |
| } finally { |
| document.addDocumentListener(owner); |
| fIsChanging= false; |
| } |
| } |
| |
| } |
| |
| /** |
| * The document listener triggering the linked updating of positions |
| * managed by this environment. |
| */ |
| private class DocumentListener implements IDocumentListener { |
| |
| private DocumentEvent fLastEvent; |
| private boolean fExit= false; |
| |
| /** |
| * Checks whether <code>event</code> occurs within any of the positions |
| * managed by this environment. If not, the linked mode is left. |
| * |
| * @param event {@inheritDoc} |
| */ |
| public void documentAboutToBeChanged(DocumentEvent event) { |
| // don't react on changes executed by the parent environment |
| if (fParentEnvironment != null && fParentEnvironment.isChanging()) |
| return; |
| |
| fExit= false; |
| fLastEvent= event; |
| |
| for (Iterator it= fGroups.iterator(); it.hasNext(); ) { |
| LinkedPositionGroup group= (LinkedPositionGroup) it.next(); |
| if (group.isLegalEvent(event)) |
| // take the first hit - exlusion is guaranteed by enforcing |
| // disjointness when adding positions |
| return; |
| } |
| |
| // the event describes a change that lies outside of any managed |
| // position -> signal to exit |
| // don't exit here already, since we want to make sure that the positions |
| // are updated to the document event. |
| // TODO we might not always want to exit, e.g. we want to stay |
| // linked if code completion has inserted import statements |
| fExit= true; |
| } |
| |
| /** |
| * Propagates a change to a linked position to all its sibling positions. |
| * |
| * @param event {@inheritDoc} |
| */ |
| public void documentChanged(DocumentEvent event) { |
| // don't react on changes executed by the parent environment |
| if (fParentEnvironment != null && fParentEnvironment.isChanging()) |
| return; |
| |
| if (event.equals(fLastEvent) && fExit) |
| LinkedEnvironment.this.exit(ILinkedListener.EXTERNAL_MODIFICATION); |
| |
| for (Iterator it= fGroups.iterator(); it.hasNext(); ) { |
| LinkedPositionGroup group= (LinkedPositionGroup) it.next(); |
| |
| Map result= group.handleEvent(event); |
| if (result == null) |
| continue; |
| |
| // edit all documents |
| for (Iterator it2= result.keySet().iterator(); it2.hasNext(); ) { |
| IDocument doc= (IDocument) it2.next(); |
| TextEdit edit= (TextEdit) result.get(doc); |
| Replace replace= new Replace(edit); |
| |
| // apply the edition, either as post notification replace |
| // on the calling document or directly on any other |
| // document |
| if (doc == event.getDocument()) { |
| if (doc instanceof IDocumentExtension) { |
| ((IDocumentExtension) doc).registerPostNotificationReplace(this, replace); |
| } else { |
| // ignore - there is no way we can log from jface text... |
| } |
| } else { |
| replace.perform(doc, this); |
| } |
| } |
| |
| // take the first hit - exlusion is guaranteed by enforcing |
| // disjointness when adding positions |
| return; |
| } |
| } |
| |
| } |
| |
| /** The set of linked position groups. */ |
| private final List fGroups= new ArrayList(); |
| /** The set of documents spanned by this group. */ |
| private final Set fDocuments= new HashSet(); |
| /** The position updater for linked positions. */ |
| private final IPositionUpdater fUpdater= new InclusivePositionUpdater(getCategory()); |
| /** The document listener on the documents affected by this environment. */ |
| private final IDocumentListener fDocumentListener= new DocumentListener(); |
| /** The parent environment for a hierachical set up, or <code>null</code>. */ |
| private LinkedEnvironment fParentEnvironment; |
| /** |
| * The position in <code>fParentEnvironment</code> that includes all |
| * positions in this object, or <code>null</code> if there is no parent |
| * environment. |
| */ |
| private LinkedPosition fParentPosition= null; |
| /** |
| * An environment is sealed once it has children - no more positions can be |
| * added. |
| */ |
| private boolean fIsSealed= false; |
| /** <code>true</code> when this environment is changing documents. */ |
| private boolean fIsChanging= false; |
| /** The linked listeners. */ |
| private final List fListeners= new ArrayList(); |
| /** Flag telling whether we have exited: */ |
| private boolean fIsActive= true; |
| /** |
| * The sequence of document positions as we are going to iterate through |
| * them. |
| */ |
| private List fPositionSequence= new ArrayList(); |
| |
| /** |
| * Whether we are in the process of editing documents (set by <code>Replace</code>, |
| * read by <code>DocumentListener</code>. |
| * |
| * @return <code>true</code> if we are in the process of editing a |
| * document, <code>false</code> otherwise |
| */ |
| private boolean isChanging() { |
| return fIsChanging || fParentEnvironment != null && fParentEnvironment.isChanging(); |
| } |
| |
| /** |
| * Throws a <code>BadLocationException</code> if <code>group</code> |
| * conflicts with this environment's groups. |
| * |
| * @param group the group being checked |
| * @throws BadLocationException if <code>group</code> conflicts with this |
| * environment's groups |
| */ |
| private void enforceDisjoint(LinkedPositionGroup group) throws BadLocationException { |
| for (Iterator it= fGroups.iterator(); it.hasNext(); ) { |
| LinkedPositionGroup g= (LinkedPositionGroup) it.next(); |
| g.enforceDisjoint(group); |
| } |
| } |
| |
| /** |
| * Causes this environment to exit. Called either if a document change |
| * outside this enviroment is detected, or by the UI. |
| * |
| * <p>This method part of the private protocol between <code>LinkedUIControl</code> and <code>LinkedEnvironment</code>.</p> |
| * |
| * @param flags the exit flags. |
| */ |
| public void exit(int flags) { |
| if (!fIsActive) |
| return; |
| fIsActive= false; |
| |
| for (Iterator it= fDocuments.iterator(); it.hasNext(); ) { |
| IDocument doc= (IDocument) it.next(); |
| try { |
| doc.removePositionCategory(getCategory()); |
| } catch (BadPositionCategoryException e) { |
| // won't happen |
| Assert.isTrue(false); |
| } |
| doc.removePositionUpdater(fUpdater); |
| doc.removeDocumentListener(fDocumentListener); |
| } |
| |
| fDocuments.clear(); |
| fGroups.clear(); |
| |
| for (Iterator it= fListeners.iterator(); it.hasNext(); ) { |
| ILinkedListener listener= (ILinkedListener) it.next(); |
| listener.left(this, flags); |
| } |
| |
| fListeners.clear(); |
| |
| if (fParentEnvironment != null) |
| fParentEnvironment.resume(flags); |
| } |
| |
| /** |
| * Puts <code>document</code> into the set of managed documents. This |
| * involves registering the document listener and adding our position |
| * category. |
| * |
| * @param document the new document |
| */ |
| private void manageDocument(IDocument document) { |
| if (!fDocuments.contains(document)) { |
| fDocuments.add(document); |
| document.addPositionCategory(getCategory()); |
| document.addPositionUpdater(fUpdater); |
| document.addDocumentListener(fDocumentListener); |
| } |
| |
| } |
| |
| /** |
| * Returns the position category used by this environment. |
| * |
| * @return the position category used by this environment |
| */ |
| private String getCategory() { |
| return toString(); |
| } |
| |
| /** |
| * Adds a position group to this <code>LinkedEnvironment</code>. This |
| * method may not be called if the environment is already sealed, i.e. a |
| * nested environment has been added to it. It is also not wise to add |
| * groups once a UI has been established on top of this environment. |
| * |
| * <p> |
| * If the positions in <code>group</code> conflict with any other groups |
| * in this environment, a <code>BadLocationException</code> is thrown. |
| * Also, if this environment is nested in another one, all positions in all |
| * groups of the child environment have to lie in a single position in the |
| * parent environment, otherwise a <code>BadLocationException</code> is |
| * thrown. |
| * </p> |
| * |
| * <p> |
| * If <code>group</code> already exists, nothing happens. |
| * </p> |
| * |
| * @param group the group to be added to this environment |
| * @throws BadLocationException if the group conflicts with the other |
| * groups in this environment or violates the nesting requirements. |
| * @throws IllegalStateException if the method is called when the |
| * environment is already sealed |
| */ |
| public void addGroup(LinkedPositionGroup group) throws BadLocationException { |
| if (group == null) |
| throw new IllegalArgumentException("group may not be null"); //$NON-NLS-1$ |
| if (fIsSealed) |
| throw new IllegalStateException("environment is already installed"); //$NON-NLS-1$ |
| if (fGroups.contains(group)) |
| // nothing happens |
| return; |
| |
| enforceDisjoint(group); |
| group.seal(); |
| fGroups.add(group); |
| } |
| |
| |
| /** |
| * Installs this environment, which includes registering as document listener |
| * on all involved documents and storing global information about this environment. If |
| * an exception is thrown, the installation failed and the environment is unusable. |
| * |
| * @throws BadLocationException if some of the positions of this environment were not valid positions on their respective documents |
| */ |
| public void forceInstall() throws BadLocationException { |
| if (!install(true)) |
| Assert.isTrue(false); |
| } |
| |
| /** |
| * Installs this environment, which includes registering as document listener |
| * on all involved documents and storing global information about this environment. The return |
| * value states whether installation was successful; if not, the environment is not installed |
| * and will not work. |
| * |
| * @return <code>true</code> if installation was successful, <code>false</code> otherwise |
| * @throws BadLocationException if some of the positions of this environment were not valid positions on their respective documents |
| */ |
| public boolean tryInstall() throws BadLocationException { |
| return install(false); |
| } |
| |
| /** |
| * Installs this environment, which includes registering as document listener |
| * on all involved documents and storing global information about this environment. The return |
| * value states whether installation was successful; if not, the environment is not installed |
| * and will not work. The return value can only then become <code>false</code> if <code>force</code> |
| * was set to <code>false</code> as well. |
| * |
| * @param force if <code>true</code>, any other environment that cannot coexist |
| * with this one is canceled; if <code>false</code>, install will fail when conflicts |
| * occur and return false |
| * @return <code>true</code> if installation was successful, <code>false</code> otherwise |
| * @throws BadLocationException if some of the positions of this environment were not valid positions on their respective documents |
| */ |
| private boolean install(boolean force) throws BadLocationException { |
| if (fIsSealed) |
| throw new IllegalStateException("environment is already installed"); //$NON-NLS-1$ |
| enforceNotEmpty(); |
| |
| IDocument[] documents= getDocuments(); |
| LinkedManager manager= LinkedManager.getLinkedManager(documents, force); |
| // if we force creation, we require a valid manager |
| Assert.isTrue(!(force && manager == null)); |
| if (manager == null) |
| return false; |
| |
| if (!manager.nestEnvironment(this, force)) |
| if (force) |
| Assert.isTrue(false); |
| else |
| return false; |
| |
| // we set up successfully. After this point, exit has to be called to |
| // remove registered listeners... |
| fIsSealed= true; |
| if (fParentEnvironment != null) |
| fParentEnvironment.suspend(); |
| |
| // register positions |
| try { |
| for (Iterator it= fGroups.iterator(); it.hasNext(); ) { |
| LinkedPositionGroup group= (LinkedPositionGroup) it.next(); |
| group.register(this); |
| } |
| return true; |
| } catch (BadLocationException e){ |
| // if we fail to add, make sure to release all listeners again |
| exit(ILinkedListener.NONE); |
| throw e; |
| } |
| } |
| |
| /** |
| * Asserts that there is at least one linked position in this linked |
| * environment, throws an IllegalStateException otherwise. |
| */ |
| private void enforceNotEmpty() { |
| boolean hasPosition= false; |
| for (Iterator it= fGroups.iterator(); it.hasNext(); ) |
| if (!((LinkedPositionGroup) it.next()).isEmtpy()) { |
| hasPosition= true; |
| break; |
| } |
| if (!hasPosition) |
| throw new IllegalStateException("must specify at least one linked position"); //$NON-NLS-1$ |
| |
| } |
| |
| /** |
| * Collects all the documents that contained positions are set upon. |
| * @return the set of documents affected by this environment |
| */ |
| private IDocument[] getDocuments() { |
| Set docs= new HashSet(); |
| for (Iterator it= fGroups.iterator(); it.hasNext(); ) { |
| LinkedPositionGroup group= (LinkedPositionGroup) it.next(); |
| docs.addAll(Arrays.asList(group.getDocuments())); |
| } |
| return (IDocument[]) docs.toArray(new IDocument[docs.size()]); |
| } |
| |
| /** |
| * Returns whether the receiver can be nested into the given <code>parent</code> |
| * environment. If yes, the parent environment and its position that the receiver |
| * fits in are remembered. |
| * |
| * @param parent the parent environment candidate |
| * @return <code>true</code> if the receiver can be nested into <code>parent</code>, <code>false</code> otherwise |
| */ |
| boolean canNestInto(LinkedEnvironment parent) { |
| for (Iterator it= fGroups.iterator(); it.hasNext(); ) { |
| LinkedPositionGroup group= (LinkedPositionGroup) it.next(); |
| if (!enforceNestability(group, parent)) { |
| fParentPosition= null; |
| return false; |
| } |
| } |
| |
| Assert.isNotNull(fParentPosition); |
| fParentEnvironment= parent; |
| return true; |
| } |
| |
| /** |
| * Called by nested environments when a group is added to them. All |
| * positions in all groups of a nested environment have to fit inside a |
| * single position in the parent environment. |
| * |
| * @param group the group of the nested environment to be adopted. |
| * @param environment the environment to check against |
| */ |
| private boolean enforceNestability(LinkedPositionGroup group, LinkedEnvironment environment) { |
| Assert.isNotNull(environment); |
| Assert.isNotNull(group); |
| |
| try { |
| for (Iterator it= environment.fGroups.iterator(); it.hasNext(); ) { |
| LinkedPositionGroup pg= (LinkedPositionGroup) it.next(); |
| LinkedPosition pos; |
| pos= pg.adopt(group); |
| if (pos != null && fParentPosition != null && fParentPosition != pos) |
| return false; // group does not fit into one parent position, which is illegal |
| else if (fParentPosition == null && pos != null) |
| fParentPosition= pos; |
| } |
| } catch (BadLocationException e) { |
| return false; |
| } |
| |
| // group must fit into exactly one of the parent's positions |
| return fParentPosition != null; |
| } |
| |
| /** |
| * Returns whether this environment is nested. |
| * |
| * <p>This method part of the private protocol between <code>LinkedUIControl</code> and <code>LinkedEnvironment</code>.</p> |
| * |
| * @return <code>true</code> if this environment is nested, <code>false</code> |
| * otherwise |
| */ |
| public boolean isNested() { |
| return fParentEnvironment != null; |
| } |
| |
| /** |
| * Returns the positions in this environment that have a tab stop, in the |
| * order they were added. |
| * |
| * <p>This method part of the private protocol between <code>LinkedUIControl</code> and <code>LinkedEnvironment</code>.</p> |
| * |
| * @return the positions in this environment that have a tab stop, in the |
| * order they were added |
| */ |
| public List getTabStopSequence() { |
| return fPositionSequence; |
| } |
| |
| /** |
| * Adds <code>listener</code> to the set of listeners that are informed |
| * upon state changes. |
| * |
| * @param listener the new listener |
| */ |
| public void addLinkedListener(ILinkedListener listener) { |
| Assert.isNotNull(listener); |
| if (!fListeners.contains(listener)) |
| fListeners.add(listener); |
| } |
| |
| /** |
| * Removes <code>listener</code> from the set of listeners that are |
| * informed upon state changes. |
| * |
| * @param listener the new listener |
| */ |
| public void removeLinkedListener(ILinkedListener listener) { |
| fListeners.remove(listener); |
| } |
| |
| /** |
| * Finds the position in this environment that is closest after <code>toFind</code>. |
| * <code>toFind</code> needs not be a position in this environment and |
| * serves merely as an offset. |
| * |
| * <p>This method part of the private protocol between <code>LinkedUIControl</code> and <code>LinkedEnvironment</code>.</p> |
| * |
| * @param toFind the position to search from |
| * @return the closest position in the same document as <code>toFind</code> |
| * after the offset of <code>toFind</code>, or <code>null</code> |
| */ |
| public LinkedPosition findPosition(LinkedPosition toFind) { |
| LinkedPosition position= null; |
| for (Iterator it= fGroups.iterator(); it.hasNext(); ) { |
| LinkedPositionGroup group= (LinkedPositionGroup) it.next(); |
| position= group.getPosition(toFind); |
| if (position != null) |
| break; |
| } |
| return position; |
| } |
| |
| /** |
| * Registers a <code>LinkedPosition</code> with this environment. Called |
| * by <code>PositionGroup</code>. |
| * |
| * @param position the position to register |
| * @throws BadLocationException if the position cannot be added to its |
| * document |
| */ |
| void register(LinkedPosition position) throws BadLocationException { |
| Assert.isNotNull(position); |
| |
| IDocument document= position.getDocument(); |
| manageDocument(document); |
| try { |
| document.addPosition(getCategory(), position); |
| } catch (BadPositionCategoryException e) { |
| // won't happen as the category has been added by manageDocument() |
| Assert.isTrue(false); |
| } |
| int seqNr= position.getSequenceNumber(); |
| if (seqNr != LinkedPositionGroup.NO_STOP) { |
| fPositionSequence.add(position); |
| } |
| } |
| |
| /** |
| * Suspends this environment. |
| */ |
| private void suspend() { |
| List l= new ArrayList(fListeners); |
| for (Iterator it= l.iterator(); it.hasNext(); ) { |
| ILinkedListener listener= (ILinkedListener) it.next(); |
| listener.suspend(this); |
| } |
| } |
| |
| /** |
| * Resumes this environment. <code>flags</code> can be <code>NONE</code> |
| * or <code>SELECT</code>. |
| * |
| * @param flags <code>NONE</code> or <code>SELECT</code> |
| */ |
| private void resume(int flags) { |
| List l= new ArrayList(fListeners); |
| for (Iterator it= l.iterator(); it.hasNext(); ) { |
| ILinkedListener listener= (ILinkedListener) it.next(); |
| listener.resume(this, flags); |
| } |
| } |
| |
| /** |
| * Returns whether an offset is contained by any position in this |
| * environment. |
| * |
| * @param offset the offset to check |
| * @return <code>true</code> if <code>offset</code> is included by any |
| * position (see {@link LinkedPosition#includes(int)}) in this |
| * environment, <code>false</code> otherwise |
| */ |
| public boolean anyPositionContains(int offset) { |
| for (Iterator it= fGroups.iterator(); it.hasNext(); ) { |
| LinkedPositionGroup group= (LinkedPositionGroup) it.next(); |
| if (group.contains(offset)) |
| // take the first hit - exlusion is guaranteed by enforcing |
| // disjointness when adding positions |
| return true; |
| } |
| return false; |
| } |
| |
| /** |
| * Returns the linked position group that contains <code>position</code>, |
| * or <code>null</code> if <code>position</code> is not contained in |
| * any group within this environment. Group containment is tested by |
| * calling <code>group.contains(position)</code> for every <code>group</code> |
| * in this environment. |
| * |
| * <p> |
| * This method part of the private protocol between <code>LinkedUIControl</code> |
| * and <code>LinkedEnvironment</code>. |
| * </p> |
| * |
| * @param position the position the group of which is requested |
| * @return the first group in this environment for which <code>group.contains(position)</code> |
| * returns <code>true</code>, or <code>null</code> if no group |
| * contains <code>position</code> |
| */ |
| public LinkedPositionGroup getGroupForPosition(Position position) { |
| for (Iterator it= fGroups.iterator(); it.hasNext(); ) { |
| LinkedPositionGroup group= (LinkedPositionGroup) it.next(); |
| if (group.contains(position)) |
| return group; |
| } |
| return null; |
| } |
| |
| } |