view dwtx/jface/text/source/projection/ProjectionViewer.d @ 192:c3583c6ec027

Added missing default cases for switch statements
author Frank Benoit <benoit@tionex.de>
date Mon, 03 Nov 2008 22:52:26 +0100
parents 1a5b8f8129df
children
line wrap: on
line source

/*******************************************************************************
 * Copyright (c) 2000, 2008 IBM Corporation and others.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v10.html
 *
 * Contributors:
 *     IBM Corporation - initial API and implementation
 * Port to the D programming language:
 *     Frank Benoit <benoit@tionex.de>
 *******************************************************************************/
module dwtx.jface.text.source.projection.ProjectionViewer;

import dwtx.jface.text.source.projection.ProjectionSupport; // packageimport
import dwtx.jface.text.source.projection.IProjectionPosition; // packageimport
import dwtx.jface.text.source.projection.AnnotationBag; // packageimport
import dwtx.jface.text.source.projection.ProjectionSummary; // packageimport
import dwtx.jface.text.source.projection.ProjectionAnnotationHover; // packageimport
import dwtx.jface.text.source.projection.ProjectionRulerColumn; // packageimport
import dwtx.jface.text.source.projection.ProjectionAnnotationModel; // packageimport
import dwtx.jface.text.source.projection.SourceViewerInformationControl; // packageimport
import dwtx.jface.text.source.projection.IProjectionListener; // packageimport
import dwtx.jface.text.source.projection.ProjectionAnnotation; // packageimport


import dwt.dwthelper.utils;
import dwtx.dwtxhelper.Collection;
import tango.core.Exception;



import dwt.DWTError;
import dwt.custom.ST;
import dwt.custom.StyledText;
import dwt.dnd.Clipboard;
import dwt.dnd.DND;
import dwt.dnd.TextTransfer;
import dwt.dnd.Transfer;
import dwt.events.VerifyEvent;
import dwt.graphics.Point;
import dwt.widgets.Composite;
import dwt.widgets.Display;
import dwtx.core.runtime.Assert;
import dwtx.jface.text.BadLocationException;
import dwtx.jface.text.DocumentEvent;
import dwtx.jface.text.FindReplaceDocumentAdapter;
import dwtx.jface.text.IDocument;
import dwtx.jface.text.IDocumentInformationMappingExtension;
import dwtx.jface.text.IDocumentListener;
import dwtx.jface.text.IRegion;
import dwtx.jface.text.ISlaveDocumentManager;
import dwtx.jface.text.ITextViewerExtension5;
import dwtx.jface.text.Position;
import dwtx.jface.text.Region;
import dwtx.jface.text.TextUtilities;
import dwtx.jface.text.projection.ProjectionDocument;
import dwtx.jface.text.projection.ProjectionDocumentEvent;
import dwtx.jface.text.projection.ProjectionDocumentManager;
import dwtx.jface.text.source.Annotation;
import dwtx.jface.text.source.AnnotationModelEvent;
import dwtx.jface.text.source.CompositeRuler;
import dwtx.jface.text.source.IAnnotationModel;
import dwtx.jface.text.source.IAnnotationModelExtension;
import dwtx.jface.text.source.IAnnotationModelListener;
import dwtx.jface.text.source.IAnnotationModelListenerExtension;
import dwtx.jface.text.source.IOverviewRuler;
import dwtx.jface.text.source.IVerticalRuler;
import dwtx.jface.text.source.IVerticalRulerColumn;
import dwtx.jface.text.source.SourceViewer;


/**
 * A projection source viewer is a source viewer which supports multiple visible
 * regions which can dynamically be changed.
 * <p>
 * A projection source viewer uses a <code>ProjectionDocumentManager</code>
 * for the management of the visible document.</p>
 * <p>
 * NOTE: The <code>ProjectionViewer</code> only supports projections that cover full lines.
 * </p>
 * <p>
 * This class should not be subclassed.</p>
 *
 * @since 3.0
 * @noextend This class is not intended to be subclassed by clients.
 */
public class ProjectionViewer : SourceViewer , ITextViewerExtension5 {

    public override IRegion getModelCoverage() {
        return super.getModelCoverage();
    }
    public override int modelLine2WidgetLine(int modelLine) {
        return super.modelLine2WidgetLine(modelLine);
    }
    public override int modelOffset2WidgetOffset(int modelOffset) {
        return super.modelOffset2WidgetOffset(modelOffset);
    }
    public override IRegion modelRange2WidgetRange(IRegion modelRange) {
        return super.modelRange2WidgetRange(modelRange);
    }
    protected override IRegion modelRange2WidgetRange(Position modelPosition) {
        return super.modelRange2WidgetRange(modelPosition);
    }
    public override int widgetOffset2ModelOffset(int widgetOffset) {
        return super.widgetOffset2ModelOffset(widgetOffset);
    }
    public override IRegion widgetRange2ModelRange(IRegion widgetRange) {
        return super.widgetRange2ModelRange(widgetRange);
    }
    public int widgetLine2ModelLine(int widgetLine) {
        return super.widgetLine2ModelLine(widgetLine);
    }
    public int widgetLineOfWidgetOffset(int widgetOffset) {
        return super.widgetLineOfWidgetOffset(widgetOffset);
    }

    private static const int BASE= INFORMATION; // see ISourceViewer.INFORMATION

    /** Operation constant for the expand operation. */
    public static const int EXPAND= BASE + 1;
    /** Operation constant for the collapse operation. */
    public static const int COLLAPSE= BASE + 2;
    /** Operation constant for the toggle projection operation. */
    public static const int TOGGLE= BASE + 3;
    /** Operation constant for the expand all operation. */
    public static const int EXPAND_ALL= BASE + 4;
    /**
     * Operation constant for the collapse all operation.
     *
     * @since 3.2
     */
    public static const int COLLAPSE_ALL= BASE + 5;

    /**
     * Internal listener to changes of the annotation model.
     */
    private class AnnotationModelListener : IAnnotationModelListener, IAnnotationModelListenerExtension {

        /*
         * @see dwtx.jface.text.source.IAnnotationModelListener#modelChanged(dwtx.jface.text.source.IAnnotationModel)
         */
        public void modelChanged(IAnnotationModel model) {
            processModelChanged(model, null);
        }

        /*
         * @see dwtx.jface.text.source.IAnnotationModelListenerExtension#modelChanged(dwtx.jface.text.source.AnnotationModelEvent)
         */
        public void modelChanged(AnnotationModelEvent event) {
            processModelChanged(event.getAnnotationModel(), event);
        }

        private void processModelChanged(IAnnotationModel model, AnnotationModelEvent event) {
            if (model is fProjectionAnnotationModel) {

                if (fProjectionSummary !is null)
                    fProjectionSummary.updateSummaries();
                processCatchupRequest(event);

            } else if (model is getAnnotationModel() && fProjectionSummary !is null)
                fProjectionSummary.updateSummaries();
        }
    }

    /**
     * Executes the 'replaceVisibleDocument' operation when called the first time. Self-destructs afterwards.
     */
    private class ReplaceVisibleDocumentExecutor : IDocumentListener {

        private IDocument fSlaveDocument;
        private IDocument fExecutionTrigger;

        /**
         * Creates a new executor in order to free the given slave document.
         *
         * @param slaveDocument the slave document to free
         */
        public this(IDocument slaveDocument) {
            fSlaveDocument= slaveDocument;
        }

        /**
         * Installs this executor on the given trigger document.
         *
         * @param executionTrigger the trigger document
         */
        public void install(IDocument executionTrigger) {
            if (executionTrigger !is null && fSlaveDocument !is null) {
                fExecutionTrigger= executionTrigger;
                fExecutionTrigger.addDocumentListener(this);
            }
        }

