2 * Copyright 2000-2014 JetBrains s.r.o.
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
8 * http://www.apache.org/licenses/LICENSE-2.0
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
16 package com.intellij.history.integration;
18 import com.intellij.history.core.LocalHistoryFacade;
19 import com.intellij.history.core.Paths;
20 import com.intellij.history.core.StoredContent;
21 import com.intellij.history.core.tree.DirectoryEntry;
22 import com.intellij.history.core.tree.Entry;
23 import com.intellij.history.core.tree.FileEntry;
24 import com.intellij.history.core.tree.RootEntry;
25 import com.intellij.openapi.application.ApplicationManager;
26 import com.intellij.openapi.editor.Document;
27 import com.intellij.openapi.fileEditor.FileDocumentManager;
28 import com.intellij.openapi.fileTypes.FileType;
29 import com.intellij.openapi.fileTypes.FileTypeManager;
30 import com.intellij.openapi.project.Project;
31 import com.intellij.openapi.project.ProjectManager;
32 import com.intellij.openapi.roots.ProjectFileIndex;
33 import com.intellij.openapi.roots.ProjectRootManager;
34 import com.intellij.openapi.util.Clock;
35 import com.intellij.openapi.util.Comparing;
36 import com.intellij.openapi.util.Key;
37 import com.intellij.openapi.util.Pair;
38 import com.intellij.openapi.util.text.StringUtil;
39 import com.intellij.openapi.vfs.*;
40 import com.intellij.openapi.vfs.encoding.EncodingRegistry;
41 import com.intellij.openapi.vfs.newvfs.ManagingFS;
42 import com.intellij.openapi.vfs.newvfs.NewVirtualFile;
43 import com.intellij.openapi.vfs.newvfs.events.VFileEvent;
44 import com.intellij.openapi.vfs.newvfs.impl.VirtualFileSystemEntry;
45 import com.intellij.util.NullableFunction;
46 import com.intellij.util.containers.ContainerUtil;
47 import org.jetbrains.annotations.NotNull;
48 import org.jetbrains.annotations.Nullable;
50 import java.io.IOException;
51 import java.io.UnsupportedEncodingException;
52 import java.util.ArrayList;
53 import java.util.Arrays;
54 import java.util.Collections;
55 import java.util.List;
57 public class IdeaGateway {
58 private static final Key<ContentAndTimestamps> SAVED_DOCUMENT_CONTENT_AND_STAMP_KEY
59 = Key.create("LocalHistory.SAVED_DOCUMENT_CONTENT_AND_STAMP_KEY");
61 public boolean isVersioned(@NotNull VirtualFile f) {
62 return isVersioned(f, false);
65 public boolean isVersioned(@NotNull VirtualFile f, boolean shouldBeInContent) {
66 if (!f.isInLocalFileSystem()) return false;
68 if (!f.isDirectory() && StringUtil.endsWith(f.getNameSequence(), ".class")) return false;
70 LocalHistoryImpl.getInstanceImpl().dispatchPendingEvents();
72 VersionedFilterData versionedFilterData;
73 VfsEventDispatchContext vfsEventDispatchContext = ourCurrentEventDispatchContext.get();
74 if (vfsEventDispatchContext != null) {
75 versionedFilterData = vfsEventDispatchContext.myFilterData;
76 if (versionedFilterData == null) versionedFilterData = vfsEventDispatchContext.myFilterData = new VersionedFilterData();
78 versionedFilterData = new VersionedFilterData();
81 boolean isInContent = false;
82 int numberOfOpenProjects = versionedFilterData.myOpenedProjects.size();
83 for (int i = 0; i < numberOfOpenProjects; ++i) {
84 if (Comparing.equal(versionedFilterData.myWorkspaceFiles.get(i), f)) return false;
85 ProjectFileIndex index = versionedFilterData.myProjectFileIndices.get(i);
87 if (index.isExcluded(f)) return false;
88 isInContent |= index.isInContent(f);
90 if (shouldBeInContent && !isInContent) return false;
92 // optimisation: FileTypeManager.isFileIgnored(f) already checked inside ProjectFileIndex.isIgnored()
93 return numberOfOpenProjects != 0 || !FileTypeManager.getInstance().isFileIgnored(f);
96 private static final ThreadLocal<VfsEventDispatchContext> ourCurrentEventDispatchContext = new ThreadLocal<>();
98 private static class VfsEventDispatchContext {
99 final List<? extends VFileEvent> myEvents;
100 final boolean myBeforeEvents;
101 final VfsEventDispatchContext myPreviousContext;
103 VersionedFilterData myFilterData;
105 VfsEventDispatchContext(List<? extends VFileEvent> events, boolean beforeEvents, VfsEventDispatchContext context) {
107 myBeforeEvents = beforeEvents;
108 myPreviousContext = context;
111 public void close() {
112 ourCurrentEventDispatchContext.set(myPreviousContext);
116 public void runWithVfsEventsDispatchContext(List<? extends VFileEvent> events, boolean beforeEvents, Runnable action) {
117 VfsEventDispatchContext vfsEventDispatchContext = new VfsEventDispatchContext(events, beforeEvents, ourCurrentEventDispatchContext.get());
118 ourCurrentEventDispatchContext.set(vfsEventDispatchContext);
122 vfsEventDispatchContext.close();
126 private static class VersionedFilterData {
127 final List<Project> myOpenedProjects = new ArrayList<>();
128 final List<ProjectFileIndex> myProjectFileIndices = new ArrayList<>();
129 final List<VirtualFile> myWorkspaceFiles = new ArrayList<>();
131 VersionedFilterData() {
132 Project[] openProjects = ProjectManager.getInstance().getOpenProjects();
134 for (Project each : openProjects) {
135 if (each.isDefault()) continue;
136 if (!each.isInitialized()) continue;
138 myWorkspaceFiles.add(each.getWorkspaceFile());
139 myOpenedProjects.add(each);
140 myProjectFileIndices.add(ProjectRootManager.getInstance(each).getFileIndex());
145 public boolean areContentChangesVersioned(@NotNull VirtualFile f) {
146 return isVersioned(f) && !f.isDirectory() && !f.getFileType().isBinary();
149 public boolean areContentChangesVersioned(@NotNull String fileName) {
150 return !FileTypeManager.getInstance().getFileTypeByFileName(fileName).isBinary();
153 public boolean ensureFilesAreWritable(@NotNull Project p, @NotNull List<VirtualFile> ff) {
154 ReadonlyStatusHandler h = ReadonlyStatusHandler.getInstance(p);
155 return !h.ensureFilesWritable(VfsUtilCore.toVirtualFileArray(ff)).hasReadonlyFiles();
159 public VirtualFile findVirtualFile(@NotNull String path) {
160 return LocalFileSystem.getInstance().findFileByPath(path);
164 public VirtualFile findOrCreateFileSafely(@NotNull VirtualFile parent, @NotNull String name, boolean isDirectory) throws IOException {
165 VirtualFile f = parent.findChild(name);
166 if (f != null && f.isDirectory() != isDirectory) {
172 ? parent.createChildDirectory(this, name)
173 : parent.createChildData(this, name);
179 public VirtualFile findOrCreateFileSafely(@NotNull String path, boolean isDirectory) throws IOException {
180 VirtualFile f = findVirtualFile(path);
181 if (f != null && f.isDirectory() != isDirectory) {
186 VirtualFile parent = findOrCreateFileSafely(Paths.getParentOf(path), true);
187 String name = Paths.getNameOf(path);
189 ? parent.createChildDirectory(this, name)
190 : parent.createChildData(this, name);
195 public List<VirtualFile> getAllFilesFrom(@NotNull String path) {
196 VirtualFile f = findVirtualFile(path);
197 if (f == null) return Collections.emptyList();
198 return collectFiles(f, new ArrayList<>());
202 private static List<VirtualFile> collectFiles(@NotNull VirtualFile f, @NotNull List<VirtualFile> result) {
203 if (f.isDirectory()) {
204 for (VirtualFile child : iterateDBChildren(f)) {
205 collectFiles(child, result);
215 public static Iterable<VirtualFile> iterateDBChildren(VirtualFile f) {
216 if (!(f instanceof NewVirtualFile)) return Collections.emptyList();
217 NewVirtualFile nf = (NewVirtualFile)f;
218 return nf.iterInDbChildren();
222 public static Iterable<VirtualFile> loadAndIterateChildren(VirtualFile f) {
223 if (!(f instanceof NewVirtualFile)) return Collections.emptyList();
224 NewVirtualFile nf = (NewVirtualFile)f;
225 return Arrays.asList(nf.getChildren());
229 public RootEntry createTransientRootEntry() {
230 ApplicationManager.getApplication().assertReadAccessAllowed();
231 RootEntry root = new RootEntry();
232 doCreateChildren(root, getLocalRoots(), false);
237 public RootEntry createTransientRootEntryForPathOnly(@NotNull String path) {
238 ApplicationManager.getApplication().assertReadAccessAllowed();
239 RootEntry root = new RootEntry();
240 doCreateChildrenForPathOnly(root, path, getLocalRoots());
244 private static List<VirtualFile> getLocalRoots() {
245 return Arrays.asList(ManagingFS.getInstance().getLocalRoots());
248 private void doCreateChildrenForPathOnly(@NotNull DirectoryEntry parent,
249 @NotNull String path,
250 @NotNull Iterable<VirtualFile> children) {
251 for (VirtualFile child : children) {
252 String name = StringUtil.trimStart(child.getName(), "/"); // on Mac FS root name is "/"
253 if (!path.startsWith(name)) continue;
254 String rest = path.substring(name.length());
255 if (!rest.isEmpty() && rest.charAt(0) != '/') continue;
256 if (!rest.isEmpty() && rest.charAt(0) == '/') {
257 rest = rest.substring(1);
259 Entry e = doCreateEntryForPathOnly(child, rest);
260 if (e == null) continue;
266 private Entry doCreateEntryForPathOnly(@NotNull VirtualFile file, @NotNull String path) {
267 if (!file.isDirectory()) {
268 if (!isVersioned(file)) return null;
270 return doCreateFileEntry(file, getActualContentNoAcquire(file));
272 DirectoryEntry newDir = new DirectoryEntry(file.getName());
273 doCreateChildrenForPathOnly(newDir, path, iterateDBChildren(file));
274 if (!isVersioned(file) && newDir.getChildren().isEmpty()) return null;
279 public Entry createTransientEntry(@NotNull VirtualFile file) {
280 ApplicationManager.getApplication().assertReadAccessAllowed();
281 return doCreateEntry(file, false);
285 public Entry createEntryForDeletion(@NotNull VirtualFile file) {
286 ApplicationManager.getApplication().assertReadAccessAllowed();
287 return doCreateEntry(file, true);
291 private Entry doCreateEntry(@NotNull VirtualFile file, boolean forDeletion) {
292 if (!file.isDirectory()) {
293 if (!isVersioned(file)) return null;
295 Pair<StoredContent, Long> contentAndStamps;
297 FileDocumentManager m = FileDocumentManager.getInstance();
298 Document d = m.isFileModified(file) ? m.getCachedDocument(file) : null; // should not try to load document
299 contentAndStamps = acquireAndClearCurrentContent(file, d);
302 contentAndStamps = getActualContentNoAcquire(file);
305 return doCreateFileEntry(file, contentAndStamps);
308 DirectoryEntry newDir = null;
309 if (file instanceof VirtualFileSystemEntry) {
310 int nameId = ((VirtualFileSystemEntry)file).getNameId();
312 newDir = new DirectoryEntry(nameId);
316 if (newDir == null) {
317 newDir = new DirectoryEntry(file.getName());
320 doCreateChildren(newDir, iterateDBChildren(file), forDeletion);
321 if (!isVersioned(file) && newDir.getChildren().isEmpty()) return null;
326 private Entry doCreateFileEntry(@NotNull VirtualFile file, Pair<StoredContent, Long> contentAndStamps) {
327 if (file instanceof VirtualFileSystemEntry) {
328 return new FileEntry(((VirtualFileSystemEntry)file).getNameId(), contentAndStamps.first, contentAndStamps.second, !file.isWritable());
330 return new FileEntry(file.getName(), contentAndStamps.first, contentAndStamps.second, !file.isWritable());
333 private void doCreateChildren(@NotNull DirectoryEntry parent, Iterable<VirtualFile> children, final boolean forDeletion) {
334 List<Entry> entries = ContainerUtil.mapNotNull(children, (NullableFunction<VirtualFile, Entry>)each -> doCreateEntry(each, forDeletion));
335 parent.addChildren(entries);
338 public void registerUnsavedDocuments(@NotNull final LocalHistoryFacade vcs) {
339 ApplicationManager.getApplication().runReadAction(() -> {
340 vcs.beginChangeSet();
341 for (Document d : FileDocumentManager.getInstance().getUnsavedDocuments()) {
342 VirtualFile f = getFile(d);
343 if (!shouldRegisterDocument(f)) continue;
344 registerDocumentContents(vcs, f, d);
346 vcs.endChangeSet(null);
350 private boolean shouldRegisterDocument(@Nullable VirtualFile f) {
351 return f != null && f.isValid() && areContentChangesVersioned(f);
354 private void registerDocumentContents(@NotNull LocalHistoryFacade vcs, @NotNull VirtualFile f, Document d) {
355 Pair<StoredContent, Long> contentAndStamp = acquireAndUpdateActualContent(f, d);
356 if (contentAndStamp != null) {
357 vcs.contentChanged(f.getPath(), contentAndStamp.first, contentAndStamp.second);
361 // returns null is content has not been changes since last time
363 public Pair<StoredContent, Long> acquireAndUpdateActualContent(@NotNull VirtualFile f, @Nullable Document d) {
364 ContentAndTimestamps contentAndStamp = f.getUserData(SAVED_DOCUMENT_CONTENT_AND_STAMP_KEY);
365 if (contentAndStamp == null) {
366 if (d != null) saveDocumentContent(f, d);
367 return Pair.create(StoredContent.acquireContent(f), f.getTimeStamp());
370 // if no need to save current document content when simply return and clear stored one
372 f.putUserData(SAVED_DOCUMENT_CONTENT_AND_STAMP_KEY, null);
373 return Pair.create(contentAndStamp.content, contentAndStamp.registeredTimestamp);
376 // if the stored content equals the current one, do not store it and return null
377 if (d.getModificationStamp() == contentAndStamp.documentModificationStamp) return null;
379 // is current content has been changed, store it and return the previous one
380 saveDocumentContent(f, d);
381 return Pair.create(contentAndStamp.content, contentAndStamp.registeredTimestamp);
384 private static void saveDocumentContent(@NotNull VirtualFile f, @NotNull Document d) {
385 f.putUserData(SAVED_DOCUMENT_CONTENT_AND_STAMP_KEY,
386 new ContentAndTimestamps(Clock.getTime(),
387 StoredContent.acquireContent(bytesFromDocument(d)),
388 d.getModificationStamp()));
392 public Pair<StoredContent, Long> acquireAndClearCurrentContent(@NotNull VirtualFile f, @Nullable Document d) {
393 ContentAndTimestamps contentAndStamp = f.getUserData(SAVED_DOCUMENT_CONTENT_AND_STAMP_KEY);
394 f.putUserData(SAVED_DOCUMENT_CONTENT_AND_STAMP_KEY, null);
396 if (d != null && contentAndStamp != null) {
397 // if previously stored content was not changed, return it
398 if (d.getModificationStamp() == contentAndStamp.documentModificationStamp) {
399 return Pair.create(contentAndStamp.content, contentAndStamp.registeredTimestamp);
403 // release previously stored
404 if (contentAndStamp != null) {
405 contentAndStamp.content.release();
408 // take document's content if any
410 return Pair.create(StoredContent.acquireContent(bytesFromDocument(d)), Clock.getTime());
413 return Pair.create(StoredContent.acquireContent(f), f.getTimeStamp());
417 private static Pair<StoredContent, Long> getActualContentNoAcquire(@NotNull VirtualFile f) {
418 ContentAndTimestamps result = f.getUserData(SAVED_DOCUMENT_CONTENT_AND_STAMP_KEY);
419 if (result == null) {
420 return Pair.create(StoredContent.transientContent(f), f.getTimeStamp());
422 return Pair.create(result.content, result.registeredTimestamp);
425 private static byte[] bytesFromDocument(@NotNull Document d) {
427 return d.getText().getBytes(getFile(d).getCharset().name());
429 catch (UnsupportedEncodingException e) {
430 return d.getText().getBytes();
434 public String stringFromBytes(@NotNull byte[] bytes, @NotNull String path) {
436 VirtualFile file = findVirtualFile(path);
438 return CharsetToolkit.bytesToString(bytes, EncodingRegistry.getInstance().getDefaultCharset());
440 return new String(bytes, file.getCharset().name());
442 catch (UnsupportedEncodingException e1) {
443 return new String(bytes);
447 public void saveAllUnsavedDocuments() {
448 FileDocumentManager.getInstance().saveAllDocuments();
452 private static VirtualFile getFile(@NotNull Document d) {
453 return FileDocumentManager.getInstance().getFile(d);
457 public Document getDocument(@NotNull String path) {
458 return FileDocumentManager.getInstance().getDocument(findVirtualFile(path));
462 public FileType getFileType(@NotNull String fileName) {
463 return FileTypeManager.getInstance().getFileTypeByFileName(fileName);
466 private static class ContentAndTimestamps {
467 long registeredTimestamp;
468 StoredContent content;
469 long documentModificationStamp;
471 private ContentAndTimestamps(long registeredTimestamp, StoredContent content, long documentModificationStamp) {
472 this.registeredTimestamp = registeredTimestamp;
473 this.content = content;
474 this.documentModificationStamp = documentModificationStamp;