Bug 568496 - Add special support for unusually slow source files
diff --git a/org.eclipse.handly.ui/src/org/eclipse/handly/internal/ui/Activator.java b/org.eclipse.handly.ui/src/org/eclipse/handly/internal/ui/Activator.java
index 1e16400..15cbb1c 100644
--- a/org.eclipse.handly.ui/src/org/eclipse/handly/internal/ui/Activator.java
+++ b/org.eclipse.handly.ui/src/org/eclipse/handly/internal/ui/Activator.java
@@ -1,5 +1,5 @@
 /*******************************************************************************
- * Copyright (c) 2014, 2019 1C-Soft LLC and others.
+ * Copyright (c) 2014, 2020 1C-Soft LLC and others.
  *
  * This program and the accompanying materials are made available under
  * the terms of the Eclipse Public License 2.0 which is available at
@@ -12,8 +12,10 @@
  *******************************************************************************/
 package org.eclipse.handly.internal.ui;
 
+import org.eclipse.core.runtime.ICoreRunnable;
 import org.eclipse.core.runtime.IStatus;
 import org.eclipse.core.runtime.Status;
+import org.eclipse.core.runtime.jobs.Job;
 import org.eclipse.jface.resource.ImageDescriptor;
 import org.eclipse.jface.resource.ImageRegistry;
 import org.eclipse.swt.graphics.Image;
@@ -107,6 +109,23 @@
         return plugin.getImageRegistry().getDescriptor(symbolicName);
     }
 
+    public static boolean timedExec(String name, ICoreRunnable runnable,
+        long timeoutMillis)
+    {
+        Job job = Job.createSystem(name, runnable);
+        job.schedule();
+        try
+        {
+            if (job.join(timeoutMillis, null))
+                return true;
+        }
+        catch (InterruptedException e)
+        {
+        }
+        job.cancel();
+        return false;
+    }
+
     @Override
     public void start(BundleContext context) throws Exception
     {
diff --git a/org.eclipse.handly.ui/src/org/eclipse/handly/ui/EditorUtility.java b/org.eclipse.handly.ui/src/org/eclipse/handly/ui/EditorUtility.java
index bdd8652..f47b9ce 100644
--- a/org.eclipse.handly.ui/src/org/eclipse/handly/ui/EditorUtility.java
+++ b/org.eclipse.handly.ui/src/org/eclipse/handly/ui/EditorUtility.java
@@ -1,5 +1,5 @@
 /*******************************************************************************
- * Copyright (c) 2016, 2019 1C-Soft LLC.
+ * Copyright (c) 2016, 2020 1C-Soft LLC.
  *
  * This program and the accompanying materials are made available under
  * the terms of the Eclipse Public License 2.0 which is available at
@@ -12,6 +12,8 @@
  *******************************************************************************/
 package org.eclipse.handly.ui;
 
+import static org.eclipse.handly.context.Contexts.EMPTY_CONTEXT;
+
 import java.util.HashMap;
 
 import org.eclipse.core.filebuffers.ITextFileBuffer;
@@ -22,6 +24,8 @@
 import org.eclipse.core.resources.IResource;
 import org.eclipse.core.runtime.Adapters;
 import org.eclipse.core.runtime.CoreException;
+import org.eclipse.core.runtime.IProgressMonitor;
+import org.eclipse.core.runtime.SubMonitor;
 import org.eclipse.handly.buffer.IBuffer;
 import org.eclipse.handly.internal.ui.Activator;
 import org.eclipse.handly.model.Elements;
@@ -397,10 +401,13 @@
                     editor.getEditorInput());
                 if (document != null && document.equals(buffer.getDocument()))
                 {
-                    Elements.ensureReconciled(element, null);
-
-                    TextRange identifyingRange = Elements.getSourceElementInfo2(
-                        element).getIdentifyingRange();
+                    TextRange[] result = new TextRange[1];
+                    Activator.timedExec("EditorUtility::revealSourceElement", //$NON-NLS-1$
+                        monitor ->
+                        {
+                            result[0] = getIdentifyingRange(element, monitor);
+                        }, 500);
+                    TextRange identifyingRange = result[0];
                     if (identifyingRange != null)
                     {
                         editor.selectAndReveal(identifyingRange.getOffset(),
@@ -412,4 +419,24 @@
         }
         return false;
     }
+
+    private static TextRange getIdentifyingRange(ISourceElement element,
+        IProgressMonitor monitor)
+    {
+        SubMonitor subMonitor = SubMonitor.convert(monitor, 2);
+        Elements.ensureReconciled(element, subMonitor.split(1));
+        try
+        {
+            return Elements.getSourceElementInfo(element, EMPTY_CONTEXT,
+                subMonitor.split(1)).getIdentifyingRange();
+        }
+        catch (CoreException e)
+        {
+            if (!Elements.exists(element))
+                ; // this is considered normal
+            else
+                Activator.logError(e);
+        }
+        return null;
+    }
 }
