[duplicates] enable duplicates analysis in PyCharm/WebStorm/PhpStorm/RubyMine
[idea/community.git] / platform / core-api / src / com / intellij / openapi / editor / colors / TextAttributesKey.java
1 // Copyright 2000-2017 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
2 package com.intellij.openapi.editor.colors;
3
4 import com.intellij.openapi.components.ServiceManager;
5 import com.intellij.openapi.diagnostic.Logger;
6 import com.intellij.openapi.editor.markup.TextAttributes;
7 import com.intellij.openapi.util.Comparing;
8 import com.intellij.openapi.util.JDOMExternalizerUtil;
9 import com.intellij.openapi.util.NullableLazyValue;
10 import com.intellij.openapi.util.VolatileNullableLazyValue;
11 import com.intellij.openapi.util.text.StringUtil;
12 import com.intellij.util.ObjectUtils;
13 import com.intellij.util.containers.JBIterable;
14 import gnu.trove.THashSet;
15 import org.jdom.Element;
16 import org.jetbrains.annotations.NonNls;
17 import org.jetbrains.annotations.NotNull;
18 import org.jetbrains.annotations.Nullable;
19 import org.jetbrains.annotations.TestOnly;
20
21 import java.util.Set;
22 import java.util.concurrent.ConcurrentHashMap;
23 import java.util.concurrent.ConcurrentMap;
24
25
26 /**
27  * A type of item with a distinct highlighting in an editor or in other views.
28  */
29 public final class TextAttributesKey implements Comparable<TextAttributesKey> {
30   private static final Logger LOG = Logger.getInstance(TextAttributesKey.class);
31   private static final String TEMP_PREFIX = "TEMP::";
32   private static final TextAttributes NULL_ATTRIBUTES = new TextAttributes();
33
34   private static final ConcurrentMap<String, TextAttributesKey> ourRegistry = new ConcurrentHashMap<>();
35
36   private static final NullableLazyValue<TextAttributeKeyDefaultsProvider> ourDefaultsProvider =
37     new VolatileNullableLazyValue<TextAttributeKeyDefaultsProvider>() {
38       @Nullable
39       @Override
40       protected TextAttributeKeyDefaultsProvider compute() {
41         return ServiceManager.getService(TextAttributeKeyDefaultsProvider.class);
42       }
43     };
44
45   private final String myExternalName;
46   private final TextAttributes myDefaultAttributes;
47   private final TextAttributesKey myFallbackAttributeKey;
48
49   private TextAttributesKey(@NotNull String externalName, TextAttributes defaultAttributes, TextAttributesKey fallbackAttributeKey) {
50     myExternalName = externalName;
51
52     myDefaultAttributes = defaultAttributes;
53     myFallbackAttributeKey = fallbackAttributeKey;
54
55     if (fallbackAttributeKey != null) {
56       JBIterable<TextAttributesKey> it = JBIterable.generate(myFallbackAttributeKey, o -> o == this ? null : o.myFallbackAttributeKey);
57       if (equals(it.find(o -> equals(o)))) {
58         throw new IllegalArgumentException("Can't use this fallback key: "+fallbackAttributeKey+": Cycle detected: " + StringUtil.join(it, "->"));
59       }
60     }
61   }
62
63   //read external only
64   public TextAttributesKey(@NotNull Element element) {
65     String name = JDOMExternalizerUtil.readField(element, "myExternalName");
66
67     Element myDefaultAttributesElement = JDOMExternalizerUtil.readOption(element, "myDefaultAttributes");
68     TextAttributes defaultAttributes = myDefaultAttributesElement == null ? null : new TextAttributes(myDefaultAttributesElement);
69     myExternalName = name;
70     myDefaultAttributes = defaultAttributes;
71     myFallbackAttributeKey = null;
72   }
73
74   @NotNull
75   public static TextAttributesKey find(@NotNull @NonNls String externalName) {
76     return ourRegistry.computeIfAbsent(externalName, name -> new TextAttributesKey(name, null, null));
77   }
78
79   @Override
80   public String toString() {
81     return myExternalName;
82   }
83
84   @NotNull
85   public String getExternalName() {
86     return myExternalName;
87   }
88
89   @Override
90   public int compareTo(@NotNull TextAttributesKey key) {
91     return myExternalName.compareTo(key.myExternalName);
92   }
93
94   /**
95    * Registers a text attribute key with the specified identifier.
96    *
97    * @param externalName the unique identifier of the key.
98    * @return the new key instance, or an existing instance if the key with the same
99    * identifier was already registered.
100    */
101   @NotNull
102   public static TextAttributesKey createTextAttributesKey(@NonNls @NotNull String externalName) {
103     return find(externalName);
104   }
105
106   public void writeExternal(Element element) {
107     JDOMExternalizerUtil.writeField(element, "myExternalName", myExternalName);
108
109     if (myDefaultAttributes != null) {
110       Element option = JDOMExternalizerUtil.writeOption(element, "myDefaultAttributes");
111       myDefaultAttributes.writeExternal(option);
112     }
113   }
114
115   @Override
116   public boolean equals(final Object o) {
117     if (this == o) return true;
118     if (o == null || getClass() != o.getClass()) return false;
119
120     final TextAttributesKey that = (TextAttributesKey)o;
121
122     return myExternalName.equals(that.myExternalName);
123   }
124
125   @Override
126   public int hashCode() {
127     return myExternalName.hashCode();
128   }
129
130   // can't use RecursionManager unfortunately because quite a few crazy tests would start screaming about prevented recursive access
131   private static final ThreadLocal<Set<String>> CALLED_RECURSIVELY = ThreadLocal.withInitial(()->new THashSet<>());
132   /**
133    * Returns the default text attributes associated with the key.
134    *
135    * @return the text attributes.
136    */
137   public TextAttributes getDefaultAttributes() {
138     TextAttributes defaultAttributes = myDefaultAttributes;
139     if (defaultAttributes == null) {
140       final TextAttributeKeyDefaultsProvider provider = ourDefaultsProvider.getValue();
141       if (provider != null) {
142         Set<String> called = CALLED_RECURSIVELY.get();
143         if (!called.add(myExternalName)) return null;
144         try {
145           return ObjectUtils.notNull(provider.getDefaultAttributes(this), NULL_ATTRIBUTES);
146         }
147         finally {
148           called.remove(myExternalName);
149         }
150       }
151     }
152     return defaultAttributes;
153   }
154
155   /**
156    * Registers a text attribute key with the specified identifier and default attributes.
157    *
158    * @param externalName      the unique identifier of the key.
159    * @param defaultAttributes the default text attributes associated with the key.
160    * @return the new key instance, or an existing instance if the key with the same
161    * identifier was already registered.
162    * @deprecated Use {@link #createTextAttributesKey(String, TextAttributesKey)} to guarantee compatibility with generic color schemes.
163    */
164   @NotNull
165   @Deprecated
166   public static TextAttributesKey createTextAttributesKey(@NonNls @NotNull String externalName, TextAttributes defaultAttributes) {
167     return getOrCreate(externalName, defaultAttributes, null);
168   }
169
170   /**
171    * Registers a text attribute key with the specified identifier and a fallback key. If text attributes for the key are not defined in
172    * a color scheme, they will be acquired by the fallback key if possible.
173    * <p>Fallback keys can be chained, for example, text attribute key
174    * A can depend on key B which in turn can depend on key C. So if text attributes neither for A nor for B are found, they will be
175    * acquired by the key C.
176    * <p>Fallback keys can be used from any place including language's own definitions. Note that there is a common set of keys called
177    * {@code DefaultLanguageHighlighterColors} which can be used as a base. Scheme designers are supposed to set colors for these
178    * keys primarily and using them guarantees that most (if not all) text attributes will be shown correctly for the language
179    * regardless of a color scheme.
180    *
181    * @param externalName         the unique identifier of the key.
182    * @param fallbackAttributeKey the fallback key to use if text attributes for this key are not defined.
183    * @return the new key instance, or an existing instance if the key with the same
184    * identifier was already registered.
185    */
186   @NotNull
187   public static TextAttributesKey createTextAttributesKey(@NonNls @NotNull String externalName, TextAttributesKey fallbackAttributeKey) {
188     return getOrCreate(externalName, null, fallbackAttributeKey);
189   }
190
191   @NotNull
192   private static TextAttributesKey getOrCreate(@NotNull @NonNls String externalName,
193                                                TextAttributes defaultAttributes,
194                                                TextAttributesKey fallbackAttributeKey) {
195     TextAttributesKey existing = ourRegistry.get(externalName);
196     if (existing != null
197         && (defaultAttributes == null || Comparing.equal(existing.myDefaultAttributes, defaultAttributes))
198         && (fallbackAttributeKey == null || Comparing.equal(existing.myFallbackAttributeKey, fallbackAttributeKey))) {
199       return existing;
200     }
201     return ourRegistry.compute(externalName, (oldName, oldKey) -> mergeKeys(oldName, oldKey, defaultAttributes, fallbackAttributeKey));
202   }
203
204   @NotNull
205   private static TextAttributesKey mergeKeys(@NonNls @NotNull String externalName,
206                                              @Nullable TextAttributesKey oldKey,
207                                              TextAttributes defaultAttributes,
208                                              TextAttributesKey fallbackAttributeKey) {
209     if (oldKey == null) return new TextAttributesKey(externalName, defaultAttributes, fallbackAttributeKey);
210     // ouch. Someone's re-creating already existing key with different attributes.
211     // Have to re-create the new one with correct attributes, re-insert to the map
212
213     // but don't allow to rewrite not-null fallback key
214     if (oldKey.myFallbackAttributeKey != null && !oldKey.myFallbackAttributeKey.equals(fallbackAttributeKey)) {
215       LOG.error(new IllegalStateException("TextAttributeKey(name:'" + externalName + "', fallbackAttributeKey:'" + fallbackAttributeKey + "') " +
216                                       " was already registered with the other fallback attribute key: " + oldKey.myFallbackAttributeKey));
217     }
218
219     // but don't allow to rewrite not-null default attributes
220     if (oldKey.myDefaultAttributes != null && !oldKey.myDefaultAttributes.equals(defaultAttributes)) {
221       LOG.error(new IllegalStateException("TextAttributeKey(name:'" + externalName + "', defaultAttributes:'" + defaultAttributes + "') " +
222                                           " was already registered with the other defaultAttributes: " + oldKey.myDefaultAttributes));
223     }
224
225     TextAttributes newDefaults = ObjectUtils.chooseNotNull(defaultAttributes, oldKey.myDefaultAttributes); // care with not calling unwanted providers
226     TextAttributesKey newFallback = ObjectUtils.chooseNotNull(fallbackAttributeKey, oldKey.myFallbackAttributeKey);
227     return new TextAttributesKey(externalName, newDefaults, newFallback);
228   }
229
230   /**
231    * Registers a temp text attribute key with the specified identifier and default attributes.
232    * The attribute of the temp attribute key is not serialized and not copied while TextAttributesScheme
233    * manipulations.
234    *
235    * @param externalName      the unique identifier of the key.
236    * @param defaultAttributes the default text attributes associated with the key.
237    * @return the new key instance, or an existing instance if the key with the same
238    * identifier was already registered.
239    */
240   @NotNull
241   public static TextAttributesKey createTempTextAttributesKey(@NonNls @NotNull String externalName, TextAttributes defaultAttributes) {
242     return createTextAttributesKey(TEMP_PREFIX + externalName, defaultAttributes);
243   }
244
245   @Nullable
246   public TextAttributesKey getFallbackAttributeKey() {
247     return myFallbackAttributeKey;
248   }
249
250   /**
251    * @deprecated Use {@link #createTextAttributesKey(String, TextAttributesKey)} instead
252    */
253   @Deprecated
254   public void setFallbackAttributeKey(@Nullable TextAttributesKey fallbackAttributeKey) {
255   }
256
257   @TestOnly
258   static void removeTextAttributesKey(@NonNls @NotNull String externalName) {
259     ourRegistry.remove(externalName);
260   }
261
262   public static boolean isTemp(@NotNull TextAttributesKey key) {
263     return key.getExternalName().startsWith(TEMP_PREFIX);
264   }
265
266   @FunctionalInterface
267   public interface TextAttributeKeyDefaultsProvider {
268     TextAttributes getDefaultAttributes(@NotNull TextAttributesKey key);
269   }
270
271   /**
272    * @deprecated For internal use only.
273    */
274   @Deprecated static final
275   TextAttributesKey DUMMY_DEPRECATED_ATTRIBUTES = createTextAttributesKey("__deprecated__");
276 }