        /*
         * @see dwtx.jface.text.IDocumentListener#documentAboutToBeChanged(dwtx.jface.text.DocumentEvent)
         */
        public void documentAboutToBeChanged(DocumentEvent event) {
        }

        /*
         * @see dwtx.jface.text.IDocumentListener#documentChanged(dwtx.jface.text.DocumentEvent)
         */
        public void documentChanged(DocumentEvent event) {
            fExecutionTrigger.removeDocumentListener(this);
            executeReplaceVisibleDocument(fSlaveDocument);
        }
    }

    /**
     * A command representing a change of the projection document. This can be either
     * adding a master document range, removing a master document change, or invalidating
     * the viewer text presentation.
     */
    private static class ProjectionCommand {

        const static int ADD= 0;
        const static int REMOVE= 1;
        const static int INVALIDATE_PRESENTATION= 2;

        ProjectionDocument fProjection;
        int fType;
        int fOffset;
        int fLength;

        this(ProjectionDocument projection, int type, int offset, int length) {
            fProjection= projection;
            fType= type;
            fOffset= offset;
            fLength= length;
        }

        this(int offset, int length) {
            fType= INVALIDATE_PRESENTATION;
            fOffset= offset;
            fLength= length;
        }

        int computeExpectedCosts() {

            switch(fType) {
                case ADD: {
                    try {
                        IRegion[] gaps= fProjection.computeUnprojectedMasterRegions(fOffset, fLength);
                        return gaps is null ? 0 : gaps.length;
                    } catch (BadLocationException x) {
                    }
                    break;
                }
                case REMOVE: {
                    try {
                        IRegion[] fragments= fProjection.computeProjectedMasterRegions(fOffset, fLength);
                        return fragments is null ? 0 : fragments.length;
                    } catch (BadLocationException x) {
                    }
                    break;
                }
                default:
            }
            return 0;
        }
    }

    /**
     * The queue of projection command objects.
     */
    private static class ProjectionCommandQueue {

        final static int REDRAW_COSTS= 15;
        final static int INVALIDATION_COSTS= 10;

        List fList;
        int fExpectedExecutionCosts= -1;

        this(){
            fList= new ArrayList(15);
        }

        void add(ProjectionCommand command) {
            fList.add(command);
        }

        Iterator iterator() {
            return fList.iterator();
        }

        void clear() {
            fList.clear();
            fExpectedExecutionCosts= -1;
        }

        bool passedRedrawCostsThreshold() {
            if (fExpectedExecutionCosts is -1)
                computeExpectedExecutionCosts();
            return fExpectedExecutionCosts > REDRAW_COSTS;
        }

        bool passedInvalidationCostsThreshold() {
            if (fExpectedExecutionCosts is -1)
                computeExpectedExecutionCosts();
            return fExpectedExecutionCosts > INVALIDATION_COSTS;
        }

        private void computeExpectedExecutionCosts() {
            int max_costs= Math.max(REDRAW_COSTS, INVALIDATION_COSTS);
            fExpectedExecutionCosts= fList.size();
            if (fExpectedExecutionCosts <= max_costs) {
                ProjectionCommand command;
                Iterator e= fList.iterator();
                while (e.hasNext()) {
                    command= cast(ProjectionCommand) e.next();
                    fExpectedExecutionCosts += command.computeExpectedCosts();
                    if (fExpectedExecutionCosts > max_costs)
                        break;
                }
            }
        }
    }

    /** The projection annotation model used by this viewer. */
    private ProjectionAnnotationModel fProjectionAnnotationModel;
    /** The annotation model listener */
    private IAnnotationModelListener fAnnotationModelListener;
    /** The projection summary. */
    private ProjectionSummary fProjectionSummary;
    /** Indication that an annotation world change has not yet been processed. */
    private bool fPendingAnnotationWorldChange= false;
    /** Indication whether projection changes in the visible document should be considered. */
    private bool fHandleProjectionChanges= true;
    /** The list of projection listeners. */
    private List fProjectionListeners;
    /** Internal lock for protecting the list of pending requests */
    private Object fLock;
    /** The list of pending requests */
    private List fPendingRequests;
    /** The replace-visible-document execution trigger */
    private IDocument fReplaceVisibleDocumentExecutionTrigger;
    /** <code>true</code> if projection was on the last time we switched to segmented mode. */
    private bool fWasProjectionEnabled;
    /** The queue of projection commands used to assess the costs of projection changes. */
    private ProjectionCommandQueue fCommandQueue;
    /**
     * The amount of lines deleted by the last document event issued by the
     * visible document event.
     * @since 3.1
     */
    private int fDeletedLines;


    /**
     * Creates a new projection source viewer.
     *
     * @param parent the DWT parent control
     * @param ruler the vertical ruler
     * @param overviewRuler the overview ruler
     * @param showsAnnotationOverview <code>true</code> if the overview ruler should be shown
     * @param styles the DWT style bits
     */
    public this(Composite parent, IVerticalRuler ruler, IOverviewRuler overviewRuler, bool showsAnnotationOverview, int styles) {

        fAnnotationModelListener= new AnnotationModelListener();
        fLock= new Object();
        fPendingRequests= new ArrayList();

        super(parent, ruler, overviewRuler, showsAnnotationOverview, styles);
    }

    /**
     * Sets the projection summary for this viewer.
     *
     * @param projectionSummary the projection summary.
     */
    public void setProjectionSummary(ProjectionSummary projectionSummary) {
        fProjectionSummary= projectionSummary;
    }

    /**
     * Adds the projection annotation model to the given annotation model.
     *
     * @param model the model to which the projection annotation model is added
     */
    private void addProjectionAnnotationModel(IAnnotationModel model) {
        if ( cast(IAnnotationModelExtension)model ) {
            IAnnotationModelExtension extension= cast(IAnnotationModelExtension) model;
            extension.addAnnotationModel(ProjectionSupport.PROJECTION, fProjectionAnnotationModel);
            model.addAnnotationModelListener(fAnnotationModelListener);
        }
    }

    /**
     * Removes the projection annotation model from the given annotation model.
     *
     * @param model the mode from which the projection annotation model is removed
     * @return the removed projection annotation model or <code>null</code> if there was none
     */
    private IAnnotationModel removeProjectionAnnotationModel(IAnnotationModel model) {
        if ( cast(IAnnotationModelExtension)model ) {
            model.removeAnnotationModelListener(fAnnotationModelListener);
            IAnnotationModelExtension extension= cast(IAnnotationModelExtension) model;
            return extension.removeAnnotationModel(ProjectionSupport.PROJECTION);
        }
        return null;
    }

    /*
     * @see dwtx.jface.text.source.SourceViewer#setDocument(dwtx.jface.text.IDocument, dwtx.jface.text.source.IAnnotationModel, int, int)
     */
    public void setDocument(IDocument document, IAnnotationModel annotationModel, int modelRangeOffset, int modelRangeLength) {
        bool wasProjectionEnabled= false;

        synchronized (fLock) {
            fPendingRequests.clear();
        }

        if (fProjectionAnnotationModel !is null) {
            wasProjectionEnabled= removeProjectionAnnotationModel(getVisualAnnotationModel()) !is null;
            fProjectionAnnotationModel= null;
        }

        super.setDocument(document, annotationModel, modelRangeOffset, modelRangeLength);

        if (wasProjectionEnabled && document !is null)
            enableProjection();
    }

    /*
     * @see dwtx.jface.text.source.SourceViewer#createVisualAnnotationModel(dwtx.jface.text.source.IAnnotationModel)
     */
    protected IAnnotationModel createVisualAnnotationModel(IAnnotationModel annotationModel) {
        IAnnotationModel model= super.createVisualAnnotationModel(annotationModel);
        fProjectionAnnotationModel= new ProjectionAnnotationModel();
        return model;
    }