diff --git a/org.eclipse.handly.ui/src/org/eclipse/handly/ui/quickoutline/HandlyOutlinePopup.java b/org.eclipse.handly.ui/src/org/eclipse/handly/ui/quickoutline/HandlyOutlinePopup.java
index ffaa22d..3a1d84d 100644
--- a/org.eclipse.handly.ui/src/org/eclipse/handly/ui/quickoutline/HandlyOutlinePopup.java
+++ b/org.eclipse.handly.ui/src/org/eclipse/handly/ui/quickoutline/HandlyOutlinePopup.java
@@ -1,5 +1,5 @@
 /*******************************************************************************
- * Copyright (c) 2014, 2018 1C-Soft LLC and others.
+ * Copyright (c) 2014, 2020 1C-Soft LLC and others.
  *
  * This program and the accompanying materials are made available under
  * the terms of the Eclipse Public License 2.0 which is available at
@@ -12,6 +12,12 @@
  *******************************************************************************/
 package org.eclipse.handly.ui.quickoutline;
 
+import static org.eclipse.handly.context.Contexts.EMPTY_CONTEXT;
+
+import org.eclipse.core.runtime.CoreException;
+import org.eclipse.core.runtime.IProgressMonitor;
+import org.eclipse.core.runtime.SubMonitor;
+import org.eclipse.handly.internal.ui.Activator;
 import org.eclipse.handly.model.Elements;
 import org.eclipse.handly.model.IElement;
 import org.eclipse.handly.model.ISourceElement;
@@ -83,12 +89,35 @@
         IElement input = getContentAdapter().adapt(getTreeViewer().getInput());
         if (!(input instanceof ISourceElement))
             return null;
-        ISourceElement sourceElement = (ISourceElement)input;
-        if (!Elements.ensureReconciled(sourceElement, null))
+        ISourceElement[] result = new ISourceElement[1];
+        Activator.timedExec("HandlyOutlinePopup::getCorrespondingElement", //$NON-NLS-1$
+            monitor ->
+            {
+                result[0] = getSourceElementAt((ISourceElement)input,
+                    ((ITextSelection)hostSelection).getOffset(), monitor);
+            }, 500);
+        return getContentAdapter().getCorrespondingElement(result[0]);
+    }
+
+    private static ISourceElement getSourceElementAt(ISourceElement element,
+        int offset, IProgressMonitor monitor)
+    {
+        SubMonitor subMonitor = SubMonitor.convert(monitor, 2);
+        if (!Elements.ensureReconciled(element, subMonitor.split(1)))
             return null;
-        return getContentAdapter().getCorrespondingElement(
-            Elements.getSourceElementAt2(sourceElement,
-                ((ITextSelection)hostSelection).getOffset(), null));
+        try
+        {
+            return Elements.getSourceElementAt(element, offset, EMPTY_CONTEXT,
+                subMonitor.split(1));
+        }
+        catch (CoreException e)
+        {
+            if (!Elements.exists(element))
+                ; // this is considered normal
+            else
+                Activator.logError(e);
+        }
+        return null;
     }
 
     /**
diff --git a/org.eclipse.handly.ui/src/org/eclipse/handly/ui/text/reconciler/EditorWorkingCopyReconciler.java b/org.eclipse.handly.ui/src/org/eclipse/handly/ui/text/reconciler/EditorWorkingCopyReconciler.java
index 2f262b1..3c8ab04 100644
--- a/org.eclipse.handly.ui/src/org/eclipse/handly/ui/text/reconciler/EditorWorkingCopyReconciler.java
+++ b/org.eclipse.handly.ui/src/org/eclipse/handly/ui/text/reconciler/EditorWorkingCopyReconciler.java
@@ -1,5 +1,5 @@
 /*******************************************************************************
- * Copyright (c) 2015, 2018 1C-Soft LLC.
+ * Copyright (c) 2015, 2020 1C-Soft LLC.
  *
  * This program and the accompanying materials are made available under
  * the terms of the Eclipse Public License 2.0 which is available at
@@ -12,7 +12,11 @@
  *******************************************************************************/
 package org.eclipse.handly.ui.text.reconciler;
 
+import java.util.function.Function;
+
+import org.eclipse.handly.model.ISourceFile;
 import org.eclipse.handly.ui.IWorkingCopyManager;
+import org.eclipse.jface.text.IDocument;
 import org.eclipse.jface.text.ITextViewer;
 import org.eclipse.ui.IEditorPart;
 import org.eclipse.ui.IPartListener;
