IDEA-CR-15780 do not write default context values
[idea/community.git] / platform / lang-impl / src / com / intellij / codeInsight / template / impl / TemplateContext.java
1 /*
2  * Copyright 2000-2015 JetBrains s.r.o.
3  *
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
7  *
8  * http://www.apache.org/licenses/LICENSE-2.0
9  *
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.
15  */
16 package com.intellij.codeInsight.template.impl;
17
18 import com.google.common.annotations.VisibleForTesting;
19 import com.intellij.codeInsight.template.TemplateContextType;
20 import com.intellij.util.JdomKt;
21 import com.intellij.util.containers.ContainerUtil;
22 import com.intellij.util.containers.JBIterable;
23 import gnu.trove.THashMap;
24 import org.jdom.Element;
25 import org.jetbrains.annotations.NotNull;
26 import org.jetbrains.annotations.Nullable;
27
28 import java.util.Arrays;
29 import java.util.HashMap;
30 import java.util.Map;
31 import java.util.function.Function;
32 import java.util.stream.Collectors;
33
34 public class TemplateContext {
35   private final Map<String, Boolean> myContextStates = ContainerUtil.newTroveMap();
36
37   private static class ContextInterner {
38     private static final Map<String, String> internMap = Arrays.stream(TemplateContextType.EP_NAME.getExtensions())
39       .map(TemplateContextType::getContextId)
40       .distinct()
41       .collect(Collectors.toMap(Function.identity(), Function.identity()));
42   }
43
44   public TemplateContext createCopy()  {
45     TemplateContext cloneResult = new TemplateContext();
46     cloneResult.myContextStates.putAll(myContextStates);
47     return cloneResult;
48   }
49
50   @Nullable
51   TemplateContextType getDifference(@NotNull TemplateContext defaultContext) {
52     return ContainerUtil.find(TemplateManagerImpl.getAllContextTypes(), type -> isEnabled(type) != defaultContext.isEnabled(type));
53   }
54
55   public boolean isEnabled(@NotNull TemplateContextType contextType) {
56     synchronized (myContextStates) {
57       Boolean storedValue = getOwnValue(contextType);
58       if (storedValue == null) {
59         TemplateContextType baseContextType = contextType.getBaseContextType();
60         return baseContextType != null && isEnabled(baseContextType);
61       }
62       return storedValue.booleanValue();
63     }
64   }
65
66   @Nullable
67   public Boolean getOwnValue(TemplateContextType contextType) {
68     synchronized (myContextStates) {
69       return myContextStates.get(contextType.getContextId());
70     }
71   }
72
73   public void setEnabled(TemplateContextType contextType, boolean value) {
74     synchronized (myContextStates) {
75       myContextStates.put(contextType.getContextId(), value);
76     }
77   }
78
79   // used during initialization => no sync
80   @VisibleForTesting
81   public void setDefaultContext(@NotNull TemplateContext defContext) {
82     HashMap<String, Boolean> copy = new HashMap<>(myContextStates);
83     myContextStates.clear();
84     myContextStates.putAll(defContext.myContextStates);
85     myContextStates.putAll(copy);
86   }
87
88   // used during initialization => no sync
89   @VisibleForTesting
90   public void readTemplateContext(@NotNull Element element) {
91     for (Element option : element.getChildren("option")) {
92       String name = option.getAttributeValue("name");
93       String value = option.getAttributeValue("value");
94       if (name != null && value != null) {
95         myContextStates.put(ContainerUtil.getOrElse(ContextInterner.internMap, name, name), Boolean.parseBoolean(value));
96       }
97     }
98
99     myContextStates.putAll(makeInheritanceExplicit());
100   }
101
102   /**
103    * Mark contexts explicitly as excluded which are excluded because some of their bases is explicitly marked as excluded.
104    * Otherwise that `excluded` status will be forgotten if the base context is enabled.
105    */
106   @NotNull
107   private Map<String, Boolean> makeInheritanceExplicit() {
108     Map<String, Boolean> explicitStates = ContainerUtil.newHashMap();
109     for (TemplateContextType type : ContainerUtil.filter(TemplateManagerImpl.getAllContextTypes(), this::isDisabledByInheritance)) {
110       explicitStates.put(type.getContextId(), false);
111     }
112     return explicitStates;
113   }
114
115   private boolean isDisabledByInheritance(TemplateContextType type) {
116     return !hasOwnValue(type) &&
117            !isEnabled(type) &&
118            JBIterable.generate(type, TemplateContextType::getBaseContextType).filter(this::hasOwnValue).first() != null;
119   }
120
121   private boolean hasOwnValue(TemplateContextType t) {
122     return getOwnValue(t) != null;
123   }
124
125   @VisibleForTesting
126   @Nullable
127   public Element writeTemplateContext(@Nullable TemplateContext defaultContext) {
128     if (myContextStates.isEmpty()) {
129       return null;
130     }
131
132     Map<String, TemplateContextType> idToType = new THashMap<>();
133     for (TemplateContextType type : TemplateManagerImpl.getAllContextTypes()) {
134       idToType.put(type.getContextId(), type);
135     }
136
137     Element element = new Element(TemplateSettings.CONTEXT);
138     for (Map.Entry<String, Boolean> entry : myContextStates.entrySet()) {
139       Boolean ownValue = entry.getValue();
140       if (ownValue == null) {
141         continue;
142       }
143
144       TemplateContextType type = idToType.get(entry.getKey());
145       if (type == null) {
146         // https://youtrack.jetbrains.com/issue/IDEA-155623#comment=27-1721029
147         JdomKt.addOptionTag(element, entry.getKey(), ownValue.toString());
148       }
149       else if (isNotDefault(ownValue, type, defaultContext)) {
150         JdomKt.addOptionTag(element, type.getContextId(), ownValue.toString());
151       }
152     }
153     return element;
154   }
155
156   /**
157    * Default value for GROOVY_STATEMENT is `true` (defined in the `plugins/groovy/groovy-psi/resources/liveTemplates/Groovy.xml`).
158    * Base value is `false`.
159    *
160    * If default value is defined (as in our example) — we must not take base value in account.
161    * Because on init `setDefaultContext` will be called and we will have own value.
162    * Otherwise it will be not possible to set value for `GROOVY_STATEMENT` neither to `true` (equals to default), nor to `false` (equals to base).
163    * See TemplateSchemeTest.
164    */
165   boolean isNotDefault(@NotNull Boolean ownValue, @NotNull TemplateContextType type, @Nullable TemplateContext defaultContext) {
166     Boolean defaultValue = defaultContext == null ? null : defaultContext.getOwnValue(type);
167     if (defaultValue == null) {
168       TemplateContextType base = type.getBaseContextType();
169       boolean baseEnabled = base != null && isEnabled(base);
170       return ownValue != baseEnabled;
171     }
172     return !ownValue.equals(defaultValue);
173   }
174
175   @Override
176   public String toString() {
177     return myContextStates.toString();
178   }
179 }