From ae21cc79ffff3e243b8754f2899f14841c7e1e46 Mon Sep 17 00:00:00 2001 From: Alexey Kudravtsev Date: Thu, 19 Mar 2020 18:10:51 +0300 Subject: [PATCH] IDEA-175757 Support File Type mapping based on shebang Add ability to detect file type based on hasbang (#! string) inside the file contents (via FileAssocTable.findAssociatedFileTypeByHashBang()). Store/persist hashbang patterns in FileTypeManagerImpl. Enable users to configure hashbang associations via Settings|File types UI. Allow plugins to configure hashbangs via in plugin.xml Replace some no more needed HashBangFileTypeDetectors for standard langs with xml configs GitOrigin-RevId: 14335912b90f2d4f665d2a71eddeebf5cfc91f30 --- java/java-impl/src/META-INF/JavaPlugin.xml | 3 +- .../fileTypes/impl/JavaFileTypeDetector.java | 10 - .../fileTypes/impl/FileTypeAssocTable.java | 84 ++-- .../fileTypes/impl/FileTypeConfigurable.java | 400 +++++++++++------- .../openapi/fileTypes/impl/FileTypePanel.form | 29 +- .../openapi/fileTypes/impl/FileTypePanel.java | 13 + .../openapi/fileTypes/impl/TypeEditor.java | 56 +++ .../messages/FileTypesBundle.properties | 11 +- .../openapi/fileTypes/impl/FileTypeBean.java | 11 +- .../fileTypes/impl/FileTypeManagerImpl.java | 102 +++-- .../openapi/fileTypes/impl/FileTypesTest.java | 13 + .../GroovyHashBangFileTypeDetector.java | 24 -- plugins/groovy/src/META-INF/plugin.xml | 4 +- plugins/sh/resources/META-INF/plugin.xml | 3 +- .../com/intellij/sh/ShFileTypeDetector.java | 10 - .../resources/META-INF/PythonPsi.xml | 1 + python/src/META-INF/python-core-common.xml | 1 - .../jetbrains/python/PyFileTypeDetector.java | 27 -- 18 files changed, 503 insertions(+), 299 deletions(-) delete mode 100644 java/java-impl/src/com/intellij/openapi/fileTypes/impl/JavaFileTypeDetector.java create mode 100644 platform/lang-impl/src/com/intellij/openapi/fileTypes/impl/FileTypePanel.java create mode 100644 platform/lang-impl/src/com/intellij/openapi/fileTypes/impl/TypeEditor.java delete mode 100644 plugins/groovy/groovy-psi/src/org/jetbrains/plugins/groovy/GroovyHashBangFileTypeDetector.java delete mode 100644 plugins/sh/src/com/intellij/sh/ShFileTypeDetector.java delete mode 100644 python/src/com/jetbrains/python/PyFileTypeDetector.java diff --git a/java/java-impl/src/META-INF/JavaPlugin.xml b/java/java-impl/src/META-INF/JavaPlugin.xml index 7d018f12888d..271a25b2576c 100644 --- a/java/java-impl/src/META-INF/JavaPlugin.xml +++ b/java/java-impl/src/META-INF/JavaPlugin.xml @@ -1259,7 +1259,7 @@ - + @@ -1990,7 +1990,6 @@ - diff --git a/java/java-impl/src/com/intellij/openapi/fileTypes/impl/JavaFileTypeDetector.java b/java/java-impl/src/com/intellij/openapi/fileTypes/impl/JavaFileTypeDetector.java deleted file mode 100644 index 015aab867557..000000000000 --- a/java/java-impl/src/com/intellij/openapi/fileTypes/impl/JavaFileTypeDetector.java +++ /dev/null @@ -1,10 +0,0 @@ -// Copyright 2000-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. -package com.intellij.openapi.fileTypes.impl; - -import com.intellij.ide.highlighter.JavaFileType; - -public class JavaFileTypeDetector extends HashBangFileTypeDetector { - public JavaFileTypeDetector() { - super(JavaFileType.INSTANCE, "java"); - } -} diff --git a/jps/model-impl/src/com/intellij/openapi/fileTypes/impl/FileTypeAssocTable.java b/jps/model-impl/src/com/intellij/openapi/fileTypes/impl/FileTypeAssocTable.java index fb6d90130ed8..a4d8bbd41d27 100644 --- a/jps/model-impl/src/com/intellij/openapi/fileTypes/impl/FileTypeAssocTable.java +++ b/jps/model-impl/src/com/intellij/openapi/fileTypes/impl/FileTypeAssocTable.java @@ -5,6 +5,7 @@ import com.intellij.openapi.fileTypes.ExactFileNameMatcher; import com.intellij.openapi.fileTypes.ExtensionFileNameMatcher; import com.intellij.openapi.fileTypes.FileNameMatcher; import com.intellij.openapi.util.Pair; +import com.intellij.openapi.util.io.FileUtil; import com.intellij.openapi.util.io.FileUtilRt; import com.intellij.util.ArrayUtilRt; import com.intellij.util.text.CharSequenceHashingStrategy; @@ -14,16 +15,19 @@ import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import java.util.*; +import java.util.stream.Collectors; public class FileTypeAssocTable { private final Map myExtensionMappings; private final Map myExactFileNameMappings; private final Map myExactFileNameAnyCaseMappings; private final List> myMatchingMappings; + private final Map myHashBangMap; private FileTypeAssocTable(@NotNull Map extensionMappings, @NotNull Map exactFileNameMappings, - @NotNull Map exactFileNameAnyCaseMappings, + @NotNull Map exactFileNameAnyCaseMappings, + @NotNull Map hashBangMap, @NotNull List> matchingMappings) { myExtensionMappings = new THashMap<>(Math.max(10, extensionMappings.size()), 0.5f, CharSequenceHashingStrategy.CASE_INSENSITIVE); myExtensionMappings.putAll(extensionMappings); @@ -31,11 +35,13 @@ public class FileTypeAssocTable { myExactFileNameMappings.putAll(exactFileNameMappings); myExactFileNameAnyCaseMappings = new THashMap<>(Math.max(10, exactFileNameAnyCaseMappings.size()), 0.5f, CharSequenceHashingStrategy.CASE_INSENSITIVE); myExactFileNameAnyCaseMappings.putAll(exactFileNameAnyCaseMappings); + myHashBangMap = new THashMap<>(Math.max(10, hashBangMap.size()), 0.5f); + myHashBangMap.putAll(hashBangMap); myMatchingMappings = new ArrayList<>(matchingMappings); } public FileTypeAssocTable() { - this(Collections.emptyMap(), Collections.emptyMap(), Collections.emptyMap(), Collections.emptyList()); + this(Collections.emptyMap(), Collections.emptyMap(), Collections.emptyMap(), Collections.emptyMap(), Collections.emptyList()); } boolean isAssociatedWith(@NotNull T type, @NotNull FileNameMatcher matcher) { @@ -65,14 +71,21 @@ public class FileTypeAssocTable { } } - boolean removeAssociation(@NotNull FileNameMatcher matcher, @NotNull T type) { + void addHashBangPattern(@NotNull String hashBang, @NotNull T type) { + myHashBangMap.put(hashBang, type); + } + void removeHashBangPattern(@NotNull String hashBang, @NotNull T type) { + myHashBangMap.remove(hashBang, type); + } + + void removeAssociation(@NotNull FileNameMatcher matcher, @NotNull T type) { if (matcher instanceof ExtensionFileNameMatcher) { String extension = ((ExtensionFileNameMatcher)matcher).getExtension(); if (myExtensionMappings.get(extension) == type) { myExtensionMappings.remove(extension); - return true; + return; } - return false; + return; } if (matcher instanceof ExactFileNameMatcher) { @@ -80,27 +93,26 @@ public class FileTypeAssocTable { String fileName = exactFileNameMatcher.getFileName(); final Map mapToUse = exactFileNameMatcher.isIgnoreCase() ? myExactFileNameAnyCaseMappings : myExactFileNameMappings; - if(mapToUse.get(fileName) == type) { + if (mapToUse.get(fileName) == type) { mapToUse.remove(fileName); - return true; } - return false; + return; } - - return myMatchingMappings.removeIf(assoc -> matcher.equals(assoc.getFirst())); + myMatchingMappings.removeIf(assoc -> matcher.equals(assoc.getFirst())); } - boolean removeAllAssociations(@NotNull T type) { - boolean changed = removeAssociationsFromMap(myExtensionMappings, type, false); + void removeAllAssociations(@NotNull T type) { + removeAssociationsFromMap(myExtensionMappings, type); - changed = removeAssociationsFromMap(myExactFileNameAnyCaseMappings, type, changed); - changed = removeAssociationsFromMap(myExactFileNameMappings, type, changed); + removeAssociationsFromMap(myExactFileNameAnyCaseMappings, type); + removeAssociationsFromMap(myExactFileNameMappings, type); - return myMatchingMappings.removeIf(assoc -> assoc.getSecond() == type); + myMatchingMappings.removeIf(assoc -> assoc.getSecond() == type); + myHashBangMap.entrySet().removeIf(e -> e.getValue().equals(type)); } - private boolean removeAssociationsFromMap(@NotNull Map extensionMappings, @NotNull T type, boolean changed) { - return extensionMappings.entrySet().removeIf(entry -> entry.getValue() == type) || changed; + private void removeAssociationsFromMap(@NotNull Map extensionMappings, @NotNull T type) { + extensionMappings.entrySet().removeIf(entry -> entry.getValue() == type); } @Nullable @@ -117,13 +129,22 @@ public class FileTypeAssocTable { //noinspection ForLoopReplaceableByForEach for (int i = 0; i < myMatchingMappings.size(); i++) { - final Pair mapping = myMatchingMappings.get(i); + Pair mapping = myMatchingMappings.get(i); if (mapping.getFirst().acceptsCharSequence(fileName)) return mapping.getSecond(); } return findByExtension(FileUtilRt.getExtension(fileName)); } + @Nullable + T findAssociatedFileTypeByHashBang(@NotNull CharSequence content) { + for (Map.Entry entry : myHashBangMap.entrySet()) { + String hashBang = entry.getKey(); + if (FileUtil.isHashBangLine(content, hashBang)) return entry.getValue(); + } + return null; + } + @Nullable T findAssociatedFileType(@NotNull FileNameMatcher matcher) { if (matcher instanceof ExtensionFileNameMatcher) { @@ -148,20 +169,19 @@ public class FileTypeAssocTable { return myExtensionMappings.get(extension); } - @Deprecated String @NotNull [] getAssociatedExtensions(@NotNull T type) { - List exts = new ArrayList<>(); + List extensions = new ArrayList<>(); for (Map.Entry entry : myExtensionMappings.entrySet()) { if (entry.getValue() == type) { - exts.add(entry.getKey().toString()); + extensions.add(entry.getKey().toString()); } } - return ArrayUtilRt.toStringArray(exts); + return ArrayUtilRt.toStringArray(extensions); } @NotNull public FileTypeAssocTable copy() { - return new FileTypeAssocTable<>(myExtensionMappings, myExactFileNameMappings, myExactFileNameAnyCaseMappings, myMatchingMappings); + return new FileTypeAssocTable<>(myExtensionMappings, myExactFileNameMappings, myExactFileNameAnyCaseMappings, myHashBangMap, myMatchingMappings); } @NotNull @@ -192,9 +212,18 @@ public class FileTypeAssocTable { return result; } + @NotNull + public List getHashBangPatterns(@NotNull T type) { + return myHashBangMap.entrySet().stream() + .filter(e -> e.getValue().equals(type)) + .map(e->e.getKey()) + .collect(Collectors.toList()); + } + boolean hasAssociationsFor(@NotNull T fileType) { if (myExtensionMappings.containsValue(fileType) || myExactFileNameMappings.containsValue(fileType) || + myHashBangMap.containsValue(fileType) || myExactFileNameAnyCaseMappings.containsValue(fileType)) { return true; } @@ -228,10 +257,11 @@ public class FileTypeAssocTable { return false; } - FileTypeAssocTable that = (FileTypeAssocTable)o; + FileTypeAssocTable that = (FileTypeAssocTable)o; return myExtensionMappings.equals(that.myExtensionMappings) && myMatchingMappings.equals(that.myMatchingMappings) && myExactFileNameMappings.equals(that.myExactFileNameMappings) && + myHashBangMap.equals(that.myHashBangMap) && myExactFileNameAnyCaseMappings.equals(that.myExactFileNameAnyCaseMappings); } @@ -239,8 +269,14 @@ public class FileTypeAssocTable { public int hashCode() { int result = myExtensionMappings.hashCode(); result = 31 * result + myMatchingMappings.hashCode(); + result = 31 * result + myHashBangMap.hashCode(); result = 31 * result + myExactFileNameMappings.hashCode(); result = 31 * result + myExactFileNameAnyCaseMappings.hashCode(); return result; } + + @NotNull + Map getAllHashBangPatterns() { + return Collections.unmodifiableMap(new THashMap<>(myHashBangMap)); + } } diff --git a/platform/lang-impl/src/com/intellij/openapi/fileTypes/impl/FileTypeConfigurable.java b/platform/lang-impl/src/com/intellij/openapi/fileTypes/impl/FileTypeConfigurable.java index 41424f6c38e4..cbba81744def 100644 --- a/platform/lang-impl/src/com/intellij/openapi/fileTypes/impl/FileTypeConfigurable.java +++ b/platform/lang-impl/src/com/intellij/openapi/fileTypes/impl/FileTypeConfigurable.java @@ -8,14 +8,10 @@ import com.intellij.lang.Language; import com.intellij.openapi.application.ApplicationManager; import com.intellij.openapi.fileTypes.*; import com.intellij.openapi.options.Configurable; -import com.intellij.openapi.options.ConfigurationException; import com.intellij.openapi.options.SearchableConfigurable; -import com.intellij.openapi.options.SettingsEditor; import com.intellij.openapi.ui.DialogBuilder; -import com.intellij.openapi.ui.DialogWrapper; import com.intellij.openapi.ui.Messages; import com.intellij.openapi.util.Condition; -import com.intellij.openapi.util.Disposer; import com.intellij.openapi.util.Pair; import com.intellij.openapi.util.text.StringUtil; import com.intellij.openapi.wm.IdeFocusManager; @@ -44,6 +40,7 @@ public class FileTypeConfigurable implements SearchableConfigurable, Configurabl private RecognizedFileTypes myRecognizedFileType; private PatternsPanel myPatterns; + private HashBangPanel myHashBangs; private FileTypePanel myFileTypePanel; private Set myTempFileTypes; private final FileTypeManagerImpl myManager; @@ -62,14 +59,15 @@ public class FileTypeConfigurable implements SearchableConfigurable, Configurabl @Override public JComponent createComponent() { - myFileTypePanel = new FileTypePanel().init(); - myRecognizedFileType = myFileTypePanel.myRecognizedFileType; - myPatterns = myFileTypePanel.myPatterns; - myRecognizedFileType.attachActions(this); - myRecognizedFileType.myFileTypesList.addListSelectionListener(e -> updateExtensionList()); - myPatterns.attachActions(this); + myFileTypePanel = new FileTypePanel(); + myFileTypePanel.myIgnorePanel.setBorder( + IdeBorderFactory.createTitledBorder(IdeBundle.message("editbox.ignore.files.and.folders"), false, TITLE_INSETS).setShowLine(false)); + myRecognizedFileType = new RecognizedFileTypes(myFileTypePanel.myRecognizedFileTypesPanel); + myPatterns = new PatternsPanel(myFileTypePanel.myPatternsPanel); + myHashBangs = new HashBangPanel(myFileTypePanel.myHashBangPanel); + myRecognizedFileType.myFileTypesList.addListSelectionListener(__ -> updateExtensionList()); myFileTypePanel.myIgnoreFilesField.setColumns(30); - return myFileTypePanel.getComponent(); + return myFileTypePanel.myWholePanel; } private void updateFileTypeList() { @@ -125,10 +123,15 @@ public class FileTypeConfigurable implements SearchableConfigurable, Configurabl @Override public void disposeUIResources() { - if (myFileTypePanel != null) myFileTypePanel.dispose(); + if (myFileTypePanel != null) { + myRecognizedFileType.setFileTypes(FileType.EMPTY_ARRAY); + myPatterns.clearList(); + myHashBangs.clearList(); + } myFileTypePanel = null; myRecognizedFileType = null; myPatterns = null; + myHashBangs = null; } private static class ExtensionRenderer extends DefaultListCellRenderer { @@ -155,12 +158,8 @@ public class FileTypeConfigurable implements SearchableConfigurable, Configurabl extensions.add(assoc.getPresentableString()); } - myPatterns.clearList(); - Collections.sort(extensions); - for (String extension : extensions) { - myPatterns.addPattern(extension); - } - myPatterns.ensureSelectionExists(); + myPatterns.refill(extensions); + myHashBangs.refill(myTempPatternsTable.getHashBangPatterns(type)); } private void editFileType() { @@ -224,7 +223,7 @@ public class FileTypeConfigurable implements SearchableConfigurable, Configurabl Language oldLanguage = item == null ? null : myTempTemplateDataLanguages.findAssociatedFileType(item); FileTypePatternDialog dialog = new FileTypePatternDialog(item, type, oldLanguage); - DialogBuilder builder = new DialogBuilder(myPatterns); + DialogBuilder builder = new DialogBuilder(myPatterns.myList); builder.setPreferredFocusComponent(dialog.getPatternField()); builder.setCenterPanel(dialog.getMainPanel()); builder.setTitle(title); @@ -237,16 +236,16 @@ public class FileTypeConfigurable implements SearchableConfigurable, Configurabl FileType registeredFileType = findExistingFileType(matcher); if (registeredFileType != null && registeredFileType != type) { if (registeredFileType.isReadOnly()) { - Messages.showMessageDialog(myPatterns.myPatternsList, + Messages.showMessageDialog(myPatterns.myList, FileTypesBundle.message("filetype.edit.add.pattern.exists.error", registeredFileType.getDescription()), title, Messages.getErrorIcon()); return; } - int ret = Messages.showOkCancelDialog(myPatterns.myPatternsList, FileTypesBundle.message("filetype.edit.add.pattern.exists.message", - registeredFileType.getDescription()), - FileTypesBundle.message("filetype.edit.add.pattern.exists.title"), - FileTypesBundle.message("filetype.edit.add.pattern.reassign.button"), - CommonBundle.getCancelButtonText(), Messages.getQuestionIcon()); + int ret = Messages.showOkCancelDialog(myPatterns.myList, FileTypesBundle.message("filetype.edit.add.pattern.exists.message", + registeredFileType.getDescription()), + FileTypesBundle.message("filetype.edit.add.pattern.exists.title"), + FileTypesBundle.message("filetype.edit.add.pattern.reassign.button"), + CommonBundle.getCancelButtonText(), Messages.getQuestionIcon()); if (ret == Messages.OK) { myTempPatternsTable.removeAssociation(matcher, registeredFileType); if (oldLanguage != null) { @@ -272,11 +271,8 @@ public class FileTypeConfigurable implements SearchableConfigurable, Configurabl } updateExtensionList(); - int index = myPatterns.getListModel().indexOf(matcher.getPresentableString()); - if (index >= 0) { - ScrollingUtil.selectItem(myPatterns.myPatternsList, index); - } - IdeFocusManager.getGlobalInstance().doWhenFocusSettlesDown(() -> IdeFocusManager.getGlobalInstance().requestFocus(myPatterns.myPatternsList, true)); + myPatterns.select(pattern); + IdeFocusManager.getGlobalInstance().doWhenFocusSettlesDown(() -> IdeFocusManager.getGlobalInstance().requestFocus(myPatterns.myList, true)); } } @@ -286,10 +282,10 @@ public class FileTypeConfigurable implements SearchableConfigurable, Configurabl @Nullable private FileType findExistingFileType(@NotNull FileNameMatcher matcher) { - FileType fileTypeByExtension = myTempPatternsTable.findAssociatedFileType(matcher); + FileType type = myTempPatternsTable.findAssociatedFileType(matcher); - if (fileTypeByExtension != null && fileTypeByExtension != FileTypes.UNKNOWN) { - return fileTypeByExtension; + if (type != null && type != FileTypes.UNKNOWN) { + return type; } FileType registeredFileType = FileTypeManager.getInstance().getFileTypeByExtension(matcher.getPresentableString()); if (registeredFileType != FileTypes.UNKNOWN && registeredFileType.isReadOnly()) { @@ -306,7 +302,16 @@ public class FileTypeConfigurable implements SearchableConfigurable, Configurabl FileNameMatcher matcher = FileTypeManager.parseFromString(extension); myTempPatternsTable.removeAssociation(matcher, type); - IdeFocusManager.getGlobalInstance().doWhenFocusSettlesDown(() -> IdeFocusManager.getGlobalInstance().requestFocus(myPatterns.myPatternsList, true)); + IdeFocusManager.getGlobalInstance().doWhenFocusSettlesDown(() -> IdeFocusManager.getGlobalInstance().requestFocus(myPatterns.myList, true)); + } + private void removeHashBang() { + FileType type = myRecognizedFileType.getSelectedFileType(); + if (type == null) return; + String extension = myHashBangs.removeSelected(); + if (extension == null) return; + + myTempPatternsTable.removeHashBangPattern(extension, type); + IdeFocusManager.getGlobalInstance().doWhenFocusSettlesDown(() -> IdeFocusManager.getGlobalInstance().requestFocus(myHashBangs.myList, true)); } @NotNull @@ -315,13 +320,10 @@ public class FileTypeConfigurable implements SearchableConfigurable, Configurabl return "preferences.fileTypes"; } - public static class RecognizedFileTypes extends JPanel { + class RecognizedFileTypes { private final JList myFileTypesList = new JBList<>(new DefaultListModel<>()); - private final MySpeedSearch mySpeedSearch; - private FileTypeConfigurable myController; - - public RecognizedFileTypes() { - super(new BorderLayout()); + RecognizedFileTypes(@NotNull JPanel panel) { + panel.setLayout(new BorderLayout()); myFileTypesList.setSelectionMode(ListSelectionModel.SINGLE_SELECTION); myFileTypesList.setCellRenderer(new FileTypeRenderer(() -> { @@ -335,15 +337,15 @@ public class FileTypeConfigurable implements SearchableConfigurable, Configurabl new DoubleClickListener() { @Override protected boolean onDoubleClick(@NotNull MouseEvent e) { - myController.editFileType(); + editFileType(); return true; } }.installOn(myFileTypesList); ToolbarDecorator toolbarDecorator = ToolbarDecorator.createDecorator(myFileTypesList) - .setAddAction(button -> myController.addFileType()) - .setRemoveAction(button -> myController.removeFileType()) - .setEditAction(button -> myController.editFileType()) + .setAddAction(__ -> addFileType()) + .setRemoveAction(__ -> removeFileType()) + .setEditAction(__ -> editFileType()) .setEditActionUpdater(e -> { FileType fileType = getSelectedFileType(); return canBeModified(fileType); @@ -351,15 +353,14 @@ public class FileTypeConfigurable implements SearchableConfigurable, Configurabl .setRemoveActionUpdater(e -> canBeModified(getSelectedFileType())) .disableUpDownActions(); - add(toolbarDecorator.createPanel(), BorderLayout.CENTER); - setBorder(IdeBorderFactory.createTitledBorder(FileTypesBundle.message("filetype.recognized.group"), false, TITLE_INSETS).setShowLine(false)); + panel.add(toolbarDecorator.createPanel(), BorderLayout.CENTER); + panel.setBorder(IdeBorderFactory.createTitledBorder(FileTypesBundle.message("filetype.recognized.group"), false, TITLE_INSETS).setShowLine(false)); - mySpeedSearch = new MySpeedSearch(myFileTypesList); + new MySpeedSearch(myFileTypesList); } - private static class MySpeedSearch extends SpeedSearchBase> { + private class MySpeedSearch extends SpeedSearchBase> { private final List>> myOrderedConverters; - private FileTypeConfigurable myController; private Object myCurrentType; private String myExtension; @@ -408,43 +409,34 @@ public class FileTypeConfigurable implements SearchableConfigurable, Configurabl protected void selectElement(Object element, String selectedText) { if (element != null) { ScrollingUtil.selectItem(myComponent, (FileType)element); - if (myCurrentType != null && myCurrentType.equals(element) && myController != null) { - myController.myPatterns.select(myExtension); + if (element.equals(myCurrentType)) { + myPatterns.select(myExtension); } } } @Override protected void onSearchFieldUpdated(String s) { - if (myController == null || myController.myTempPatternsTable == null) return; + if (myTempPatternsTable == null) return; int index = s.lastIndexOf('.'); if (index < 0) { s = "." + s; } - myCurrentType = myController.myTempPatternsTable.findAssociatedFileType(s); + myCurrentType = myTempPatternsTable.findAssociatedFileType(s); if (myCurrentType != null) { myExtension = s; - } else { + } + else { myExtension = null; } } } - void attachActions(@NotNull FileTypeConfigurable controller) { - myController = controller; - mySpeedSearch.myController = controller; - } - FileType getSelectedFileType() { return myFileTypesList.getSelectedValue(); } - @NotNull - public JComponent getComponent() { - return this; - } - - public void setFileTypes(FileType @NotNull [] types) { + void setFileTypes(FileType @NotNull [] types) { DefaultListModel listModel = (DefaultListModel)myFileTypesList.getModel(); listModel.clear(); for (FileType type : types) { @@ -455,60 +447,47 @@ public class FileTypeConfigurable implements SearchableConfigurable, Configurabl ScrollingUtil.ensureSelectionExists(myFileTypesList); } - void selectFileType(FileType fileType) { + void selectFileType(@NotNull FileType fileType) { myFileTypesList.setSelectedValue(fileType, true); IdeFocusManager.getGlobalInstance().doWhenFocusSettlesDown(() -> IdeFocusManager.getGlobalInstance().requestFocus(myFileTypesList, true)); } } - static class PatternsPanel extends JPanel { - private final JBList myPatternsList = new JBList<>(new DefaultListModel<>()); - private FileTypeConfigurable myController; + class PatternsPanel { + private final JBList myList = new JBList<>(new DefaultListModel<>()); - PatternsPanel() { - super(new BorderLayout()); - myPatternsList.setSelectionMode(ListSelectionModel.SINGLE_SELECTION); - myPatternsList.setCellRenderer(new ExtensionRenderer()); - myPatternsList.getEmptyText().setText(FileTypesBundle.message("filetype.settings.no.patterns")); + PatternsPanel(@NotNull JPanel panel) { + panel.setLayout(new BorderLayout()); + myList.setSelectionMode(ListSelectionModel.SINGLE_SELECTION); + myList.setCellRenderer(new ExtensionRenderer()); + myList.getEmptyText().setText(FileTypesBundle.message("filetype.settings.no.patterns")); - ToolbarDecorator decorator = ToolbarDecorator.createDecorator(myPatternsList) - .setAddAction(__ -> myController.addPattern()) - .setEditAction(__ -> myController.editPattern()) - .setRemoveAction(__ -> myController.removePattern()) + ToolbarDecorator decorator = ToolbarDecorator.createDecorator(myList) + .setAddAction(__ -> addPattern()) + .setEditAction(__ -> editPattern()) + .setRemoveAction(__ -> removePattern()) .disableUpDownActions(); - add(decorator.createPanel(), BorderLayout.CENTER); + panel.add(decorator.createPanel(), BorderLayout.CENTER); - setBorder(IdeBorderFactory.createTitledBorder(FileTypesBundle.message("filetype.registered.patterns.group"), false, TITLE_INSETS).setShowLine(false)); - } - - void attachActions(@NotNull FileTypeConfigurable controller) { - myController = controller; + panel.setBorder(IdeBorderFactory.createTitledBorder(FileTypesBundle.message("filetype.registered.patterns.group"), false, TITLE_INSETS).setShowLine(false)); } void clearList() { getListModel().clear(); - myPatternsList.clearSelection(); + myList.clearSelection(); } @NotNull private DefaultListModel getListModel() { - return (DefaultListModel)myPatternsList.getModel(); - } - - void addPattern(@NotNull String pattern) { - getListModel().addElement(pattern); - } - - void ensureSelectionExists() { - ScrollingUtil.ensureSelectionExists(myPatternsList); + return (DefaultListModel)myList.getModel(); } void select(@NotNull String pattern) { - for (int i = 0; i < myPatternsList.getItemsCount(); i++) { - String at = myPatternsList.getModel().getElementAt(i); + for (int i = 0; i < myList.getItemsCount(); i++) { + String at = myList.getModel().getElementAt(i); FileNameMatcher matcher = FileTypeManager.parseFromString(at); if (matcher.acceptsCharSequence(pattern)) { - ScrollingUtil.selectItem(myPatternsList, i); + ScrollingUtil.selectItem(myList, i); return; } } @@ -517,79 +496,184 @@ public class FileTypeConfigurable implements SearchableConfigurable, Configurabl String removeSelected() { String selectedValue = getSelectedItem(); if (selectedValue == null) return null; - ListUtil.removeSelectedItems(myPatternsList); + ListUtil.removeSelectedItems(myList); return selectedValue; } String getSelectedItem() { - return myPatternsList.getSelectedValue(); - } - } - - private static class FileTypePanel { - private JPanel myWholePanel; - private RecognizedFileTypes myRecognizedFileType; - private PatternsPanel myPatterns; - private JTextField myIgnoreFilesField; - private JPanel myIgnorePanel; - - JComponent getComponent() { - return myWholePanel; + return myList.getSelectedValue(); } - void dispose() { - myRecognizedFileType.setFileTypes(FileType.EMPTY_ARRAY); - myPatterns.clearList(); - } - - private FileTypePanel init() { - myIgnorePanel.setBorder( - IdeBorderFactory.createTitledBorder(IdeBundle.message("editbox.ignore.files.and.folders"), false, TITLE_INSETS).setShowLine(false)); - return this; - } - } - - private static class TypeEditor> extends DialogWrapper { - private final T myFileType; - private final SettingsEditor myEditor; - - TypeEditor(Component parent, T fileType, String title) { - super(parent, false); - myFileType = fileType; - myEditor = fileType.getEditor(); - setTitle(title); - init(); - Disposer.register(myDisposable, myEditor); - } - - @Override - protected void init() { - super.init(); - myEditor.resetFrom(myFileType); - } - - @Override - protected JComponent createCenterPanel() { - return myEditor.getComponent(); - } - - @Override - protected void doOKAction() { - try { - myEditor.applyTo(myFileType); + private void refill(@NotNull List extensions) { + clearList(); + Collections.sort(extensions); + for (String extension : extensions) { + getListModel().addElement(extension); } - catch (ConfigurationException e) { - Messages.showErrorDialog(getContentPane(), e.getMessage(), e.getTitle()); + ScrollingUtil.ensureSelectionExists(myList); + } + } + + class HashBangPanel { + private final JBList myList = new JBList<>(new DefaultListModel<>()); + + HashBangPanel(@NotNull JPanel panel) { + panel.setLayout(new BorderLayout()); + myList.setSelectionMode(ListSelectionModel.SINGLE_SELECTION); + myList.setCellRenderer(new ExtensionRenderer(){ + @Override + public @NotNull Component getListCellRendererComponent(@NotNull JList list, + Object value, + int index, + boolean isSelected, + boolean cellHasFocus) { + Component component = super.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus); + setText(" #!*"+value+"*"); + return component; + } + }); + myList.getEmptyText().setText(FileTypesBundle.message("filetype.settings.no.patterns")); + + ToolbarDecorator decorator = ToolbarDecorator.createDecorator(myList) + .setAddAction(__ -> editHashBang(null)) + .setAddActionName("Add HashBang Pattern") + .setEditAction(__ -> editHashBang()) + .setRemoveAction(__ -> removeHashBang()) + .disableUpDownActions(); + + panel.add(decorator.createPanel(), BorderLayout.CENTER); + + panel.setBorder(IdeBorderFactory.createTitledBorder(FileTypesBundle.message("filetype.hashbang.group"), false, TITLE_INSETS).setShowLine(false)); + } + + void clearList() { + getListModel().clear(); + myList.clearSelection(); + } + + @NotNull + private DefaultListModel getListModel() { + return (DefaultListModel)myList.getModel(); + } + + void select(@NotNull String pattern) { + ScrollingUtil.selectItem(myList, pattern); + } + + String removeSelected() { + String selectedValue = getSelectedItem(); + if (selectedValue == null) return null; + ListUtil.removeSelectedItems(myList); + return selectedValue; + } + + String getSelectedItem() { + return myList.getSelectedValue(); + } + + private void refill(@NotNull List values) { + clearList(); + Collections.sort(values); + for (String extension : values) { + getListModel().addElement(extension); + } + ScrollingUtil.ensureSelectionExists(myList); + } + } + + private void editHashBang() { + String item = myHashBangs.getSelectedItem(); + if (item == null) return; + + editHashBang(item); + } + private void editHashBang(@Nullable("null means new") String oldHashBang) { + FileType type = myRecognizedFileType.getSelectedFileType(); + if (type == null) return; + + String title = FileTypesBundle.message("filetype.edit.hashbang.title"); + + Language oldLanguage = oldHashBang == null ? null : myTempTemplateDataLanguages.findAssociatedFileType(oldHashBang); + String hashbang = Messages.showInputDialog(myHashBangs.myList, FileTypesBundle.message("filetype.edit.hashbang.prompt"), title, null, oldHashBang, null); + if (StringUtil.isEmpty(hashbang)) { + return; //canceled or empty + } + HashBangConflict conflict = checkHashBangConflict(hashbang); + if (conflict != null) { + FileType existingFileType = conflict.fileType; + if (existingFileType == type) return; // ignore duplicate + if (!conflict.writeable) { + String message = conflict.exact ? FileTypesBundle.message("filetype.edit.hashbang.exists.exact.error", existingFileType.getDescription()) + : FileTypesBundle.message("filetype.edit.hashbang.exists.similar.error", existingFileType.getDescription(), conflict.existingHashBang); + Messages.showMessageDialog(myHashBangs.myList, message, title, Messages.getErrorIcon()); return; } - super.doOKAction(); + String message = conflict.exact ? FileTypesBundle.message("filetype.edit.hashbang.exists.exact.message", existingFileType.getDescription()) + : FileTypesBundle.message("filetype.edit.hashbang.exists.similar.message", existingFileType.getDescription(), conflict.existingHashBang); + int ret = Messages.showOkCancelDialog(myHashBangs.myList, message, + FileTypesBundle.message("filetype.edit.hashbang.exists.title"), + FileTypesBundle.message("filetype.edit.hashbang.reassign.button"), + CommonBundle.getCancelButtonText(), Messages.getQuestionIcon()); + if (ret != Messages.OK) { + return; + } + myTempPatternsTable.removeHashBangPattern(hashbang, existingFileType); + if (oldLanguage != null) { + myTempTemplateDataLanguages.removeHashBangPattern(hashbang, oldLanguage); + } + myTempPatternsTable.removeHashBangPattern(conflict.existingHashBang, conflict.fileType); } + if (oldHashBang != null) { + myTempPatternsTable.removeHashBangPattern(oldHashBang, type); + if (oldLanguage != null) { + myTempTemplateDataLanguages.removeHashBangPattern(oldHashBang, oldLanguage); + } + } + myTempPatternsTable.addHashBangPattern(hashbang, type); - @Override - protected String getHelpId() { - //noinspection SpellCheckingInspection - return "reference.dialogs.newfiletype"; + updateExtensionList(); + myHashBangs.select(hashbang); + IdeFocusManager.getGlobalInstance().doWhenFocusSettlesDown(() -> IdeFocusManager.getGlobalInstance().requestFocus(myPatterns.myList, true)); + } + + // describes conflict between two hashbang patterns when user tried to create new/edit existing hashbang + private static class HashBangConflict { + FileType fileType; // conflicting file type + boolean exact; // true: conflict with the file type with the exactly the same hashbang/false: similar hashbang (more selective or less selective) + boolean writeable; //file type can be changed + String existingHashBang; // the hashbang of the conflicting file type + } + private static boolean isStandardFileType(@NotNull FileType fileType) { + return FileTypeManager.getInstance().getStdFileType(fileType.getName()) == fileType; + } + + // check if there is a conflict between new hasbang and exising ones + private HashBangConflict checkHashBangConflict(@NotNull String hashbang) { + HashBangConflict conflict = new HashBangConflict(); + for (Map.Entry entry : myTempPatternsTable.getAllHashBangPatterns().entrySet()) { + String existingHashBang = entry.getKey(); + if (hashbang.contains(existingHashBang) || existingHashBang.contains(hashbang)) { + conflict.fileType = entry.getValue(); + conflict.exact = existingHashBang.equals(hashbang); + conflict.writeable = !conflict.fileType.isReadOnly() && !isStandardFileType(conflict.fileType); + conflict.existingHashBang = existingHashBang; + return conflict; + } } + List detectors = FileTypeRegistry.FileTypeDetector.EP_NAME.getExtensionList(); + for (FileTypeRegistry.FileTypeDetector detector : detectors) { + if (detector instanceof HashBangFileTypeDetector) { + String existingHashBang = ((HashBangFileTypeDetector)detector).getMarker(); + if (hashbang.contains(existingHashBang) || existingHashBang.contains(hashbang)) { + conflict.fileType = ((HashBangFileTypeDetector)detector).getFileType(); + conflict.exact = existingHashBang.equals(hashbang); + conflict.writeable = false; + conflict.existingHashBang = existingHashBang; + return conflict; + } + } + } + return null; } @NotNull diff --git a/platform/lang-impl/src/com/intellij/openapi/fileTypes/impl/FileTypePanel.form b/platform/lang-impl/src/com/intellij/openapi/fileTypes/impl/FileTypePanel.form index e9d30ddbd9b8..135732f929e3 100644 --- a/platform/lang-impl/src/com/intellij/openapi/fileTypes/impl/FileTypePanel.form +++ b/platform/lang-impl/src/com/intellij/openapi/fileTypes/impl/FileTypePanel.form @@ -1,6 +1,6 @@ -
- + + @@ -8,22 +8,28 @@ - + + - - + + + + + - + + + - + @@ -45,6 +51,15 @@ + + + + + + + + +
diff --git a/platform/lang-impl/src/com/intellij/openapi/fileTypes/impl/FileTypePanel.java b/platform/lang-impl/src/com/intellij/openapi/fileTypes/impl/FileTypePanel.java new file mode 100644 index 000000000000..07bf11953bca --- /dev/null +++ b/platform/lang-impl/src/com/intellij/openapi/fileTypes/impl/FileTypePanel.java @@ -0,0 +1,13 @@ +// Copyright 2000-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. +package com.intellij.openapi.fileTypes.impl; + +import javax.swing.*; + +class FileTypePanel { + JPanel myWholePanel; + JPanel myRecognizedFileTypesPanel; + JPanel myPatternsPanel; + JTextField myIgnoreFilesField; + JPanel myIgnorePanel; + JPanel myHashBangPanel; +} diff --git a/platform/lang-impl/src/com/intellij/openapi/fileTypes/impl/TypeEditor.java b/platform/lang-impl/src/com/intellij/openapi/fileTypes/impl/TypeEditor.java new file mode 100644 index 000000000000..cb564a756527 --- /dev/null +++ b/platform/lang-impl/src/com/intellij/openapi/fileTypes/impl/TypeEditor.java @@ -0,0 +1,56 @@ +// Copyright 2000-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. +package com.intellij.openapi.fileTypes.impl; + +import com.intellij.openapi.fileTypes.UserFileType; +import com.intellij.openapi.options.ConfigurationException; +import com.intellij.openapi.options.SettingsEditor; +import com.intellij.openapi.ui.DialogWrapper; +import com.intellij.openapi.ui.Messages; +import com.intellij.openapi.util.Disposer; +import org.jetbrains.annotations.NotNull; + +import javax.swing.*; +import java.awt.*; + +class TypeEditor> extends DialogWrapper { + private final T myFileType; + private final SettingsEditor myEditor; + + TypeEditor(@NotNull Component parent, @NotNull T fileType, @NotNull String title) { + super(parent, false); + myFileType = fileType; + myEditor = fileType.getEditor(); + setTitle(title); + init(); + Disposer.register(myDisposable, myEditor); + } + + @Override + protected void init() { + super.init(); + myEditor.resetFrom(myFileType); + } + + @Override + protected JComponent createCenterPanel() { + return myEditor.getComponent(); + } + + @Override + protected void doOKAction() { + try { + myEditor.applyTo(myFileType); + } + catch (ConfigurationException e) { + Messages.showErrorDialog(getContentPane(), e.getMessage(), e.getTitle()); + return; + } + super.doOKAction(); + } + + @Override + protected String getHelpId() { + //noinspection SpellCheckingInspection + return "reference.dialogs.newfiletype"; + } +} diff --git a/platform/platform-impl/resources/messages/FileTypesBundle.properties b/platform/platform-impl/resources/messages/FileTypesBundle.properties index d27dfbfe40ca..648fd0b3c96e 100644 --- a/platform/platform-impl/resources/messages/FileTypesBundle.properties +++ b/platform/platform-impl/resources/messages/FileTypesBundle.properties @@ -2,7 +2,7 @@ filetype.chooser.title=Register New File Type Association filetype.chooser.prompt=The file ''{0}'' is not associated with any file type. Please define the association: filetype.chooser.change.prompt=The file ''{0}'' is associated with {1} file type. Please define a new association: filetype.chooser.association=Open matching files in {0}: -filetype.registered.patterns.group=Registered Patterns: +filetype.registered.patterns.group=File Name Patterns: filetype.settings.title=File Types filetype.edit.existing.title=Edit File Type filetype.edit.new.title=New File Type @@ -17,5 +17,14 @@ filetype.settings.component=File types filetype.settings.no.patterns=No registered file patterns filetype.recognized.group=Recognized File Types: filetype.chooser.file.pattern=File &pattern: +filetype.hashbang.group=HashBang Patterns: +filetype.edit.hashbang.title=Edit HashBang Pattern +filetype.edit.hashbang.exists.exact.error=This hashbang is reserved for ''{0}'' filetype and cannot be reassigned +filetype.edit.hashbang.exists.similar.error=The similar hashbang (''{1}'') is reserved for ''{0}'' filetype and cannot be reassigned +filetype.edit.hashbang.exists.exact.message=This hashbang is already registered by ''{0}'' filetype +filetype.edit.hashbang.exists.similar.message=The similar hashbang (''{1}'') is already registered by ''{0}'' filetype +filetype.edit.hashbang.reassign.button=&Reassign hashbang +filetype.edit.hashbang.exists.title=Add HashBang +filetype.edit.hashbang.prompt=HashBang substring (E.g. 'sh' to be associated with hashbang '#!/bin/sh'): notification.file.extension.0.was.reassigned.to.1.revert=File extension {0} was reassigned to {1} Revert notification.title.file.type.recognized=File type recognized diff --git a/platform/platform-impl/src/com/intellij/openapi/fileTypes/impl/FileTypeBean.java b/platform/platform-impl/src/com/intellij/openapi/fileTypes/impl/FileTypeBean.java index 27e9a38de30c..bcd66740fb94 100644 --- a/platform/platform-impl/src/com/intellij/openapi/fileTypes/impl/FileTypeBean.java +++ b/platform/platform-impl/src/com/intellij/openapi/fileTypes/impl/FileTypeBean.java @@ -82,13 +82,20 @@ public final class FileTypeBean implements PluginAware { @Attribute("language") public String language; + /** + * Semicolon-separated list of hash bang patterns to be associated with the file type + */ + @Attribute("hashBangs") + @NonNls + public String hashBangs; + @ApiStatus.Internal - public void addMatchers(List matchers) { + public void addMatchers(@NotNull List matchers) { myMatchers.addAll(matchers); } @ApiStatus.Internal - public List getMatchers() { + public @NotNull List getMatchers() { return new ArrayList<>(myMatchers); } diff --git a/platform/platform-impl/src/com/intellij/openapi/fileTypes/impl/FileTypeManagerImpl.java b/platform/platform-impl/src/com/intellij/openapi/fileTypes/impl/FileTypeManagerImpl.java index a9b7f25cd485..ddf7f28a3c9d 100644 --- a/platform/platform-impl/src/com/intellij/openapi/fileTypes/impl/FileTypeManagerImpl.java +++ b/platform/platform-impl/src/com/intellij/openapi/fileTypes/impl/FileTypeManagerImpl.java @@ -14,7 +14,10 @@ import com.intellij.openapi.components.PersistentStateComponent; import com.intellij.openapi.components.State; import com.intellij.openapi.components.Storage; import com.intellij.openapi.diagnostic.Logger; -import com.intellij.openapi.extensions.*; +import com.intellij.openapi.extensions.ExtensionPointListener; +import com.intellij.openapi.extensions.ExtensionPointName; +import com.intellij.openapi.extensions.PluginDescriptor; +import com.intellij.openapi.extensions.PluginId; import com.intellij.openapi.fileEditor.impl.LoadTextUtil; import com.intellij.openapi.fileTypes.*; import com.intellij.openapi.fileTypes.ex.ExternalizableFileType; @@ -364,7 +367,7 @@ public class FileTypeManagerImpl extends FileTypeManagerEx implements Persistent }); for (StandardFileType pair : myStandardFileTypes.values()) { - registerFileTypeWithoutNotification(pair.fileType, pair.matchers, true); + registerFileTypeWithoutNotification(pair.fileType, pair.matchers, Collections.emptyList(), true); } try { @@ -438,7 +441,7 @@ public class FileTypeManagerImpl extends FileTypeManagerEx implements Persistent } } - private static void initializeMatchers(FileTypeBean bean) { + private static void initializeMatchers(@NotNull FileTypeBean bean) { bean.addMatchers(ContainerUtil.concat( parse(bean.extensions), parse(bean.fileNames, token -> new ExactFileNameMatcher(token)), @@ -463,14 +466,14 @@ public class FileTypeManagerImpl extends FileTypeManagerEx implements Persistent return type.fileType; } - private FileType instantiateFileTypeBean(@NotNull FileTypeBean fileTypeBean) { + private FileType instantiateFileTypeBean(@NotNull FileTypeBean bean) { FileType fileType; - PluginId pluginId = fileTypeBean.getPluginDescriptor().getPluginId(); + PluginId pluginId = bean.getPluginDescriptor().getPluginId(); try { @SuppressWarnings("unchecked") - Class beanClass = (Class)Class.forName(fileTypeBean.implementationClass, true, fileTypeBean.getPluginDescriptor().getPluginClassLoader()); - if (fileTypeBean.fieldName != null) { - Field field = beanClass.getDeclaredField(fileTypeBean.fieldName); + Class beanClass = (Class)Class.forName(bean.implementationClass, true, bean.getPluginDescriptor().getPluginClassLoader()); + if (bean.fieldName != null) { + Field field = beanClass.getDeclaredField(bean.fieldName); field.setAccessible(true); fileType = (FileType)field.get(null); } @@ -484,23 +487,24 @@ public class FileTypeManagerImpl extends FileTypeManagerEx implements Persistent return null; } - if (!fileType.getName().equals(fileTypeBean.name)) { - LOG.error(new PluginException("Incorrect name specified in , should be " + fileType.getName() + ", actual " + fileTypeBean.name, pluginId)); + if (!fileType.getName().equals(bean.name)) { + LOG.error(new PluginException("Incorrect name specified in , should be " + fileType.getName() + ", actual " + bean.name, pluginId)); } if (fileType instanceof LanguageFileType) { final LanguageFileType languageFileType = (LanguageFileType)fileType; String expectedLanguage = languageFileType.isSecondary() ? null : languageFileType.getLanguage().getID(); - if (!Comparing.equal(fileTypeBean.language, expectedLanguage)) { - LOG.error(new PluginException("Incorrect language specified in for " + fileType.getName() + ", should be " + expectedLanguage + ", actual " + fileTypeBean.language, pluginId)); + if (!Comparing.equal(bean.language, expectedLanguage)) { + LOG.error(new PluginException("Incorrect language specified in for " + fileType.getName() + ", should be " + expectedLanguage + ", actual " + bean.language, pluginId)); } } - final StandardFileType standardFileType = new StandardFileType(fileType, fileTypeBean.getMatchers()); - myStandardFileTypes.put(fileTypeBean.name, standardFileType); - registerFileTypeWithoutNotification(standardFileType.fileType, standardFileType.matchers, true); + final StandardFileType standardFileType = new StandardFileType(fileType, bean.getMatchers()); + myStandardFileTypes.put(bean.name, standardFileType); + List hashBangs = bean.hashBangs == null ? Collections.emptyList() : StringUtil.split(bean.hashBangs, ";"); + registerFileTypeWithoutNotification(fileType, standardFileType.matchers, hashBangs, true); - myPendingAssociations.removeAllAssociations(fileTypeBean); - myPendingFileTypes.remove(fileTypeBean.name); + myPendingAssociations.removeAllAssociations(bean); + myPendingFileTypes.remove(bean.name); return fileType; } @@ -1083,6 +1087,9 @@ public class FileTypeManagerImpl extends FileTypeManagerEx implements Persistent } } + if (detected == null && text != null) { + detected = myPatternsTable.findAssociatedFileTypeByHashBang(text); + } if (detected == null) { detected = text == null ? UnknownFileType.INSTANCE : PlainTextFileType.INSTANCE; if (toLog()) { @@ -1155,7 +1162,7 @@ public class FileTypeManagerImpl extends FileTypeManagerEx implements Persistent DeprecatedMethodException.report("Use fileType extension instead."); ApplicationManager.getApplication().runWriteAction(() -> { fireBeforeFileTypesChanged(); - registerFileTypeWithoutNotification(type, defaultAssociations, true); + registerFileTypeWithoutNotification(type, defaultAssociations, Collections.emptyList(), true); fireFileTypesChanged(type, null); }); } @@ -1386,7 +1393,8 @@ public class FileTypeManagerImpl extends FileTypeManagerEx implements Persistent private void readGlobalMappings(@NotNull Element e, boolean isAddToInit) { for (Pair association : AbstractFileType.readAssociations(e)) { - FileType type = getFileTypeByName(association.getSecond()); + String fileTypeName = association.getSecond(); + FileType type = getFileTypeByName(fileTypeName); FileNameMatcher matcher = association.getFirst(); final FileTypeBean pendingFileTypeBean = myPendingAssociations.findAssociatedFileType(matcher); if (pendingFileTypeBean != null) { @@ -1406,7 +1414,16 @@ public class FileTypeManagerImpl extends FileTypeManagerEx implements Persistent } } else { - myUnresolvedMappings.put(matcher, association.getSecond()); + myUnresolvedMappings.put(matcher, fileTypeName); + } + } + + for (Map.Entry entry : readHashBangs(e).entrySet()) { + String hashBang = entry.getKey(); + FileType fileType = entry.getValue(); + myPatternsTable.addHashBangPattern(hashBang, fileType); + if (isAddToInit) { + myInitialAssociations.addHashBangPattern(hashBang, fileType); } } @@ -1419,6 +1436,19 @@ public class FileTypeManagerImpl extends FileTypeManagerEx implements Persistent } } + private @NotNull Map readHashBangs(@NotNull Element e) { + List children = e.getChildren("hashBang"); + Map result = new THashMap<>(children.size()); + for (Element hashBangTag : children) { + String typeName = hashBangTag.getAttributeValue("type"); + String hashBangPattern = hashBangTag.getAttributeValue("value"); + FileType fileType = typeName == null ? null : getFileTypeByName(typeName); + if (hashBangPattern == null || fileType == null) continue; + result.put(hashBangPattern, fileType); + } + return result; + } + private void addIgnore(@NonNls @NotNull String ignoreMask) { myIgnoredPatterns.addIgnoreMask(ignoreMask); } @@ -1483,11 +1513,13 @@ public class FileTypeManagerImpl extends FileTypeManagerEx implements Persistent myRemovedMappingTracker.save(map); if (!myUnresolvedMappings.isEmpty()) { - FileNameMatcher[] unresolvedMappingKeys = myUnresolvedMappings.keySet().toArray(new FileNameMatcher[0]); - Arrays.sort(unresolvedMappingKeys, Comparator.comparing(FileNameMatcher::getPresentableString)); + List> entries = new ArrayList<>(myUnresolvedMappings.entrySet()); + entries.sort(Comparator.comparing(e->e.getKey().getPresentableString())); - for (FileNameMatcher fileNameMatcher : unresolvedMappingKeys) { - Element content = AbstractFileType.writeMapping(myUnresolvedMappings.get(fileNameMatcher), fileNameMatcher, true); + for (Map.Entry entry : entries) { + FileNameMatcher fileNameMatcher = entry.getKey(); + String typeName = entry.getValue(); + Element content = AbstractFileType.writeMapping(typeName, fileNameMatcher, true); if (content != null) { map.addContent(content); } @@ -1517,7 +1549,17 @@ public class FileTypeManagerImpl extends FileTypeManagerEx implements Persistent } } } - + List readOnlyHashBangs = myInitialAssociations.getHashBangPatterns(type); + List hashBangPatterns = myPatternsTable.getHashBangPatterns(type); + hashBangPatterns.sort(Comparator.naturalOrder()); + for (String hashBangPattern : hashBangPatterns) { + if (!readOnlyHashBangs.contains(hashBangPattern)) { + Element hashBangTag = new Element("hashBang"); + hashBangTag.setAttribute("value", hashBangPattern); + hashBangTag.setAttribute("type", type.getName()); + map.addContent(hashBangTag); + } + } myRemovedMappingTracker.saveRemovedMappingsForFileType(map, type.getName(), defaultAssociations, specifyTypeName); } @@ -1545,7 +1587,7 @@ public class FileTypeManagerImpl extends FileTypeManagerEx implements Persistent } StringTokenizer tokenizer = new StringTokenizer(semicolonDelimited, FileTypeConsumer.EXTENSION_DELIMITER, false); - ArrayList list = new ArrayList<>(semicolonDelimited.length() / "py;".length()); + List list = new ArrayList<>(semicolonDelimited.length() / "py;".length()); while (tokenizer.hasMoreTokens()) { list.add(matcherFactory.fun(tokenizer.nextToken().trim())); } @@ -1555,7 +1597,7 @@ public class FileTypeManagerImpl extends FileTypeManagerEx implements Persistent /** * Registers a standard file type. Doesn't notifyListeners any change events. */ - private void registerFileTypeWithoutNotification(@NotNull FileType fileType, @NotNull List matchers, boolean addScheme) { + private void registerFileTypeWithoutNotification(@NotNull FileType fileType, @NotNull List matchers, @NotNull List hasBangPatterns, boolean addScheme) { if (addScheme) { mySchemeManager.addScheme(fileType); } @@ -1563,6 +1605,10 @@ public class FileTypeManagerImpl extends FileTypeManagerEx implements Persistent myPatternsTable.addAssociation(matcher, fileType); myInitialAssociations.addAssociation(matcher, fileType); } + for (String hashBang : hasBangPatterns) { + myPatternsTable.addHashBangPattern(hashBang, fileType); + myInitialAssociations.addHashBangPattern(hashBang, fileType); + } if (fileType instanceof FileTypeIdentifiableByVirtualFile) { mySpecialFileTypes = ArrayUtil.append(mySpecialFileTypes, (FileTypeIdentifiableByVirtualFile)fileType, FileTypeIdentifiableByVirtualFile.ARRAY_FACTORY); @@ -1611,7 +1657,7 @@ public class FileTypeManagerImpl extends FileTypeManagerEx implements Persistent } setFileTypeAttributes((UserFileType)type, fileTypeName, fileTypeDescr, iconPath); - registerFileTypeWithoutNotification(type, parse(extensionsStr), isDefault); + registerFileTypeWithoutNotification(type, parse(extensionsStr), Collections.emptyList(), isDefault); if (isDefault) { myDefaultTypes.add(type); diff --git a/platform/platform-tests/testSrc/com/intellij/openapi/fileTypes/impl/FileTypesTest.java b/platform/platform-tests/testSrc/com/intellij/openapi/fileTypes/impl/FileTypesTest.java index 3908c5c65912..8bf699b687f5 100644 --- a/platform/platform-tests/testSrc/com/intellij/openapi/fileTypes/impl/FileTypesTest.java +++ b/platform/platform-tests/testSrc/com/intellij/openapi/fileTypes/impl/FileTypesTest.java @@ -989,4 +989,17 @@ public class FileTypesTest extends HeavyPlatformTestCase { return null; } } + + public void testHashBangPatternsCanBeConfiguredDynamically() throws IOException { + VirtualFile file0 = createTempFile("xxxx", null, "#!/usr/bin/gogogo\na=b", CharsetToolkit.UTF8_CHARSET); + assertEquals(StdFileTypes.PLAIN_TEXT, file0.getFileType()); + myFileTypeManager.getExtensionMap().addHashBangPattern("gogogo", StdFileTypes.PROPERTIES); + try { + VirtualFile file = createTempFile("xxxx", null, "#!/usr/bin/gogogo\na=b", CharsetToolkit.UTF8_CHARSET); + assertEquals(StdFileTypes.PROPERTIES, file.getFileType()); + } + finally { + myFileTypeManager.getExtensionMap().removeHashBangPattern("gogogo", StdFileTypes.PROPERTIES); + } + } } diff --git a/plugins/groovy/groovy-psi/src/org/jetbrains/plugins/groovy/GroovyHashBangFileTypeDetector.java b/plugins/groovy/groovy-psi/src/org/jetbrains/plugins/groovy/GroovyHashBangFileTypeDetector.java deleted file mode 100644 index 9237853cea69..000000000000 --- a/plugins/groovy/groovy-psi/src/org/jetbrains/plugins/groovy/GroovyHashBangFileTypeDetector.java +++ /dev/null @@ -1,24 +0,0 @@ -/* - * Copyright 2000-2014 JetBrains s.r.o. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.jetbrains.plugins.groovy; - -import com.intellij.openapi.fileTypes.impl.HashBangFileTypeDetector; - -public class GroovyHashBangFileTypeDetector extends HashBangFileTypeDetector { - public GroovyHashBangFileTypeDetector() { - super(GroovyFileType.GROOVY_FILE_TYPE, "groovy"); - } -} diff --git a/plugins/groovy/src/META-INF/plugin.xml b/plugins/groovy/src/META-INF/plugin.xml index 061813d0843b..159beb8ecaee 100644 --- a/plugins/groovy/src/META-INF/plugin.xml +++ b/plugins/groovy/src/META-INF/plugin.xml @@ -304,7 +304,7 @@ - @@ -329,8 +329,6 @@ - - ]]> - + @@ -67,7 +67,6 @@ Adds support for working with shell script files - diff --git a/plugins/sh/src/com/intellij/sh/ShFileTypeDetector.java b/plugins/sh/src/com/intellij/sh/ShFileTypeDetector.java deleted file mode 100644 index ba3b5595d35c..000000000000 --- a/plugins/sh/src/com/intellij/sh/ShFileTypeDetector.java +++ /dev/null @@ -1,10 +0,0 @@ -// Copyright 2000-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. -package com.intellij.sh; - -import com.intellij.openapi.fileTypes.impl.HashBangFileTypeDetector; - -public class ShFileTypeDetector extends HashBangFileTypeDetector { - public ShFileTypeDetector() { - super(ShFileType.INSTANCE, "sh"); - } -} diff --git a/python/python-psi-api/resources/META-INF/PythonPsi.xml b/python/python-psi-api/resources/META-INF/PythonPsi.xml index 2d504217a105..974d41171bf0 100644 --- a/python/python-psi-api/resources/META-INF/PythonPsi.xml +++ b/python/python-psi-api/resources/META-INF/PythonPsi.xml @@ -21,6 +21,7 @@ - diff --git a/python/src/com/jetbrains/python/PyFileTypeDetector.java b/python/src/com/jetbrains/python/PyFileTypeDetector.java deleted file mode 100644 index b7f29fe9ba64..000000000000 --- a/python/src/com/jetbrains/python/PyFileTypeDetector.java +++ /dev/null @@ -1,27 +0,0 @@ -/* - * Copyright 2000-2014 JetBrains s.r.o. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.jetbrains.python; - -import com.intellij.openapi.fileTypes.impl.HashBangFileTypeDetector; - -/** - * @author yole - */ -public class PyFileTypeDetector extends HashBangFileTypeDetector { - public PyFileTypeDetector() { - super(PythonFileType.INSTANCE, "python"); - } -}