diff dwtx/jface/text/source/projection/ProjectionViewer.d @ 129:eb30df5ca28b

Added JFace Text sources
author Frank Benoit <benoit@tionex.de>
date Sat, 23 Aug 2008 19:10:48 +0200
parents
children c4fb132a086c
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/dwtx/jface/text/source/projection/ProjectionViewer.d	Sat Aug 23 19:10:48 2008 +0200
@@ -0,0 +1,1788 @@
+/*******************************************************************************
+ * 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 dwt.dwthelper.utils;
+
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+
+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 {
+
+    private static final int BASE= INFORMATION; // see ISourceViewer.INFORMATION
+
+    /** Operation constant for the expand operation. */
+    public static final int EXPAND= BASE + 1;
+    /** Operation constant for the collapse operation. */
+    public static final int COLLAPSE= BASE + 2;
+    /** Operation constant for the toggle projection operation. */
+    public static final int TOGGLE= BASE + 3;
+    /** Operation constant for the expand all operation. */
+    public static final int EXPAND_ALL= BASE + 4;
+    /**
+     * Operation constant for the collapse all operation.
+     * 
+     * @since 3.2
+     */
+    public static final 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 ReplaceVisibleDocumentExecutor(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 {
+
+        final static int ADD= 0;
+        final static int REMOVE= 1;
+        final static int INVALIDATE_PRESENTATION= 2;
+
+        ProjectionDocument fProjection;
+        int fType;
+        int fOffset;
+        int fLength;
+
+        ProjectionCommand(ProjectionDocument projection, int type, int offset, int length) {
+            fProjection= projection;
+            fType= type;
+            fOffset= offset;
+            fLength= length;
+        }
+
+        ProjectionCommand(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;
+                }
+            }
+            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= new ArrayList(15);
+        int fExpectedExecutionCosts= -1;
+
+
+        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= (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= new AnnotationModelListener();
+    /** 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= new Object();
+    /** The list of pending requests */
+    private List fPendingRequests= new ArrayList();
+    /** 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 ProjectionViewer(Composite parent, IVerticalRuler ruler, IOverviewRuler overviewRuler, bool showsAnnotationOverview, int styles) {
+        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 (model instanceof IAnnotationModelExtension) {
+            IAnnotationModelExtension extension= (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 (model instanceof IAnnotationModelExtension) {
+            model.removeAnnotationModelListener(fAnnotationModelListener);
+            IAnnotationModelExtension extension= (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 (model instanceof IAnnotationModelExtension) {
+            IAnnotationModelExtension extension= (IAnnotationModelExtension) model;
+            return (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) throws BadLocationException {
+        if (slaveDocument instanceof ProjectionDocument) {
+            ProjectionDocument projection= (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(listener);
+
+        if (fProjectionListeners is null)
+            fProjectionListeners= new ArrayList();
+
+        if (!fProjectionListeners.contains(listener))
+            fProjectionListeners.add(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(listener);
+
+        if (fProjectionListeners !is null) {
+            fProjectionListeners.remove(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= (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= (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= (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= (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) throws BadLocationException {
+
+        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) throws BadLocationException {
+        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) throws BadLocationException {
+        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) throws BadLocationException {
+        ProjectionDocument projection= null;
+
+        IDocument visibleDocument= getVisibleDocument();
+        if (visibleDocument instanceof ProjectionDocument)
+            projection= (ProjectionDocument) visibleDocument;
+        else {
+            IDocument master= getDocument();
+            IDocument slave= createSlaveDocument(getDocument());
+            if (slave instanceof ProjectionDocument) {
+                projection= (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) throws BadLocationException {
+        IDocument slave= getVisibleDocument();
+        if (slave instanceof ProjectionDocument) {
+            ProjectionDocument projection= (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();
+                }
+
+            } 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(final 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 Runnable() {
+                            public void run() {
+                                try {
+                                    while (true) {
+                                        AnnotationModelEvent ame= null;
+                                        synchronized (fLock) {
+                                            if (fPendingRequests.size() is 0)
+                                                return;
+                                            ame= (AnnotationModelEvent) fPendingRequests.remove(0);
+                                        }
+                                        catchupWithProjectionAnnotationModel(ame);
+                                    }
+                                } catch (BadLocationException x) {
+                                    try {
+                                        catchupWithProjectionAnnotationModel(null);
+                                    } catch (BadLocationException x1) {
+                                        throw new IllegalArgumentException();
+                                    } 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 (visibleDocument instanceof ProjectionDocument) && ((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) throws BadLocationException {
+        
+        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) throws BadLocationException {
+
+        ProjectionCommand command;
+        Iterator e= commandQueue.iterator();
+        while (e.hasNext()) {
+            command= (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;
+            }
+        }
+
+        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= (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) throws BadLocationException {
+        for (int i= 0; i < removedAnnotations.length; i++) {
+            ProjectionAnnotation annotation= (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 (position instanceof IProjectionPosition) {
+                IProjectionPosition projPosition= (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 IRegion[] {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 (position instanceof IProjectionPosition)
+                captionOffset+= ((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) throws BadLocationException {
+        for (int i= 0; i < annotations.length; i++) {
+            ProjectionAnnotation annotation= (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= (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() throws BadLocationException {
+
+        ProjectionDocument projection= null;
+
+        ISlaveDocumentManager manager= getSlaveDocumentManager();
+        if (manager !is null) {
+            IDocument master= getDocument();
+            if (master !is null) {
+                IDocument slave= manager.createSlaveDocument(master);
+                if (slave instanceof ProjectionDocument) {
+                    projection= (ProjectionDocument) slave;
+                    addMasterDocumentRange(projection, 0, master.getLength());
+                }
+            }
+        }
+
+        if (projection !is null) {
+            Iterator e= fProjectionAnnotationModel.getAnnotationIterator();
+            while (e.hasNext()) {
+                ProjectionAnnotation annotation= (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 (ruler instanceof CompositeRuler) {
+            CompositeRuler compositeRuler= (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 (ruler instanceof CompositeRuler) {
+            CompositeRuler compositeRuler= (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= (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((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 && event instanceof ProjectionDocumentEvent && isProjectionMode()) {
+            ProjectionDocumentEvent e= (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 && event instanceof ProjectionDocumentEvent && 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 IRegion[] { new Region(modelRange.getOffset(), modelRange.getLength()) };
+
+        if (fInformationMapping instanceof IDocumentInformationMappingExtension) {
+            IDocumentInformationMappingExtension extension= (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;
+                }
+        }
+
+        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();
+        }
+
+        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 && !visible.equals(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= new Transfer[] { TextTransfer.getInstance() };
+                Object[] data= new Object[] { 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) throws BadLocationException {
+        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= (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 (Position[]) positions.toArray(new Position[positions.size()]);
+    }
+
+    /*
+     * @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;
+    }
+}