    /**
     * Returns the projection annotation model.
     *
     * @return the projection annotation model
     */
    public ProjectionAnnotationModel getProjectionAnnotationModel() {
        IAnnotationModel model= getVisualAnnotationModel();
        if ( cast(IAnnotationModelExtension)model ) {
            IAnnotationModelExtension extension= cast(IAnnotationModelExtension) model;
            return cast(ProjectionAnnotationModel) extension.getAnnotationModel(ProjectionSupport.PROJECTION);
        }
        return null;
    }

    /*
     * @see dwtx.jface.text.TextViewer#createSlaveDocumentManager()
     */
    protected ISlaveDocumentManager createSlaveDocumentManager() {
        return new ProjectionDocumentManager();
    }

    /*
     * @see dwtx.jface.text.TextViewer#updateSlaveDocument(dwtx.jface.text.IDocument, int, int)
     */
    protected bool updateSlaveDocument(IDocument slaveDocument, int modelRangeOffset, int modelRangeLength)  {
        if ( cast(ProjectionDocument)slaveDocument ) {
            ProjectionDocument projection= cast(ProjectionDocument) slaveDocument;

            int offset= modelRangeOffset;
            int length= modelRangeLength;

            if (!isProjectionMode()) {
                // mimic original TextViewer behavior
                IDocument master= projection.getMasterDocument();
                int line= master.getLineOfOffset(modelRangeOffset);
                offset= master.getLineOffset(line);
                length= (modelRangeOffset - offset) + modelRangeLength;

            }

            try {
                fHandleProjectionChanges= false;
                projection.replaceMasterDocumentRanges(offset, length);
            } finally {
                fHandleProjectionChanges= true;
            }
            return true;
        }
        return false;
    }

    /**
     * Adds a projection annotation listener to this viewer. The listener may
     * not be <code>null</code>. If the listener is already registered, this method
     * does not have any effect.
     *
     * @param listener the listener to add
     */
    public void addProjectionListener(IProjectionListener listener) {

        Assert.isNotNull(cast(Object)listener);

        if (fProjectionListeners is null)
            fProjectionListeners= new ArrayList();

        if (!fProjectionListeners.contains(cast(Object)listener))
            fProjectionListeners.add(cast(Object)listener);
    }

    /**
     * Removes the given listener from this viewer. The listener may not be
     * <code>null</code>. If the listener is not registered with this viewer,
     * this method is without effect.
     *
     * @param listener the listener to remove
     */
    public void removeProjectionListener(IProjectionListener listener) {

        Assert.isNotNull(cast(Object)listener);

        if (fProjectionListeners !is null) {
            fProjectionListeners.remove(cast(Object)listener);
            if (fProjectionListeners.size() is 0)
                fProjectionListeners= null;
        }
    }

    /**
     * Notifies all registered projection listeners
     * that projection mode has been enabled.
     */
    protected void fireProjectionEnabled() {
        if (fProjectionListeners !is null) {
            Iterator e= (new ArrayList(fProjectionListeners)).iterator();
            while (e.hasNext()) {
                IProjectionListener l= cast(IProjectionListener) e.next();
                l.projectionEnabled();
            }
        }
    }

    /**
     * Notifies all registered projection listeners
     * that projection mode has been disabled.
     */
    protected void fireProjectionDisabled() {
        if (fProjectionListeners !is null) {
            Iterator e= (new ArrayList(fProjectionListeners)).iterator();
            while (e.hasNext()) {
                IProjectionListener l= cast(IProjectionListener) e.next();
                l.projectionDisabled();
            }
        }
    }

    /**
     * Returns whether this viewer is in projection mode.
     *
     * @return <code>true</code> if this viewer is in projection mode,
     *         <code>false</code> otherwise
     */
    public final bool isProjectionMode() {
        return getProjectionAnnotationModel() !is null;
    }

    /**
     * Disables the projection mode.
     */
    public final void disableProjection() {
        if (isProjectionMode()) {
            removeProjectionAnnotationModel(getVisualAnnotationModel());
            fProjectionAnnotationModel.removeAllAnnotations();
            fFindReplaceDocumentAdapter= null;
            fireProjectionDisabled();
        }
    }

    /**
     * Enables the projection mode.
     */
    public final void enableProjection() {
        if (!isProjectionMode()) {
            addProjectionAnnotationModel(getVisualAnnotationModel());
            fFindReplaceDocumentAdapter= null;
            fireProjectionEnabled();
        }
    }

    private void expandAll() {
        int offset= 0;
        IDocument doc= getDocument();
        int length= doc is null ? 0 : doc.getLength();
        if (isProjectionMode()) {
            fProjectionAnnotationModel.expandAll(offset, length);
        }
    }

    private void expand() {
        if (isProjectionMode()) {
            Position found= null;
            Annotation bestMatch= null;
            Point selection= getSelectedRange();
            for (Iterator e= fProjectionAnnotationModel.getAnnotationIterator(); e.hasNext();) {
                ProjectionAnnotation annotation= cast(ProjectionAnnotation) e.next();
                if (annotation.isCollapsed()) {
                    Position position= fProjectionAnnotationModel.getPosition(annotation);
                    // take the first most fine grained match
                    if (position !is null && touches(selection, position))
                        if (found is null || position.includes(found.offset) && position.includes(found.offset + found.length)) {
                            found= position;
                            bestMatch= annotation;
                        }
                }
            }

            if (bestMatch !is null) {
                fProjectionAnnotationModel.expand(bestMatch);
                revealRange(selection.x, selection.y);
            }
        }
    }

    private bool touches(Point selection, Position position) {
        return position.overlapsWith(selection.x, selection.y) || selection.y is 0 && position.offset + position.length is selection.x + selection.y;
    }

    private void collapse() {
        if (isProjectionMode()) {
            Position found= null;
            Annotation bestMatch= null;
            Point selection= getSelectedRange();
            for (Iterator e= fProjectionAnnotationModel.getAnnotationIterator(); e.hasNext();) {
                ProjectionAnnotation annotation= cast(ProjectionAnnotation) e.next();
                if (!annotation.isCollapsed()) {
                    Position position= fProjectionAnnotationModel.getPosition(annotation);
                    // take the first most fine grained match
                    if (position !is null && touches(selection, position))
                        if (found is null || found.includes(position.offset) && found.includes(position.offset + position.length)) {
                            found= position;
                            bestMatch= annotation;
                        }
                }
            }

            if (bestMatch !is null) {
                fProjectionAnnotationModel.collapse(bestMatch);
                revealRange(selection.x, selection.y);
            }
        }
    }

    /*
     * @since 3.2
     */
    private void collapseAll() {
        int offset= 0;
        IDocument doc= getDocument();
        int length= doc is null ? 0 : doc.getLength();
        if (isProjectionMode()) {
            fProjectionAnnotationModel.collapseAll(offset, length);
        }
    }

    /**
     * Adds the given master range to the given projection document. While the
     * modification is processed, the viewer no longer handles projection
     * changes, as it is causing them.
     *
     * @param projection the projection document
     * @param offset the offset in the master document
     * @param length the length in the master document
     * @throws BadLocationException in case the specified range is invalid
     *
     * @see ProjectionDocument#addMasterDocumentRange(int, int)
     */
    private void addMasterDocumentRange(ProjectionDocument projection, int offset, int length)  {

        if (fCommandQueue !is null) {
            fCommandQueue.add(new ProjectionCommand(projection, ProjectionCommand.ADD, offset, length));
        } else {
            try {
                fHandleProjectionChanges= false;
                // https://bugs.eclipse.org/bugs/show_bug.cgi?id=108258
                // make sure the document range is strictly line based
                int end= offset + length;
                offset= toLineStart(projection.getMasterDocument(), offset, false);
                length= toLineStart(projection.getMasterDocument(), end, true) - offset;
                projection.addMasterDocumentRange(offset, length);
            } finally {
                fHandleProjectionChanges= true;
            }
        }
    }

