blob: 30822c0aec7943a4b53c2165c1362aee78001878 [file] [log] [blame]
/*
* Copyright 2000-2014 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.intellij.history.integration;
import com.intellij.history.core.LocalHistoryFacade;
import com.intellij.history.core.Paths;
import com.intellij.history.core.StoredContent;
import com.intellij.history.core.tree.DirectoryEntry;
import com.intellij.history.core.tree.Entry;
import com.intellij.history.core.tree.FileEntry;
import com.intellij.history.core.tree.RootEntry;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.editor.Document;
import com.intellij.openapi.fileEditor.FileDocumentManager;
import com.intellij.openapi.fileTypes.FileType;
import com.intellij.openapi.fileTypes.FileTypeManager;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.project.ProjectManager;
import com.intellij.openapi.roots.ProjectFileIndex;
import com.intellij.openapi.roots.ProjectRootManager;
import com.intellij.openapi.util.Clock;
import com.intellij.openapi.util.Comparing;
import com.intellij.openapi.util.Key;
import com.intellij.openapi.util.Pair;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.openapi.vfs.*;
import com.intellij.openapi.vfs.encoding.EncodingRegistry;
import com.intellij.openapi.vfs.newvfs.ManagingFS;
import com.intellij.openapi.vfs.newvfs.NewVirtualFile;
import com.intellij.util.NullableFunction;
import com.intellij.util.containers.ContainerUtil;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
public class IdeaGateway {
private static final Key<ContentAndTimestamps> SAVED_DOCUMENT_CONTENT_AND_STAMP_KEY
= Key.create("LocalHistory.SAVED_DOCUMENT_CONTENT_AND_STAMP_KEY");
public boolean isVersioned(@NotNull VirtualFile f) {
return isVersioned(f, false);
}
public boolean isVersioned(@NotNull VirtualFile f, boolean shouldBeInContent) {
if (!f.isInLocalFileSystem()) return false;
if (!f.isDirectory() && StringUtil.endsWith(f.getNameSequence(), ".class")) return false;
Project[] openProjects = ProjectManager.getInstance().getOpenProjects();
boolean isInContent = false;
for (Project each : openProjects) {
if (each.isDefault()) continue;
if (!each.isInitialized()) continue;
if (Comparing.equal(each.getWorkspaceFile(), f)) return false;
ProjectFileIndex index = ProjectRootManager.getInstance(each).getFileIndex();
if (index.isExcluded(f)) return false;
isInContent |= index.isInContent(f);
}
if (shouldBeInContent && !isInContent) return false;
// optimisation: FileTypeManager.isFileIgnored(f) already checked inside ProjectFileIndex.isIgnored()
return openProjects.length != 0 || !FileTypeManager.getInstance().isFileIgnored(f);
}
public boolean areContentChangesVersioned(@NotNull VirtualFile f) {
return isVersioned(f) && !f.isDirectory() && areContentChangesVersioned(f.getName());
}
public boolean areContentChangesVersioned(@NotNull String fileName) {
return !FileTypeManager.getInstance().getFileTypeByFileName(fileName).isBinary();
}
public boolean ensureFilesAreWritable(@NotNull Project p, @NotNull List<VirtualFile> ff) {
ReadonlyStatusHandler h = ReadonlyStatusHandler.getInstance(p);
return !h.ensureFilesWritable(VfsUtilCore.toVirtualFileArray(ff)).hasReadonlyFiles();
}
@Nullable
public VirtualFile findVirtualFile(@NotNull String path) {
return LocalFileSystem.getInstance().findFileByPath(path);
}
@NotNull
public VirtualFile findOrCreateFileSafely(@NotNull VirtualFile parent, @NotNull String name, boolean isDirectory) throws IOException {
VirtualFile f = parent.findChild(name);
if (f != null && f.isDirectory() != isDirectory) {
f.delete(this);
f = null;
}
if (f == null) {
f = isDirectory
? parent.createChildDirectory(this, name)
: parent.createChildData(this, name);
}
return f;
}
@NotNull
public VirtualFile findOrCreateFileSafely(@NotNull String path, boolean isDirectory) throws IOException {
VirtualFile f = findVirtualFile(path);
if (f != null && f.isDirectory() != isDirectory) {
f.delete(this);
f = null;
}
if (f == null) {
VirtualFile parent = findOrCreateFileSafely(Paths.getParentOf(path), true);
String name = Paths.getNameOf(path);
f = isDirectory
? parent.createChildDirectory(this, name)
: parent.createChildData(this, name);
}
return f;
}
public List<VirtualFile> getAllFilesFrom(@NotNull String path) {
VirtualFile f = findVirtualFile(path);
if (f == null) return Collections.emptyList();
return collectFiles(f, new ArrayList<VirtualFile>());
}
@NotNull
private static List<VirtualFile> collectFiles(@NotNull VirtualFile f, @NotNull List<VirtualFile> result) {
if (f.isDirectory()) {
for (VirtualFile child : iterateDBChildren(f)) {
collectFiles(child, result);
}
}
else {
result.add(f);
}
return result;
}
@NotNull
public static Iterable<VirtualFile> iterateDBChildren(VirtualFile f) {
if (!(f instanceof NewVirtualFile)) return Collections.emptyList();
NewVirtualFile nf = (NewVirtualFile)f;
return nf.iterInDbChildren();
}
@NotNull
public static Iterable<VirtualFile> loadAndIterateChildren(VirtualFile f) {
if (!(f instanceof NewVirtualFile)) return Collections.emptyList();
NewVirtualFile nf = (NewVirtualFile)f;
return Arrays.asList(nf.getChildren());
}
@NotNull
public RootEntry createTransientRootEntry() {
ApplicationManager.getApplication().assertReadAccessAllowed();
RootEntry root = new RootEntry();
doCreateChildren(root, getLocalRoots(), false);
return root;
}
@NotNull
public RootEntry createTransientRootEntryForPathOnly(@NotNull String path) {
ApplicationManager.getApplication().assertReadAccessAllowed();
RootEntry root = new RootEntry();
doCreateChildrenForPathOnly(root, path, getLocalRoots());
return root;
}
private static List<VirtualFile> getLocalRoots() {
return Arrays.asList(ManagingFS.getInstance().getLocalRoots());
}
private void doCreateChildrenForPathOnly(@NotNull DirectoryEntry parent,
@NotNull String path,
@NotNull Iterable<VirtualFile> children) {
for (VirtualFile child : children) {
String name = StringUtil.trimStart(child.getName(), "/"); // on Mac FS root name is "/"
if (!path.startsWith(name)) continue;
String rest = path.substring(name.length());
if (!rest.isEmpty() && rest.charAt(0) != '/') continue;
if (!rest.isEmpty() && rest.charAt(0) == '/') {
rest = rest.substring(1);
}
Entry e = doCreateEntryForPathOnly(child, rest);
if (e == null) continue;
parent.addChild(e);
}
}
@Nullable
private Entry doCreateEntryForPathOnly(@NotNull VirtualFile file, @NotNull String path) {
if (!file.isDirectory()) {
if (!isVersioned(file)) return null;
Pair<StoredContent, Long> contentAndStamps = getActualContentNoAcquire(file);
return new FileEntry(file.getName(), contentAndStamps.first, contentAndStamps.second, !file.isWritable());
}
DirectoryEntry newDir = new DirectoryEntry(file.getName());
doCreateChildrenForPathOnly(newDir, path, iterateDBChildren(file));
if (!isVersioned(file) && newDir.getChildren().isEmpty()) return null;
return newDir;
}
@Nullable
public Entry createTransientEntry(@NotNull VirtualFile file) {
ApplicationManager.getApplication().assertReadAccessAllowed();
return doCreateEntry(file, false);
}
@Nullable
public Entry createEntryForDeletion(@NotNull VirtualFile file) {
ApplicationManager.getApplication().assertReadAccessAllowed();
return doCreateEntry(file, true);
}
@Nullable
private Entry doCreateEntry(@NotNull VirtualFile file, boolean forDeletion) {
if (!file.isDirectory()) {
if (!isVersioned(file)) return null;
Pair<StoredContent, Long> contentAndStamps;
if (forDeletion) {
FileDocumentManager m = FileDocumentManager.getInstance();
Document d = m.isFileModified(file) ? m.getCachedDocument(file) : null; // should not try to load document
contentAndStamps = acquireAndClearCurrentContent(file, d);
}
else {
contentAndStamps = getActualContentNoAcquire(file);
}
return new FileEntry(file.getName(), contentAndStamps.first, contentAndStamps.second, !file.isWritable());
}
DirectoryEntry newDir = new DirectoryEntry(file.getName());
doCreateChildren(newDir, iterateDBChildren(file), forDeletion);
if (!isVersioned(file) && newDir.getChildren().isEmpty()) return null;
return newDir;
}
private void doCreateChildren(@NotNull DirectoryEntry parent, Iterable<VirtualFile> children, final boolean forDeletion) {
List<Entry> entries = ContainerUtil.mapNotNull(children, new NullableFunction<VirtualFile, Entry>() {
@Override
public Entry fun(@NotNull VirtualFile each) {
return doCreateEntry(each, forDeletion);
}
});
parent.addChildren(entries);
}
public void registerUnsavedDocuments(@NotNull final LocalHistoryFacade vcs) {
ApplicationManager.getApplication().runReadAction(new Runnable() {
@Override
public void run() {
vcs.beginChangeSet();
for (Document d : FileDocumentManager.getInstance().getUnsavedDocuments()) {
VirtualFile f = getFile(d);
if (!shouldRegisterDocument(f)) continue;
registerDocumentContents(vcs, f, d);
}
vcs.endChangeSet(null);
}
});
}
private boolean shouldRegisterDocument(@Nullable VirtualFile f) {
return f != null && f.isValid() && areContentChangesVersioned(f);
}
private void registerDocumentContents(@NotNull LocalHistoryFacade vcs, @NotNull VirtualFile f, Document d) {
Pair<StoredContent, Long> contentAndStamp = acquireAndUpdateActualContent(f, d);
if (contentAndStamp != null) {
vcs.contentChanged(f.getPath(), contentAndStamp.first, contentAndStamp.second);
}
}
// returns null is content has not been changes since last time
@Nullable
public Pair<StoredContent, Long> acquireAndUpdateActualContent(@NotNull VirtualFile f, @Nullable Document d) {
ContentAndTimestamps contentAndStamp = f.getUserData(SAVED_DOCUMENT_CONTENT_AND_STAMP_KEY);
if (contentAndStamp == null) {
if (d != null) saveDocumentContent(f, d);
return Pair.create(StoredContent.acquireContent(f), f.getTimeStamp());
}
// if no need to save current document content when simply return and clear stored one
if (d == null) {
f.putUserData(SAVED_DOCUMENT_CONTENT_AND_STAMP_KEY, null);
return Pair.create(contentAndStamp.content, contentAndStamp.registeredTimestamp);
}
// if the stored content equals the current one, do not store it and return null
if (d.getModificationStamp() == contentAndStamp.documentModificationStamp) return null;
// is current content has been changed, store it and return the previous one
saveDocumentContent(f, d);
return Pair.create(contentAndStamp.content, contentAndStamp.registeredTimestamp);
}
private static void saveDocumentContent(@NotNull VirtualFile f, @NotNull Document d) {
f.putUserData(SAVED_DOCUMENT_CONTENT_AND_STAMP_KEY,
new ContentAndTimestamps(Clock.getTime(),
StoredContent.acquireContent(bytesFromDocument(d)),
d.getModificationStamp()));
}
@NotNull
public Pair<StoredContent, Long> acquireAndClearCurrentContent(@NotNull VirtualFile f, @Nullable Document d) {
ContentAndTimestamps contentAndStamp = f.getUserData(SAVED_DOCUMENT_CONTENT_AND_STAMP_KEY);
f.putUserData(SAVED_DOCUMENT_CONTENT_AND_STAMP_KEY, null);
if (d != null && contentAndStamp != null) {
// if previously stored content was not changed, return it
if (d.getModificationStamp() == contentAndStamp.documentModificationStamp) {
return Pair.create(contentAndStamp.content, contentAndStamp.registeredTimestamp);
}
}
// release previously stored
if (contentAndStamp != null) {
contentAndStamp.content.release();
}
// take document's content if any
if (d != null) {
return Pair.create(StoredContent.acquireContent(bytesFromDocument(d)), Clock.getTime());
}
return Pair.create(StoredContent.acquireContent(f), f.getTimeStamp());
}
@NotNull
private static Pair<StoredContent, Long> getActualContentNoAcquire(@NotNull VirtualFile f) {
ContentAndTimestamps result = f.getUserData(SAVED_DOCUMENT_CONTENT_AND_STAMP_KEY);
if (result == null) {
return Pair.create(StoredContent.transientContent(f), f.getTimeStamp());
}
return Pair.create(result.content, result.registeredTimestamp);
}
private static byte[] bytesFromDocument(@NotNull Document d) {
try {
return d.getText().getBytes(getFile(d).getCharset().name());
}
catch (UnsupportedEncodingException e) {
return d.getText().getBytes();
}
}
public String stringFromBytes(@NotNull byte[] bytes, @NotNull String path) {
try {
VirtualFile file = findVirtualFile(path);
if (file == null) {
return CharsetToolkit.bytesToString(bytes, EncodingRegistry.getInstance().getDefaultCharset());
}
return new String(bytes, file.getCharset().name());
}
catch (UnsupportedEncodingException e1) {
return new String(bytes);
}
}
public void saveAllUnsavedDocuments() {
FileDocumentManager.getInstance().saveAllDocuments();
}
@Nullable
private static VirtualFile getFile(@NotNull Document d) {
return FileDocumentManager.getInstance().getFile(d);
}
@Nullable
public Document getDocument(@NotNull String path) {
return FileDocumentManager.getInstance().getDocument(findVirtualFile(path));
}
@NotNull
public FileType getFileType(@NotNull String fileName) {
return FileTypeManager.getInstance().getFileTypeByFileName(fileName);
}
private static class ContentAndTimestamps {
long registeredTimestamp;
StoredContent content;
long documentModificationStamp;
private ContentAndTimestamps(long registeredTimestamp, StoredContent content, long documentModificationStamp) {
this.registeredTimestamp = registeredTimestamp;
this.content = content;
this.documentModificationStamp = documentModificationStamp;
}
}
}