All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.eclipse.jface.text.link.LinkedModeModel Maven / Gradle / Ivy

There is a newer version: 1.9.22.1
Show newest version
/*******************************************************************************
 * Copyright (c) 2000, 2015 IBM Corporation and others.
 *
 * This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License 2.0
 * which accompanies this distribution, and is available at
 * https://www.eclipse.org/legal/epl-2.0/
 *
 * SPDX-License-Identifier: EPL-2.0
 *
 * 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.List;
import java.util.Map;
import java.util.Map.Entry;
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.IDocumentExtension.IReplace;
import org.eclipse.jface.text.IDocumentListener;
import org.eclipse.jface.text.IPositionUpdater;
import org.eclipse.jface.text.Position;


/**
 * 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 * LinkedPositionGroups 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 IDocuments 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; } @Override 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} */ @Override public void documentAboutToBeChanged(DocumentEvent event) { // don't react on changes executed by the parent model if (fParentEnvironment != null && fParentEnvironment.isChanging()) return; for (LinkedPositionGroup group : fGroups) { if (!group.isLegalEvent(event)) { fExit= true; return; } } } /** * Propagates a change to a linked position to all its sibling positions. * * @param event {@inheritDoc} */ @Override 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 (LinkedPositionGroup group : fGroups) { 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 (Entry entry : result.entrySet()) { IDocument doc = entry.getKey(); TextEdit edit= entry.getValue(); 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 (LinkedPositionGroup g : fGroups) { 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 (IDocument doc : fDocuments) { 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 (ILinkedModeListener listener : listeners) { 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 (LinkedPositionGroup group : fGroups) { 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 (LinkedPositionGroup linkedPositionGroup : fGroups) if (!linkedPositionGroup.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 (LinkedPositionGroup group : fGroups) { docs.addAll(Arrays.asList(group.getDocuments())); } return 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 (LinkedPositionGroup group : fGroups) { 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 (LinkedPositionGroup pg : model.fGroups) { 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 (LinkedPositionGroup group : fGroups) { 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 (ILinkedModeListener listener : l) { 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 (ILinkedModeListener listener : l) { 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 (LinkedPositionGroup group : fGroups) { 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 (LinkedPositionGroup group : fGroups) { if (group.contains(position)) return group; } return null; } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy