Merge remote-tracking branch 'origin/master'
[idea/community.git] / platform / platform-impl / src / com / intellij / openapi / fileTypes / impl / FileTypeManagerImpl.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.openapi.fileTypes.impl;
17
18 import com.google.common.annotations.VisibleForTesting;
19 import com.intellij.ide.highlighter.custom.SyntaxTable;
20 import com.intellij.ide.plugins.PluginManager;
21 import com.intellij.ide.util.PropertiesComponent;
22 import com.intellij.openapi.Disposable;
23 import com.intellij.openapi.application.ApplicationManager;
24 import com.intellij.openapi.application.impl.TransferToPooledThreadQueue;
25 import com.intellij.openapi.components.*;
26 import com.intellij.openapi.diagnostic.Logger;
27 import com.intellij.openapi.extensions.Extensions;
28 import com.intellij.openapi.fileEditor.impl.LoadTextUtil;
29 import com.intellij.openapi.fileTypes.*;
30 import com.intellij.openapi.fileTypes.ex.*;
31 import com.intellij.openapi.options.BaseSchemeProcessor;
32 import com.intellij.openapi.options.SchemesManager;
33 import com.intellij.openapi.options.SchemesManagerFactory;
34 import com.intellij.openapi.project.Project;
35 import com.intellij.openapi.util.*;
36 import com.intellij.openapi.util.io.ByteSequence;
37 import com.intellij.openapi.util.io.FileUtil;
38 import com.intellij.openapi.util.text.StringUtil;
39 import com.intellij.openapi.util.text.StringUtilRt;
40 import com.intellij.openapi.vfs.*;
41 import com.intellij.openapi.vfs.newvfs.BulkFileListener;
42 import com.intellij.openapi.vfs.newvfs.FileAttribute;
43 import com.intellij.openapi.vfs.newvfs.FileSystemInterface;
44 import com.intellij.openapi.vfs.newvfs.events.VFileCreateEvent;
45 import com.intellij.openapi.vfs.newvfs.events.VFileEvent;
46 import com.intellij.openapi.vfs.newvfs.impl.StubVirtualFile;
47 import com.intellij.psi.SingleRootFileViewProvider;
48 import com.intellij.testFramework.LightVirtualFile;
49 import com.intellij.util.*;
50 import com.intellij.util.containers.ConcurrentPackedBitsArray;
51 import com.intellij.util.containers.ContainerUtil;
52 import com.intellij.util.io.URLUtil;
53 import com.intellij.util.messages.MessageBus;
54 import com.intellij.util.messages.MessageBusConnection;
55 import gnu.trove.THashMap;
56 import gnu.trove.THashSet;
57 import org.jdom.Element;
58 import org.jetbrains.annotations.NonNls;
59 import org.jetbrains.annotations.NotNull;
60 import org.jetbrains.annotations.Nullable;
61 import org.jetbrains.annotations.TestOnly;
62
63 import java.io.DataInputStream;
64 import java.io.DataOutputStream;
65 import java.io.IOException;
66 import java.io.InputStream;
67 import java.net.URL;
68 import java.nio.charset.Charset;
69 import java.util.*;
70 import java.util.concurrent.atomic.AtomicInteger;
71 import java.util.concurrent.atomic.AtomicLong;
72
73 @State(
74   name = "FileTypeManager",
75   storages = @Storage(file = StoragePathMacros.APP_CONFIG + "/filetypes.xml"),
76   additionalExportFile = FileTypeManagerImpl.FILE_SPEC
77 )
78 public class FileTypeManagerImpl extends FileTypeManagerEx implements PersistentStateComponent<Element>, ApplicationComponent, Disposable {
79   private static final Logger LOG = Logger.getInstance(FileTypeManagerImpl.class);
80
81   // You must update all existing default configurations accordingly
82   private static final int VERSION = 14;
83   private static final Key<FileType> FILE_TYPE_KEY = Key.create("FILE_TYPE_KEY");
84   // cached auto-detected file type. If the file was auto-detected as plain text or binary
85   // then the value is null and autoDetectedAsText, autoDetectedAsBinary and autoDetectWasRun sets are used instead.
86   private static final Key<FileType> DETECTED_FROM_CONTENT_FILE_TYPE_KEY = Key.create("DETECTED_FROM_CONTENT_FILE_TYPE_KEY");
87   private static final int DETECT_BUFFER_SIZE = 8192; // the number of bytes to read from the file to feed to the file type detector
88
89   @NonNls
90   private static final String DEFAULT_IGNORED =
91     "*.hprof;*.pyc;*.pyo;*.rbc;*~;.DS_Store;.bundle;.git;.hg;.svn;CVS;RCS;SCCS;__pycache__;.tox;_svn;rcs;vssver.scc;vssver2.scc;";
92
93   private static boolean RE_DETECT_ASYNC = !ApplicationManager.getApplication().isUnitTestMode();
94   private final Collection<FileType> myDefaultTypes = new THashSet<FileType>();
95   private final List<FileTypeIdentifiableByVirtualFile> mySpecialFileTypes = new ArrayList<FileTypeIdentifiableByVirtualFile>();
96
97   private FileTypeAssocTable<FileType> myPatternsTable = new FileTypeAssocTable<FileType>();
98   private final IgnoredPatternSet myIgnoredPatterns = new IgnoredPatternSet();
99   private final IgnoredFileCache myIgnoredFileCache = new IgnoredFileCache(myIgnoredPatterns);
100
101   private final FileTypeAssocTable<FileType> myInitialAssociations = new FileTypeAssocTable<FileType>();
102   private final Map<FileNameMatcher, String> myUnresolvedMappings = new THashMap<FileNameMatcher, String>();
103   private final Map<FileNameMatcher, Trinity<String, String, Boolean>> myUnresolvedRemovedMappings = new THashMap<FileNameMatcher, Trinity<String, String, Boolean>>();
104   /** This will contain removed mappings with "approved" states */
105   private final Map<FileNameMatcher, Pair<FileType, Boolean>> myRemovedMappings = new THashMap<FileNameMatcher, Pair<FileType, Boolean>>();
106
107   @NonNls private static final String ELEMENT_FILETYPE = "filetype";
108   @NonNls private static final String ELEMENT_IGNORE_FILES = "ignoreFiles";
109   @NonNls private static final String ATTRIBUTE_LIST = "list";
110
111   @NonNls private static final String ATTRIBUTE_VERSION = "version";
112   @NonNls private static final String ATTRIBUTE_NAME = "name";
113   @NonNls private static final String ATTRIBUTE_DESCRIPTION = "description";
114
115   private static class StandardFileType {
116     @NotNull private final FileType fileType;
117     @NotNull private final List<FileNameMatcher> matchers;
118
119     private StandardFileType(@NotNull FileType fileType, @NotNull List<FileNameMatcher> matchers) {
120       this.fileType = fileType;
121       this.matchers = matchers;
122     }
123   }
124
125   private final MessageBus myMessageBus;
126   private final Map<String, StandardFileType> myStandardFileTypes = new LinkedHashMap<String, StandardFileType>();
127   @NonNls
128   private static final String[] FILE_TYPES_WITH_PREDEFINED_EXTENSIONS = {"JSP", "JSPX", "DTD", "HTML", "Properties", "XHTML"};
129   private final SchemesManager<FileType, AbstractFileType> mySchemesManager;
130   @NonNls
131   static final String FILE_SPEC = StoragePathMacros.ROOT_CONFIG + "/filetypes";
132
133   // these flags are stored in 'packedFlags' as chunks of four bits
134   private static final int AUTO_DETECTED_AS_TEXT_MASK = 1;
135   private static final int AUTO_DETECTED_AS_BINARY_MASK = 2;
136   private static final int AUTO_DETECT_WAS_RUN_MASK = 4;
137   private static final int ATTRIBUTES_WERE_LOADED_MASK = 8;
138   private final ConcurrentPackedBitsArray packedFlags = new ConcurrentPackedBitsArray(4);
139
140   private final AtomicInteger counterAutoDetect = new AtomicInteger();
141   private final AtomicLong elapsedAutoDetect = new AtomicLong();
142
143   public FileTypeManagerImpl(MessageBus bus, SchemesManagerFactory schemesManagerFactory, PropertiesComponent propertiesComponent) {
144     int fileTypeChangedCounter = StringUtilRt.parseInt(propertiesComponent.getValue("fileTypeChangedCounter"), 0);
145     fileTypeChangedCount = new AtomicInteger(fileTypeChangedCounter);
146     autoDetectedAttribute = new FileAttribute("AUTO_DETECTION_CACHE_ATTRIBUTE", fileTypeChangedCounter, true);
147
148     myMessageBus = bus;
149     mySchemesManager = schemesManagerFactory.createSchemesManager(FILE_SPEC, new BaseSchemeProcessor<AbstractFileType>() {
150       @NotNull
151       @Override
152       public AbstractFileType readScheme(@NotNull Element element, boolean duringLoad) {
153         if (!duringLoad) {
154           fireBeforeFileTypesChanged();
155         }
156         AbstractFileType type = (AbstractFileType)loadFileType(element, false);
157         if (!duringLoad) {
158           fireFileTypesChanged();
159         }
160         return type;
161       }
162
163       @NotNull
164       @Override
165       public State getState(@NotNull AbstractFileType fileType) {
166         if (!shouldSave(fileType)) {
167           return State.NON_PERSISTENT;
168         }
169         if (!myDefaultTypes.contains(fileType)) {
170           return State.POSSIBLY_CHANGED;
171         }
172         return fileType.isModified() ? State.POSSIBLY_CHANGED : State.NON_PERSISTENT;
173       }
174
175       @Override
176       public Element writeScheme(@NotNull AbstractFileType fileType) {
177         Element root = new Element(ELEMENT_FILETYPE);
178
179         root.setAttribute("binary", String.valueOf(fileType.isBinary()));
180         if (!StringUtil.isEmpty(fileType.getDefaultExtension())) {
181           root.setAttribute("default_extension", fileType.getDefaultExtension());
182         }
183         root.setAttribute(ATTRIBUTE_DESCRIPTION, fileType.getDescription());
184         root.setAttribute(ATTRIBUTE_NAME, fileType.getName());
185
186         fileType.writeExternal(root);
187
188         Element map = new Element(AbstractFileType.ELEMENT_EXTENSION_MAP);
189         writeExtensionsMap(map, fileType, false);
190         if (!map.getChildren().isEmpty()) {
191           root.addContent(map);
192         }
193         return root;
194       }
195
196       @Override
197       public void onSchemeDeleted(@NotNull AbstractFileType scheme) {
198         fireBeforeFileTypesChanged();
199         myPatternsTable.removeAllAssociations(scheme);
200         fireFileTypesChanged();
201       }
202     }, RoamingType.PER_USER);
203     bus.connect().subscribe(VirtualFileManager.VFS_CHANGES, new BulkFileListener.Adapter() {
204       @Override
205       public void after(@NotNull List<? extends VFileEvent> events) {
206         Collection<VirtualFile> files = ContainerUtil.map2Set(events, new Function<VFileEvent, VirtualFile>() {
207           @Override
208           public VirtualFile fun(VFileEvent event) {
209             VirtualFile file = event instanceof VFileCreateEvent ? null : event.getFile();
210             return file != null && wasAutoDetectedBefore(file) && isDetectable(file) ? file : null;
211           }
212         });
213         files.remove(null);
214         if (toLog()) {
215           log("F: VFS events: " + events);
216         }
217         if (!files.isEmpty() && RE_DETECT_ASYNC) {
218           if (toLog()) {
219             log("F: queued to redetect: " + files);
220           }
221           reDetectQueue.offerIfAbsent(files);
222         }
223       }
224     });
225
226     //noinspection SpellCheckingInspection
227     myIgnoredPatterns.setIgnoreMasks(DEFAULT_IGNORED);
228
229     // this should be done BEFORE reading state
230     initStandardFileTypes();
231   }
232
233   @VisibleForTesting
234   void initStandardFileTypes() {
235     FileTypeConsumer consumer = new FileTypeConsumer() {
236       @Override
237       public void consume(@NotNull FileType fileType) {
238         register(fileType, parse(fileType.getDefaultExtension()));
239       }
240
241       @Override
242       public void consume(@NotNull final FileType fileType, String extensions) {
243         register(fileType, parse(extensions));
244       }
245
246       @Override
247       public void consume(@NotNull final FileType fileType, @NotNull final FileNameMatcher... matchers) {
248         register(fileType, new ArrayList<FileNameMatcher>(Arrays.asList(matchers)));
249       }
250
251       @Override
252       public FileType getStandardFileTypeByName(@NotNull final String name) {
253         final StandardFileType type = myStandardFileTypes.get(name);
254         return type != null ? type.fileType : null;
255       }
256
257       private void register(@NotNull FileType fileType, @NotNull List<FileNameMatcher> fileNameMatchers) {
258         final StandardFileType type = myStandardFileTypes.get(fileType.getName());
259         if (type != null) {
260           type.matchers.addAll(fileNameMatchers);
261         }
262         else {
263           myStandardFileTypes.put(fileType.getName(), new StandardFileType(fileType, fileNameMatchers));
264         }
265       }
266     };
267
268     for (FileTypeFactory factory : FileTypeFactory.FILE_TYPE_FACTORY_EP.getExtensions()) {
269       try {
270         factory.createFileTypes(consumer);
271       }
272       catch (Throwable e) {
273         PluginManager.handleComponentError(e, factory.getClass().getName(), null);
274       }
275     }
276     for (StandardFileType pair : myStandardFileTypes.values()) {
277       registerFileTypeWithoutNotification(pair.fileType, pair.matchers, true);
278     }
279
280     if (PlatformUtils.isDatabaseIDE() || PlatformUtils.isCidr()) {
281       // build scripts are correct, but it is required to run from sources
282       return;
283     }
284
285     try {
286       URL defaultFileTypesUrl = FileTypeManagerImpl.class.getResource("/defaultFileTypes.xml");
287       if (defaultFileTypesUrl != null) {
288         Element defaultFileTypesElement = JDOMUtil.load(URLUtil.openStream(defaultFileTypesUrl));
289         for (Element e : defaultFileTypesElement.getChildren()) {
290           //noinspection SpellCheckingInspection
291           if ("filetypes".equals(e.getName())) {
292             for (Element element : e.getChildren(ELEMENT_FILETYPE)) {
293               loadFileType(element, true);
294             }
295           }
296           else if (AbstractFileType.ELEMENT_EXTENSION_MAP.equals(e.getName())) {
297             readGlobalMappings(e);
298           }
299         }
300       }
301     }
302     catch (Exception e) {
303       LOG.error(e);
304     }
305   }
306
307   private static boolean toLog() {
308     return RE_DETECT_ASYNC && ApplicationManager.getApplication().isUnitTestMode();
309   }
310
311   private static void log(@SuppressWarnings("UnusedParameters") String message) {
312     //System.out.println(message);
313   }
314
315   private final TransferToPooledThreadQueue<Collection<VirtualFile>> reDetectQueue = new TransferToPooledThreadQueue<Collection<VirtualFile>>("File type re-detect", Conditions.alwaysFalse(), -1, new Processor<Collection<VirtualFile>>() {
316     @Override
317     public boolean process(Collection<VirtualFile> files) {
318       reDetect(files);
319       return true;
320     }
321   });
322
323   @TestOnly
324   public void drainReDetectQueue() {
325     reDetectQueue.waitFor();
326   }
327
328   @TestOnly
329   @NotNull
330   Collection<VirtualFile> dumpReDetectQueue() {
331     return ContainerUtil.flatten(reDetectQueue.dump());
332   }
333
334   @TestOnly
335   static void reDetectAsync(boolean enable) {
336     RE_DETECT_ASYNC = enable;
337   }
338
339   private void reDetect(@NotNull Collection<VirtualFile> files) {
340     final Collection<VirtualFile> changed = new ArrayList<VirtualFile>();
341     for (VirtualFile file : files) {
342       boolean shouldRedetect = wasAutoDetectedBefore(file) && isDetectable(file);
343       if (toLog()) {
344         log("F: Redetect file: " + file.getName() + "; shouldRedetect: " + shouldRedetect);
345       }
346       if (shouldRedetect) {
347         int id = file instanceof VirtualFileWithId ? ((VirtualFileWithId)file).getId() : -1;
348         FileType before = getAutoDetectedType(file, id);
349
350         packedFlags.set(id, ATTRIBUTES_WERE_LOADED_MASK);
351
352         file.putUserData(DETECTED_FROM_CONTENT_FILE_TYPE_KEY, null);
353         FileType after = getFileTypeByFile(file); // may be back to standard file type
354         if (toLog()) {
355           log("F: After redetect file: " + file.getName() + "; before: " + before.getName() + "; after: " + after.getName()+"; now getFileType()="+file.getFileType().getName());
356         }
357
358         if (before != after) {
359           changed.add(file);
360           LOG.debug(file+" type was re-detected. Was: "+before.getName()+"; now: "+after.getName());
361         }
362       }
363     }
364     if (!changed.isEmpty()) {
365       ApplicationManager.getApplication().invokeLater(new Runnable() {
366         @Override
367         public void run() {
368           FileContentUtilCore.reparseFiles(changed);
369         }
370       }, ApplicationManager.getApplication().getDisposed());
371     }
372   }
373
374   private boolean wasAutoDetectedBefore(@NotNull VirtualFile file) {
375     if (file.getUserData(DETECTED_FROM_CONTENT_FILE_TYPE_KEY) != null) return true;
376     if (file instanceof VirtualFileWithId) {
377       int id = Math.abs(((VirtualFileWithId)file).getId());
378       // do not re-detect binary files
379       return (packedFlags.get(id) & (AUTO_DETECT_WAS_RUN_MASK | AUTO_DETECTED_AS_BINARY_MASK)) == AUTO_DETECT_WAS_RUN_MASK;
380     }
381     return false;
382   }
383
384   @Override
385   @NotNull
386   public FileType getStdFileType(@NotNull @NonNls String name) {
387     StandardFileType stdFileType = myStandardFileTypes.get(name);
388     return stdFileType != null ? stdFileType.fileType : PlainTextFileType.INSTANCE;
389   }
390
391   @Override
392   public void disposeComponent() {
393   }
394
395   @Override
396   public void initComponent() {
397     if (!myUnresolvedMappings.isEmpty()) {
398       for (StandardFileType pair : myStandardFileTypes.values()) {
399         registerReDetectedMappings(pair);
400       }
401     }
402     // Resolve unresolved mappings initialized before certain plugin initialized.
403     for (StandardFileType pair : myStandardFileTypes.values()) {
404       bindUnresolvedMappings(pair.fileType);
405     }
406
407     boolean isAtLeastOneStandardFileTypeHasBeenRead = false;
408     for (AbstractFileType fileType : mySchemesManager.loadSchemes()) {
409       isAtLeastOneStandardFileTypeHasBeenRead |= myInitialAssociations.hasAssociationsFor(fileType);
410     }
411     if (isAtLeastOneStandardFileTypeHasBeenRead) {
412       restoreStandardFileExtensions();
413     }
414   }
415
416   @Override
417   @NotNull
418   public FileType getFileTypeByFileName(@NotNull String fileName) {
419     return getFileTypeByFileName((CharSequence)fileName);
420   }
421
422   @NotNull
423   private FileType getFileTypeByFileName(@NotNull CharSequence fileName) {
424     FileType type = myPatternsTable.findAssociatedFileType(fileName);
425     return type == null ? UnknownFileType.INSTANCE : type;
426   }
427
428   public static void cacheFileType(@NotNull VirtualFile file, @Nullable FileType fileType) {
429     file.putUserData(FILE_TYPE_KEY, fileType);
430     if (toLog()) {
431       log("F: Cached file type for "+file.getName()+" to "+(fileType == null ? null : fileType.getName()));
432     }
433   }
434
435   @Override
436   @NotNull
437   public FileType getFileTypeByFile(@NotNull VirtualFile file) {
438     FileType fileType = file.getUserData(FILE_TYPE_KEY);
439     if (fileType != null) return fileType;
440
441     if (file instanceof LightVirtualFile) {
442       fileType = ((LightVirtualFile)file).getAssignedFileType();
443       if (fileType != null) return fileType;
444     }
445
446     //noinspection ForLoopReplaceableByForEach
447     for (int i = 0; i < mySpecialFileTypes.size(); i++) {
448       FileTypeIdentifiableByVirtualFile type = mySpecialFileTypes.get(i);
449       if (type.isMyFileType(file)) {
450         if (toLog()) {
451           log("F: Special file type for "+file.getName()+"; type: "+type.getName());
452         }
453         return type;
454       }
455     }
456
457     fileType = getFileTypeByFileName(file.getNameSequence());
458     if (fileType != UnknownFileType.INSTANCE) {
459       if (toLog()) {
460         log("F: By name file type for "+file.getName()+"; type: "+fileType.getName());
461       }
462       return fileType;
463     }
464
465     if (!(file instanceof StubVirtualFile)) {
466       fileType = getOrDetectFromContent(file);
467     }
468
469     return fileType;
470   }
471
472   @NotNull
473   private FileType getOrDetectFromContent(@NotNull VirtualFile file) {
474     if (!isDetectable(file)) return UnknownFileType.INSTANCE;
475     if (file instanceof VirtualFileWithId) {
476       int id = ((VirtualFileWithId)file).getId();
477       if (id < 0) return UnknownFileType.INSTANCE;
478
479       long flags = packedFlags.get(id);
480       boolean autoDetectWasRun = (flags & AUTO_DETECT_WAS_RUN_MASK) != 0;
481       if (autoDetectWasRun) {
482         FileType type = getAutoDetectedType(file, id);
483         if (toLog()) {
484           log("F: autodetected getFileType("+file.getName()+") = "+type.getName());
485         }
486         return type;
487       }
488       if ((flags & ATTRIBUTES_WERE_LOADED_MASK) == 0) {
489         flags = readFlagsFromCache(file);
490         packedFlags.set(id, flags);
491       }
492       boolean wasDetectedAsText = BitUtil.isSet(flags, AUTO_DETECTED_AS_TEXT_MASK);
493       boolean wasDetectedAsBinary = BitUtil.isSet(flags, AUTO_DETECTED_AS_BINARY_MASK);
494       boolean wasAutoDetectRun = BitUtil.isSet(flags, AUTO_DETECT_WAS_RUN_MASK);
495       if (wasAutoDetectRun && (wasDetectedAsText || wasDetectedAsBinary)) {
496         return wasDetectedAsText ? FileTypes.PLAIN_TEXT : UnknownFileType.INSTANCE;
497       }
498     }
499     FileType fileType = file.getUserData(DETECTED_FROM_CONTENT_FILE_TYPE_KEY);
500     // run autodetection
501     if (fileType == null) {
502       fileType = detectFromContent(file);
503     }
504
505     if (toLog()) {
506       log("F: getFileType after detect run("+file.getName()+") = "+fileType.getName());
507     }
508
509     return fileType;
510   }
511
512   private volatile FileAttribute autoDetectedAttribute;
513   protected byte readFlagsFromCache(@NotNull VirtualFile file) {
514     DataInputStream stream = autoDetectedAttribute.readAttribute(file);
515     boolean wasAutoDetectRun = false;
516     byte status = 0;
517     try {
518       try {
519         status = stream != null ? stream.readByte() : 0;
520         wasAutoDetectRun = stream != null;
521       }
522       finally {
523         if (stream != null) {
524           stream.close();
525         }
526       }
527     }
528     catch (IOException ignored) {
529     }
530     status = (byte)BitUtil.set(status, ATTRIBUTES_WERE_LOADED_MASK, wasAutoDetectRun);
531
532     return status;
533   }
534
535   protected void writeFlagsToCache(@NotNull VirtualFile file, int flags) {
536     DataOutputStream stream = autoDetectedAttribute.writeAttribute(file);
537     try {
538       try {
539         stream.writeByte(flags);
540       }
541       finally {
542         stream.close();
543       }
544     }
545     catch (IOException e) {
546       LOG.error(e);
547     }
548   }
549
550   private void clearCaches() {
551     int count = fileTypeChangedCount.incrementAndGet();
552     autoDetectedAttribute = autoDetectedAttribute.newVersion(count);
553     PropertiesComponent.getInstance().setValue("fileTypeChangedCounter", Integer.toString(count));
554     packedFlags.clear();
555   }
556
557   @NotNull
558   private FileType getAutoDetectedType(@NotNull VirtualFile file, int id) {
559     long flags = packedFlags.get(id);
560     return BitUtil.isSet(flags, AUTO_DETECTED_AS_TEXT_MASK) ? FileTypes.PLAIN_TEXT :
561            BitUtil.isSet(flags, AUTO_DETECTED_AS_BINARY_MASK) ? UnknownFileType.INSTANCE :
562            ObjectUtils.notNull(file.getUserData(DETECTED_FROM_CONTENT_FILE_TYPE_KEY), FileTypes.PLAIN_TEXT);
563   }
564
565   @NotNull
566   @Override
567   @Deprecated
568   public FileType detectFileTypeFromContent(@NotNull VirtualFile file) {
569     return file.getFileType();
570   }
571
572   private void cacheAutoDetectedFileType(@NotNull VirtualFile file, @NotNull FileType fileType) {
573     boolean wasAutodetectedAsText = fileType == FileTypes.PLAIN_TEXT;
574     boolean wasAutodetectedAsBinary = fileType == FileTypes.UNKNOWN;
575
576     int flags = BitUtil.set(0, AUTO_DETECTED_AS_TEXT_MASK, wasAutodetectedAsText);
577     flags = BitUtil.set(flags, AUTO_DETECTED_AS_BINARY_MASK, wasAutodetectedAsBinary);
578     writeFlagsToCache(file, flags);
579     if (file instanceof VirtualFileWithId) {
580       int id = Math.abs(((VirtualFileWithId)file).getId());
581       flags = AUTO_DETECT_WAS_RUN_MASK | ATTRIBUTES_WERE_LOADED_MASK;
582       flags = BitUtil.set(flags, AUTO_DETECTED_AS_TEXT_MASK, wasAutodetectedAsText);
583       flags = BitUtil.set(flags, AUTO_DETECTED_AS_BINARY_MASK, wasAutodetectedAsBinary);
584       packedFlags.set(id, flags);
585
586       if (wasAutodetectedAsText || wasAutodetectedAsBinary) {
587         file.putUserData(DETECTED_FROM_CONTENT_FILE_TYPE_KEY, null);
588         return;
589       }
590     }
591     file.putUserData(DETECTED_FROM_CONTENT_FILE_TYPE_KEY, fileType);
592   }
593
594   @Override
595   public FileType findFileTypeByName(String fileTypeName) {
596     FileType type = getStdFileType(fileTypeName);
597     // TODO: Abstract file types are not std one, so need to be restored specially,
598     // currently there are 6 of them and restoration does not happen very often so just iteration is enough
599     if (type == PlainTextFileType.INSTANCE && !fileTypeName.equals(type.getName())) {
600       for (FileType fileType: mySchemesManager.getAllSchemes()) {
601         if (fileTypeName.equals(fileType.getName())) {
602           return fileType;
603         }
604       }
605     }
606     return type;
607   }
608
609   private static boolean isDetectable(@NotNull final VirtualFile file) {
610     if (file.isDirectory() || !file.isValid() || file.is(VFileProperty.SPECIAL) || file.getLength() == 0) {
611       // for empty file there is still hope its type will change
612       return false;
613     }
614     return file.getFileSystem() instanceof FileSystemInterface && !SingleRootFileViewProvider.isTooLargeForContentLoading(file);
615   }
616
617   @NotNull
618   private FileType detectFromContent(@NotNull final VirtualFile file) {
619     long start = System.currentTimeMillis();
620     try {
621       final InputStream inputStream = ((FileSystemInterface)file.getFileSystem()).getInputStream(file);
622       final Ref<FileType> result = new Ref<FileType>(UnknownFileType.INSTANCE);
623       try {
624         FileUtil.processFirstBytes(inputStream, DETECT_BUFFER_SIZE, new Processor<ByteSequence>() {
625           @Override
626           public boolean process(ByteSequence byteSequence) {
627             boolean isText = guessIfText(file, byteSequence);
628             CharSequence text;
629             if (isText) {
630               byte[] bytes = Arrays.copyOf(byteSequence.getBytes(), byteSequence.getLength());
631               text = LoadTextUtil.getTextByBinaryPresentation(bytes, file, true, true, UnknownFileType.INSTANCE);
632             }
633             else {
634               text = null;
635             }
636
637             FileType detected = null;
638             for (FileTypeDetector detector : Extensions.getExtensions(FileTypeDetector.EP_NAME)) {
639               try {
640                 detected = detector.detect(file, byteSequence, text);
641               }
642               catch (Exception e) {
643                 LOG.error("Detector " + detector + " (" + detector.getClass() + ") exception occurred:", e);
644               }
645               if (detected != null) break;
646             }
647
648             if (detected == null) {
649               detected = isText ? PlainTextFileType.INSTANCE : UnknownFileType.INSTANCE;
650             }
651             result.set(detected);
652             return true;
653           }
654         });
655       }
656       finally {
657         inputStream.close();
658       }
659       FileType fileType = result.get();
660       if (toLog()) {
661         log("F: Redetect run for file: " + file.getName() + "; result: "+fileType.getName());
662       }
663
664       if (LOG.isDebugEnabled()) {
665         LOG.debug(file + "; type=" + fileType.getDescription() + "; " + counterAutoDetect);
666       }
667
668       cacheAutoDetectedFileType(file, fileType);
669       counterAutoDetect.incrementAndGet();
670       long elapsed = System.currentTimeMillis() - start;
671       elapsedAutoDetect.addAndGet(elapsed);
672
673       return fileType;
674     }
675     catch (IOException ignored) {
676       return UnknownFileType.INSTANCE; // return unknown, do not cache
677     }
678   }
679
680   private static boolean guessIfText(@NotNull VirtualFile file, @NotNull ByteSequence byteSequence) {
681     byte[] bytes = byteSequence.getBytes();
682     Trinity<Charset, CharsetToolkit.GuessedEncoding, byte[]> guessed = LoadTextUtil.guessFromContent(file, bytes, byteSequence.getLength());
683     if (guessed == null) return false;
684     file.setBOM(guessed.third);
685     if (guessed.first != null) {
686       // charset was detected unambiguously
687       return true;
688     }
689     // use wild guess
690     CharsetToolkit.GuessedEncoding guess = guessed.second;
691     return guess != null && (guess == CharsetToolkit.GuessedEncoding.VALID_UTF8 || guess == CharsetToolkit.GuessedEncoding.SEVEN_BIT);
692   }
693
694   @Override
695   public boolean isFileOfType(@NotNull VirtualFile file, @NotNull FileType type) {
696     if (type instanceof FileTypeIdentifiableByVirtualFile) {
697       return ((FileTypeIdentifiableByVirtualFile)type).isMyFileType(file);
698     }
699
700     return getFileTypeByFileName(file.getName()) == type;
701   }
702
703   @Override
704   @NotNull
705   public FileType getFileTypeByExtension(@NotNull String extension) {
706     return getFileTypeByFileName("IntelliJ_IDEA_RULES." + extension);
707   }
708
709   @Override
710   public void registerFileType(@NotNull FileType fileType) {
711     //noinspection deprecation
712     registerFileType(fileType, ArrayUtil.EMPTY_STRING_ARRAY);
713   }
714
715   @Override
716   public void registerFileType(@NotNull final FileType type, @NotNull final List<FileNameMatcher> defaultAssociations) {
717     ApplicationManager.getApplication().runWriteAction(new Runnable() {
718       @Override
719       public void run() {
720         fireBeforeFileTypesChanged();
721         registerFileTypeWithoutNotification(type, defaultAssociations, true);
722         fireFileTypesChanged();
723       }
724     });
725   }
726
727   @Override
728   public void unregisterFileType(@NotNull final FileType fileType) {
729     ApplicationManager.getApplication().runWriteAction(new Runnable() {
730       @Override
731       public void run() {
732         fireBeforeFileTypesChanged();
733         unregisterFileTypeWithoutNotification(fileType);
734         fireFileTypesChanged();
735       }
736     });
737   }
738
739   private void unregisterFileTypeWithoutNotification(FileType fileType) {
740     myPatternsTable.removeAllAssociations(fileType);
741     mySchemesManager.removeScheme(fileType);
742     if (fileType instanceof FileTypeIdentifiableByVirtualFile) {
743       final FileTypeIdentifiableByVirtualFile fakeFileType = (FileTypeIdentifiableByVirtualFile)fileType;
744       mySpecialFileTypes.remove(fakeFileType);
745     }
746   }
747
748   @Override
749   @NotNull
750   public FileType[] getRegisteredFileTypes() {
751     Collection<FileType> fileTypes = mySchemesManager.getAllSchemes();
752     return fileTypes.toArray(new FileType[fileTypes.size()]);
753   }
754
755   @Override
756   @NotNull
757   public String getExtension(@NotNull String fileName) {
758     int index = fileName.lastIndexOf('.');
759     if (index < 0) return "";
760     return fileName.substring(index + 1);
761   }
762
763   @Override
764   @NotNull
765   public String getIgnoredFilesList() {
766     Set<String> masks = myIgnoredPatterns.getIgnoreMasks();
767     return masks.isEmpty() ? "" : StringUtil.join(masks, ";") + ";";
768   }
769
770   @Override
771   public void setIgnoredFilesList(@NotNull String list) {
772     fireBeforeFileTypesChanged();
773     myIgnoredFileCache.clearCache();
774     myIgnoredPatterns.setIgnoreMasks(list);
775     fireFileTypesChanged();
776   }
777
778   @Override
779   public boolean isIgnoredFilesListEqualToCurrent(@NotNull String list) {
780     Set<String> tempSet = new THashSet<String>();
781     StringTokenizer tokenizer = new StringTokenizer(list, ";");
782     while (tokenizer.hasMoreTokens()) {
783       tempSet.add(tokenizer.nextToken());
784     }
785     return tempSet.equals(myIgnoredPatterns.getIgnoreMasks());
786   }
787
788   @Override
789   public boolean isFileIgnored(@NotNull String name) {
790     return myIgnoredPatterns.isIgnored(name);
791   }
792
793   @Override
794   public boolean isFileIgnored(@NonNls @NotNull VirtualFile file) {
795     return myIgnoredFileCache.isFileIgnored(file);
796   }
797
798   @Override
799   @SuppressWarnings({"deprecation"})
800   @NotNull
801   public String[] getAssociatedExtensions(@NotNull FileType type) {
802     return myPatternsTable.getAssociatedExtensions(type);
803   }
804
805   @Override
806   @NotNull
807   public List<FileNameMatcher> getAssociations(@NotNull FileType type) {
808     return myPatternsTable.getAssociations(type);
809   }
810
811   @Override
812   public void associate(@NotNull FileType type, @NotNull FileNameMatcher matcher) {
813     associate(type, matcher, true);
814   }
815
816   @Override
817   public void removeAssociation(@NotNull FileType type, @NotNull FileNameMatcher matcher) {
818     removeAssociation(type, matcher, true);
819   }
820
821   @Override
822   public void fireBeforeFileTypesChanged() {
823     FileTypeEvent event = new FileTypeEvent(this);
824     myMessageBus.syncPublisher(TOPIC).beforeFileTypesChanged(event);
825   }
826
827   private final AtomicInteger fileTypeChangedCount;
828   @Override
829   public void fireFileTypesChanged() {
830     clearCaches();
831     myMessageBus.syncPublisher(TOPIC).fileTypesChanged(new FileTypeEvent(this));
832   }
833
834   private final Map<FileTypeListener, MessageBusConnection> myAdapters = new HashMap<FileTypeListener, MessageBusConnection>();
835
836   @Override
837   public void addFileTypeListener(@NotNull FileTypeListener listener) {
838     final MessageBusConnection connection = myMessageBus.connect();
839     connection.subscribe(TOPIC, listener);
840     myAdapters.put(listener, connection);
841   }
842
843   @Override
844   public void removeFileTypeListener(@NotNull FileTypeListener listener) {
845     final MessageBusConnection connection = myAdapters.remove(listener);
846     if (connection != null) {
847       connection.disconnect();
848     }
849   }
850
851   @Override
852   public void loadState(Element state) {
853     int savedVersion = StringUtilRt.parseInt(state.getAttributeValue(ATTRIBUTE_VERSION), 0);
854
855     for (Element element : state.getChildren()) {
856       if (element.getName().equals(ELEMENT_IGNORE_FILES)) {
857         myIgnoredPatterns.setIgnoreMasks(element.getAttributeValue(ATTRIBUTE_LIST));
858       }
859       else if (AbstractFileType.ELEMENT_EXTENSION_MAP.equals(element.getName())) {
860         readGlobalMappings(element);
861       }
862     }
863
864     if (savedVersion < 4) {
865       if (savedVersion == 0) {
866         addIgnore(".svn");
867       }
868
869       if (savedVersion < 2) {
870         restoreStandardFileExtensions();
871       }
872
873       addIgnore("*.pyc");
874       addIgnore("*.pyo");
875       addIgnore(".git");
876     }
877
878     if (savedVersion < 5) {
879       addIgnore("*.hprof");
880     }
881
882     if (savedVersion < 6) {
883       addIgnore("_svn");
884     }
885
886     if (savedVersion < 7) {
887       addIgnore(".hg");
888     }
889
890     if (savedVersion < 8) {
891       addIgnore("*~");
892     }
893
894     if (savedVersion < 9) {
895       addIgnore("__pycache__");
896     }
897
898     if (savedVersion < 10) {
899       addIgnore(".bundle");
900     }
901
902     if (savedVersion < 11) {
903       addIgnore("*.rbc");
904     }
905
906     if (savedVersion < 13) {
907       // we want *.lib back since it's an important user artifact for CLion, also for IDEA project itself, since we have some libs.  
908       Set<String> masks = new LinkedHashSet<String>(myIgnoredPatterns.getIgnoreMasks());
909       masks.remove("*.lib");
910       
911       myIgnoredPatterns.clearPatterns();
912       for (String each : masks) {
913         myIgnoredPatterns.addIgnoreMask(each);
914       }
915     }
916
917     if (savedVersion < 14) {
918       addIgnore(".tox");
919     }
920
921     myIgnoredFileCache.clearCache();
922
923     String counter = JDOMExternalizer.readString(state, "fileTypeChangedCounter");
924     if (counter != null) {
925       fileTypeChangedCount.set(StringUtilRt.parseInt(counter, 0));
926       autoDetectedAttribute = autoDetectedAttribute.newVersion(fileTypeChangedCount.get());
927     }
928   }
929
930   private void readGlobalMappings(@NotNull Element e) {
931     for (Pair<FileNameMatcher, String> association : AbstractFileType.readAssociations(e)) {
932       FileType type = getFileTypeByName(association.getSecond());
933       FileNameMatcher matcher = association.getFirst();
934       if (type != null) {
935         if (PlainTextFileType.INSTANCE == type) {
936           FileType newFileType = myPatternsTable.findAssociatedFileType(matcher);
937           if (newFileType != null && newFileType != PlainTextFileType.INSTANCE && newFileType != UnknownFileType.INSTANCE) {
938             myRemovedMappings.put(matcher, Pair.create(newFileType, false));
939           }
940         }
941         associate(type, matcher, false);
942       }
943       else {
944         myUnresolvedMappings.put(matcher, association.getSecond());
945       }
946     }
947
948     List<Trinity<FileNameMatcher, String, Boolean>> removedAssociations = AbstractFileType.readRemovedAssociations(e);
949     for (Trinity<FileNameMatcher, String, Boolean> trinity : removedAssociations) {
950       FileType type = getFileTypeByName(trinity.getSecond());
951       FileNameMatcher matcher = trinity.getFirst();
952       if (type != null) {
953         removeAssociation(type, matcher, false);
954       }
955       else {
956         myUnresolvedRemovedMappings.put(matcher, Trinity.create(trinity.getSecond(), myUnresolvedMappings.get(matcher), trinity.getThird()));
957       }
958     }
959   }
960
961   private void addIgnore(@NonNls @NotNull String ignoreMask) {
962     myIgnoredPatterns.addIgnoreMask(ignoreMask);
963   }
964
965   private void restoreStandardFileExtensions() {
966     for (final String name : FILE_TYPES_WITH_PREDEFINED_EXTENSIONS) {
967       final StandardFileType stdFileType = myStandardFileTypes.get(name);
968       if (stdFileType != null) {
969         FileType fileType = stdFileType.fileType;
970         for (FileNameMatcher matcher : myPatternsTable.getAssociations(fileType)) {
971           FileType defaultFileType = myInitialAssociations.findAssociatedFileType(matcher);
972           if (defaultFileType != null && defaultFileType != fileType) {
973             removeAssociation(fileType, matcher, false);
974             associate(defaultFileType, matcher, false);
975           }
976         }
977
978         for (FileNameMatcher matcher : myInitialAssociations.getAssociations(fileType)) {
979           associate(fileType, matcher, false);
980         }
981       }
982     }
983   }
984
985   @Nullable
986   @Override
987   public Element getState() {
988     Element state = new Element("state");
989
990     Set<String> masks = myIgnoredPatterns.getIgnoreMasks();
991     String ignoreFiles;
992     if (masks.isEmpty()) {
993       ignoreFiles = "";
994     }
995     else {
996       String[] strings = ArrayUtil.toStringArray(masks);
997       Arrays.sort(strings);
998       ignoreFiles = StringUtil.join(strings, ";") + ";";
999     }
1000
1001     if (!ignoreFiles.equalsIgnoreCase(DEFAULT_IGNORED)) {
1002       // empty means empty list - we need to distinguish null and empty to apply or not to apply default value
1003       state.addContent(new Element(ELEMENT_IGNORE_FILES).setAttribute(ATTRIBUTE_LIST, ignoreFiles));
1004     }
1005
1006     Element map = new Element(AbstractFileType.ELEMENT_EXTENSION_MAP);
1007
1008     List<FileType> notExternalizableFileTypes = new ArrayList<FileType>();
1009     for (FileType type : mySchemesManager.getAllSchemes()) {
1010       if (!(type instanceof AbstractFileType)) {
1011         notExternalizableFileTypes.add(type);
1012       }
1013     }
1014     if (!notExternalizableFileTypes.isEmpty()) {
1015       Collections.sort(notExternalizableFileTypes, new Comparator<FileType>() {
1016         @Override
1017         public int compare(@NotNull FileType o1, @NotNull FileType o2) {
1018           return o1.getName().compareTo(o2.getName());
1019         }
1020       });
1021       for (FileType type : notExternalizableFileTypes) {
1022         writeExtensionsMap(map, type, true);
1023       }
1024     }
1025
1026     if (!myUnresolvedMappings.isEmpty()) {
1027       FileNameMatcher[] unresolvedMappingKeys = myUnresolvedMappings.keySet().toArray(new FileNameMatcher[myUnresolvedMappings.size()]);
1028       Arrays.sort(unresolvedMappingKeys, new Comparator<FileNameMatcher>() {
1029         @Override
1030         public int compare(FileNameMatcher o1, FileNameMatcher o2) {
1031           return o1.getPresentableString().compareTo(o2.getPresentableString());
1032         }
1033       });
1034
1035       for (FileNameMatcher fileNameMatcher : unresolvedMappingKeys) {
1036         Element content = AbstractFileType.writeMapping(myUnresolvedMappings.get(fileNameMatcher), fileNameMatcher, true);
1037         if (content != null) {
1038           map.addContent(content);
1039         }
1040       }
1041     }
1042
1043     if (!map.getChildren().isEmpty()) {
1044       state.addContent(map);
1045     }
1046
1047     if (!state.getChildren().isEmpty()) {
1048       state.setAttribute(ATTRIBUTE_VERSION, String.valueOf(VERSION));
1049     }
1050     return state;
1051   }
1052
1053   private void writeExtensionsMap(@NotNull Element map, @NotNull FileType type, boolean specifyTypeName) {
1054     List<FileNameMatcher> associations = myPatternsTable.getAssociations(type);
1055     Set<FileNameMatcher> defaultAssociations = new THashSet<FileNameMatcher>(myInitialAssociations.getAssociations(type));
1056
1057     for (FileNameMatcher matcher : associations) {
1058       if (defaultAssociations.contains(matcher)) {
1059         defaultAssociations.remove(matcher);
1060       }
1061       else if (shouldSave(type)) {
1062         Element content = AbstractFileType.writeMapping(type.getName(), matcher, specifyTypeName);
1063         if (content != null) {
1064           map.addContent(content);
1065         }
1066       }
1067     }
1068
1069     for (FileNameMatcher matcher : defaultAssociations) {
1070       Element content = AbstractFileType.writeRemovedMapping(type, matcher, specifyTypeName, isApproved(matcher));
1071       if (content != null) {
1072         map.addContent(content);
1073       }
1074     }
1075   }
1076
1077   private boolean isApproved(FileNameMatcher matcher) {
1078     Pair<FileType, Boolean> pair = myRemovedMappings.get(matcher);
1079     return pair != null && pair.getSecond();
1080   }
1081
1082   // -------------------------------------------------------------------------
1083   // Helper methods
1084   // -------------------------------------------------------------------------
1085
1086   @Nullable
1087   private FileType getFileTypeByName(@NotNull String name) {
1088     return mySchemesManager.findSchemeByName(name);
1089   }
1090
1091   @NotNull
1092   private static List<FileNameMatcher> parse(@Nullable String semicolonDelimited) {
1093     if (semicolonDelimited == null) {
1094       return Collections.emptyList();
1095     }
1096
1097     StringTokenizer tokenizer = new StringTokenizer(semicolonDelimited, FileTypeConsumer.EXTENSION_DELIMITER, false);
1098     ArrayList<FileNameMatcher> list = new ArrayList<FileNameMatcher>();
1099     while (tokenizer.hasMoreTokens()) {
1100       list.add(new ExtensionFileNameMatcher(tokenizer.nextToken().trim()));
1101     }
1102     return list;
1103   }
1104
1105   /**
1106    * Registers a standard file type. Doesn't notifyListeners any change events.
1107    */
1108   private void registerFileTypeWithoutNotification(@NotNull FileType fileType, @NotNull List<FileNameMatcher> matchers, boolean addScheme) {
1109     if (addScheme) {
1110       mySchemesManager.addNewScheme(fileType, true);
1111     }
1112     for (FileNameMatcher matcher : matchers) {
1113       myPatternsTable.addAssociation(matcher, fileType);
1114       myInitialAssociations.addAssociation(matcher, fileType);
1115     }
1116
1117     if (fileType instanceof FileTypeIdentifiableByVirtualFile) {
1118       mySpecialFileTypes.add((FileTypeIdentifiableByVirtualFile)fileType);
1119     }
1120   }
1121
1122   private void bindUnresolvedMappings(@NotNull FileType fileType) {
1123     for (FileNameMatcher matcher : new THashSet<FileNameMatcher>(myUnresolvedMappings.keySet())) {
1124       String name = myUnresolvedMappings.get(matcher);
1125       if (Comparing.equal(name, fileType.getName())) {
1126         myPatternsTable.addAssociation(matcher, fileType);
1127         myUnresolvedMappings.remove(matcher);
1128       }
1129     }
1130
1131     for (FileNameMatcher matcher : new THashSet<FileNameMatcher>(myUnresolvedRemovedMappings.keySet())) {
1132       Trinity<String, String, Boolean> trinity = myUnresolvedRemovedMappings.get(matcher);
1133       if (Comparing.equal(trinity.getFirst(), fileType.getName())) {
1134         removeAssociation(fileType, matcher, false);
1135         myUnresolvedRemovedMappings.remove(matcher);
1136       }
1137     }
1138   }
1139
1140   @NotNull
1141   private FileType loadFileType(@NotNull Element typeElement, boolean isDefault) {
1142     String fileTypeName = typeElement.getAttributeValue(ATTRIBUTE_NAME);
1143     String fileTypeDescr = typeElement.getAttributeValue(ATTRIBUTE_DESCRIPTION);
1144     String iconPath = typeElement.getAttributeValue("icon");
1145
1146     String extensionsStr = StringUtil.nullize(typeElement.getAttributeValue("extensions"));
1147     if (isDefault && extensionsStr != null) {
1148       // todo support wildcards
1149       extensionsStr = filterAlreadyRegisteredExtensions(extensionsStr);
1150     }
1151
1152     FileType type = isDefault ? getFileTypeByName(fileTypeName) : null;
1153     if (type != null) {
1154       return type;
1155     }
1156
1157     Element element = typeElement.getChild(AbstractFileType.ELEMENT_HIGHLIGHTING);
1158     if (element == null) {
1159       for (CustomFileTypeFactory factory : CustomFileTypeFactory.EP_NAME.getExtensions()) {
1160         type = factory.createFileType(typeElement);
1161         if (type != null) {
1162           break;
1163         }
1164       }
1165
1166       if (type == null) {
1167         type = new UserBinaryFileType();
1168       }
1169     }
1170     else {
1171       SyntaxTable table = AbstractFileType.readSyntaxTable(element);
1172       type = new AbstractFileType(table);
1173       ((AbstractFileType)type).initSupport();
1174     }
1175
1176     setFileTypeAttributes((UserFileType)type, fileTypeName, fileTypeDescr, iconPath);
1177     registerFileTypeWithoutNotification(type, parse(extensionsStr), isDefault);
1178
1179     if (isDefault) {
1180       myDefaultTypes.add(type);
1181       if (type instanceof ExternalizableFileType) {
1182         ((ExternalizableFileType)type).markDefaultSettings();
1183       }
1184     }
1185     else {
1186       Element extensions = typeElement.getChild(AbstractFileType.ELEMENT_EXTENSION_MAP);
1187       if (extensions != null) {
1188         for (Pair<FileNameMatcher, String> association : AbstractFileType.readAssociations(extensions)) {
1189           associate(type, association.getFirst(), false);
1190         }
1191
1192         for (Trinity<FileNameMatcher, String, Boolean> removedAssociation : AbstractFileType.readRemovedAssociations(extensions)) {
1193           removeAssociation(type, removedAssociation.getFirst(), false);
1194         }
1195       }
1196     }
1197     return type;
1198   }
1199
1200   @Nullable
1201   private String filterAlreadyRegisteredExtensions(@NotNull String semicolonDelimited) {
1202     StringTokenizer tokenizer = new StringTokenizer(semicolonDelimited, FileTypeConsumer.EXTENSION_DELIMITER, false);
1203     StringBuilder builder = null;
1204     while (tokenizer.hasMoreTokens()) {
1205       String extension = tokenizer.nextToken().trim();
1206       if (getFileTypeByExtension(extension) == UnknownFileType.INSTANCE) {
1207         if (builder == null) {
1208           builder = new StringBuilder();
1209         }
1210         else if (builder.length() > 0) {
1211           builder.append(FileTypeConsumer.EXTENSION_DELIMITER);
1212         }
1213         builder.append(extension);
1214       }
1215     }
1216     return builder == null ? null : builder.toString();
1217   }
1218
1219   private static void setFileTypeAttributes(@NotNull UserFileType fileType, @Nullable String name, @Nullable String description, @Nullable String iconPath) {
1220     if (!StringUtil.isEmptyOrSpaces(iconPath)) {
1221       fileType.setIcon(IconLoader.getIcon(iconPath));
1222     }
1223     if (description != null) {
1224       fileType.setDescription(description);
1225     }
1226     if (name != null) {
1227       fileType.setName(name);
1228     }
1229   }
1230
1231   private static boolean shouldSave(FileType fileType) {
1232     return fileType != FileTypes.UNKNOWN && !fileType.isReadOnly();
1233   }
1234
1235   // -------------------------------------------------------------------------
1236   // Setup
1237   // -------------------------------------------------------------------------
1238
1239   @Override
1240   @NotNull
1241   public String getComponentName() {
1242     return getFileTypeComponentName();
1243   }
1244
1245   public static String getFileTypeComponentName() {
1246     return PlatformUtils.isIdeaCommunity() ? "CommunityFileTypes" : "FileTypeManager";
1247   }
1248
1249   @NotNull
1250   FileTypeAssocTable getExtensionMap() {
1251     return myPatternsTable;
1252   }
1253
1254   void setPatternsTable(@NotNull Set<FileType> fileTypes, @NotNull FileTypeAssocTable<FileType> assocTable) {
1255     fireBeforeFileTypesChanged();
1256     for (FileType existing : getRegisteredFileTypes()) {
1257       if (!fileTypes.contains(existing)) {
1258         mySchemesManager.removeScheme(existing);
1259       }
1260     }
1261     for (FileType fileType : fileTypes) {
1262       mySchemesManager.addNewScheme(fileType, true);
1263       if (fileType instanceof AbstractFileType) {
1264         ((AbstractFileType)fileType).initSupport();
1265       }
1266     }
1267     myPatternsTable = assocTable.copy();
1268     fireFileTypesChanged();
1269   }
1270
1271   public void associate(FileType fileType, FileNameMatcher matcher, boolean fireChange) {
1272     if (!myPatternsTable.isAssociatedWith(fileType, matcher)) {
1273       if (fireChange) {
1274         fireBeforeFileTypesChanged();
1275       }
1276       myPatternsTable.addAssociation(matcher, fileType);
1277       if (fireChange) {
1278         fireFileTypesChanged();
1279       }
1280     }
1281   }
1282
1283   public void removeAssociation(FileType fileType, FileNameMatcher matcher, boolean fireChange) {
1284     if (myPatternsTable.isAssociatedWith(fileType, matcher)) {
1285       if (fireChange) {
1286         fireBeforeFileTypesChanged();
1287       }
1288       myPatternsTable.removeAssociation(matcher, fileType);
1289       if (fireChange) {
1290         fireFileTypesChanged();
1291       }
1292     }
1293   }
1294
1295   @Override
1296   @Nullable
1297   public FileType getKnownFileTypeOrAssociate(@NotNull VirtualFile file) {
1298     FileType type = file.getFileType();
1299     if (type != UnknownFileType.INSTANCE) return type;
1300     return FileTypeChooser.getKnownFileTypeOrAssociate(file.getName());
1301   }
1302
1303   @Override
1304   public FileType getKnownFileTypeOrAssociate(@NotNull VirtualFile file, @NotNull Project project) {
1305     return FileTypeChooser.getKnownFileTypeOrAssociate(file, project);
1306   }
1307
1308   private void registerReDetectedMappings(StandardFileType pair) {
1309     FileType fileType = pair.fileType;
1310     if (fileType == PlainTextFileType.INSTANCE) return;
1311     for (FileNameMatcher matcher : pair.matchers) {
1312       registerReDetectedMapping(fileType, matcher);
1313       if (matcher instanceof ExtensionFileNameMatcher) {
1314         // also check exact file name matcher
1315         ExtensionFileNameMatcher extMatcher = (ExtensionFileNameMatcher)matcher;
1316         registerReDetectedMapping(fileType, new ExactFileNameMatcher("." + extMatcher.getExtension()));
1317       }
1318     }
1319   }
1320
1321   private void registerReDetectedMapping(@NotNull FileType fileType, @NotNull FileNameMatcher matcher) {
1322     String typeName = myUnresolvedMappings.get(matcher);
1323     if (typeName != null && !typeName.equals(fileType.getName())) {
1324       Trinity<String, String, Boolean> trinity = myUnresolvedRemovedMappings.get(matcher);
1325       myRemovedMappings.put(matcher, Pair.create(fileType, trinity != null && trinity.third));
1326     }
1327   }
1328
1329   Map<FileNameMatcher, Pair<FileType, Boolean>> getRemovedMappings() {
1330     return myRemovedMappings;
1331   }
1332
1333   @TestOnly
1334   void clearForTests() {
1335     myStandardFileTypes.clear();
1336     myUnresolvedMappings.clear();
1337     mySchemesManager.clearAllSchemes();
1338   }
1339
1340   @Override
1341   public void dispose() {
1342     LOG.info("FileTypeManager: "+ counterAutoDetect +" auto-detected files\nElapsed time on auto-detect: "+elapsedAutoDetect+" ms");
1343   }
1344 }