Bug 533821 - Revise text file snapshot implementation
diff --git a/org.eclipse.handly.tests/src/org/eclipse/handly/snapshot/TextFileSnapshotTest.java b/org.eclipse.handly.tests/src/org/eclipse/handly/snapshot/TextFileSnapshotTest.java
new file mode 100644
index 0000000..4984836
--- /dev/null
+++ b/org.eclipse.handly.tests/src/org/eclipse/handly/snapshot/TextFileSnapshotTest.java
@@ -0,0 +1,213 @@
+/*******************************************************************************
+ * Copyright (c) 2018 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.snapshot;
+
+import java.io.ByteArrayInputStream;
+import java.util.function.Supplier;
+
+import org.eclipse.core.filesystem.EFS;
+import org.eclipse.core.filesystem.IFileStore;
+import org.eclipse.core.resources.IFile;
+import org.eclipse.core.resources.IProject;
+import org.eclipse.core.runtime.CoreException;
+import org.eclipse.handly.junit.WorkspaceTestCase;
+
+/**
+ * <code>TextFileSnapshot</code> and <code>TextFileStoreSnapshot</code> tests.
+ */
+public class TextFileSnapshotTest
+    extends WorkspaceTestCase
+{
+    private IFile fileX;
+    private IFile fileY;
+    private IFile fileZ;
+
+    @Override
+    protected void setUp() throws Exception
+    {
+        super.setUp();
+        IProject p = setUpProject("Test002");
+        fileX = p.getFile("x.txt");
+        fileY = p.getFile("y.txt");
+        fileZ = p.getFile("z.txt");
+    }
+
+    public void test01() throws Exception
+    {
+        _testA(snapshotSupplier1(fileX), snapshotSupplier1(fileX));
+        _testA(snapshotSupplier1(fileX), snapshotSupplier2(fileX));
+        _testA(snapshotSupplier1(fileX), snapshotSupplier3(fileX));
+    }
+
+    public void test02() throws Exception
+    {
+        _testA(snapshotSupplier1(fileX), snapshotSupplier1(fileY));
+        _testA(snapshotSupplier1(fileX), snapshotSupplier2(fileY));
+        _testA(snapshotSupplier1(fileX), snapshotSupplier3(fileY));
+    }
+
+    public void test03() throws Exception
+    {
+        _testB(snapshotSupplier1(fileX), snapshotSupplier1(fileZ));
+        _testB(snapshotSupplier1(fileX), snapshotSupplier2(fileZ));
+        _testB(snapshotSupplier1(fileX), snapshotSupplier3(fileZ));
+    }
+
+    public void test04() throws Exception
+    {
+        _testC(snapshotSupplier1(fileX));
+    }
+
+    public void test05() throws Exception
+    {
+        _testD(snapshotSupplier1(fileX));
+    }
+
+    public void test06() throws Exception
+    {
+        _testA(snapshotSupplier2(fileX), snapshotSupplier1(fileX));
+        _testA(snapshotSupplier2(fileX), snapshotSupplier2(fileX));
+        _testA(snapshotSupplier2(fileX), snapshotSupplier3(fileX));
+    }
+
+    public void test07() throws Exception
+    {
+        _testA(snapshotSupplier2(fileX), snapshotSupplier1(fileY));
+        _testA(snapshotSupplier2(fileX), snapshotSupplier2(fileY));
+        _testA(snapshotSupplier2(fileX), snapshotSupplier3(fileY));
+    }
+
+    public void test08() throws Exception
+    {
+        _testB(snapshotSupplier2(fileX), snapshotSupplier1(fileZ));
+        _testB(snapshotSupplier2(fileX), snapshotSupplier2(fileZ));
+        _testB(snapshotSupplier2(fileX), snapshotSupplier3(fileZ));
+    }
+
+    public void test09() throws Exception
+    {
+        _testC(snapshotSupplier2(fileX));
+    }
+
+    public void test10() throws Exception
+    {
+        _testD(snapshotSupplier2(fileX));
+    }
+
+    public void test11() throws Exception
+    {
+        _testA(snapshotSupplier3(fileX), snapshotSupplier1(fileX));
+        _testA(snapshotSupplier3(fileX), snapshotSupplier2(fileX));
+        _testA(snapshotSupplier3(fileX), snapshotSupplier3(fileX));
+    }
+
+    public void test12() throws Exception
+    {
+        _testA(snapshotSupplier3(fileX), snapshotSupplier1(fileY));
+        _testA(snapshotSupplier3(fileX), snapshotSupplier2(fileY));
+        _testA(snapshotSupplier3(fileX), snapshotSupplier3(fileY));
+    }
+
+    public void test13() throws Exception
+    {
+        _testB(snapshotSupplier3(fileX), snapshotSupplier1(fileZ));
+        _testB(snapshotSupplier3(fileX), snapshotSupplier2(fileZ));
+        _testB(snapshotSupplier3(fileX), snapshotSupplier3(fileZ));
+    }
+
+    public void test14() throws Exception
+    {
+        _testC(snapshotSupplier3(fileX));
+    }
+
+    public void test15() throws Exception
+    {
+        _testD(snapshotSupplier3(fileX));
+    }
+
+    private Supplier<TextFileSnapshotBase> snapshotSupplier1(IFile file)
+    {
+        return () -> new TextFileSnapshot(file,
+            TextFileSnapshot.Layer.WORKSPACE);
+    }
+
+    private Supplier<TextFileSnapshotBase> snapshotSupplier2(IFile file)
+    {
+        return () -> new TextFileSnapshot(file,
+            TextFileSnapshot.Layer.FILESYSTEM);
+    }
+
+    private Supplier<TextFileSnapshotBase> snapshotSupplier3(IFile file)
+    {
+        return () ->
+        {
+            IFileStore fileStore;
+            try
+            {
+                fileStore = EFS.getStore(file.getLocationURI());
+            }
+            catch (CoreException e)
+            {
+                throw new RuntimeException(e);
+            }
+            return new TextFileStoreSnapshot(fileStore);
+        };
+    }
+
+    private void _testA(Supplier<TextFileSnapshotBase> supplier,
+        Supplier<TextFileSnapshotBase> otherSupplier)
+    {
+        TextFileSnapshotBase snapshot1 = supplier.get();
+        TextFileSnapshotBase snapshot2 = otherSupplier.get();
+        assertNotSame(snapshot1, snapshot2);
+        assertTrue(snapshot1.isEqualTo(snapshot2));
+    }
+
+    private void _testB(Supplier<TextFileSnapshotBase> supplier,
+        Supplier<TextFileSnapshotBase> otherSupplier)
+    {
+        TextFileSnapshotBase snapshot1 = supplier.get();
+        TextFileSnapshotBase snapshot2 = otherSupplier.get();
+        assertFalse(snapshot1.isEqualTo(snapshot2));
+    }
+
+    private void _testC(Supplier<TextFileSnapshotBase> snapshotSupplier)
+        throws Exception
+    {
+        TextFileSnapshotBase snapshot = snapshotSupplier.get();
+        assertTrue(snapshot.exists());
+        assertTrue(snapshot.getStatus().isOK());
+        assertEquals("hello", snapshot.getContents());
+        fileX.appendContents(new ByteArrayInputStream(", world!".getBytes()),
+            true, false, null);
+        TextFileSnapshotBase snapshot2 = snapshotSupplier.get();
+        assertTrue(snapshot2.exists());
+        assertTrue(snapshot2.getStatus().isOK());
+        assertEquals("hello, world!", snapshot2.getContents());
+        assertFalse(snapshot.isEqualTo(snapshot2));
+    }
+
+    private void _testD(Supplier<TextFileSnapshotBase> snapshotSupplier)
+        throws Exception
+    {
+        TextFileSnapshotBase snapshot = snapshotSupplier.get();
+        assertTrue(snapshot.exists());
+        assertTrue(snapshot.getStatus().isOK());
+        assertEquals("hello", snapshot.getContents());
+        fileX.delete(true, null);
+        assertTrue(snapshot.exists());
+        TextFileSnapshotBase snapshot2 = snapshotSupplier.get();
+        assertFalse(snapshot2.exists());
+        assertTrue(snapshot2.getStatus().isOK());
+        assertEquals("", snapshot2.getContents());
+        assertFalse(snapshot.isEqualTo(snapshot2));
+    }
+}
diff --git a/org.eclipse.handly.tests/src/org/eclipse/handly/snapshot/TextFileSnapshotWsTest.java b/org.eclipse.handly.tests/src/org/eclipse/handly/snapshot/TextFileSnapshotWsTest.java
new file mode 100644
index 0000000..c5808e9
--- /dev/null
+++ b/org.eclipse.handly.tests/src/org/eclipse/handly/snapshot/TextFileSnapshotWsTest.java
@@ -0,0 +1,71 @@
+/*******************************************************************************
+ * Copyright (c) 2018 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.snapshot;
+
+import org.eclipse.core.resources.IFile;
+import org.eclipse.core.resources.IProject;
+import org.eclipse.handly.junit.WorkspaceTestCase;
+
+/**
+ * <code>TextFileSnapshotWs</code> tests.
+ */
+public class TextFileSnapshotWsTest
+    extends WorkspaceTestCase
+{
+    private IFile file;
+    private TextFileSnapshotWs snapshot;
+
+    @Override
+    protected void setUp() throws Exception
+    {
+        super.setUp();
+        IProject p = setUpProject("Test002");
+        file = p.getFile("x.txt");
+        snapshot = new TextFileSnapshotWs(file);
+    }
+
+    public void test1() throws Exception
+    {
+        assertEquals("hello", snapshot.getContents());
+        assertEquals("hello", snapshot.getContents()); // another code path
+        file.touch(null);
+        assertNull(snapshot.getContents());
+    }
+
+    public void test2()
+    {
+        assertEquals("hello", snapshot.getContents());
+        assertTrue(file.getLocation().toFile().delete());
+        assertEquals("hello", snapshot.getContents());
+    }
+
+    public void test3()
+    {
+        assertTrue(file.getLocation().toFile().delete());
+        assertNull(snapshot.getContents());
+        assertNull(snapshot.getContents()); // another code path
+    }
+
+    public void test4()
+    {
+        assertEquals("hello", snapshot.getContents());
+        snapshot.clearContents();
+        assertEquals("hello", snapshot.getContents());
+    }
+
+    public void test5()
+    {
+        assertEquals("hello", snapshot.getContents());
+        assertTrue(file.getLocation().toFile().delete());
+        snapshot.clearContents();
+        assertNull(snapshot.getContents());
+    }
+}
diff --git a/org.eclipse.handly.tests/src/org/eclipse/handly/snapshot/TextFileStoreSnapshotTest.java b/org.eclipse.handly.tests/src/org/eclipse/handly/snapshot/TextFileStoreSnapshotTest.java
new file mode 100644
index 0000000..538d84a
--- /dev/null
+++ b/org.eclipse.handly.tests/src/org/eclipse/handly/snapshot/TextFileStoreSnapshotTest.java
@@ -0,0 +1,50 @@
+/*******************************************************************************
+ * Copyright (c) 2018 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.snapshot;
+
+import java.nio.charset.Charset;
+
+import org.eclipse.core.filesystem.EFS;
+import org.eclipse.core.filesystem.IFileInfo;
+import org.eclipse.core.filesystem.IFileStore;
+import org.eclipse.core.resources.IFile;
+import org.eclipse.core.resources.IProject;
+import org.eclipse.handly.junit.WorkspaceTestCase;
+
+/**
+ * <code>TextFileStoreSnapshot</code> tests.
+ */
+public class TextFileStoreSnapshotTest
+    extends WorkspaceTestCase
+{
+    private IFileStore fileStore;
+    private TextFileStoreSnapshot snapshot;
+
+    @Override
+    protected void setUp() throws Exception
+    {
+        super.setUp();
+        IProject p = setUpProject("Test002");
+        IFile file = p.getFile("x.txt");
+        fileStore = EFS.getStore(file.getLocationURI());
+        snapshot = new TextFileStoreSnapshot(fileStore, Charset.forName(
+            "UTF-8"));
+    }
+
+    public void test1() throws Exception
+    {
+        assertEquals("hello", snapshot.getContents());
+        IFileInfo info = fileStore.fetchInfo();
+        info.setLastModified(info.getLastModified() + 1000);
+        fileStore.putInfo(info, EFS.SET_LAST_MODIFIED, null);
+        assertNull(snapshot.getContents());
+    }
+}
diff --git a/org.eclipse.handly.tests/workspace/Test002/.project b/org.eclipse.handly.tests/workspace/Test002/.project
new file mode 100644
index 0000000..47360f5
--- /dev/null
+++ b/org.eclipse.handly.tests/workspace/Test002/.project
@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<projectDescription>
+	<name>Test002</name>
+	<comment></comment>
+	<projects>
+	</projects>
+	<buildSpec>
+	</buildSpec>
+	<natures>
+	</natures>
+</projectDescription>
diff --git a/org.eclipse.handly.tests/workspace/Test002/.settings/org.eclipse.core.resources.prefs b/org.eclipse.handly.tests/workspace/Test002/.settings/org.eclipse.core.resources.prefs
new file mode 100755
index 0000000..8a7cd42
--- /dev/null
+++ b/org.eclipse.handly.tests/workspace/Test002/.settings/org.eclipse.core.resources.prefs
@@ -0,0 +1,3 @@
+eclipse.preferences.version=1
+encoding/<project>=UTF-8
+encoding//x.txt=UTF-8
diff --git a/org.eclipse.handly.tests/workspace/Test002/x.txt b/org.eclipse.handly.tests/workspace/Test002/x.txt
new file mode 100644
index 0000000..b6fc4c6
--- /dev/null
+++ b/org.eclipse.handly.tests/workspace/Test002/x.txt
@@ -0,0 +1 @@
+hello
\ No newline at end of file
diff --git a/org.eclipse.handly.tests/workspace/Test002/y.txt b/org.eclipse.handly.tests/workspace/Test002/y.txt
new file mode 100644
index 0000000..b6fc4c6
--- /dev/null
+++ b/org.eclipse.handly.tests/workspace/Test002/y.txt
@@ -0,0 +1 @@
+hello
\ No newline at end of file
diff --git a/org.eclipse.handly.tests/workspace/Test002/z.txt b/org.eclipse.handly.tests/workspace/Test002/z.txt
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/org.eclipse.handly.tests/workspace/Test002/z.txt
diff --git a/org.eclipse.handly/src/org/eclipse/handly/snapshot/TextFileSnapshot.java b/org.eclipse.handly/src/org/eclipse/handly/snapshot/TextFileSnapshot.java
index 9b55adf..1a66674 100644
--- a/org.eclipse.handly/src/org/eclipse/handly/snapshot/TextFileSnapshot.java
+++ b/org.eclipse.handly/src/org/eclipse/handly/snapshot/TextFileSnapshot.java
@@ -1,5 +1,5 @@
 /*******************************************************************************
- * Copyright (c) 2014, 2016 1C-Soft LLC and others.
+ * Copyright (c) 2014, 2018 1C-Soft LLC and others.
  * All rights reserved. This program and the accompanying materials
  * are made available under the terms of the Eclipse Public License v1.0
  * which accompanies this distribution, and is available at
@@ -10,16 +10,13 @@
  *******************************************************************************/
 package org.eclipse.handly.snapshot;
 
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.InputStreamReader;
 import java.net.URI;
 
 import org.eclipse.core.filesystem.EFS;
 import org.eclipse.core.filesystem.IFileStore;
 import org.eclipse.core.resources.IFile;
