view dwtx/jface/text/formatter/ContentFormatter.d @ 151:eb21d3dfc767

fix module statement
author Frank Benoit <benoit@tionex.de>
date Sun, 24 Aug 2008 23:55:45 +0200
parents 75302ef3f92f
children f70d9508c95c
line wrap: on
line source

/*******************************************************************************
 * Copyright (c) 2000, 2006 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
 * Port to the D programming language:
 *     Frank Benoit <benoit@tionex.de>
 *******************************************************************************/


module dwtx.jface.text.formatter.ContentFormatter;

import dwtx.jface.text.formatter.MultiPassContentFormatter; // packageimport
import dwtx.jface.text.formatter.ContextBasedFormattingStrategy; // packageimport
import dwtx.jface.text.formatter.FormattingContext; // packageimport
import dwtx.jface.text.formatter.IFormattingStrategy; // packageimport
import dwtx.jface.text.formatter.IContentFormatterExtension; // packageimport
import dwtx.jface.text.formatter.IFormattingStrategyExtension; // packageimport
import dwtx.jface.text.formatter.IContentFormatter; // packageimport
import dwtx.jface.text.formatter.FormattingContextProperties; // packageimport
import dwtx.jface.text.formatter.IFormattingContext; // packageimport

import dwt.dwthelper.utils;


import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import dwtx.core.runtime.Assert;
import dwtx.jface.text.BadLocationException;
import dwtx.jface.text.BadPositionCategoryException;
import dwtx.jface.text.DefaultPositionUpdater;
import dwtx.jface.text.DocumentEvent;
import dwtx.jface.text.IDocument;
import dwtx.jface.text.IDocumentExtension3;
import dwtx.jface.text.IPositionUpdater;
import dwtx.jface.text.IRegion;
import dwtx.jface.text.ITypedRegion;
import dwtx.jface.text.Position;
import dwtx.jface.text.TextUtilities;
import dwtx.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 : IContentFormatter {

    /**
     * Defines a reference to either the offset or the end offset of
     * a particular position.
     */
    static class PositionReference : Comparable {

        /** The referenced position */
        protected Position fPosition;
        /** The reference to either the offset or the end offset */
        protected bool 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 this(Position position, bool 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 bool 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();
        }

        /*
         * @see Comparable#compareTo(Object)
         */
        public int compareTo(Object obj) {

            if ( cast(PositionReference)obj ) {
                PositionReference r= cast(PositionReference) obj;
                return getCharacterPosition() - r.getCharacterPosition();
            }

            throw new ClassCastException();
        }
    }

    /**
     * The position updater used to update the remembered partitions.
     *
     * @see IPositionUpdater
     * @see DefaultPositionUpdater
     */
    class NonDeletingPositionUpdater : DefaultPositionUpdater {

        /**
         * Creates a new updater for the given category.
         *
         * @param category the category
         */
        protected this(String category) {
            super(category);
        }

        /*
         * @see DefaultPositionUpdater#notDeleted()
         */
        protected bool 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 : IPositionUpdater {
        /*
         * @see IPositionUpdater#update(DocumentEvent)
         */
        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 : 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 this(int[] positions, int offset) {
            fPositions= positions;
            fOffset= offset;
        }

        /*
         * @see IPositionUpdater#update(DocumentEvent)
         */
        public void update(DocumentEvent event) {
            updateAffectedPositions(event.getDocument(), fPositions, fOffset);
        }
    }


    /** Internal position category used for the formatter partitioning */
    private const static String PARTITIONING= "__formatter_partitioning"; //$NON-NLS-1$

    /** The map of <code>IFormattingStrategy</code> objects */
    private Map fStrategies;
    /** The indicator of whether the formatter operates in partition aware mode or not */
    private bool 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 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 bool 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 this() {
        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 is null)
            fStrategies= new HashMap();

        if (strategy is 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.
     */
    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(bool enable) {
        fIsPartitionAware= enable;
    }

    /*
     * @see IContentFormatter#getFormattingStrategy(String)
     */
    public IFormattingStrategy getFormattingStrategy(String contentType) {

        Assert.isNotNull(contentType);

        if (fStrategies is null)
            return null;

        return cast(IFormattingStrategy) fStrategies.get(contentType);
    }

    /*
     * @see IContentFormatter#format(IDocument, IRegion)
     */
    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 !is 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 !is 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)  {

        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 !is 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 !is 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 !is 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 !is 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 is 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 bool ignoreCategory(String category) {

        if (PARTITIONING.equals(category))
            return true;

        String[] categories= getPartitionManagingCategories();
        if (categories !is 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 !is 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= cast(PositionReference) 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= cast(PositionReference) 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 !is fDocument)
            return;

        if (positions.length is 0)
            return;

        for (int i= 0; i < positions.length; i++) {

            PositionReference r= cast(PositionReference) 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 bool 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' is c || ' ' is 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 bool isLineStart(int offset)  {
        int start= fDocument.getLineOfOffset(offset);
        start= fDocument.getLineOffset(start);
        return (start is offset);
    }
}