/******************************************************************************* * 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 (TypedPosition region : regions) { IFormattingStrategy s= getFormattingStrategy(region.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 (TypedPosition range : ranges) { IFormattingStrategy s= getFormattingStrategy(range.getType()); if (s != null) { format(s, range); } } } /** * 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 (TypedPosition region : regions) { IFormattingStrategy s= getFormattingStrategy(region.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 (String cat : categories) { if (cat.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 (String cat : categories) { if (ignoreCategory(cat)) continue; try { Position[] positions= fDocument.getPositions(cat); for (Position p : positions) { if (p.overlapsWith(offset, length)) { if (offset < p.getOffset()) fOverlappingPositionReferences.add(new PositionReference(p, true, cat)); if (p.getOffset() + p.getLength() < offset + length) fOverlappingPositionReferences.add(new PositionReference(p, false, cat)); } } } 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); } }