org.eclipse.jface.text.link.LinkedModeModel Maven / Gradle / Ivy
/*******************************************************************************
* Copyright (c) 2000, 2008 IBM Corporation and others.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-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.core.runtime.Assert;
import org.eclipse.text.edits.MalformedTreeException;
import org.eclipse.text.edits.TextEdit;
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;
/**
* The model for linked mode, umbrellas several
* {@link LinkedPositionGroup}s. Once installed, the model
* propagates any changes to a position to all its siblings in the same position
* group.
*
* Setting up a model consists of first adding
* LinkedPositionGroup
s to it, and then installing the
* model by either calling {@link #forceInstall()} or
* {@link #tryInstall()}. After installing the model, it becomes
* sealed and no more groups may be added.
*
*
* If a document change occurs that would modify more than one position
* group or that would invalidate the disjointness requirement of the positions,
* the model is torn down and all positions are deleted. The same happens
* upon calling {@link #exit(int)}.
*
* Nesting
*
* A LinkedModeModel
may be nested into another model. This
* happens when installing a model the positions of which all fit into a
* single position in a parent model that has previously been installed on
* the same document(s).
*
*
* Clients may instantiate instances of this class.
*
*
* @since 3.0
* @noextend This class is not intended to be subclassed by clients.
*/
public class LinkedModeModel {
/**
* Checks whether there is already a model installed on document
.
*
* @param document the IDocument
of interest
* @return true
if there is an existing model, false
* otherwise
*/
public static boolean hasInstalledModel(IDocument document) {
// if there is a manager, there also is a model
return LinkedModeManager.hasManager(document);
}
/**
* Checks whether there is already a linked mode model installed on any of
* the documents
.
*
* @param documents the IDocument
s of interest
* @return true
if there is an existing model, false
* otherwise
*/
public static boolean hasInstalledModel(IDocument[] documents) {
// if there is a manager, there also is a model
return LinkedModeManager.hasManager(documents);
}
/**
* Cancels any linked mode model on the specified document. If there is no
* model, nothing happens.
*
* @param document the document whose LinkedModeModel
should
* be canceled
*/
public static void closeAllModels(IDocument document) {
LinkedModeManager.cancelManager(document);
}
/**
* Returns the model currently active on document
at
* offset
, or null
if there is none.
*
* @param document the document for which the caller asks for a
* model
* @param offset the offset into document
, as there may be
* several models on a document
* @return the model currently active on document
, or
* null
*/
public static LinkedModeModel getModel(IDocument document, int offset) {
if (!hasInstalledModel(document))
return null;
LinkedModeManager mgr= LinkedModeManager.getLinkedManager(new IDocument[] {document}, false);
if (mgr != null)
return mgr.getTopEnvironment();
return null;
}
/**
* Encapsulates the edition triggered by a change to a linking 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) {
/* XXX: perform should really throw a BadLocationException
* 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 model.
*/
private class DocumentListener implements IDocumentListener {
private boolean fExit= false;
/**
* Checks whether event
occurs within any of the positions
* managed by this model. If not, the linked mode is left.
*
* @param event {@inheritDoc}
*/
public void documentAboutToBeChanged(DocumentEvent event) {
// don't react on changes executed by the parent model
if (fParentEnvironment != null && fParentEnvironment.isChanging())
return;
for (Iterator it= fGroups.iterator(); it.hasNext(); ) {
LinkedPositionGroup group= (LinkedPositionGroup) it.next();
if (!group.isLegalEvent(event)) {
fExit= true;
return;
}
}
}
/**
* Propagates a change to a linked position to all its sibling positions.
*
* @param event {@inheritDoc}
*/
public void documentChanged(DocumentEvent event) {
if (fExit) {
LinkedModeModel.this.exit(ILinkedModeListener.EXTERNAL_MODIFICATION);
return;
}
fExit= false;
// don't react on changes executed by the parent model
if (fParentEnvironment != null && fParentEnvironment.isChanging())
return;
// collect all results
Map result= null;
for (Iterator it= fGroups.iterator(); it.hasNext();) {
LinkedPositionGroup group= (LinkedPositionGroup) it.next();
Map map= group.handleEvent(event);
if (result != null && map != null) {
// exit if more than one position was changed
LinkedModeModel.this.exit(ILinkedModeListener.EXTERNAL_MODIFICATION);
return;
}
if (map != null)
result= map;
}
if (result != null) {
// 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);
}
}
}
}
}
/** 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 model. */
private final DocumentListener fDocumentListener= new DocumentListener();
/** The parent model for a hierarchical set up, or null
. */
private LinkedModeModel fParentEnvironment;
/**
* The position in fParentEnvironment
that includes all
* positions in this object, or null
if there is no parent
* model.
*/
private LinkedPosition fParentPosition= null;
/**
* A model is sealed once it has children - no more positions can be
* added.
*/
private boolean fIsSealed= false;
/** true
when this model 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 Replace
,
* read by DocumentListener
.
*
* @return true
if we are in the process of editing a
* document, false
otherwise
*/
private boolean isChanging() {
return fIsChanging || fParentEnvironment != null && fParentEnvironment.isChanging();
}
/**
* Throws a BadLocationException
if group
* conflicts with this model's groups.
*
* @param group the group being checked
* @throws BadLocationException if group
conflicts with this
* model'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 model to exit. Called either if an illegal document change
* is detected, or by the UI.
*
* @param flags the exit flags as defined in {@link ILinkedModeListener}
*/
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();
List listeners= new ArrayList(fListeners);
fListeners.clear();
for (Iterator it= listeners.iterator(); it.hasNext(); ) {
ILinkedModeListener listener= (ILinkedModeListener) it.next();
listener.left(this, flags);
}
if (fParentEnvironment != null)
fParentEnvironment.resume(flags);
}
/**
* Causes this model to stop forwarding updates. The positions are not
* unregistered however, which will only happen when exit
* is called, or after the next document change.
*
* @param flags the exit flags as defined in {@link ILinkedModeListener}
* @since 3.1
*/
public void stopForwarding(int flags) {
fDocumentListener.fExit= true;
}
/**
* Puts document
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 model.
*
* @return the position category used by this model
*/
private String getCategory() {
return toString();
}
/**
* Adds a position group to this LinkedModeModel
. This
* method may not be called if the model has been installed. Also, if
* a UI has been set up for this model, it may not pick up groups
* added afterwards.
*
* If the positions in group
conflict with any other group in
* this model, a BadLocationException
is thrown. Also,
* if this model is nested inside another one, all positions in all
* groups of the child model have to reside within a single position in the
* parent model, otherwise a BadLocationException
is thrown.
*
*
* If group
already exists, nothing happens.
*
*
* @param group the group to be added to this model
* @throws BadLocationException if the group conflicts with the other groups
* in this model or violates the nesting requirements.
* @throws IllegalStateException if the method is called when the
* model 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("model is already installed"); //$NON-NLS-1$
if (fGroups.contains(group))
// nothing happens
return;
enforceDisjoint(group);
group.seal();
fGroups.add(group);
}
/**
* Creates a new model.
* @since 3.1
*/
public LinkedModeModel() {
}
/**
* Installs this model, which includes registering as document
* listener on all involved documents and storing global information about
* this model. Any conflicting model already present will be
* closed.
*
* If an exception is thrown, the installation failed and
* the model is unusable.
*
*
* @throws BadLocationException if some of the positions of this model
* were not valid positions on their respective documents
*/
public void forceInstall() throws BadLocationException {
if (!install(true))
Assert.isTrue(false);
}
/**
* Installs this model, which includes registering as document
* listener on all involved documents and storing global information about
* this model. If there is another model installed on the
* document(s) targeted by the receiver that conflicts with it, installation
* may fail.
*
* The return value states whether installation was
* successful; if not, the model is not installed and will not work.
*
*
* @return true
if installation was successful,
* false
otherwise
* @throws BadLocationException if some of the positions of this model
* were not valid positions on their respective documents
*/
public boolean tryInstall() throws BadLocationException {
return install(false);
}
/**
* Installs this model, which includes registering as document
* listener on all involved documents and storing global information about
* this model. The return value states whether installation was
* successful; if not, the model is not installed and will not work.
* The return value can only then become false
if
* force
was set to false
as well.
*
* @param force if true
, any other model that cannot
* coexist with this one is canceled; if false
,
* install will fail when conflicts occur and return false
* @return true
if installation was successful,
* false
otherwise
* @throws BadLocationException if some of the positions of this model
* were not valid positions on their respective documents
*/
private boolean install(boolean force) throws BadLocationException {
if (fIsSealed)
throw new IllegalStateException("model is already installed"); //$NON-NLS-1$
enforceNotEmpty();
IDocument[] documents= getDocuments();
LinkedModeManager manager= LinkedModeManager.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(ILinkedModeListener.NONE);
throw e;
}
}
/**
* Asserts that there is at least one linked position in this linked mode
* model, throws an IllegalStateException otherwise.
*/
private void enforceNotEmpty() {
boolean hasPosition= false;
for (Iterator it= fGroups.iterator(); it.hasNext(); )
if (!((LinkedPositionGroup) it.next()).isEmpty()) {
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 model
*/
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 parent
* model. If yes, the parent model and its position that the receiver
* fits in are remembered.
*
* @param parent the parent model candidate
* @return true
if the receiver can be nested into parent
, false
otherwise
*/
boolean canNestInto(LinkedModeModel 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 models when a group is added to them. All
* positions in all groups of a nested model have to fit inside a
* single position in the parent model.
*
* @param group the group of the nested model to be adopted.
* @param model the model to check against
* @return false
if it failed to enforce nestability
*/
private boolean enforceNestability(LinkedPositionGroup group, LinkedModeModel model) {
Assert.isNotNull(model);
Assert.isNotNull(group);
try {
for (Iterator it= model.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 model is nested.
*
*
* This method is part of the private protocol between
* LinkedModeUI
and LinkedModeModel
.
*
*
* @return true
if this model is nested,
* false
otherwise
*/
public boolean isNested() {
return fParentEnvironment != null;
}
/**
* Returns the positions in this model that have a tab stop, in the
* order they were added.
*
*
* This method is part of the private protocol between
* LinkedModeUI
and LinkedModeModel
.
*
*
* @return the positions in this model that have a tab stop, in the
* order they were added
*/
public List getTabStopSequence() {
return fPositionSequence;
}
/**
* Adds listener
to the set of listeners that are informed
* upon state changes.
*
* @param listener the new listener
*/
public void addLinkingListener(ILinkedModeListener listener) {
Assert.isNotNull(listener);
if (!fListeners.contains(listener))
fListeners.add(listener);
}
/**
* Removes listener
from the set of listeners that are
* informed upon state changes.
*
* @param listener the new listener
*/
public void removeLinkingListener(ILinkedModeListener listener) {
fListeners.remove(listener);
}
/**
* Finds the position in this model that is closest after
* toFind
. toFind
needs not be a position in
* this model and serves merely as an offset.
*
*
* This method part of the private protocol between
* LinkedModeUI
and LinkedModeModel
.
*
*
* @param toFind the position to search from
* @return the closest position in the same document as toFind
* after the offset of toFind
, or null
*/
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 LinkedPosition
with this model. Called
* by PositionGroup
.
*
* @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 model.
*/
private void suspend() {
List l= new ArrayList(fListeners);
for (Iterator it= l.iterator(); it.hasNext(); ) {
ILinkedModeListener listener= (ILinkedModeListener) it.next();
listener.suspend(this);
}
}
/**
* Resumes this model. flags
can be NONE
* or SELECT
.
*
* @param flags NONE
or SELECT
*/
private void resume(int flags) {
List l= new ArrayList(fListeners);
for (Iterator it= l.iterator(); it.hasNext(); ) {
ILinkedModeListener listener= (ILinkedModeListener) it.next();
listener.resume(this, flags);
}
}
/**
* Returns whether an offset is contained by any position in this
* model.
*
* @param offset the offset to check
* @return true
if offset
is included by any
* position (see {@link LinkedPosition#includes(int)}) in this
* model, false
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 - exclusion is guaranteed by enforcing
// disjointness when adding positions
return true;
}
return false;
}
/**
* Returns the linked position group that contains position
,
* or null
if position
is not contained in any
* group within this model. Group containment is tested by calling
* group.contains(position)
for every group
in
* this model.
*
*
* This method part of the private protocol between
* LinkedModeUI
and LinkedModeModel
.
*
*
* @param position the position the group of which is requested
* @return the first group in this model for which
* group.contains(position)
returns true
,
* or null
if no group contains position
*/
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;
}
}