@@ -68,7 +72,25 @@
     public EditorWorkingCopyReconciler(IEditorPart editor,
         IWorkingCopyManager workingCopyManager)
     {
-        super(workingCopyManager);
+        this(editor, workingCopyManager::getWorkingCopy);
+    }
+
+    /**
+     * Creates a new working copy reconciler for the given editor and with
+     * a function that is used to determine the source file for the reconciler's
+     * document. The reconciler is configured with a single reconciling strategy
+     * (by default, a {@link WorkingCopyReconcilingStrategy}) that is used
+     * irrespective of where a dirty region is located in the reconciler's
+     * document.
+     *
+     * @param editor not <code>null</code>
+     * @param documentToSourceFile not <code>null</code>
+     * @since 1.5
+     */
+    public EditorWorkingCopyReconciler(IEditorPart editor,
+        Function<IDocument, ISourceFile> documentToSourceFile)
+    {
+        super(documentToSourceFile);
         if (editor == null)
             throw new IllegalArgumentException();
         this.editor = editor;
diff --git a/org.eclipse.handly.ui/src/org/eclipse/handly/ui/text/reconciler/WorkingCopyReconciler.java b/org.eclipse.handly.ui/src/org/eclipse/handly/ui/text/reconciler/WorkingCopyReconciler.java
index a31a702..1101017 100644
--- a/org.eclipse.handly.ui/src/org/eclipse/handly/ui/text/reconciler/WorkingCopyReconciler.java
+++ b/org.eclipse.handly.ui/src/org/eclipse/handly/ui/text/reconciler/WorkingCopyReconciler.java
@@ -1,5 +1,5 @@
 /*******************************************************************************
- * Copyright (c) 2015, 2018 1C-Soft LLC.
+ * Copyright (c) 2015, 2020 1C-Soft LLC.
  *
  * This program and the accompanying materials are made available under
  * the terms of the Eclipse Public License 2.0 which is available at
@@ -18,6 +18,8 @@
 import static org.eclipse.handly.model.IElementDeltaConstants.F_UNDERLYING_RESOURCE;
 import static org.eclipse.handly.model.IElementDeltaConstants.F_WORKING_COPY;
 
+import java.util.function.Function;
+
 import org.eclipse.core.resources.IMarker;
 import org.eclipse.core.resources.IMarkerDelta;
 import org.eclipse.core.runtime.IProgressMonitor;
@@ -50,9 +52,9 @@
 public abstract class WorkingCopyReconciler
     extends AbstractReconciler
 {
-    private IWorkingCopyManager workingCopyManager;
+    private Function<IDocument, ISourceFile> documentToSourceFile;
     private IReconcilingStrategy strategy;
-    private volatile ISourceFile workingCopy;
+    private volatile ISourceFile sourceFile;
     private volatile boolean active = true;
     private volatile boolean modelChanged = false;
     private volatile boolean initialProcessDone = false;
@@ -83,14 +85,30 @@
      */
     public WorkingCopyReconciler(IWorkingCopyManager workingCopyManager)
     {
-        if (workingCopyManager == null)
+        this(workingCopyManager::getWorkingCopy);
+    }
+
+    /**
+     * Creates a new working copy reconciler with a function that is used to
+     * determine the source file for the reconciler's document. The reconciler
+     * is configured with a single reconciling strategy (by default, a {@link
+     * WorkingCopyReconcilingStrategy}) that is used irrespective of where a
+     * dirty region is located in the reconciler's document.
+     *
+     * @param documentToSourceFile not <code>null</code>
+     * @since 1.5
+     */
+    public WorkingCopyReconciler(
+        Function<IDocument, ISourceFile> documentToSourceFile)
+    {
+        if (documentToSourceFile == null)
             throw new IllegalArgumentException();
-        this.workingCopyManager = workingCopyManager;
+        this.documentToSourceFile = documentToSourceFile;
         // Just some reasonable defaults that can be overwritten:
         setIsIncrementalReconciler(false);
         setIsAllowedToModifyDocument(false);
         setReconcilingStrategy(new WorkingCopyReconcilingStrategy(
-            workingCopyManager));
+            documentToSourceFile));
     }
 
     /**
@@ -141,8 +159,7 @@
     {
         super.install(textViewer);
 
-        setWorkingCopy(workingCopyManager.getWorkingCopy(
-            textViewer.getDocument()));
+        setSourceFile(documentToSourceFile.apply(textViewer.getDocument()));
 
         addElementChangeListener(elementChangeListener);
 
@@ -161,7 +178,7 @@
 
         removeElementChangeListener(elementChangeListener);
 
-        setWorkingCopy(null);
+        setSourceFile(null);
 
         super.uninstall();
     }
@@ -228,7 +245,7 @@
     @Override
     protected void reconcilerDocumentChanged(IDocument newDocument)
     {
-        setWorkingCopy(workingCopyManager.getWorkingCopy(newDocument));
+        setSourceFile(documentToSourceFile.apply(newDocument));
         strategy.setDocument(newDocument);
     }
 
@@ -268,7 +285,7 @@
      * by the given element change event.
      * <p>
      * This implementation delegates to {@link #isAffectedBy(IElementDelta,
-     * ISourceFile)}, passing the working copy for the reconciler's document.
+     * ISourceFile)}, passing the source file for the reconciler's document.
      * </p>
      *
      * @param event never <code>null</code>
@@ -277,15 +294,14 @@
      */
     protected boolean isAffectedBy(IElementChangeEvent event)
     {
-        return isAffectedBy(event.getDeltas(), getWorkingCopy());
+        return isAffectedBy(event.getDeltas(), getSourceFile());
     }
 
