| /* |
| * 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. |
| */ |
| |
| /* |
| * Created by IntelliJ IDEA. |
| * User: cdr |
| * Date: Jul 17, 2007 |
| * Time: 3:20:51 PM |
| */ |
| package com.intellij.openapi.vfs.encoding; |
| |
| import com.intellij.openapi.Disposable; |
| import com.intellij.openapi.application.ApplicationManager; |
| import com.intellij.openapi.components.*; |
| import com.intellij.openapi.editor.Document; |
| import com.intellij.openapi.fileEditor.FileDocumentManager; |
| import com.intellij.openapi.fileTypes.StdFileTypes; |
| import com.intellij.openapi.progress.ProgressManager; |
| import com.intellij.openapi.project.Project; |
| import com.intellij.openapi.roots.ProjectFileIndex; |
| import com.intellij.openapi.roots.ProjectRootManager; |
| import com.intellij.openapi.util.Comparing; |
| import com.intellij.openapi.util.ModificationTracker; |
| import com.intellij.openapi.util.SimpleModificationTracker; |
| import com.intellij.openapi.vfs.*; |
| import com.intellij.openapi.vfs.newvfs.impl.VirtualFileSystemEntry; |
| import com.intellij.psi.PsiDocumentManager; |
| import com.intellij.psi.PsiFile; |
| import com.intellij.util.Processor; |
| import com.intellij.util.containers.ContainerUtil; |
| import com.intellij.util.ui.UIUtil; |
| import gnu.trove.THashSet; |
| import org.jdom.Element; |
| import org.jetbrains.annotations.NonNls; |
| import org.jetbrains.annotations.NotNull; |
| import org.jetbrains.annotations.Nullable; |
| |
| import java.beans.PropertyChangeListener; |
| import java.io.IOException; |
| import java.nio.charset.Charset; |
| import java.util.*; |
| |
| @State( |
| name = "Encoding", |
| storages = { |
| @Storage(file = StoragePathMacros.PROJECT_FILE), |
| @Storage(file = StoragePathMacros.PROJECT_CONFIG_DIR + "/encodings.xml", scheme = StorageScheme.DIRECTORY_BASED) |
| } |
| ) |
| public class EncodingProjectManagerImpl extends EncodingProjectManager implements NamedComponent, PersistentStateComponent<Element> { |
| @NonNls private static final String PROJECT_URL = "PROJECT"; |
| private final Project myProject; |
| private boolean myNative2AsciiForPropertiesFiles; |
| private Charset myDefaultCharsetForPropertiesFiles; |
| private final SimpleModificationTracker myModificationTracker = new SimpleModificationTracker(); |
| |
| public EncodingProjectManagerImpl(Project project, PsiDocumentManager documentManager) { |
| myProject = project; |
| documentManager.addListener(new PsiDocumentManager.Listener() { |
| @Override |
| public void documentCreated(@NotNull Document document, PsiFile psiFile) { |
| ((EncodingManagerImpl)EncodingManager.getInstance()).queueUpdateEncodingFromContent(document); |
| } |
| |
| @Override |
| public void fileCreated(@NotNull PsiFile file, @NotNull Document document) { |
| } |
| }); |
| } |
| |
| //null key means project |
| private final Map<VirtualFile, Charset> myMapping = new HashMap<VirtualFile, Charset>(); |
| |
| @Override |
| public Element getState() { |
| Element element = new Element("x"); |
| List<VirtualFile> files = new ArrayList<VirtualFile>(myMapping.keySet()); |
| ContainerUtil.quickSort(files, new Comparator<VirtualFile>() { |
| @Override |
| public int compare(final VirtualFile o1, final VirtualFile o2) { |
| if (o1 == null || o2 == null) return o1 == null ? o2 == null ? 0 : 1 : -1; |
| return o1.getPath().compareTo(o2.getPath()); |
| } |
| }); |
| for (VirtualFile file : files) { |
| Charset charset = myMapping.get(file); |
| Element child = new Element("file"); |
| element.addContent(child); |
| child.setAttribute("url", file == null ? PROJECT_URL : file.getUrl()); |
| child.setAttribute("charset", charset.name()); |
| } |
| element.setAttribute("useUTFGuessing", Boolean.toString(true)); |
| element.setAttribute("native2AsciiForPropertiesFiles", Boolean.toString(myNative2AsciiForPropertiesFiles)); |
| if (myDefaultCharsetForPropertiesFiles != null) { |
| element.setAttribute("defaultCharsetForPropertiesFiles", myDefaultCharsetForPropertiesFiles.name()); |
| } |
| return element; |
| } |
| |
| @Override |
| public void loadState(Element element) { |
| List<Element> files = element.getChildren("file"); |
| final Map<VirtualFile, Charset> mapping = new HashMap<VirtualFile, Charset>(); |
| for (Element fileElement : files) { |
| String url = fileElement.getAttributeValue("url"); |
| String charsetName = fileElement.getAttributeValue("charset"); |
| Charset charset = CharsetToolkit.forName(charsetName); |
| if (charset == null) continue; |
| VirtualFile file = url.equals(PROJECT_URL) ? null : VirtualFileManager.getInstance().findFileByUrl(url); |
| if (file != null || url.equals(PROJECT_URL)) { |
| mapping.put(file, charset); |
| } |
| } |
| myMapping.clear(); |
| myMapping.putAll(mapping); |
| |
| myNative2AsciiForPropertiesFiles = Boolean.parseBoolean(element.getAttributeValue("native2AsciiForPropertiesFiles")); |
| myDefaultCharsetForPropertiesFiles = CharsetToolkit.forName(element.getAttributeValue("defaultCharsetForPropertiesFiles")); |
| |
| myModificationTracker.incModificationCount(); |
| } |
| |
| @Override |
| @NonNls |
| @NotNull |
| public String getComponentName() { |
| return "EncodingProjectManager"; |
| } |
| |
| @Override |
| @Nullable |
| public Charset getEncoding(@Nullable VirtualFile virtualFile, boolean useParentDefaults) { |
| VirtualFile parent = virtualFile; |
| while (true) { |
| Charset charset = myMapping.get(parent); |
| if (charset != null || !useParentDefaults) return charset; |
| if (parent == null) break; |
| parent = parent.getParent(); |
| } |
| return null; |
| } |
| |
| @NotNull |
| public ModificationTracker getModificationTracker() { |
| return myModificationTracker; |
| } |
| |
| @Override |
| public void setEncoding(@Nullable final VirtualFile virtualFileOrDir, @Nullable final Charset charset) { |
| Charset oldCharset; |
| |
| if (charset == null) { |
| oldCharset = myMapping.remove(virtualFileOrDir); |
| } |
| else { |
| oldCharset = myMapping.put(virtualFileOrDir, charset); |
| } |
| |
| if (!Comparing.equal(oldCharset, charset)) { |
| myModificationTracker.incModificationCount(); |
| if (virtualFileOrDir != null) { |
| virtualFileOrDir.setCharset(virtualFileOrDir.getBOM() == null ? charset : null); |
| } |
| reloadAllFilesUnder(virtualFileOrDir); |
| } |
| } |
| |
| private static void clearAndReload(@NotNull VirtualFile virtualFileOrDir) { |
| virtualFileOrDir.setCharset(null); |
| reload(virtualFileOrDir); |
| } |
| |
| private static void reload(@NotNull VirtualFile virtualFile) { |
| FileDocumentManager documentManager = FileDocumentManager.getInstance(); |
| ((VirtualFileListener)documentManager) |
| .contentsChanged(new VirtualFileEvent(null, virtualFile, virtualFile.getName(), virtualFile.getParent())); |
| } |
| |
| @Override |
| @NotNull |
| public Collection<Charset> getFavorites() { |
| Set<Charset> result = new HashSet<Charset>(); |
| result.addAll(myMapping.values()); |
| result.add(CharsetToolkit.UTF8_CHARSET); |
| result.add(CharsetToolkit.getDefaultSystemCharset()); |
| result.add(CharsetToolkit.UTF_16_CHARSET); |
| result.add(CharsetToolkit.forName("ISO-8859-1")); |
| result.add(CharsetToolkit.forName("US-ASCII")); |
| result.add(EncodingManager.getInstance().getDefaultCharset()); |
| result.add(EncodingManager.getInstance().getDefaultCharsetForPropertiesFiles(null)); |
| |
| result.remove(null); |
| return result; |
| } |
| |
| @NotNull |
| @Override |
| public Map<VirtualFile, Charset> getAllMappings() { |
| return myMapping; |
| } |
| |
| @Override |
| public void setMapping(@NotNull final Map<VirtualFile, Charset> mapping) { |
| ApplicationManager.getApplication().assertIsDispatchThread(); |
| FileDocumentManager.getInstance().saveAllDocuments(); // consider all files as unmodified |
| final Map<VirtualFile, Charset> newMap = new HashMap<VirtualFile, Charset>(mapping.size()); |
| final Map<VirtualFile, Charset> oldMap = new HashMap<VirtualFile, Charset>(myMapping); |
| |
| // ChangeFileEncodingAction should not start progress "reload files..." |
| suppressReloadDuring(new Runnable() { |
| @Override |
| public void run() { |
| ProjectFileIndex fileIndex = ProjectRootManager.getInstance(myProject).getFileIndex(); |
| for (Map.Entry<VirtualFile, Charset> entry : mapping.entrySet()) { |
| VirtualFile virtualFile = entry.getKey(); |
| Charset charset = entry.getValue(); |
| if (charset == null) throw new IllegalArgumentException("Null charset for " + virtualFile + "; mapping: " + mapping); |
| if (virtualFile != null) { |
| if (!fileIndex.isInContent(virtualFile)) continue; |
| if (!virtualFile.isDirectory() && !Comparing.equal(charset, oldMap.get(virtualFile))) { |
| Document document; |
| byte[] bytes; |
| try { |
| document = FileDocumentManager.getInstance().getDocument(virtualFile); |
| if (document == null) throw new IOException(); |
| bytes = virtualFile.contentsToByteArray(); |
| } |
| catch (IOException e) { |
| continue; |
| } |
| // ask whether to reload/convert when in doubt |
| boolean changed = new ChangeFileEncodingAction().chosen(document, null, virtualFile, bytes, charset); |
| |
| if (!changed) continue; |
| } |
| } |
| newMap.put(virtualFile, charset); |
| } |
| } |
| }); |
| |
| myMapping.clear(); |
| myMapping.putAll(newMap); |
| |
| final Set<VirtualFile> changed = new HashSet<VirtualFile>(oldMap.keySet()); |
| for (VirtualFile newFile : newMap.keySet()) { |
| if (Comparing.equal(oldMap.get(newFile), newMap.get(newFile))) changed.remove(newFile); |
| } |
| |
| Set<VirtualFile> added = new HashSet<VirtualFile>(newMap.keySet()); |
| added.removeAll(oldMap.keySet()); |
| |
| Set<VirtualFile> removed = new HashSet<VirtualFile>(oldMap.keySet()); |
| removed.removeAll(newMap.keySet()); |
| |
| changed.addAll(added); |
| changed.addAll(removed); |
| changed.remove(null); |
| |
| if (!changed.isEmpty()) { |
| final Processor<VirtualFile> reloadProcessor = createChangeCharsetProcessor(); |
| tryStartReloadWithProgress(new Runnable() { |
| @Override |
| public void run() { |
| Set<VirtualFile> processed = new THashSet<VirtualFile>(); |
| next: |
| for (VirtualFile changedFile : changed) { |
| for (VirtualFile processedFile : processed) { |
| if (VfsUtilCore.isAncestor(processedFile, changedFile, false)) continue next; |
| } |
| processSubFiles(changedFile, reloadProcessor); |
| processed.add(changedFile); |
| } |
| } |
| }); |
| } |
| |
| myModificationTracker.incModificationCount(); |
| } |
| |
| private static Processor<VirtualFile> createChangeCharsetProcessor() { |
| return new Processor<VirtualFile>() { |
| @Override |
| public boolean process(final VirtualFile file) { |
| if (!(file instanceof VirtualFileSystemEntry)) return false; |
| Document cachedDocument = FileDocumentManager.getInstance().getCachedDocument(file); |
| if (cachedDocument == null) return true; |
| ProgressManager.progress("Reloading files...", file.getPresentableUrl()); |
| UIUtil.invokeLaterIfNeeded(new Runnable() { |
| @Override |
| public void run() { |
| clearAndReload(file); |
| } |
| }); |
| return true; |
| } |
| }; |
| } |
| |
| private boolean processSubFiles(@Nullable("null means in the project") VirtualFile file, @NotNull final Processor<VirtualFile> processor) { |
| if (file == null) { |
| for (VirtualFile virtualFile : ProjectRootManager.getInstance(myProject).getContentRoots()) { |
| if (!processSubFiles(virtualFile, processor)) return false; |
| } |
| return true; |
| } |
| |
| return VirtualFileVisitor.CONTINUE == VfsUtilCore.visitChildrenRecursively(file, new VirtualFileVisitor() { |
| @Override |
| public boolean visitFile(@NotNull final VirtualFile file) { |
| return processor.process(file); |
| } |
| }); |
| } |
| |
| //retrieves encoding for the Project node |
| @Override |
| @Nullable |
| public Charset getDefaultCharset() { |
| Charset charset = getEncoding(null, false); |
| return charset == null ? EncodingManager.getInstance().getDefaultCharset() : charset; |
| } |
| |
| @Override |
| public boolean isUseUTFGuessing(final VirtualFile virtualFile) { |
| return true; |
| } |
| |
| @Override |
| public void setUseUTFGuessing(final VirtualFile virtualFile, final boolean useUTFGuessing) { |
| } |
| |
| private static final ThreadLocal<Boolean> SUPPRESS_RELOAD = new ThreadLocal<Boolean>(); |
| static void suppressReloadDuring(@NotNull Runnable action) { |
| Boolean old = SUPPRESS_RELOAD.get(); |
| try { |
| SUPPRESS_RELOAD.set(true); |
| action.run(); |
| } |
| finally { |
| SUPPRESS_RELOAD.set(old); |
| } |
| } |
| |
| private boolean tryStartReloadWithProgress(@NotNull final Runnable reloadAction) { |
| Boolean suppress = SUPPRESS_RELOAD.get(); |
| if (suppress == Boolean.TRUE) return false; |
| FileDocumentManager.getInstance().saveAllDocuments(); // consider all files as unmodified |
| return ProgressManager.getInstance().runProcessWithProgressSynchronously(new Runnable() { |
| @Override |
| public void run() { |
| suppressReloadDuring(reloadAction); |
| } |
| }, "Reload Files", false, myProject); |
| } |
| |
| private void reloadAllFilesUnder(final VirtualFile root) { |
| tryStartReloadWithProgress(new Runnable() { |
| @Override |
| public void run() { |
| processSubFiles(root, new Processor<VirtualFile>() { |
| @Override |
| public boolean process(final VirtualFile file) { |
| if (!(file instanceof VirtualFileSystemEntry)) return true; |
| Document cachedDocument = FileDocumentManager.getInstance().getCachedDocument(file); |
| if (cachedDocument != null) { |
| ProgressManager.progress("Reloading file...", file.getPresentableUrl()); |
| UIUtil.invokeLaterIfNeeded(new Runnable() { |
| @Override |
| public void run() { |
| reload(file); |
| } |
| }); |
| } |
| // for not loaded files deep under project, reset encoding to give them chance re-detect the right one later |
| else if (file.isCharsetSet() && !file.equals(root)) { |
| file.setCharset(null); |
| } |
| return true; |
| } |
| }); |
| } |
| }); |
| } |
| |
| @Override |
| public boolean isNative2Ascii(@NotNull final VirtualFile virtualFile) { |
| return virtualFile.getFileType() == StdFileTypes.PROPERTIES && myNative2AsciiForPropertiesFiles; |
| } |
| |
| @Override |
| public boolean isNative2AsciiForPropertiesFiles() { |
| return myNative2AsciiForPropertiesFiles; |
| } |
| |
| @Override |
| public void setNative2AsciiForPropertiesFiles(final VirtualFile virtualFile, final boolean native2Ascii) { |
| if (myNative2AsciiForPropertiesFiles != native2Ascii) { |
| myNative2AsciiForPropertiesFiles = native2Ascii; |
| ((EncodingManagerImpl)EncodingManager.getInstance()).firePropertyChange(null, PROP_NATIVE2ASCII_SWITCH, !native2Ascii, native2Ascii); |
| } |
| } |
| |
| @Override |
| @Nullable |
| public Charset getDefaultCharsetForPropertiesFiles(@Nullable final VirtualFile virtualFile) { |
| return myDefaultCharsetForPropertiesFiles; |
| } |
| |
| @Override |
| public void setDefaultCharsetForPropertiesFiles(@Nullable final VirtualFile virtualFile, @Nullable Charset charset) { |
| Charset old = myDefaultCharsetForPropertiesFiles; |
| if (!Comparing.equal(old, charset)) { |
| myDefaultCharsetForPropertiesFiles = charset; |
| ((EncodingManagerImpl)EncodingManager.getInstance()).firePropertyChange(null, PROP_PROPERTIES_FILES_ENCODING, old, charset); |
| } |
| } |
| |
| @Override |
| public void addPropertyChangeListener(@NotNull PropertyChangeListener listener){ |
| EncodingManager.getInstance().addPropertyChangeListener(listener); |
| } |
| |
| @Override |
| public void addPropertyChangeListener(@NotNull PropertyChangeListener listener, @NotNull Disposable parentDisposable) { |
| EncodingManager.getInstance().addPropertyChangeListener(listener,parentDisposable); |
| } |
| |
| @Override |
| public void removePropertyChangeListener(@NotNull PropertyChangeListener listener){ |
| EncodingManager.getInstance().removePropertyChangeListener(listener); |
| } |
| |
| @Override |
| @Nullable |
| public Charset getCachedCharsetFromContent(@NotNull Document document) { |
| return EncodingManager.getInstance().getCachedCharsetFromContent(document); |
| } |
| } |