-import org.eclipse.core.resources.IResource;
 import org.eclipse.core.runtime.CoreException;
+import org.eclipse.core.runtime.IStatus;
 import org.eclipse.handly.internal.Activator;
 
 /**
@@ -28,10 +25,7 @@
 public final class TextFileSnapshot
     extends TextFileSnapshotBase
 {
-    private final IFile file;
-    private final boolean fromFs;
-    private final long modificationStamp;
-    private String charset;
+    private final TextFileSnapshotBase delegate;
 
     /**
      * Takes a snapshot of the given text file in the workspace.
@@ -52,93 +46,43 @@
     {
         if (file == null)
             throw new IllegalArgumentException();
-        this.file = file;
-        this.fromFs = layer.equals(Layer.FILESYSTEM);
-        this.modificationStamp = getFileModificationStamp(file, fromFs);
+        this.delegate = createDelegate(file, layer);
+    }
+
+    @Override
+    public String getContents()
+    {
+        return delegate.getContents();
+    }
+
+    @Override
+    public IStatus getStatus()
+    {
+        return delegate.getStatus();
     }
 
     @Override
     public boolean exists()
     {
-        if (fromFs)
-            return modificationStamp != EFS.NONE;
-        else
-            return modificationStamp != IResource.NULL_STAMP;
+        return delegate.exists();
     }
 
     @Override
     protected Boolean predictEquality(Snapshot other)
     {
         if (other instanceof TextFileSnapshot)
-        {
-            TextFileSnapshot otherSnapshot = (TextFileSnapshot)other;
-            if (file.equals(otherSnapshot.file)
-                && fromFs == otherSnapshot.fromFs
-                && modificationStamp == otherSnapshot.modificationStamp)
-                return true;
-        }
-
-        if (!isSynchronized())
-            return false; // expired
+            return delegate.predictEquality(((TextFileSnapshot)other).delegate);
 
         return null;
     }
 
-    @Override
-    boolean isSynchronized()
+    private static TextFileSnapshotBase createDelegate(IFile file, Layer layer)
     {
-        return modificationStamp == getFileModificationStamp(file, fromFs)
-            && getStatus().isOK();
-    }
-
-    @Override
-    void cacheCharset() throws CoreException
-    {
-        if (charset != null)
-            return;
-
-        charset = file.getCharset(false);
-        if (charset == null)
-        {
-            try (InputStream contents = file.getContents(fromFs))
-            {
-                charset = getCharset(contents, file.getName());
-            }
-            catch (IOException e)
-            {
-                throw new CoreException(Activator.createErrorStatus(
-                    e.getMessage(), e));
-            }
-        }
-        if (charset == null)
-            charset = file.getParent().getDefaultCharset();
-    }
-
-    @Override
-    String readContents() throws CoreException
-    {
-        try (
-            InputStream stream = file.getContents(fromFs);
-            InputStreamReader reader = new InputStreamReader(stream, charset))
-        {
-            return String.valueOf(getInputStreamAsCharArray(stream, reader));
-        }
-        catch (IOException e)
-        {
-            throw new CoreException(Activator.createErrorStatus(e.getMessage(),
-                e));
-        }
-    }
-
-    private static long getFileModificationStamp(IFile file, boolean fromFs)
-    {
-        if (!fromFs)
-            return file.getModificationStamp();
-        else
+        if (layer == Layer.FILESYSTEM)
         {
             URI uri = file.getLocationURI();
             if (uri == null)
-                return EFS.NONE;
+                return NON_EXISTING;
             IFileStore fileStore;
             try
             {
@@ -147,10 +91,11 @@
             catch (CoreException e)
             {
                 Activator.log(e.getStatus());
-                return EFS.NONE;
+                return NON_EXISTING;
             }
-            return fileStore.fetchInfo().getLastModified();
+            return new TextFileStoreSnapshot(fileStore);
         }
+        return new TextFileSnapshotWs(file);
     }
 
     /**
diff --git a/org.eclipse.handly/src/org/eclipse/handly/snapshot/TextFileSnapshotBase.java b/org.eclipse.handly/src/org/eclipse/handly/snapshot/TextFileSnapshotBase.java
index b8a57ce..722deee 100644
--- a/org.eclipse.handly/src/org/eclipse/handly/snapshot/TextFileSnapshotBase.java
+++ b/org.eclipse.handly/src/org/eclipse/handly/snapshot/TextFileSnapshotBase.java
@@ -1,5 +1,5 @@
 /*******************************************************************************
- * Copyright (c) 2014, 2016 1C-Soft LLC and others.
+ * Copyright (c) 2014, 2018 1C-Soft LLC and others.
  * All rights reserved. This program and the accompanying materials
  * are made available under the terms of the Eclipse Public License v1.0
  * which accompanies this distribution, and is available at
@@ -13,16 +13,12 @@
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.InputStreamReader;
-import java.lang.ref.Reference;
-import java.lang.ref.SoftReference;
 
-import org.eclipse.core.runtime.CoreException;
 import org.eclipse.core.runtime.IStatus;
 import org.eclipse.core.runtime.Platform;
 import org.eclipse.core.runtime.QualifiedName;
 import org.eclipse.core.runtime.Status;
 import org.eclipse.core.runtime.content.IContentDescription;
-import org.eclipse.handly.internal.Activator;
 
 /**
  * Internal base for file related snapshot implementations.
@@ -30,56 +26,37 @@
 abstract class TextFileSnapshotBase
     extends Snapshot
 {
+    static final TextFileSnapshotBase NON_EXISTING = new TextFileSnapshotBase()
+    {
+        @Override
+        public String getContents()
+        {
+            return ""; //$NON-NLS-1$
+        }
+
+        @Override
+        public IStatus getStatus()
+        {
+            return Status.OK_STATUS;
+        }
+
+        @Override
+        public boolean exists()
+        {
+            return false;
+        }
+    };
+
     private static final int DEFAULT_READING_SIZE = 8192;
     private static final char[] EMPTY_CHAR_ARRAY = new char[0];
 
-    private Reference<String> contents;
-    private volatile IStatus status = Status.OK_STATUS;
-
-    @Override
-    public synchronized String getContents()
-    {
-        if (!exists())
-            return ""; //$NON-NLS-1$
-
-        String result = null;
-        boolean sync = isSynchronized();
-        if (contents != null)
-        {
-            if (!sync)
-                contents = null; // no need to continue holding on contents
-            else
-                result = contents.get();
-        }
-        if (result == null && sync)
-        {
-            try
-            {
-                cacheCharset();
-                String currentContents = readContents();
-                if (isSynchronized()) // still current
-                    contents = new SoftReference<String>(result =
-                        currentContents);
-            }
-            catch (CoreException e)
-            {
-                Activator.log(e.getStatus());
-                status = e.getStatus();
-            }
-        }
-        return result;
-    }
-
     /**
      * Returns whether an I/O error was encountered while reading the file.
      *
      * @return an error status if an I/O error was encountered, or OK status
      *  otherwise
      */
