Merge branch 'ypankratyev/goto_testdata_fixes'
authorYaroslav Pankratyev <yaroslav.pankratyev@jetbrains.com>
Mon, 18 Sep 2017 04:09:20 +0000 (11:09 +0700)
committerYaroslav Pankratyev <yaroslav.pankratyev@jetbrains.com>
Mon, 18 Sep 2017 04:09:20 +0000 (11:09 +0700)
22 files changed:
platform/testFramework/src/com/intellij/testFramework/fixtures/CodeInsightTestFixture.java
platform/testFramework/src/com/intellij/testFramework/fixtures/impl/CodeInsightTestFixtureImpl.java
platform/util/src/com/intellij/openapi/util/text/StringUtil.java
platform/util/testSrc/com/intellij/util/text/StringUtilTest.java
plugins/devkit/resources/META-INF/plugin.xml
plugins/devkit/resources/org/jetbrains/idea/devkit/DevKitBundle.properties
plugins/devkit/src/testAssistant/TestDataGroupEditorProvider.java
plugins/devkit/src/testAssistant/TestDataGroupEditorTabTitleProvider.java [new file with mode: 0644]
plugins/devkit/src/testAssistant/TestDataGroupFileEditor.java
plugins/devkit/src/testAssistant/TestDataGuessByExistingFilesUtil.java
plugins/devkit/src/testAssistant/TestDataNavigationElement.java [new file with mode: 0644]
plugins/devkit/src/testAssistant/TestDataNavigationElementFactory.java [new file with mode: 0644]
plugins/devkit/src/testAssistant/TestDataNavigationHandler.java
plugins/devkit/src/testAssistant/TestDataReferenceCollector.java
plugins/devkit/src/testAssistant/TestDataUtil.java [new file with mode: 0644]
plugins/devkit/src/testAssistant/vfs/TestDataGroupFileSystem.java [new file with mode: 0644]
plugins/devkit/src/testAssistant/vfs/TestDataGroupVirtualFile.java [moved from plugins/devkit/src/testAssistant/TestDataGroupVirtualFile.java with 85% similarity]
plugins/devkit/testData/guessByExistingFiles/Test/testdata_file.txt [new file with mode: 0644]
plugins/devkit/testData/guessByExistingFiles/TestMore/testdata_file.txt [new file with mode: 0644]
plugins/devkit/testData/guessByExistingFiles/TestMoreRelevant.java [new file with mode: 0644]
plugins/devkit/testData/guessByExistingFiles/TestMoreRelevant/testdata_file.txt [new file with mode: 0644]
plugins/devkit/testSources/testAssistant/TestDataGuessByExistingFilesUtilTest.java

