PY-6149 Add Support for Marking Directories as "Resource Roots"
authorliana.bakradze <liana.bakradze@jetbrains.com>
Mon, 4 May 2015 18:51:43 +0000 (21:51 +0300)
committerliana.bakradze <liana.bakradze@jetbrains.com>
Mon, 4 May 2015 18:51:43 +0000 (21:51 +0300)
python/pluginSrc/META-INF/python-plugin-core.xml
python/src/META-INF/python-core.xml
python/src/com/jetbrains/python/module/PyContentEntriesEditor.java
python/src/com/jetbrains/python/module/PyRootTypeProvider.java [new file with mode: 0644]

index 33b1489475b9c9e6f979bf717bf4e3a25ef29a29..eacd7e22002d2a9185285a739a007145423c7097 100644 (file)
@@ -15,6 +15,9 @@
 
     <!-- Console folding for Jython only, thus it's located in python-plugin only -->
     <stacktrace.fold substring="*sys-package-mgr*:"/>
+    <moduleService serviceInterface="com.jetbrains.python.resourceRoots.PyPluginResourceRootProvider"
+                   serviceImplementation="com.jetbrains.python.resourceRoots.PyPluginResourceRootProvider"/>
+    <psi.fileReferenceHelper implementation="com.jetbrains.python.resourceRoots.PyResourceRootFileReferenceHelper"/>
   </extensions>
 
   <extensions defaultExtensionNs="Pythonid">
@@ -22,6 +25,7 @@
     <typeProvider implementation="com.jetbrains.python.psi.impl.PyJavaTypeProvider"/>
     <pySuperMethodsSearch implementation="com.jetbrains.python.psi.impl.PyJavaSuperMethodsSearchExecutor"/>
     <importCandidateProvider implementation="com.jetbrains.python.psi.impl.PyJavaImportCandidateProvider"/>
+    <pyRootTypeProvider implementation="com.jetbrains.python.resourceRoots.PyPluginResourceRootProvider"/>
   </extensions>
 
   <application-components>
index c8b9c46b7117fc2b4bd518670f9a4a9b277a60d6..7e5746e95a5b39bdab42105c25b2d9596df7adbe 100644 (file)
     <extensionPoint qualifiedName="Pythonid.pyReferenceResolveProvider" interface="com.jetbrains.python.psi.resolve.PyReferenceResolveProvider"/>
     <extensionPoint qualifiedName="Pythonid.breakpointHandler" interface="com.jetbrains.python.debugger.PyBreakpointHandlerFactory"/>
     <extensionPoint qualifiedName="Pythonid.consoleOptionsProvider" interface="com.jetbrains.python.console.PyConsoleOptionsProvider"/>
+    <extensionPoint qualifiedName="Pythonid.pyRootTypeProvider" interface="com.jetbrains.python.module.PyRootTypeProvider"/>
   </extensionPoints>
 
   <extensions defaultExtensionNs="Pythonid">
index 3a25e029f61d6cf1d631a04742fd894e2ee8ae9b..c6602224b19278e56ded52bfd80572a3a211cc96 100644 (file)
@@ -16,9 +16,8 @@
 package com.jetbrains.python.module;
 
 import com.intellij.openapi.Disposable;
-import com.intellij.openapi.actionSystem.AnActionEvent;
 import com.intellij.openapi.actionSystem.CustomShortcutSet;
-import com.intellij.openapi.actionSystem.Presentation;
+import com.intellij.openapi.extensions.Extensions;
 import com.intellij.openapi.module.Module;
 import com.intellij.openapi.options.ConfigurationException;
 import com.intellij.openapi.project.Project;
@@ -26,21 +25,14 @@ import com.intellij.openapi.roots.ContentEntry;
 import com.intellij.openapi.roots.ContentFolder;
 import com.intellij.openapi.roots.ModifiableRootModel;
 import com.intellij.openapi.roots.impl.ContentEntryImpl;
-import com.intellij.openapi.roots.impl.ContentFolderBaseImpl;
 import com.intellij.openapi.roots.ui.configuration.*;
 import com.intellij.openapi.roots.ui.configuration.actions.ContentEntryEditingAction;
 import com.intellij.openapi.util.Comparing;
 import com.intellij.openapi.util.Disposer;