    /**
     * Removes the given master range from the given projection document. While the
     * modification is processed, the viewer no longer handles projection
     * changes, as it is causing them.
     *
     * @param projection the projection document
     * @param offset the offset in the master document
     * @param length the length in the master document
     * @throws BadLocationException in case the specified range is invalid
     *
     * @see ProjectionDocument#removeMasterDocumentRange(int, int)
     */
    private void removeMasterDocumentRange(ProjectionDocument projection, int offset, int length)  {
        if (fCommandQueue !is null) {
            fCommandQueue.add(new ProjectionCommand(projection, ProjectionCommand.REMOVE, offset, length));
        } else {
            try {
                fHandleProjectionChanges= false;
                // https://bugs.eclipse.org/bugs/show_bug.cgi?id=108258
                // make sure the document range is strictly line based
                int end= offset + length;
                offset= toLineStart(projection.getMasterDocument(), offset, false);
                length= toLineStart(projection.getMasterDocument(), end, true) - offset;
                projection.removeMasterDocumentRange(offset, length);
            } finally {
                fHandleProjectionChanges= true;
            }
        }
    }

    /**
     * Returns the first line offset &lt;= <code>offset</code>. If <code>testLastLine</code>
     * is <code>true</code> and the offset is on last line then <code>offset</code> is returned.
     *
     * @param document the document
     * @param offset the master document offset
     * @param testLastLine <code>true</code> if the test for the last line should be performed
     * @return the closest line offset &gt;= <code>offset</code>
     * @throws BadLocationException if the offset is invalid
     * @since 3.2
     */
    private int toLineStart(IDocument document, int offset, bool testLastLine)  {
        if (document is null)
            return offset;

        if (testLastLine && offset >= document.getLineInformationOfOffset(document.getLength() - 1).getOffset())
            return offset;

        return document.getLineInformationOfOffset(offset).getOffset();
    }

    /*
     * @see dwtx.jface.text.TextViewer#setVisibleRegion(int, int)
     */
    public void setVisibleRegion(int start, int length) {
        fWasProjectionEnabled= isProjectionMode();
        disableProjection();
        super.setVisibleRegion(start, length);
    }

    /*
     * @see dwtx.jface.text.TextViewer#setVisibleDocument(dwtx.jface.text.IDocument)
     */
    protected void setVisibleDocument(IDocument document) {
        if (!isProjectionMode()) {
            super.setVisibleDocument(document);
            return;
        }

        // In projection mode we don't want to throw away the find/replace document adapter
        FindReplaceDocumentAdapter adapter= fFindReplaceDocumentAdapter;
        super.setVisibleDocument(document);
        fFindReplaceDocumentAdapter= adapter;
    }

    /*
     * @see dwtx.jface.text.TextViewer#resetVisibleRegion()
     */
    public void resetVisibleRegion() {
        super.resetVisibleRegion();
        if (fWasProjectionEnabled)
            enableProjection();
    }

    /*
     * @see dwtx.jface.text.ITextViewer#getVisibleRegion()
     */
    public IRegion getVisibleRegion() {
        disableProjection();
        IRegion visibleRegion= getModelCoverage();
        if (visibleRegion is null)
            visibleRegion= new Region(0, 0);

        return visibleRegion;
    }

    /*
     * @see dwtx.jface.text.ITextViewer#overlapsWithVisibleRegion(int,int)
     */
    public bool overlapsWithVisibleRegion(int offset, int length) {
        disableProjection();
        IRegion coverage= getModelCoverage();
        if (coverage is null)
            return false;

        bool appending= (offset is coverage.getOffset() + coverage.getLength()) && length is 0;
        return appending || TextUtilities.overlaps(coverage, new Region(offset, length));
    }

    /**
     * Replace the visible document with the given document. Maintains the
     * scroll offset and the selection.
     *
     * @param slave the visible document
     */
    private void replaceVisibleDocument(IDocument slave) {
        if (fReplaceVisibleDocumentExecutionTrigger !is null) {
            ReplaceVisibleDocumentExecutor executor= new ReplaceVisibleDocumentExecutor(slave);
            executor.install(fReplaceVisibleDocumentExecutionTrigger);
        } else
            executeReplaceVisibleDocument(slave);
    }


    private void executeReplaceVisibleDocument(IDocument visibleDocument) {
        StyledText textWidget= getTextWidget();
        try {
            if (textWidget !is null && !textWidget.isDisposed())
                textWidget.setRedraw(false);

            int topIndex= getTopIndex();
            Point selection= getSelectedRange();
            setVisibleDocument(visibleDocument);
            Point currentSelection= getSelectedRange();
            if (currentSelection.x !is selection.x || currentSelection.y !is selection.y)
                setSelectedRange(selection.x, selection.y);
            setTopIndex(topIndex);

        } finally {
            if (textWidget !is null && !textWidget.isDisposed())
                textWidget.setRedraw(true);
        }
    }

    /**
     * Hides the given range by collapsing it. If requested, a redraw request is issued.
     *
     * @param offset the offset of the range to hide
     * @param length the length of the range to hide
     * @param fireRedraw <code>true</code> if a redraw request should be issued, <code>false</code> otherwise
     * @throws BadLocationException in case the range is invalid
     */
    private void collapse(int offset, int length, bool fireRedraw)  {
        ProjectionDocument projection= null;

        IDocument visibleDocument= getVisibleDocument();
        if ( cast(ProjectionDocument)visibleDocument )
            projection= cast(ProjectionDocument) visibleDocument;
        else {
            IDocument master= getDocument();
            IDocument slave= createSlaveDocument(getDocument());
            if ( cast(ProjectionDocument)slave ) {
                projection= cast(ProjectionDocument) slave;
                addMasterDocumentRange(projection, 0, master.getLength());
                replaceVisibleDocument(projection);
            }
        }

        if (projection !is null)
            removeMasterDocumentRange(projection, offset, length);

        if (projection !is null && fireRedraw) {
            // repaint line above to get the folding box
            IDocument document= getDocument();
            int line= document.getLineOfOffset(offset);
            if (line > 0) {
                IRegion info= document.getLineInformation(line - 1);
                internalInvalidateTextPresentation(info.getOffset(), info.getLength());
            }
        }
    }

    /**
     * Makes the given range visible again while not changing the folding state of any contained
     * ranges. If requested, a redraw request is issued.
     *
     * @param offset the offset of the range to be expanded
     * @param length the length of the range to be expanded
     * @param fireRedraw <code>true</code> if a redraw request should be issued,
     *        <code>false</code> otherwise
     * @throws BadLocationException in case the range is invalid
     */
    private void expand(int offset, int length, bool fireRedraw)  {
        IDocument slave= getVisibleDocument();
        if ( cast(ProjectionDocument)slave ) {
            ProjectionDocument projection= cast(ProjectionDocument) slave;

            // expand
            addMasterDocumentRange(projection, offset, length);

            // collapse contained regions
            ProjectionAnnotation[] collapsed= computeCollapsedNestedAnnotations(offset, length);
            if (collapsed !is null) {
                for (int i= 0; i < collapsed.length; i++) {
                    IRegion[] regions= computeCollapsedRegions(fProjectionAnnotationModel.getPosition(collapsed[i]));
                    if (regions !is null)
                        for (int j= 0; j < regions.length; j++)
                            removeMasterDocumentRange(projection, regions[j].getOffset(), regions[j].getLength());
                }
            }

            // redraw if requested
            if (fireRedraw)
                internalInvalidateTextPresentation(offset, length);
        }
    }