index ddaa3790beee7e452d098fc5a562a94311dd8763..074a7bedbc8c628b9a472505d62674d876cc701b 100644 (file)
@@ -390,12 +390,12 @@ public interface CodeInsightTestFixture extends IdeaProjectTestFixture {
    */
   void testCompletion(@TestDataFile @NotNull String fileBefore,
                       @NotNull @TestDataFile String fileAfter,
-                      @NotNull String... additionalFiles);
+                      @TestDataFile @NotNull String... additionalFiles);
 
   void testCompletionTyping(@NotNull @TestDataFile String fileBefore,
                             @NotNull String toType,
                             @NotNull @TestDataFile String fileAfter,
-                            @NotNull String... additionalFiles);
+                            @TestDataFile @NotNull String... additionalFiles);
 
   /**
    * Runs basic completion in caret position in fileBefore.
@@ -416,7 +416,7 @@ public interface CodeInsightTestFixture extends IdeaProjectTestFixture {
   void testRename(@NotNull @TestDataFile String fileBefore,
                   @NotNull @TestDataFile String fileAfter,
                   @NotNull String newName,
-                  @NotNull String... additionalFiles);
+                  @TestDataFile @NotNull String... additionalFiles);
 
   void testRename(@NotNull @TestDataFile String fileAfter, @NotNull String newName);
 
@@ -429,7 +429,7 @@ public interface CodeInsightTestFixture extends IdeaProjectTestFixture {
   @NotNull
   RangeHighlighter[] testHighlightUsages(@NotNull @TestDataFile String... files);
 
-  void moveFile(@NotNull @TestDataFile String filePath, @NotNull String to, @NotNull String... additionalFiles);
+  void moveFile(@NotNull @TestDataFile String filePath, @NotNull String to, @TestDataFile @NotNull String... additionalFiles);
 
   /**
    * Returns gutter renderer at the caret position.
index 6f3c7ec15bb4ca6119b3d1db46caa6bc836f6ecb..89a552668a03ad7c146c87dfc2fd8b566f56c3a4 100644 (file)
@@ -646,7 +646,9 @@ public class CodeInsightTestFixtureImpl extends BaseFixture implements CodeInsig
   }
 
   @Override
-  public void testCompletion(@NotNull String fileBefore, @NotNull String fileAfter, @NotNull final String... additionalFiles) {
+  public void testCompletion(@NotNull String fileBefore,
+                             @NotNull String fileAfter,
+                             @TestDataFile @NotNull String... additionalFiles) {
     testCompletionTyping(fileBefore, "", fileAfter, additionalFiles);
   }
 
@@ -654,7 +656,7 @@ public class CodeInsightTestFixtureImpl extends BaseFixture implements CodeInsig
   public void testCompletionTyping(@NotNull @TestDataFile String fileBefore,
                                    @NotNull String toType,
                                    @NotNull @TestDataFile String fileAfter,
-                                   @NotNull String... additionalFiles) {
+                                   @TestDataFile @NotNull String... additionalFiles) {
     testCompletionTyping(ArrayUtil.reverseArray(ArrayUtil.append(additionalFiles, fileBefore)), toType, fileAfter);
   }
 
@@ -699,7 +701,7 @@ public class CodeInsightTestFixtureImpl extends BaseFixture implements CodeInsig
   public void testRename(@NotNull final String fileBefore,
                          @NotNull String fileAfter,
                          @NotNull String newName,
-                         @NotNull String... additionalFiles) {
+                         @TestDataFile @NotNull String... additionalFiles) {
     assertInitialized();
     configureByFiles(ArrayUtil.reverseArray(ArrayUtil.append(additionalFiles, fileBefore)));
     testRename(fileAfter, newName);
@@ -922,7 +924,7 @@ public class CodeInsightTestFixtureImpl extends BaseFixture implements CodeInsig
   }
 
   @Override
-  public void moveFile(@NotNull final String filePath, @NotNull final String to, @NotNull final String... additionalFiles) {
+  public void moveFile(@NotNull final String filePath, @NotNull final String to, @TestDataFile @NotNull final String... additionalFiles) {
     assertInitialized();
     final Project project = getProject();
     configureByFiles(ArrayUtil.reverseArray(ArrayUtil.append(additionalFiles, filePath)));
index 7fe86afd920d5a57cfb091253c32b2faae46fb11..dd12715efd62f343e25a40f0fab99bb123bf6f75 100644 (file)
@@ -1979,6 +1979,14 @@ public class StringUtil extends StringUtilRt {
     return text.substring(0, i);
   }
 
+  @NotNull
+  @Contract(pure = true)
+  public static String substringBeforeLast(@NotNull String text, @NotNull String subString) {
+    int i = text.lastIndexOf(subString);
+    if (i == -1) return text;
+    return text.substring(0, i);
+  }
+
   @Nullable
   @Contract(pure = true)
   public static String substringAfter(@NotNull String text, @NotNull String subString) {
@@ -3145,7 +3153,6 @@ public class StringUtil extends StringUtilRt {
         return false;
     }
 
-
   private static final Pattern UNICODE_CHAR = Pattern.compile("\\\\u[0-9a-eA-E]{4}");
 
   public static String replaceUnicodeEscapeSequences(String text) {
index c49c78bf17a00f3bc54e140adcdf512d0d0ff9e1..a912f2339871b44966105d56b9e4ac884014b60b 100644 (file)
@@ -600,4 +600,13 @@ public class StringUtilTest {
     assertEquals(3, StringUtil.countChars("abcddddefghd", 'd', 4, true));
     assertEquals(2, StringUtil.countChars("abcddddefghd", 'd', 4, 6, false));
   }
+
+  @Test
+  public void testSubstringBeforeLast() {
+    assertEquals("a", StringUtil.substringBeforeLast("abc", "b"));
+    assertEquals("abab", StringUtil.substringBeforeLast("ababbccc", "b"));
+    assertEquals("abc", StringUtil.substringBeforeLast("abc", ""));
+    assertEquals("abc", StringUtil.substringBeforeLast("abc", "1"));
+    assertEquals("", StringUtil.substringBeforeLast("", "1"));
+  }
 }
index 4c4d00c1c79815c40ccba1398e3b060fe3ac2ff2..661dea1d7cc706c31b71f7fcab2790f0a55b8b35 100644 (file)
@@ -16,6 +16,8 @@
   <resource-bundle>org.jetbrains.idea.devkit.DevKitBundle</resource-bundle>
 
   <extensions defaultExtensionNs="com.intellij">
+    <virtualFileSystem key="testdata" implementationClass="org.jetbrains.idea.devkit.testAssistant.vfs.TestDataGroupFileSystem" />
+    <editorTabTitleProvider implementation="org.jetbrains.idea.devkit.testAssistant.TestDataGroupEditorTabTitleProvider" />
 
     <runLineMarkerContributor language="JAVA" implementationClass="org.jetbrains.idea.devkit.testAssistant.TestDataLineMarkerProvider"/>
     <fileEditorProvider implementation="org.jetbrains.idea.devkit.testAssistant.TestDataGroupEditorProvider"/>
index db4a9f666f5de527cf7e64a92eb0f5aec51b3c4d..232ae71c55989033e4a346ea07e229479914c2d3 100644 (file)
@@ -61,6 +61,13 @@ run.configuration.no.module.specified=No plugin module specified for configurati
 run.configuration.title=Plugin
 run.configuration.type.description=Plugin Sandbox Environment
 
+#Test Data
+testdata.create.dialog.title=Create Testdata File
+testdata.file.doesn.not.exist=The referenced testdata file {0} does not exist. Would you like to create it?
+testdata.create.missing.files=Create Missing Files
+testdata.confirm.create.missing.files.dialog.message=The following testdata files will be created:\n{0}
+testdata.searching=Searching for Testdata Files
+
 #Misc
 info.message=Info
 new.action.id=&Action ID:
index 5f9bb41f3f4e80e7bc50f1473fe3eeccd1f1db06..a08d63c366c7be1055dda31bc98d28b407445750 100644 (file)
@@ -22,6 +22,7 @@ import com.intellij.openapi.project.DumbAware;
 import com.intellij.openapi.project.Project;
 import com.intellij.openapi.vfs.VirtualFile;
 import org.jetbrains.annotations.NotNull;
+import org.jetbrains.idea.devkit.testAssistant.vfs.TestDataGroupVirtualFile;
 
 /**
  * @author yole
diff --git a/plugins/devkit/src/testAssistant/TestDataGroupEditorTabTitleProvider.java b/plugins/devkit/src/testAssistant/TestDataGroupEditorTabTitleProvider.java
new file mode 100644 (file)
index 0000000..5d4e1af
--- /dev/null
@@ -0,0 +1,35 @@
+/*
+ * Copyright 2000-2017 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.idea.devkit.testAssistant;
+
+import com.intellij.openapi.fileEditor.impl.EditorTabTitleProvider;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.vfs.VirtualFile;
+import org.jetbrains.annotations.Nullable;
+import org.jetbrains.idea.devkit.testAssistant.vfs.TestDataGroupVirtualFile;
+
+public class TestDataGroupEditorTabTitleProvider implements EditorTabTitleProvider {
+  @Nullable
+  @Override
+  public String getEditorTabTitle(Project project, VirtualFile file) {
+    if (!(file instanceof TestDataGroupVirtualFile)) {
+      return null;
+    }
+
+    // TestDataGroupVirtualFile.getName() implementation is fine
+    return file.getName();
+  }
+}
index 678f00e04bf1a52834d8d4e6753e951f8c1343ec..47b891d631509ca976021520fdfce2af4fb1cbc8 100644 (file)
@@ -27,6 +27,7 @@ import com.intellij.openapi.util.UserDataHolderBase;
 import com.intellij.reference.SoftReference;
 import com.intellij.util.ui.JBUI;
 import org.jetbrains.annotations.NotNull;
+import org.jetbrains.idea.devkit.testAssistant.vfs.TestDataGroupVirtualFile;
 
 import javax.swing.*;
 import java.awt.*;
index d1773dac4504f525578a303c4e896a027e9ca378..148415831366755f7e0673776b19ecb08dadccec 100644 (file)
@@ -18,7 +18,13 @@ package org.jetbrains.idea.devkit.testAssistant;
 import com.intellij.codeInsight.AnnotationUtil;
 import com.intellij.codeInsight.TestFrameworks;
 import com.intellij.ide.util.gotoByName.GotoFileModel;
+import com.intellij.openapi.application.ApplicationManager;
+import com.intellij.openapi.module.Module;
+import com.intellij.openapi.module.ModuleUtilCore;
+import com.intellij.openapi.progress.ProcessCanceledException;
+import com.intellij.openapi.progress.ProgressIndicator;
 import com.intellij.openapi.progress.ProgressManager;
+import com.intellij.openapi.project.Project;
 import com.intellij.openapi.roots.ProjectFileIndex;
 import com.intellij.openapi.roots.ProjectRootManager;
 import com.intellij.openapi.util.io.FileUtil;
@@ -28,6 +34,7 @@ import com.intellij.openapi.vfs.VirtualFile;
 import com.intellij.psi.PsiClass;
 import com.intellij.psi.PsiFile;
 import com.intellij.psi.PsiMethod;
+import com.intellij.psi.codeStyle.NameUtil;
 import com.intellij.psi.util.CachedValueProvider;
 import com.intellij.psi.util.CachedValuesManager;
 import com.intellij.psi.util.PsiModificationTracker;
@@ -36,9 +43,10 @@ import com.intellij.testIntegration.TestFramework;
 import com.intellij.util.CommonProcessors;
 import com.intellij.util.PathUtil;
 import com.intellij.util.containers.ContainerUtil;
-import com.intellij.util.containers.HashSet;
+import com.intellij.util.containers.HashMap;
 import org.jetbrains.annotations.NotNull;
 import org.jetbrains.annotations.Nullable;
+import org.jetbrains.idea.devkit.DevKitBundle;
 import org.jetbrains.jps.model.java.JavaModuleSourceRootTypes;
 
 import java.io.File;
@@ -170,59 +178,79 @@ public class TestDataGuessByExistingFilesUtil {
   private static TestDataDescriptor buildDescriptor(@NotNull String test,
                                                     @NotNull PsiClass psiClass)
   {
-    ProjectFileIndex fileIndex = ProjectRootManager.getInstance(psiClass.getProject()).getFileIndex();
-    GotoFileModel gotoModel = new GotoFileModel(psiClass.getProject());
-    Set<TestLocationDescriptor> descriptors = new HashSet<>();
-    // PhpStorm has tests that use '$' symbol as a file path separator, e.g. 'test$while_stmt$declaration' test 
+    // PhpStorm has tests that use '$' symbol as a file path separator, e.g. 'test$while_stmt$declaration' test
     // stands for '/while_smt/declaration.php' file somewhere in a test data.
     final String possibleFileName = ContainerUtil.getLastItem(StringUtil.split(test, "$"), test);
     assert possibleFileName != null;
-    final String possibleFilePath = test.replace('$', '/');
-    final Collection<String> fileNames = getAllFileNames(possibleFileName, gotoModel);
-    for (String name : fileNames) {
-      ProgressManager.checkCanceled();
-      final Object[] elements = gotoModel.getElementsByName(name, false, name);
-      for (Object element : elements) {
-        if (!(element instanceof PsiFile)) {
-          continue;
-        }
-        final VirtualFile file = ((PsiFile)element).getVirtualFile();
-        if (file == null || fileIndex.isInSource(file) && !fileIndex.isUnderSourceRootOfType(file, JavaModuleSourceRootTypes.RESOURCES)) {
-          continue;
-        }
-
-        final String filePath = file.getPath();
-        if (!StringUtil.containsIgnoreCase(filePath, possibleFilePath) && !StringUtil.containsIgnoreCase(filePath, test)) {
-          continue;
-        }
-        final String fileName = PathUtil.getFileName(filePath).toLowerCase();
-        int i = fileName.indexOf(possibleFileName.toLowerCase());
-        // Skip files that doesn't contain target test name and files that contain digit after target test name fragment.
-        // Example: there are tests with names 'testEnter()' and 'testEnter2()' and we don't want test data file 'testEnter2'
-        // to be matched to the test 'testEnter()'.
-        if (i < 0 || (i + possibleFileName.length() < fileName.length())
-                     && Character.isDigit(fileName.charAt(i + possibleFileName.length()))) {
-          continue;
-        }
+    if (possibleFileName.isEmpty()) {
+      return TestDataDescriptor.NOTHING_FOUND;
+    }
 
-        TestLocationDescriptor current = new TestLocationDescriptor();
-        current.populate(possibleFileName, file);
-        if (!current.isComplete()) {
-          continue;
+    Project project = psiClass.getProject();
+    ProjectFileIndex fileIndex = ProjectRootManager.getInstance(project).getFileIndex();
+    GotoFileModel gotoModel = new GotoFileModel(project);
+    final String possibleFilePath = test.replace('$', '/');
+    Map<String, TestLocationDescriptor> descriptorsByFileNames = new HashMap<>();
+    boolean completed = ProgressManager.getInstance().runProcessWithProgressSynchronously(() -> {
+      Module module = ModuleUtilCore.findModuleForPsiElement(psiClass);
+      final Collection<String> fileNames = getAllFileNames(possibleFileName, gotoModel);
+      ProgressIndicator indicator = ProgressManager.getInstance().getProgressIndicator();
+      indicator.setIndeterminate(false);
+      ApplicationManager.getApplication().runReadAction(() -> {
+        int fileNamesCount = fileNames.size();
+        double currentIndex = 0;
+        for (String name : fileNames) {
+          ProgressManager.checkCanceled();
+          final Object[] elements = gotoModel.getElementsByName(name, false, name);
+          for (Object element : elements) {
+            if (!(element instanceof PsiFile)) {
+              continue;
+            }
+            final VirtualFile file = ((PsiFile)element).getVirtualFile();
+            if (file == null || fileIndex.isInSource(file) && !fileIndex.isUnderSourceRootOfType(file, JavaModuleSourceRootTypes.RESOURCES)) {
+              continue;
+            }
+
+            final String filePath = file.getPath();
+            if (!StringUtil.containsIgnoreCase(filePath, possibleFilePath) && !StringUtil.containsIgnoreCase(filePath, test)) {
+              continue;
+            }
+            final String fileName = PathUtil.getFileName(filePath).toLowerCase();
+            int i = fileName.indexOf(possibleFileName.toLowerCase());
+            // Skip files that doesn't contain target test name and files that contain digit after target test name fragment.
+            // Example: there are tests with names 'testEnter()' and 'testEnter2()' and we don't want test data file 'testEnter2'
+            // to be matched to the test 'testEnter()'.
+            if (i < 0 || (i + possibleFileName.length() < fileName.length())
+                         && Character.isDigit(fileName.charAt(i + possibleFileName.length()))) {
+              continue;
+            }
+
+            TestLocationDescriptor current = new TestLocationDescriptor();
+            current.populate(possibleFileName, file, project, module);
+            if (!current.isComplete()) {
+              continue;
+            }
+
+            TestLocationDescriptor previousDescriptor = descriptorsByFileNames.get(name);
+            if (previousDescriptor == null) {
+              descriptorsByFileNames.put(name, current);
+              continue;
+            }
+            if (moreRelevantPath(current, previousDescriptor, psiClass)) {
+              descriptorsByFileNames.put(name, current);
+            }
+          }
+          indicator.setFraction(++currentIndex / fileNamesCount);
         }
+      });
+    }, DevKitBundle.message("testdata.searching"), true, project);
 
-        if (descriptors.isEmpty() || (descriptors.iterator().next().dir.equals(current.dir) && !descriptors.contains(current))) {
-          descriptors.add(current);
-          continue;
-        }
-        if (moreRelevantPath(current, descriptors, psiClass)) {
-          descriptors.clear();
-          descriptors.add(current);
-        }
-        break;
-      }
+    if (!completed) {
+      throw new ProcessCanceledException();
     }
-    return new TestDataDescriptor(descriptors, possibleFileName);
+
+    filterDirsFromOtherModules(descriptorsByFileNames);
+    return new TestDataDescriptor(descriptorsByFileNames.values(), possibleFileName);
   }
 
   private static Collection<String> getAllFileNames(final String testName, final GotoFileModel model) {
@@ -237,6 +265,16 @@ public class TestDataGuessByExistingFilesUtil {
     return processor.getResults();
   }
 
+  private static void filterDirsFromOtherModules(Map<String, TestLocationDescriptor> descriptorsByFileNames) {
+    if (descriptorsByFileNames.size() < 2) {
+      return;
+    }
+    if (descriptorsByFileNames.values().stream().noneMatch(descriptor -> descriptor.isFromCurrentModule)) {
+      return;
+    }
+    descriptorsByFileNames.entrySet().removeIf(e -> !e.getValue().isFromCurrentModule);
+  }
+
   @Nullable
   private static String getSimpleClassName(@NotNull PsiClass psiClass) {
     String result = psiClass.getQualifiedName();
@@ -252,7 +290,7 @@ public class TestDataGuessByExistingFilesUtil {
   }
   
   private static boolean moreRelevantPath(@NotNull TestLocationDescriptor candidate,
-                                          @NotNull Set<TestLocationDescriptor> currentDescriptors,
+                                          @NotNull TestLocationDescriptor current,
                                           @NotNull PsiClass psiClass)
   {
     final String className = psiClass.getQualifiedName();
@@ -260,16 +298,17 @@ public class TestDataGuessByExistingFilesUtil {
       return false;
     }
 
-    final TestLocationDescriptor current = currentDescriptors.iterator().next();
     boolean candidateMatched;
     boolean currentMatched;
 
     // By package.
-    int i = className.lastIndexOf(".");
-    if (i >= 0) {
-      String packageAsPath = className.substring(0, i).replace('.', '/').toLowerCase();
-      candidateMatched = candidate.dir.toLowerCase().contains(packageAsPath);
-      currentMatched = current.dir.toLowerCase().contains(packageAsPath);
+    int lastDotIndex = className.lastIndexOf(".");
+    String candidateLcDir = candidate.dir.toLowerCase();
+    String currentLcDir = current.dir.toLowerCase();
+    if (lastDotIndex >= 0) {
+      String packageAsPath = className.substring(0, lastDotIndex).replace('.', '/').toLowerCase();
+      candidateMatched = candidateLcDir.contains(packageAsPath);
+      currentMatched = currentLcDir.contains(packageAsPath);
       if (candidateMatched ^ currentMatched) {
         return candidateMatched;
       }
@@ -277,31 +316,64 @@ public class TestDataGuessByExistingFilesUtil {
 
     // By class name.
     String simpleName = getSimpleClassName(psiClass);
-    if (simpleName != null) {
-      String pattern = simpleName.toLowerCase();
-      candidateMatched = candidate.dir.toLowerCase().contains(pattern);
-      currentMatched = current.dir.toLowerCase().contains(pattern);
-      if (candidateMatched ^ currentMatched) {
-        return candidateMatched;
+    if (simpleName == null) {
+      return false;
+    }
+    String pattern = simpleName.toLowerCase();
+    candidateMatched = candidateLcDir.contains(pattern);
+    currentMatched = currentLcDir.contains(pattern);
+    if (candidateMatched ^ currentMatched) {
+      return candidateMatched;
+    }
+
+    // By class name words and their position. More words + greater position = better.
+    String[] words = NameUtil.nameToWords(simpleName);
+    int candidateWordsMatched = 0;
+    int currentWordsMatched = 0;
+    int candidateMatchPosition = -1;
+    int currentMatchPosition = -1;
+
+    StringBuilder currentNameSubstringSb = new StringBuilder();
+    for (int i = 0; i < words.length; i++) {
+      currentNameSubstringSb.append(words[i]);
+      String currentNameLcSubstring = currentNameSubstringSb.toString().toLowerCase();
+
+      int candidateWordsIndex = candidateLcDir.lastIndexOf(currentNameLcSubstring);
+      if (candidateWordsIndex > 0) {
+        candidateWordsMatched = i + 1;
+        candidateMatchPosition = candidateWordsIndex;
+      }
+
+      int currentWordsIndex = currentLcDir.lastIndexOf(currentNameLcSubstring);
+      if (currentWordsIndex > 0) {
+        currentWordsMatched = i + 1;
+        candidateMatchPosition = currentWordsIndex;
+      }
+
+      if (candidateWordsMatched != currentWordsMatched) {
+        break; // no need to continue
       }
     }
 
-    return false;
+    if (candidateWordsMatched != currentWordsMatched) {
+      return candidateWordsMatched > currentWordsMatched;
+    }
+    return candidateMatchPosition > currentMatchPosition;
   }
 
   private static class TestLocationDescriptor {
-
     public String dir;
     public String filePrefix;
     public String fileSuffix;
     public String ext;
     public boolean startWithLowerCase;
+    public boolean isFromCurrentModule;
 
     public boolean isComplete() {
       return dir != null && filePrefix != null && fileSuffix != null && ext != null;
     }
 
-    public void populate(@NotNull String testName, @NotNull VirtualFile matched) {
+    public void populate(@NotNull String testName, @NotNull VirtualFile matched, @NotNull Project project, @Nullable Module module) {
       if (testName.isEmpty()) return;
       final String withoutExtension = FileUtil.getNameWithoutExtension(testName);
       boolean excludeExtension = !withoutExtension.equals(testName);
@@ -327,6 +399,9 @@ public class TestDataGuessByExistingFilesUtil {
       fileSuffix = fileName.substring(i + testName.length());
       ext = excludeExtension ? "" : matched.getExtension();
       dir = matched.getParent().getPath();
+      if (module != null) {
+        isFromCurrentModule = module.equals(ModuleUtilCore.findModuleForFile(matched, project));
+      }
     }
 
     @Override
@@ -403,10 +478,10 @@ public class TestDataGuessByExistingFilesUtil {
       for (TestLocationDescriptor descriptor : myDescriptors) {
         if (root != null && !root.equals(descriptor.dir)) continue;
         result.add(String.format(
-          "%s/%s%c%s%s.%s",
+          "%s/%s%c%s%s%s",
           descriptor.dir, descriptor.filePrefix,
           descriptor.startWithLowerCase ? Character.toLowerCase(testName.charAt(0)) : Character.toUpperCase(testName.charAt(0)),
-          testName.substring(1), descriptor.fileSuffix, descriptor.ext
+          testName.substring(1), descriptor.fileSuffix, StringUtil.isEmpty(descriptor.ext) ? "" : "." + descriptor.ext
         ));
       }
       return result;
diff --git a/plugins/devkit/src/testAssistant/TestDataNavigationElement.java b/plugins/devkit/src/testAssistant/TestDataNavigationElement.java
new file mode 100644 (file)
index 0000000..9d8572f
--- /dev/null
@@ -0,0 +1,35 @@
+/*
+ * Copyright 2000-2017 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.idea.devkit.testAssistant;
+
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.util.Pair;
+import com.intellij.ui.SimpleTextAttributes;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import javax.swing.*;
+import java.util.List;
+
+public interface TestDataNavigationElement {
+  void performAction(@NotNull Project project);
+
+  @Nullable
+  Icon getIcon();
+
+  @NotNull
+  List<Pair<String, SimpleTextAttributes>> getTitleFragments();
+}
diff --git a/plugins/devkit/src/testAssistant/TestDataNavigationElementFactory.java b/plugins/devkit/src/testAssistant/TestDataNavigationElementFactory.java
new file mode 100644 (file)
index 0000000..35e8d94
--- /dev/null
@@ -0,0 +1,213 @@
+/*
+ * Copyright 2000-2017 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.idea.devkit.testAssistant;
+
+import com.google.common.collect.ImmutableList;
+import com.intellij.icons.AllIcons;
+import com.intellij.openapi.fileEditor.OpenFileDescriptor;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.ui.Messages;
+import com.intellij.openapi.util.Pair;
+import com.intellij.openapi.util.text.StringUtil;
+import com.intellij.openapi.vfs.LocalFileSystem;
+import com.intellij.openapi.vfs.VirtualFile;
+import com.intellij.ui.SimpleTextAttributes;
+import com.intellij.util.PathUtil;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+import org.jetbrains.idea.devkit.DevKitBundle;
+import org.jetbrains.idea.devkit.testAssistant.vfs.TestDataGroupVirtualFile;
+
+import javax.swing.*;
+import java.util.*;
+
+public class TestDataNavigationElementFactory {
+  private static final int CREATE_MISSING_FILES_WITHOUT_CONFIRMATION_LIMIT = 3;
+
+  private TestDataNavigationElementFactory() {
+  }
+
+  @NotNull
+  public static TestDataNavigationElement createForFile(@NotNull Project project, @NotNull String path) {
+    return new TestDataFileNavigationElement(project, path);
+  }
+
+  @NotNull
+  public static TestDataNavigationElement createForGroup(@NotNull Project project, @NotNull TestDataGroupVirtualFile group) {
+    return new TestDataGroupNavigationElement(project, group);
+  }
+
+  @NotNull
+  public static TestDataNavigationElement createForCreateMissingFilesOption(@NotNull List<String> filePaths) {
+    return new CreateMissingTestDataFilesNavigationElement(filePaths);
+  }
+
+
+  private static class CreateMissingTestDataFilesNavigationElement implements TestDataNavigationElement {
+    private final List<String> myFilePaths;
+
+    private CreateMissingTestDataFilesNavigationElement(List<String> filePaths) {
+      myFilePaths = filePaths;
+    }
+
+    @Override
+    public void performAction(@NotNull Project project) {
+      Set<String> filePathsToCreate = new HashSet<>();
+      for (String path : myFilePaths) {
+        if (LocalFileSystem.getInstance().refreshAndFindFileByPath(path) == null) {
+          filePathsToCreate.add(path);
+        }
+      }
+
+      if (filePathsToCreate.size() > CREATE_MISSING_FILES_WITHOUT_CONFIRMATION_LIMIT) {
+        int code = Messages.showOkCancelDialog(
+          project, DevKitBundle.message("testdata.confirm.create.missing.files.dialog.message", StringUtil.join(filePathsToCreate, "\n")),
+          DevKitBundle.message("testdata.create.missing.files"), Messages.getQuestionIcon());
+        if (code != Messages.OK) {
+          return;
+        }
+      }
+
+      filePathsToCreate.forEach(path -> {
+        VirtualFile file = TestDataUtil.createFileByName(project, path);
+        new OpenFileDescriptor(project, file).navigate(true);
+      });
+    }
+
+    @Override
+    public Icon getIcon() {
+      return null;
+    }
+
+    @NotNull
+    @Override
+    public List<Pair<String, SimpleTextAttributes>> getTitleFragments() {
+      return Collections.singletonList(new Pair<>(
+        DevKitBundle.message("testdata.create.missing.files"), SimpleTextAttributes.REGULAR_ITALIC_ATTRIBUTES));
+    }
+  }
+
+  private static class TestDataGroupNavigationElement implements TestDataNavigationElement {
+    private final Project myProject;
+    private final TestDataGroupVirtualFile myGroup;
+
+    private TestDataGroupNavigationElement(Project project, TestDataGroupVirtualFile group) {
+      myProject = project;
+      myGroup = group;
+    }
+
+    @Override
+    public void performAction(@NotNull Project project) {
+      new OpenFileDescriptor(project, myGroup).navigate(true);
+    }
+
+    @Nullable
+    @Override
+    public Icon getIcon() {
+      return AllIcons.Nodes.TestSourceFolder;
+    }
+
+    @NotNull
+    @Override
+    public List<Pair<String, SimpleTextAttributes>> getTitleFragments() {
+      VirtualFile beforeFile = myGroup.getBeforeFile();
+      VirtualFile afterFile = myGroup.getAfterFile();
+      String beforeName = beforeFile.getName();
+      String afterName = afterFile.getName();
+
+      List<Pair<String, SimpleTextAttributes>> result = new ArrayList<>();
+      result.add(new Pair<>("<" + beforeName + ", " + afterName + "> (", SimpleTextAttributes.REGULAR_ATTRIBUTES));
+
+      Pair<String, String> beforeRelativePath = TestDataUtil.getModuleOrProjectRelativeParentPath(myProject, beforeFile);
+      Pair<String, String> afterRelativePath = TestDataUtil.getModuleOrProjectRelativeParentPath(myProject, afterFile);
+      if (beforeRelativePath != null && afterRelativePath != null) {
+        String beforeBase = beforeRelativePath.getFirst();
+        String afterBase = afterRelativePath.getFirst();
+        String beforeBaseRelativePath = beforeRelativePath.getSecond();
+        String afterBaseRelativePath = afterRelativePath.getSecond();
+
+        if (beforeBase.equals(afterBase)) {
+          result.add(new Pair<>(beforeBase, SimpleTextAttributes.REGULAR_BOLD_ATTRIBUTES));
+          if (beforeBaseRelativePath.equals(afterBaseRelativePath)) { // same dir
+            result.add(new Pair<>("/" + beforeBaseRelativePath + "/)", SimpleTextAttributes.REGULAR_ATTRIBUTES));
+          }
+          else { // same base but different dirs
+            result.add(new Pair<>("/", SimpleTextAttributes.REGULAR_ATTRIBUTES));
+            String commonPrefix = StringUtil.commonPrefix(beforeBaseRelativePath, afterBaseRelativePath);
+            if (!commonPrefix.isEmpty()) {
+              result.add(new Pair<>(commonPrefix, SimpleTextAttributes.REGULAR_ATTRIBUTES));
+            }
+            String beforeUniqueSuffix = beforeBaseRelativePath.substring(commonPrefix.length());
+            String afterUniqueSuffix = afterBaseRelativePath.substring(commonPrefix.length());
+            result.add(new Pair<>("<" + beforeUniqueSuffix + "/, " + afterUniqueSuffix + "/>", SimpleTextAttributes.REGULAR_ATTRIBUTES));
+          }
+        }
+        else { // different bases
+          result.add(new Pair<>(beforeBase, SimpleTextAttributes.REGULAR_BOLD_ATTRIBUTES));
+          result.add(new Pair<>("/" + beforeBaseRelativePath + "/, ", SimpleTextAttributes.REGULAR_ATTRIBUTES));
+          result.add(new Pair<>(afterBase, SimpleTextAttributes.REGULAR_BOLD_ATTRIBUTES));
+          result.add(new Pair<>("/" + afterBaseRelativePath + "/", SimpleTextAttributes.REGULAR_ATTRIBUTES));
+        }
+      }
+
+      return result;
+    }
+  }
+
+  private static class TestDataFileNavigationElement implements TestDataNavigationElement {
+    private final Project myProject;
+    private final String myPath;
+
+    private TestDataFileNavigationElement(Project project, String path) {
+      myProject = project;
+      myPath = path;
+    }
+
+    @Override
+    public void performAction(@NotNull Project project) {
+      TestDataUtil.openOrAskToCreateFile(project, myPath);
+    }
+
+    @Nullable
+    @Override
+    public Icon getIcon() {
+      return TestDataUtil.getIcon(myPath);
+    }
+
+    @NotNull
+    @Override
+    public List<Pair<String, SimpleTextAttributes>> getTitleFragments() {
+      VirtualFile file = TestDataUtil.getFileByPath(myPath);
+      if (file == null) {
+        return Collections.singletonList(new Pair<>(
+          String.format("%s (%s)", PathUtil.getFileName(myPath), PathUtil.getParentPath(myPath)),
+          SimpleTextAttributes.GRAYED_ATTRIBUTES));
+      }
+
+      Pair<String, String> relativePath = TestDataUtil.getModuleOrProjectRelativeParentPath(myProject, file);
+      if (relativePath == null) {
+        // cannot calculate module/project relative path, use absolute path
+        return Collections.singletonList(new Pair<>(
+          String.format("%s (%s)", file.getName(), PathUtil.getParentPath(myPath) + "/"),
+          SimpleTextAttributes.REGULAR_ATTRIBUTES));
+      }
+
+      return ImmutableList.of(new Pair<>(file.getName() + " (", SimpleTextAttributes.REGULAR_ATTRIBUTES),
+                              new Pair<>(relativePath.getFirst(), SimpleTextAttributes.REGULAR_BOLD_ATTRIBUTES),
+                              new Pair<>("/" + relativePath.getSecond() + "/)", SimpleTextAttributes.REGULAR_ATTRIBUTES));
+    }
+  }
+}
index df14cc55ade87a52610b588c8d0b40c0bf2de0b4..50eb17334622e4957d394905d54e3d2a438a5eca 100644 (file)
 package org.jetbrains.idea.devkit.testAssistant;
 
 import com.intellij.codeInsight.daemon.GutterIconNavigationHandler;
-import com.intellij.openapi.application.ApplicationManager;
 import com.intellij.openapi.fileEditor.OpenFileDescriptor;
-import com.intellij.openapi.fileTypes.FileType;
-import com.intellij.openapi.fileTypes.FileTypeManager;
 import com.intellij.openapi.project.Project;
-import com.intellij.openapi.ui.Messages;
 import com.intellij.openapi.ui.popup.PopupChooserBuilder;
-import com.intellij.openapi.util.Computable;
 import com.intellij.openapi.util.text.StringUtil;
 import com.intellij.openapi.vfs.LocalFileSystem;
-import com.intellij.openapi.vfs.VfsUtil;
-import com.intellij.openapi.vfs.VirtualFile;
 import com.intellij.psi.PsiMethod;
 import com.intellij.ui.ColoredListCellRenderer;
 import com.intellij.ui.awt.RelativePoint;
 import com.intellij.ui.components.JBList;
-import com.intellij.util.ArrayUtil;
 import com.intellij.util.PathUtil;
+import com.intellij.util.containers.ContainerUtil;
 import org.jetbrains.annotations.NotNull;
 import org.jetbrains.annotations.Nullable;
+import org.jetbrains.idea.devkit.testAssistant.vfs.TestDataGroupVirtualFile;
 
 import javax.swing.*;
 import java.awt.event.MouseEvent;
-import java.io.File;
-import java.io.IOException;
 import java.util.ArrayList;
+import java.util.Comparator;
 import java.util.List;
+import java.util.ListIterator;
 
-/**
-* @author yole
-*/
 public class TestDataNavigationHandler implements GutterIconNavigationHandler<PsiMethod> {
-  public void navigate(MouseEvent e, final PsiMethod elt) {
+  @Override
+  public void navigate(MouseEvent e, PsiMethod elt) {
     List<String> fileNames = getFileNames(elt);
 
     if (fileNames == null || fileNames.isEmpty()) {
@@ -71,14 +63,14 @@ public class TestDataNavigationHandler implements GutterIconNavigationHandler<Ps
     return fileNames;
   }
 
-  public static void navigate(@NotNull final RelativePoint point,
-                              @NotNull List<String> testDataFiles, 
-                              final Project project) {
+  public static void navigate(@NotNull RelativePoint point,
+                              @NotNull List<String> testDataFiles,
+                              Project project) {
     if (testDataFiles.size() == 1) {
-      openFileByIndex(project, testDataFiles, 0);
+      TestDataUtil.openOrAskToCreateFile(project, testDataFiles.get(0));
     }
     else if (testDataFiles.size() > 1) {
-      TestDataGroupVirtualFile groupFile = getTestDataGroup(testDataFiles);
+      TestDataGroupVirtualFile groupFile = TestDataUtil.getTestDataGroup(testDataFiles);
       if (groupFile != null) {
         new OpenFileDescriptor(project, groupFile).navigate(true);
       }
@@ -88,104 +80,97 @@ public class TestDataNavigationHandler implements GutterIconNavigationHandler<Ps
     }
   }
 
-  @Nullable
-  private static TestDataGroupVirtualFile getTestDataGroup(List<String> fileNames) {
-    if (fileNames.size() != 2) {
-      return null;
-    }
-    VirtualFile file1 = LocalFileSystem.getInstance().refreshAndFindFileByPath(fileNames.get(0));
-    VirtualFile file2 = LocalFileSystem.getInstance().refreshAndFindFileByPath(fileNames.get(1));
-    if (file1 == null || file2 == null) {
-      return null;
-    }
-    final int commonPrefixLength = StringUtil.commonPrefixLength(file1.getName(), file2.getName());
-    if (file1.getName().substring(commonPrefixLength).toLowerCase().contains("after")) {
-      return new TestDataGroupVirtualFile(file2, file1);
-    }
-    if (file2.getName().substring(commonPrefixLength).toLowerCase().contains("after")) {
-      return new TestDataGroupVirtualFile(file1, file2);
-    }
-    return null;
-  }
+  /**
+   * Shows navigation popup with list of testdata files and (optionally) "Create missing files" option.
+   * @param project project.
+   * @param filePaths paths of testdata files with "/" path separator. This List can be changed.
+   * @param point point where the popup will be shown.
+   */
+  private static void showNavigationPopup(Project project, List<String> filePaths, RelativePoint point) {
+    List<TestDataNavigationElement> elementsToDisplay = getElementsToDisplay(project, filePaths);
 
-  private static void showNavigationPopup(final Project project, final List<String> fileNames, final RelativePoint point) {
-    List<String> listPaths = new ArrayList<>(fileNames);
-    final String CREATE_MISSING_OPTION = "Create Missing Files";
-    if (fileNames.size() == 2) {
-      VirtualFile file1 = LocalFileSystem.getInstance().refreshAndFindFileByPath(fileNames.get(0));
-      VirtualFile file2 = LocalFileSystem.getInstance().refreshAndFindFileByPath(fileNames.get(1));
-      if (file1 == null || file2 == null) {
-        listPaths.add(CREATE_MISSING_OPTION);
+    // if at least one file doesn't exist add "Create missing files" element
+    for (String path : filePaths) {
+      if (LocalFileSystem.getInstance().refreshAndFindFileByPath(path) == null) {
+        elementsToDisplay.add(TestDataNavigationElementFactory.createForCreateMissingFilesOption(filePaths));
+        break;
       }
     }
-    final JList list = new JBList(ArrayUtil.toStringArray(listPaths));
-    list.setCellRenderer(new ColoredListCellRenderer() {
+
+    JList<TestDataNavigationElement> list = new JBList<>(elementsToDisplay);
+    list.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
+    list.setCellRenderer(new ColoredListCellRenderer<TestDataNavigationElement>() {
       @Override
-      protected void customizeCellRenderer(@NotNull JList list, Object value, int index, boolean selected, boolean hasFocus) {
-        String path = (String)value;
-        String fileName = PathUtil.getFileName(path);
-        if (!fileName.equals(CREATE_MISSING_OPTION)) {
-          final FileType fileType = FileTypeManager.getInstance().getFileTypeByFileName(fileName);
-          setIcon(fileType.getIcon());
-        }
-        append(String.format("%s (%s)", fileName, PathUtil.getParentPath(path)));
+      protected void customizeCellRenderer(@NotNull JList list, TestDataNavigationElement element, int index,
+                                           boolean selected, boolean hasFocus) {
+        element.getTitleFragments().forEach(pair -> append(pair.getFirst(), pair.getSecond()));
+        setIcon(element.getIcon());
       }
     });
+
     PopupChooserBuilder builder = new PopupChooserBuilder(list);
     builder.setItemChoosenCallback(() -> {
-      final int[] indices = list.getSelectedIndices();
-      if (ArrayUtil.indexOf(indices, fileNames.size()) >= 0) {
-        createMissingFiles(project, fileNames);
-      }
-      else {
-        for (int index : indices) {
-          openFileByIndex(project, fileNames, index);
-        }
+      TestDataNavigationElement selectedElement = list.getSelectedValue();
+      if (selectedElement != null) {
+        selectedElement.performAction(project);
       }
     }).createPopup().show(point);
   }
 
-  private static void createMissingFiles(Project project, List<String> fileNames) {
-    for (String name : fileNames) {
-      if (LocalFileSystem.getInstance().refreshAndFindFileByPath(name) == null) {
-        createFileByName(project, name);
-      }
-    }
-    final TestDataGroupVirtualFile testDataGroup = getTestDataGroup(fileNames);
-    if (testDataGroup != null) {
-      new OpenFileDescriptor(project, testDataGroup).navigate(true);
-    }
-  }
+  private static List<TestDataNavigationElement> getElementsToDisplay(Project project, List<String> filePaths) {
+    ContainerUtil.removeDuplicates(filePaths);
 
-  private static void openFileByIndex(final Project project, final List<String> fileNames, final int index) {
-    final String path = fileNames.get(index);
-    final VirtualFile file = LocalFileSystem.getInstance().refreshAndFindFileByPath(path);
-    if (file != null) {
-      new OpenFileDescriptor(project, file).navigate(true);
-    }
-    else {
-      int rc = Messages.showYesNoDialog(project, "The referenced testdata file " + path + " does not exist. Would you like to create it?",
-                                        "Create Testdata File", Messages.getQuestionIcon());
-      if (rc == Messages.YES) {
-        VirtualFile vFile = createFileByName(project, path);
-        new OpenFileDescriptor(project, vFile).navigate(true);
+    filePaths.sort(new Comparator<String>() {
+      @Override
+      public int compare(String path1, String path2) {
+        String name1 = stripBeforeAfterFromFileName(PathUtil.getFileName(path1));
+        String name2 = stripBeforeAfterFromFileName(PathUtil.getFileName(path2));
+        return name1.compareToIgnoreCase(name2);
       }
-    }
-  }
 
-  private static VirtualFile createFileByName(final Project project, final String path) {
-    return ApplicationManager.getApplication().runWriteAction(new Computable<VirtualFile>() {
-      public VirtualFile compute() {
-        try {
-          final File file = new File(path);
-          final VirtualFile parent = VfsUtil.createDirectories(file.getParent());
-          return parent.createChildData(this, file.getName());
+      private String stripBeforeAfterFromFileName(String name) {
+        String result = StringUtil.trimStart(name, TestDataUtil.TESTDATA_FILE_BEFORE_PREFIX);
+        result = StringUtil.trimStart(result, TestDataUtil.TESTDATA_FILE_AFTER_PREFIX);
+
+        String extension = PathUtil.getFileExtension(result);
+        if (extension != null) {
+          extension = "." + extension;
+          if (result.endsWith(TestDataUtil.TESTDATA_FILE_AFTER_SUFFIX + extension)) {
+            result = StringUtil.substringBeforeLast(result, TestDataUtil.TESTDATA_FILE_AFTER_SUFFIX + extension) + extension;
+          }
+          else if (result.endsWith(TestDataUtil.TESTDATA_FILE_BEFORE_SUFFIX + extension)) {
+            result = StringUtil.substringBeforeLast(result, TestDataUtil.TESTDATA_FILE_BEFORE_SUFFIX + extension) + extension;
+          }
         }
-        catch (IOException e) {
-          Messages.showErrorDialog(project, e.getMessage(), "Create Testdata File");
-          return null;
+        else {
+          result = StringUtil.trimEnd(result, TestDataUtil.TESTDATA_FILE_AFTER_SUFFIX);
+          result = StringUtil.trimEnd(result, TestDataUtil.TESTDATA_FILE_BEFORE_SUFFIX);
         }
+
+        return result;
       }
     });
+
+    List<TestDataNavigationElement> result = new ArrayList<>();
+    for (ListIterator<String> iterator = filePaths.listIterator(); iterator.hasNext(); ) {
+      String path = iterator.next();
+
+      // check if there's a testdata group
+      if (iterator.hasNext()) {
+        String nextPath = iterator.next();
+        TestDataGroupVirtualFile group = TestDataUtil.getTestDataGroup(path, nextPath);
+        if (group != null) {
+          result.add(TestDataNavigationElementFactory.createForGroup(project, group));
+          continue;
+        }
+        else {
+          iterator.previous();
+        }
+      }
+
+      result.add(TestDataNavigationElementFactory.createForFile(project, path));
+    }
+
+    return result;
   }
 }
index 587d1c212f32bc13c5b77a6aa514cd57a10a4210..dfcfe9a2041f16040ee53d51a0b1cf622fd5a618 100644 (file)
@@ -17,6 +17,7 @@ package org.jetbrains.idea.devkit.testAssistant;
 
 import com.intellij.openapi.util.Computable;
 import com.intellij.openapi.util.NullableComputable;
+import com.intellij.openapi.util.Pair;
 import com.intellij.openapi.util.text.StringUtil;
 import com.intellij.psi.*;
 import com.intellij.testFramework.PlatformTestUtil;
@@ -70,7 +71,7 @@ public class TestDataReferenceCollector {
   @NotNull
   private List<String> collectTestDataReferences(final PsiMethod method,
                                                  final Map<String, Computable<UValue>> argumentMap,
-                                                 final HashSet<PsiMethod> proceed) {
+                                                 final HashSet<Pair<PsiMethod, Set<UExpression>>> proceed) {
     final List<String> result = new ArrayList<>();
     if (myTestDataPath == null) {
       return result;
@@ -84,6 +85,7 @@ public class TestDataReferenceCollector {
       public boolean visitCallExpression(@NotNull UCallExpression expression) {
         String callText = expression.getMethodName();
         if (callText == null) return true;
+
         UMethod callee = UastContextKt.toUElement(expression.resolve(), UMethod.class);
         if (callee != null && callee.hasModifierProperty(PsiModifier.ABSTRACT)) {
           final PsiClass calleeContainingClass = callee.getContainingClass();
@@ -94,7 +96,9 @@ public class TestDataReferenceCollector {
             }
           }
         }
-        if (callee != null && proceed.add(callee)) {
+
+        Pair<PsiMethod, Set<UExpression>> methodWithArguments = new Pair<>(callee, new HashSet<>(expression.getValueArguments()));
+        if (callee != null && proceed.add(methodWithArguments)) {
           boolean haveAnnotatedParameters = false;
           final PsiParameter[] psiParameters = callee.getParameterList().getParameters();
           for (int i = 0, psiParametersLength = psiParameters.length; i < psiParametersLength; i++) {
@@ -102,7 +106,12 @@ public class TestDataReferenceCollector {
             final PsiModifierList modifierList = psiParameter.getModifierList();
             if (modifierList != null && modifierList.findAnnotation(TEST_DATA_FILE_ANNOTATION_QUALIFIED_NAME) != null) {
               myFoundTestDataParameters = true;
-              processCallArgument(expression, argumentMap, result, i);
+              if (psiParameter.isVarArgs()) {
+                processVarargCallArgument(expression, argumentMap, result);
+              }
+              else {
+                processCallArgument(expression, argumentMap, result, i);
+              }
               haveAnnotatedParameters = true;
             }
           }
@@ -112,21 +121,32 @@ public class TestDataReferenceCollector {
         }
         return true;
       }
-    });
-    return result;
-  }
 
-  private void processCallArgument(UCallExpression expression,
-                                   Map<String, Computable<UValue>> argumentMap,
-                                   List<String> result,
-                                   final int index) {
-    final List<UExpression> arguments = expression.getValueArguments();
-    if (arguments.size() > index) {
-      UValue testDataFileValue = UEvaluationContextKt.uValueOf(arguments.get(index), new TestDataEvaluatorExtension(argumentMap));
-      if (testDataFileValue instanceof UStringConstant) {
-        result.add(myTestDataPath + ((UStringConstant) testDataFileValue).getValue());
+      private void processCallArgument(UCallExpression expression, Map<String, Computable<UValue>> argumentMap,
+                                       Collection<String> result, int index) {
+        List<UExpression> arguments = expression.getValueArguments();
+        if (arguments.size() > index) {
+          handleArgument(arguments.get(index), argumentMap, result);
+        }
       }
-    }
+
+      private void processVarargCallArgument(UCallExpression expression, Map<String, Computable<UValue>> argumentMap,
+                                             Collection<String> result) {
+        List<UExpression> arguments = expression.getValueArguments();
+        for (UExpression argument : arguments) {
+          handleArgument(argument, argumentMap, result);
+        }
+      }
+
+      private void handleArgument(UExpression argument, Map<String, Computable<UValue>> argumentMap, Collection<String> result) {
+        UValue testDataFileValue = UEvaluationContextKt.uValueOf(argument, new TestDataEvaluatorExtension(argumentMap));
+        if (testDataFileValue instanceof UStringConstant) {
+          result.add(myTestDataPath + ((UStringConstant) testDataFileValue).getValue());
+        }
+      }
+    });
+
+    return result;
   }
 
   private Map<String, Computable<UValue>> buildArgumentMap(UCallExpression expression, PsiMethod method) {
diff --git a/plugins/devkit/src/testAssistant/TestDataUtil.java b/plugins/devkit/src/testAssistant/TestDataUtil.java
new file mode 100644 (file)
index 0000000..ca317bd
--- /dev/null
@@ -0,0 +1,182 @@
+/*
+ * Copyright 2000-2017 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.idea.devkit.testAssistant;
+
+import com.intellij.openapi.application.ApplicationManager;
+import com.intellij.openapi.fileEditor.OpenFileDescriptor;
+import com.intellij.openapi.fileTypes.FileType;
+import com.intellij.openapi.fileTypes.FileTypeManager;
+import com.intellij.openapi.module.Module;
+import com.intellij.openapi.module.ModuleUtilCore;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.ui.Messages;
+import com.intellij.openapi.util.Computable;
+import com.intellij.openapi.util.Pair;
+import com.intellij.openapi.util.text.StringUtil;
+import com.intellij.openapi.vfs.LocalFileSystem;
+import com.intellij.openapi.vfs.VfsUtil;
+import com.intellij.openapi.vfs.VfsUtilCore;
+import com.intellij.openapi.vfs.VirtualFile;
+import org.jetbrains.annotations.NonNls;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+import org.jetbrains.idea.devkit.DevKitBundle;
+import org.jetbrains.idea.devkit.testAssistant.vfs.TestDataGroupVirtualFile;
+
+import javax.swing.*;
+import java.io.File;
+import java.io.IOException;
+import java.util.List;
+
+class TestDataUtil {
+  public static final String TESTDATA_FILE_BEFORE_PREFIX = "before";
+  public static final String TESTDATA_FILE_AFTER_PREFIX = "after";
+  // Note: these suffixes are used BEFORE the file extension
+  public static final String TESTDATA_FILE_BEFORE_SUFFIX = "_before";
+  public static final String TESTDATA_FILE_AFTER_SUFFIX = "_after";
+
+  private TestDataUtil() {
+  }
+
+  @Nullable
+  static TestDataGroupVirtualFile getTestDataGroup(@NotNull List<String> fileNames) {
+    if (fileNames.size() != 2) {
+      return null;
+    }
+    return getTestDataGroup(fileNames.get(0), fileNames.get(1));
+  }
+
+  @Nullable
+  static TestDataGroupVirtualFile getTestDataGroup(@NotNull String fileName1, @NotNull String fileName2) {
+    VirtualFile file1 = getFileByPath(fileName1);
+    VirtualFile file2 = getFileByPath(fileName2);
+    if (file1 == null || file2 == null) {
+      return null;
+    }
+    @NonNls String file1Name = file1.getName();
+    @NonNls String file2Name = file2.getName();
+
+    int commonPrefixLength = StringUtil.commonPrefixLength(file1Name, file2Name);
+    if (commonPrefixLength == 0) {
+      //noinspection ConstantConditions - no NPE
+      if (isBeforeAfterPrefixedPair(file1Name, file2Name)) {
+        return new TestDataGroupVirtualFile(file1, file2);
+      }
+      if (isBeforeAfterPrefixedPair(file2Name, file1Name)) {
+        return new TestDataGroupVirtualFile(file2, file1);
+      }
+    }
+
+    if (isAfterSuffixed(file1Name, commonPrefixLength)) {
+      return new TestDataGroupVirtualFile(file2, file1);
+    }
+    if (isAfterSuffixed(file2Name, commonPrefixLength)) {
+      return new TestDataGroupVirtualFile(file1, file2);
+    }
+
+    return null;
+  }
+
+  private static boolean isBeforeAfterPrefixedPair(@NonNls String name1, @NonNls String name2) {
+    //noinspection ConstantConditions - no NPE
+    return name1.toLowerCase().startsWith(TESTDATA_FILE_BEFORE_PREFIX) && name2.startsWith(TESTDATA_FILE_AFTER_PREFIX)
+           && StringUtil.substringAfter(name1, TESTDATA_FILE_BEFORE_PREFIX)
+             .equals(StringUtil.substringAfter(name2, TESTDATA_FILE_AFTER_PREFIX));
+  }
+
+  private static boolean isAfterSuffixed(@NonNls String name, int commonPrefixLength) {
+    return name.substring(commonPrefixLength).toLowerCase().contains(TESTDATA_FILE_AFTER_SUFFIX);
+  }
+
+  static VirtualFile createFileByName(final Project project, final String path) {
+    return ApplicationManager.getApplication().runWriteAction(new Computable<VirtualFile>() {
+      public VirtualFile compute() {
+        try {
+          File file = new File(path);
+          VirtualFile parent = VfsUtil.createDirectories(file.getParent());
+          return parent.createChildData(this, file.getName());
+        }
+        catch (IOException e) {
+          Messages.showErrorDialog(project, e.getMessage(), DevKitBundle.message("testdata.create.dialog.title"));
+          return null;
+        }
+      }
+    });
+  }
+
+  static void openOrAskToCreateFile(@NotNull Project project, @NotNull String path) {
+    VirtualFile file = getFileByPath(path);
+    if (file != null) {
+      new OpenFileDescriptor(project, file).navigate(true);
+    }
+    else {
+      int rc = Messages.showYesNoDialog(project, DevKitBundle.message("testdata.file.doesn.not.exist", path),
+                                        DevKitBundle.message("testdata.create.dialog.title"), Messages.getQuestionIcon());
+      if (rc == Messages.YES) {
+        VirtualFile vFile = createFileByName(project, path);
+        new OpenFileDescriptor(project, vFile).navigate(true);
+      }
+    }
+  }
+
+  @Nullable
+  static Icon getIcon(@NotNull String path) {
+    VirtualFile file = getFileByPath(path);
+    if (file == null) {
+      return null;
+    }
+    FileType fileType = FileTypeManager.getInstance().getFileTypeByFile(file);
+    return fileType.getIcon();
+  }
+
+  @Nullable
+  static VirtualFile getFileByPath(String path) {
+    return LocalFileSystem.getInstance().refreshAndFindFileByPath(path);
+  }
+
+  @Nullable
+  static Pair<String, String> getModuleOrProjectRelativeParentPath(Project project, VirtualFile file) {
+    VirtualFile parent = file.getParent();
+    if (parent == null) {
+      // shouldn't happen
+      return null;
+    }
+
+    Module module = ModuleUtilCore.findModuleForFile(parent, project);
+    if (module != null) {
+      VirtualFile moduleFile = module.getModuleFile();
+      if (moduleFile != null) {
+        VirtualFile moduleFileDir = moduleFile.getParent();
+        if (moduleFileDir != null) {
+          String moduleRelativePath = VfsUtilCore.getRelativePath(parent, moduleFileDir);
+          if (moduleRelativePath != null) {
+            return new Pair<>(module.getName(), moduleRelativePath);
+          }
+        }
+      }
+    }
+
+    VirtualFile projectDir = project.getBaseDir();
+    if (projectDir != null) {
+      String projectRelativePath = VfsUtilCore.getRelativePath(parent, projectDir);
+      if (projectRelativePath != null) {
+        return new Pair<>(project.getName(), projectRelativePath);
+      }
+    }
+
+    return null;
+  }
+}
diff --git a/plugins/devkit/src/testAssistant/vfs/TestDataGroupFileSystem.java b/plugins/devkit/src/testAssistant/vfs/TestDataGroupFileSystem.java
new file mode 100644 (file)
index 0000000..f4235b0
--- /dev/null
@@ -0,0 +1,69 @@
+/*
+ * Copyright 2000-2017 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.idea.devkit.testAssistant.vfs;
+
+import com.intellij.openapi.util.text.StringUtil;
+import com.intellij.openapi.vfs.LocalFileSystem;
+import com.intellij.openapi.vfs.VirtualFile;
+import com.intellij.openapi.vfs.VirtualFileManager;
+import com.intellij.openapi.vfs.ex.dummy.DummyCachingFileSystem;
+import org.jetbrains.annotations.NotNull;
+
+public class TestDataGroupFileSystem extends DummyCachingFileSystem<VirtualFile> {
+  /**
+   * We must have a separator for two arbitrary file paths, considering that almost all symbols are possible in Unix paths.
+   * It is very unlikely that this UUID will be present in file path so it's a pretty reliable separator.
+   */
+  private static final String GROUP_FILES_SEPARATOR = "33d0ee30-8c8f-11e7-bb31-be2e44b06b34";
+  private static final String PROTOCOL = "testdata";
+
+  public TestDataGroupFileSystem() {
+    super(PROTOCOL);
+  }
+
+
+  public static TestDataGroupFileSystem getTestDataGroupFileSystem() {
+    return (TestDataGroupFileSystem)VirtualFileManager.getInstance().getFileSystem(PROTOCOL);
+  }
+
+  public static String getPath(VirtualFile beforeFile, VirtualFile afterFile) {
+    return beforeFile.getPath() + GROUP_FILES_SEPARATOR + afterFile.getPath();
+  }
+
+
+  @Override
+  protected VirtualFile findFileByPathInner(@NotNull String path) {
+    String[] parts = path.split(GROUP_FILES_SEPARATOR);
+    if (parts.length != 2) {
+      return null;
+    }
+
+    String beforePath = parts[0];
+    String afterPath = parts[1];
+    if (StringUtil.isEmpty(beforePath) || StringUtil.isEmpty(afterPath)) {
+      return null;
+    }
+
+    LocalFileSystem localFileSystem = LocalFileSystem.getInstance();
+    VirtualFile beforeFile = localFileSystem.refreshAndFindFileByPath(beforePath);
+    VirtualFile afterFile = localFileSystem.refreshAndFindFileByPath(afterPath);
+    if (beforeFile == null || afterFile == null) {
+      return null;
+    }
+
+    return new TestDataGroupVirtualFile(beforeFile, afterFile);
+  }
+}
similarity index 85%
rename from plugins/devkit/src/testAssistant/TestDataGroupVirtualFile.java
rename to plugins/devkit/src/testAssistant/vfs/TestDataGroupVirtualFile.java
index f40aa30bebb380edb2ef1c5b7c2c16d1db6dc92e..b1aa9d9f0defeef29f85d9d6a596de72a7193b6d 100644 (file)
@@ -1,5 +1,5 @@
 /*
- * Copyright 2000-2014 JetBrains s.r.o.
+ * Copyright 2000-2017 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.
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.jetbrains.idea.devkit.testAssistant;
+package org.jetbrains.idea.devkit.testAssistant.vfs;
 
 import com.intellij.ide.presentation.Presentation;
 import com.intellij.openapi.fileTypes.FileType;
-import com.intellij.openapi.util.text.StringUtil;
-import com.intellij.openapi.vfs.LocalFileSystem;
 import com.intellij.openapi.vfs.VirtualFile;
 import com.intellij.openapi.vfs.VirtualFileSystem;
-import com.intellij.openapi.vfs.VirtualFileWithId;
 import com.intellij.util.ArrayUtil;
 import org.jetbrains.annotations.NotNull;
 
@@ -45,17 +42,15 @@ public class TestDataGroupVirtualFile extends VirtualFile {
   @NotNull
   @Override
   public String getName() {
-    final String prefix = StringUtil.commonPrefix(myBeforeFile.getName(), myAfterFile.getName());
-    if (prefix.isEmpty()) {
-      return StringUtil.commonSuffix(myBeforeFile.getName(), myAfterFile.getName());
-    }
-    return prefix + "." + myBeforeFile.getExtension();
+    return myBeforeFile.getName() + " | " + myAfterFile.getName();
   }
 
+  @NotNull
   public VirtualFile getBeforeFile() {
     return myBeforeFile;
   }
 
+  @NotNull
   public VirtualFile getAfterFile() {
     return myAfterFile;
   }
@@ -63,13 +58,13 @@ public class TestDataGroupVirtualFile extends VirtualFile {
   @NotNull
   @Override
   public VirtualFileSystem getFileSystem() {
-    return LocalFileSystem.getInstance();
+    return TestDataGroupFileSystem.getTestDataGroupFileSystem();
   }
 
   @NotNull
   @Override
   public String getPath() {
-    return myBeforeFile.getPath();
+    return TestDataGroupFileSystem.getPath(myBeforeFile, myAfterFile);
   }
 
   @Override
diff --git a/plugins/devkit/testData/guessByExistingFiles/Test/testdata_file.txt b/plugins/devkit/testData/guessByExistingFiles/Test/testdata_file.txt
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/plugins/devkit/testData/guessByExistingFiles/TestMore/testdata_file.txt b/plugins/devkit/testData/guessByExistingFiles/TestMore/testdata_file.txt
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/plugins/devkit/testData/guessByExistingFiles/TestMoreRelevant.java b/plugins/devkit/testData/guessByExistingFiles/TestMoreRelevant.java
new file mode 100644 (file)
index 0000000..8cb85cf
--- /dev/null
@@ -0,0 +1,6 @@
+import junit.framework.TestCase;
+
+public class TestMoreRelevant extends TestCase {
+  public void test<caret>MoreRelevant() {
+  }
+}
\ No newline at end of file
diff --git a/plugins/devkit/testData/guessByExistingFiles/TestMoreRelevant/testdata_file.txt b/plugins/devkit/testData/guessByExistingFiles/TestMoreRelevant/testdata_file.txt
new file mode 100644 (file)
index 0000000..e69de29
index 60a62c3f0ab10ac4df7f6f39a8fb326f68d8d0e6..ed1889600fd1a91a03856469e84fb1123e69c402 100644 (file)
@@ -20,6 +20,7 @@ import com.intellij.openapi.application.PluginPathManager;
 import com.intellij.openapi.projectRoots.ex.JavaSdkUtil;
 import com.intellij.openapi.util.ThrowableComputable;
 import com.intellij.openapi.vfs.VirtualFile;
+import com.intellij.psi.PsiClass;
 import com.intellij.psi.PsiMethod;
 import com.intellij.psi.util.PsiTreeUtil;
 import com.intellij.testFramework.PsiTestUtil;
@@ -63,6 +64,16 @@ public class TestDataGuessByExistingFilesUtilTest extends TestDataPathTestCase {
     assertEquals("TestName", result);
   }
 
+  public void testMoreRelevantFiles() {
+    PsiMethod testMethod = getTestMethod("TestMoreRelevant.java",
+                                         "Test/testdata_file.txt", "TestMore/testdata_file.txt", "TestMoreRelevant/testdata_file.txt");
+    PsiClass testClass = (PsiClass)testMethod.getParent();
+
+    List<String> result = TestDataGuessByExistingFilesUtil.suggestTestDataFiles("testdata_file", null, testClass);
+    String resultPath = assertOneElement(result);
+    assertTrue(resultPath, resultPath.endsWith("TestMoreRelevant/testdata_file.txt"));
+  }
+
   public void testCollectTestDataByExistingFilesBeforeAndAfter() {
     PsiMethod testMethod = getTestMethodWithBeforeAndAfterTestData();
     List<String> result = TestDataGuessByExistingFilesUtil.collectTestDataByExistingFiles(testMethod);