/* * Copyright 2000-2016 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.ide.scratch; import com.intellij.ide.navigationToolbar.AbstractNavBarModelExtension; import com.intellij.lang.Language; import com.intellij.lang.LanguageUtil; import com.intellij.lang.PerFileMappings; import com.intellij.lang.PerFileMappingsBase; import com.intellij.openapi.application.AccessToken; import com.intellij.openapi.application.Application; import com.intellij.openapi.application.ApplicationManager; import com.intellij.openapi.application.PathManager; import com.intellij.openapi.components.PersistentStateComponent; import com.intellij.openapi.components.State; import com.intellij.openapi.components.Storage; import com.intellij.openapi.components.StoragePathMacros; import com.intellij.openapi.fileEditor.FileDocumentManager; import com.intellij.openapi.fileEditor.FileEditorManager; import com.intellij.openapi.fileEditor.FileEditorManagerAdapter; import com.intellij.openapi.fileEditor.FileEditorManagerListener; import com.intellij.openapi.fileEditor.impl.EditorTabTitleProvider; import com.intellij.openapi.fileEditor.impl.FileEditorManagerImpl; import com.intellij.openapi.fileEditor.impl.NonProjectFileWritingAccessExtension; import com.intellij.openapi.fileTypes.*; import com.intellij.openapi.module.Module; import com.intellij.openapi.project.Project; import com.intellij.openapi.project.ProjectManager; import com.intellij.openapi.project.ProjectManagerAdapter; import com.intellij.openapi.util.io.FileUtil; import com.intellij.openapi.util.text.StringUtil; import com.intellij.openapi.vfs.LocalFileSystem; import com.intellij.openapi.vfs.VfsUtil; import com.intellij.openapi.vfs.VirtualFile; import com.intellij.psi.LanguageSubstitutor; import com.intellij.psi.LanguageSubstitutors; import com.intellij.psi.PsiElement; import com.intellij.psi.PsiFile; import com.intellij.psi.search.*; import com.intellij.psi.util.PsiUtilCore; import com.intellij.usages.impl.rules.UsageType; import com.intellij.usages.impl.rules.UsageTypeProvider; import com.intellij.util.PathUtil; import com.intellij.util.containers.ConcurrentFactoryMap; import com.intellij.util.containers.ContainerUtil; import com.intellij.util.indexing.IndexableSetContributor; import com.intellij.util.indexing.LightDirectoryIndex; import com.intellij.util.messages.MessageBus; import consulo.annotations.RequiredReadAction; import consulo.ide.IconDescriptor; import consulo.ide.IconDescriptorUpdater; import org.jdom.Element; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import java.io.IOException; import java.util.*; @State( name = "ScratchFileService", storages = {@Storage(file = StoragePathMacros.APP_CONFIG + "/scratches.xml")}) public class ScratchFileServiceImpl extends ScratchFileService implements PersistentStateComponent<Element> { private static final RootType NULL_TYPE = new RootType("", null) { }; private final LightDirectoryIndex<RootType> myIndex; private final MyLanguages myScratchMapping = new MyLanguages(); protected ScratchFileServiceImpl(Application application) { myIndex = new LightDirectoryIndex<>(application, NULL_TYPE, index -> { LocalFileSystem fileSystem = LocalFileSystem.getInstance(); for (RootType r : RootType.getAllRootIds()) { index.putInfo(fileSystem.findFileByPath(getRootPath(r)), r); } }); initFileOpenedListener(application.getMessageBus()); } @NotNull @Override public String getRootPath(@NotNull RootType rootId) { return getRootPath() + "/" + rootId.getId(); } @Nullable @Override public RootType getRootType(@Nullable VirtualFile file) { if (file == null) return null; VirtualFile directory = file.isDirectory() ? file : file.getParent(); RootType result = myIndex.getInfoForFile(directory); return result == NULL_TYPE ? null : result; } private void initFileOpenedListener(MessageBus messageBus) { final FileEditorManagerAdapter editorListener = new FileEditorManagerAdapter() { @Override public void fileOpened(@NotNull FileEditorManager source, @NotNull VirtualFile file) { if (!isEditable(file)) return; RootType rootType = getRootType(file); if (rootType == null) return; rootType.fileOpened(file, source); } @Override public void fileClosed(@NotNull FileEditorManager source, @NotNull VirtualFile file) { if (Boolean.TRUE.equals(file.getUserData(FileEditorManagerImpl.CLOSING_TO_REOPEN))) return; if (!isEditable(file)) return; RootType rootType = getRootType(file); if (rootType == null) return; rootType.fileClosed(file, source); } boolean isEditable(@NotNull VirtualFile file) { return FileDocumentManager.getInstance().getDocument(file) != null; } }; ProjectManagerAdapter projectListener = new ProjectManagerAdapter() { @Override public void projectOpened(Project project) { project.getMessageBus().connect(project).subscribe(FileEditorManagerListener.FILE_EDITOR_MANAGER, editorListener); FileEditorManager editorManager = FileEditorManager.getInstance(project); for (VirtualFile virtualFile : editorManager.getOpenFiles()) { editorListener.fileOpened(editorManager, virtualFile); } } }; for (Project project : ProjectManager.getInstance().getOpenProjects()) { projectListener.projectOpened(project); } messageBus.connect().subscribe(ProjectManager.TOPIC, projectListener); } @NotNull protected String getRootPath() { return FileUtil.toSystemIndependentName(PathManager.getScratchPath()); } @NotNull @Override public PerFileMappings<Language> getScratchesMapping() { return myScratchMapping; } @Nullable @Override public Element getState() { return myScratchMapping.getState(); } @Override public void loadState(Element state) { myScratchMapping.loadState(state); } private static class MyLanguages extends PerFileMappingsBase<Language> { @Override protected List<Language> getAvailableValues() { return LanguageUtil.getFileLanguages(); } @Nullable @Override protected String serialize(Language language) { return language.getID(); } @Nullable @Override protected Language handleUnknownMapping(VirtualFile file, String value) { return PlainTextLanguage.INSTANCE; } } public static class TypeFactory extends FileTypeFactory { @Override public void createFileTypes(@NotNull FileTypeConsumer consumer) { consumer.consume(ScratchFileType.INSTANCE); } } public static class Substitutor extends LanguageSubstitutor { @Nullable @Override public Language getLanguage(@NotNull VirtualFile file, @NotNull Project project) { return substituteLanguage(project, file); } @Nullable public static Language substituteLanguage(@NotNull Project project, @NotNull VirtualFile file) { RootType rootType = ScratchFileService.getInstance().getRootType(file); if (rootType == null) return null; Language language = rootType.substituteLanguage(project, file); Language adjusted = language != null ? language : getLanguageByFileName(file); return adjusted != null && adjusted != ScratchFileType.INSTANCE.getLanguage() ? LanguageSubstitutors.INSTANCE.substituteLanguage(adjusted, file, project) : adjusted; } } public static class Highlighter implements SyntaxHighlighterProvider { @Override @Nullable public SyntaxHighlighter create(@NotNull FileType fileType, @Nullable Project project, @Nullable VirtualFile file) { if (project == null || file == null || !(fileType instanceof ScratchFileType)) return null; Language language = LanguageUtil.getLanguageForPsi(project, file); return language == null ? null : SyntaxHighlighterFactory.getSyntaxHighlighter(language, project, file); } } public static class FilePresentation implements IconDescriptorUpdater, EditorTabTitleProvider { @RequiredReadAction @Override public void updateIcon(@NotNull IconDescriptor iconDescriptor, @NotNull PsiElement element, int flags) { if (element instanceof PsiFile) { VirtualFile virtualFile = ((PsiFile)element).getVirtualFile(); if (virtualFile == null) { return; } RootType rootType = ScratchFileService.getInstance().getRootType(virtualFile); if (rootType == null) return; iconDescriptor.setMainIcon(rootType.substituteIcon(element.getProject(), virtualFile)); } } @Nullable @Override public String getEditorTabTitle(@NotNull Project project, @NotNull VirtualFile file) { RootType rootType = ScratchFileService.getInstance().getRootType(file); if (rootType == null) return null; return rootType.substituteName(project, file); } } public static class AccessExtension implements NonProjectFileWritingAccessExtension { @Override public boolean isWritable(@NotNull VirtualFile file) { return file.getFileType() == ScratchFileType.INSTANCE; } } public static class NavBarExtension extends AbstractNavBarModelExtension { @Nullable @Override public String getPresentableText(Object object) { if (!(object instanceof PsiElement)) return null; Project project = ((PsiElement)object).getProject(); VirtualFile virtualFile = PsiUtilCore.getVirtualFile((PsiElement)object); if (virtualFile == null || !virtualFile.isValid()) return null; RootType rootType = ScratchFileService.getInstance().getRootType(virtualFile); if (rootType == null) return null; if (virtualFile.isDirectory() && additionalRoots(project).contains(virtualFile)) { return rootType.getDisplayName(); } return rootType.substituteName(project, virtualFile); } @NotNull @Override public Collection<VirtualFile> additionalRoots(Project project) { Set<VirtualFile> result = ContainerUtil.newLinkedHashSet(); LocalFileSystem fileSystem = LocalFileSystem.getInstance(); ScratchFileService app = ScratchFileService.getInstance(); for (RootType r : RootType.getAllRootIds()) { ContainerUtil.addIfNotNull(result, fileSystem.findFileByPath(app.getRootPath(r))); } return result; } } @Override public VirtualFile findFile(@NotNull RootType rootType, @NotNull String pathName, @NotNull Option option) throws IOException { ApplicationManager.getApplication().assertReadAccessAllowed(); String fullPath = getRootPath(rootType) + "/" + pathName; if (option != Option.create_new_always) { VirtualFile file = LocalFileSystem.getInstance().findFileByPath(fullPath); if (file != null && !file.isDirectory()) return file; if (option == Option.existing_only) return null; } String ext = PathUtil.getFileExtension(pathName); String fileNameExt = PathUtil.getFileName(pathName); String fileName = StringUtil.trimEnd(fileNameExt, ext == null ? "" : "." + ext); AccessToken token = ApplicationManager.getApplication().acquireWriteActionLock(getClass()); try { VirtualFile dir = VfsUtil.createDirectories(PathUtil.getParentPath(fullPath)); if (option == Option.create_new_always) { return VfsUtil.createChildSequent(LocalFileSystem.getInstance(), dir, fileName, StringUtil.notNullize(ext)); } else { return dir.createChildData(LocalFileSystem.getInstance(), fileNameExt); } } finally { token.finish(); } } @Nullable private static Language getLanguageByFileName(@Nullable VirtualFile file) { return file == null ? null : LanguageUtil.getFileTypeLanguage(FileTypeManager.getInstance().getFileTypeByFileName(file.getName())); } @NotNull public static GlobalSearchScope buildScratchesSearchScope() { final ScratchFileService service = ScratchFileService.getInstance(); return new GlobalSearchScope() { @NotNull @Override public String getDisplayName() { return "Scratches and Consoles"; } @Override public boolean contains(@NotNull VirtualFile file) { RootType rootType = file.getFileType() == ScratchFileType.INSTANCE ? service.getRootType(file) : null; return rootType != null && !rootType.isHidden(); } @Override public boolean isSearchOutsideRootModel() { return true; } @Override public int compare(@NotNull VirtualFile file1, @NotNull VirtualFile file2) { return 0; } @Override public boolean isSearchInModuleContent(@NotNull Module aModule) { return false; } @Override public boolean isSearchInLibraries() { return false; } @NotNull @Override public GlobalSearchScope intersectWith(@NotNull GlobalSearchScope scope) { if (scope instanceof ProjectAndLibrariesScope) return this; return super.intersectWith(scope); } @Override public String toString() { return getDisplayName(); } }; } public static class UseScopeExtension extends UseScopeEnlarger { @Nullable @Override public SearchScope getAdditionalUseScope(@NotNull PsiElement element) { SearchScope useScope = element.getUseScope(); if (useScope instanceof LocalSearchScope) return null; return buildScratchesSearchScope(); } } public static class UsageTypeExtension implements UsageTypeProvider { private static final ConcurrentFactoryMap<RootType, UsageType> ourUsageTypes = new ConcurrentFactoryMap<RootType, UsageType>() { @Nullable @Override protected UsageType create(RootType key) { return new UsageType("Usage in " + key.getDisplayName()); } }; @Nullable @Override public UsageType getUsageType(PsiElement element) { VirtualFile file = PsiUtilCore.getVirtualFile(element); RootType rootType = file != null && file.getFileType() == ScratchFileType.INSTANCE ? ScratchFileService.getInstance().getRootType(file) : null; return rootType == null ? null : ourUsageTypes.get(rootType); } } public static class IndexSetContributor extends IndexableSetContributor { @NotNull @Override public Set<VirtualFile> getAdditionalRootsToIndex() { ScratchFileService instance = ScratchFileService.getInstance(); LocalFileSystem fileSystem = LocalFileSystem.getInstance(); HashSet<VirtualFile> result = ContainerUtil.newHashSet(); for (RootType rootType : RootType.getAllRootIds()) { if (rootType.isHidden()) continue; ContainerUtil.addIfNotNull(result, fileSystem.findFileByPath(instance.getRootPath(rootType))); } return result; } @NotNull @Override public Set<VirtualFile> getAdditionalProjectRootsToIndex(@NotNull Project project) { return Collections.emptySet(); } } }