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