    /**
     * Processes the request for catch up with the annotation model in the UI thread. If the current
     * thread is not the UI thread or there are pending catch up requests, a new request is posted.
     *
     * @param event the annotation model event
     */
    protected final void processCatchupRequest(AnnotationModelEvent event) {
        if (Display.getCurrent() !is null) {
            bool run= false;
            synchronized (fLock) {
                run= fPendingRequests.isEmpty();
            }
            if (run) {

                try {
                    catchupWithProjectionAnnotationModel(event);
                } catch (BadLocationException x) {
                    throw new IllegalArgumentException(null);
                }

            } else
                postCatchupRequest(event);
        } else {
            postCatchupRequest(event);
        }
    }

    /**
     * Posts the request for catch up with the annotation model into the UI thread.
     *
     * @param event the annotation model event
     */
    protected final void postCatchupRequest(AnnotationModelEvent event) {
        synchronized (fLock) {
            fPendingRequests.add(event);
            if (fPendingRequests.size() is 1) {
                StyledText widget= getTextWidget();
                if (widget !is null) {
                    Display display= widget.getDisplay();
                    if (display !is null) {
                        display.asyncExec(new class()  Runnable {
                            public void run() {
                                try {
                                    while (true) {
                                        AnnotationModelEvent ame= null;
                                        synchronized (fLock) {
                                            if (fPendingRequests.size() is 0)
                                                return;
                                            ame= cast(AnnotationModelEvent) fPendingRequests.remove(0);
                                        }
                                        catchupWithProjectionAnnotationModel(ame);
                                    }
                                } catch (BadLocationException x) {
                                    try {
                                        catchupWithProjectionAnnotationModel(null);
                                    } catch (BadLocationException x1) {
                                        throw new IllegalArgumentException(null);
                                    } finally {
                                        synchronized (fLock) {
                                            fPendingRequests.clear();
                                        }
                                    }
                                }
                            }
                        });
                    }
                }
            }
        }
    }

    /**
     * Tests whether the visible document's master document
     * is identical to this viewer's document.
     *
     * @return <code>true</code> if the visible document's master is
     *          identical to this viewer's document
     * @since 3.1
     */
    private bool isVisibleMasterDocumentSameAsDocument() {
        IDocument visibleDocument= getVisibleDocument();
        return ( cast(ProjectionDocument)visibleDocument ) && (cast(ProjectionDocument)visibleDocument).getMasterDocument() is getDocument();
    }

    /**
     * Adapts the slave visual document of this viewer to the changes described
     * in the annotation model event. When the event is <code>null</code>,
     * this is identical to a world change event.
     *
     * @param event the annotation model event or <code>null</code>
     * @exception BadLocationException in case the annotation model event is no longer in synchronization with the document
     */
    private void catchupWithProjectionAnnotationModel(AnnotationModelEvent event)  {

        if (event is null || !isVisibleMasterDocumentSameAsDocument()) {

            fPendingAnnotationWorldChange= false;
            reinitializeProjection();

        } else if (event.isWorldChange()) {

            if (event.isValid()) {
                fPendingAnnotationWorldChange= false;
                reinitializeProjection();
            } else
                fPendingAnnotationWorldChange= true;

        } else if (fPendingAnnotationWorldChange) {
            if (event.isValid()) {
                fPendingAnnotationWorldChange= false;
                reinitializeProjection();
            }
        } else {

            Annotation[] addedAnnotations= event.getAddedAnnotations();
            Annotation[] changedAnnotation= event.getChangedAnnotations();
            Annotation[] removedAnnotations= event.getRemovedAnnotations();

            fCommandQueue= new ProjectionCommandQueue();

            bool isRedrawing= redraws();
            int topIndex= isRedrawing ? getTopIndex() : -1;

            processDeletions(event, removedAnnotations, true);
            List coverage= new ArrayList();
            processChanges(addedAnnotations, true, coverage);
            processChanges(changedAnnotation, true, coverage);

            ProjectionCommandQueue commandQueue= fCommandQueue;
            fCommandQueue= null;

            if (commandQueue.passedRedrawCostsThreshold()) {
                setRedraw(false);
                try {
                    executeProjectionCommands(commandQueue, false);
                } catch (IllegalArgumentException x) {
                    reinitializeProjection();
                } finally {
                    setRedraw(true, topIndex);
                }
            } else {
                try {
                    bool fireRedraw= !commandQueue.passedInvalidationCostsThreshold();
                    executeProjectionCommands(commandQueue, fireRedraw);
                    if (!fireRedraw)
                        invalidateTextPresentation();
                } catch (IllegalArgumentException x) {
                    reinitializeProjection();
                }
            }
        }
    }

    private void executeProjectionCommands(ProjectionCommandQueue commandQueue, bool fireRedraw)  {

        ProjectionCommand command;
        Iterator e= commandQueue.iterator();
        while (e.hasNext()) {
            command= cast(ProjectionCommand) e.next();
            switch (command.fType) {
                case ProjectionCommand.ADD:
                    addMasterDocumentRange(command.fProjection, command.fOffset, command.fLength);
                    break;
                case ProjectionCommand.REMOVE:
                    removeMasterDocumentRange(command.fProjection, command.fOffset, command.fLength);
                    break;
                case ProjectionCommand.INVALIDATE_PRESENTATION:
                    if (fireRedraw)
                        invalidateTextPresentation(command.fOffset, command.fLength);
                    break;
                default:
            }
        }

        commandQueue.clear();
    }

    private bool covers(int offset, int length, Position position) {
        if (!(position.offset is offset && position.length is length) && !position.isDeleted())
            return offset <= position.getOffset() && position.getOffset() + position.getLength() <= offset + length;
        return false;
    }

    private ProjectionAnnotation[] computeCollapsedNestedAnnotations(int offset, int length) {
        List annotations= new ArrayList(5);
        Iterator e= fProjectionAnnotationModel.getAnnotationIterator();
        while (e.hasNext()) {
            ProjectionAnnotation annotation= cast(ProjectionAnnotation) e.next();
            if (annotation.isCollapsed()) {
                Position position= fProjectionAnnotationModel.getPosition(annotation);
                if (position is null) {
                    // annotation might already be deleted, we will be informed later on about this deletion
                    continue;
                }
                if (covers(offset, length, position))
                    annotations.add(annotation);
            }
        }

        if (annotations.size() > 0) {
            ProjectionAnnotation[] result= new ProjectionAnnotation[annotations.size()];
            annotations.toArray(result);
            return result;
        }

        return null;
    }

    private void internalInvalidateTextPresentation(int offset, int length) {
        if (fCommandQueue !is null) {
            fCommandQueue.add(new ProjectionCommand(offset, length));
        } else {
            invalidateTextPresentation(offset, length);
        }
    }

    /*
     * We pass the removed annotation into this method for performance reasons only. Otherwise, they could be fetch from the event.
     */
    private void processDeletions(AnnotationModelEvent event, Annotation[] removedAnnotations, bool fireRedraw)  {
        for (int i= 0; i < removedAnnotations.length; i++) {
            ProjectionAnnotation annotation= cast(ProjectionAnnotation) removedAnnotations[i];
            if (annotation.isCollapsed()) {
                Position expanded= event.getPositionOfRemovedAnnotation(annotation);
                expand(expanded.getOffset(), expanded.getLength(), fireRedraw);
            }
        }
    }

