diff --git a/java/execution/impl/src/com/intellij/execution/testDiscovery/ShowDiscoveredTestsAction.java b/java/execution/impl/src/com/intellij/execution/testDiscovery/ShowDiscoveredTestsAction.java index 7b3baab8dfee..5500e086f749 100644 --- a/java/execution/impl/src/com/intellij/execution/testDiscovery/ShowDiscoveredTestsAction.java +++ b/java/execution/impl/src/com/intellij/execution/testDiscovery/ShowDiscoveredTestsAction.java @@ -1,6 +1,7 @@ // Copyright 2000-2018 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.execution.testDiscovery; +import com.intellij.codeInsight.actions.FormatChangedTextUtil; import com.intellij.codeInsight.navigation.ListBackgroundUpdaterTask; import com.intellij.execution.Executor; import com.intellij.execution.JavaTestConfigurationBase; @@ -12,11 +13,14 @@ import com.intellij.find.FindUtil; import com.intellij.find.actions.CompositeActiveComponent; import com.intellij.icons.AllIcons; import com.intellij.ide.DataManager; +import com.intellij.ide.highlighter.JavaFileType; import com.intellij.ide.util.DefaultPsiElementCellRenderer; import com.intellij.openapi.actionSystem.*; import com.intellij.openapi.application.ApplicationManager; import com.intellij.openapi.application.ReadAction; +import com.intellij.openapi.editor.Document; import com.intellij.openapi.editor.Editor; +import com.intellij.openapi.fileEditor.FileDocumentManager; import com.intellij.openapi.keymap.KeymapUtil; import com.intellij.openapi.module.Module; import com.intellij.openapi.module.ModuleUtilCore; @@ -24,10 +28,14 @@ import com.intellij.openapi.project.Project; import com.intellij.openapi.ui.popup.IconButton; import com.intellij.openapi.ui.popup.JBPopup; import com.intellij.openapi.ui.popup.PopupChooserBuilder; +import com.intellij.openapi.util.Couple; import com.intellij.openapi.util.Ref; +import com.intellij.openapi.vcs.VcsDataKeys; +import com.intellij.openapi.vcs.changes.Change; import com.intellij.psi.*; import com.intellij.psi.search.GlobalSearchScope; import com.intellij.psi.util.PsiTreeUtil; +import com.intellij.psi.util.PsiUtilCore; import com.intellij.ui.CollectionListModel; import com.intellij.ui.InplaceButton; import com.intellij.ui.components.JBList; @@ -37,12 +45,13 @@ import com.intellij.util.ArrayUtil; import com.intellij.util.PsiNavigateUtil; import com.intellij.util.ui.EdtInvocationManager; import com.intellij.util.ui.JBDimension; +import one.util.streamex.StreamEx; +import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import javax.swing.*; import java.awt.event.ActionListener; -import java.util.List; -import java.util.Objects; +import java.util.*; import java.util.stream.Collectors; import static com.intellij.openapi.actionSystem.CommonDataKeys.EDITOR; @@ -57,43 +66,89 @@ public class ShowDiscoveredTestsAction extends AnAction { PsiElement at = file == null || editor == null ? null : file.findElementAt(editor.getCaretModel().getOffset()); PsiMethod method = PsiTreeUtil.getParentOfType(at, PsiMethod.class); - if (method == null || project == null) { - e.getPresentation().setEnabledAndVisible(false); - } + e.getPresentation().setEnabledAndVisible(method != null && project != null); } @Override public void actionPerformed(AnActionEvent e) { - Editor editor = e.getData(EDITOR); - PsiFile file = e.getData(PSI_FILE); + Editor editor = e.getRequiredData(EDITOR); + PsiFile file = e.getRequiredData(PSI_FILE); Project project = e.getProject(); - assert project != null; - PsiElement at = file == null || editor == null ? null : file.findElementAt(editor.getCaretModel().getOffset()); + PsiElement at = file.findElementAt(editor.getCaretModel().getOffset()); PsiMethod method = PsiTreeUtil.getParentOfType(at, PsiMethod.class); assert method != null; + PsiClass c = method.getContainingClass(); String fqn = c != null ? c.getQualifiedName() : null; if (fqn == null) return; String methodName = method.getName(); String methodPresentationName = c.getName() + "." + methodName; + DataContext dataContext = DataManager.getInstance().getDataContext(editor.getContentComponent()); + showDiscoveredTests(project, dataContext, methodPresentationName, Collections.singletonList(method)); + } + + public static class FromChangeList extends AnAction { + @Override + public void update(AnActionEvent e) { + Project project = e.getProject(); + Change[] changes = e.getData(VcsDataKeys.CHANGES); + + e.getPresentation().setEnabledAndVisible(project != null && changes != null); + } + + @Override + public void actionPerformed(AnActionEvent e) { + Change[] changes = e.getRequiredData(VcsDataKeys.CHANGES); + Project project = e.getProject(); + assert project != null; + + List methods = FormatChangedTextUtil.getInstance().getChangedElements(project, Arrays.asList(changes), file -> { + if (file.getFileType() != JavaFileType.INSTANCE) return null; + PsiFile psiFile = PsiUtilCore.getPsiFile(project, file); + + Document document = FileDocumentManager.getInstance().getDocument(file); + if (document == null) return null; + PsiDocumentManager.getInstance(project).commitDocument(document); + + List fileMethods = new ArrayList<>(); + psiFile.accept(new JavaRecursiveElementVisitor() { + @Override + public void visitMethod(PsiMethod method) { + super.visitMethod(method); + if (psiFile.equals(method.getContainingFile())) { + fileMethods.add(method); + } + } + }); + return fileMethods; + }); + + showDiscoveredTests(project, e.getDataContext(), "Selected Changes", methods); + } + } + + private static void showDiscoveredTests(@NotNull Project project, + @NotNull DataContext dataContext, + @NotNull String title, + @NotNull List methods) { CollectionListModel model = new CollectionListModel<>(); final JBList list = new JBList<>(model); //list.setFixedCellHeight(); HintUpdateSupply.installSimpleHintUpdateSupply(list); list.getSelectionModel().setSelectionMode(ListSelectionModel.MULTIPLE_INTERVAL_SELECTION); - String initTitle = "Tests for " + methodPresentationName; + String initTitle = "Tests for " + title; DefaultPsiElementCellRenderer renderer = new DefaultPsiElementCellRenderer(); Ref ref = new Ref<>(); + ConfigurationContext context = ConfigurationContext.getFromContext(dataContext); + InplaceButton runButton = new InplaceButton(new IconButton("Run All", AllIcons.Actions.Execute), __ -> { Executor executor = DefaultRunExecutor.getRunExecutorInstance(); - DataContext dataContext = DataManager.getInstance().getDataContext(editor.getContentComponent()); - ConfigurationContext context = ConfigurationContext.getFromContext(dataContext); List containingModules = model.getItems().stream() .map(element -> ModuleUtilCore.findModuleForPsiElement(element)) @@ -101,17 +156,17 @@ public class ShowDiscoveredTestsAction extends AnAction { .collect(Collectors.toList()); Module targetModule = TestDiscoveryConfigurationProducer.detectTargetModule(containingModules, project); //first producer with results will be picked - getProducers(project).stream() - .map(producer -> producer.createDelegate(method, targetModule).findOrCreateConfigurationFromContext(context)) - .filter(Objects::nonNull) - .findFirst() - .ifPresent(configuration -> { - ExecutionUtil.runConfiguration(configuration.getConfigurationSettings(), executor); - JBPopup popup = ref.get(); - if (popup != null) { - popup.cancel(); - } - }); + StreamEx.of(getProducers(project)).cross(methods) + .mapKeyValue((producer, method) -> producer.createDelegate(method, targetModule).findOrCreateConfigurationFromContext(context)) + .filter(Objects::nonNull) + .findFirst() + .ifPresent(configuration -> { + ExecutionUtil.runConfiguration(configuration.getConfigurationSettings(), executor); + JBPopup popup = ref.get(); + if (popup != null) { + popup.cancel(); + } + }); }); ActionListener pinActionListener = __ -> { @@ -146,10 +201,10 @@ public class ShowDiscoveredTestsAction extends AnAction { JBPopup popup = builder.createPopup(); ref.set(popup); - list.setEmptyText("No tests captured for " + methodPresentationName); + list.setEmptyText("No tests captured for " + title); list.setPaintBusy(true); - popup.showInBestPositionFor(editor); + popup.showInBestPositionFor(dataContext); JavaPsiFacade javaFacade = JavaPsiFacade.getInstance(project); GlobalSearchScope scope = GlobalSearchScope.projectScope(project); @@ -158,25 +213,32 @@ public class ShowDiscoveredTestsAction extends AnAction { ListBackgroundUpdaterTask loadTestsTask = new ListBackgroundUpdaterTask(project, "Load tests", renderer.getComparator()) { @Override public String getCaption(int size) { - return "Found " + size + " Tests for " + methodPresentationName; + return "Found " + size + " Tests for " + title; } }; loadTestsTask.init((AbstractPopup)popup, list, new Ref<>()); ApplicationManager.getApplication().executeOnPooledThread(() -> { - for (TestDiscoveryConfigurationProducer producer : getProducers(project)) { - String frameworkPrefix = - ((JavaTestConfigurationBase)producer.getConfigurationFactory().createTemplateConfiguration(project)).getFrameworkPrefix(); - TestDiscoveryProducer.consumeDiscoveredTests(project, fqn, methodName, frameworkPrefix, (testClass, testMethod) -> { - PsiMethod psiMethod = ReadAction.compute(() -> { - PsiClass cc = testClass == null ? null : javaFacade.findClass(testClass, scope); - return cc == null ? null : ArrayUtil.getFirstElement(cc.findMethodsByName(testMethod, false)); + for (PsiMethod method : methods) { + Couple methodFqnName = ReadAction.compute(() -> getMethodQualifiedName(method)); + if (methodFqnName == null) continue; + String fqn = methodFqnName.first; + String methodName = methodFqnName.second; + + for (TestDiscoveryConfigurationProducer producer : getProducers(project)) { + String frameworkPrefix = + ((JavaTestConfigurationBase)producer.getConfigurationFactory().createTemplateConfiguration(project)).getFrameworkPrefix(); + TestDiscoveryProducer.consumeDiscoveredTests(project, fqn, methodName, frameworkPrefix, (testClass, testMethod) -> { + PsiMethod psiMethod = ReadAction.compute(() -> { + PsiClass cc = testClass == null ? null : javaFacade.findClass(testClass, scope); + return cc == null ? null : ArrayUtil.getFirstElement(cc.findMethodsByName(testMethod, false)); + }); + if (psiMethod != null) { + loadTestsTask.updateComponent(psiMethod); + } }); - if (psiMethod != null) { - loadTestsTask.updateComponent(psiMethod); - } - }); + } } EdtInvocationManager.getInstance().invokeLater(() -> { @@ -186,6 +248,15 @@ public class ShowDiscoveredTestsAction extends AnAction { }); } + @Nullable + private static Couple getMethodQualifiedName(@NotNull PsiMethod method) { + PsiClass c = method.getContainingClass(); + String fqn = c != null ? c.getQualifiedName() : null; + if (fqn == null) return null; + String methodName = method.getName(); + return Couple.of(fqn, methodName); + } + @Nullable protected static KeyStroke findUsagesKeyStroke() { AnAction action = ActionManager.getInstance().getAction(IdeActions.ACTION_FIND_USAGES); diff --git a/platform/lang-impl/src/com/intellij/codeInsight/actions/FormatChangedTextUtil.java b/platform/lang-impl/src/com/intellij/codeInsight/actions/FormatChangedTextUtil.java index 996f4e9676dd..0c1bac7d9936 100644 --- a/platform/lang-impl/src/com/intellij/codeInsight/actions/FormatChangedTextUtil.java +++ b/platform/lang-impl/src/com/intellij/codeInsight/actions/FormatChangedTextUtil.java @@ -30,15 +30,18 @@ import com.intellij.openapi.vcs.changes.Change; import com.intellij.openapi.vcs.changes.ChangeListManager; import com.intellij.openapi.vfs.VirtualFile; import com.intellij.psi.PsiDirectory; +import com.intellij.psi.PsiElement; import com.intellij.psi.PsiFile; import com.intellij.psi.PsiManager; import com.intellij.psi.codeStyle.ChangedRangesInfo; import com.intellij.util.Function; import com.intellij.util.containers.ContainerUtil; +import com.intellij.util.containers.Convertor; import com.intellij.util.diff.FilesTooBigForDiffException; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; +import java.util.ArrayList; import java.util.Collection; import java.util.List; @@ -157,6 +160,23 @@ public class FormatChangedTextUtil { return null; } + @NotNull + public List getChangedElements(@NotNull Project project, + @NotNull List changes, + @NotNull Convertor> elementsConvertor) { + List result = new ArrayList<>(); + for (Change change : changes) { + VirtualFile file = change.getVirtualFile(); + if (file != null) { + List elements = elementsConvertor.convert(file); + if (elements != null) { + result.addAll(elements); + } + } + } + return result; + } + /** * Allows to temporally suppress document modification tracking. * diff --git a/platform/vcs-impl/src/com/intellij/codeInsight/actions/VcsAwareFormatChangedTextUtil.java b/platform/vcs-impl/src/com/intellij/codeInsight/actions/VcsAwareFormatChangedTextUtil.java index 4d1b58f2ec03..0c15b12938f4 100644 --- a/platform/vcs-impl/src/com/intellij/codeInsight/actions/VcsAwareFormatChangedTextUtil.java +++ b/platform/vcs-impl/src/com/intellij/codeInsight/actions/VcsAwareFormatChangedTextUtil.java @@ -17,42 +17,44 @@ package com.intellij.codeInsight.actions; import com.intellij.openapi.application.ApplicationManager; import com.intellij.openapi.editor.Document; +import com.intellij.openapi.fileEditor.FileDocumentManager; import com.intellij.openapi.project.Project; import com.intellij.openapi.util.TextRange; import com.intellij.openapi.util.text.StringUtilRt; import com.intellij.openapi.vcs.VcsException; -import com.intellij.openapi.vcs.changes.Change; -import com.intellij.openapi.vcs.changes.ChangeListManager; -import com.intellij.openapi.vcs.changes.ChangeListManagerImpl; -import com.intellij.openapi.vcs.changes.ContentRevision; +import com.intellij.openapi.vcs.changes.*; import com.intellij.openapi.vcs.ex.LineStatusTracker; +import com.intellij.openapi.vcs.ex.PartialLocalLineStatusTracker; import com.intellij.openapi.vcs.ex.Range; import com.intellij.openapi.vcs.ex.RangesBuilder; import com.intellij.openapi.vcs.impl.LineStatusTrackerManager; import com.intellij.openapi.vfs.VirtualFile; import com.intellij.psi.PsiDocumentManager; +import com.intellij.psi.PsiElement; import com.intellij.psi.PsiFile; import com.intellij.psi.codeStyle.ChangedRangesInfo; import com.intellij.util.containers.ContainerUtil; -import com.intellij.util.diff.FilesTooBigForDiffException; +import com.intellij.util.containers.Convertor; import com.intellij.vcsUtil.VcsUtil; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; +import java.util.ArrayList; +import java.util.BitSet; import java.util.List; class VcsAwareFormatChangedTextUtil extends FormatChangedTextUtil { @Override @NotNull - public List getChangedTextRanges(@NotNull Project project, @NotNull PsiFile file) throws FilesTooBigForDiffException { + public List getChangedTextRanges(@NotNull Project project, @NotNull PsiFile file) { ChangedRangesInfo helper = getChangedRangesInfo(file); return helper != null ? helper.allChangedRanges : ContainerUtil.newArrayList(); } @Override @Nullable - public ChangedRangesInfo getChangedRangesInfo(@NotNull PsiFile file) throws FilesTooBigForDiffException { + public ChangedRangesInfo getChangedRangesInfo(@NotNull PsiFile file) { Project project = file.getProject(); Document document = PsiDocumentManager.getInstance(project).getDocument(file); if (document == null) return null; @@ -82,6 +84,98 @@ class VcsAwareFormatChangedTextUtil extends FormatChangedTextUtil { return contentFromVcs != null ? calculateChangedRangesInfo(document, contentFromVcs) : null; } + @NotNull + @Override + public List getChangedElements(@NotNull Project project, + @NotNull List changes, + @NotNull Convertor> elementsConvertor) { + List result = new ArrayList<>(); + for (Change change : changes) { + if (change.getType() == Change.Type.DELETED) continue; + if (!(change.getAfterRevision() instanceof CurrentContentRevision)) continue; + + VirtualFile file = ((CurrentContentRevision)change.getAfterRevision()).getVirtualFile(); + if (file == null) continue; + + Document document = FileDocumentManager.getInstance().getDocument(file); + if (document == null) continue; + + List elements = elementsConvertor.convert(file); + if (ContainerUtil.isEmpty(elements)) continue; + + BitSet changedLines = getChangedLines(project, document, change); + if (changedLines != null) { + for (T element : elements) { + if (isElementChanged(element, document, changedLines)) { + result.add(element); + } + } + } + else { + result.addAll(elements); + } + } + return result; + } + + @Nullable + private static BitSet getChangedLines(@NotNull Project project, @NotNull Document document, @NotNull Change change) { + if (change.getType() == Change.Type.NEW) return null; + + List ranges = getChangedRanges(project, document, change); + if (ranges == null) return null; + + BitSet changedLines = new BitSet(); + for (Range range : ranges) { + if (range.getType() == Range.DELETED) { + changedLines.set(range.getLine1() - 1, range.getLine1() + 1); + } + else { + changedLines.set(range.getLine1(), range.getLine2()); + } + } + return changedLines; + } + + @Nullable + private static List getChangedRanges(@NotNull Project project, @NotNull Document document, @NotNull Change change) { + LineStatusTracker tracker = LineStatusTrackerManager.getInstance(project).getLineStatusTracker(document); + if (tracker != null) { + if (change instanceof ChangeListChange && tracker instanceof PartialLocalLineStatusTracker) { + String changeListId = ((ChangeListChange)change).getChangeListId(); + List ranges = ((PartialLocalLineStatusTracker)tracker).getRanges(); + if (ranges != null) { + return ContainerUtil.filter(ranges, range -> range.getChangelistId().equals(changeListId)); + } + else { + return null; + } + } + else { + return tracker.getRanges(); + } + } + else { + String contentFromVcs = getRevisionedContentFrom(change); + if (contentFromVcs != null) { + return getRanges(document, contentFromVcs); + } + else { + return null; + } + } + } + + private static boolean isElementChanged(@NotNull PsiElement element, @NotNull Document document, @NotNull BitSet changedLines) { + TextRange textRange = element.getTextRange(); + int startLine = document.getLineNumber(textRange.getStartOffset()); + int endLine = textRange.isEmpty() + ? startLine + 1 + : document.getLineNumber(textRange.getEndOffset() - 1) + 1; + int nextSetBit = changedLines.nextSetBit(startLine); + return nextSetBit != -1 && nextSetBit < endLine; + } + @Nullable private static String getRevisionedContentFrom(@NotNull Change change) { ContentRevision revision = change.getBeforeRevision(); @@ -111,39 +205,33 @@ class VcsAwareFormatChangedTextUtil extends FormatChangedTextUtil { } @NotNull - private static ChangedRangesInfo calculateChangedRangesInfo(@NotNull Document document, - @NotNull CharSequence contentFromVcs) throws FilesTooBigForDiffException { + private static ChangedRangesInfo calculateChangedRangesInfo(@NotNull Document document, @NotNull CharSequence contentFromVcs) { return getChangedTextRanges(document, getRanges(document, contentFromVcs)); } @NotNull private static List getRanges(@NotNull Document document, - @NotNull CharSequence contentFromVcs) throws FilesTooBigForDiffException { + @NotNull CharSequence contentFromVcs) { return RangesBuilder.createRanges(document.getImmutableCharSequence(), StringUtilRt.convertLineSeparators(contentFromVcs, "\n")); } @Override public int calculateChangedLinesNumber(@NotNull Document document, @NotNull CharSequence contentFromVcs) { - try { - List changedRanges = getRanges(document, contentFromVcs); - int linesChanges = 0; - for (Range range : changedRanges) { - int inserted = range.getLine2() - range.getLine1(); - int deleted = range.getVcsLine2() - range.getVcsLine1(); - linesChanges += Math.max(inserted, deleted); - } - return linesChanges; - } catch (FilesTooBigForDiffException e) { - LOG.info("File too big, can not calculate changed lines number"); - return -1; + List changedRanges = getRanges(document, contentFromVcs); + int linesChanges = 0; + for (Range range : changedRanges) { + int inserted = range.getLine2() - range.getLine1(); + int deleted = range.getVcsLine2() - range.getVcsLine1(); + linesChanges += Math.max(inserted, deleted); } + return linesChanges; } @NotNull private static ChangedRangesInfo getChangedTextRanges(@NotNull Document document, @NotNull List changedRanges) { final List ranges = ContainerUtil.newArrayList(); final List insertedRanges = ContainerUtil.newArrayList(); - + for (Range range : changedRanges) { if (range.getType() != Range.DELETED) { int changeStartLine = range.getLine1(); @@ -159,7 +247,7 @@ class VcsAwareFormatChangedTextUtil extends FormatChangedTextUtil { } } } - + return new ChangedRangesInfo(ranges, insertedRanges); } diff --git a/resources/src/META-INF/IdeaPlugin.xml b/resources/src/META-INF/IdeaPlugin.xml index b72064f2ad43..28f5cc648921 100644 --- a/resources/src/META-INF/IdeaPlugin.xml +++ b/resources/src/META-INF/IdeaPlugin.xml @@ -402,6 +402,9 @@ + + +