Bug 506332 - Revise working copy related APIs
diff --git a/org.eclipse.handly.examples.basic.ui.tests/src/org/eclipse/handly/internal/examples/basic/ui/model/FooWorkingCopyNotificationTest.java b/org.eclipse.handly.examples.basic.ui.tests/src/org/eclipse/handly/internal/examples/basic/ui/model/FooWorkingCopyNotificationTest.java
index 6baad1b..8ce8104 100644
--- a/org.eclipse.handly.examples.basic.ui.tests/src/org/eclipse/handly/internal/examples/basic/ui/model/FooWorkingCopyNotificationTest.java
+++ b/org.eclipse.handly.examples.basic.ui.tests/src/org/eclipse/handly/internal/examples/basic/ui/model/FooWorkingCopyNotificationTest.java
@@ -10,6 +10,7 @@
  *******************************************************************************/
 package org.eclipse.handly.internal.examples.basic.ui.model;
 
+import static org.eclipse.handly.context.Contexts.EMPTY_CONTEXT;
 import static org.eclipse.handly.model.IElementDeltaConstants.F_CONTENT;
 import static org.eclipse.handly.model.IElementDeltaConstants.F_UNDERLYING_RESOURCE;
 import static org.eclipse.handly.model.IElementDeltaConstants.F_WORKING_COPY;
@@ -18,9 +19,7 @@
 import org.eclipse.core.runtime.CoreException;
 import org.eclipse.core.runtime.IProgressMonitor;
 import org.eclipse.handly.buffer.BufferChange;
-import org.eclipse.handly.buffer.IBuffer;
 import org.eclipse.handly.buffer.SaveMode;
-import org.eclipse.handly.buffer.TextFileBuffer;
 import org.eclipse.handly.examples.basic.ui.model.FooModelCore;
 import org.eclipse.handly.examples.basic.ui.model.IFooDef;
 import org.eclipse.handly.examples.basic.ui.model.IFooElement;
@@ -44,7 +43,6 @@
     extends WorkspaceTestCase
 {
     private FooFile workingCopy;
-    private IBuffer buffer;
     private IFooModel fooModel = FooModelCore.getFooModel();
     private FooModelListener listener = new FooModelListener();
 
@@ -55,14 +53,11 @@
         IFooProject fooProject = FooModelCore.create(setUpProject("Test002"));
         workingCopy = (FooFile)fooProject.getFooFile("test.foo");
         fooModel.addElementChangeListener(listener);
-        buffer = TextFileBuffer.forFile(workingCopy.getFile());
     }
 
     @Override
     protected void tearDown() throws Exception
     {
-        if (buffer != null)
-            buffer.release();
         fooModel.removeElementChangeListener(listener);
         super.tearDown();
     }
@@ -109,7 +104,8 @@
                 BufferChange change = new BufferChange(new ReplaceEdit(
                     r.getOffset(), r.getLength(), "g")); // rename f() to g()
                 change.setSaveMode(SaveMode.LEAVE_UNSAVED);
-                buffer.applyChange(change, null);
+                workingCopy.hWorkingCopyInfo().getBuffer().applyChange(change,
+                    null);
 
                 assertDelta(null, listener.delta);
 
@@ -148,7 +144,8 @@
                 BufferChange change = new BufferChange(new DeleteEdit(
                     r.getOffset(), r.getLength())); // delete 'var y;'
                 change.setSaveMode(SaveMode.LEAVE_UNSAVED);
-                buffer.applyChange(change, null);
+                workingCopy.hWorkingCopyInfo().getBuffer().applyChange(change,
+                    null);
 
                 assertDelta(null, listener.delta);
 
@@ -165,7 +162,8 @@
                 change = // insert 'var y;' before 'var x;'
                     new BufferChange(new InsertEdit(r.getOffset(), varYText));
                 change.setSaveMode(SaveMode.LEAVE_UNSAVED);
-                buffer.applyChange(change, null);
+                workingCopy.hWorkingCopyInfo().getBuffer().applyChange(change,
+                    null);
 
                 assertDelta(null, listener.delta);
 
@@ -197,7 +195,8 @@
                 BufferChange change = new BufferChange(new ReplaceEdit(
                     r.getOffset(), r.getLength(), "def f(y) {}")); // instead of 'def f(x) {}'
                 change.setSaveMode(SaveMode.LEAVE_UNSAVED);
-                buffer.applyChange(change, null);
+                workingCopy.hWorkingCopyInfo().getBuffer().applyChange(change,
+                    null);
 
                 assertDelta(null, listener.delta);
 
@@ -212,14 +211,14 @@
     private void doWithWorkingCopy(IWorkspaceRunnable runnable)
         throws CoreException
     {
-        workingCopy.hBecomeWorkingCopy(buffer, null);
+        workingCopy.hBecomeWorkingCopy(EMPTY_CONTEXT, null);
         try
         {
             runnable.run(null);
         }
         finally
         {
-            workingCopy.hDiscardWorkingCopy();
+            workingCopy.hReleaseWorkingCopy();
         }
     }
 
diff --git a/org.eclipse.handly.examples.basic.ui.tests/src/org/eclipse/handly/internal/examples/basic/ui/model/FooWorkingCopyTest.java b/org.eclipse.handly.examples.basic.ui.tests/src/org/eclipse/handly/internal/examples/basic/ui/model/FooWorkingCopyTest.java
index 9c144a3..6b18725 100644
--- a/org.eclipse.handly.examples.basic.ui.tests/src/org/eclipse/handly/internal/examples/basic/ui/model/FooWorkingCopyTest.java
+++ b/org.eclipse.handly.examples.basic.ui.tests/src/org/eclipse/handly/internal/examples/basic/ui/model/FooWorkingCopyTest.java
@@ -10,13 +10,16 @@
  *******************************************************************************/
 package org.eclipse.handly.internal.examples.basic.ui.model;
 
+import static org.eclipse.handly.context.Contexts.*;
+
 import org.eclipse.core.resources.IWorkspaceRunnable;
 import org.eclipse.core.runtime.CoreException;
 import org.eclipse.core.runtime.IProgressMonitor;
+import org.eclipse.handly.buffer.Buffer;
 import org.eclipse.handly.buffer.BufferChange;
 import org.eclipse.handly.buffer.IBuffer;
 import org.eclipse.handly.buffer.SaveMode;
-import org.eclipse.handly.buffer.TextFileBuffer;
+import org.eclipse.handly.context.IContext;
 import org.eclipse.handly.examples.basic.ui.model.FooModelCore;
 import org.eclipse.handly.examples.basic.ui.model.IFooDef;
 import org.eclipse.handly.examples.basic.ui.model.IFooProject;
@@ -24,6 +27,7 @@
 import org.eclipse.handly.junit.WorkspaceTestCase;
 import org.eclipse.handly.model.ISourceElementInfo;
 import org.eclipse.handly.model.impl.Element;
+import org.eclipse.handly.model.impl.SourceFile;
 import org.eclipse.handly.model.impl.WorkingCopyInfo;
 import org.eclipse.handly.util.TextRange;
 import org.eclipse.text.edits.DeleteEdit;