    /**
     * Computes the region that must be collapsed when the given position is the
     * position of an expanded projection annotation.
     *
     * @param position the position
     * @return the range that must be collapsed
     */
    public IRegion computeCollapsedRegion(Position position) {
        try {
            IDocument document= getDocument();
            if (document is null)
                return null;

            int line= document.getLineOfOffset(position.getOffset());
            int offset= document.getLineOffset(line + 1);

            int length= position.getLength() - (offset - position.getOffset());
            if (length > 0)
                return new Region(offset, length);
        } catch (BadLocationException x) {
        }

        return null;
    }

    /**
     * Computes the regions that must be collapsed when the given position is
     * the position of an expanded projection annotation.
     *
     * @param position the position
     * @return the ranges that must be collapsed, or <code>null</code> if
     *         there are none
     * @since 3.1
     */
    IRegion[] computeCollapsedRegions(Position position) {
        try {
            IDocument document= getDocument();
            if (document is null)
                return null;

            if ( cast(IProjectionPosition)position ) {
                IProjectionPosition projPosition= cast(IProjectionPosition) position;
                return projPosition.computeProjectionRegions(document);
            }

            int line= document.getLineOfOffset(position.getOffset());
            int offset= document.getLineOffset(line + 1);

            int length= position.getLength() - (offset - position.getOffset());
            if (length > 0)
                return [new Region(offset, length)];

            return null;
        } catch (BadLocationException x) {
            return null;
        }
    }

    /**
     * Computes the collapsed region anchor for the given position. Assuming
     * that the position is the position of an expanded projection annotation,
     * the anchor is the region that is still visible after the projection
     * annotation has been collapsed.
     *
     * @param position the position
     * @return the collapsed region anchor
     */
    public Position computeCollapsedRegionAnchor(Position position) {
        try {
            IDocument document= getDocument();
            if (document is null)
                return null;

            int captionOffset= position.getOffset();
            if ( cast(IProjectionPosition)position )
                captionOffset+= (cast(IProjectionPosition) position).computeCaptionOffset(document);

            IRegion lineInfo= document.getLineInformationOfOffset(captionOffset);
            return new Position(lineInfo.getOffset() + lineInfo.getLength(), 0);
        } catch (BadLocationException x) {
        }
        return null;
    }

    private void processChanges(Annotation[] annotations, bool fireRedraw, List coverage)  {
        for (int i= 0; i < annotations.length; i++) {
            ProjectionAnnotation annotation= cast(ProjectionAnnotation) annotations[i];
            Position position= fProjectionAnnotationModel.getPosition(annotation);

            if (position is null)
                continue;

            if (!covers(coverage, position)) {
                if (annotation.isCollapsed()) {
                    coverage.add(position);
                    IRegion[] regions= computeCollapsedRegions(position);
                    if (regions !is null)
                        for (int j= 0; j < regions.length; j++)
                            collapse(regions[j].getOffset(), regions[j].getLength(), fireRedraw);
                } else {
                    expand(position.getOffset(), position.getLength(), fireRedraw);
                }
            }
        }
    }

    private bool covers(List coverage, Position position) {
        Iterator e= coverage.iterator();
        while (e.hasNext()) {
            Position p= cast(Position) e.next();
            if (p.getOffset() <= position.getOffset() && position.getOffset() + position.getLength() <= p.getOffset() + p.getLength())
                return true;
        }
        return false;
    }

    /**
     * Forces this viewer to throw away any old state and to initialize its content
     * from its projection annotation model.
     *
     * @throws BadLocationException in case something goes wrong during initialization
     */
    public final void reinitializeProjection()  {

        ProjectionDocument projection= null;

        ISlaveDocumentManager manager= getSlaveDocumentManager();
        if (manager !is null) {
            IDocument master= getDocument();
            if (master !is null) {
                IDocument slave= manager.createSlaveDocument(master);
                if ( cast(ProjectionDocument)slave ) {
                    projection= cast(ProjectionDocument) slave;
                    addMasterDocumentRange(projection, 0, master.getLength());
                }
            }
        }

        if (projection !is null) {
            Iterator e= fProjectionAnnotationModel.getAnnotationIterator();
            while (e.hasNext()) {
                ProjectionAnnotation annotation= cast(ProjectionAnnotation) e.next();
                if (annotation.isCollapsed()) {
                    Position position= fProjectionAnnotationModel.getPosition(annotation);
                    if (position !is null) {
                        IRegion[] regions= computeCollapsedRegions(position);
                        if (regions !is null)
                            for (int i= 0; i < regions.length; i++)
                                removeMasterDocumentRange(projection, regions[i].getOffset(), regions[i].getLength());
                    }
                }
            }

        }

        replaceVisibleDocument(projection);
    }

    /*
     * @see dwtx.jface.text.TextViewer#handleVerifyEvent(dwt.events.VerifyEvent)
     */
    protected void handleVerifyEvent(VerifyEvent e) {
        IRegion modelRange= event2ModelRange(e);
        if (exposeModelRange(modelRange))
            e.doit= false;
        else
            super.handleVerifyEvent(e);
    }

    /**
     * Adds the give column as last column to this viewer's vertical ruler.
     *
     * @param column the column to be added
     */
    public void addVerticalRulerColumn(IVerticalRulerColumn column) {
        IVerticalRuler ruler= getVerticalRuler();
        if ( cast(CompositeRuler)ruler ) {
            CompositeRuler compositeRuler= cast(CompositeRuler) ruler;
            compositeRuler.addDecorator(99, column);
        }
    }

    /**
     * Removes the give column from this viewer's vertical ruler.
     *
     * @param column the column to be removed
     */
    public void removeVerticalRulerColumn(IVerticalRulerColumn column) {
        IVerticalRuler ruler= getVerticalRuler();
        if ( cast(CompositeRuler)ruler ) {
            CompositeRuler compositeRuler= cast(CompositeRuler) ruler;
            compositeRuler.removeDecorator(column);
        }
    }

    /*
     * @see dwtx.jface.text.ITextViewerExtension5#exposeModelRange(dwtx.jface.text.IRegion)
     */
    public bool exposeModelRange(IRegion modelRange) {
        if (isProjectionMode())
            return fProjectionAnnotationModel.expandAll(modelRange.getOffset(), modelRange.getLength());

        if (!overlapsWithVisibleRegion(modelRange.getOffset(), modelRange.getLength())) {
            resetVisibleRegion();
            return true;
        }

        return false;
    }

    /*
     * @see dwtx.jface.text.source.SourceViewer#setRangeIndication(int, int, bool)
     */
    public void setRangeIndication(int offset, int length, bool moveCursor) {
        IRegion rangeIndication= getRangeIndication();
        if (moveCursor && fProjectionAnnotationModel !is null && (rangeIndication is null || offset !is rangeIndication.getOffset() || length !is rangeIndication.getLength())) {
            List expand= new ArrayList(2);
            // expand the immediate effected collapsed regions
            Iterator iterator= fProjectionAnnotationModel.getAnnotationIterator();
            while (iterator.hasNext()) {
                ProjectionAnnotation annotation= cast(ProjectionAnnotation)iterator.next();
                if (annotation.isCollapsed() && willAutoExpand(fProjectionAnnotationModel.getPosition(annotation), offset, length))
                    expand.add(annotation);
            }

            if (!expand.isEmpty()) {
                Iterator e= expand.iterator();
                while (e.hasNext())
                    fProjectionAnnotationModel.expand(cast(Annotation)e.next());
            }
        }
        super.setRangeIndication(offset, length, moveCursor);
    }

    private bool willAutoExpand(Position position, int offset, int length) {
        if (position is null || position.isDeleted())
            return false;
        // right or left boundary
        if (position.getOffset() is offset || position.getOffset() + position.getLength() is offset + length)
            return true;
        // completely embedded in given position
        if (position.getOffset() < offset && offset + length < position.getOffset() + position.getLength())
            return true;
        return false;
    }