-    public IStatus getStatus()
-    {
-        return status;
-    }
+    public abstract IStatus getStatus();
 
     /**
      * Returns whether the file existed at the moment this snapshot was taken.
@@ -89,12 +66,6 @@
      */
     public abstract boolean exists();
 
-    abstract boolean isSynchronized();
-
-    abstract void cacheCharset() throws CoreException;
-
-    abstract String readContents() throws CoreException;
-
     static String getCharset(InputStream contents, String fileName)
         throws IOException
     {
diff --git a/org.eclipse.handly/src/org/eclipse/handly/snapshot/TextFileSnapshotWs.java b/org.eclipse.handly/src/org/eclipse/handly/snapshot/TextFileSnapshotWs.java
new file mode 100644
index 0000000..affa949
--- /dev/null
+++ b/org.eclipse.handly/src/org/eclipse/handly/snapshot/TextFileSnapshotWs.java
@@ -0,0 +1,165 @@
+/*******************************************************************************
+ * Copyright (c) 2018 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.snapshot;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.lang.ref.Reference;
+import java.lang.ref.SoftReference;
+
+import org.eclipse.core.resources.IFile;
+import org.eclipse.core.resources.IResource;
+import org.eclipse.core.runtime.CoreException;
+import org.eclipse.core.runtime.IStatus;
+import org.eclipse.core.runtime.Status;
+import org.eclipse.handly.internal.Activator;
+
+/**
+ * Internal representation for a snapshot of a text file in the workspace.
+ * Thread-safe.
+ */
+final class TextFileSnapshotWs
+    extends TextFileSnapshotBase
+{
+    private final IFile file;
+    private final long modificationStamp;
+    private Reference<String> contents;
+    private String charset;
+    private volatile IStatus status = Status.OK_STATUS;
+
+    /**
+     * Takes a snapshot of the given text file in the workspace.
+     *
+     * @param file not <code>null</code>
+     */
+    public TextFileSnapshotWs(IFile file)
+    {
+        if (file == null)
+            throw new IllegalArgumentException();
+        this.file = file;
+        this.modificationStamp = file.getModificationStamp();
+    }
+
+    @Override
+    public synchronized String getContents()
+    {
+        if (!exists())
+            return ""; //$NON-NLS-1$
+
+        String result = null;
+        boolean current = isCurrent();
+        if (contents != null)
+        {
+            if (!current)
+                contents = null; // no need to continue holding on contents
+            else
+                result = contents.get();
+        }
+        if (result == null && current)
+        {
+            try
+            {
+                cacheCharset();
+                String currentContents = readContents();
+                if (isCurrent()) // still current
+                    contents = new SoftReference<String>(result =
+                        currentContents);
+            }
+            catch (CoreException e)
+            {
+                Activator.log(e.getStatus());
+                status = e.getStatus();
+            }
+        }
+        return result;
+    }
+
+    @Override
+    public IStatus getStatus()
+    {
+        return status;
+    }
+
+    @Override
+    public boolean exists()
+    {
+        return modificationStamp != IResource.NULL_STAMP;
+    }
+
+    @Override
+    protected Boolean predictEquality(Snapshot other)
+    {
+        if (other instanceof TextFileSnapshotWs)
+        {
+            TextFileSnapshotWs otherSnapshot = (TextFileSnapshotWs)other;
+            if (file.equals(otherSnapshot.file)
+                && modificationStamp == otherSnapshot.modificationStamp)
+                return true;
+        }
+
+        if (!isCurrent())
+            return false; // expired
+
+        return null;
+    }
+
+    private boolean isCurrent()
+    {
+        return modificationStamp == file.getModificationStamp()
+            && status.isOK();
+    }
+
+    private void cacheCharset() throws CoreException
+    {
+        if (charset != null)
+            return;
+
+        charset = file.getCharset(false);
+        if (charset == null)
+        {
+            try (InputStream contents = file.getContents(false))
+            {
+                charset = getCharset(contents, file.getName());
+            }
+            catch (IOException e)
+            {
+                throw new CoreException(Activator.createErrorStatus(
+                    e.getMessage(), e));
+            }
+        }
+        if (charset == null)
+            charset = file.getParent().getDefaultCharset();
+    }
+
+    private String readContents() throws CoreException
+    {
+        try (
+            InputStream stream = file.getContents(false);
+            InputStreamReader reader = new InputStreamReader(stream, charset))
+        {
+            return String.valueOf(getInputStreamAsCharArray(stream, reader));
+        }
+        catch (IOException e)
+        {
+            throw new CoreException(Activator.createErrorStatus(e.getMessage(),
+                e));
+        }
+    }
+
+    /*
+     * For testing purposes only.
+     */
+    void clearContents()
+    {
+        contents.clear();
+    }
+}
diff --git a/org.eclipse.handly/src/org/eclipse/handly/snapshot/TextFileStoreSnapshot.java b/org.eclipse.handly/src/org/eclipse/handly/snapshot/TextFileStoreSnapshot.java
index 1615bef..80755a6 100644
--- a/org.eclipse.handly/src/org/eclipse/handly/snapshot/TextFileStoreSnapshot.java
+++ b/org.eclipse.handly/src/org/eclipse/handly/snapshot/TextFileStoreSnapshot.java
@@ -1,5 +1,5 @@
 /*******************************************************************************
- * Copyright (c) 2016 1C-Soft LLC.
+ * Copyright (c) 2016, 2018 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
@@ -19,6 +19,8 @@
 import org.eclipse.core.filesystem.EFS;
 import org.eclipse.core.filesystem.IFileStore;
 import org.eclipse.core.runtime.CoreException;
+import org.eclipse.core.runtime.IStatus;
+import org.eclipse.core.runtime.Status;
 import org.eclipse.handly.internal.Activator;
 
 /**
@@ -28,8 +30,9 @@
     extends TextFileSnapshotBase
 {
     private final IFileStore fileStore;
-    private final long modificationStamp;
-    private String charset;
+    private final long lastModified;
+    private final IStatus status;
+    private String contents;
 
     /**
      * Takes a snapshot of the given text file store. The snapshot may use a
@@ -40,10 +43,7 @@
      */
     public TextFileStoreSnapshot(IFileStore fileStore)
     {
-        if (fileStore == null)
-            throw new IllegalArgumentException();
-        this.fileStore = fileStore;
-        this.modificationStamp = getFileStoreModificationStamp(fileStore);
+        this(fileStore, (String)null);
     }
 
     /**
@@ -55,62 +55,73 @@
      */
     public TextFileStoreSnapshot(IFileStore fileStore, Charset charset)
     {
-        this(fileStore);
-        this.charset = charset.name();
+        this(fileStore, charset.name());
+    }
+
+    private TextFileStoreSnapshot(IFileStore fileStore, String charset)
+    {
+        if (fileStore == null)
+            throw new IllegalArgumentException();
+        this.fileStore = fileStore;
+        this.lastModified = getLastModified(fileStore);
+        if (this.lastModified == EFS.NONE)
+        {
+            this.status = Status.OK_STATUS;
+            this.contents = ""; //$NON-NLS-1$
+        }
+        else
+        {
+            IStatus status = Status.OK_STATUS;
+            String contents = null;
+            try
+            {
+                contents = readContents(fileStore, charset);
+            }
+            catch (CoreException e)
+            {
+                Activator.log(e.getStatus());
+                status = e.getStatus();
+            }
+            this.status = status;
+            this.contents = contents;
+        }
+    }
+
+    @Override
+    public synchronized String getContents()
+    {
+        if (contents != null && lastModified != getLastModified(fileStore))
+            contents = null;
+
+        return contents;
+    }
+
+    @Override
+    public IStatus getStatus()
+    {
+        return status;
     }
 
     @Override
     public boolean exists()
     {
-        return modificationStamp != EFS.NONE;
+        return lastModified != EFS.NONE;
     }
 
     @Override
     protected Boolean predictEquality(Snapshot other)
     {
-        if (other instanceof TextFileStoreSnapshot)
-        {
-            TextFileStoreSnapshot otherSnapshot = (TextFileStoreSnapshot)other;
-            if (fileStore.equals(otherSnapshot.fileStore)
-                && modificationStamp == otherSnapshot.modificationStamp)
-                return true;
-        }
-
-        if (!isSynchronized())
+        if (lastModified != getLastModified(fileStore) || !status.isOK())
             return false; // expired
 
         return null;
     }
 
-    @Override
-    boolean isSynchronized()
+    private static String readContents(IFileStore fileStore, String charset)
+        throws CoreException
     {
-        return modificationStamp == getFileStoreModificationStamp(fileStore)
-            && getStatus().isOK();
-    }
-
-    @Override
-    void cacheCharset() throws CoreException
-    {
-        if (charset != null)
-            return;
-
-        try (InputStream contents = fileStore.openInputStream(EFS.NONE, null))
-        {
-            charset = getCharset(contents, fileStore.getName());
-        }
-        catch (IOException e)
-        {
-            throw new CoreException(Activator.createErrorStatus(e.getMessage(),
-                e));
-        }
         if (charset == null)
-            charset = ITextFileBufferManager.DEFAULT.getDefaultEncoding();
-    }
-
-    @Override
-    String readContents() throws CoreException
-    {
+            charset = detectCharset(fileStore);
         try (
             InputStream stream = fileStore.openInputStream(EFS.NONE, null);
             InputStreamReader reader = new InputStreamReader(stream, charset))
@@ -124,7 +135,25 @@
         }
     }
 
-    private static long getFileStoreModificationStamp(IFileStore fileStore)
+    private static String detectCharset(IFileStore fileStore)
+        throws CoreException
+    {
+        String charset = null;
+        try (InputStream contents = fileStore.openInputStream(EFS.NONE, null))
+        {
+            charset = getCharset(contents, fileStore.getName());
+        }
+        catch (IOException e)
+        {
+            throw new CoreException(Activator.createErrorStatus(e.getMessage(),
+                e));
+        }
+        if (charset == null)
+            charset = ITextFileBufferManager.DEFAULT.getDefaultEncoding();
+        return charset;
+    }
+
+    private static long getLastModified(IFileStore fileStore)
     {
         return fileStore.fetchInfo().getLastModified();
     }