blob: c1a186b614c7141a3216509e98c0792ea4c4f984 [file] [log] [blame]
/*
* Copyright 2000-2009 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.lang;
import com.intellij.injected.editor.VirtualFileWindow;
import com.intellij.openapi.components.PersistentStateComponent;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.roots.impl.FilePropertyPusher;
import com.intellij.openapi.roots.impl.PushedFilePropertiesUpdater;
import com.intellij.openapi.util.Comparing;
import com.intellij.openapi.util.Key;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.openapi.vfs.VirtualFileManager;
import com.intellij.psi.PsiDocumentManager;
import com.intellij.testFramework.LightVirtualFile;
import com.intellij.util.containers.ContainerUtil;
import gnu.trove.THashMap;
import org.jdom.Element;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.annotations.TestOnly;
import java.util.*;
/**
* @author peter
*/
public abstract class LanguagePerFileMappings<T> implements PersistentStateComponent<Element>, PerFileMappings<T> {
private static final Logger LOG = Logger.getInstance("com.intellij.lang.LanguagePerFileMappings");
private final Map<VirtualFile, T> myMappings = new HashMap<VirtualFile, T>();
private final Project myProject;
public LanguagePerFileMappings(final Project project) {
myProject = project;
}
@Nullable
protected FilePropertyPusher<T> getFilePropertyPusher() {
return null;
}
@Override
public Map<VirtualFile, T> getMappings() {
synchronized (myMappings) {
cleanup();
return Collections.unmodifiableMap(myMappings);
}
}
private void cleanup() {
for (final VirtualFile file : new ArrayList<VirtualFile>(myMappings.keySet())) {
if (file != null //PROJECT, top-level
&& !file.isValid()) {
myMappings.remove(file);
}
}
}
@Override
@Nullable
public T getMapping(@Nullable VirtualFile file) {
FilePropertyPusher<T> pusher = getFilePropertyPusher();
T t = getMappingInner(file, myMappings, pusher == null? null : pusher.getFileDataKey());
return t == null? getDefaultMapping(file) : t;
}
@Nullable
protected static <T> T getMappingInner(@Nullable VirtualFile file, @Nullable Map<VirtualFile, T> mappings, @Nullable Key<T> pusherKey) {
if (file instanceof VirtualFileWindow) {
final VirtualFileWindow window = (VirtualFileWindow)file;
file = window.getDelegate();
}
VirtualFile originalFile = file instanceof LightVirtualFile ? ((LightVirtualFile)file).getOriginalFile() : null;
if (Comparing.equal(originalFile, file)) originalFile = null;
if (file != null) {
final T pushedValue = pusherKey == null? null : file.getUserData(pusherKey);
if (pushedValue != null) return pushedValue;
}
if (originalFile != null) {
final T pushedValue = pusherKey == null? null : originalFile.getUserData(pusherKey);
if (pushedValue != null) return pushedValue;
}
if (mappings == null) return null;
synchronized (mappings) {
for (VirtualFile cur = file; ; cur = cur.getParent()) {
T t = mappings.get(cur);
if (t != null) return t;
if (originalFile != null) {
t = mappings.get(originalFile);
if (t != null) return t;
originalFile = originalFile.getParent();
}
if (cur == null) break;
}
}
return null;
}
@Override
public T chosenToStored(VirtualFile file, T value) {
return value;
}
@Override
public boolean isSelectable(T value) {
return true;
}
@Override
@Nullable
public T getDefaultMapping(@Nullable final VirtualFile file) {
return null;
}
@Nullable
public T getImmediateMapping(@Nullable final VirtualFile file) {
synchronized (myMappings) {
return myMappings.get(file);
}
}
@Override
public void setMappings(final Map<VirtualFile, T> mappings) {
final Collection<VirtualFile> oldFiles;
synchronized (myMappings) {
oldFiles = new ArrayList<VirtualFile>(myMappings.keySet());
myMappings.clear();
myMappings.putAll(mappings);
cleanup();
}
handleMappingChange(mappings.keySet(), oldFiles, !getProject().isDefault());
}
public void setMapping(@Nullable final VirtualFile file, @Nullable T dialect) {
synchronized (myMappings) {
if (dialect == null) {
myMappings.remove(file);
}
else {
myMappings.put(file, dialect);
}
}
final List<VirtualFile> files = ContainerUtil.createMaybeSingletonList(file);
handleMappingChange(files, files, false);
}
private void handleMappingChange(final Collection<VirtualFile> files, Collection<VirtualFile> oldFiles, final boolean includeOpenFiles) {
final FilePropertyPusher<T> pusher = getFilePropertyPusher();
if (pusher != null) {
for (VirtualFile oldFile : oldFiles) {
if (oldFile == null) continue; // project
oldFile.putUserData(pusher.getFileDataKey(), null);
}
PushedFilePropertiesUpdater updater = PushedFilePropertiesUpdater.getInstance(myProject);
if (updater == null) {
if (!myProject.isDefault()) {
LOG.error("updater = null. project=" + myProject.getName()+", this="+getClass().getSimpleName());
}
}
else {
updater.pushAll(pusher);
}
}
if (shouldReparseFiles()) {
PsiDocumentManager.getInstance(myProject).reparseFiles(files, includeOpenFiles);
}
}
@Override
public Collection<T> getAvailableValues(VirtualFile file) {
return getAvailableValues();
}
protected abstract List<T> getAvailableValues();
@Nullable
protected abstract String serialize(T t);
@Override
public Element getState() {
synchronized (myMappings) {
cleanup();
final Element element = new Element("x");
final List<VirtualFile> files = new ArrayList<VirtualFile>(myMappings.keySet());
Collections.sort(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) {
final T dialect = myMappings.get(file);
String value = serialize(dialect);
if (value != null) {
final Element child = new Element("file");
element.addContent(child);
child.setAttribute("url", file == null ? "PROJECT" : file.getUrl());
child.setAttribute(getValueAttribute(), value);
}
}
return element;
}
}
@Nullable
protected T handleUnknownMapping(VirtualFile file, String value) {
return null;
}
@NotNull
protected String getValueAttribute() {
return "dialect";
}
@Override
public void loadState(final Element state) {
synchronized (myMappings) {
final THashMap<String, T> dialectMap = new THashMap<String, T>();
for (T dialect : getAvailableValues()) {
String key = serialize(dialect);
if (key != null) {
dialectMap.put(key, dialect);
}
}
final List<Element> files = state.getChildren("file");
for (Element fileElement : files) {
final String url = fileElement.getAttributeValue("url");
final String dialectID = fileElement.getAttributeValue(getValueAttribute());
final VirtualFile file = url.equals("PROJECT") ? null : VirtualFileManager.getInstance().findFileByUrl(url);
T dialect = dialectMap.get(dialectID);
if (dialect == null) {
dialect = handleUnknownMapping(file, dialectID);
if (dialect == null) continue;
}
if (file != null || url.equals("PROJECT")) {
myMappings.put(file, dialect);
}
}
}
}
@TestOnly
public void cleanupForNextTest() {
synchronized (myMappings) {
myMappings.clear();
}
}
protected Project getProject() {
return myProject;
}
protected boolean shouldReparseFiles() {
return true;
}
public boolean hasMappings() {
synchronized (myMappings) {
return !myMappings.isEmpty();
}
}
}