    /*
     * @see dwtx.jface.text.source.SourceViewer#handleDispose()
     * @since 3.0
     */
    protected void handleDispose() {
        fWasProjectionEnabled= false;
        super.handleDispose();
    }

    /*
     * @see dwtx.jface.text.TextViewer#handleVisibleDocumentAboutToBeChanged(dwtx.jface.text.DocumentEvent)
     */
    protected void handleVisibleDocumentChanged(DocumentEvent event) {
        if (fHandleProjectionChanges && cast(ProjectionDocumentEvent)event  && isProjectionMode()) {
            ProjectionDocumentEvent e= cast(ProjectionDocumentEvent) event;

            DocumentEvent master= e.getMasterEvent();
            if (master !is null)
                fReplaceVisibleDocumentExecutionTrigger= master.getDocument();

            try {

                int replaceLength= e.getText() is null ? 0 : e.getText().length();
                if (ProjectionDocumentEvent.PROJECTION_CHANGE is e.getChangeType()) {
                    if (e.getLength() is 0 && replaceLength !is 0)
                        fProjectionAnnotationModel.expandAll(e.getMasterOffset(), e.getMasterLength());
                } else if (master !is null && (replaceLength > 0 || fDeletedLines > 1)) {
                    try {
                        int numberOfLines= e.getDocument().getNumberOfLines(e.getOffset(), replaceLength);
                        if (numberOfLines > 1 || fDeletedLines > 1)
                            fProjectionAnnotationModel.expandAll(master.getOffset(), master.getLength());
                    } catch (BadLocationException x) {
                    }
                }

            } finally {
                fReplaceVisibleDocumentExecutionTrigger= null;
            }

        }
    }

    /*
     * @see dwtx.jface.text.TextViewer#handleVisibleDocumentAboutToBeChanged(dwtx.jface.text.DocumentEvent)
     * @since 3.1
     */
    protected void handleVisibleDocumentAboutToBeChanged(DocumentEvent event) {
        if (fHandleProjectionChanges && cast(ProjectionDocumentEvent)event  && isProjectionMode()) {
            int deletedLines;
            try {
                deletedLines= event.getDocument().getNumberOfLines(event.getOffset(), event.getLength());
            } catch (BadLocationException e1) {
                deletedLines= 0;
            }
            fDeletedLines= deletedLines;
        }
    }

    /*
     * @see dwtx.jface.text.ITextViewerExtension5#getCoveredModelRanges(dwtx.jface.text.IRegion)
     */
    public IRegion[] getCoveredModelRanges(IRegion modelRange) {
        if (fInformationMapping is null)
            return [ new Region(modelRange.getOffset(), modelRange.getLength()) ];

        if ( cast(IDocumentInformationMappingExtension)fInformationMapping ) {
            IDocumentInformationMappingExtension extension= cast(IDocumentInformationMappingExtension) fInformationMapping;
            try {
                return extension.getExactCoverage(modelRange);
            } catch (BadLocationException x) {
            }
        }

        return null;
    }

    /*
     * @see dwtx.jface.text.ITextOperationTarget#doOperation(int)
     */
    public void doOperation(int operation) {
        switch (operation) {
            case TOGGLE:
                if (canDoOperation(TOGGLE)) {
                    if (!isProjectionMode()) {
                        enableProjection();
                    } else {
                        expandAll();
                        disableProjection();
                    }
                    return;
                }
            default:
        }

        if (!isProjectionMode()) {
            super.doOperation(operation);
            return;
        }

        StyledText textWidget= getTextWidget();
        if (textWidget is null)
            return;

        Point selection= null;
        switch (operation) {

            case CUT:

                if (redraws()) {
                    selection= getSelectedRange();
                    if (exposeModelRange(new Region(selection.x, selection.y)))
                        return;

                    if (selection.y is 0)
                        copyMarkedRegion(true);
                    else
                        copyToClipboard(selection.x, selection.y, true, textWidget);

                    selection= textWidget.getSelectionRange();
                    fireSelectionChanged(selection.x, selection.y);
                }
                break;

            case COPY:

                if (redraws()) {
                    selection= getSelectedRange();
                    if (selection.y is 0)
                        copyMarkedRegion(false);
                    else
                        copyToClipboard(selection.x, selection.y, false, textWidget);
                }
                break;

            case DELETE:

                if (redraws()) {
                    try {
                        selection= getSelectedRange();
                        Point widgetSelection= textWidget.getSelectionRange();
                        if (selection.y is 0 || selection.y is widgetSelection.y)
                            getTextWidget().invokeAction(ST.DELETE_NEXT);
                        else
                            deleteTextRange(selection.x, selection.y, textWidget);

                        selection= textWidget.getSelectionRange();
                        fireSelectionChanged(selection.x, selection.y);

                    } catch (BadLocationException x) {
                        // ignore
                    }
                }
                break;


            case EXPAND_ALL:
                if (redraws())
                    expandAll();
                break;

            case EXPAND:
                if (redraws()) {
                    expand();
                }
                break;

            case COLLAPSE_ALL:
                if (redraws())
                    collapseAll();
                break;

            case COLLAPSE:
                if (redraws()) {
                    collapse();
                }
                break;

            default:
                super.doOperation(operation);
        }
    }

    /*
     * @see dwtx.jface.text.source.SourceViewer#canDoOperation(int)
     */
    public bool canDoOperation(int operation) {

        switch (operation) {
            case COLLAPSE:
            case COLLAPSE_ALL:
            case EXPAND:
            case EXPAND_ALL:
                return isProjectionMode();
            case TOGGLE:
                return isProjectionMode() || !isSegmented();
            default:
        }

        return super.canDoOperation(operation);
    }

    private bool isSegmented() {
        IDocument document= getDocument();
        int length= document is null ? 0 : document.getLength();
        IRegion visible= getModelCoverage();
        bool isSegmented= visible !is null && !(cast(Object)visible).opEquals(new Region(0, length));
        return isSegmented;
    }

    private IRegion getMarkedRegion() {
        if (getTextWidget() is null)
            return null;

        if (fMarkPosition is null || fMarkPosition.isDeleted())
            return null;

        int start= fMarkPosition.getOffset();
        int end= getSelectedRange().x;

        return start > end ? new Region (end, start - end) : new Region(start, end - start);
    }

    /*
     * @see dwtx.jface.text.TextViewer#copyMarkedRegion(bool)
     */
    protected void copyMarkedRegion(bool delete_) {
        IRegion markedRegion= getMarkedRegion();
        if (markedRegion !is null)
            copyToClipboard(markedRegion.getOffset(), markedRegion.getLength(), delete_, getTextWidget());
    }

    private void copyToClipboard(int offset, int length, bool delete_, StyledText textWidget) {

        String copyText= null;

        try {
            IDocument document= getDocument();
            copyText= document.get(offset, length);
        } catch (BadLocationException ex) {
            // XXX: should log here, but JFace Text has no Log
            // As a fallback solution let the widget handle this
            textWidget.copy();
        }

        if (copyText !is null && copyText.equals(textWidget.getSelectionText())) {
            /*
             * XXX: Reduce pain of https://bugs.eclipse.org/bugs/show_bug.cgi?id=64498
             * by letting the widget handle the copy operation in this special case.
             */
            textWidget.copy();
        } else if (copyText !is null) {

            Clipboard clipboard= new Clipboard(textWidget.getDisplay());

            try {
                Transfer[] dataTypes= [ TextTransfer.getInstance() ];
                Object[] data= [ stringcast(copyText) ];
                try {
                    clipboard.setContents(data, dataTypes);
                } catch (DWTError e) {
                    if (e.code !is DND.ERROR_CANNOT_SET_CLIPBOARD)
                        throw e;
                    /*
                     * TODO see https://bugs.eclipse.org/bugs/show_bug.cgi?id=59459
                     * we should either log and/or inform the user
                     * silently fail for now.
                     */
                    return;
                }

            } finally {
                clipboard.dispose();
            }
        }

        if (delete_) {
            try {
                deleteTextRange(offset, length, textWidget);
            } catch (BadLocationException x) {
                // XXX: should log here, but JFace Text has no Log
            }
        }
    }