-    private boolean isAffectedBy(IElementDelta[] deltas,
-        ISourceFile workingCopy)
+    private boolean isAffectedBy(IElementDelta[] deltas, ISourceFile sourceFile)
     {
         for (IElementDelta delta : deltas)
         {
-            if (isAffectedBy(delta, workingCopy))
+            if (isAffectedBy(delta, sourceFile))
                 return true;
         }
         return false;
@@ -293,24 +309,24 @@
 
     /**
      * Returns whether this reconciler is affected by the given element delta
-     * with regard to the given working copy.
+     * with regard to the given source file.
      *
      * @param delta never <code>null</code>
-     * @param workingCopy may be <code>null</code>
+     * @param sourceFile may be <code>null</code>
      * @return <code>true</code> if the reconciler is affected
      *  by the given delta, and <code>false</code> otherwise
      */
-    protected boolean isAffectedBy(IElementDelta delta, ISourceFile workingCopy)
+    protected boolean isAffectedBy(IElementDelta delta, ISourceFile sourceFile)
     {
         long flags = ElementDeltas.getFlags(delta);
         if (flags == F_SYNC || flags == F_WORKING_COPY)
             return false;
         IElement element = ElementDeltas.getElement(delta);
-        if (flags == F_UNDERLYING_RESOURCE && element.equals(workingCopy))
-            return false; // saving this reconciler's working copy
+        if (flags == F_UNDERLYING_RESOURCE && element.equals(sourceFile))
+            return false; // saving the working copy
         if (flags == F_MARKERS)
         {
-            if (element.equals(workingCopy))
+            if (element.equals(sourceFile))
             {
                 for (IMarkerDelta markerDelta : ElementDeltas.getMarkerDeltas(
                     delta))
@@ -325,7 +341,7 @@
             return true;
         for (IElementDelta child : ElementDeltas.getAffectedChildren(delta))
         {
-            if (isAffectedBy(child, workingCopy))
+            if (isAffectedBy(child, sourceFile))
                 return true;
         }
         return false;
@@ -396,14 +412,14 @@
             forceReconciling();
     }
 
-    private ISourceFile getWorkingCopy()
+    private ISourceFile getSourceFile()
     {
-        return workingCopy;
+        return sourceFile;
     }
 
-    private void setWorkingCopy(ISourceFile workingCopy)
+    private void setSourceFile(ISourceFile sourceFile)
     {
-        this.workingCopy = workingCopy;
+        this.sourceFile = sourceFile;
     }
 
     private boolean hasModelChanged()
diff --git a/org.eclipse.handly.ui/src/org/eclipse/handly/ui/text/reconciler/WorkingCopyReconcilingStrategy.java b/org.eclipse.handly.ui/src/org/eclipse/handly/ui/text/reconciler/WorkingCopyReconcilingStrategy.java
index c615032..4e9e800 100644
--- a/org.eclipse.handly.ui/src/org/eclipse/handly/ui/text/reconciler/WorkingCopyReconcilingStrategy.java
+++ b/org.eclipse.handly.ui/src/org/eclipse/handly/ui/text/reconciler/WorkingCopyReconcilingStrategy.java
@@ -1,5 +1,5 @@
 /*******************************************************************************
- * Copyright (c) 2015, 2018 1C-Soft LLC.
+ * Copyright (c) 2015, 2020 1C-Soft LLC.
  *
  * This program and the accompanying materials are made available under
  * the terms of the Eclipse Public License 2.0 which is available at
@@ -12,6 +12,8 @@
  *******************************************************************************/
 package org.eclipse.handly.ui.text.reconciler;
 
+import java.util.function.Function;
+
 import org.eclipse.core.runtime.CoreException;
 import org.eclipse.core.runtime.IProgressMonitor;
 import org.eclipse.core.runtime.ISafeRunnable;
@@ -32,8 +34,8 @@
 public class WorkingCopyReconcilingStrategy
     implements IReconcilingStrategy, IReconcilingStrategyExtension
 {
-    private final IWorkingCopyManager workingCopyManager;
-    private volatile ISourceFile workingCopy;
+    private final Function<IDocument, ISourceFile> documentToSourceFile;
+    private volatile ISourceFile sourceFile;
     private volatile IProgressMonitor monitor;
 
     /**
@@ -46,15 +48,29 @@
     public WorkingCopyReconcilingStrategy(
         IWorkingCopyManager workingCopyManager)
     {
-        if (workingCopyManager == null)
+        this(workingCopyManager::getWorkingCopy);
+    }
+
+    /**
+     * Creates a new working copy reconciling strategy with a function that
+     * is used to determine the source file for the reconciling strategy's
+     * document.
+     *
+     * @param documentToSourceFile not <code>null</code>
+     * @since 1.5
+     */
+    public WorkingCopyReconcilingStrategy(
+        Function<IDocument, ISourceFile> documentToSourceFile)
+    {
+        if (documentToSourceFile == null)
             throw new IllegalArgumentException();
-        this.workingCopyManager = workingCopyManager;
+        this.documentToSourceFile = documentToSourceFile;
     }
 
     @Override
     public void setDocument(IDocument document)
     {
-        setWorkingCopy(workingCopyManager.getWorkingCopy(document));
+        setSourceFile(documentToSourceFile.apply(document));
     }
 
     @Override
@@ -107,13 +123,15 @@
     }
 
     /**
-     * Reconciles the given working copy.
+     * Reconciles the given source file. Does nothing if the source file is
+     * not in working copy mode or if its buffer has not been modified since
+     * the last time it was reconciled.
      * <p>
      * This implementation invokes <code>Elements.{@link Elements#reconcile(ISourceFile,
-     * IProgressMonitor) reconcile}(workingCopy, monitor)</code>.
+     * IProgressMonitor) reconcile}(sourceFile, monitor)</code>.
      * </p>
      *
-     * @param workingCopy never <code>null</code>
+     * @param sourceFile never <code>null</code>
      * @param initialReconcile <code>true</code> if this is the initial reconcile,
      *  and <code>false</code> otherwise
      * @param monitor a progress monitor, or <code>null</code>
@@ -122,22 +140,22 @@
      * @throws CoreException if the working copy could not be reconciled
      * @throws OperationCanceledException if this method is canceled
      */
-    protected void reconcile(ISourceFile workingCopy, boolean initialReconcile,
+    protected void reconcile(ISourceFile sourceFile, boolean initialReconcile,
         IProgressMonitor monitor) throws CoreException
     {
-        Elements.reconcile(workingCopy, monitor);
+        Elements.reconcile(sourceFile, monitor);
     }
 
     private void reconcile(boolean initialReconcile)
     {
-        ISourceFile workingCopy = getWorkingCopy();
-        if (workingCopy == null)
+        ISourceFile sourceFile = getSourceFile();
+        if (sourceFile == null)
             return;
         SafeRunner.run(new ISafeRunnable()
         {
             public void run() throws Exception
             {
-                reconcile(workingCopy, initialReconcile, monitor);
+                reconcile(sourceFile, initialReconcile, monitor);
             }
 
             public void handleException(Throwable exception)
@@ -147,13 +165,13 @@
         });
     }
 
-    private void setWorkingCopy(ISourceFile workingCopy)
+    private void setSourceFile(ISourceFile sourceFile)
     {
-        this.workingCopy = workingCopy;
+        this.sourceFile = sourceFile;
     }
 
-    private ISourceFile getWorkingCopy()
+    private ISourceFile getSourceFile()
     {
-        return workingCopy;
+        return sourceFile;
     }
 }
diff --git a/org.eclipse.handly.ui/src/org/eclipse/handly/ui/texteditor/DeferredSourceFileDocumentProvider.java b/org.eclipse.handly.ui/src/org/eclipse/handly/ui/texteditor/DeferredSourceFileDocumentProvider.java
new file mode 100644
index 0000000..2f3f84e
--- /dev/null
+++ b/org.eclipse.handly.ui/src/org/eclipse/handly/ui/texteditor/DeferredSourceFileDocumentProvider.java
@@ -0,0 +1,345 @@
+/*******************************************************************************
+ * Copyright (c) 2020 1C-Soft LLC.
+ *
+ * This program and the accompanying materials are made available under
+ * the terms of the Eclipse Public License 2.0 which is available at
+ * https://www.eclipse.org/legal/epl-2.0/
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ *
+ * Contributors:
+ *     Vladimir Piskarev (1C) - initial API and implementation
+ *******************************************************************************/
+package org.eclipse.handly.ui.texteditor;
+
+import static org.eclipse.handly.context.Contexts.EMPTY_CONTEXT;
+
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+
+import org.eclipse.core.runtime.CoreException;
+import org.eclipse.core.runtime.IProgressMonitor;
+import org.eclipse.core.runtime.OperationCanceledException;
+import org.eclipse.core.runtime.jobs.Job;
+import org.eclipse.handly.buffer.IBuffer;
+import org.eclipse.handly.model.Elements;
+import org.eclipse.handly.model.ISourceFile;
+import org.eclipse.handly.model.impl.ISourceFileImplExtension;
+import org.eclipse.jface.text.IDocument;
+import org.eclipse.ui.editors.text.TextFileDocumentProvider;
+import org.eclipse.ui.texteditor.IDocumentProvider;
+
+/**
+ * In contrast to {@link SourceFileDocumentProvider}, which acquires a working
+ * copy in the calling thread, this class defers working copy acquisition to a
+ * worker thread.
+ *
+ * @since 1.5
+ */
+public abstract class DeferredSourceFileDocumentProvider
+    extends TextFileDocumentProvider
+{
+    private static final ISourceFile[] NO_SOURCE_FILES = new ISourceFile[0];
+
+    /**
+     * Creates a new source file document provider with no parent.
+     */
+    public DeferredSourceFileDocumentProvider()
+    {
+        this(null);
+    }
+
+    /**
+     * Creates a new source file document provider with the given parent.
+     *
+     * @param parent the parent document provider
+     */
+    public DeferredSourceFileDocumentProvider(IDocumentProvider parent)
+    {
+        super(parent);
+    }
+
+    /**
+     * Returns the source file managed for the given element,
+     * or <code>null</code> if this provider does not currently manage
+     * a source file for the element.
+     * <p>
+     * This implementation returns the source file retained by the file info for
+     * the given element. The file info is obtained via {@link #getFileInfo(Object)}.
+     * </p>
+     *
+     * @param element the element for which to find the source file,
+     *  or <code>null</code>
+     * @return the source file managed for the given element,
+     *  or <code>null</code> if none
+     */
+    public ISourceFile getConnectedSourceFile(Object element)
+    {
+        FileInfo info = getFileInfo(element);
+        if (info instanceof SourceFileInfo)
+            return ((SourceFileInfo)info).sourceFile;
+        return null;
+    }
+
+    /**
+     * Returns the source file managed for the given document,
+     * or <code>null</code> if this provider does not currently manage
+     * a source file for the document.
+     * <p>
+     * <b>Note:</b> An implementation of this method may go through the list
+     * of source files and test whether the source file buffer's document
+     * equals the given document. Therefore, this method should not be used
+     * in performance critical code.
+     * </p>
+     * <p>
+     * This implementation returns the source file retained by the file info
+     * for the given document. The file info is found by iterating over
+     * this provider's file info objects via {@link #getFileInfosIterator()}
+     * and testing whether the document of the file info's text file buffer
+     * equals the given document.
+     * </p>
+     *
+     * @param document the document for which to find the source file,
+     *  or <code>null</code>
+     * @return the source file managed for the given document,
+     *  or <code>null</code> if none
+     */
+    public ISourceFile getConnectedSourceFile(IDocument document)
+    {
+        Iterator<FileInfo> it = getFileInfosIterator();
+        while (it.hasNext())
+        {
+            FileInfo info = it.next();
+            IDocument infoDocument = null;
+            if (info.fTextFileBuffer != null)
+                infoDocument = info.fTextFileBuffer.getDocument();
+            if (infoDocument != null && infoDocument.equals(document))
+            {
+                if (info instanceof SourceFileInfo)
+                    return ((SourceFileInfo)info).sourceFile;
+            }
+        }
+        return null;
+    }
+
+    /**
+     * Returns all source files that are currently managed by this provider.
+     * <p>
+     * This implementation iterates over this provider's file info objects
+     * via {@link #getFileInfosIterator()} and collects the source files
+     * they retain.
+     * </p>
+     *
+     * @return the source files currently managed by this provider
+     *  (never <code>null</code>)
+     */
+    public ISourceFile[] getConnectedSourceFiles()
+    {
+        List<ISourceFile> result = new ArrayList<>();
+        Iterator<FileInfo> it = getFileInfosIterator();
+        while (it.hasNext())
+        {
+            FileInfo info = it.next();
+            if (info instanceof SourceFileInfo)
+            {
+                ISourceFile sourceFile = ((SourceFileInfo)info).sourceFile;
+                if (sourceFile != null)
+                    result.add(sourceFile);
+            }
+        }
+        return result.toArray(NO_SOURCE_FILES);
+    }
+
+    /**
+     * Returns the source file that corresponds to the given element.
+     *
+     * @param element the element
+     * @return the source file that corresponds to the given element,
+     *  or <code>null</code> if none
+     */
+    protected abstract ISourceFile getSourceFile(Object element);
+
+    /**
+     * {@inheritDoc}
+     * <p>
+     * This implementation returns a new instance of
+     * {@link DeferredSourceFileDocumentProvider.SourceFileInfo SourceFileInfo}.
+     * </p>
+     */
+    @Override
+    protected FileInfo createEmptyFileInfo()
+    {
+        return new SourceFileInfo();
+    }
+
+    /**
+     * {@inheritDoc}
+     * <p>
+     * This implementation invokes the superclass implementation to create the
+     * file info object. If the created object is an instance of {@link
+     * DeferredSourceFileDocumentProvider.SourceFileInfo SourceFileInfo}, it stores
+     * a reference to the corresponding {@link #getSourceFile(Object) source file}
+     * in the created file info, and then attempts to {@link #acquireWorkingCopy
+     * acquire} a working copy for the source file asynchronously.
+     * </p>
+     */
+    @Override
+    protected FileInfo createFileInfo(Object element) throws CoreException
+    {
+        FileInfo info = super.createFileInfo(element);
+        if (info instanceof SourceFileInfo)
+        {
+            setUpSourceFileInfo(element, (SourceFileInfo)info);
+        }
+        return info;
+    }
+
+    /**
+     * {@inheritDoc}
+     * <p>
+     * This implementation invokes the superclass implementation after trying to
+     * {@link #releaseWorkingCopy(ISourceFile, Object, TextFileDocumentProvider.FileInfo)
+     * release} the working copy retained by the given file info object.
+     * </p>
+     */
+    @Override
+    protected void disposeFileInfo(Object element, FileInfo info)
+    {
+        try
+        {
+            if (info instanceof SourceFileInfo)
+                disposeSourceFileInfo(element, (SourceFileInfo)info);
+        }
+        finally
+        {
+            super.disposeFileInfo(element, info);
+        }
+    }
+
+    /**
+     * Attempts to acquire a working copy for the given source file. The working
+     * copy acquired by this method <b>must</b> be released eventually via a call
+     * to {@link #releaseWorkingCopy releaseWorkingCopy}.
+     * <p>
+     * If the given source file implements {@link ISourceFileImplExtension}, this
+     * implementation invokes <code>{@link ISourceFileImplExtension#becomeWorkingCopy_
+     * becomeWorkingCopy_}(EMPTY_CONTEXT, monitor)</code> on it and returns
+     * <code>true</code>. Otherwise, <code>false</code> is returned.
+     * </p>
+     *
+     * @param sourceFile the source file
+     * @param element the element
+     * @param info the element info
+     * @param monitor a progress monitor, or <code>null</code>
+     *  if progress reporting is not desired. The caller must not rely on
+     *  {@link IProgressMonitor#done()} having been called by the receiver
+     * @return <code>true</code> if a working copy has been acquired;
+     *  <code>false</code> if no working copy can be acquired for
+     *  the given source file
+     * @throws CoreException if the working copy could not be acquired successfully
+     * @throws OperationCanceledException if this method is canceled
+     */
+    protected boolean acquireWorkingCopy(ISourceFile sourceFile, Object element,
+        FileInfo info, IProgressMonitor monitor) throws CoreException
+    {
+        if (sourceFile instanceof ISourceFileImplExtension)
+        {
+            ((ISourceFileImplExtension)sourceFile).becomeWorkingCopy_(
+                EMPTY_CONTEXT, monitor);
+            return true;
+        }
+        return false;
+    }
+
+    /**
+     * Releases the given working copy that was acquired via a call to
+     * {@link #acquireWorkingCopy acquireWorkingCopy}.
+     * <p>
+     * This implementation invokes <code>((ISourceFileImplExtension)workingCopy).{@link
+     * ISourceFileImplExtension#releaseWorkingCopy_() releaseWorkingCopy_()}</code>.
+     * </p>
+     *
+     * @param workingCopy the working copy to release
+     * @param element the element
+     * @param info the element info
+     */
+    protected void releaseWorkingCopy(ISourceFile workingCopy, Object element,
+        FileInfo info)
+    {
+        ((ISourceFileImplExtension)workingCopy).releaseWorkingCopy_();
+    }
+
+    private void setUpSourceFileInfo(Object element, SourceFileInfo info)
+    {
+        ISourceFile sourceFile = getSourceFile(element);
+        if (sourceFile == null)
+            return;
+
+        info.sourceFile = sourceFile;
+
+        (info.setUpWorkingCopyJob = Job.createSystem(
+            "DeferredSourceFileDocumentProvider::setUpSourceFileInfo", //$NON-NLS-1$
+            monitor ->
+            {
+                if (!acquireWorkingCopy(sourceFile, element, info, monitor))
+                    return;
+
+                if (!Elements.isWorkingCopy(sourceFile))
+                    throw new AssertionError();
+
+                boolean releaseWorkingCopy = true;
+                try (IBuffer buffer = Elements.getBuffer(sourceFile))
+                {
+                    IDocument document = null;
+                    if (info.fTextFileBuffer != null)
+                        document = info.fTextFileBuffer.getDocument();
+                    if (!buffer.getDocument().equals(document))
+                        throw new AssertionError();
+
+                    synchronized (info)
+                    {
+                        if (!info.disposed)
+                        {
+                            info.workingCopyAcquired = true;
+                            releaseWorkingCopy = false;
+                        }
+                    }
+                }
+                finally
+                {
+                    if (releaseWorkingCopy)
+                        releaseWorkingCopy(sourceFile, element, info);
+                }
+            })).schedule();
+    }
+
+    private void disposeSourceFileInfo(Object element, SourceFileInfo info)
+    {
+        info.setUpWorkingCopyJob.cancel();
+
+        ISourceFile workingCopy = null;
+        synchronized (info)
+        {
+            if (info.workingCopyAcquired)
+                workingCopy = info.sourceFile;
+
+            info.disposed = true;
+        }
+        if (workingCopy != null)
+            releaseWorkingCopy(workingCopy, element, info);
+    }
+
+    /**
+     * Subclass of {@link org.eclipse.ui.editors.text.TextFileDocumentProvider.FileInfo
+     * FileInfo} that can retain a reference to a source file.
+     */
+    protected static class SourceFileInfo
+        extends FileInfo
+    {
+        ISourceFile sourceFile;
+        Job setUpWorkingCopyJob;
+        boolean workingCopyAcquired;
+        boolean disposed;
+    }
+}
diff --git a/org.eclipse.handly.ui/src/org/eclipse/handly/ui/viewer/DeferredElementTreeContentProvider.java b/org.eclipse.handly.ui/src/org/eclipse/handly/ui/viewer/DeferredElementTreeContentProvider.java
new file mode 100644
index 0000000..2174b10
--- /dev/null
+++ b/org.eclipse.handly.ui/src/org/eclipse/handly/ui/viewer/DeferredElementTreeContentProvider.java
@@ -0,0 +1,143 @@
+/*******************************************************************************
+ * Copyright (c) 2020 1C-Soft LLC.
+ *
+ * This program and the accompanying materials are made available under
+ * the terms of the Eclipse Public License 2.0 which is available at
+ * https://www.eclipse.org/legal/epl-2.0/
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ *
+ * Contributors:
+ *     Vladimir Piskarev (1C) - initial API and implementation
+ *******************************************************************************/
+package org.eclipse.handly.ui.viewer;
+
+import static org.eclipse.handly.context.Contexts.EMPTY_CONTEXT;
+
+import org.eclipse.core.runtime.CoreException;
+import org.eclipse.core.runtime.IProgressMonitor;
+import org.eclipse.core.runtime.jobs.ISchedulingRule;
+import org.eclipse.handly.internal.ui.Activator;
+import org.eclipse.handly.model.Elements;
+import org.eclipse.handly.model.IElement;
+import org.eclipse.handly.model.ISourceFile;
+import org.eclipse.jface.viewers.AbstractTreeViewer;
+import org.eclipse.ui.IWorkbenchPartSite;
+import org.eclipse.ui.progress.IElementCollector;
+
+/**
+ * A deferred content provider for {@link IElement}s.
+ * Uses the existing structure of the elements.
+ *
+ * @since 1.5
+ */
+public class DeferredElementTreeContentProvider
+    extends DeferredTreeContentProvider
+{
+    /**
+     * A zero-length array of the runtime type <code>Object[]</code>.
+     */
+    protected static final Object[] NO_CHILDREN = new Object[0];
+
+    /**
+     * Creates a new instance of the content provider.
+     *
+     * @param viewer the tree viewer that will use this content provider
+     *  (not <code>null</code>)
+     * @param site the site of the containing workbench part
+     *  (may be <code>null</code>)
+     */
+    public DeferredElementTreeContentProvider(AbstractTreeViewer viewer,
+        IWorkbenchPartSite site)
+    {
+        super(viewer, site);
+    }
+
+    @Override
+    public Object[] getElements(Object inputElement)
+    {
+        return getChildren(inputElement);
+    }
+
+    @Override
+    public Object[] getChildren(Object parentElement)
+    {
+        if (parentElement instanceof ISourceFile && !Elements.isWorkingCopy(
+            (ISourceFile)parentElement))
+            return getDeferredTreeContentManager().getChildren(parentElement);
+
+        return getChildren(parentElement, null);
+    }
+
+    @Override
+    public Object getParent(Object element)
+    {
+        if (element instanceof IElement)
+            return Elements.getParent((IElement)element);
+
+        return null;
+    }
+
+    @Override
+    public boolean hasChildren(Object element)
+    {
+        if (element instanceof ISourceFile && !Elements.isWorkingCopy(
+            (ISourceFile)element))
+            return true;
+
+        return getChildren(element, null).length > 0;
+    }
+
+    @Override
+    protected void fetchDeferredChildren(Object parentElement,
+        IElementCollector collector, IProgressMonitor monitor)
+    {
+        collector.add(getChildren(parentElement, monitor), null);
+        collector.done();
+    }
+
+    /**
+     * Returns the child elements of the given parent element.
+     * <p>
+     * Default implementation invokes <code>Elements.getChildren((IElement)parentElement,
+     * EMPTY_CONTEXT, monitor)</code> if the parent element is an IElement.
+     * Subclasses may override or extend.
+     * </p>
+     *
+     * @param parentElement the parent element
+     * @param monitor a progress monitor to support reporting and cancellation
+     *  (may be <code>null</code>)
+     * @return an array of child elements (not <code>null</code>)
+     */
+    protected Object[] getChildren(Object parentElement,
+        IProgressMonitor monitor)
+    {
+        if (parentElement instanceof IElement)
+        {
+            try
+            {
+                return Elements.getChildren((IElement)parentElement,
+                    EMPTY_CONTEXT, monitor);
+            }
+            catch (CoreException e)
+            {
+                Activator.logError(e);
+            }
+        }
+        return NO_CHILDREN;
+    }
+
+    /**
+     * Returns the rule used to schedule the deferred fetching of children
+     * for the given parent element.
+     * <p>
+     * {@link DeferredElementTreeContentProvider}'s implementation
+     * of this method always returns <code>null</code>.
+     * </p>
+     */
+    @Override
+    protected ISchedulingRule getRule(Object parentElement)
+    {
+        return null;
+    }
+}
diff --git a/org.eclipse.handly/src/org/eclipse/handly/model/impl/DefaultWorkingCopyCallback.java b/org.eclipse.handly/src/org/eclipse/handly/model/impl/DefaultWorkingCopyCallback.java
index 14b3211..2e790ea 100644
--- a/org.eclipse.handly/src/org/eclipse/handly/model/impl/DefaultWorkingCopyCallback.java
+++ b/org.eclipse.handly/src/org/eclipse/handly/model/impl/DefaultWorkingCopyCallback.java
@@ -1,5 +1,5 @@
 /*******************************************************************************
- * Copyright (c) 2014, 2017 1C-Soft LLC and others.
+ * Copyright (c) 2014, 2020 1C-Soft LLC and others.
  *
  * This program and the accompanying materials are made available under
  * the terms of the Eclipse Public License 2.0 which is available at
@@ -22,6 +22,7 @@
 
 import org.eclipse.core.runtime.CoreException;
 import org.eclipse.core.runtime.IProgressMonitor;
+import org.eclipse.core.runtime.OperationCanceledException;
 import org.eclipse.handly.context.IContext;
 import org.eclipse.handly.snapshot.ISnapshot;
 import org.eclipse.handly.snapshot.NonExpiringSnapshot;
@@ -55,6 +56,9 @@
 
         synchronized (reconcilingLock)
         {
+            if (monitor.isCanceled())
+                throw new OperationCanceledException();
+
             boolean needsReconciling = needsReconciling();
             if (needsReconciling || context.getOrDefault(FORCE_RECONCILING))
             {
diff --git a/org.eclipse.handly/src/org/eclipse/handly/model/impl/support/ISourceFileImplSupport.java b/org.eclipse.handly/src/org/eclipse/handly/model/impl/support/ISourceFileImplSupport.java
index d800f18..2d0de9f 100644
--- a/org.eclipse.handly/src/org/eclipse/handly/model/impl/support/ISourceFileImplSupport.java
+++ b/org.eclipse.handly/src/org/eclipse/handly/model/impl/support/ISourceFileImplSupport.java
@@ -135,7 +135,8 @@
         throws CoreException
     {
         SubMonitor subMonitor = SubMonitor.convert(monitor, 100);
-        if (!acquireExistingWorkingCopy_(subMonitor.split(10)))
+        if (!isWorkingCopy_() || !acquireExistingWorkingCopy_(subMonitor.split(
+            10)))
         {
             return getFileBuffer_(context, subMonitor.split(90));
         }
@@ -240,7 +241,7 @@
     @Override
     default boolean needsReconciling_()
     {
-        if (!acquireExistingWorkingCopy_(null))
+        if (!isWorkingCopy_() || !acquireExistingWorkingCopy_(null))
             return false;
         else
         {
@@ -277,7 +278,8 @@
         throws CoreException
     {
         SubMonitor subMonitor = SubMonitor.convert(monitor, 100);
-        if (!acquireExistingWorkingCopy_(subMonitor.split(10)))
+        if (!isWorkingCopy_() || !acquireExistingWorkingCopy_(subMonitor.split(
+            10)))
             return; // not a working copy
         else
         {