blob: 07c7eaa6ec837fd261c04ce145f28c61fa283544 [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.
*/
/*
* 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.application.impl.TransferToPooledThreadQueue;
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.editor.Document;
import com.intellij.openapi.editor.EditorFactory;
import com.intellij.openapi.editor.event.DocumentAdapter;
import com.intellij.openapi.editor.event.DocumentEvent;
import com.intellij.openapi.editor.event.EditorFactoryAdapter;
import com.intellij.openapi.editor.event.EditorFactoryEvent;
import com.intellij.openapi.fileEditor.FileDocumentManager;
import com.intellij.openapi.fileEditor.impl.LoadTextUtil;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.project.ProjectLocator;
import com.intellij.openapi.project.ProjectManager;
import com.intellij.openapi.util.Comparing;
import com.intellij.openapi.util.Computable;
import com.intellij.openapi.util.Disposer;
import com.intellij.openapi.util.Key;
import com.intellij.openapi.vfs.CharsetToolkit;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.util.Alarm;
import com.intellij.util.Processor;
import gnu.trove.Equality;
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.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.beans.PropertyChangeSupport;
import java.lang.ref.Reference;
import java.lang.ref.WeakReference;
import java.nio.charset.Charset;
import java.util.Collection;
import java.util.Set;
@State(
name = "Encoding",
storages = {
@Storage(file = StoragePathMacros.APP_CONFIG + "/encoding.xml")
}
)
public class EncodingManagerImpl extends EncodingManager implements PersistentStateComponent<Element>, Disposable {
private static final Equality<Reference<Document>> REFERENCE_EQUALITY = new Equality<Reference<Document>>() {
@Override
public boolean equals(Reference<Document> o1, Reference<Document> o2) {
Object v1 = o1 == null ? REFERENCE_EQUALITY : o1.get();
Object v2 = o2 == null ? REFERENCE_EQUALITY : o2.get();
return v1 == v2;
}
};
private final PropertyChangeSupport myPropertyChangeSupport = new PropertyChangeSupport(this);
private Charset myDefaultEncoding = CharsetToolkit.UTF8_CHARSET;
private final Alarm updateEncodingFromContent = new Alarm(Alarm.ThreadToUse.POOLED_THREAD, this);
private static final Key<Charset> CACHED_CHARSET_FROM_CONTENT = Key.create("CACHED_CHARSET_FROM_CONTENT");
private final TransferToPooledThreadQueue<Reference<Document>> myChangedDocuments = new TransferToPooledThreadQueue<Reference<Document>>(
"Encoding detection thread",
ApplicationManager.getApplication().getDisposed(),
-1, // drain the whole queue, do not reschedule
new Processor<Reference<Document>>() {
@Override
public boolean process(Reference<Document> ref) {
Document document = ref.get();
if (document == null) return true; // document gced, don't bother
handleDocument(document);
return true;
}
});
public EncodingManagerImpl(@NotNull EditorFactory editorFactory) {
editorFactory.getEventMulticaster().addDocumentListener(new DocumentAdapter() {
@Override
public void documentChanged(DocumentEvent e) {
queueUpdateEncodingFromContent(e.getDocument());
}
}, this);
editorFactory.addEditorFactoryListener(new EditorFactoryAdapter() {
@Override
public void editorCreated(@NotNull EditorFactoryEvent event) {
queueUpdateEncodingFromContent(event.getEditor().getDocument());
}
}, this);
}
@NonNls public static final String PROP_CACHED_ENCODING_CHANGED = "cachedEncoding";
private void handleDocument(@NotNull final Document document) {
VirtualFile virtualFile = FileDocumentManager.getInstance().getFile(document);
if (virtualFile == null) return;
Project project = guessProject(virtualFile);
if (project != null && project.isDisposed()) return;
Charset charset = LoadTextUtil.charsetFromContentOrNull(project, virtualFile, document.getImmutableCharSequence());
Charset oldCached = getCachedCharsetFromContent(document);
if (!Comparing.equal(charset, oldCached)) {
setCachedCharsetFromContent(charset, oldCached, document);
}
}
private void setCachedCharsetFromContent(Charset charset, Charset oldCached, @NotNull Document document) {
document.putUserData(CACHED_CHARSET_FROM_CONTENT, charset);
firePropertyChange(document, PROP_CACHED_ENCODING_CHANGED, oldCached, charset);
}
@Nullable("returns null if charset set cannot be determined from content")
public Charset computeCharsetFromContent(@NotNull final VirtualFile virtualFile) {
final Document document = FileDocumentManager.getInstance().getDocument(virtualFile);
if (document == null) return null;
final Charset cached = EncodingManager.getInstance().getCachedCharsetFromContent(document);
if (cached != null) return cached;
final Project project = ProjectLocator.getInstance().guessProjectForFile(virtualFile);
return ApplicationManager.getApplication().runReadAction(new Computable<Charset>() {
@Override
public Charset compute() {
Charset charsetFromContent = LoadTextUtil.charsetFromContentOrNull(project, virtualFile, document.getImmutableCharSequence());
if (charsetFromContent != null) {
setCachedCharsetFromContent(charsetFromContent, cached, document);
}
return charsetFromContent;
}
});
}
@Override
public void dispose() {
updateEncodingFromContent.cancelAllRequests();
clearDocumentQueue();
}
public void queueUpdateEncodingFromContent(@NotNull Document document) {
myChangedDocuments.offerIfAbsent(new WeakReference<Document>(document), REFERENCE_EQUALITY);
}
@Override
@Nullable
public Charset getCachedCharsetFromContent(@NotNull Document document) {
return document.getUserData(CACHED_CHARSET_FROM_CONTENT);
}
@NonNls private static final String DEFAULT_ENCODING_TAG = "default_encoding";
@Override
public Element getState() {
Element result = new Element("x");
result.setAttribute(DEFAULT_ENCODING_TAG, getDefaultCharsetName());
return result;
}
@Override
public void loadState(final Element state) {
String name = state.getAttributeValue(DEFAULT_ENCODING_TAG);
setDefaultCharsetName(name);
}
@Override
@NotNull
public Collection<Charset> getFavorites() {
Set<Charset> result = new THashSet<Charset>();
Project[] projects = ProjectManager.getInstance().getOpenProjects();
for (Project project : projects) {
result.addAll(EncodingProjectManager.getInstance(project).getFavorites());
}
return result;
}
@Override
@Nullable
public Charset getEncoding(@Nullable VirtualFile virtualFile, boolean useParentDefaults) {
Project project = guessProject(virtualFile);
if (project == null) return null;
EncodingProjectManager encodingManager = EncodingProjectManager.getInstance(project);
if (encodingManager == null) return null; //tests
return encodingManager.getEncoding(virtualFile, useParentDefaults);
}
public void clearDocumentQueue() {
myChangedDocuments.stop();
}
@Nullable
private static Project guessProject(final VirtualFile virtualFile) {
return ProjectLocator.getInstance().guessProjectForFile(virtualFile);
}
@Override
public void setEncoding(@Nullable VirtualFile virtualFileOrDir, @Nullable Charset charset) {
Project project = guessProject(virtualFileOrDir);
EncodingProjectManager.getInstance(project).setEncoding(virtualFileOrDir, charset);
}
@Override
public boolean isUseUTFGuessing(final VirtualFile virtualFile) {
return true;
}
@Override
public void setUseUTFGuessing(final VirtualFile virtualFile, final boolean useUTFGuessing) {
}
@Override
public boolean isNative2Ascii(@NotNull final VirtualFile virtualFile) {
Project project = guessProject(virtualFile);
return project != null && EncodingProjectManager.getInstance(project).isNative2Ascii(virtualFile);
}
@Override
public boolean isNative2AsciiForPropertiesFiles() {
Project project = guessProject(null);
return project != null && EncodingProjectManager.getInstance(project).isNative2AsciiForPropertiesFiles();
}
@Override
public void setNative2AsciiForPropertiesFiles(final VirtualFile virtualFile, final boolean native2Ascii) {
Project project = guessProject(virtualFile);
if (project == null) return;
EncodingProjectManager.getInstance(project).setNative2AsciiForPropertiesFiles(virtualFile, native2Ascii);
}
@Override
@NotNull
public Charset getDefaultCharset() {
return myDefaultEncoding == ChooseFileEncodingAction.NO_ENCODING ? CharsetToolkit.getDefaultSystemCharset() : myDefaultEncoding;
}
@Override
@NotNull
public String getDefaultCharsetName() {
return myDefaultEncoding == ChooseFileEncodingAction.NO_ENCODING ? "" : myDefaultEncoding.name();
}
@Override
public void setDefaultCharsetName(@NotNull String name) {
if (name.isEmpty()) {
myDefaultEncoding = ChooseFileEncodingAction.NO_ENCODING;
return;
}
myDefaultEncoding = CharsetToolkit.forName(name);
if (myDefaultEncoding == null) myDefaultEncoding = CharsetToolkit.getDefaultSystemCharset();
if (myDefaultEncoding == null) myDefaultEncoding = CharsetToolkit.UTF8_CHARSET;
}
@Override
@Nullable
public Charset getDefaultCharsetForPropertiesFiles(@Nullable final VirtualFile virtualFile) {
Project project = guessProject(virtualFile);
if (project == null) return null;
return EncodingProjectManager.getInstance(project).getDefaultCharsetForPropertiesFiles(virtualFile);
}
@Override
public void setDefaultCharsetForPropertiesFiles(@Nullable final VirtualFile virtualFile, final Charset charset) {
Project project = guessProject(virtualFile);
if (project == null) return;
EncodingProjectManager.getInstance(project).setDefaultCharsetForPropertiesFiles(virtualFile, charset);
}
@Override
public void addPropertyChangeListener(@NotNull PropertyChangeListener listener){
myPropertyChangeSupport.addPropertyChangeListener(listener);
}
@Override
public void addPropertyChangeListener(@NotNull final PropertyChangeListener listener, @NotNull Disposable parentDisposable) {
myPropertyChangeSupport.addPropertyChangeListener(listener);
Disposer.register(parentDisposable, new Disposable() {
@Override
public void dispose() {
removePropertyChangeListener(listener);
}
});
}
@Override
public void removePropertyChangeListener(@NotNull PropertyChangeListener listener){
myPropertyChangeSupport.removePropertyChangeListener(listener);
}
void firePropertyChange(@Nullable Document document, @NotNull String propertyName, final Object oldValue, final Object newValue) {
Object source = document == null ? this : document;
myPropertyChangeSupport.firePropertyChange(new PropertyChangeEvent(source, propertyName, oldValue, newValue));
}
}