    private void deleteTextRange(int offset, int length, StyledText textWidget)  {
        getDocument().replace(offset, length, ""); //$NON-NLS-1$
        int widgetCaret= modelOffset2WidgetOffset(offset);
        if (widgetCaret > -1)
            textWidget.setSelection(widgetCaret);
    }

    /**
     * Adapts the behavior of the super class to respect line based folding.
     *
     * @param widgetSelection the widget selection
     * @return the model selection while respecting line based folding
     */
    protected Point widgetSelection2ModelSelection(Point widgetSelection) {

        if (!isProjectionMode())
            return super.widgetSelection2ModelSelection(widgetSelection);

        /*
         * There is one requirement that governs preservation of logical
         * positions:
         *
         * 1) a selection with widget_length is 0 should never expand to have
         * model_length > 0.
         *
         * There are a number of ambiguities to resolve with projection regions.
         * A projected region P has a widget-length of zero. Its widget offset
         * may interact with the selection S in various ways:
         *
         * A) P.widget_offset lies at the caret, S.widget_length is zero.
         * Requirement 1 applies. S is *behind* P (done so by widgetRange2ModelRange).
         *
         * B) P.widget_offset lies inside the widget selection. This case is
         * easy: P is included in S, which is automatically done so by
         * widgetRange2ModelRange.
         *
         * C) P.widget_offset lies at S.widget_end: This is
         * arguable - our policy is to include P if it belongs to a projection
         * annotation that overlaps with the widget selection.
         *
         * D) P.widget_offset lies at S.widget_offset: Arguable - our policy
         * is to include P if it belongs to a projection annotation that
         * overlaps with the widget selection
         */
        IRegion modelSelection= widgetRange2ModelRange(new Region(widgetSelection.x, widgetSelection.y));
        if (modelSelection is null)
            return null;

        int modelOffset= modelSelection.getOffset();
        int modelEndOffset= modelOffset + modelSelection.getLength();

        /* Case A: never expand a zero-length selection. S is *behind* P. */
        if (widgetSelection.y is 0)
            return new Point(modelEndOffset, 0);

        int widgetSelectionExclusiveEnd= widgetSelection.x + widgetSelection.y;
        Position[] annotationPositions= computeOverlappingAnnotationPositions(modelSelection);
        for (int i= 0; i < annotationPositions.length; i++) {
            IRegion[] regions= computeCollapsedRegions(annotationPositions[i]);
            if (regions is null)
                continue;
            for (int j= 0; j < regions.length; j++) {
                IRegion modelRange= regions[j];
                IRegion widgetRange= modelRange2ClosestWidgetRange(modelRange);
                // only take collapsed ranges, i.e. widget length is 0
                if (widgetRange !is null && widgetRange.getLength() is 0) {
                    int widgetOffset= widgetRange.getOffset();
                    // D) region is collapsed at S.widget_offset
                    if (widgetOffset is widgetSelection.x)
                        modelOffset= Math.min(modelOffset, modelRange.getOffset());
                    // C) region is collapsed at S.widget_end
                    else if (widgetOffset is widgetSelectionExclusiveEnd)
                        modelEndOffset= Math.max(modelEndOffset, modelRange.getOffset() + modelRange.getLength());
                }
            }
        }
        return new Point(modelOffset, modelEndOffset - modelOffset);
    }

    /**
     * Returns the positions of all annotations that intersect with
     * <code>modelSelection</code> and that are at least partly visible.
     * @param modelSelection a model range
     * @return the positions of all annotations that intersect with
     *         <code>modelSelection</code>
     * @since 3.1
     */
    private Position[] computeOverlappingAnnotationPositions(IRegion modelSelection) {
        List positions= new ArrayList();
        for (Iterator e= fProjectionAnnotationModel.getAnnotationIterator(); e.hasNext();) {
            ProjectionAnnotation annotation= cast(ProjectionAnnotation) e.next();
            Position position= fProjectionAnnotationModel.getPosition(annotation);
            if (position !is null && position.overlapsWith(modelSelection.getOffset(), modelSelection.getLength()) && modelRange2WidgetRange(position) !is null)
                positions.add(position);
        }
        return arraycast!(Position)( positions.toArray());
    }

    /*
     * @see dwtx.jface.text.TextViewer#getFindReplaceDocumentAdapter()
     */
    protected FindReplaceDocumentAdapter getFindReplaceDocumentAdapter() {
        if (fFindReplaceDocumentAdapter is null) {
            IDocument document= isProjectionMode() ? getDocument() : getVisibleDocument();
            fFindReplaceDocumentAdapter= new FindReplaceDocumentAdapter(document);
        }
        return fFindReplaceDocumentAdapter;
    }

    /*
     * @see dwtx.jface.text.TextViewer#findAndSelect(int, java.lang.String, bool, bool, bool, bool)
     */
    protected int findAndSelect(int startPosition, String findString, bool forwardSearch, bool caseSensitive, bool wholeWord, bool regExSearch) {

        if (!isProjectionMode())
            return super.findAndSelect(startPosition, findString, forwardSearch, caseSensitive, wholeWord, regExSearch);

        StyledText textWidget= getTextWidget();
        if (textWidget is null)
            return -1;

        try {

            IRegion matchRegion= getFindReplaceDocumentAdapter().find(startPosition, findString, forwardSearch, caseSensitive, wholeWord, regExSearch);
            if (matchRegion !is null) {
                exposeModelRange(matchRegion);
                revealRange(matchRegion.getOffset(), matchRegion.getLength());
                setSelectedRange(matchRegion.getOffset(), matchRegion.getLength());
                return matchRegion.getOffset();
            }

        } catch (BadLocationException x) {
        }

        return -1;
    }

    /*
     * @see dwtx.jface.text.TextViewer#findAndSelectInRange(int, java.lang.String, bool, bool, bool, int, int, bool)
     */
    protected int findAndSelectInRange(int startPosition, String findString, bool forwardSearch, bool caseSensitive, bool wholeWord, int rangeOffset, int rangeLength, bool regExSearch) {

        if (!isProjectionMode())
            return super.findAndSelectInRange(startPosition, findString, forwardSearch, caseSensitive, wholeWord, rangeOffset, rangeLength, regExSearch);

        StyledText textWidget= getTextWidget();
        if (textWidget is null)
            return -1;

        try {

            int modelOffset= startPosition;
            if (forwardSearch && (startPosition is -1 || startPosition < rangeOffset)) {
                modelOffset= rangeOffset;
            } else if (!forwardSearch && (startPosition is -1 || startPosition > rangeOffset + rangeLength)) {
                modelOffset= rangeOffset + rangeLength;
            }

            IRegion matchRegion= getFindReplaceDocumentAdapter().find(modelOffset, findString, forwardSearch, caseSensitive, wholeWord, regExSearch);
            if (matchRegion !is null) {
                int offset= matchRegion.getOffset();
                int length= matchRegion.getLength();
                if (rangeOffset <= offset && offset + length <= rangeOffset + rangeLength) {
                    exposeModelRange(matchRegion);
                    revealRange(offset, length);
                    setSelectedRange(offset, length);
                    return offset;
                }
            }

        } catch (BadLocationException x) {
        }

        return -1;
    }
}