-import com.intellij.openapi.vfs.VfsUtilCore;
 import com.intellij.openapi.vfs.VirtualFile;
 import com.intellij.openapi.vfs.pointers.VirtualFilePointer;
-import com.intellij.openapi.vfs.pointers.VirtualFilePointerListener;
-import com.intellij.openapi.vfs.pointers.VirtualFilePointerManager;
-import com.intellij.ui.JBColor;
 import com.intellij.util.EventDispatcher;
 import com.intellij.util.containers.MultiMap;
-import com.jetbrains.python.templateLanguages.TemplatesService;
-import icons.PythonIcons;
 import org.jetbrains.annotations.NotNull;
 import org.jetbrains.annotations.Nullable;
 import org.jetbrains.jps.model.module.JpsModuleSourceRootType;
@@ -50,34 +42,26 @@ import javax.swing.event.ChangeEvent;
 import javax.swing.event.ChangeListener;
 import javax.swing.tree.TreeCellRenderer;
 import java.awt.*;
-import java.awt.event.InputEvent;
-import java.awt.event.KeyEvent;
-import java.util.ArrayList;
 import java.util.List;
 
 public class PyContentEntriesEditor extends CommonContentEntriesEditor {
-  private static final Color TEMPLATES_COLOR = JBColor.MAGENTA;
-  private final MultiMap<ContentEntry, VirtualFilePointer> myTemplateRoots = new MultiMap<ContentEntry, VirtualFilePointer>();
+  private final PyRootTypeProvider[] myRootTypeProviders;
   private final Module myModule;
   private Disposable myFilePointersDisposable;
-
-  private final VirtualFilePointerListener DUMMY_LISTENER = new VirtualFilePointerListener() {
-    @Override
-    public void beforeValidityChanged(@NotNull VirtualFilePointer[] pointers) {
-    }
-
-    @Override
-    public void validityChanged(@NotNull VirtualFilePointer[] pointers) {
-    }
-  };
+  private MyContentEntryEditor myContentEntryEditor;
 
   public PyContentEntriesEditor(Module module, ModuleConfigurationState moduleConfigurationState,
                                       JpsModuleSourceRootType<?>... rootTypes) {
     super(module.getName(), moduleConfigurationState, rootTypes);
+    myRootTypeProviders = Extensions.getExtensions(PyRootTypeProvider.EP_NAME);
     myModule = module;
     reset();
   }
 
+  public MyContentEntryEditor getContentEntryEditor() {
+    return myContentEntryEditor;
+  }
+
   @Override
   protected ContentEntryTreeEditor createContentEntryTreeEditor(Project project) {
     return new MyContentEntryTreeEditor(project, getEditHandlers());
@@ -90,24 +74,19 @@ public class PyContentEntriesEditor extends CommonContentEntriesEditor {
     return entries;
   }
 
+  public ContentEntry[] getContentEntries() {
+    return getModel().getContentEntries();
+  }
+
   @Override
   public void reset() {
     if (myFilePointersDisposable != null) {
       Disposer.dispose(myFilePointersDisposable);
     }
-    myTemplateRoots.clear();
 
     myFilePointersDisposable = Disposer.newDisposable();
-    final TemplatesService instance = TemplatesService.getInstance(myModule);
-    if (instance != null) {
-      final List<VirtualFile> folders = instance.getTemplateFolders();
-      for (VirtualFile folder : folders) {
-        ContentEntry contentEntry = findContentEntryForFile(folder);
-        if (contentEntry != null) {
-          myTemplateRoots.putValue(contentEntry, VirtualFilePointerManager.getInstance().create(folder, myFilePointersDisposable,
-                                                                                                DUMMY_LISTENER));
-        }
-      }
+    for (PyRootTypeProvider provider : myRootTypeProviders) {
+      provider.reset(myFilePointersDisposable, this, myModule);
     }
 
     if (myRootTreeEditor != null) {
@@ -117,17 +96,6 @@ public class PyContentEntriesEditor extends CommonContentEntriesEditor {
     }
   }
 
-  @Nullable
-  private ContentEntry findContentEntryForFile(VirtualFile virtualFile) {
-    for (ContentEntry contentEntry : getModel().getContentEntries()) {
-      final VirtualFile file = contentEntry.getFile();
-      if (file != null && VfsUtilCore.isAncestor(file, virtualFile, false)) {
-        return contentEntry;
-      }
-    }
-    return null;
-  }
-
   @Override
   public void disposeUIResources() {
     super.disposeUIResources();
@@ -139,40 +107,26 @@ public class PyContentEntriesEditor extends CommonContentEntriesEditor {
   @Override
   public void apply() throws ConfigurationException {
     super.apply();
-    List<VirtualFile> templateRoots = getCurrentState();
-    final TemplatesService templatesService = TemplatesService.getInstance(myModule);
-    if (templatesService != null) {
-      templatesService.setTemplateFolders(templateRoots.toArray(new VirtualFile[templateRoots.size()]));
-    }
-  }
-
-  private List<VirtualFile> getCurrentState() {
-    List<VirtualFile> result = new ArrayList<VirtualFile>();
-    for (ContentEntry entry : myTemplateRoots.keySet()) {
-      for (VirtualFilePointer filePointer : myTemplateRoots.get(entry)) {
-        result.add(filePointer.getFile());
-      }
+    for (PyRootTypeProvider provider : myRootTypeProviders) {
+      provider.apply(myModule);
     }
-    return result;
   }
 
   @Override
   public boolean isModified() {
     if (super.isModified()) return true;
-    final TemplatesService templatesService = TemplatesService.getInstance(myModule);
-    if (templatesService != null) {
-      List<VirtualFile> original = templatesService.getTemplateFolders();
-      List<VirtualFile> current = getCurrentState();
-
-      if (!Comparing.haveEqualElements(original, current)) return true;
-
+    for (PyRootTypeProvider provider : myRootTypeProviders) {
+     if (provider.isModified(myModule)) {
+       return true;
+     }
     }
     return false;
   }
 
   @Override
   protected MyContentEntryEditor createContentEntryEditor(String contentEntryUrl) {
-    return new MyContentEntryEditor(contentEntryUrl, getEditHandlers());
+    myContentEntryEditor = new MyContentEntryEditor(contentEntryUrl, getEditHandlers());
+    return myContentEntryEditor;
   }
 
   protected class MyContentEntryEditor extends ContentEntryEditor {
@@ -202,37 +156,33 @@ public class PyContentEntriesEditor extends CommonContentEntriesEditor {
 
     @Override
     public void deleteContentFolder(ContentEntry contentEntry, ContentFolder folder) {
-      if (folder instanceof TemplateRootFolder) {
-        removeTemplateRoot(folder.getUrl());
-      }
-      else {
-        super.deleteContentFolder(contentEntry, folder);
+      for (PyRootTypeProvider provider : myRootTypeProviders) {
+        if (provider.isMine(folder)) {
+          removeRoot(contentEntry, folder.getUrl(), provider);
+          return;
+        }
       }
+      super.deleteContentFolder(contentEntry, folder);
     }
 
-    public void addTemplateRoot(@NotNull final VirtualFile file) {
-      final VirtualFilePointer root = VirtualFilePointerManager.getInstance().create(file, myFilePointersDisposable, DUMMY_LISTENER);
-      myTemplateRoots.putValue(getContentEntry(), root);
-      myEventDispatcher.getMulticaster().stateChanged(new ChangeEvent(this));
-      update();
-    }
-
-    public void removeTemplateRoot(@NotNull final String url) {
-      final VirtualFilePointer root = getTemplateRoot(url);
+    public void removeRoot(@Nullable ContentEntry contentEntry, String folder, PyRootTypeProvider provider) {
+      if (contentEntry == null) {
+        contentEntry = getContentEntry();
+      }
+      VirtualFilePointer root = getRoot(provider, folder);
       if (root != null) {
-        myTemplateRoots.remove(getContentEntry(), root);
-        myEventDispatcher.getMulticaster().stateChanged(new ChangeEvent(this));
-        update();
+        provider.removeRoot(contentEntry, root);
+        fireUpdate();
       }
     }
 
-    public boolean hasTemplateRoot(@NotNull final VirtualFile file) {
-      return getTemplateRoot(file.getUrl()) != null;
+    public void fireUpdate() {
+      myEventDispatcher.getMulticaster().stateChanged(new ChangeEvent(this));
+      update();
     }
 
-    @Nullable
-    public VirtualFilePointer getTemplateRoot(@NotNull final String url) {
-      for (VirtualFilePointer filePointer : myTemplateRoots.get(getContentEntry())) {
+    public VirtualFilePointer getRoot(PyRootTypeProvider provider, @NotNull final String url) {
+      for (VirtualFilePointer filePointer : provider.getRoots().get(getContentEntry())) {
         if (Comparing.equal(filePointer.getUrl(), url)) {
           return filePointer;
         }
@@ -240,6 +190,11 @@ public class PyContentEntriesEditor extends CommonContentEntriesEditor {
       return null;
     }
 
+    public void addRoot(PyRootTypeProvider provider, @NotNull final VirtualFilePointer root) {
+      provider.getRoots().putValue(getContentEntry(), root);
+      fireUpdate();
+    }
+
     protected class MyContentRootPanel extends ContentRootPanel {
       public MyContentRootPanel() {
         super(MyContentEntryEditor.this, getEditHandlers());
@@ -255,22 +210,22 @@ public class PyContentEntriesEditor extends CommonContentEntriesEditor {
       @Override
       protected void addFolderGroupComponents() {
         super.addFolderGroupComponents();
-        if (!myTemplateRoots.get(getContentEntry()).isEmpty()) {
-          final List<TemplateRootFolder> folders = new ArrayList<TemplateRootFolder>(myTemplateRoots.size());
-          for (VirtualFilePointer root : myTemplateRoots.get(getContentEntry())) {
-            folders.add(new TemplateRootFolder(root, getContentEntry()));
+        for (PyRootTypeProvider provider : myRootTypeProviders) {
+          MultiMap<ContentEntry, VirtualFilePointer> roots = provider.getRoots();
+          if (!roots.get(getContentEntry()).isEmpty()) {
+            final JComponent sourcesComponent = createFolderGroupComponent(provider.getName() + " Folders",
+                                                                           provider.createFolders(getContentEntry()),
+                                                                           provider.getColor(), null);
+            this.add(sourcesComponent, new GridBagConstraints(0, GridBagConstraints.RELATIVE, 1, 1, 1.0, 0.0, GridBagConstraints.NORTH,
+                                                              GridBagConstraints.HORIZONTAL, new Insets(0, 0, 10, 0), 0, 0));
           }
-          final JComponent sourcesComponent = createFolderGroupComponent("Template Folders",
-                                                                         folders.toArray(new ContentFolder[folders.size()]),
-                                                                         TEMPLATES_COLOR, null);
-          this.add(sourcesComponent, new GridBagConstraints(0, GridBagConstraints.RELATIVE, 1, 1, 1.0, 0.0, GridBagConstraints.NORTH,
-                                                            GridBagConstraints.HORIZONTAL, new Insets(0, 0, 10, 0), 0, 0));
+
         }
       }
     }
   }
 
-  private static class MyContentEntryTreeEditor extends ContentEntryTreeEditor {
+  private class MyContentEntryTreeEditor extends ContentEntryTreeEditor {
 
     private final ChangeListener myListener = new ChangeListener() {
       @Override
@@ -306,43 +261,14 @@ public class PyContentEntriesEditor extends CommonContentEntriesEditor {
     @Override
     protected void createEditingActions() {
       super.createEditingActions();
-
-      ContentEntryEditingAction a = new ContentEntryEditingAction(myTree) {
-        {
-          final Presentation templatePresentation = getTemplatePresentation();
-          templatePresentation.setText("Templates");
-          templatePresentation.setDescription("Template Folders");
-          templatePresentation.setIcon(PythonIcons.Python.TemplateRoot);
-        }
-
-        @Override
-        public boolean isSelected(AnActionEvent e) {
-          final VirtualFile[] selectedFiles = getSelectedFiles();
-          return selectedFiles.length != 0 && getContentEntryEditor().hasTemplateRoot(selectedFiles[0]);
+      for (PyRootTypeProvider provider : myRootTypeProviders) {
+        ContentEntryEditingAction action = provider.createRootEntryEditingAction(myTree, myFilePointersDisposable, PyContentEntriesEditor.this, getModel());
+        myEditingActionsGroup.add(action);
+        CustomShortcutSet shortcut = provider.getShortcut();
+        if (shortcut != null) {
+          action.registerCustomShortcutSet(shortcut, myTree);
         }
-
-        @Override
-        public void setSelected(AnActionEvent e, boolean isSelected) {
-          final VirtualFile[] selectedFiles = getSelectedFiles();
-          assert selectedFiles.length != 0;
-
-          for (VirtualFile selectedFile : selectedFiles) {
-            boolean wasSelected = getContentEntryEditor().hasTemplateRoot(selectedFile);
-            if (isSelected) {
-              if (!wasSelected) {
-                getContentEntryEditor().addTemplateRoot(selectedFile);
-              }
-            }
-            else {
-              if (wasSelected) {
-                getContentEntryEditor().removeTemplateRoot(selectedFile.getUrl());
-              }
-            }
-          }
-        }
-      };
-      myEditingActionsGroup.add(a);
-      a.registerCustomShortcutSet(new CustomShortcutSet(KeyStroke.getKeyStroke(KeyEvent.VK_R, InputEvent.ALT_MASK)), myTree);
+      }
     }
 
     @Override
@@ -350,18 +276,14 @@ public class PyContentEntriesEditor extends CommonContentEntriesEditor {
       return new ContentEntryTreeCellRenderer(this, getEditHandlers()) {
         @Override
         protected Icon updateIcon(final ContentEntry entry, final VirtualFile file, final Icon originalIcon) {
-          if (getContentEntryEditor().hasTemplateRoot(file)) {
-            return PythonIcons.Python.TemplateRoot;
+          for (PyRootTypeProvider provider : myRootTypeProviders) {
+            if (provider.hasRoot(file, PyContentEntriesEditor.this)) {
+              return provider.getIcon();
+            }
           }
           return super.updateIcon(entry, file, originalIcon);
         }
       };
     }
   }
-  private static class TemplateRootFolder extends ContentFolderBaseImpl {
-    protected TemplateRootFolder(@NotNull VirtualFilePointer filePointer, @NotNull ContentEntryImpl contentEntry) {
-      super(filePointer, contentEntry);
-    }
-  }
-
 }
diff --git a/python/src/com/jetbrains/python/module/PyRootTypeProvider.java b/python/src/com/jetbrains/python/module/PyRootTypeProvider.java
new file mode 100644 (file)
index 0000000..56610ea
--- /dev/null
@@ -0,0 +1,141 @@
+/*
+ * Copyright 2000-2015 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.module;
+
+import com.intellij.openapi.Disposable;
+import com.intellij.openapi.actionSystem.AnActionEvent;
+import com.intellij.openapi.actionSystem.CustomShortcutSet;
+import com.intellij.openapi.actionSystem.Presentation;
+import com.intellij.openapi.extensions.ExtensionPointName;
+import com.intellij.openapi.module.Module;
+import com.intellij.openapi.roots.ContentEntry;
+import com.intellij.openapi.roots.ContentFolder;
+import com.intellij.openapi.roots.ModifiableRootModel;
+import com.intellij.openapi.roots.ui.configuration.actions.ContentEntryEditingAction;
+import com.intellij.openapi.vfs.VirtualFile;
+import com.intellij.openapi.vfs.pointers.VirtualFilePointer;
+import com.intellij.openapi.vfs.pointers.VirtualFilePointerListener;
+import com.intellij.openapi.vfs.pointers.VirtualFilePointerManager;
+import com.intellij.util.containers.MultiMap;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import javax.swing.*;
+import java.awt.*;
+
+public abstract class PyRootTypeProvider {
+  public static final ExtensionPointName<PyRootTypeProvider> EP_NAME = ExtensionPointName.create("Pythonid.pyRootTypeProvider");
+  protected final VirtualFilePointerListener DUMMY_LISTENER = new VirtualFilePointerListener() {
+    @Override
+    public void beforeValidityChanged(@NotNull VirtualFilePointer[] pointers) {
+    }
+
+    @Override
+    public void validityChanged(@NotNull VirtualFilePointer[] pointers) {
+    }
+  };
+
+  public abstract void reset(@NotNull final Disposable disposable, PyContentEntriesEditor editor, Module module);
+
+  public abstract void apply(Module module);
+
+  public abstract boolean isModified(Module module);
+
+  public abstract boolean isMine(ContentFolder folder);
+
+  public void removeRoot(ContentEntry contentEntry, @NotNull final VirtualFilePointer root) {
+    getRoots().remove(contentEntry, root);
+  }
+  public abstract MultiMap<ContentEntry, VirtualFilePointer> getRoots();
+
+  public abstract Icon getIcon();
+
+  public abstract String getName();
+
+  public String getNamePlural() {
+    return getName() + "s";
+  }
+
+  public abstract Color getColor();
+
+  @Nullable
+  public CustomShortcutSet getShortcut() {
+    return null;
+  }
+
+
+  protected class RootEntryEditingAction extends ContentEntryEditingAction {
+    private final Disposable myDisposable;
+    private final PyContentEntriesEditor myEditor;
+    private final ModifiableRootModel myModel;
+
+    public RootEntryEditingAction(JTree tree, Disposable disposable, PyContentEntriesEditor editor, ModifiableRootModel model) {
+      super(tree);
+      final Presentation templatePresentation = getTemplatePresentation();
+      templatePresentation.setText(getNamePlural());
+      templatePresentation.setDescription(getName() + " Folders");
+      templatePresentation.setIcon(getIcon());
+      myDisposable = disposable;
+      myEditor = editor;
+      myModel = model;
+    }
+
+    @Override
+    public boolean isSelected(AnActionEvent e) {
+      final VirtualFile[] selectedFiles = getSelectedFiles();
+      return selectedFiles.length != 0 && hasRoot(selectedFiles[0], myEditor);
+    }
+
+    @Override
+    public void setSelected(AnActionEvent e, boolean isSelected) {
+      final VirtualFile[] selectedFiles = getSelectedFiles();
+      assert selectedFiles.length != 0;
+
+      for (VirtualFile selectedFile : selectedFiles) {
+        boolean wasSelected = hasRoot(selectedFile, myEditor);
+        if (isSelected) {
+          if (!wasSelected) {
+            final VirtualFilePointer root = VirtualFilePointerManager.getInstance().create(selectedFile, myDisposable, DUMMY_LISTENER);
+            addRoot(root, myEditor);
+          }
+        }
+        else {
+          if (wasSelected) {
+            removeRoot(selectedFile, myEditor, myModel);
+          }
+        }
+      }
+    }
+  }
+
+  private void addRoot(VirtualFilePointer root, PyContentEntriesEditor editor) {
+    editor.getContentEntryEditor().addRoot(this, root);
+  }
+
+  protected void removeRoot(VirtualFile selectedFile, PyContentEntriesEditor editor, ModifiableRootModel model) {
+    editor.getContentEntryEditor().removeRoot(null, selectedFile.getUrl(), this);
+  }
+
+  protected boolean hasRoot(VirtualFile file, PyContentEntriesEditor editor) {
+    PyContentEntriesEditor.MyContentEntryEditor entryEditor = editor.getContentEntryEditor();
+    return entryEditor.getRoot(this, file.getUrl()) != null;
+  }
+
+  public abstract ContentEntryEditingAction createRootEntryEditingAction(JTree tree,
+                                                                         Disposable disposable, PyContentEntriesEditor editor, ModifiableRootModel model);
+
+  public abstract ContentFolder[] createFolders(ContentEntry contentEntry);
+}