| /******************************************************************************* |
| * Copyright (c) 2000, 2015 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.formatter; |
| |
| |
| import java.util.ArrayList; |
| import java.util.Collections; |
| import java.util.HashMap; |
| import java.util.List; |
| import java.util.Map; |
| |
| import org.eclipse.core.runtime.Assert; |
| |
| import org.eclipse.jface.text.BadLocationException; |
| import org.eclipse.jface.text.BadPositionCategoryException; |
| import org.eclipse.jface.text.DefaultPositionUpdater; |
| import org.eclipse.jface.text.DocumentEvent; |
| import org.eclipse.jface.text.IDocument; |
| import org.eclipse.jface.text.IDocumentExtension3; |
| import org.eclipse.jface.text.IPositionUpdater; |
| import org.eclipse.jface.text.IRegion; |
| import org.eclipse.jface.text.ITypedRegion; |
| import org.eclipse.jface.text.Position; |
| import org.eclipse.jface.text.TextUtilities; |
| import org.eclipse.jface.text.TypedPosition; |
| |
| |
| /** |
| * Standard implementation of <code>IContentFormatter</code>. |
| * The formatter supports two operation modes: partition aware and |
| * partition unaware. <p> |
| * In the partition aware mode, the formatter determines the |
| * partitioning of the document region to be formatted. For each |
| * partition it determines all document positions which are affected |
| * when text changes are applied to the partition. Those which overlap |
| * with the partition are remembered as character positions. These |
| * character positions are passed over to the formatting strategy |
| * registered for the partition's content type. The formatting strategy |
| * returns a string containing the formatted document partition as well |
| * as the adapted character positions. The formatted partition replaces |
| * the old content of the partition. The remembered document positions |
| * are updated with the adapted character positions. In addition, all |
| * other document positions are accordingly adapted to the formatting |
| * changes.<p> |
| * In the partition unaware mode, the document's partitioning is ignored |
| * and the document is considered consisting of only one partition of |
| * the content type <code>IDocument.DEFAULT_CONTENT_TYPE</code>. The |
| * formatting process is similar to the partition aware mode, with the |
| * exception of having only one partition.<p> |
| * Usually, clients instantiate this class and configure it before using it. |
| * |
| * @see IContentFormatter |
| * @see IDocument |
| * @see ITypedRegion |
| * @see Position |
| */ |
| public class ContentFormatter implements IContentFormatter { |
| |
| /** |
| * Defines a reference to either the offset or the end offset of |
| * a particular position. |
| */ |
| static class PositionReference implements Comparable<PositionReference> { |
| |
| /** The referenced position */ |
| protected Position fPosition; |
| /** The reference to either the offset or the end offset */ |
| protected boolean fRefersToOffset; |
| /** The original category of the referenced position */ |
| protected String fCategory; |
| |
| /** |
| * Creates a new position reference. |
| * |
| * @param position the position to be referenced |
| * @param refersToOffset <code>true</code> if position offset should be referenced |
| * @param category the category the given position belongs to |
| */ |
| protected PositionReference(Position position, boolean refersToOffset, String category) { |
| fPosition= position; |
| fRefersToOffset= refersToOffset; |
| fCategory= category; |
| } |
| |
| /** |
| * Returns the offset of the referenced position. |
| * |
| * @return the offset of the referenced position |
| */ |
| protected int getOffset() { |
| return fPosition.getOffset(); |
| } |
| |
| /** |
| * Manipulates the offset of the referenced position. |
| * |
| * @param offset the new offset of the referenced position |
| */ |
| protected void setOffset(int offset) { |
| fPosition.setOffset(offset); |
| } |
| |
| /** |
| * Returns the length of the referenced position. |
| * |
| * @return the length of the referenced position |
| */ |
| protected int getLength() { |
| return fPosition.getLength(); |
| } |
| |
| /** |
| * Manipulates the length of the referenced position. |
| * |
| * @param length the new length of the referenced position |
| */ |
| protected void setLength(int length) { |
| fPosition.setLength(length); |
| } |
| |
| /** |
| * Returns whether this reference points to the offset or end offset |
| * of the references position. |
| * |
| * @return <code>true</code> if the offset of the position is referenced, <code>false</code> otherwise |
| */ |
| protected boolean refersToOffset() { |
| return fRefersToOffset; |
| } |
| |
| /** |
| * Returns the category of the referenced position. |
| * |
| * @return the category of the referenced position |
| */ |
| protected String getCategory() { |
| return fCategory; |
| } |
| |
| /** |
| * Returns the referenced position. |
| * |
| * @return the referenced position |
| */ |
| protected Position getPosition() { |
| return fPosition; |
| } |
| |
| /** |
| * Returns the referenced character position |
| * |
| * @return the referenced character position |
| */ |
| protected int getCharacterPosition() { |
| if (fRefersToOffset) |
| return getOffset(); |
| return getOffset() + getLength(); |
| } |
| |
| @Override |
| public int compareTo(PositionReference r) { |
| return getCharacterPosition() - r.getCharacterPosition(); |
| } |
| } |
| |
| /** |
| * The position updater used to update the remembered partitions. |
| * |
| * @see IPositionUpdater |
| * @see DefaultPositionUpdater |
| */ |
| class NonDeletingPositionUpdater extends DefaultPositionUpdater { |
| |
| /** |
| * Creates a new updater for the given category. |
| * |
| * @param category the category |
| */ |
| protected NonDeletingPositionUpdater(String category) { |
| super(category); |
| } |
| |
| @Override |
| protected boolean notDeleted() { |
| return true; |
| } |
| } |
| |
| /** |
| * The position updater which runs as first updater on the document's positions. |
| * Used to remove all affected positions from their categories to avoid them |
| * from being regularly updated. |
| * |
| * @see IPositionUpdater |
| */ |
| class RemoveAffectedPositions implements IPositionUpdater { |
| @Override |
| public void update(DocumentEvent event) { |
| removeAffectedPositions(event.getDocument()); |
| } |
| } |
| |
| /** |
| * The position updater which runs as last updater on the document's positions. |
| * Used to update all affected positions and adding them back to their |
| * original categories. |
| * |
| * @see IPositionUpdater |
| */ |
| class UpdateAffectedPositions implements IPositionUpdater { |
| |
| /** The affected positions */ |
| private int[] fPositions; |
| /** The offset */ |
| private int fOffset; |
| |
| /** |
| * Creates a new updater. |
| * |
| * @param positions the affected positions |
| * @param offset the offset |
| */ |
| public UpdateAffectedPositions(int[] positions, int offset) { |
| fPositions= positions; |
| fOffset= offset; |
| } |
| |
| @Override |
| public void update(DocumentEvent event) { |
| updateAffectedPositions(event.getDocument(), fPositions, fOffset); |
| } |
| } |
| |
| |
| /** Internal position category used for the formatter partitioning */ |
| private final static String PARTITIONING= "__formatter_partitioning"; //$NON-NLS-1$ |
| |
| /** The map of <code>IFormattingStrategy</code> objects */ |
| private Map<String, IFormattingStrategy> fStrategies; |
| /** The indicator of whether the formatter operates in partition aware mode or not */ |
| private boolean fIsPartitionAware= true; |
| |
| /** The partition information managing document position categories */ |
| private String[] fPartitionManagingCategories; |
| /** The list of references to offset and end offset of all overlapping positions */ |
| private List<PositionReference> fOverlappingPositionReferences; |
| /** Position updater used for partitioning positions */ |
| private IPositionUpdater fPartitioningUpdater; |
| /** |
| * The document partitioning used by this formatter. |
| * @since 3.0 |
| */ |
| private String fPartitioning; |
| /** |
| * The document this formatter works on. |
| * @since 3.0 |
| */ |
| private IDocument fDocument; |
| /** |
| * The external partition managing categories. |
| * @since 3.0 |
| */ |
| private String[] fExternalPartitonManagingCategories; |
| /** |
| * Indicates whether <code>fPartitionManagingCategories</code> must be computed. |
| * @since 3.0 |
| */ |
| private boolean fNeedsComputation= true; |
| |
| |
| /** |
| * Creates a new content formatter. The content formatter operates by default |
| * in the partition-aware mode. There are no preconfigured formatting strategies. |
| * Will use the default document partitioning if not further configured. |
| */ |
| public ContentFormatter() { |
| fPartitioning= IDocumentExtension3.DEFAULT_PARTITIONING; |
| } |
| |
| /** |
| * Registers a strategy for a particular content type. If there is already a strategy |
| * registered for this type, the new strategy is registered instead of the old one. |
| * If the given content type is <code>null</code> the given strategy is registered for |
| * all content types as is called only once per formatting session. |
| * |
| * @param strategy the formatting strategy to register, or <code>null</code> to remove an existing one |
| * @param contentType the content type under which to register |
| */ |
| public void setFormattingStrategy(IFormattingStrategy strategy, String contentType) { |
| |
| Assert.isNotNull(contentType); |
| |
| if (fStrategies == null) |
| fStrategies= new HashMap<>(); |
| |
| if (strategy == null) |
| fStrategies.remove(contentType); |
| else |
| fStrategies.put(contentType, strategy); |
| } |
| |
| /** |
| * Informs this content formatter about the names of those position categories |
| * which are used to manage the document's partitioning information and thus should |
| * be ignored when this formatter updates positions. |
| * |
| * @param categories the categories to be ignored |
| * @deprecated incompatible with an open set of document partitionings. The provided information is only used |
| * if this formatter can not compute the partition managing position categories. |
| */ |
| @Deprecated |
| public void setPartitionManagingPositionCategories(String[] categories) { |
| fExternalPartitonManagingCategories= TextUtilities.copy(categories); |
| } |
| |
| /** |
| * Sets the document partitioning to be used by this formatter. |
| * |
| * @param partitioning the document partitioning |
| * @since 3.0 |
| */ |
| public void setDocumentPartitioning(String partitioning) { |
| fPartitioning= partitioning; |
| } |
| |
| /** |
| * Sets the formatter's operation mode. |
| * |
| * @param enable indicates whether the formatting process should be partition ware |
| */ |
| public void enablePartitionAwareFormatting(boolean enable) { |
| fIsPartitionAware= enable; |
| } |
| |
| @Override |
| public IFormattingStrategy getFormattingStrategy(String contentType) { |
| |
| Assert.isNotNull(contentType); |
| |
| if (fStrategies == null) |
| return null; |
| |
| return fStrategies.get(contentType); |
| } |
| |
| @Override |
| public void format(IDocument document, IRegion region) { |
| fNeedsComputation= true; |
| fDocument= document; |
| try { |
| |
| if (fIsPartitionAware) |
| formatPartitions(region); |
| else |
| formatRegion(region); |
| |
| } finally { |
| fNeedsComputation= true; |
| fDocument= null; |
| } |
| } |
| |
| /** |
| * Determines the partitioning of the given region of the document. |
| * Informs the formatting strategies of each partition about the start, |
| * the process, and the termination of the formatting session. |
| * |
| * @param region the document region to be formatted |
| * @since 3.0 |
| */ |
| private void formatPartitions(IRegion region) { |
| |
| addPartitioningUpdater(); |
| |
| try { |
| |
| TypedPosition[] ranges= getPartitioning(region); |
| if (ranges != null) { |
| start(ranges, getIndentation(region.getOffset())); |
| format(ranges); |
| stop(ranges); |
| } |
| |
| } catch (BadLocationException x) { |
| } |
| |
| removePartitioningUpdater(); |
| } |
| |
| /** |
| * Formats the given region with the strategy registered for the default |
| * content type. The strategy is informed about the start, the process, and |
| * the termination of the formatting session. |
| * |
| * @param region the region to be formatted |
| * @since 3.0 |
| */ |
| private void formatRegion(IRegion region) { |
| |
| IFormattingStrategy strategy= getFormattingStrategy(IDocument.DEFAULT_CONTENT_TYPE); |
| if (strategy != null) { |
| strategy.formatterStarts(getIndentation(region.getOffset())); |
| format(strategy, new TypedPosition(region.getOffset(), region.getLength(), IDocument.DEFAULT_CONTENT_TYPE)); |
| strategy.formatterStops(); |
| } |
| } |
| |
| /** |
| * Returns the partitioning of the given region of the document to be formatted. |
| * As one partition after the other will be formatted and formatting will |
| * probably change the length of the formatted partition, it must be kept |
| * track of the modifications in order to submit the correct partition to all |
| * formatting strategies. For this, all partitions are remembered as positions |
| * in a dedicated position category. (As formatting strategies might rely on each |
| * other, calling them in reversed order is not an option.) |
| * |
| * @param region the region for which the partitioning must be determined |
| * @return the partitioning of the specified region |
| * @exception BadLocationException of region is invalid in the document |
| * @since 3.0 |
| */ |
| private TypedPosition[] getPartitioning(IRegion region) throws BadLocationException { |
| |
| ITypedRegion[] regions= TextUtilities.computePartitioning(fDocument, fPartitioning, region.getOffset(), region.getLength(), false); |
| TypedPosition[] positions= new TypedPosition[regions.length]; |
| |
| for (int i= 0; i < regions.length; i++) { |
| positions[i]= new TypedPosition(regions[i]); |
| try { |
| fDocument.addPosition(PARTITIONING, positions[i]); |
| } catch (BadPositionCategoryException x) { |
| // should not happen |
| } |
| } |
| |
| return positions; |
| } |
| |
| /** |
| * Fires <code>formatterStarts</code> to all formatter strategies |
| * which will be involved in the forthcoming formatting process. |
| * |
| * @param regions the partitioning of the document to be formatted |
| * @param indentation the initial indentation |
| */ |
| private void start(TypedPosition[] regions, String indentation) { |
| for (int i= 0; i < regions.length; i++) { |
| IFormattingStrategy s= getFormattingStrategy(regions[i].getType()); |
| if (s != null) |
| s.formatterStarts(indentation); |
| } |
| } |
| |
| /** |
| * Formats one partition after the other using the formatter strategy registered for |
| * the partition's content type. |
| * |
| * @param ranges the partitioning of the document region to be formatted |
| * @since 3.0 |
| */ |
| private void format(TypedPosition[] ranges) { |
| for (int i= 0; i < ranges.length; i++) { |
| IFormattingStrategy s= getFormattingStrategy(ranges[i].getType()); |
| if (s != null) { |
| format(s, ranges[i]); |
| } |
| } |
| } |
| |
| /** |
| * Formats the given region of the document using the specified formatting |
| * strategy. In order to maintain positions correctly, first all affected |
| * positions determined, after all document listeners have been informed about |
| * the coming change, the affected positions are removed to avoid that they |
| * are regularly updated. After all position updaters have run, the affected |
| * positions are updated with the formatter's information and added back to |
| * their categories, right before the first document listener is informed about |
| * that a change happened. |
| * |
| * @param strategy the strategy to be used |
| * @param region the region to be formatted |
| * @since 3.0 |
| */ |
| private void format(IFormattingStrategy strategy, TypedPosition region) { |
| try { |
| |
| final int offset= region.getOffset(); |
| int length= region.getLength(); |
| |
| String content= fDocument.get(offset, length); |
| final int[] positions= getAffectedPositions(offset, length); |
| String formatted= strategy.format(content, isLineStart(offset), getIndentation(offset), positions); |
| |
| if (formatted != null && !formatted.equals(content)) { |
| |
| IPositionUpdater first= new RemoveAffectedPositions(); |
| fDocument.insertPositionUpdater(first, 0); |
| IPositionUpdater last= new UpdateAffectedPositions(positions, offset); |
| fDocument.addPositionUpdater(last); |
| |
| fDocument.replace(offset, length, formatted); |
| |
| fDocument.removePositionUpdater(first); |
| fDocument.removePositionUpdater(last); |
| } |
| |
| } catch (BadLocationException x) { |
| // should not happen |
| } |
| } |
| |
| /** |
| * Fires <code>formatterStops</code> to all formatter strategies which were |
| * involved in the formatting process which is about to terminate. |
| * |
| * @param regions the partitioning of the document which has been formatted |
| */ |
| private void stop(TypedPosition[] regions) { |
| for (int i= 0; i < regions.length; i++) { |
| IFormattingStrategy s= getFormattingStrategy(regions[i].getType()); |
| if (s != null) |
| s.formatterStops(); |
| } |
| } |
| |
| /** |
| * Installs those updaters which the formatter needs to keep track of the partitions. |
| * @since 3.0 |
| */ |
| private void addPartitioningUpdater() { |
| fPartitioningUpdater= new NonDeletingPositionUpdater(PARTITIONING); |
| fDocument.addPositionCategory(PARTITIONING); |
| fDocument.addPositionUpdater(fPartitioningUpdater); |
| } |
| |
| /** |
| * Removes the formatter's internal position updater and category. |
| * |
| * @since 3.0 |
| */ |
| private void removePartitioningUpdater() { |
| |
| try { |
| |
| fDocument.removePositionUpdater(fPartitioningUpdater); |
| fDocument.removePositionCategory(PARTITIONING); |
| fPartitioningUpdater= null; |
| |
| } catch (BadPositionCategoryException x) { |
| // should not happen |
| } |
| } |
| |
| /** |
| * Returns the partition managing position categories for the formatted document. |
| * |
| * @return the position managing position categories |
| * @since 3.0 |
| */ |
| private String[] getPartitionManagingCategories() { |
| if (fNeedsComputation) { |
| fNeedsComputation= false; |
| fPartitionManagingCategories= TextUtilities.computePartitionManagingCategories(fDocument); |
| if (fPartitionManagingCategories == null) |
| fPartitionManagingCategories= fExternalPartitonManagingCategories; |
| } |
| return fPartitionManagingCategories; |
| } |
| |
| /** |
| * Determines whether the given document position category should be ignored |
| * by this formatter's position updating. |
| * |
| * @param category the category to check |
| * @return <code>true</code> if the category should be ignored, <code>false</code> otherwise |
| */ |
| private boolean ignoreCategory(String category) { |
| |
| if (PARTITIONING.equals(category)) |
| return true; |
| |
| String[] categories= getPartitionManagingCategories(); |
| if (categories != null) { |
| for (int i= 0; i < categories.length; i++) { |
| if (categories[i].equals(category)) |
| return true; |
| } |
| } |
| |
| return false; |
| } |
| |
| /** |
| * Determines all embracing, overlapping, and follow up positions |
| * for the given region of the document. |
| * |
| * @param offset the offset of the document region to be formatted |
| * @param length the length of the document to be formatted |
| * @since 3.0 |
| */ |
| private void determinePositionsToUpdate(int offset, int length) { |
| |
| String[] categories= fDocument.getPositionCategories(); |
| if (categories != null) { |
| for (int i= 0; i < categories.length; i++) { |
| |
| if (ignoreCategory(categories[i])) |
| continue; |
| |
| try { |
| |
| Position[] positions= fDocument.getPositions(categories[i]); |
| |
| for (int j= 0; j < positions.length; j++) { |
| |
| Position p= positions[j]; |
| if (p.overlapsWith(offset, length)) { |
| |
| if (offset < p.getOffset()) |
| fOverlappingPositionReferences.add(new PositionReference(p, true, categories[i])); |
| |
| if (p.getOffset() + p.getLength() < offset + length) |
| fOverlappingPositionReferences.add(new PositionReference(p, false, categories[i])); |
| } |
| } |
| |
| } catch (BadPositionCategoryException x) { |
| // can not happen |
| } |
| } |
| } |
| } |
| |
| /** |
| * Returns all offset and the end offset of all positions overlapping with the |
| * specified document range. |
| * |
| * @param offset the offset of the document region to be formatted |
| * @param length the length of the document to be formatted |
| * @return all character positions of the interleaving positions |
| * @since 3.0 |
| */ |
| private int[] getAffectedPositions(int offset, int length) { |
| |
| fOverlappingPositionReferences= new ArrayList<>(); |
| |
| determinePositionsToUpdate(offset, length); |
| |
| Collections.sort(fOverlappingPositionReferences); |
| |
| int[] positions= new int[fOverlappingPositionReferences.size()]; |
| for (int i= 0; i < positions.length; i++) { |
| PositionReference r= fOverlappingPositionReferences.get(i); |
| positions[i]= r.getCharacterPosition() - offset; |
| } |
| |
| return positions; |
| } |
| |
| /** |
| * Removes the affected positions from their categories to avoid |
| * that they are invalidly updated. |
| * |
| * @param document the document |
| */ |
| private void removeAffectedPositions(IDocument document) { |
| int size= fOverlappingPositionReferences.size(); |
| for (int i= 0; i < size; i++) { |
| PositionReference r= fOverlappingPositionReferences.get(i); |
| try { |
| document.removePosition(r.getCategory(), r.getPosition()); |
| } catch (BadPositionCategoryException x) { |
| // can not happen |
| } |
| } |
| } |
| |
| /** |
| * Updates all the overlapping positions. Note, all other positions are |
| * automatically updated by their document position updaters. |
| * |
| * @param document the document to has been formatted |
| * @param positions the adapted character positions to be used to update the document positions |
| * @param offset the offset of the document region that has been formatted |
| */ |
| protected void updateAffectedPositions(IDocument document, int[] positions, int offset) { |
| |
| if (document != fDocument) |
| return; |
| |
| if (positions.length == 0) |
| return; |
| |
| for (int i= 0; i < positions.length; i++) { |
| |
| PositionReference r= fOverlappingPositionReferences.get(i); |
| |
| if (r.refersToOffset()) |
| r.setOffset(offset + positions[i]); |
| else |
| r.setLength((offset + positions[i]) - r.getOffset()); |
| |
| Position p= r.getPosition(); |
| String category= r.getCategory(); |
| if (!document.containsPosition(category, p.offset, p.length)) { |
| try { |
| if (positionAboutToBeAdded(document, category, p)) |
| document.addPosition(r.getCategory(), p); |
| } catch (BadPositionCategoryException x) { |
| // can not happen |
| } catch (BadLocationException x) { |
| // should not happen |
| } |
| } |
| |
| } |
| |
| fOverlappingPositionReferences= null; |
| } |
| |
| /** |
| * The given position is about to be added to the given position category of the given document. <p> |
| * This default implementation return <code>true</code>. |
| * |
| * @param document the document |
| * @param category the position category |
| * @param position the position that will be added |
| * @return <code>true</code> if the position can be added, <code>false</code> if it should be ignored |
| */ |
| protected boolean positionAboutToBeAdded(IDocument document, String category, Position position) { |
| return true; |
| } |
| |
| /** |
| * Returns the indentation of the line of the given offset. |
| * |
| * @param offset the offset |
| * @return the indentation of the line of the offset |
| * @since 3.0 |
| */ |
| private String getIndentation(int offset) { |
| |
| try { |
| int start= fDocument.getLineOfOffset(offset); |
| start= fDocument.getLineOffset(start); |
| |
| int end= start; |
| char c= fDocument.getChar(end); |
| while ('\t' == c || ' ' == c) |
| c= fDocument.getChar(++end); |
| |
| return fDocument.get(start, end - start); |
| } catch (BadLocationException x) { |
| } |
| |
| return ""; //$NON-NLS-1$ |
| } |
| |
| /** |
| * Determines whether the offset is the beginning of a line in the given document. |
| * |
| * @param offset the offset |
| * @return <code>true</code> if offset is the beginning of a line |
| * @exception BadLocationException if offset is invalid in document |
| * @since 3.0 |
| */ |
| private boolean isLineStart(int offset) throws BadLocationException { |
| int start= fDocument.getLineOfOffset(offset); |
| start= fDocument.getLineOffset(start); |
| return (start == offset); |
| } |
| } |