@@ -37,7 +41,6 @@
     extends WorkspaceTestCase
 {
     private FooFile workingCopy;
-    private IBuffer buffer;
 
     @Override
     protected void setUp() throws Exception
@@ -45,20 +48,11 @@
         super.setUp();
         IFooProject fooProject = FooModelCore.create(setUpProject("Test002"));
         workingCopy = (FooFile)fooProject.getFooFile("test.foo");
-        buffer = TextFileBuffer.forFile(workingCopy.getFile());
-    }
-
-    @Override
-    protected void tearDown() throws Exception
-    {
-        if (buffer != null)
-            buffer.release();
-        super.tearDown();
     }
 
     public void test1() throws Exception
     {
-        doWithWorkingCopy(new IWorkspaceRunnable()
+        doWithWorkingCopy(EMPTY_CONTEXT, new IWorkspaceRunnable()
         {
             @Override
             public void run(IProgressMonitor monitor) throws CoreException
@@ -73,7 +67,8 @@
                 BufferChange change = new BufferChange(new ReplaceEdit(
                     r.getOffset(), r.getLength(), "g"));
                 change.setSaveMode(SaveMode.LEAVE_UNSAVED);
-                buffer.applyChange(change, null);
+                workingCopy.hWorkingCopyInfo().getBuffer().applyChange(change,
+                    null);
 
                 defs = workingCopy.getDefs();
                 assertEquals(3, defs.length);
@@ -92,7 +87,7 @@
 
     public void test2() throws Exception
     {
-        doWithWorkingCopy(new IWorkspaceRunnable()
+        doWithWorkingCopy(EMPTY_CONTEXT, new IWorkspaceRunnable()
         {
             @Override
             public void run(IProgressMonitor monitor) throws CoreException
@@ -112,7 +107,8 @@
                 BufferChange change = new BufferChange(new DeleteEdit(
                     r.getOffset(), r.getLength()));
                 change.setSaveMode(SaveMode.LEAVE_UNSAVED);
-                buffer.applyChange(change, null);
+                workingCopy.hWorkingCopyInfo().getBuffer().applyChange(change,
+                    null);
 
                 vars = workingCopy.getVars();
                 assertEquals(2, vars.length);
@@ -132,7 +128,8 @@
                 change = new BufferChange(new InsertEdit(r.getOffset(),
                     var2Text));
                 change.setSaveMode(SaveMode.LEAVE_UNSAVED);
-                buffer.applyChange(change, null);
+                workingCopy.hWorkingCopyInfo().getBuffer().applyChange(change,
+                    null);
 
                 vars = workingCopy.getVars();
                 assertEquals(1, vars.length);
@@ -151,20 +148,27 @@
     {
         // working copy for a non-existing source file
         workingCopy.getParent().getResource().delete(true, null);
-        doWithWorkingCopy(new IWorkspaceRunnable()
+        try (
+            IBuffer buffer = new Buffer(
+                "var x; var y; def f() {} def f(x) {} def f(x, y) {}"))
         {
-            @Override
-            public void run(IProgressMonitor monitor) throws CoreException
-            {
-                assertFalse(workingCopy.getParent().exists());
-                assertTrue(workingCopy.exists());
+            doWithWorkingCopy(of(SourceFile.WORKING_COPY_BUFFER, buffer),
+                new IWorkspaceRunnable()
+                {
+                    @Override
+                    public void run(IProgressMonitor monitor)
+                        throws CoreException
+                    {
+                        assertFalse(workingCopy.getParent().exists());
+                        assertTrue(workingCopy.exists());
 
-                IFooVar[] vars = workingCopy.getVars();
-                assertEquals(2, vars.length);
-                IFooDef[] defs = workingCopy.getDefs();
-                assertEquals(3, defs.length);
-            }
-        });
+                        IFooVar[] vars = workingCopy.getVars();
+                        assertEquals(2, vars.length);
+                        IFooDef[] defs = workingCopy.getDefs();
+                        assertEquals(3, defs.length);
+                    }
+                });
+        }
     }
 
     public void testBug479623() throws Exception
@@ -179,7 +183,8 @@
             {
                 while (!stop[0])
                 {
-                    WorkingCopyInfo info = workingCopy.hAcquireWorkingCopy();
+                    WorkingCopyInfo info =
+                        workingCopy.hAcquireExistingWorkingCopy(null);
                     if (info != null)
                     {
                         try
@@ -192,7 +197,7 @@
                         }
                         finally
                         {
-                            workingCopy.hDiscardWorkingCopy();
+                            workingCopy.hReleaseWorkingCopy();
                         }
                     }
                 }
@@ -201,7 +206,7 @@
         thread.start();
         try
         {
-            doWithWorkingCopy(new IWorkspaceRunnable()
+            doWithWorkingCopy(EMPTY_CONTEXT, new IWorkspaceRunnable()
             {
                 @Override
                 public void run(IProgressMonitor monitor) throws CoreException
@@ -221,7 +226,7 @@
     public void testBug480397_2() throws Exception
     {
         // working copy cannot be closed
-        doWithWorkingCopy(new IWorkspaceRunnable()
+        doWithWorkingCopy(EMPTY_CONTEXT, new IWorkspaceRunnable()
         {
             @Override
             public void run(IProgressMonitor monitor) throws CoreException
@@ -253,21 +258,21 @@
             }
         };
         // non-openable elements cannot be closed, in working copy or not
-        doWithWorkingCopy(testRunnable);
+        doWithWorkingCopy(EMPTY_CONTEXT, testRunnable);
         testRunnable.run(null);
     }
 
-    private void doWithWorkingCopy(IWorkspaceRunnable runnable)
-        throws CoreException
+    private void doWithWorkingCopy(IContext context,
+        IWorkspaceRunnable runnable) throws CoreException
     {
-        workingCopy.hBecomeWorkingCopy(buffer, null);
+        workingCopy.hBecomeWorkingCopy(context, null);
         try
         {
             runnable.run(null);
         }
         finally
         {
-            workingCopy.hDiscardWorkingCopy();
+            workingCopy.hReleaseWorkingCopy();
         }
     }
 }
diff --git a/org.eclipse.handly.examples.basic.ui/src/org/eclipse/handly/internal/examples/basic/ui/model/FooFile.java b/org.eclipse.handly.examples.basic.ui/src/org/eclipse/handly/internal/examples/basic/ui/model/FooFile.java
index c05c23d..9a446bb 100644
--- a/org.eclipse.handly.examples.basic.ui/src/org/eclipse/handly/internal/examples/basic/ui/model/FooFile.java
+++ b/org.eclipse.handly.examples.basic.ui/src/org/eclipse/handly/internal/examples/basic/ui/model/FooFile.java
@@ -91,12 +91,27 @@
     }
 
     @Override
-    protected void hBuildStructure(Object ast, IContext context,
-        IProgressMonitor monitor)
+    protected void hBuildStructure(IContext context, IProgressMonitor monitor)
+        throws CoreException
     {
         Map<IElement, Object> newElements = context.get(NEW_ELEMENTS);
         SourceElementBody body = new SourceElementBody();
-        XtextResource resource = (XtextResource)ast;
+
+        XtextResource resource = (XtextResource)context.get(SOURCE_AST);
+        if (resource == null)
+        {
+            try
+            {
+                resource = parse(context.get(SOURCE_CONTENTS),
+                    getFile().getCharset());
+            }
+            catch (IOException e)
+            {
+                throw new CoreException(Activator.createErrorStatus(
+                    e.getMessage(), e));
+            }
+        }
+
         IParseResult parseResult = resource.getParseResult();
         if (parseResult != null)
         {
@@ -108,32 +123,8 @@
                 builder.buildStructure(this, body, (Module)root, monitor);
             }
         }
-        newElements.put(this, body);
-    }
 
-    /**
-     * Returns a new <code>XtextResource</code> loaded from the given source
-     * string. The resource is created in a new <code>ResourceSet</code>
-     * obtained from the <code>IResourceSetProvider</code> corresponding to
-     * this file.
-     *
-     * @return the new <code>XtextResource</code> loaded from the given source
-     *  string (never <code>null</code>)
-     * @throws CoreException if resource loading failed
-     */
-    @Override
-    protected XtextResource hCreateAst(String source, IContext context,
-        IProgressMonitor monitor) throws CoreException
-    {
-        try
-        {
-            return parse(source, getFile().getCharset());
-        }
-        catch (IOException e)
-        {
-            throw new CoreException(Activator.createErrorStatus(e.getMessage(),
-                e));
-        }
+        newElements.put(this, body);
     }
 
     /**
diff --git a/org.eclipse.handly.examples.javamodel.tests/src/org/eclipse/handly/internal/examples/javamodel/WorkingCopyNotificationTest.java b/org.eclipse.handly.examples.javamodel.tests/src/org/eclipse/handly/internal/examples/javamodel/WorkingCopyNotificationTest.java
index f07eb20..71cacd3 100644
--- a/org.eclipse.handly.examples.javamodel.tests/src/org/eclipse/handly/internal/examples/javamodel/WorkingCopyNotificationTest.java
+++ b/org.eclipse.handly.examples.javamodel.tests/src/org/eclipse/handly/internal/examples/javamodel/WorkingCopyNotificationTest.java
@@ -10,15 +10,15 @@
  *******************************************************************************/
 package org.eclipse.handly.internal.examples.javamodel;
 
+import static org.eclipse.handly.context.Contexts.EMPTY_CONTEXT;
+
 import org.eclipse.core.resources.IProject;
 import org.eclipse.core.resources.IWorkspaceRunnable;
 import org.eclipse.core.runtime.CoreException;
 import org.eclipse.core.runtime.IProgressMonitor;
 import org.eclipse.core.runtime.Path;
 import org.eclipse.handly.buffer.BufferChange;
-import org.eclipse.handly.buffer.IBuffer;
 import org.eclipse.handly.buffer.SaveMode;
-import org.eclipse.handly.buffer.TextFileBuffer;
 import org.eclipse.handly.examples.javamodel.ICompilationUnit;
 import org.eclipse.handly.examples.javamodel.IField;
 import org.eclipse.handly.examples.javamodel.IMethod;
@@ -37,7 +37,6 @@
     extends WorkspaceTestCase
 {
     private CompilationUnit workingCopy;
-    private IBuffer buffer;
     private JavaModelListener listener = new JavaModelListener();
 
     @Override
@@ -47,7 +46,6 @@
         IProject project = setUpProject("Test010");
         workingCopy = (CompilationUnit)JavaModelCore.createCompilationUnitFrom(
             project.getFile(new Path("src/X.java")));
-        buffer = TextFileBuffer.forFile(workingCopy.getFile());
         workingCopy.getJavaModel().addElementChangeListener(listener);
     }
 
@@ -56,8 +54,6 @@
     {
         if (workingCopy != null)
             workingCopy.getJavaModel().removeElementChangeListener(listener);
-        if (buffer != null)
-            buffer.release();
         super.tearDown();
     }
 
@@ -115,7 +111,8 @@
                 BufferChange change = new BufferChange(new ReplaceEdit(
                     r.getOffset(), r.getLength(), "Y"));
                 change.setSaveMode(SaveMode.LEAVE_UNSAVED);
-                buffer.applyChange(change, null);
+                workingCopy.hWorkingCopyInfo().getBuffer().applyChange(change,
+                    null);
 
                 assertNull(listener.delta);
 
@@ -145,7 +142,8 @@
                 BufferChange change = new BufferChange(new DeleteEdit(
                     r.getOffset(), r.getLength()));
                 change.setSaveMode(SaveMode.LEAVE_UNSAVED);
-                buffer.applyChange(change, null);
+                workingCopy.hWorkingCopyInfo().getBuffer().applyChange(change,
+                    null);
 
                 assertNull(listener.delta);
 
@@ -164,7 +162,8 @@
                 change = new BufferChange(new InsertEdit(r.getOffset(),
                     "int y;"));
                 change.setSaveMode(SaveMode.LEAVE_UNSAVED);
-                buffer.applyChange(change, null);
+                workingCopy.hWorkingCopyInfo().getBuffer().applyChange(change,
+                    null);
 
                 assertNull(listener.delta);
 
@@ -195,7 +194,8 @@
                 BufferChange change = new BufferChange(new ReplaceEdit(
                     r.getOffset(), r.getLength(), "void f() {}"));
                 change.setSaveMode(SaveMode.LEAVE_UNSAVED);
-                buffer.applyChange(change, null);
+                workingCopy.hWorkingCopyInfo().getBuffer().applyChange(change,
+                    null);
 
                 assertNull(listener.delta);
 
@@ -227,7 +227,8 @@
                 BufferChange change = new BufferChange(new ReplaceEdit(
                     r.getOffset(), r.getLength(), "void f(int y) {}")); // renamed arg
                 change.setSaveMode(SaveMode.LEAVE_UNSAVED);
-                buffer.applyChange(change, null);
+                workingCopy.hWorkingCopyInfo().getBuffer().applyChange(change,
+                    null);
 
                 assertNull(listener.delta);
 
@@ -247,14 +248,14 @@
     private void doWithWorkingCopy(IWorkspaceRunnable runnable)
         throws CoreException
     {
-        workingCopy.hBecomeWorkingCopy(buffer, null);
+        workingCopy.hBecomeWorkingCopy(EMPTY_CONTEXT, null);
         try
         {
             runnable.run(null);
         }
         finally
         {
-            workingCopy.hDiscardWorkingCopy();
+            workingCopy.hReleaseWorkingCopy();
         }
     }
 }
diff --git a/org.eclipse.handly.examples.javamodel.tests/src/org/eclipse/handly/internal/examples/javamodel/WorkingCopyTest.java b/org.eclipse.handly.examples.javamodel.tests/src/org/eclipse/handly/internal/examples/javamodel/WorkingCopyTest.java
index 7c9185e..914b40c 100644
--- a/org.eclipse.handly.examples.javamodel.tests/src/org/eclipse/handly/internal/examples/javamodel/WorkingCopyTest.java
+++ b/org.eclipse.handly.examples.javamodel.tests/src/org/eclipse/handly/internal/examples/javamodel/WorkingCopyTest.java
@@ -10,6 +10,8 @@
  *******************************************************************************/
 package org.eclipse.handly.internal.examples.javamodel;
 
+import static org.eclipse.handly.context.Contexts.of;
+
 import java.util.ArrayList;
 import java.util.List;
 
@@ -22,13 +24,14 @@
 import org.eclipse.handly.buffer.ChildBuffer;
 import org.eclipse.handly.buffer.IBuffer;
 import org.eclipse.handly.buffer.SaveMode;
-import org.eclipse.handly.buffer.TextFileBuffer;
+import org.eclipse.handly.context.IContext;
 import org.eclipse.handly.examples.javamodel.ICompilationUnit;
 import org.eclipse.handly.examples.javamodel.IField;
 import org.eclipse.handly.examples.javamodel.IMethod;
 import org.eclipse.handly.examples.javamodel.IType;
 import org.eclipse.handly.examples.javamodel.JavaModelCore;
 import org.eclipse.handly.junit.WorkspaceTestCase;
+import org.eclipse.handly.model.impl.SourceFile;
 import org.eclipse.handly.util.TextRange;
 import org.eclipse.jdt.core.IProblemRequestor;
 import org.eclipse.jdt.core.WorkingCopyOwner;
@@ -50,7 +53,6 @@
     private static final int AST_LEVEL = AST.JLS8;
 
     private CompilationUnit workingCopy;
-    private IBuffer buffer;
     private List<IProblem> problems;
     private IProblemRequestor problemRequestor = new ProblemRequestor();
 
@@ -61,18 +63,9 @@
         IProject project = setUpProject("Test010");
         workingCopy = (CompilationUnit)JavaModelCore.createCompilationUnitFrom(
             project.getFile(new Path("src/X.java")));
-        buffer = TextFileBuffer.forFile(workingCopy.getFile());
         problems = new ArrayList<IProblem>();
     }
 
-    @Override
-    protected void tearDown() throws Exception
-    {
-        if (buffer != null)
-            buffer.release();
-        super.tearDown();
-    }
-
     public void test001() throws Exception
     {
         doWithWorkingCopy(new IWorkspaceRunnable()
@@ -89,7 +82,8 @@
                 BufferChange change = new BufferChange(new ReplaceEdit(
                     r.getOffset(), r.getLength(), "Y"));
                 change.setSaveMode(SaveMode.LEAVE_UNSAVED);
-                buffer.applyChange(change, null);
+                workingCopy.hWorkingCopyInfo().getBuffer().applyChange(change,
+                    null);
 
                 types = workingCopy.getTypes();
                 assertEquals(1, types.length);
@@ -123,7 +117,8 @@
                 BufferChange change = new BufferChange(new DeleteEdit(
                     r.getOffset(), r.getLength()));
                 change.setSaveMode(SaveMode.LEAVE_UNSAVED);
-                buffer.applyChange(change, null);
+                workingCopy.hWorkingCopyInfo().getBuffer().applyChange(change,
+                    null);
 
                 fields = typeX.getFields();
                 assertEquals(1, fields.length);
@@ -138,7 +133,8 @@
                 change = new BufferChange(new InsertEdit(r.getOffset(),
                     "int y;"));
                 change.setSaveMode(SaveMode.LEAVE_UNSAVED);
-                buffer.applyChange(change, null);
+                workingCopy.hWorkingCopyInfo().getBuffer().applyChange(change,
+                    null);
 
                 fields = typeX.getFields();
                 assertEquals(0, fields.length);
@@ -169,7 +165,8 @@
                 BufferChange change = new BufferChange(new ReplaceEdit(
                     r.getOffset(), r.getLength(), "void f() {}"));
                 change.setSaveMode(SaveMode.LEAVE_UNSAVED);
-                buffer.applyChange(change, null);
+                workingCopy.hWorkingCopyInfo().getBuffer().applyChange(change,
+                    null);
 
                 methods = typeX.getMethods();
                 assertEquals(1, methods.length);
@@ -313,9 +310,12 @@
                     {
                     });
                 assertFalse(privateCopy.equals(workingCopy));
-                try (IBuffer privateBuffer = new ChildBuffer(buffer))
+                try (
+                    IBuffer buffer = workingCopy.getBuffer();
+                    IBuffer privateBuffer = new ChildBuffer(buffer))
                 {
-                    doWithWorkingCopy(privateCopy, privateBuffer, null,
+                    doWithWorkingCopy(privateCopy, of(
+                        SourceFile.WORKING_COPY_BUFFER, privateBuffer),
                         new IWorkspaceRunnable()
                         {
                             public void run(IProgressMonitor monitor)
@@ -362,22 +362,21 @@
     private void doWithWorkingCopy(IWorkspaceRunnable runnable)
         throws CoreException
     {
-        doWithWorkingCopy(workingCopy, buffer, problemRequestor, runnable);
+        doWithWorkingCopy(workingCopy, of(IProblemRequestor.class,
+            problemRequestor), runnable);
     }
 
-    private static void doWithWorkingCopy(CompilationUnit workingCopy,
-        IBuffer buffer, IProblemRequestor problemRequestor,
+    private static void doWithWorkingCopy(CompilationUnit cu, IContext context,
         IWorkspaceRunnable runnable) throws CoreException
     {
-        workingCopy.hBecomeWorkingCopy(buffer, (IBuffer b) ->
-            new JavaWorkingCopyInfo(b, problemRequestor), null);
+        cu.hBecomeWorkingCopy(context, null);
         try
         {
             runnable.run(null);
         }
         finally
         {
-            workingCopy.hDiscardWorkingCopy();
+            cu.hReleaseWorkingCopy();
         }
     }
 
diff --git a/org.eclipse.handly.examples.javamodel.ui/src/org/eclipse/handly/internal/examples/javamodel/ui/editor/CompilatonUnitDocumentProvider.java b/org.eclipse.handly.examples.javamodel.ui/src/org/eclipse/handly/internal/examples/javamodel/ui/editor/CompilatonUnitDocumentProvider.java
index bc7b965..5d00c63 100644
--- a/org.eclipse.handly.examples.javamodel.ui/src/org/eclipse/handly/internal/examples/javamodel/ui/editor/CompilatonUnitDocumentProvider.java
+++ b/org.eclipse.handly.examples.javamodel.ui/src/org/eclipse/handly/internal/examples/javamodel/ui/editor/CompilatonUnitDocumentProvider.java
@@ -1,5 +1,5 @@
 /*******************************************************************************
- * Copyright (c) 2015 1C-Soft LLC.
+ * Copyright (c) 2015, 2016 1C-Soft LLC.
  * 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
@@ -10,13 +10,12 @@
  *******************************************************************************/
 package org.eclipse.handly.internal.examples.javamodel.ui.editor;
 
-import org.eclipse.handly.buffer.IBuffer;
-import org.eclipse.handly.internal.examples.javamodel.CompilationUnit;
-import org.eclipse.handly.internal.examples.javamodel.JavaWorkingCopyInfo;
+import org.eclipse.handly.examples.javamodel.ICompilationUnit;
 import org.eclipse.handly.internal.examples.javamodel.ui.JavaInputElementProvider;
-import org.eclipse.handly.model.impl.IWorkingCopyInfoFactory;
-import org.eclipse.handly.model.impl.SourceFile;
+import org.eclipse.handly.model.IElement;
+import org.eclipse.handly.model.ISourceFile;
 import org.eclipse.handly.ui.texteditor.SourceFileDocumentProvider;
+import org.eclipse.ui.IEditorInput;
 
 /**
  * Compilation unit document provider.
@@ -24,24 +23,15 @@
 public class CompilatonUnitDocumentProvider
     extends SourceFileDocumentProvider
 {
-    public CompilatonUnitDocumentProvider()
-    {
-        super(JavaInputElementProvider.INSTANCE);
-    }
-
     @Override
-    protected SourceFile getSourceFile(Object element)
+    protected ISourceFile getSourceFile(Object input)
     {
-        SourceFile sourceFile = super.getSourceFile(element);
-        if (!(sourceFile instanceof CompilationUnit))
+        if (!(input instanceof IEditorInput))
             return null;
-        return sourceFile;
-    }
-
-    @Override
-    protected IWorkingCopyInfoFactory getWorkingCopyInfoFactory(
-        SourceFile sourceFile, Object element, FileInfo fileInfo)
-    {
-        return (IBuffer buffer) -> new JavaWorkingCopyInfo(buffer, null);
+        IElement element = JavaInputElementProvider.INSTANCE.getElement(
+            (IEditorInput)input);
+        if (!(element instanceof ICompilationUnit))
+            return null;
+        return (ICompilationUnit)element;
     }
 }
diff --git a/org.eclipse.handly.examples.javamodel.ui/src/org/eclipse/handly/internal/examples/javamodel/ui/editor/JavaReconciler.java b/org.eclipse.handly.examples.javamodel.ui/src/org/eclipse/handly/internal/examples/javamodel/ui/editor/JavaReconciler.java
index a7f956e..cae9c41 100644
--- a/org.eclipse.handly.examples.javamodel.ui/src/org/eclipse/handly/internal/examples/javamodel/ui/editor/JavaReconciler.java
+++ b/org.eclipse.handly.examples.javamodel.ui/src/org/eclipse/handly/internal/examples/javamodel/ui/editor/JavaReconciler.java
@@ -1,5 +1,5 @@
 /*******************************************************************************
- * Copyright (c) 2015 1C-Soft LLC.
+ * Copyright (c) 2015, 2016 1C-Soft LLC.
  * 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
@@ -13,14 +13,14 @@
 import org.eclipse.handly.examples.javamodel.JavaModelCore;
 import org.eclipse.handly.model.IElementChangeListener;
 import org.eclipse.handly.ui.IWorkingCopyManager;
-import org.eclipse.handly.ui.text.reconciler.HandlyReconciler;
+import org.eclipse.handly.ui.text.reconciler.EditorWorkingCopyReconciler;
 import org.eclipse.ui.texteditor.ITextEditor;
 
 /**
  * Reconciler for a Java specific text editor.
  */
 public class JavaReconciler
-    extends HandlyReconciler
+    extends EditorWorkingCopyReconciler
 {
     /**
      * Creates a new Java reconciler.
diff --git a/org.eclipse.handly.examples.javamodel/src/org/eclipse/handly/internal/examples/javamodel/CompilationUnit.java b/org.eclipse.handly.examples.javamodel/src/org/eclipse/handly/internal/examples/javamodel/CompilationUnit.java
index d7e072c..ea578ae 100644
--- a/org.eclipse.handly.examples.javamodel/src/org/eclipse/handly/internal/examples/javamodel/CompilationUnit.java
+++ b/org.eclipse.handly.examples.javamodel/src/org/eclipse/handly/internal/examples/javamodel/CompilationUnit.java
@@ -11,6 +11,8 @@
 package org.eclipse.handly.internal.examples.javamodel;
 
 import static org.eclipse.handly.context.Contexts.EMPTY_CONTEXT;
+import static org.eclipse.handly.context.Contexts.of;
+import static org.eclipse.handly.context.Contexts.with;
 import static org.eclipse.handly.model.Elements.FORCE_RECONCILING;
 
 import java.util.Map;
@@ -30,7 +32,6 @@
 import org.eclipse.handly.examples.javamodel.IType;
 import org.eclipse.handly.model.IElement;
 import org.eclipse.handly.model.impl.SourceElementBody;
-import org.eclipse.handly.model.impl.WorkingCopyInfo;
 import org.eclipse.handly.model.impl.WorkspaceSourceFile;
 import org.eclipse.handly.snapshot.ISnapshot;
 import org.eclipse.handly.util.Property;
@@ -237,8 +238,7 @@
         CompilationUnit.class.getName() + ".ignoreMethodBodies", //$NON-NLS-1$
         Boolean.class).withDefault(false);
 
-    @Override
-    protected org.eclipse.jdt.core.dom.CompilationUnit hCreateAst(String source,
+    org.eclipse.jdt.core.dom.CompilationUnit createAst(String source,
         IContext context, IProgressMonitor monitor) throws CoreException
     {
         ASTParser parser = ASTParser.newParser(context.getOrDefault(AST_LEVEL));
@@ -259,19 +259,32 @@
     }
 
     @Override
-    protected void hBuildStructure(Object ast, IContext context,
-        IProgressMonitor monitor)
+    protected void hBuildStructure(IContext context, IProgressMonitor monitor)
+        throws CoreException
     {
         Map<IElement, Object> newElements = context.get(NEW_ELEMENTS);
         SourceElementBody body = new SourceElementBody();
+
+        org.eclipse.jdt.core.dom.CompilationUnit cu =
+            (org.eclipse.jdt.core.dom.CompilationUnit)context.get(SOURCE_AST);
+        if (cu == null)
+            cu = createAst(context.get(SOURCE_CONTENTS), context, monitor);
+
         CompilatonUnitStructureBuilder builder =
             new CompilatonUnitStructureBuilder(newElements);
-        builder.buildStructure(this, body,
-            (org.eclipse.jdt.core.dom.CompilationUnit)ast);
+        builder.buildStructure(this, body, cu);
+
         newElements.put(this, body);
     }
 
     @Override
+    protected IContext hWorkingCopyContext(IContext context)
+    {
+        return of(IProblemRequestor.class, context.get(
+            IProblemRequestor.class));
+    }
+
+    @Override
     protected ReconcileOperation hReconcileOperation()
     {
         return new CuReconcileOperation();
@@ -281,13 +294,19 @@
         extends NotifyingReconcileOperation
     {
         @Override
-        protected void reconcile(Object ast, IContext context,
-            IProgressMonitor monitor) throws CoreException
+        protected void reconcile(IContext context, IProgressMonitor monitor)
+            throws CoreException
         {
-            super.reconcile(ast, context, monitor);
-
             org.eclipse.jdt.core.dom.CompilationUnit cu =
-                (org.eclipse.jdt.core.dom.CompilationUnit)ast;
+                (org.eclipse.jdt.core.dom.CompilationUnit)context.get(
+                    SOURCE_AST);
+            if (cu == null)
+            {
+                cu = createAst(context.get(SOURCE_CONTENTS), context, monitor);
+                context = with(of(SOURCE_AST, cu), context);
+            }
+
+            super.reconcile(context, monitor);
 
             reportProblems(cu.getProblems());
 
@@ -300,12 +319,8 @@
         {
             if (problems == null || problems.length == 0)
                 return;
-            WorkingCopyInfo info = hPeekAtWorkingCopyInfo();
-            if (info instanceof JavaWorkingCopyInfo)
-            {
-                reportProblems(((JavaWorkingCopyInfo)info).problemRequestor,
-                    problems);
-            }
+            reportProblems(hWorkingCopyInfo().getContext().get(
+                IProblemRequestor.class), problems);
         }
 
         private void reportProblems(IProblemRequestor requestor,
diff --git a/org.eclipse.handly.examples.javamodel/src/org/eclipse/handly/internal/examples/javamodel/JavaWorkingCopyInfo.java b/org.eclipse.handly.examples.javamodel/src/org/eclipse/handly/internal/examples/javamodel/JavaWorkingCopyInfo.java
deleted file mode 100644
index ea2d849..0000000
--- a/org.eclipse.handly.examples.javamodel/src/org/eclipse/handly/internal/examples/javamodel/JavaWorkingCopyInfo.java
+++ /dev/null
@@ -1,31 +0,0 @@
-/*******************************************************************************
- * Copyright (c) 2015, 2016 1C-Soft LLC.
- * 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:
- *     Vladimir Piskarev (1C) - initial API and implementation
- *******************************************************************************/
-package org.eclipse.handly.internal.examples.javamodel;
-
-import org.eclipse.handly.buffer.IBuffer;
-import org.eclipse.handly.model.impl.DefaultWorkingCopyInfo;
-import org.eclipse.jdt.core.IProblemRequestor;
-
-/**
- * Model-specific extension of {@link DefaultWorkingCopyInfo}.
- */
-public class JavaWorkingCopyInfo
-    extends DefaultWorkingCopyInfo
-{
-    final IProblemRequestor problemRequestor;
-
-    public JavaWorkingCopyInfo(IBuffer buffer,
-        IProblemRequestor problemRequestor)
-    {
-        super(buffer);
-        this.problemRequestor = problemRequestor;
-    }
-}
diff --git a/org.eclipse.handly.tests/src/org/eclipse/handly/model/impl/SimpleSourceFile.java b/org.eclipse.handly.tests/src/org/eclipse/handly/model/impl/SimpleSourceFile.java
index a2635dd..511ade4 100644
--- a/org.eclipse.handly.tests/src/org/eclipse/handly/model/impl/SimpleSourceFile.java
+++ b/org.eclipse.handly.tests/src/org/eclipse/handly/model/impl/SimpleSourceFile.java
@@ -11,7 +11,6 @@
 package org.eclipse.handly.model.impl;
 
 import org.eclipse.core.resources.IFile;
-import org.eclipse.core.runtime.CoreException;
 import org.eclipse.core.runtime.IProgressMonitor;
 import org.eclipse.handly.context.IContext;
 import org.eclipse.handly.model.IModel;
@@ -53,15 +52,7 @@
     }
 
     @Override
-    protected Object hCreateAst(String source, IContext context,
-        IProgressMonitor monitor) throws CoreException
-    {
-        return new Object();
-    }
-
-    @Override
-    protected void hBuildStructure(Object ast, IContext context,
-        IProgressMonitor monitor)
+    protected void hBuildStructure(IContext context, IProgressMonitor monitor)
     {
     }
 }
diff --git a/org.eclipse.handly.ui/src/org/eclipse/handly/ui/IWorkingCopyManager.java b/org.eclipse.handly.ui/src/org/eclipse/handly/ui/IWorkingCopyManager.java
index 2493471..5e077e0 100644
--- a/org.eclipse.handly.ui/src/org/eclipse/handly/ui/IWorkingCopyManager.java
+++ b/org.eclipse.handly.ui/src/org/eclipse/handly/ui/IWorkingCopyManager.java
@@ -1,5 +1,5 @@
 /*******************************************************************************
- * Copyright (c) 2015 1C-Soft LLC.
+ * Copyright (c) 2015, 2016 1C-Soft LLC.
  * 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
@@ -10,28 +10,78 @@
  *******************************************************************************/
 package org.eclipse.handly.ui;
 
+import org.eclipse.core.runtime.CoreException;
 import org.eclipse.handly.model.ISourceFile;
-import org.eclipse.ui.IEditorInput;
+import org.eclipse.jface.text.IDocument;
 
 /**
  * Interface for accessing working copies of source files.
- * The original source file is only given indirectly by means
- * of an <code>IEditorInput</code>.
- *
- * @noimplement This interface is not intended to be implemented by clients.
- * @noextend This interface is not intended to be extended by clients.
+ * The life cycle is as follows:
+ * <ul>
+ * <li>
+ * {@link #connect} attempts to acquire a working copy for the given element
+ * </li>
+ * <li>
+ * {@link #getWorkingCopy} returns the working copy acquired on {@code connect}
+ * </li>
+ * <li>
+ * {@link #disconnect} releases the working copy acquired on {@code connect}
+ * </li>
+ * </ul>
+ * <p>
+ * Implementations are generally not expected to be thread safe and, if not
+ * mentioned otherwise, may only be called from the user-interface thread.
+ * </p>
  */
 public interface IWorkingCopyManager
 {
     /**
-     * Returns the working copy remembered for the source file corresponding to
-     * the given editor input.
+     * Connects the given element to this manager. Attempts to acquire a
+     * working copy for the given element.
      *
-     * @param editorInput the editor input (may be <code>null</code>)
-     * @return the working copy remembered for the source file corresponding
-     *  to the given editor input, or <code>null</code> if there is no source
-     *  file corresponding to the input or if there is no working copy
-     *  remembered for the corresponding source file
+     * @param element the element (not <code>null</code>)
+     * @throws CoreException if working copy could not be acquired successfully
      */
-    ISourceFile getWorkingCopy(IEditorInput editorInput);
+    void connect(Object element) throws CoreException;
+
+    /**
+     * Disconnects the given element from this manager. Releases the working copy
+     * acquired on {@link #connect}.
+     *
+     * @param element the element (not <code>null</code>)
+     */
+    void disconnect(Object element);
+
+    /**
+     * Returns the working copy managed for the given element.
+     *
+     * @param element the element for which to find the working copy,
+     *  or <code>null</code>
+     * @return the working copy managed for the given element,
+     *  or <code>null</code> if none
+     */
+    ISourceFile getWorkingCopy(Object element);
+
+    /**
+     * Returns the working copy managed for the given document.
+     * <p>
+     * <b>Note:</b> An implementation may go through the list of working copies and
+     * test whether the working copy buffer's document is equal to the given one.
+     * Therefore, this method should not be used in performance critical code.
+     * </p>
+     *
+     * @param document the document for which to find the working copy,
+     *  or <code>null</code>
+     * @return the working copy managed for the given document,
+     *  or <code>null</code> if none
+     */
+    ISourceFile getWorkingCopy(IDocument document);
+
+    /**
+     * Returns all working copies that are currently managed by this manager.
+     *
+     * @return the working copies currently managed by this manager
+     *  (never <code>null</code>)
+     */
+    ISourceFile[] getWorkingCopies();
 }
diff --git a/org.eclipse.handly.ui/src/org/eclipse/handly/ui/IWorkingCopyProvider.java b/org.eclipse.handly.ui/src/org/eclipse/handly/ui/IWorkingCopyProvider.java
deleted file mode 100644
index 476c141..0000000
--- a/org.eclipse.handly.ui/src/org/eclipse/handly/ui/IWorkingCopyProvider.java
+++ /dev/null
@@ -1,30 +0,0 @@
-/*******************************************************************************
- * Copyright (c) 2015 1C-Soft LLC.
- * 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:
- *     Vladimir Piskarev (1C) - initial API and implementation
- *******************************************************************************/
-package org.eclipse.handly.ui;
-
-import org.eclipse.handly.model.ISourceFile;
-
-/**
- * An object capable of providing a working copy.
- * This interface may be implemented by clients.
- */
-public interface IWorkingCopyProvider
-{
-    /**
-     * Returns a working copy according to the provider strategy.
-     * The result may or may not be the same each time this method is called
-     * on the provider.
-     *
-     * @return the provided working copy, or <code>null</code>
-     *  if no working copy can be provided
-     */
-    ISourceFile getWorkingCopy();
-}
diff --git a/org.eclipse.handly.ui/src/org/eclipse/handly/ui/WorkingCopyProvider.java b/org.eclipse.handly.ui/src/org/eclipse/handly/ui/WorkingCopyProvider.java
deleted file mode 100644
index 2ecb3af..0000000
--- a/org.eclipse.handly.ui/src/org/eclipse/handly/ui/WorkingCopyProvider.java
+++ /dev/null
@@ -1,48 +0,0 @@
-/*******************************************************************************
- * Copyright (c) 2015 1C-Soft LLC.
- * 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:
- *     Vladimir Piskarev (1C) - initial API and implementation
- *******************************************************************************/
-package org.eclipse.handly.ui;
-
-import org.eclipse.handly.model.ISourceFile;
-import org.eclipse.ui.IEditorPart;
-
-/**
- * Provides the working copy associated with the given editor
- * via the given working copy manager.
- */
-public final class WorkingCopyProvider
-    implements IWorkingCopyProvider
-{
-    private final IEditorPart editor;
-    private final IWorkingCopyManager manager;
-
-    /**
-     * Creates a new working copy provider for the given editor
-     * and the given working copy manager.
-     *
-     * @param editor the editor (not <code>null</code>)
-     * @param manager the working copy manager (not <code>null</code>)
-     */
-    public WorkingCopyProvider(IEditorPart editor, IWorkingCopyManager manager)
-    {
-        if (editor == null)
-            throw new IllegalArgumentException();
-        if (manager == null)
-            throw new IllegalArgumentException();
-        this.editor = editor;
-        this.manager = manager;
-    }
-
-    @Override
-    public ISourceFile getWorkingCopy()
-    {
-        return manager.getWorkingCopy(editor.getEditorInput());
-    }
-}
diff --git a/org.eclipse.handly.ui/src/org/eclipse/handly/ui/text/reconciler/HandlyReconciler.java b/org.eclipse.handly.ui/src/org/eclipse/handly/ui/text/reconciler/EditorWorkingCopyReconciler.java
similarity index 83%
rename from org.eclipse.handly.ui/src/org/eclipse/handly/ui/text/reconciler/HandlyReconciler.java
rename to org.eclipse.handly.ui/src/org/eclipse/handly/ui/text/reconciler/EditorWorkingCopyReconciler.java
index 3cfc42d..2aca2ca 100644
--- a/org.eclipse.handly.ui/src/org/eclipse/handly/ui/text/reconciler/HandlyReconciler.java
+++ b/org.eclipse.handly.ui/src/org/eclipse/handly/ui/text/reconciler/EditorWorkingCopyReconciler.java
@@ -1,5 +1,5 @@
 /*******************************************************************************
- * Copyright (c) 2015 1C-Soft LLC.
+ * Copyright (c) 2015, 2016 1C-Soft LLC.
  * 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
@@ -11,23 +11,22 @@
 package org.eclipse.handly.ui.text.reconciler;
 
 import org.eclipse.handly.ui.IWorkingCopyManager;
-import org.eclipse.handly.ui.WorkingCopyProvider;
 import org.eclipse.jface.text.ITextViewer;
+import org.eclipse.ui.IEditorPart;
 import org.eclipse.ui.IPartListener;
 import org.eclipse.ui.IWorkbenchPart;
 import org.eclipse.ui.IWorkbenchPartSite;
 import org.eclipse.ui.IWorkbenchWindow;
-import org.eclipse.ui.texteditor.ITextEditor;
 
 /**
  * An abstract base class of a working copy reconciler that is activated on
  * editor activation and forces reconciling on a significant change in the
  * underlying model.
  */
-public abstract class HandlyReconciler
-    extends BaseReconciler
+public abstract class EditorWorkingCopyReconciler
+    extends WorkingCopyReconciler
 {
-    protected final ITextEditor editor;
+    private final IEditorPart editor;
     private final IPartListener partListener = new IPartListener()
     {
         public void partActivated(IWorkbenchPart part)
@@ -58,11 +57,12 @@
      * with the given text editor.
      *
      * @param editor the editor (not <code>null</code>)
-     * @param manager the working copy manager (not <code>null</code>)
+     * @param workingCopyManager the working copy manager (not <code>null</code>)
      */
-    public HandlyReconciler(ITextEditor editor, IWorkingCopyManager manager)
+    public EditorWorkingCopyReconciler(IEditorPart editor,
+        IWorkingCopyManager workingCopyManager)
     {
-        super(new WorkingCopyProvider(editor, manager));
+        super(workingCopyManager);
         this.editor = editor;
     }
 
@@ -91,4 +91,9 @@
     {
         return editor;
     }
+
+    protected final IEditorPart getEditor()
+    {
+        return editor;
+    }
 }
diff --git a/org.eclipse.handly.ui/src/org/eclipse/handly/ui/text/reconciler/BaseReconciler.java b/org.eclipse.handly.ui/src/org/eclipse/handly/ui/text/reconciler/WorkingCopyReconciler.java
similarity index 92%
rename from org.eclipse.handly.ui/src/org/eclipse/handly/ui/text/reconciler/BaseReconciler.java
rename to org.eclipse.handly.ui/src/org/eclipse/handly/ui/text/reconciler/WorkingCopyReconciler.java
index 7568d8a..e37f752 100644
--- a/org.eclipse.handly.ui/src/org/eclipse/handly/ui/text/reconciler/BaseReconciler.java
+++ b/org.eclipse.handly.ui/src/org/eclipse/handly/ui/text/reconciler/WorkingCopyReconciler.java
@@ -25,7 +25,7 @@
 import org.eclipse.handly.model.IElementChangeListener;
 import org.eclipse.handly.model.IElementDelta;
 import org.eclipse.handly.model.ISourceFile;
-import org.eclipse.handly.ui.IWorkingCopyProvider;
+import org.eclipse.handly.ui.IWorkingCopyManager;
 import org.eclipse.jface.text.IDocument;
 import org.eclipse.jface.text.ITextViewer;
 import org.eclipse.jface.text.Region;
@@ -45,11 +45,10 @@
  * viewer activation and forces reconciling on a significant change in the
  * underlying model.
  */
-public abstract class BaseReconciler
+public abstract class WorkingCopyReconciler
     extends AbstractReconciler
-    implements IWorkingCopyProvider
 {
-    private IWorkingCopyProvider provider;
+    private IWorkingCopyManager workingCopyManager;
     private IReconcilingStrategy strategy;
     private volatile ISourceFile workingCopy;
     private volatile boolean active = true;
@@ -65,26 +64,27 @@
                     return;
 
                 if (isAffectedBy(event))
-                    BaseReconciler.this.elementChanged(event);
+                    WorkingCopyReconciler.this.elementChanged(event);
             }
         };
     private ShellListener activationListener;
 
     /**
      * Creates a new reconciler that reconciles the working copy provided
-     * by the given provider.
+     * by the given manager.
      *
-     * @param provider the working copy provider (not <code>null</code>)
+     * @param workingCopyManager the working copy manager (not <code>null</code>)
      */
-    public BaseReconciler(IWorkingCopyProvider provider)
+    public WorkingCopyReconciler(IWorkingCopyManager workingCopyManager)
     {
-        if (provider == null)
+        if (workingCopyManager == null)
             throw new IllegalArgumentException();
-        this.provider = provider;
+        this.workingCopyManager = workingCopyManager;
         // Just some reasonable defaults that can be overwritten:
         setIsIncrementalReconciler(false);
         setIsAllowedToModifyDocument(false);
-        setReconcilingStrategy(new WorkingCopyReconcilingStrategy(this));
+        setReconcilingStrategy(new WorkingCopyReconcilingStrategy(
+            workingCopyManager));
     }
 
     /**
@@ -122,7 +122,8 @@
     {
         super.install(textViewer);
 
-        setWorkingCopy(provider.getWorkingCopy());
+        setWorkingCopy(workingCopyManager.getWorkingCopy(
+            textViewer.getDocument()));
 
         addElementChangeListener(elementChangeListener);
 
@@ -141,6 +142,8 @@
 
         removeElementChangeListener(elementChangeListener);
 
+        setWorkingCopy(null);
+
         super.uninstall();
     }
 
@@ -151,12 +154,6 @@
     }
 
     @Override
-    public ISourceFile getWorkingCopy()
-    {
-        return workingCopy;
-    }
-
-    @Override
     protected void initialProcess()
     {
         synchronized (getReconcilerLock())
@@ -196,7 +193,7 @@
     @Override
     protected void reconcilerDocumentChanged(IDocument newDocument)
     {
-        setWorkingCopy(provider.getWorkingCopy());
+        setWorkingCopy(workingCopyManager.getWorkingCopy(newDocument));
         strategy.setDocument(newDocument);
     }
 
@@ -209,7 +206,7 @@
      */
     protected Object getReconcilerLock()
     {
-        return this; // Null Object
+        return this;
     }
 
     /**
@@ -328,13 +325,23 @@
         this.active = active;
         if (Display.getCurrent() == null)
             throw new AssertionError(
-                "This method can only be executed by the UI thread"); //$NON-NLS-1$
+                "This method may only be executed by the user-interface thread"); //$NON-NLS-1$
         if (!active)
             setModelChanged(false);
         else if (hasModelChanged())
             forceReconciling();
     }
 
+    private ISourceFile getWorkingCopy()
+    {
+        return workingCopy;
+    }
+
+    private void setWorkingCopy(ISourceFile workingCopy)
+    {
+        this.workingCopy = workingCopy;
+    }
+
     private boolean hasModelChanged()
     {
         return modelChanged;
@@ -345,11 +352,6 @@
         this.modelChanged = modelChanged;
     }
 
-    private void setWorkingCopy(ISourceFile workingCopy)
-    {
-        this.workingCopy = workingCopy;
-    }
-
     private class ActivationListener
         extends ShellAdapter
     {
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 414082d..e80cb9e 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
@@ -10,15 +10,14 @@
  *******************************************************************************/
 package org.eclipse.handly.ui.text.reconciler;
 
-import static org.eclipse.handly.context.Contexts.of;
-
 import org.eclipse.core.runtime.CoreException;
 import org.eclipse.core.runtime.IProgressMonitor;
 import org.eclipse.core.runtime.ISafeRunnable;
+import org.eclipse.core.runtime.OperationCanceledException;
 import org.eclipse.core.runtime.SafeRunner;
 import org.eclipse.handly.model.Elements;
 import org.eclipse.handly.model.ISourceFile;
-import org.eclipse.handly.ui.IWorkingCopyProvider;
+import org.eclipse.handly.ui.IWorkingCopyManager;
 import org.eclipse.jface.text.IDocument;
 import org.eclipse.jface.text.IRegion;
 import org.eclipse.jface.text.reconciler.DirtyRegion;
@@ -31,20 +30,28 @@
 public class WorkingCopyReconcilingStrategy
     implements IReconcilingStrategy, IReconcilingStrategyExtension
 {
-    private final IWorkingCopyProvider provider;
-    private IProgressMonitor monitor;
+    private final IWorkingCopyManager workingCopyManager;
+    private volatile ISourceFile workingCopy;
+    private volatile IProgressMonitor monitor;
 
     /**
      * Creates a new working copy reconciling strategy
-     * with the given working copy provider.
+     * with the given working copy manager.
      *
-     * @param provider the working copy provider (not <code>null</code>)
+     * @param workingCopyManager the working copy manager (not <code>null</code>)
      */
-    public WorkingCopyReconcilingStrategy(IWorkingCopyProvider provider)
+    public WorkingCopyReconcilingStrategy(
+        IWorkingCopyManager workingCopyManager)
     {
-        if (provider == null)
+        if (workingCopyManager == null)
             throw new IllegalArgumentException();
-        this.provider = provider;
+        this.workingCopyManager = workingCopyManager;
+    }
+
+    @Override
+    public void setDocument(IDocument document)
+    {
+        setWorkingCopy(workingCopyManager.getWorkingCopy(document));
     }
 
     @Override
@@ -60,11 +67,6 @@
     }
 
     @Override
-    public void setDocument(IDocument document)
-    {
-    }
-
-    @Override
     public final void reconcile(DirtyRegion dirtyRegion, IRegion subRegion)
     {
         reconcile(false);
@@ -81,42 +83,44 @@
      *
      * @param workingCopy the given working copy (never <code>null</code>)
      * @param initialReconcile <code>true</code> if this is the initial reconcile
-     * @throws CoreException if the given working copy cannot be reconciled
+     * @param monitor a progress monitor, or <code>null</code>
+     *  if progress reporting is not desired
+     * @throws CoreException if the working copy cannot be reconciled
+     * @throws OperationCanceledException if this method is canceled
      * @see #initialReconcile()
      */
-    protected void reconcile(ISourceFile workingCopy, boolean initialReconcile)
-        throws CoreException
+    protected void reconcile(ISourceFile workingCopy, boolean initialReconcile,
+        IProgressMonitor monitor) throws CoreException
     {
-        Elements.reconcile(workingCopy, of(Elements.FORCE_RECONCILING, true),
-            getProgressMonitor());
+        Elements.reconcile(workingCopy, monitor);
     }
 
-    /**
-     * @return the progress monitor set for this strategy,
-     *  or <code>null</code> if none
-     */
-    protected final IProgressMonitor getProgressMonitor()
+    private void reconcile(boolean initialReconcile)
     {
-        return monitor;
-    }
-
-    private void reconcile(final boolean initialReconcile)
-    {
-        final ISourceFile workingCopy = provider.getWorkingCopy();
-        if (workingCopy != null)
+        ISourceFile workingCopy = getWorkingCopy();
+        if (workingCopy == null)
+            return;
+        SafeRunner.run(new ISafeRunnable()
         {
-            SafeRunner.run(new ISafeRunnable()
+            public void run() throws Exception
             {
-                public void run() throws Exception
-                {
-                    reconcile(workingCopy, initialReconcile);
-                }
+                reconcile(workingCopy, initialReconcile, monitor);
+            }
 
-                public void handleException(Throwable exception)
-                {
-                    // already logged by Platform
-                }
-            });
-        }
+            public void handleException(Throwable exception)
+            {
+                // already logged by Platform
+            }
+        });
+    }
+
+    private void setWorkingCopy(ISourceFile workingCopy)
+    {
+        this.workingCopy = workingCopy;
+    }
+
+    private ISourceFile getWorkingCopy()
+    {
+        return workingCopy;
     }
 }
diff --git a/org.eclipse.handly.ui/src/org/eclipse/handly/ui/texteditor/SourceFileDocumentProvider.java b/org.eclipse.handly.ui/src/org/eclipse/handly/ui/texteditor/SourceFileDocumentProvider.java
index 60aa622..da3ac6b 100644
--- a/org.eclipse.handly.ui/src/org/eclipse/handly/ui/texteditor/SourceFileDocumentProvider.java
+++ b/org.eclipse.handly.ui/src/org/eclipse/handly/ui/texteditor/SourceFileDocumentProvider.java
@@ -10,208 +10,190 @@
  *******************************************************************************/
 package org.eclipse.handly.ui.texteditor;
 
-import java.text.MessageFormat;
+import static org.eclipse.handly.context.Contexts.EMPTY_CONTEXT;
 
-import org.eclipse.core.resources.IFile;
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+
 import org.eclipse.core.runtime.CoreException;
-import org.eclipse.handly.buffer.TextFileBuffer;
-import org.eclipse.handly.internal.ui.Activator;
-import org.eclipse.handly.model.IElement;
+import org.eclipse.handly.buffer.IBuffer;
+import org.eclipse.handly.model.Elements;
 import org.eclipse.handly.model.ISourceFile;
-import org.eclipse.handly.model.impl.IWorkingCopyInfoFactory;
 import org.eclipse.handly.model.impl.SourceFile;
-import org.eclipse.handly.ui.IInputElementProvider;
 import org.eclipse.handly.ui.IWorkingCopyManager;
-import org.eclipse.ui.IEditorInput;
+import org.eclipse.jface.text.IDocument;
 import org.eclipse.ui.editors.text.TextFileDocumentProvider;
 import org.eclipse.ui.texteditor.IDocumentProvider;
 
 /**
  * Subclass of {@link TextFileDocumentProvider} specialized for
- * working copy management of {@link SourceFile}s.
- * <p>
- * Clients can use this class as it stands or subclass it
- * as circumstances warrant.
- * </p>
+ * working copy management of source files.
  */
-public class SourceFileDocumentProvider
+public abstract class SourceFileDocumentProvider
     extends TextFileDocumentProvider
     implements IWorkingCopyManager
 {
-    protected final IInputElementProvider inputElementProvider;
+    private static final ISourceFile[] NO_WORKING_COPIES = new ISourceFile[0];
 
     /**
      * Creates a new source file document provider with no parent.
-     * <p>
-     * The given input element provider is used in the default implementation
-     * of {@link #getSourceFile(Object)}.
-     * </p>
-     *
-     * @param inputElementProvider the input element provider
      */
-    public SourceFileDocumentProvider(
-        IInputElementProvider inputElementProvider)
+    public SourceFileDocumentProvider()
     {
-        this(null, inputElementProvider);
+        this(null);
     }
 
     /**
      * Creates a new source file document provider with the given parent.
-     * <p>
-     * The given input element provider is used in the default implementation
-     * of {@link #getSourceFile(Object)}.
-     * </p>
      *
      * @param parent the parent document provider
-     * @param inputElementProvider the input element provider
      */
-    public SourceFileDocumentProvider(IDocumentProvider parent,
-        IInputElementProvider inputElementProvider)
+    public SourceFileDocumentProvider(IDocumentProvider parent)
     {
         super(parent);
-        this.inputElementProvider = inputElementProvider;
     }
 
     @Override
-    public ISourceFile getWorkingCopy(IEditorInput editorInput)
+    public ISourceFile getWorkingCopy(Object element)
     {
-        FileInfo info = getFileInfo(editorInput);
-        if (info instanceof SourceFileInfo)
+        SourceFileInfo info = (SourceFileInfo)getFileInfo(element);
+        if (info == null)
+            return null;
+        return info.workingCopy;
+    }
+
+    @Override
+    public ISourceFile getWorkingCopy(IDocument document)
+    {
+        Iterator<?> it = getFileInfosIterator();
+        while (it.hasNext())
         {
-            return ((SourceFileInfo)info).workingCopy;
+            SourceFileInfo info = (SourceFileInfo)it.next();
+            if (info.fTextFileBuffer.getDocument().equals(document))
+                return info.workingCopy;
         }
         return null;
     }
 
     @Override
+    public ISourceFile[] getWorkingCopies()
+    {
+        List<ISourceFile> result = new ArrayList<>();
+        Iterator<?> it = getFileInfosIterator();
+        while (it.hasNext())
+        {
+            SourceFileInfo info = (SourceFileInfo)it.next();
+            if (info.workingCopy != null)
+                result.add(info.workingCopy);
+        }
+        return result.toArray(NO_WORKING_COPIES);
+    }
+
+    /**
+     * Returns the source file for the given element.
+     *
+     * @param element the element
+     * @return the source file for the given element,
+     *  or <code>null</code> if none
+     */
+    protected abstract ISourceFile getSourceFile(Object element);
+
+    @Override
     protected FileInfo createEmptyFileInfo()
     {
         return new SourceFileInfo();
     }
 
-    /*
-     * Subclasses may extend this method.
-     */
     @Override
     protected FileInfo createFileInfo(Object element) throws CoreException
     {
+        SourceFileInfo info = (SourceFileInfo)super.createFileInfo(element);
+        if (info == null)
+            return null;
         boolean f = false;
-        FileInfo info = super.createFileInfo(element);
         try
         {
-            if (!(info instanceof SourceFileInfo))
-                return null;
-            SourceFile sourceFile = getSourceFile(element);
-            if (sourceFile == null)
-                return null;
-            IFile file = sourceFile.hFile();
-            if (file == null)
-                return null;
-            try (TextFileBuffer buffer = TextFileBuffer.forFile(file))
+            ISourceFile workingCopy = acquireWorkingCopy(element, info);
+            if (workingCopy != null)
             {
-                if (sourceFile.hBecomeWorkingCopy(buffer, // will addRef() the buffer
-                    getWorkingCopyInfoFactory(sourceFile, element, info),
-                    null) != null)
+                if (!Elements.isWorkingCopy(workingCopy))
+                    throw new AssertionError();
+                try (IBuffer buffer = Elements.getBuffer(workingCopy))
                 {
-                    sourceFile.hDiscardWorkingCopy();
-
-                    throw new CoreException(Activator.createErrorStatus(
-                        MessageFormat.format(
-                            Messages.SourceFileDocumentProvider_Working_copy_already_exists__0,
-                            sourceFile), null));
+                    if (buffer.getDocument() != info.fTextFileBuffer.getDocument())
+                    {
+                        releaseWorkingCopy(workingCopy, element, info);
+                        throw new AssertionError();
+                    }
                 }
+                info.workingCopy = workingCopy;
             }
-            ((SourceFileInfo)info).workingCopy = sourceFile;
             f = true;
             return info;
         }
         finally
         {
-            if (!f && info != null)
+            if (!f)
                 super.disposeFileInfo(element, info);
         }
     }
 
-    /*
-     * Subclasses may extend this method.
-     */
     @Override
     protected void disposeFileInfo(Object element, FileInfo info)
     {
-        if (info instanceof SourceFileInfo)
+        try
         {
-            ((SourceFileInfo)info).workingCopy.hDiscardWorkingCopy();
+            ISourceFile workingCopy = ((SourceFileInfo)info).workingCopy;
+            if (workingCopy != null)
+                releaseWorkingCopy(workingCopy, element, info);
         }
-        super.disposeFileInfo(element, info);
+        finally
+        {
+            super.disposeFileInfo(element, info);
+        }
     }
 
     /**
-     * Returns the source file corresponding to the given element.
-     * <p>
-     * The resulting source file will be switched to working copy mode
-     * and associated with the file info object for the given element
-     * in {@link #createFileInfo(Object)}.
-     * </p>
-     * <p>
-     * If the given element is an <code>IEditorInput</code>, this implementation
-     * uses the {@link IInputElementProvider} specified in the constructor to get
-     * the input element for the editor input. If the provided input element is
-     * a {@link SourceFile}, it is returned. Otherwise, <code>null</code> is
-     * returned.
-     * </p>
-     * <p>
-     * Subclasses may extend this method or override it completely.
-     * </p>
+     * Attempts to acquire a working copy for the given element. A working copy
+     * acquired by this method <b>must</b> be released eventually via a call to
+     * {@link #releaseWorkingCopy(ISourceFile, Object, FileInfo)}.
      *
-     * @param element the element from which to compute the source file
-     * @return the source file for the given element,
-     *  or <code>null</code> if none
+     * @param element the element
+     * @param info the element info
+     * @return an acquired working copy, or <code>null</code> if no working copy
+     *  can be acquired for the given element
+     * @throws CoreException if working copy could not be acquired successfully
      */
-    protected SourceFile getSourceFile(Object element)
+    protected ISourceFile acquireWorkingCopy(Object element, FileInfo info)
+        throws CoreException
     {
-        if (!(element instanceof IEditorInput))
-            return null;
-        IElement inputElement = inputElementProvider.getElement(
-            (IEditorInput)element);
-        if (!(inputElement instanceof SourceFile))
-            return null;
-        return (SourceFile)inputElement;
-    }
-
-    /**
-     * Returns the working copy info factory for the given source file,
-     * or <code>null</code> if a default factory is to be used.
-     * <p>
-     * This implementation returns <code>null</code>. Subclasses may override.
-     * </p>
-     *
-     * @param sourceFile the source file corresponding to the given element
-     *  (never <code>null</code>)
-     * @param element the element (never <code>null</code>)
-     * @param fileInfo the file info for the given element
-     *  (never <code>null</code>)
-     * @return the working copy info factory for the given source file,
-     *  or <code>null</code> if a default factory is to be used
-     */
-    protected IWorkingCopyInfoFactory getWorkingCopyInfoFactory(
-        SourceFile sourceFile, Object element, FileInfo fileInfo)
-    {
+        ISourceFile sourceFile = getSourceFile(element);
+        if (sourceFile instanceof SourceFile)
+        {
+            ((SourceFile)sourceFile).hBecomeWorkingCopy(EMPTY_CONTEXT, null);
+            return sourceFile;
+        }
         return null;
     }
 
     /**
-     * Bundle of all required information to allow working copy management.
-     * <p>
-     * Can be used as it stands or extended in subclasses as circumstances
-     * warrant.
-     * </p>
+     * Releases the working copy acquired via a call to {@link
+     * #acquireWorkingCopy(Object, FileInfo)}.
+     *
+     * @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)
+    {
+        ((SourceFile)workingCopy).hReleaseWorkingCopy();
+    }
+
     protected static class SourceFileInfo
         extends FileInfo
     {
-        /**
-         * A source file in working copy mode.
-         */
-        SourceFile workingCopy;
+        ISourceFile workingCopy;
     }
 }
diff --git a/org.eclipse.handly.xtext.ui/src/org/eclipse/handly/xtext/ui/editor/HandlyXtextDocument.java b/org.eclipse.handly.xtext.ui/src/org/eclipse/handly/xtext/ui/editor/HandlyXtextDocument.java
index 908cf61..4a596c4 100644
--- a/org.eclipse.handly.xtext.ui/src/org/eclipse/handly/xtext/ui/editor/HandlyXtextDocument.java
+++ b/org.eclipse.handly.xtext.ui/src/org/eclipse/handly/xtext/ui/editor/HandlyXtextDocument.java
@@ -138,16 +138,6 @@
         return reconciledSnapshot.getWrappedSnapshot();
     }
 
-    public void addReconcilingListener(IReconcilingListener listener)
-    {
-        reconcilingListeners.add(listener);
-    }
-
-    public void removeReconcilingListener(IReconcilingListener listener)
-    {
-        reconcilingListeners.remove(listener);
-    }
-
     @Override
     public boolean needsReconciling()
     {
@@ -225,6 +215,16 @@
         this.dirtyStateEditorSupport = dirtyStateEditorSupport;
     }
 
+    void addReconcilingListener(IReconcilingListener listener)
+    {
+        reconcilingListeners.add(listener);
+    }
+
+    void removeReconcilingListener(IReconcilingListener listener)
+    {
+        reconcilingListeners.remove(listener);
+    }
+
     private PendingChange getAndResetPendingChange()
     {
         final PendingChange result;
@@ -325,7 +325,7 @@
     /**
      * Document reconciling listener protocol.
      */
-    public interface IReconcilingListener
+    interface IReconcilingListener
     {
         /**
          * Called just after a reconciling operation has been performed. Informs
diff --git a/org.eclipse.handly.xtext.ui/src/org/eclipse/handly/xtext/ui/editor/HandlyXtextEditorCallback.java b/org.eclipse.handly.xtext.ui/src/org/eclipse/handly/xtext/ui/editor/HandlyXtextEditorCallback.java
index 6f9d6d1..e8b77f7 100644
--- a/org.eclipse.handly.xtext.ui/src/org/eclipse/handly/xtext/ui/editor/HandlyXtextEditorCallback.java
+++ b/org.eclipse.handly.xtext.ui/src/org/eclipse/handly/xtext/ui/editor/HandlyXtextEditorCallback.java
@@ -10,6 +10,8 @@
  *******************************************************************************/
 package org.eclipse.handly.xtext.ui.editor;
 
+import static org.eclipse.handly.context.Contexts.of;
+import static org.eclipse.handly.context.Contexts.with;
 import static org.eclipse.handly.model.Elements.exists;
 import static org.eclipse.handly.model.Elements.getSourceElementAt;
 import static org.eclipse.handly.model.Elements.getSourceElementInfo;
@@ -18,7 +20,6 @@
 import java.util.HashMap;
 import java.util.HashSet;
 import java.util.Map;
-import java.util.Map.Entry;
 import java.util.Set;
 
 import org.eclipse.core.runtime.CoreException;
@@ -28,8 +29,10 @@
 import org.eclipse.core.runtime.jobs.Job;
 import org.eclipse.handly.buffer.IBuffer;
 import org.eclipse.handly.internal.xtext.ui.Activator;
+import org.eclipse.handly.model.Elements;
 import org.eclipse.handly.model.IElement;
 import org.eclipse.handly.model.ISourceElement;
+import org.eclipse.handly.model.ISourceFile;
 import org.eclipse.handly.model.impl.SourceFile;
 import org.eclipse.handly.ui.IInputElementProvider;
 import org.eclipse.handly.ui.texteditor.TextEditorBuffer;
@@ -58,9 +61,11 @@
 
 /**
  * Integrates Xtext editor with Handly working copy management facility.
- * Creates a working copy when a source file is opened in Xtext editor.
- * Discards the working copy when the editor is being disposed. Also,
- * sets the editor highlight range for the currently selected element.
+ * <p>
+ * Multiple Xtext editor instances may simultaneously be open for a given
+ * source file, each with its own underlying document, but only one of them
+ * (the most recently used one) is connected to the source file's working copy.
+ * </p>
  * <p>
  * Note that this class relies on a language-specific implementation of
  * {@link IInputElementProvider} being available through injection.
@@ -98,16 +103,11 @@
 {
     private IInputElementProvider inputElementProvider;
 
-    private Map<XtextEditor, WorkingCopyInfo> workingCopies =
-        new HashMap<XtextEditor, WorkingCopyInfo>();
+    private Map<IEditorInput, WorkingCopyEditorInfo> workingCopyEditors =
+        new HashMap<>();
     private Map<MultiPageEditorPart, Set<XtextEditor>> nestedEditors =
-        new HashMap<MultiPageEditorPart, Set<XtextEditor>>();
-    private Map<XtextEditor, IPartListener> partListeners =
-        new HashMap<XtextEditor, IPartListener>();
-    private Map<XtextEditor, ISelectionChangedListener> selectionListeners =
-        new HashMap<XtextEditor, ISelectionChangedListener>();
-    private Map<XtextEditor, HighlightRangeJob> highlightRangeJobs =
-        new HashMap<XtextEditor, HighlightRangeJob>();
+        new HashMap<>();
+    private Map<XtextEditor, EditorInfo> editorInfoMap = new HashMap<>();
 
     @Inject
     public void setInputElementProvider(IInputElementProvider provider)
@@ -118,19 +118,16 @@
     @Override
     public void afterCreatePartControl(XtextEditor editor)
     {
+        connect(editor);
         registerContainer(editor);
-        registerPartListener(editor);
-        registerSelectionListener(editor);
     }
 
     @Override
     public void beforeDispose(XtextEditor editor)
     {
-        deregisterPartListener(editor);
-        deregisterSelectionListener(editor);
         disconnectWorkingCopy(editor);
-        disposeHighlightRangeJob(editor);
         deregisterContainer(editor);
+        disconnect(editor);
     }
 
     @Override
@@ -149,34 +146,56 @@
     protected void afterSelectionChange(XtextEditor editor,
         ISelection selection)
     {
-        setHighlightRange(editor, selection);
+        if (selection != null)
+            setHighlightRange(editor, selection);
     }
 
     protected void setHighlightRange(XtextEditor editor, ISelection selection)
     {
-        if (selection == null)
-            return;
-        SourceFile sourceFile = getWorkingCopy(editor);
-        if (sourceFile == null)
-            return;
-        scheduleHighlightRangeJob(editor, sourceFile, selection);
+        scheduleHighlightRangeJob(editor, selection);
     }
 
-    protected SourceFile getSourceFile(XtextEditor editor)
+    protected ISourceFile getSourceFile(XtextEditor editor)
     {
         IElement inputElement = inputElementProvider.getElement(
             editor.getEditorInput());
-        if (!(inputElement instanceof SourceFile))
+        if (!(inputElement instanceof ISourceFile))
             return null;
-        return (SourceFile)inputElement;
+        return (ISourceFile)inputElement;
     }
 
-    protected final SourceFile getWorkingCopy(XtextEditor editor)
+    protected final ISourceFile getWorkingCopy(XtextEditor editor)
     {
-        WorkingCopyInfo workingCopyInfo = workingCopies.get(editor);
-        if (workingCopyInfo == null || !workingCopyInfo.success)
+        WorkingCopyEditorInfo info = workingCopyEditors.get(
+            editor.getEditorInput());
+        if (info == null || info.editor != editor)
             return null;
-        return workingCopyInfo.sourceFile;
+        return info.workingCopy;
+    }
+
+    protected ISourceFile acquireWorkingCopy(XtextEditor editor)
+        throws CoreException
+    {
+        ISourceFile sourceFile = getSourceFile(editor);
+        if (sourceFile instanceof SourceFile)
+        {
+            try (TextEditorBuffer buffer = new TextEditorBuffer(editor))
+            {
+                ((SourceFile)sourceFile).hBecomeWorkingCopy(with(of(
+                    SourceFile.WORKING_COPY_BUFFER, buffer), of(
+                        SourceFile.WORKING_COPY_INFO_FACTORY, (SourceFile sf,
+                            IBuffer b) -> new XtextWorkingCopyInfo(sf, b))),
+                    null);
+                return sourceFile;
+            }
+        }
+        return null;
+    }
+
+    protected void releaseWorkingCopy(XtextEditor editor,
+        ISourceFile workingCopy)
+    {
+        ((SourceFile)workingCopy).hReleaseWorkingCopy();
     }
 
     private boolean isActive(XtextEditor editor)
@@ -229,9 +248,9 @@
         }
     }
 
-    private void registerPartListener(final XtextEditor editor)
+    private void connect(XtextEditor editor)
     {
-        IPartListener listener = new IPartListener()
+        IPartListener partListener = new IPartListener()
         {
             public void partActivated(IWorkbenchPart part)
             {
@@ -252,137 +271,140 @@
             // @formatter:on
         };
         editor.getSite().getWorkbenchWindow().getPartService().addPartListener(
-            listener);
-        partListeners.put(editor, listener);
-    }
+            partListener);
 
-    private void deregisterPartListener(XtextEditor editor)
-    {
-        IPartListener listener = partListeners.remove(editor);
-        if (listener != null)
-        {
-            editor.getSite().getWorkbenchWindow().getPartService().removePartListener(
-                listener);
-        }
-    }
-
-    private void registerSelectionListener(final XtextEditor editor)
-    {
-        ISelectionChangedListener listener = new ISelectionChangedListener()
-        {
-            @Override
-            public void selectionChanged(SelectionChangedEvent event)
+        ISelectionChangedListener selectionChangedListener =
+            new ISelectionChangedListener()
             {
-                afterSelectionChange(editor, event.getSelection());
-            }
-        };
+                @Override
+                public void selectionChanged(SelectionChangedEvent event)
+                {
+                    afterSelectionChange(editor, event.getSelection());
+                }
+            };
         ISelectionProvider selectionProvider = editor.getSelectionProvider();
         if (selectionProvider instanceof IPostSelectionProvider)
             ((IPostSelectionProvider)selectionProvider).addPostSelectionChangedListener(
-                listener);
+                selectionChangedListener);
         else
-            selectionProvider.addSelectionChangedListener(listener);
-        selectionListeners.put(editor, listener);
+            selectionProvider.addSelectionChangedListener(
+                selectionChangedListener);
+
+        EditorInfo info = new EditorInfo();
+        info.partListener = partListener;
+        info.selectionChangedListener = selectionChangedListener;
+        info.highlightRangeJob = new HighlightRangeJob(editor);
+        editorInfoMap.put(editor, info);
     }
 
-    private void deregisterSelectionListener(XtextEditor editor)
+    private void disconnect(XtextEditor editor)
     {
-        ISelectionChangedListener listener = selectionListeners.remove(editor);
-        if (listener != null)
-        {
-            ISelectionProvider selectionProvider =
-                editor.getSelectionProvider();
-            if (selectionProvider instanceof IPostSelectionProvider)
-                ((IPostSelectionProvider)selectionProvider).removePostSelectionChangedListener(
-                    listener);
-            else
-                selectionProvider.removeSelectionChangedListener(listener);
-        }
+        EditorInfo info = editorInfoMap.remove(editor);
+        if (info == null)
+            return;
+
+        editor.getSite().getWorkbenchWindow().getPartService().removePartListener(
+            info.partListener);
+
+        ISelectionProvider selectionProvider = editor.getSelectionProvider();
+        if (selectionProvider instanceof IPostSelectionProvider)
+            ((IPostSelectionProvider)selectionProvider).removePostSelectionChangedListener(
+                info.selectionChangedListener);
+        else
+            selectionProvider.removeSelectionChangedListener(
+                info.selectionChangedListener);
+
+        HighlightRangeJob highlightRangeJob = info.highlightRangeJob;
+        highlightRangeJob.cancel();
+        highlightRangeJob.setArgs(null);
     }
 
     private void connectWorkingCopy(XtextEditor editor)
     {
-        SourceFile sourceFile = getSourceFile(editor);
-        if (sourceFile == null)
-            return;
-
-        XtextEditor workingCopyEditor = getWorkingCopyEditor(sourceFile);
+        XtextEditor workingCopyEditor = getWorkingCopyEditor(
+            editor.getEditorInput());
         if (editor != workingCopyEditor)
         {
             if (workingCopyEditor != null)
-                discardWorkingCopy(workingCopyEditor);
+                disconnectWorkingCopy0(workingCopyEditor);
 
-            createWorkingCopy(sourceFile, editor);
+            connectWorkingCopy0(editor);
         }
     }
 
     private void disconnectWorkingCopy(XtextEditor editor)
     {
-        SourceFile sourceFile = discardWorkingCopy(editor);
-        if (sourceFile == null)
+        if (!disconnectWorkingCopy0(editor))
             return;
 
         XtextEditor mruClone = findMruClone(editor);
         if (mruClone != null)
         {
-            createWorkingCopy(sourceFile, mruClone);
+            connectWorkingCopy0(mruClone);
         }
     }
 
-    private void createWorkingCopy(SourceFile sourceFile, XtextEditor editor)
+    private void connectWorkingCopy0(XtextEditor editor)
     {
-        try (TextEditorBuffer buffer = new TextEditorBuffer(editor))
+        ISourceFile workingCopy = null;
+        try
         {
-            if (sourceFile.hBecomeWorkingCopy(buffer, // will addRef() the buffer
-                (IBuffer b) -> new XtextWorkingCopyInfo(b), null) != null)
-            {
-                sourceFile.hDiscardWorkingCopy();
-
-                throw new IllegalStateException("Already a working copy: " //$NON-NLS-1$
-                    + sourceFile);
-            }
-
-            workingCopies.put(editor, new WorkingCopyInfo(sourceFile, true));
-
-            setHighlightRange(editor,
-                editor.getSelectionProvider().getSelection());
+            workingCopy = acquireWorkingCopy(editor);
         }
         catch (CoreException e)
         {
-            workingCopies.put(editor, new WorkingCopyInfo(sourceFile, false));
-
-            editor.resetHighlightRange();
-
             if (!editor.getEditorInput().exists())
                 ; // this is considered normal
             else
                 Activator.log(e.getStatus());
         }
+        if (workingCopy != null)
+        {
+            if (!Elements.isWorkingCopy(workingCopy))
+                throw new AssertionError();
+            try (IBuffer buffer = Elements.getBuffer(workingCopy))
+            {
+                if (buffer.getDocument() != editor.getDocument())
+                {
+                    releaseWorkingCopy(editor, workingCopy);
+                    throw new AssertionError();
+                }
+            }
+            catch (CoreException e)
+            {
+                Activator.log(e.getStatus());
+            }
+        }
+        workingCopyEditors.put(editor.getEditorInput(),
+            new WorkingCopyEditorInfo(editor, workingCopy));
+        if (workingCopy != null)
+            setHighlightRange(editor,
+                editor.getSelectionProvider().getSelection());
+        else
+            editor.resetHighlightRange();
     }
 
-    private SourceFile discardWorkingCopy(XtextEditor editor)
+    private boolean disconnectWorkingCopy0(XtextEditor editor)
     {
-        WorkingCopyInfo workingCopyInfo = workingCopies.remove(editor);
-        if (workingCopyInfo == null)
-            return null;
-        if (workingCopyInfo.success)
+        WorkingCopyEditorInfo info = workingCopyEditors.get(
+            editor.getEditorInput());
+        if (info == null || info.editor != editor)
+            return false;
+        workingCopyEditors.remove(editor.getEditorInput());
+        if (info.workingCopy != null)
         {
-            workingCopyInfo.sourceFile.hDiscardWorkingCopy();
+            releaseWorkingCopy(editor, info.workingCopy);
             editor.resetHighlightRange();
         }
-        return workingCopyInfo.sourceFile;
+        return true;
     }
 
-    private XtextEditor getWorkingCopyEditor(SourceFile sourceFile)
+    private XtextEditor getWorkingCopyEditor(IEditorInput editorInput)
     {
-        Set<Entry<XtextEditor, WorkingCopyInfo>> entrySet =
-            workingCopies.entrySet();
-        for (Entry<XtextEditor, WorkingCopyInfo> entry : entrySet)
-        {
-            if (entry.getValue().sourceFile.equals(sourceFile))
-                return entry.getKey();
-        }
-        return null;
+        WorkingCopyEditorInfo info = workingCopyEditors.get(editorInput);
+        if (info == null)
+            return null;
+        return info.editor;
     }
 
     private XtextEditor findMruClone(XtextEditor editor)
@@ -419,43 +441,34 @@
     }
 
     private void scheduleHighlightRangeJob(XtextEditor editor,
-        SourceFile sourceFile, ISelection selection)
+        ISelection selection)
     {
-        HighlightRangeJob highlightRangeJob = highlightRangeJobs.get(editor);
-        if (highlightRangeJob == null)
-        {
-            highlightRangeJob = new HighlightRangeJob(editor);
-            highlightRangeJobs.put(editor, highlightRangeJob);
-        }
+        ISourceFile workingCopy = getWorkingCopy(editor);
+        if (workingCopy == null)
+            return;
+        EditorInfo info = editorInfoMap.get(editor);
+        if (info == null)
+            return;
+        HighlightRangeJob highlightRangeJob = info.highlightRangeJob;
         highlightRangeJob.cancel();
-        highlightRangeJob.setArgs(new HighlightArgs(sourceFile, selection));
+        highlightRangeJob.setArgs(new HighlightArgs(workingCopy, selection));
         highlightRangeJob.schedule();
     }
 
-    private void disposeHighlightRangeJob(XtextEditor editor)
-    {
-        HighlightRangeJob highlightRangeJob = highlightRangeJobs.remove(editor);
-        if (highlightRangeJob != null)
-        {
-            highlightRangeJob.cancel();
-            highlightRangeJob.setArgs(null);
-        }
-    }
-
     private class HighlightRangeJob
         extends Job
     {
         private final XtextEditor editor;
         private volatile HighlightArgs args;
 
-        public HighlightRangeJob(XtextEditor editor)
+        HighlightRangeJob(XtextEditor editor)
         {
             super(""); //$NON-NLS-1$
             setSystem(true);
             this.editor = editor;
         }
 
-        public void setArgs(HighlightArgs args)
+        void setArgs(HighlightArgs args)
         {
             this.args = args;
         }
@@ -466,7 +479,7 @@
             HighlightArgs args = this.args;
             if (args == null)
                 return Status.OK_STATUS;
-            SourceFile sourceFile = args.sourceFile;
+            ISourceFile sourceFile = args.sourceFile;
             ISelection selection = args.selection;
             ISourceElement selectedElement = null;
             if (selection instanceof ITextSelection)
@@ -524,8 +537,8 @@
             return Status.OK_STATUS;
         }
 
-        private void setEditorHighlightRange(final HighlightArgs args,
-            final int offset, final int length)
+        private void setEditorHighlightRange(HighlightArgs args, int offset,
+            int length)
         {
             PlatformUI.getWorkbench().getDisplay().asyncExec(new Runnable()
             {
@@ -537,7 +550,7 @@
             });
         }
 
-        private void resetEditorHighlightRange(final HighlightArgs args)
+        private void resetEditorHighlightRange(HighlightArgs args)
         {
             PlatformUI.getWorkbench().getDisplay().asyncExec(new Runnable()
             {
@@ -559,45 +572,40 @@
 
     private static class HighlightArgs
     {
-        public final SourceFile sourceFile;
-        public final ISelection selection;
+        final ISourceFile sourceFile;
+        final ISelection selection;
 
         /*
          * @param sourceFile not null
          * @param selection not null
          */
-        public HighlightArgs(SourceFile sourceFile, ISelection selection)
+        HighlightArgs(ISourceFile sourceFile, ISelection selection)
         {
             this.sourceFile = sourceFile;
             this.selection = selection;
         }
     }
 
-    /*
-     * Multiple XtextEditor instances may simultaneously be opened on a given
-     * source file, each with its own underlying document, but only one of them
-     * can be designated the working copy editor and connected to the source
-     * file's working copy. This class is used for tracking the source file's
-     * working copy editor. The success flag indicates whether the working copy
-     * of the source file was created successfully by the working copy editor.
-     * (A common reason for failure is that the editor input doesn't exist.)
-     *
-     * @see #getWorkingCopyEditor(SourceFile)
-     */
-    private static class WorkingCopyInfo
+    private static class EditorInfo
     {
-        public final SourceFile sourceFile;
-        public final boolean success;
+        IPartListener partListener;
+        ISelectionChangedListener selectionChangedListener;
+        HighlightRangeJob highlightRangeJob;
+    }
+
+    private static class WorkingCopyEditorInfo
+    {
+        final XtextEditor editor;
+        final ISourceFile workingCopy;
 
         /*
-         * @param sourceFile not null
-         * @param success whether sourceFile.becomeWorkingCopy was successful,
-         *  so sourceFile.discardWorkingCopy() is to be called
+         * @param editor not null
+         * @param workingCopy may be null
          */
-        public WorkingCopyInfo(SourceFile sourceFile, boolean success)
+        WorkingCopyEditorInfo(XtextEditor editor, ISourceFile workingCopy)
         {
-            this.sourceFile = sourceFile;
-            this.success = success;
+            this.editor = editor;
+            this.workingCopy = workingCopy;
         }
     }
 }
diff --git a/org.eclipse.handly.xtext.ui/src/org/eclipse/handly/xtext/ui/editor/XtextWorkingCopyInfo.java b/org.eclipse.handly.xtext.ui/src/org/eclipse/handly/xtext/ui/editor/XtextWorkingCopyInfo.java
index 5919361..66c3f2f 100644
--- a/org.eclipse.handly.xtext.ui/src/org/eclipse/handly/xtext/ui/editor/XtextWorkingCopyInfo.java
+++ b/org.eclipse.handly.xtext.ui/src/org/eclipse/handly/xtext/ui/editor/XtextWorkingCopyInfo.java
@@ -19,6 +19,7 @@
 import org.eclipse.handly.context.Context;
 import org.eclipse.handly.context.IContext;
 import org.eclipse.handly.internal.xtext.ui.Activator;
+import org.eclipse.handly.model.impl.SourceFile;
 import org.eclipse.handly.model.impl.WorkingCopyInfo;
 import org.eclipse.handly.snapshot.NonExpiringSnapshot;
 import org.eclipse.jface.text.IDocument;
@@ -46,20 +47,25 @@
                 context.bind(SOURCE_CONTENTS).to(snapshot.getContents());
                 context.bind(SOURCE_SNAPSHOT).to(snapshot.getWrappedSnapshot());
                 context.bind(RECONCILING_FORCED).to(forced);
-                basicReconcile(context, monitor);
+                reconcile0(context, monitor);
             }
         };
 
     /**
      * Constructs a new working copy info and associates it with the given
-     * buffer; the buffer is NOT <code>addRef</code>'ed.
+     * source file and buffer. Does not <code>addRef</code> the given buffer.
+     * <p>
+     * Clients should explicitly {@link #dispose} the working copy info
+     * after it is no longer needed.
+     * </p>
      *
-     * @param buffer the working copy buffer (not <code>null</code>,
+     * @param sourceFile the working copy's source file (not <code>null</code>)
+     * @param buffer the working copy's buffer (not <code>null</code>,
      *  must provide a <code>HandlyXtextDocument</code>)
      */
-    public XtextWorkingCopyInfo(IBuffer buffer)
+    public XtextWorkingCopyInfo(SourceFile sourceFile, IBuffer buffer)
     {
-        super(buffer);
+        super(sourceFile, buffer);
         IDocument document = buffer.getDocument();
         if (!(document instanceof HandlyXtextDocument))
             throw new IllegalArgumentException();
@@ -80,13 +86,13 @@
     }
 
     @Override
-    protected boolean needsReconciling()
+    protected final boolean needsReconciling()
     {
         return getDocument().needsReconciling();
     }
 
     @Override
-    protected void reconcile(IContext context, IProgressMonitor monitor)
+    protected final void reconcile(IContext context, IProgressMonitor monitor)
         throws CoreException
     {
         try
@@ -105,7 +111,7 @@
         }
     }
 
-    protected HandlyXtextDocument getDocument()
+    protected final HandlyXtextDocument getDocument()
     {
         return (HandlyXtextDocument)getBuffer().getDocument();
     }
diff --git a/org.eclipse.handly/src/org/eclipse/handly/model/ElementDeltas.java b/org.eclipse.handly/src/org/eclipse/handly/model/ElementDeltas.java
index ef90293..0cd39db 100644
--- a/org.eclipse.handly/src/org/eclipse/handly/model/ElementDeltas.java
+++ b/org.eclipse.handly/src/org/eclipse/handly/model/ElementDeltas.java
@@ -42,8 +42,8 @@
  * on delta A will return the handle for B. The delta for B will have status
  * <code>ADDED</code>, with change flag <code>F_MOVED_FROM</code>, and {@link
  * #getMovedFromElement} on delta B will return the handle for A. (Note,
- * the handle to A in this case represents an element that no longer exists).
- * Note that the move change flags only describe the changes to a single element,
+ * the handle to A in this case represents an element that no longer exists.)
+ * Move change flags only describe changes to a single element,
  * they do not imply anything about the parent or children of the element.
  * </p>
  * <p>
diff --git a/org.eclipse.handly/src/org/eclipse/handly/model/Elements.java b/org.eclipse.handly/src/org/eclipse/handly/model/Elements.java
index b72dda1..2d6b618 100644
--- a/org.eclipse.handly/src/org/eclipse/handly/model/Elements.java
+++ b/org.eclipse.handly/src/org/eclipse/handly/model/Elements.java
@@ -582,6 +582,11 @@
      * be shared by multiple clients, so the returned buffer may have unsaved
      * changes if it has been modified by another client.
      * <p>
+     * For working copies, the relationship between a source file and its buffer
+     * does not change over the lifetime of a working copy. Otherwise, a new
+     * buffer may be returned each time this method is invoked.
+     * </p>
+     * <p>
      * The client takes (potentially shared) ownership of the returned buffer
      * and is responsible for releasing it when finished. The buffer will be
      * disposed only after it is released by every owner. The buffer must not
@@ -604,6 +609,11 @@
      * be shared by multiple clients, so the returned buffer may have unsaved
      * changes if it has been modified by another client.
      * <p>
+     * For working copies, the relationship between a source file and its buffer
+     * does not change over the lifetime of a working copy. Otherwise, a new
+     * buffer may be returned each time this method is invoked.
+     * </p>
+     * <p>
      * The client takes (potentially shared) ownership of the returned buffer
      * and is responsible for releasing it when finished. The buffer will be
      * disposed only after it is released by every owner. The buffer must not
diff --git a/org.eclipse.handly/src/org/eclipse/handly/model/ISourceFileExtension.java b/org.eclipse.handly/src/org/eclipse/handly/model/ISourceFileExtension.java
index 1c00693..173069e 100644
--- a/org.eclipse.handly/src/org/eclipse/handly/model/ISourceFileExtension.java
+++ b/org.eclipse.handly/src/org/eclipse/handly/model/ISourceFileExtension.java
@@ -90,6 +90,11 @@
      * be shared by multiple clients, so the returned buffer may have unsaved
      * changes if it has been modified by another client.
      * <p>
+     * For working copies, the relationship between a source file and its buffer
+     * does not change over the lifetime of a working copy. Otherwise, a new
+     * buffer may be returned each time this method is invoked.
+     * </p>
+     * <p>
      * The client takes (potentially shared) ownership of the returned buffer
      * and is responsible for releasing it when finished. The buffer will be
      * disposed only after it is released by every owner. The buffer must not
diff --git a/org.eclipse.handly/src/org/eclipse/handly/model/impl/DefaultWorkingCopyInfo.java b/org.eclipse.handly/src/org/eclipse/handly/model/impl/DefaultWorkingCopyInfo.java
index 11c5f82..d7d3115 100644
--- a/org.eclipse.handly/src/org/eclipse/handly/model/impl/DefaultWorkingCopyInfo.java
+++ b/org.eclipse.handly/src/org/eclipse/handly/model/impl/DefaultWorkingCopyInfo.java
@@ -38,17 +38,22 @@
 
     /**
      * Constructs a new working copy info and associates it with the given
-     * buffer; the buffer is NOT <code>addRef</code>'ed.
+     * source file and buffer. Does not <code>addRef</code> the given buffer.
+     * <p>
+     * Clients should explicitly {@link #dispose} the working copy info
+     * after it is no longer needed.
+     * </p>
      *
-     * @param buffer the working copy buffer (not <code>null</code>)
+     * @param sourceFile the working copy's source file (not <code>null</code>)
+     * @param buffer the working copy's buffer (not <code>null</code>)
      */
-    public DefaultWorkingCopyInfo(IBuffer buffer)
+    public DefaultWorkingCopyInfo(SourceFile sourceFile, IBuffer buffer)
     {
-        super(buffer);
+        super(sourceFile, buffer);
     }
 
     @Override
-    protected boolean needsReconciling()
+    protected final boolean needsReconciling()
     {
         return !getBuffer().getSnapshot().isEqualTo(reconciledSnapshot);
     }
@@ -58,7 +63,7 @@
         throws CoreException
     {
         if (context.containsKey(SOURCE_AST))
-            throw new IllegalArgumentException(); // just to be safe that we don't pass SOURCE_AST to #basicReconcile accidentally
+            throw new IllegalArgumentException(); // just to be safe that we don't pass SOURCE_AST to #reconcile0 accidentally
 
         synchronized (reconcilingLock)
         {
@@ -67,8 +72,8 @@
             {
                 NonExpiringSnapshot snapshot = new NonExpiringSnapshot(
                     getBuffer());
-                basicReconcile(with(of(SOURCE_CONTENTS, snapshot.getContents()),
-                    of(SOURCE_SNAPSHOT, snapshot.getWrappedSnapshot()), of(
+                reconcile0(with(of(SOURCE_CONTENTS, snapshot.getContents()), of(
+                    SOURCE_SNAPSHOT, snapshot.getWrappedSnapshot()), of(
                         RECONCILING_FORCED, !needsReconciling), context),
                     monitor);
                 reconciledSnapshot = snapshot.getWrappedSnapshot();
diff --git a/org.eclipse.handly/src/org/eclipse/handly/model/impl/Element.java b/org.eclipse.handly/src/org/eclipse/handly/model/impl/Element.java
index 646f059..6fc1222 100644
--- a/org.eclipse.handly/src/org/eclipse/handly/model/impl/Element.java
+++ b/org.eclipse.handly/src/org/eclipse/handly/model/impl/Element.java
@@ -572,7 +572,7 @@
                 if (monitor.isCanceled())
                     throw new OperationCanceledException();
 
-                hBuildStructure(context, new SubProgressMonitor(monitor, 1));
+                hBuildStructure0(context, new SubProgressMonitor(monitor, 1));
 
                 Object body = context.get(NEW_ELEMENTS).get(this);
                 if (body == null)
@@ -589,6 +589,12 @@
         }
     }
 
+    void hBuildStructure0(IContext context, IProgressMonitor monitor)
+        throws CoreException
+    {
+        hBuildStructure(context, monitor);
+    }
+
     /**
      * Closes this element, removing any previously registered handle/body
      * relationships for it and its existing descendants.
diff --git a/org.eclipse.handly/src/org/eclipse/handly/model/impl/ElementManager.java b/org.eclipse.handly/src/org/eclipse/handly/model/impl/ElementManager.java
index f0d6f7f..030689c 100644
--- a/org.eclipse.handly/src/org/eclipse/handly/model/impl/ElementManager.java
+++ b/org.eclipse.handly/src/org/eclipse/handly/model/impl/ElementManager.java
@@ -36,8 +36,7 @@
     private ThreadLocal<Map<IElement, Object>> temporaryCache =
         new ThreadLocal<>();
 
-    private Map<ISourceFile, WorkingCopyInfo> workingCopyInfos =
-        new HashMap<>();
+    private Map<SourceFile, WorkingCopyInfo> workingCopyInfos = new HashMap<>();
 
     /**
      * Constructs an element manager with the given body cache.
@@ -54,7 +53,15 @@
     }
 
     /**
-     * Returns the source files that have corresponding working copy info.
+     * Returns the source files that are currently in working copy mode.
+     * Performs atomically.
+     * <p>
+     * Note that the result may immediately become stale if other threads can
+     * create or destroy working copies that are managed by this manager.
+     * </p>
+     *
+     * @return the source files that are currently in working copy mode
+     *  (never <code>null</code>)
      */
     public final synchronized ISourceFile[] getWorkingCopies()
     {
@@ -62,42 +69,19 @@
     }
 
     /**
-     * A handle/body relationship is going to be removed from the body cache
-     * associated with this manager. Do any necessary cleanup.
+     * Attempts to close the given element. If the current state of the element
+     * does not permit closing (e.g., a working copy), it will stay open. Closing
+     * of an element usually involves closing its children and removal of its body
+     * from the cache.
      * <p>
-     * This method is called internally; it is not intended to be called by clients.
+     * This method is called internally; it is not intended to be invoked by clients.
      * </p>
      *
-     * @param element the element whose body is going to be removed
-     *  (never <code>null</code>)
-     * @param body the corresponding body that is going to be removed
-     *  (never <code>null</code>)
+     * @param element the element that needs closing (never <code>null</code>)
      */
-    protected void removing(IElement element, Object body)
+    protected void close(IElement element)
     {
-        ((Element)element).hRemoving(body);
-    }
-
-    /**
-     * Given a body, closes the children of the given element. If the current
-     * state of a child element does not permit closing (e.g., a working copy),
-     * it will stay open. Closing of an element usually involves closing its
-     * children and removal of its body from the cache.
-     * <p>
-     * This method is called internally; it is not intended to be called by clients.
-     * </p>
-     *
-     * @param element the element whose children need to be closed
-     *  (never <code>null</code>)
-     * @param body the body corresponding to the given element
-     *  (never <code>null</code>)
-     */
-    protected void closeChildren(IElement element, Object body)
-    {
-        for (IElement child : (((Element)element).hChildren(body)))
-        {
-            ((Element)child).hClose(false);
-        }
+        ((Element)element).hClose(false);
     }
 
     /**
@@ -113,7 +97,7 @@
      * @return the corresponding body for the given element, or
      *  <code>null</code> if no body is registered for the element
      */
-    synchronized Object get(IElement element)
+    synchronized Object get(Element element)
     {
         Map<IElement, Object> tempCache = temporaryCache.get();
         if (tempCache != null)
@@ -139,7 +123,7 @@
      * @return the corresponding body for the given element, or
      *  <code>null</code> if no body is registered for the element
      */
-    synchronized Object peek(IElement element)
+    synchronized Object peek(Element element)
     {
         Map<IElement, Object> tempCache = temporaryCache.get();
         if (tempCache != null)
@@ -160,16 +144,16 @@
      *  to be stored in the body cache (not <code>null</code>). At a minimum,
      *  it must contain a body for the given element
      */
-    synchronized void put(IElement element, Map<IElement, Object> newElements)
+    synchronized void put(Element element, Map<IElement, Object> newElements)
     {
         // remove existing children as they are replaced with the new children contained in newElements
         remove(element);
 
         cache.putAll(newElements);
 
-        if (element instanceof ISourceFile)
+        if (element instanceof SourceFile)
         {
-            WorkingCopyInfo info = workingCopyInfos.get((ISourceFile)element);
+            WorkingCopyInfo info = workingCopyInfos.get((SourceFile)element);
             if (info != null && !info.created) // case of wc creation
                 info.created = true;
         }
@@ -187,7 +171,7 @@
      * @return the previous body for the given element, or <code>null</code>
      *  if the body cache did not previously contain a body for the element
      */
-    synchronized Object putIfAbsent(IElement element,
+    synchronized Object putIfAbsent(Element element,
         Map<IElement, Object> newElements)
     {
         Object existingBody = cache.peek(element);
@@ -204,16 +188,18 @@
      * contained no body for the element. Performs atomically.
      *
      * @param element the element whose body is to be removed from the body cache
-     * @see #removing(IElement, Object)
-     * @see #closeChildren(IElement, Object)
+     * @see #close(IElement)
      */
-    synchronized void remove(IElement element)
+    synchronized void remove(Element element)
     {
         Object body = cache.peek(element);
         if (body != null)
         {
-            removing(element, body);
-            closeChildren(element, body);
+            element.hRemoving(body);
+            for (IElement child : element.hChildren(body))
+            {
+                close(child);
+            }
             cache.remove(element);
         }
     }
@@ -268,7 +254,7 @@
      * Performs atomically.
      * <p>
      * Each successful call to this method must ultimately be followed
-     * by exactly one call to <code>discardWorkingCopyInfo</code>.
+     * by exactly one call to <code>releaseWorkingCopyInfo</code>.
      * </p>
      *
      * @param sourceFile the source file with which a working copy info
@@ -279,10 +265,10 @@
      * @return the previous working copy info associated with the given
      *  source file, or <code>null</code> if there was no working copy info
      *  for the source file
-     * @see #discardWorkingCopyInfo(ISourceFile)
+     * @see #releaseWorkingCopyInfo(SourceFile)
      */
-    WorkingCopyInfo putWorkingCopyInfoIfAbsent(ISourceFile sourceFile,
-        IBuffer buffer, IWorkingCopyInfoFactory factory)
+    WorkingCopyInfo putWorkingCopyInfoIfAbsent(SourceFile sourceFile,
+        IBuffer buffer, WorkingCopyInfo.Factory factory)
     {
         if (sourceFile == null)
             throw new IllegalArgumentException();
@@ -291,11 +277,13 @@
 
         final WorkingCopyInfo info;
         if (factory == null)
-            info = new DefaultWorkingCopyInfo(buffer);
+            info = new DefaultWorkingCopyInfo(sourceFile, buffer);
         else
-            info = factory.createWorkingCopyInfo(buffer);
+            info = factory.newWorkingCopyInfo(sourceFile, buffer);
         if (info.refCount != 0)
             throw new AssertionError();
+        if (info.getSourceFile() != sourceFile)
+            throw new AssertionError();
         boolean disposeInfo = true;
         boolean releaseBuffer = false;
         try
@@ -340,15 +328,15 @@
      * <p>
      * Each successful call to this method that did not return
      * <code>null</code> must ultimately be followed by exactly
-     * one call to <code>discardWorkingCopyInfo</code>.
+     * one call to <code>releaseWorkingCopyInfo</code>.
      * </p>
      *
      * @param sourceFile the source file whose working copy info is to be returned
      * @return the working copy info for the given source file,
      *  or <code>null</code> if the source file has no working copy info
-     * @see #discardWorkingCopyInfo(ISourceFile)
+     * @see #releaseWorkingCopyInfo(SourceFile)
      */
-    synchronized WorkingCopyInfo getWorkingCopyInfo(ISourceFile sourceFile)
+    synchronized WorkingCopyInfo getWorkingCopyInfo(SourceFile sourceFile)
     {
         WorkingCopyInfo info = workingCopyInfos.get(sourceFile);
         if (info != null)
@@ -364,7 +352,7 @@
      * @return the working copy info for the given source file,
      *  or <code>null</code> if the source file has no working copy info
      */
-    synchronized WorkingCopyInfo peekAtWorkingCopyInfo(ISourceFile sourceFile)
+    synchronized WorkingCopyInfo peekAtWorkingCopyInfo(SourceFile sourceFile)
     {
         return workingCopyInfos.get(sourceFile);
     }
@@ -375,11 +363,11 @@
      * working copy info and releases the working copy buffer. Has no effect if
      * there was no working copy info for the source file. Performs atomically.
      *
-     * @param sourceFile the source file whose working copy info is to be discarded
+     * @param sourceFile the source file whose working copy info is to be released
      * @return the working copy info for the given source file,
      *  or <code>null</code> if the source file had no working copy info
      */
-    WorkingCopyInfo discardWorkingCopyInfo(ISourceFile sourceFile)
+    WorkingCopyInfo releaseWorkingCopyInfo(SourceFile sourceFile)
     {
         WorkingCopyInfo infoToDispose = null;
         try
diff --git a/org.eclipse.handly/src/org/eclipse/handly/model/impl/IBodyCache.java b/org.eclipse.handly/src/org/eclipse/handly/model/impl/IBodyCache.java
index cfcb3f3..cadd4b6 100644
--- a/org.eclipse.handly/src/org/eclipse/handly/model/impl/IBodyCache.java
+++ b/org.eclipse.handly/src/org/eclipse/handly/model/impl/IBodyCache.java
@@ -63,9 +63,9 @@
      * @param elementBodies handle/body relationships to be stored in the cache
      *  (not <code>null</code>)
      */
-    default void putAll(Map<IElement, Object> elementBodies)
+    default void putAll(Map<? extends IElement, Object> elementBodies)
     {
-        for (Map.Entry<IElement, Object> entry : elementBodies.entrySet())
+        for (Map.Entry<? extends IElement, Object> entry : elementBodies.entrySet())
         {
             put(entry.getKey(), entry.getValue());
         }
diff --git a/org.eclipse.handly/src/org/eclipse/handly/model/impl/ISourceFileImpl.java b/org.eclipse.handly/src/org/eclipse/handly/model/impl/ISourceFileImpl.java
index 9c25458..bbc67ee 100644
--- a/org.eclipse.handly/src/org/eclipse/handly/model/impl/ISourceFileImpl.java
+++ b/org.eclipse.handly/src/org/eclipse/handly/model/impl/ISourceFileImpl.java
@@ -89,6 +89,11 @@
      * be shared by multiple clients, so the returned buffer may have unsaved
      * changes if it has been modified by another client.
      * <p>
+     * For working copies, the relationship between a source file and its buffer
+     * does not change over the lifetime of a working copy. Otherwise, a new
+     * buffer may be returned each time this method is invoked.
+     * </p>
+     * <p>
      * The client takes (potentially shared) ownership of the returned buffer
      * and is responsible for releasing it when finished. The buffer will be
      * disposed only after it is released by every owner. The buffer must not
diff --git a/org.eclipse.handly/src/org/eclipse/handly/model/impl/IWorkingCopyInfoFactory.java b/org.eclipse.handly/src/org/eclipse/handly/model/impl/IWorkingCopyInfoFactory.java
deleted file mode 100644
index a323dda..0000000
--- a/org.eclipse.handly/src/org/eclipse/handly/model/impl/IWorkingCopyInfoFactory.java
+++ /dev/null
@@ -1,30 +0,0 @@
-/*******************************************************************************
- * Copyright (c) 2015, 2016 1C-Soft LLC.
- * 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:
- *     Vladimir Piskarev (1C) - initial API and implementation
- *******************************************************************************/
-package org.eclipse.handly.model.impl;
-
-import org.eclipse.handly.buffer.IBuffer;
-
-/**
- * A factory of working copy info.
- */
-public interface IWorkingCopyInfoFactory
-{
-    /**
-     * Returns a new working copy info associated with the given buffer;
-     * the buffer is NOT <code>addRef</code>'ed. The created working copy
-     * info must be explicitly disposed after it is no longer needed.
-     *
-     * @param buffer the buffer to be associated with the
-     *  created working copy info (not <code>null</code>)
-     * @return the created working copy info (never <code>null</code>)
-     */
-    WorkingCopyInfo createWorkingCopyInfo(IBuffer buffer);
-}
diff --git a/org.eclipse.handly/src/org/eclipse/handly/model/impl/SourceFile.java b/org.eclipse.handly/src/org/eclipse/handly/model/impl/SourceFile.java
index 693c8e4..037ec5a 100644
--- a/org.eclipse.handly/src/org/eclipse/handly/model/impl/SourceFile.java
+++ b/org.eclipse.handly/src/org/eclipse/handly/model/impl/SourceFile.java
@@ -10,6 +10,7 @@
  *******************************************************************************/
 package org.eclipse.handly.model.impl;
 
+import static org.eclipse.handly.context.Contexts.EMPTY_CONTEXT;
 import static org.eclipse.handly.context.Contexts.of;
 import static org.eclipse.handly.context.Contexts.with;
 import static org.eclipse.handly.model.IElementDeltaConstants.F_WORKING_COPY;
@@ -72,113 +73,111 @@
     public final IBuffer hBuffer(IContext context, IProgressMonitor monitor)
         throws CoreException
     {
-        WorkingCopyInfo info = hAcquireWorkingCopy();
-        if (info == null)
+        if (monitor == null)
+            monitor = new NullProgressMonitor();
+        monitor.beginTask("", 100); //$NON-NLS-1$
+        try
         {
-            return hFileBuffer(context, monitor);
+            WorkingCopyInfo info = hAcquireExistingWorkingCopy(
+                new SubProgressMonitor(monitor, 10));
+            if (info == null)
+            {
+                return hFileBuffer(context, new SubProgressMonitor(monitor,
+                    90));
+            }
+            else
+            {
+                try
+                {
+                    IBuffer buffer = info.getBuffer();
+                    buffer.addRef();
+                    return buffer;
+                }
+                finally
+                {
+                    hReleaseWorkingCopy();
+                }
+            }
         }
-        else
+        finally
         {
-            try
-            {
-                IBuffer buffer = info.getBuffer();
-                buffer.addRef();
-                return buffer;
-            }
-            finally
-            {
-                hDiscardWorkingCopy();
-            }
+            monitor.done();
         }
     }
 
     /**
      * If this source file is not already in working copy mode, switches it
-     * into a working copy, associates it with the given buffer, and acquires
-     * an independent ownership of the working copy and its buffer. Performs
-     * atomically.
+     * into a working copy, associates it with a working copy buffer via a new
+     * working copy info, and acquires an independent ownership of the working
+     * copy (and, hence, of the working copy buffer). Performs atomically.
      * <p>
-     * Switching to working copy means that the source file's structure and
-     * properties will no longer correspond to the underlying resource contents
-     * and will no longer be updated by a resource delta processor. Instead,
-     * those structure and properties can be explicitly {@link #hReconcile(
-     * IContext, IProgressMonitor) reconciled} with the current contents of
-     * the working copy buffer.
+     * In working copy mode the source file's structure and properties
+     * shall no longer correspond to the underlying resource contents
+     * and must no longer be updated by a resource delta processor.
+     * Instead, the source file's structure and properties can be explicitly
+     * {@link #hReconcile(IContext, IProgressMonitor) reconciled} with the
+     * current contents of the working copy buffer.
+     * </p>
+     * <p>
+     * This method supports the following options, which may be specified
+     * in the given context:
+     * </p>
+     * <ul>
+     * <li>
+     * {@link #WORKING_COPY_BUFFER} - Specifies the working copy buffer.
+     * If not set, a default buffer is associated with the working copy.
+     * </li>
+     * <li>
+     * {@link #WORKING_COPY_INFO_FACTORY} - Specifies the working copy info factory.
+     * If not set, a default factory is used for obtaining a new working copy info.
+     * </li>
+     * </ul>
      * </p>
      * <p>
      * If the source file was already in working copy mode, this method acquires
      * a new independent ownership of the working copy by incrementing an internal
      * counter and returns the info associated with the working copy; the given
-     * buffer is ignored.
+     * context is ignored. The returned info is owned by the working copy and
+     * must not be explicitly disposed by the client.
      * </p>
      * <p>
      * Each successful call to this method must ultimately be followed
-     * by exactly one call to <code>hDiscardWorkingCopy</code>.
+     * by exactly one call to <code>hReleaseWorkingCopy</code>.
      * </p>
      *
-     * @param buffer the working copy buffer (not <code>null</code>)
+     * @param context the operation context (not <code>null</code>)
      * @param monitor a progress monitor, or <code>null</code>
      *  if progress reporting is not desired
      * @return the working copy info previously associated with
      *  this source file, or <code>null</code> if there was no
      *  working copy info for this source file
-     * @throws CoreException if the working copy cannot be created
+     * @throws CoreException if the working copy could not be created successfully
      * @throws OperationCanceledException if this method is canceled
-     * @see #hDiscardWorkingCopy()
+     * @see #hReleaseWorkingCopy()
+     * @see #hAcquireExistingWorkingCopy(IProgressMonitor)
      */
-    public final WorkingCopyInfo hBecomeWorkingCopy(IBuffer buffer,
+    public final WorkingCopyInfo hBecomeWorkingCopy(IContext context,
         IProgressMonitor monitor) throws CoreException
     {
-        return hBecomeWorkingCopy(buffer, null, monitor);
-    }
-
-    /**
-     * If this source file is not already in working copy mode, switches it
-     * into a working copy, associates it with the given buffer via a new
-     * working copy info obtained from the given factory, and acquires an
-     * independent ownership of the working copy and its buffer. Performs
-     * atomically.
-     * <p>
-     * Switching to working copy means that the source file's structure and
-     * properties will no longer correspond to the underlying resource contents
-     * and will no longer be updated by a resource delta processor. Instead,
-     * those structure and properties can be explicitly {@link #hReconcile(
-     * IContext, IProgressMonitor) reconciled} with the current contents of
-     * the working copy buffer.
-     * </p>
-     * <p>
-     * If the source file was already in working copy mode, this method acquires
-     * a new independent ownership of the working copy by incrementing an internal
-     * counter and returns the info associated with the working copy; the given
-     * buffer and factory are ignored.
-     * </p>
-     * <p>
-     * Each successful call to this method must ultimately be followed
-     * by exactly one call to <code>hDiscardWorkingCopy</code>.
-     * </p>
-     *
-     * @param buffer the working copy buffer (not <code>null</code>)
-     * @param factory the working copy info factory, or <code>null</code>
-     *  if a default factory is to be used
-     * @param monitor a progress monitor, or <code>null</code>
-     *  if progress reporting is not desired
-     * @return the working copy info previously associated with
-     *  this source file, or <code>null</code> if there was no
-     *  working copy info for this source file
-     * @throws CoreException if the working copy cannot be created
-     * @throws OperationCanceledException if this method is canceled
-     * @see #hDiscardWorkingCopy()
-     */
-    public final WorkingCopyInfo hBecomeWorkingCopy(IBuffer buffer,
-        IWorkingCopyInfoFactory factory, IProgressMonitor monitor)
-        throws CoreException
-    {
+        if (context == null)
+            throw new IllegalArgumentException();
+        IBuffer buffer = context.get(WORKING_COPY_BUFFER);
+        if (buffer == null)
+        {
+            try (IBuffer defaultBuffer = hFileBuffer(context, monitor))
+            {
+                return hBecomeWorkingCopy(with(of(WORKING_COPY_BUFFER,
+                    defaultBuffer), context), monitor);
+            }
+        }
         WorkingCopyProvider provider = new WorkingCopyProvider()
         {
             @Override
             protected WorkingCopyInfo doAcquireWorkingCopy()
             {
-                return putWorkingCopyInfoIfAbsent(buffer, factory);
+                return hElementManager().putWorkingCopyInfoIfAbsent(
+                    SourceFile.this, buffer, context.get(
+                        WORKING_COPY_INFO_FACTORY));
             }
 
             @Override
@@ -195,42 +194,68 @@
             boolean success = false;
             try
             {
-                WorkingCopyInfo newInfo = hPeekAtWorkingCopyInfo();
-                newInfo.workingCopy = this;
-                newInfo.initTask.execute(monitor);
+                WorkingCopyInfo newInfo =
+                    hElementManager().peekAtWorkingCopyInfo(this);
+                newInfo.initTask.execute(context, monitor);
                 success = true;
             }
             finally
             {
                 if (!success)
-                    hDiscardWorkingCopy();
+                    hReleaseWorkingCopy();
             }
         }
         return oldInfo;
     }
 
     /**
+     * Specifies the working copy buffer.
+     * @see #hBecomeWorkingCopy(IContext, IProgressMonitor)
+     */
+    public static final Property<IBuffer> WORKING_COPY_BUFFER = Property.get(
+        SourceFile.class.getName() + ".workingCopyBuffer", IBuffer.class); //$NON-NLS-1$
+    /**
+     * Specifies the working copy info factory.
+     * @see #hBecomeWorkingCopy(IContext, IProgressMonitor)
+     */
+    public static final Property<WorkingCopyInfo.Factory> WORKING_COPY_INFO_FACTORY =
+        Property.get(SourceFile.class.getName() + ".workingCopyInfoFactory", //$NON-NLS-1$
+            WorkingCopyInfo.Factory.class);
+
+    /**
      * If this source file is in working copy mode, acquires a new independent
      * ownership of the working copy by incrementing an internal counter and
-     * returns the info associated with the working copy. Returns <code>null</code>
-     * if this source file is not a working copy. Performs atomically.
+     * returns the info associated with the working copy. The returned info is
+     * owned by the working copy and must not be explicitly disposed by the client.
+     * Returns <code>null</code> if this source file is not a working copy.
+     * Performs atomically.
      * <p>
      * Each successful call to this method that did not return <code>null</code>
-     * must ultimately be followed by exactly one call to <code>hDiscardWorkingCopy</code>.
+     * must ultimately be followed by exactly one call to <code>hReleaseWorkingCopy</code>.
      * </p>
      *
      * @return the working copy info for this source file,
      *  or <code>null</code> if this source file is not a working copy
-     * @see #hDiscardWorkingCopy()
+     * @see #hReleaseWorkingCopy()
+     * @see #hBecomeWorkingCopy(IContext, IProgressMonitor)
      */
-    public final WorkingCopyInfo hAcquireWorkingCopy()
+    public final WorkingCopyInfo hAcquireExistingWorkingCopy(
+        IProgressMonitor monitor)
     {
         WorkingCopyProvider provider = new WorkingCopyProvider()
         {
             @Override
             protected WorkingCopyInfo doAcquireWorkingCopy()
             {
-                return getWorkingCopyInfo();
+                return hElementManager().getWorkingCopyInfo(SourceFile.this);
+            }
+
+            @Override
+            protected boolean isCanceled()
+            {
+                if (monitor == null)
+                    return false;
+                return monitor.isCanceled();
             }
         };
         return provider.acquireWorkingCopy();
@@ -238,23 +263,22 @@
 
     /**
      * Relinquishes an independent ownership of the working copy by decrementing
-     * an internal counter. If there are no remaining independent owners of the
-     * working copy, switches this source file from working copy mode back to
-     * its original mode and releases the working copy buffer. Performs
-     * atomically.
+     * an internal counter. If there are no remaining owners of the working copy,
+     * switches this source file from working copy mode back to its original mode
+     * and releases the working copy buffer. Performs atomically.
      * <p>
      * Each independent ownership of the working copy must ultimately end
-     * with exactly one call to this method. If a client is not an independent
-     * owner of the working copy, it must not call this method.
+     * with exactly one call to this method. Clients that do not own the
+     * working copy must not call this method.
      * </p>
      *
      * @return <code>true</code> if this source file was switched from
      *  working copy mode back to its original mode, <code>false</code>
      *  otherwise
      */
-    public final boolean hDiscardWorkingCopy()
+    public final boolean hReleaseWorkingCopy()
     {
-        WorkingCopyInfo info = hElementManager().discardWorkingCopyInfo(this);
+        WorkingCopyInfo info = hElementManager().releaseWorkingCopyInfo(this);
         if (info == null)
             throw new IllegalStateException("Not a working copy: " + hToString( //$NON-NLS-1$
                 of(FORMAT_STYLE, MEDIUM)));
@@ -266,24 +290,45 @@
         return false;
     }
 
+    /**
+     * If this source file is in working copy mode, returns the working copy info
+     * without acquiring an independent ownership of the working copy. The
+     * returned info is owned by the working copy and must not be explicitly
+     * disposed by the client. Returns <code>null</code> if this source file
+     * is not a working copy.
+     * <p>
+     * Note that if this method is invoked by a client that does not own the
+     * working copy, the returned info may get disposed from another thread.
+     * </p>
+     *
+     * @return the working copy info for this source file,
+     *  or <code>null</code> if this source file is not a working copy
+     * @see #hAcquireExistingWorkingCopy(IProgressMonitor)
+     * @see #hBecomeWorkingCopy(IContext, IProgressMonitor)
+     */
+    public final WorkingCopyInfo hWorkingCopyInfo()
+    {
+        WorkingCopyInfo info = hElementManager().peekAtWorkingCopyInfo(this);
+        if (info == null)
+            return null;
+        if (info.created)
+            return info;
+        // special case: wc creation is in progress on the current thread
+        if (this.equals(CURRENTLY_RECONCILED.get()))
+            return info;
+        return null;
+    }
+
     @Override
     public final boolean hIsWorkingCopy()
     {
-        WorkingCopyInfo info = hPeekAtWorkingCopyInfo();
-        if (info == null)
-            return false;
-        if (info.created)
-            return true;
-        // special case: wc creation is in progress on the current thread
-        if (this.equals(CURRENTLY_RECONCILED.get()))
-            return true;
-        return false;
+        return hWorkingCopyInfo() != null;
     }
 
     @Override
     public final boolean hNeedsReconciling()
     {
-        WorkingCopyInfo info = hAcquireWorkingCopy();
+        WorkingCopyInfo info = hAcquireExistingWorkingCopy(null);
         if (info == null)
             return false;
         else
@@ -294,7 +339,7 @@
             }
             finally
             {
-                hDiscardWorkingCopy();
+                hReleaseWorkingCopy();
             }
         }
     }
@@ -303,22 +348,31 @@
     public final void hReconcile(IContext context, IProgressMonitor monitor)
         throws CoreException
     {
-        WorkingCopyInfo info = hAcquireWorkingCopy();
-        if (info == null)
-            return; // not a working copy
-        else
+        if (monitor == null)
+            monitor = new NullProgressMonitor();
+        monitor.beginTask("", 100); //$NON-NLS-1$
+        try
         {
-            try
+            WorkingCopyInfo info = hAcquireExistingWorkingCopy(
+                new SubProgressMonitor(monitor, 10));
+            if (info == null)
+                return; // not a working copy
+            else
             {
-                if (monitor == null)
-                    monitor = new NullProgressMonitor();
-
-                info.reconcile(context, monitor);
+                try
+                {
+                    info.reconcile(context, new SubProgressMonitor(monitor,
+                        90));
+                }
+                finally
+                {
+                    hReleaseWorkingCopy();
+                }
             }
-            finally
-            {
-                hDiscardWorkingCopy();
-            }
+        }
+        finally
+        {
+            monitor.done();
         }
     }
 
@@ -345,17 +399,19 @@
     }
 
     /**
-     * If this source file is in working copy mode, returns the working copy info
-     * without acquiring an independent ownership of the working copy. Returns
-     * <code>null</code> if this source file is not a working copy.
+     * Returns a context to be associated with a new working copy of this
+     * source file. The given operation context is propagated from the
+     * {@link #hBecomeWorkingCopy} method.
+     * <p>
+     * This implementation returns an empty context. Subclasses may override.
+     * </p>
      *
-     * @return the working copy info for this source file,
-     *  or <code>null</code> if this source file is not a working copy
-     * @see #hAcquireWorkingCopy()
+     * @param context the operation context (never <code>null</code>)
+     * @return the working copy context (not <code>null</code>)
      */
-    protected final WorkingCopyInfo hPeekAtWorkingCopyInfo()
+    protected IContext hWorkingCopyContext(IContext context)
     {
-        return hElementManager().peekAtWorkingCopyInfo(this);
+        return EMPTY_CONTEXT;
     }
 
     /**
@@ -404,65 +460,51 @@
     }
 
     @Override
-    protected final void hBuildStructure(IContext context,
-        IProgressMonitor monitor) throws CoreException
+    void hBuildStructure0(IContext context, IProgressMonitor monitor)
+        throws CoreException
     {
-        int ticks = 2;
-        monitor.beginTask("", ticks); //$NON-NLS-1$
-        try
+        if (!context.containsKey(SOURCE_CONTENTS) && !context.containsKey(
+            SOURCE_AST))
         {
-            Object ast = context.get(SOURCE_AST);
-
-            if (ast == null) // not a working copy
+            if (hIsWorkingCopy())
+                throw new AssertionError();
+            // NOTE: source files that are not working copies must reflect
+            // the structure of the underlying file rather than the buffer
+            NonExpiringSnapshot snapshot;
+            try (ISnapshotProvider provider = hFileSnapshotProvider())
             {
-                // NOTE: AST is created from the underlying file contents,
-                // not from the buffer contents, since source files that are not
-                // working copies must reflect the structure of the underlying file
-                NonExpiringSnapshot snapshot;
-                try (
-                    ISnapshotProvider provider = hFileSnapshotProvider(context))
+                try
                 {
-                    try
-                    {
-                        snapshot = new NonExpiringSnapshot(provider);
-                    }
-                    catch (IllegalStateException e)
-                    {
-                        Throwable cause = e.getCause();
-                        if (cause instanceof CoreException)
-                            throw (CoreException)cause;
-                        throw new CoreException(Activator.createErrorStatus(
-                            e.getMessage(), e));
-                    }
+                    snapshot = new NonExpiringSnapshot(provider);
                 }
-                ast = hCreateAst(snapshot.getContents(), context,
-                    new SubProgressMonitor(monitor, 1));
-                context = with(of(SOURCE_CONTENTS, snapshot.getContents()), of(
-                    SOURCE_SNAPSHOT, snapshot.getWrappedSnapshot()), context);
-                --ticks;
+                catch (IllegalStateException e)
+                {
+                    Throwable cause = e.getCause();
+                    if (cause instanceof CoreException)
+                        throw (CoreException)cause;
+                    throw new CoreException(Activator.createErrorStatus(
+                        e.getMessage(), e));
+                }
             }
-
-            hBuildStructure(ast, context, new SubProgressMonitor(monitor,
-                ticks));
-
-            Map<IElement, Object> newElements = context.get(NEW_ELEMENTS);
-            Object body = newElements.get(this);
-            if (body instanceof SourceElementBody)
-            {
-                SourceElementBody thisBody = (SourceElementBody)body;
-
-                String source = context.get(SOURCE_CONTENTS);
-                if (source != null)
-                    thisBody.setFullRange(new TextRange(0, source.length()));
-
-                ISnapshot snapshot = context.get(SOURCE_SNAPSHOT);
-                if (snapshot != null)
-                    setSnapshot(thisBody, snapshot, newElements);
-            }
+            context = with(of(SOURCE_CONTENTS, snapshot.getContents()), of(
+                SOURCE_SNAPSHOT, snapshot.getWrappedSnapshot()), context);
         }
-        finally
+
+        super.hBuildStructure0(context, monitor);
+
+        Map<IElement, Object> newElements = context.get(NEW_ELEMENTS);
+        Object body = newElements.get(this);
+        if (body instanceof SourceElementBody)
         {
-            monitor.done();
+            SourceElementBody thisBody = (SourceElementBody)body;
+
+            String source = context.get(SOURCE_CONTENTS);
+            if (source != null)
+                thisBody.setFullRange(new TextRange(0, source.length()));
+
+            ISnapshot snapshot = context.get(SOURCE_SNAPSHOT);
+            if (snapshot != null)
+                setSnapshot(thisBody, snapshot, newElements);
         }
     }
 
@@ -483,13 +525,11 @@
      * be accessed by clients that don't own it.
      * </p>
      *
-     * @param context the operation context (never <code>null</code>)
      * @return a snapshot provider for the underlying file's stored contents
      *  (not <code>null</code>)
      * @see ISnapshotProvider
      */
-    protected abstract ISnapshotProvider hFileSnapshotProvider(
-        IContext context);
+    protected abstract ISnapshotProvider hFileSnapshotProvider();
 
     /**
      * Returns the buffer opened for the underlying file of this source file.
@@ -518,8 +558,7 @@
      * @return the buffer opened for the underlying file of this source file,
      *  or <code>null</code> if <code>CREATE_BUFFER == false</code> and
      *  there is currently no buffer opened for that file
-     * @throws CoreException if the underlying file does not exist
-     *  or its contents cannot be accessed
+     * @throws CoreException if the buffer could not be opened successfully
      * @throws OperationCanceledException if this method is canceled
      * @see IBuffer
      */
@@ -527,31 +566,28 @@
         IProgressMonitor monitor) throws CoreException;
 
     /**
-     * Returns a new AST created from the given source string. Unless otherwise
-     * indicated by options specified in the given context, the AST may contain
-     * just enough information for computing the structure and properties of
-     * this element and each of its descendant elements.
-     *
-     * @param source the source string to parse (not <code>null</code>)
-     * @param context the operation context (not <code>null</code>)
-     * @param monitor a progress monitor (not <code>null</code>)
-     * @return the AST created from the given source string (never <code>null</code>)
-     * @throws CoreException if the AST could not be created
-     * @throws OperationCanceledException if this method is canceled
-     */
-    protected abstract Object hCreateAst(String source, IContext context,
-        IProgressMonitor monitor) throws CoreException;
-
-    /**
      * Creates and initializes bodies for this element and for each
-     * of its descendant elements using information in the given AST.
-     * Uses the {@link #NEW_ELEMENTS} map in the given context to associate
-     * the created bodies with their respective elements.
+     * of its descendant elements according to options specified in the
+     * given context. Uses the {@link #NEW_ELEMENTS} map in the given context
+     * to associate the created bodies with their respective elements.
      * <p>
-     * The AST is safe to read in the dynamic context of this method call,
-     * but must not be modified. In general, implementations should not keep
-     * references to any part of the AST or the context outside the dynamic
-     * scope of the invocation of this method.
+     * The following context options influence how the structure is built and,
+     * if simultaneously present, must be mutually consistent:
+     * </p>
+     * <ul>
+     * <li>
+     * {@link #SOURCE_AST} - Specifies the AST to use when building the structure.
+     * The AST is safe to read in the dynamic context of this method call, but
+     * must not be modified.
+     * </li>
+     * <li>
+     * {@link #SOURCE_CONTENTS} - Specifies the source string to use when
+     * building the structure.
+     * </li>
+     * </ul>
+     * <p>
+     * At least one of <code>SOURCE_AST</code> or <code>SOURCE_CONTENTS</code>
+     * will have a non-null value in the given context.
      * </p>
      * <p>
      * The given context may provide additional data that this method can use,
@@ -559,34 +595,36 @@
      * </p>
      * <ul>
      * <li>
-     * {@link #SOURCE_CONTENTS} - Specifies the source string from which the
-     * given AST was created.
-     * </li>
-     * <li>
-     * {@link #SOURCE_SNAPSHOT} - Specifies the source snapshot from which the
-     * given AST was created. The snapshot may expire. Implementations may
-     * keep references to the snapshot outside the dynamic scope of the
-     * invocation of this method.
+     * {@link #SOURCE_SNAPSHOT} - Specifies the source snapshot from which
+     * <code>SOURCE_AST</code> was created or <code>SOURCE_CONTENTS</code>
+     * was obtained. The snapshot may expire.
      * </li>
      * </ul>
      *
-     * @param ast the AST (never <code>null</code>)
      * @param context the operation context (never <code>null</code>)
      * @param monitor a progress monitor (never <code>null</code>)
+     * @throws CoreException if this method fails
      * @throws OperationCanceledException if this method is canceled
      */
-    protected abstract void hBuildStructure(Object ast, IContext context,
-        IProgressMonitor monitor);
+    @Override
+    protected abstract void hBuildStructure(IContext context,
+        IProgressMonitor monitor) throws CoreException;
 
     /**
+     * Specifies the source AST.
+     * @see #hBuildStructure(IContext, IProgressMonitor)
+     */
+    protected static final Property<Object> SOURCE_AST = Property.get(
+        SourceFile.class.getName() + ".sourceAst", Object.class); //$NON-NLS-1$
+    /**
      * Specifies the source string.
-     * @see #hBuildStructure(Object, IContext, IProgressMonitor)
+     * @see #hBuildStructure(IContext, IProgressMonitor)
      */
     protected static final Property<String> SOURCE_CONTENTS = Property.get(
         SourceFile.class.getName() + ".sourceContents", String.class); //$NON-NLS-1$
     /**
      * Specifies the source snapshot.
-     * @see #hBuildStructure(Object, IContext, IProgressMonitor)
+     * @see #hBuildStructure(IContext, IProgressMonitor)
      */
     protected static final Property<ISnapshot> SOURCE_SNAPSHOT = Property.get(
         SourceFile.class.getName() + ".sourceSnapshot", ISnapshot.class); //$NON-NLS-1$
@@ -631,22 +669,10 @@
         }
     }
 
-    private WorkingCopyInfo putWorkingCopyInfoIfAbsent(IBuffer buffer,
-        IWorkingCopyInfoFactory factory)
-    {
-        return hElementManager().putWorkingCopyInfoIfAbsent(this, buffer,
-            factory);
-    }
-
-    private WorkingCopyInfo getWorkingCopyInfo()
-    {
-        return hElementManager().getWorkingCopyInfo(this);
-    }
-
     /**
      * Indicates whether the structure should be rebuilt when reconciling
      * is forced.
-     * @see ReconcileOperation#reconcile(Object, IContext, IProgressMonitor)
+     * @see ReconcileOperation#reconcile(IContext, IProgressMonitor)
      */
     protected static final Property<Boolean> REBUILD_STRUCTURE_IF_FORCED =
         Property.get(SourceFile.class.getName() + ".rebuildStructureIfForced", //$NON-NLS-1$
@@ -654,16 +680,12 @@
     /**
      * Indicates whether reconciling was forced, i.e. the working copy buffer
      * has not been modified since the last time it was reconciled.
-     * @see ReconcileOperation#reconcile(Object, IContext, IProgressMonitor)
+     * @see ReconcileOperation#reconcile(IContext, IProgressMonitor)
      */
     static final Property<Boolean> RECONCILING_FORCED = Property.get(
         SourceFile.class.getName() + ".reconcilingForced", //$NON-NLS-1$
         Boolean.class).withDefault(false);
 
-    private static final Property<Object> SOURCE_AST = Property.get(
-        SourceFile.class.getName() + ".sourceAst", //$NON-NLS-1$
-        Object.class); // the AST to use when building the source file structure
-
     private static final ThreadLocal<SourceFile> CURRENTLY_RECONCILED =
         new ThreadLocal<SourceFile>(); // the source file being reconciled
 
@@ -683,40 +705,48 @@
     protected class ReconcileOperation
     {
         /**
-         * Reconciles this working copy according to the given AST and
-         * additional data provided in the context.
-         * <p>
-         * The AST is safe to read in the dynamic context of this method call,
-         * but must not be modified. In general, implementations should not keep
-         * references to any part of the AST or the context outside the dynamic
-         * scope of the invocation of this method.
-         * </p>
+         * Reconciles this working copy according to options specified
+         * in the given context.
          * <p>
          * The following context options can influence whether the structure
          * of the working copy gets rebuilt:
          * </p>
          * <ul>
          * <li>
-         * {@link SourceFile#REBUILD_STRUCTURE_IF_FORCED REBUILD_STRUCTURE_IF_FORCED} -
-         * Indicates whether the structure should be rebuilt even if reconciling
-         * was forced, i.e. the working copy buffer has not been modified since
-         * the last time it was reconciled.
+         * {@link #REBUILD_STRUCTURE_IF_FORCED} - Indicates whether the structure
+         * should be rebuilt even if reconciling was forced, i.e. the working copy
+         * buffer has not been modified since the last time it was reconciled.
          * </li>
          * </ul>
          * <p>
+         * The following context options influence rebuilding of the structure
+         * of the working copy and, if simultaneously present, must be mutually
+         * consistent:
+         * </p>
+         * <ul>
+         * <li>
+         * {@link #SOURCE_AST} - Specifies the AST to use when building the
+         * structure. The AST is safe to read in the dynamic context of this
+         * method call, but must not be modified.
+         * </li>
+         * <li>
+         * {@link #SOURCE_CONTENTS} - Specifies the source string to use when
+         * building the structure.
+         * </li>
+         * </ul>
+         * <p>
+         * At least one of <code>SOURCE_AST</code> or <code>SOURCE_CONTENTS</code>
+         * must have a non-null value in the given context.
+         * </p>
+         * <p>
          * The given context may provide additional data that this method can use,
          * including the following:
          * </p>
          * <ul>
          * <li>
-         * {@link #SOURCE_CONTENTS} - Specifies the source string from which the
-         * given AST was created.
-         * </li>
-         * <li>
          * {@link #SOURCE_SNAPSHOT} - Specifies the source snapshot from which
-         * the given AST was created. The snapshot may expire. Implementations
-         * may keep references to the snapshot outside the dynamic scope of the
-         * invocation of this method.
+         * <code>SOURCE_AST</code> was created or <code>SOURCE_CONTENTS</code>
+         * was obtained. The snapshot may expire.
          * </li>
          * </ul>
          * <p>
@@ -724,21 +754,22 @@
          * implementation.
          * </p>
          *
-         * @param ast the working copy AST (not <code>null</code>)
          * @param context the operation context (not <code>null</code>)
          * @param monitor a progress monitor, or <code>null</code>
          *  if progress reporting is not desired
          * @throws CoreException if the working copy cannot be reconciled
          * @throws OperationCanceledException if this method is canceled
          */
-        protected void reconcile(Object ast, IContext context,
-            IProgressMonitor monitor) throws CoreException
+        protected void reconcile(IContext context, IProgressMonitor monitor)
+            throws CoreException
         {
-            if (ast == null)
+            if (context.get(SOURCE_AST) == null && context.get(
+                SOURCE_CONTENTS) == null)
+            {
                 throw new IllegalArgumentException();
-            if (context == null)
-                throw new IllegalArgumentException();
-            WorkingCopyInfo info = hPeekAtWorkingCopyInfo();
+            }
+            WorkingCopyInfo info = hElementManager().peekAtWorkingCopyInfo(
+                SourceFile.this);
             boolean create = !info.created; // case of wc creation
             if (create || !context.getOrDefault(RECONCILING_FORCED)
                 || context.getOrDefault(REBUILD_STRUCTURE_IF_FORCED))
@@ -748,8 +779,7 @@
                 CURRENTLY_RECONCILED.set(SourceFile.this);
                 try
                 {
-                    hOpen(with(of(SOURCE_AST, ast), of(FORCE_OPEN, true),
-                        context), monitor);
+                    hOpen(with(of(FORCE_OPEN, true), context), monitor);
                 }
                 finally
                 {
@@ -777,8 +807,8 @@
         extends ReconcileOperation
     {
         @Override
-        protected void reconcile(Object ast, IContext context,
-            IProgressMonitor monitor) throws CoreException
+        protected void reconcile(IContext context, IProgressMonitor monitor)
+            throws CoreException
         {
             ElementDelta.Factory deltaFactory = hModel().getModelContext().get(
                 ElementDelta.Factory.class);
@@ -788,7 +818,7 @@
             ElementDifferencer differ = new ElementDifferencer(
                 new ElementDelta.Builder(rootDelta));
 
-            doReconcile(ast, context, monitor);
+            doReconcile(context, monitor);
 
             differ.buildDelta();
             if (!differ.isEmptyDelta())
@@ -802,24 +832,23 @@
         }
 
         /**
-         * This implementation calls {@link ReconcileOperation#reconcile(Object,
+         * This implementation calls {@link ReconcileOperation#reconcile(
          * IContext, IProgressMonitor) super.reconcile(..)}.
          * <p>
          * Subclasses may override this method, but must call its <b>super</b>
          * implementation.
          * </p>
          *
-         * @param ast the working copy AST (not <code>null</code>)
          * @param context the operation context (not <code>null</code>)
          * @param monitor a progress monitor, or <code>null</code>
          *  if progress reporting is not desired
          * @throws CoreException if the working copy cannot be reconciled
          * @throws OperationCanceledException if this method is canceled
          */
-        protected void doReconcile(Object ast, IContext context,
-            IProgressMonitor monitor) throws CoreException
+        protected void doReconcile(IContext context, IProgressMonitor monitor)
+            throws CoreException
         {
-            super.reconcile(ast, context, monitor);
+            super.reconcile(context, monitor);
         }
     }
 
@@ -842,7 +871,7 @@
                 finally
                 {
                     if (!success)
-                        hDiscardWorkingCopy();
+                        hReleaseWorkingCopy();
                 }
                 if (success)
                     return info;
diff --git a/org.eclipse.handly/src/org/eclipse/handly/model/impl/WorkingCopyInfo.java b/org.eclipse.handly/src/org/eclipse/handly/model/impl/WorkingCopyInfo.java
index ea0678f..3ec2b1b 100644
--- a/org.eclipse.handly/src/org/eclipse/handly/model/impl/WorkingCopyInfo.java
+++ b/org.eclipse.handly/src/org/eclipse/handly/model/impl/WorkingCopyInfo.java
@@ -10,6 +10,7 @@
  *******************************************************************************/
 package org.eclipse.handly.model.impl;
 
+import static org.eclipse.handly.context.Contexts.EMPTY_CONTEXT;
 import static org.eclipse.handly.context.Contexts.of;
 import static org.eclipse.handly.model.Elements.FORCE_RECONCILING;
 
@@ -29,37 +30,58 @@
 import org.eclipse.handly.util.Property;
 
 /**
- * Holds information related to a working copy.
+ * Holds information related to a working copy of a {@link SourceFile}.
  * <p>
- * Concrete implementations of this abstract class are expected to be safe
+ * Concrete subclasses of this abstract class are expected to be safe
  * for use by multiple threads.
  * </p>
+ *
+ * @noextend This class is not intended to be directly extended by clients.
+ *  However, clients may extend concrete subclasses of this class.
  */
 public abstract class WorkingCopyInfo
 {
+    private final SourceFile sourceFile;
     private final IBuffer buffer;
+    private IContext context = EMPTY_CONTEXT;
     final InitTask initTask = new InitTask();
-    SourceFile workingCopy;
     volatile boolean created; // whether wc was created (from the model POV)
     int refCount;
 
     /**
      * Constructs a new working copy info and associates it with the given
-     * buffer; the buffer is NOT <code>addRef</code>'ed.
+     * source file and buffer. Does not <code>addRef</code> the given buffer.
+     * <p>
+     * Clients should explicitly {@link #dispose} the working copy info
+     * after it is no longer needed.
+     * </p>
      *
-     * @param buffer the working copy buffer (not <code>null</code>)
+     * @param sourceFile the working copy's source file (not <code>null</code>)
+     * @param buffer the working copy's buffer (not <code>null</code>)
      */
-    public WorkingCopyInfo(IBuffer buffer)
+    public WorkingCopyInfo(SourceFile sourceFile, IBuffer buffer)
     {
+        if ((this.sourceFile = sourceFile) == null)
+            throw new IllegalArgumentException();
         if ((this.buffer = buffer) == null)
             throw new IllegalArgumentException();
     }
 
     /**
-     * Returns the buffer associated with this working copy info;
-     * does NOT <code>addRef</code> the buffer.
+     * Returns the source file associated with this working copy info.
      *
-     * @return the working copy buffer (never <code>null</code>)
+     * @return the working copy's source file (never <code>null</code>)
+     */
+    public final SourceFile getSourceFile()
+    {
+        return sourceFile;
+    }
+
+    /**
+     * Returns the buffer associated with this working copy info.
+     * Does not <code>addRef</code> the buffer.
+     *
+     * @return the working copy's buffer (never <code>null</code>)
      */
     public final IBuffer getBuffer()
     {
@@ -67,6 +89,16 @@
     }
 
     /**
+     * Returns the context associated with this working copy info.
+     *
+     * @return the working copy's context (never <code>null</code>)
+     */
+    public IContext getContext()
+    {
+        return context;
+    }
+
+    /**
      * Disposes of this working copy info. Does nothing if the working copy info
      * is already disposed.
      *
@@ -123,6 +155,7 @@
      *
      * @return <code>true</code> if the working copy needs reconciling,
      *  <code>false</code> otherwise
+     * @noreference This method is for internal use only.
      */
     protected abstract boolean needsReconciling();
 
@@ -141,9 +174,9 @@
      * </li>
      * </ul>
      * <p>
-     * An implementation of this method is supposed to call {@link
-     * #basicReconcile} with an appropriately augmented context while
-     * providing the necessary synchronization guarantees.
+     * An implementation of this method is supposed to call {@link #reconcile0}
+     * with an appropriately augmented context while providing the necessary
+     * synchronization guarantees.
      * </p>
      *
      * @param context the operation context (not <code>null</code>)
@@ -151,6 +184,7 @@
      *  if progress reporting is not desired
      * @throws CoreException if the working copy cannot be reconciled
      * @throws OperationCanceledException if this method is canceled
+     * @noreference This method is for internal use only.
      */
     protected abstract void reconcile(IContext context,
         IProgressMonitor monitor) throws CoreException;
@@ -212,39 +246,39 @@
      *  if progress reporting is not desired
      * @throws CoreException if the working copy cannot be reconciled
      * @throws OperationCanceledException if this method is canceled
+     * @noreference This method is for internal use only.
      */
-    protected final void basicReconcile(IContext context,
-        IProgressMonitor monitor) throws CoreException
+    protected final void reconcile0(IContext context, IProgressMonitor monitor)
+        throws CoreException
     {
-        Object ast = context.get(SOURCE_AST);
-        if (ast == null)
-            ast = workingCopy.hCreateAst(context.get(SOURCE_CONTENTS), context,
-                monitor);
-        workingCopy.hReconcileOperation().reconcile(ast, context, monitor);
+        sourceFile.hReconcileOperation().reconcile(context, monitor);
     }
 
     /**
      * Specifies the source AST for reconciling.
-     * @see #basicReconcile(IContext, IProgressMonitor)
+     * @see #reconcile0(IContext, IProgressMonitor)
+     * @noreference This property is for internal use only.
      */
-    protected static final Property<Object> SOURCE_AST = Property.get(
-        WorkingCopyInfo.class.getName() + ".sourceAst", Object.class); //$NON-NLS-1$
+    protected static final Property<Object> SOURCE_AST = SourceFile.SOURCE_AST;
     /**
      * Specifies the source string for reconciling.
-     * @see #basicReconcile(IContext, IProgressMonitor)
+     * @see #reconcile0(IContext, IProgressMonitor)
+     * @noreference This property is for internal use only.
      */
     protected static final Property<String> SOURCE_CONTENTS =
         SourceFile.SOURCE_CONTENTS;
     /**
      * Specifies the source snapshot for reconciling.
-     * @see #basicReconcile(IContext, IProgressMonitor)
+     * @see #reconcile0(IContext, IProgressMonitor)
+     * @noreference This property is for internal use only.
      */
     protected static final Property<ISnapshot> SOURCE_SNAPSHOT =
         SourceFile.SOURCE_SNAPSHOT;
     /**
      * Indicates whether reconciling was forced, i.e. the working copy buffer
      * has not been modified since the last time it was reconciled.
-     * @see #basicReconcile(IContext, IProgressMonitor)
+     * @see #reconcile0(IContext, IProgressMonitor)
+     * @noreference This property is for internal use only.
      */
     protected static final Property<Boolean> RECONCILING_FORCED =
         SourceFile.RECONCILING_FORCED;
@@ -253,7 +287,7 @@
      * Clients should not be exposed to working copy info if it has not been
      * initialized.
      *
-     * @noreference For internal use only.
+     * @noreference This method is for internal use only.
      */
     public final boolean isInitialized()
     {
@@ -271,8 +305,33 @@
         }
     }
 
+    /**
+     * A factory of working copy info.
+     */
+    public interface Factory
+    {
+        /**
+         * Returns a new working copy info associated with the given source file
+         * and buffer. This method is not expected to <code>addRef</code> the
+         * given buffer.
+         * <p>
+         * Clients should explicitly {@link WorkingCopyInfo#dispose() dispose}
+         * the working copy info after it is no longer needed.
+         * </p>
+         *
+         * @param sourceFile the source file to be associated with the
+         *  created working copy info (not <code>null</code>)
+         * @param buffer the buffer to be associated with the
+         *  created working copy info (not <code>null</code>)
+         * @return a new working copy info (never <code>null</code>)
+         */
+        WorkingCopyInfo newWorkingCopyInfo(SourceFile sourceFile,
+            IBuffer buffer);
+    }
+
     class InitTask
     {
+        private IContext creationContext;
         private IProgressMonitor monitor;
         private final FutureTask<?> futureTask = new FutureTask<Object>(
             new Callable<Object>()
@@ -285,13 +344,16 @@
                 }
             });
 
-        void execute(IProgressMonitor monitor) throws CoreException
+        void execute(IContext context, IProgressMonitor monitor)
+            throws CoreException
         {
+            this.creationContext = context;
             if (monitor == null)
                 monitor = new NullProgressMonitor();
             this.monitor = monitor;
             futureTask.run();
             this.monitor = null;
+            this.creationContext = null;
             try
             {
                 futureTask.get();
@@ -329,6 +391,7 @@
 
         private void run() throws CoreException
         {
+            context = sourceFile.hWorkingCopyContext(creationContext);
             onInit();
             reconcile(of(FORCE_RECONCILING, true), monitor);
             if (!created)
diff --git a/org.eclipse.handly/src/org/eclipse/handly/model/impl/WorkspaceSourceFile.java b/org.eclipse.handly/src/org/eclipse/handly/model/impl/WorkspaceSourceFile.java
index 48e8ecb..c509945 100644
--- a/org.eclipse.handly/src/org/eclipse/handly/model/impl/WorkspaceSourceFile.java
+++ b/org.eclipse.handly/src/org/eclipse/handly/model/impl/WorkspaceSourceFile.java
@@ -102,7 +102,7 @@
     }
 
     @Override
-    protected final ISnapshotProvider hFileSnapshotProvider(IContext context)
+    protected final ISnapshotProvider hFileSnapshotProvider()
     {
         return () ->
         {