diff --git a/platform/lang-api/src/com/intellij/refactoring/util/CommonRefactoringUtil.java b/platform/lang-api/src/com/intellij/refactoring/util/CommonRefactoringUtil.java index c8892f351473..f7deece87d89 100644 --- a/platform/lang-api/src/com/intellij/refactoring/util/CommonRefactoringUtil.java +++ b/platform/lang-api/src/com/intellij/refactoring/util/CommonRefactoringUtil.java @@ -59,7 +59,7 @@ public class CommonRefactoringUtil { } } - public static void showErrorHint(Project project, @Nullable Editor editor, String message, String title, String helpId) { + public static void showErrorHint(Project project, @Nullable Editor editor, String message, String title, @Nullable @NonNls String helpId) { if (ApplicationManager.getApplication().isUnitTestMode()) throw new RefactoringErrorHintException(message); if (editor == null) { diff --git a/plugins/groovy/resources/intentionDescriptions/RemoveUnnecessarySemicolonsIntention/after.groovy.template b/plugins/groovy/resources/intentionDescriptions/RemoveUnnecessarySemicolonsIntention/after.groovy.template new file mode 100644 index 000000000000..015b148cbee1 --- /dev/null +++ b/plugins/groovy/resources/intentionDescriptions/RemoveUnnecessarySemicolonsIntention/after.groovy.template @@ -0,0 +1,2 @@ +print 2 +print 3 \ No newline at end of file diff --git a/plugins/groovy/resources/intentionDescriptions/RemoveUnnecessarySemicolonsIntention/before.groovy.template b/plugins/groovy/resources/intentionDescriptions/RemoveUnnecessarySemicolonsIntention/before.groovy.template new file mode 100644 index 000000000000..9849021583a6 --- /dev/null +++ b/plugins/groovy/resources/intentionDescriptions/RemoveUnnecessarySemicolonsIntention/before.groovy.template @@ -0,0 +1,2 @@ +print 2; +print 3 \ No newline at end of file diff --git a/plugins/groovy/resources/intentionDescriptions/RemoveUnnecessarySemicolonsIntention/description.html b/plugins/groovy/resources/intentionDescriptions/RemoveUnnecessarySemicolonsIntention/description.html new file mode 100644 index 000000000000..b347e0197809 --- /dev/null +++ b/plugins/groovy/resources/intentionDescriptions/RemoveUnnecessarySemicolonsIntention/description.html @@ -0,0 +1,5 @@ + + +Removes unnecessary semicolons from selected text or a semicolon under caret if it is unnecessary. + + \ No newline at end of file diff --git a/plugins/groovy/src/META-INF/plugin.xml b/plugins/groovy/src/META-INF/plugin.xml index 85c4a56a3cc8..03f92e0930d7 100644 --- a/plugins/groovy/src/META-INF/plugin.xml +++ b/plugins/groovy/src/META-INF/plugin.xml @@ -914,6 +914,11 @@ intention.category.groovy/intention.category.groovy.style org.jetbrains.plugins.groovy.intentions.style.JavaStylePropertiesInvocationIntention + + org.jetbrains.plugins.groovy.intentions.GroovyIntentionsBundle + intention.category.groovy/intention.category.groovy.style + org.jetbrains.plugins.groovy.intentions.style.RemoveUnnecessarySemicolonsIntention + org.jetbrains.plugins.groovy.intentions.GroovyIntentionsBundle intention.category.groovy/intention.category.groovy.style diff --git a/plugins/groovy/src/org/jetbrains/plugins/groovy/intentions/GroovyIntentionsBundle.properties b/plugins/groovy/src/org/jetbrains/plugins/groovy/intentions/GroovyIntentionsBundle.properties index 23bda96924da..f5e53960a867 100644 --- a/plugins/groovy/src/org/jetbrains/plugins/groovy/intentions/GroovyIntentionsBundle.properties +++ b/plugins/groovy/src/org/jetbrains/plugins/groovy/intentions/GroovyIntentionsBundle.properties @@ -138,3 +138,6 @@ convert.to.geese.braces.intention.family.name=Format closing closure braces in o convert.from.geese.braces.intention.name=Format braces in separate lines convert.from.geese.braces.intention.family.name=Format braces in separate lines +remove.unnecessary.semicolons.name=Remove unnecessary semicolons +remove.unnecessary.semicolons.family.name=Remove unnecessary semicolons from selected text +no.unnecessary.semicolons.found=No unnecessary semicolons found diff --git a/plugins/groovy/src/org/jetbrains/plugins/groovy/intentions/style/RemoveUnnecessarySemicolonsIntention.java b/plugins/groovy/src/org/jetbrains/plugins/groovy/intentions/style/RemoveUnnecessarySemicolonsIntention.java new file mode 100644 index 000000000000..3afe51a7405b --- /dev/null +++ b/plugins/groovy/src/org/jetbrains/plugins/groovy/intentions/style/RemoveUnnecessarySemicolonsIntention.java @@ -0,0 +1,198 @@ +/* + * Copyright 2000-2011 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jetbrains.plugins.groovy.intentions.style; + +import com.intellij.codeInsight.intention.IntentionAction; +import com.intellij.openapi.editor.Document; +import com.intellij.openapi.editor.Editor; +import com.intellij.openapi.editor.SelectionModel; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.util.TextRange; +import com.intellij.psi.PsiElement; +import com.intellij.psi.PsiFile; +import com.intellij.psi.PsiRecursiveElementVisitor; +import com.intellij.psi.tree.IElementType; +import com.intellij.psi.util.PsiTreeUtil; +import com.intellij.refactoring.util.CommonRefactoringUtil; +import com.intellij.util.IncorrectOperationException; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.jetbrains.plugins.groovy.intentions.GroovyIntentionsBundle; +import org.jetbrains.plugins.groovy.lang.lexer.GroovyTokenTypes; +import org.jetbrains.plugins.groovy.lang.psi.GroovyFile; +import org.jetbrains.plugins.groovy.lang.psi.GroovyFileBase; +import org.jetbrains.plugins.groovy.lang.psi.GroovyPsiElementFactory; +import org.jetbrains.plugins.groovy.lang.psi.api.statements.GrStatement; +import org.jetbrains.plugins.groovy.lang.psi.util.PsiUtil; + +import java.util.ArrayList; + +/** + * @author Max Medvedev + */ +public class RemoveUnnecessarySemicolonsIntention implements IntentionAction { + @NotNull + @Override + public String getText() { + return GroovyIntentionsBundle.message("remove.unnecessary.semicolons.name"); + } + + @NotNull + @Override + public String getFamilyName() { + return GroovyIntentionsBundle.message("remove.unnecessary.semicolons.family.name"); + } + + @Override + public boolean isAvailable(@NotNull Project project, Editor editor, PsiFile file) { + if (!(file instanceof GroovyFileBase) || editor.getSelectionModel().hasBlockSelection()) { + return false; + } + + + if (editor.getSelectionModel().hasSelection()) return true; + + int offset = editor.getCaretModel().getOffset(); + if (offset >= editor.getDocument().getTextLength()) offset = editor.getDocument().getTextLength() - 1; + final PsiElement element = file.findElementAt(offset); + if (element == null) return false; + if (element.getNode().getElementType() == GroovyTokenTypes.mSEMI) return true; + + final PsiElement next = PsiTreeUtil.nextLeaf(element); + if (next != null && next.getNode().getElementType() == GroovyTokenTypes.mSEMI) return true; + + + final PsiElement prev = PsiTreeUtil.prevLeaf(element); + if (prev != null && prev.getNode().getElementType() == GroovyTokenTypes.mSEMI) return true; + + return false; + } + + @Override + public void invoke(@NotNull Project project, Editor editor, PsiFile file) throws IncorrectOperationException { + final SelectionModel selectionModel = editor.getSelectionModel(); + if (selectionModel.hasBlockSelection()) return; + + Document document = editor.getDocument(); + if (selectionModel.hasSelection()) { + final int start = selectionModel.getSelectionStart(); + final int end = selectionModel.getSelectionEnd(); + final TextRange range = new TextRange(start, end); + + final ArrayList colons = new ArrayList(); + file.accept(new PsiRecursiveElementVisitor() { + @Override + public void visitElement(PsiElement element) { + if (!range.intersects(element.getTextRange())) return; + + final IElementType elementType = element.getNode().getElementType(); + if (elementType == GroovyTokenTypes.mSEMI) { + colons.add(element); + } + else { + super.visitElement(element); + } + } + }); + + boolean removed = false; + for (PsiElement colon : colons) { + final boolean b = checkAndRemove(project, colon, document); + removed = removed || b; + } + + if (!removed) { + CommonRefactoringUtil.showErrorHint(project, editor, GroovyIntentionsBundle.message("no.unnecessary.semicolons.found"), + GroovyIntentionsBundle.message("remove.unnecessary.semicolons.name"), null); + } + } + else { + int offset = editor.getCaretModel().getOffset(); + if (offset >= document.getTextLength()) offset = document.getTextLength() - 1; + final PsiElement element = file.findElementAt(offset); + if (element == null) return; + if (checkAndRemove(project, element, document)) return; + + if (checkAndRemove(project, PsiTreeUtil.nextLeaf(element), document)) return; + if (checkAndRemove(project, PsiTreeUtil.prevLeaf(element), document)) return; + + CommonRefactoringUtil.showErrorHint(project, editor, GroovyIntentionsBundle.message("no.unnecessary.semicolons.found"), + GroovyIntentionsBundle.message("remove.unnecessary.semicolons.name"), null); + } + } + + private static boolean checkAndRemove(Project project, @Nullable PsiElement element, Document document) { + if (element != null && element.getNode().getElementType() == GroovyTokenTypes.mSEMI) { + if (isColonUnnecessary(element, document.getText(), project)) { + element.delete(); + return true; + } + } + return false; + } + + private static boolean isColonUnnecessary(PsiElement colon, String text, Project project) { + final GrStatement prev = getPreviousStatement(colon); + final GrStatement next = getNextStatement(colon); + + if (prev == null || next == null) return true; + + final int startOffset = prev.getTextRange().getStartOffset(); + final int endOffset = next.getTextRange().getEndOffset(); + + final int offset = colon.getTextRange().getStartOffset(); + final String statementWithoutColon = text.substring(startOffset, offset) + text.substring(offset + 1, endOffset); + final GroovyFile file = GroovyPsiElementFactory.getInstance(project).createGroovyFile(statementWithoutColon, false, null); + final GrStatement[] statements = file.getStatements(); + if (statements.length != 2) return false; + + return checkEquality(prev, statements[0]) && checkEquality(next, statements[1]); + } + + private static boolean checkEquality(PsiElement l, PsiElement r) { + if (!l.getText().equals(r.getText())) return false; + if (l.getNode().getElementType() != r.getNode().getElementType()) return false; + + final PsiElement[] lChildren = l.getChildren(); + final PsiElement[] rChildren = r.getChildren(); + + if (lChildren.length != rChildren.length) return false; + + for (int i = 0; i < rChildren.length; i++) { + if (!checkEquality(lChildren[i], rChildren[i])) return false; + } + return true; + } + + @Nullable + private static GrStatement getPreviousStatement(PsiElement colon) { + final PsiElement prev = PsiUtil.skipWhitespaces(colon.getPrevSibling(), false); + if (prev instanceof GrStatement) return (GrStatement)prev; + return null; + } + + @Nullable + private static GrStatement getNextStatement(PsiElement colon) { + final PsiElement next = PsiUtil.skipWhitespaces(colon.getNextSibling(), true); + if (next instanceof GrStatement) return (GrStatement)next; + return null; + } + + @Override + public boolean startInWriteAction() { + return true; + } +} diff --git a/plugins/groovy/src/org/jetbrains/plugins/groovy/lang/psi/GroovyPsiElementFactory.java b/plugins/groovy/src/org/jetbrains/plugins/groovy/lang/psi/GroovyPsiElementFactory.java index 75570c56ae7b..c52feb2632cf 100644 --- a/plugins/groovy/src/org/jetbrains/plugins/groovy/lang/psi/GroovyPsiElementFactory.java +++ b/plugins/groovy/src/org/jetbrains/plugins/groovy/lang/psi/GroovyPsiElementFactory.java @@ -188,7 +188,7 @@ public abstract class GroovyPsiElementFactory implements JVMElementFactory { public abstract GrAnnotation createAnnotationFromText(String annoText); - public abstract GroovyFile createGroovyFile(String text, boolean isPhysical, PsiElement context); + public abstract GroovyFile createGroovyFile(String text, boolean isPhysical, @Nullable PsiElement context); public abstract GrMethod createMethodFromText(String modifier, String name, String type, String[] paramTypes, PsiElement context); diff --git a/plugins/groovy/test/org/jetbrains/plugins/groovy/intentions/RemoveUnnecessarySemicolonTest.groovy b/plugins/groovy/test/org/jetbrains/plugins/groovy/intentions/RemoveUnnecessarySemicolonTest.groovy new file mode 100644 index 000000000000..8955a0e2b6f7 --- /dev/null +++ b/plugins/groovy/test/org/jetbrains/plugins/groovy/intentions/RemoveUnnecessarySemicolonTest.groovy @@ -0,0 +1,549 @@ +/* + * Copyright 2000-2011 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jetbrains.plugins.groovy.intentions + +import com.intellij.refactoring.util.CommonRefactoringUtil.RefactoringErrorHintException +import com.intellij.testFramework.fixtures.LightCodeInsightFixtureTestCase +import org.jetbrains.plugins.groovy.util.TestUtils + +/** + * @author Max Medvedev + */ +class RemoveUnnecessarySemicolonTest extends LightCodeInsightFixtureTestCase { + private static final String hint = GroovyIntentionsBundle.message('remove.unnecessary.semicolons.name'); + + @Override + protected String getBasePath() { + return "${TestUtils.testDataPath}intentions/removeUnnecessaryBraces/"; + } + + void testSimpleCase1() { + doTest('print 2;\nprint 3', 'print 2\nprint 3') + } + + void testSimpleCase2() { + doTest('print 2;\nprint 3', 'print 2\nprint 3') + } + + void testSimpleCase3() { + doTest('print 2;\nprint 3', 'print 2\nprint 3') + } + + void testDontRemoveSimple() { + doTest('print 2; print 3') + } + + void testClosure() { + doTest('print( a );\n{->print 3}') + } + + private doTest(String before, String after) { + myFixture.configureByText('a.groovy', before) + def intentions = myFixture.filterAvailableIntentions(hint) + assertEquals(1, intentions.size()) + + myFixture.launchAction(intentions[0]) + myFixture.checkResult(after) + } + + private doTest(String text) { + try { + doTest(text, text) + } + catch (RefactoringErrorHintException e) { + return; + } + fail('RefactoringErrorHintException should be thrown') + } + + private doFalseTest(String before) { + myFixture.configureByText('a.groovy', before) + def intentions = myFixture.filterAvailableIntentions(hint) + assertEmpty(intentions) + } + + void testBig() { + doTest('''\ +/* + * Copyright 2000-2011 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.intellij.refactoring.util; + +import com.intellij.codeInsight.hint.HintManager; +import com.intellij.openapi.application.ApplicationManager; +import com.intellij.openapi.editor.Editor; +import com.intellij.openapi.fileTypes.FileTypeManager; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.vfs.JarFileSystem; +import com.intellij.openapi.vfs.ReadonlyStatusHandler; +import com.intellij.openapi.vfs.VfsUtil; +import com.intellij.openapi.vfs.VirtualFile; +import com.intellij.psi.*; +import com.intellij.psi.util.PsiTreeUtil; +import com.intellij.refactoring.RefactoringBundle; +import com.intellij.util.containers.ContainerUtil; +import gnu.trove.THashSet; +import org.jetbrains.annotations.NonNls; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; + +/** + * @author ven + */ +public class CommonRefactoringUtil { + private CommonRefactoringUtil() {} + + public static void showErrorMessage(String title, String message, @Nullable @NonNls String helpId, @NotNull Project project) { + if (ApplicationManager.getApplication().isUnitTestMode()) throw new RuntimeException(message); + RefactoringMessageDialog dialog = new RefactoringMessageDialog(title, message, helpId, "OptionPane.errorIcon", false, project); + dialog.show(); + } + + /** + * Fatal refactoring problem during unit test run. Corresponds to message of modal dialog shown during user driven refactoring. + */ + public static class RefactoringErrorHintException extends RuntimeException { + public RefactoringErrorHintException(String message) { + super(message); + } + } + + public static void showErrorHint(Project project, @Nullable Editor editor, String message, String title, @Nullable @NonNls String helpId) { + if (ApplicationManager.getApplication().isUnitTestMode()) throw new RefactoringErrorHintException(message); + + if (editor == null) { + showErrorMessage(title, message, helpId, project); + } + else { + HintManager.getInstance().showErrorHint(editor, message); + } + } + + @NonNls + public static String htmlEmphasize(String text) { + return "" + text + ""; + } + + public static boolean checkReadOnlyStatus(@NotNull PsiElement element) { + final VirtualFile file = element.getContainingFile().getVirtualFile(); + return file != null && !ReadonlyStatusHandler.getInstance(element.getProject()).ensureFilesWritable(file).hasReadonlyFiles(); + } + + public static boolean checkReadOnlyStatus(@NotNull Project project, @NotNull PsiElement element) { + return checkReadOnlyStatus(element, project, RefactoringBundle.message("refactoring.cannot.be.performed")); + } + + public static boolean checkReadOnlyStatus(@NotNull Project project, @NotNull PsiElement... elements) { + return checkReadOnlyStatus(Arrays.asList(elements), project, RefactoringBundle.message("refactoring.cannot.be.performed"), false, true); + } + + public static boolean checkReadOnlyStatus(@NotNull PsiElement element, @NotNull Project project, String messagePrefix) { + return element.isWritable() || + checkReadOnlyStatus(Collections.singleton(element), project, messagePrefix, false, true); + } + + public static boolean checkReadOnlyStatusRecursively(@NotNull Project project, @NotNull Collection elements) { + return checkReadOnlyStatus(elements, project, RefactoringBundle.message("refactoring.cannot.be.performed"), true, false); + } + public static boolean checkReadOnlyStatusRecursively(@NotNull Project project, @NotNull Collection elements, boolean notifyOnFail) { + return checkReadOnlyStatus(elements, project, RefactoringBundle.message("refactoring.cannot.be.performed"), true, notifyOnFail); + } + + private static boolean checkReadOnlyStatus(@NotNull Collection elements, + @NotNull Project project, + final String messagePrefix, + boolean recursively, + final boolean notifyOnFail) { + final Collection readonly = new THashSet(); // not writable, but could be checked out + final Collection failed = new THashSet(); // those located in jars + boolean seenNonWritablePsiFilesWithoutVirtualFile = false; + + for (PsiElement element : elements) { + if (element instanceof PsiDirectory) { + final PsiDirectory dir = (PsiDirectory)element; + final VirtualFile vFile = dir.getVirtualFile(); + if (vFile.getFileSystem() instanceof JarFileSystem) { + failed.add(vFile); + } + else { + if (recursively) { + addVirtualFiles(vFile, readonly); + } + else { + readonly.add(vFile); + } + } + } + else if (element instanceof PsiDirectoryContainer) { + final PsiDirectory[] directories = ((PsiDirectoryContainer)element).getDirectories(); + for (PsiDirectory directory : directories) { + VirtualFile virtualFile = directory.getVirtualFile(); + if (recursively) { + if (virtualFile.getFileSystem() instanceof JarFileSystem) { + failed.add(virtualFile); + } + else { + addVirtualFiles(virtualFile, readonly); + } + } + else { + if (virtualFile.getFileSystem() instanceof JarFileSystem) { + failed.add(virtualFile); + } + else { + readonly.add(virtualFile); + } + } + } + } + else if (element instanceof PsiCompiledElement) { + final PsiFile file = element.getContainingFile(); + if (file != null) { + failed.add(file.getVirtualFile()); + } + } + else { + PsiFile file = element.getContainingFile(); + if (file == null) { + if (!element.isWritable()) { + seenNonWritablePsiFilesWithoutVirtualFile = true; + } + } + else { + final VirtualFile vFile = file.getVirtualFile(); + if (vFile != null) { + readonly.add(vFile); + } + else { + if (!element.isWritable()) { + seenNonWritablePsiFilesWithoutVirtualFile = true; + } + } + } + } + } + + final VirtualFile[] files = VfsUtil.toVirtualFileArray(readonly); + final ReadonlyStatusHandler.OperationStatus status = ReadonlyStatusHandler.getInstance(project).ensureFilesWritable(files); + ContainerUtil.addAll(failed, status.getReadonlyFiles()); + if (notifyOnFail && (!failed.isEmpty() || seenNonWritablePsiFilesWithoutVirtualFile && readonly.isEmpty())) { + StringBuilder message = new StringBuilder(messagePrefix); + message.append('\\n'); + int i = 0; + for (VirtualFile virtualFile : failed) { + final String presentableUrl = virtualFile.getPresentableUrl(); + final String subj = virtualFile.isDirectory() + ? RefactoringBundle.message("directory.description", presentableUrl) + : RefactoringBundle.message("file.description", presentableUrl); + if (virtualFile.getFileSystem() instanceof JarFileSystem) { + message.append(RefactoringBundle.message("0.is.located.in.a.jar.file", subj)); + } + else { + message.append(RefactoringBundle.message("0.is.read.only", subj)); + } + if (i++ > 20) { + message.append("...\\n"); + break; + } + } + showErrorMessage(RefactoringBundle.message("error.title"), message.toString(), null, project); + return false; + } + + return failed.isEmpty(); + } + + private static void addVirtualFiles(final VirtualFile vFile, final Collection list) { + if (!vFile.isWritable()) { + list.add(vFile); + } + if (!vFile.isSymLink()) { + final VirtualFile[] children = vFile.getChildren(); + if (children != null) { + final FileTypeManager fileTypeManager = FileTypeManager.getInstance(); + for (VirtualFile virtualFile : children) { + if (fileTypeManager.isFileIgnored(virtualFile)) continue; + addVirtualFiles(virtualFile, list); + } + } + } + } + + public static String capitalize(String text) { + return Character.toUpperCase(text.charAt(0)) + text.substring(1); + } + + public static boolean isAncestor(final PsiElement resolved, final Collection scopes) { + for (final PsiElement scope : scopes) { + if (PsiTreeUtil.isAncestor(scope, resolved, false)) return true; + } + return false; + } +} +''', '''\ +/* + * Copyright 2000-2011 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.intellij.refactoring.util + +import com.intellij.codeInsight.hint.HintManager +import com.intellij.openapi.application.ApplicationManager +import com.intellij.openapi.editor.Editor +import com.intellij.openapi.fileTypes.FileTypeManager +import com.intellij.openapi.project.Project +import com.intellij.openapi.vfs.JarFileSystem +import com.intellij.openapi.vfs.ReadonlyStatusHandler +import com.intellij.openapi.vfs.VfsUtil +import com.intellij.openapi.vfs.VirtualFile +import com.intellij.psi.* +import com.intellij.psi.util.PsiTreeUtil +import com.intellij.refactoring.RefactoringBundle +import com.intellij.util.containers.ContainerUtil +import gnu.trove.THashSet +import org.jetbrains.annotations.NonNls +import org.jetbrains.annotations.NotNull +import org.jetbrains.annotations.Nullable + +import java.util.Arrays +import java.util.Collection +import java.util.Collections + +/** + * @author ven + */ +public class CommonRefactoringUtil { + private CommonRefactoringUtil() {} + + public static void showErrorMessage(String title, String message, @Nullable @NonNls String helpId, @NotNull Project project) { + if (ApplicationManager.getApplication().isUnitTestMode()) throw new RuntimeException(message) + RefactoringMessageDialog dialog = new RefactoringMessageDialog(title, message, helpId, "OptionPane.errorIcon", false, project) + dialog.show() + } + + /** + * Fatal refactoring problem during unit test run. Corresponds to message of modal dialog shown during user driven refactoring. + */ + public static class RefactoringErrorHintException extends RuntimeException { + public RefactoringErrorHintException(String message) { + super(message) + } + } + + public static void showErrorHint(Project project, @Nullable Editor editor, String message, String title, @Nullable @NonNls String helpId) { + if (ApplicationManager.getApplication().isUnitTestMode()) throw new RefactoringErrorHintException(message) + + if (editor == null) { + showErrorMessage(title, message, helpId, project) + } + else { + HintManager.getInstance().showErrorHint(editor, message) + } + } + + @NonNls + public static String htmlEmphasize(String text) { + return "" + text + "" + } + + public static boolean checkReadOnlyStatus(@NotNull PsiElement element) { + final VirtualFile file = element.getContainingFile().getVirtualFile() + return file != null && !ReadonlyStatusHandler.getInstance(element.getProject()).ensureFilesWritable(file).hasReadonlyFiles() + } + + public static boolean checkReadOnlyStatus(@NotNull Project project, @NotNull PsiElement element) { + return checkReadOnlyStatus(element, project, RefactoringBundle.message("refactoring.cannot.be.performed")) + } + + public static boolean checkReadOnlyStatus(@NotNull Project project, @NotNull PsiElement... elements) { + return checkReadOnlyStatus(Arrays.asList(elements), project, RefactoringBundle.message("refactoring.cannot.be.performed"), false, true) + } + + public static boolean checkReadOnlyStatus(@NotNull PsiElement element, @NotNull Project project, String messagePrefix) { + return element.isWritable() || + checkReadOnlyStatus(Collections.singleton(element), project, messagePrefix, false, true) + } + + public static boolean checkReadOnlyStatusRecursively(@NotNull Project project, @NotNull Collection elements) { + return checkReadOnlyStatus(elements, project, RefactoringBundle.message("refactoring.cannot.be.performed"), true, false) + } + public static boolean checkReadOnlyStatusRecursively(@NotNull Project project, @NotNull Collection elements, boolean notifyOnFail) { + return checkReadOnlyStatus(elements, project, RefactoringBundle.message("refactoring.cannot.be.performed"), true, notifyOnFail) + } + + private static boolean checkReadOnlyStatus(@NotNull Collection elements, + @NotNull Project project, + final String messagePrefix, + boolean recursively, + final boolean notifyOnFail) { + final Collection readonly = new THashSet() // not writable, but could be checked out + final Collection failed = new THashSet() // those located in jars + boolean seenNonWritablePsiFilesWithoutVirtualFile = false + + for (PsiElement element : elements) { + if (element instanceof PsiDirectory) { + final PsiDirectory dir = (PsiDirectory)element + final VirtualFile vFile = dir.getVirtualFile() + if (vFile.getFileSystem() instanceof JarFileSystem) { + failed.add(vFile) + } + else { + if (recursively) { + addVirtualFiles(vFile, readonly) + } + else { + readonly.add(vFile) + } + } + } + else if (element instanceof PsiDirectoryContainer) { + final PsiDirectory[] directories = ((PsiDirectoryContainer)element).getDirectories() + for (PsiDirectory directory : directories) { + VirtualFile virtualFile = directory.getVirtualFile() + if (recursively) { + if (virtualFile.getFileSystem() instanceof JarFileSystem) { + failed.add(virtualFile) + } + else { + addVirtualFiles(virtualFile, readonly) + } + } + else { + if (virtualFile.getFileSystem() instanceof JarFileSystem) { + failed.add(virtualFile) + } + else { + readonly.add(virtualFile) + } + } + } + } + else if (element instanceof PsiCompiledElement) { + final PsiFile file = element.getContainingFile() + if (file != null) { + failed.add(file.getVirtualFile()) + } + } + else { + PsiFile file = element.getContainingFile() + if (file == null) { + if (!element.isWritable()) { + seenNonWritablePsiFilesWithoutVirtualFile = true + } + } + else { + final VirtualFile vFile = file.getVirtualFile() + if (vFile != null) { + readonly.add(vFile) + } + else { + if (!element.isWritable()) { + seenNonWritablePsiFilesWithoutVirtualFile = true + } + } + } + } + } + + final VirtualFile[] files = VfsUtil.toVirtualFileArray(readonly) + final ReadonlyStatusHandler.OperationStatus status = ReadonlyStatusHandler.getInstance(project).ensureFilesWritable(files) + ContainerUtil.addAll(failed, status.getReadonlyFiles()) + if (notifyOnFail && (!failed.isEmpty() || seenNonWritablePsiFilesWithoutVirtualFile && readonly.isEmpty())) { + StringBuilder message = new StringBuilder(messagePrefix) + message.append('\\n') + int i = 0 + for (VirtualFile virtualFile : failed) { + final String presentableUrl = virtualFile.getPresentableUrl() + final String subj = virtualFile.isDirectory() + ? RefactoringBundle.message("directory.description", presentableUrl) + : RefactoringBundle.message("file.description", presentableUrl) + if (virtualFile.getFileSystem() instanceof JarFileSystem) { + message.append(RefactoringBundle.message("0.is.located.in.a.jar.file", subj)) + } + else { + message.append(RefactoringBundle.message("0.is.read.only", subj)) + } + if (i++ > 20) { + message.append("...\\n") + break + } + } + showErrorMessage(RefactoringBundle.message("error.title"), message.toString(), null, project) + return false + } + + return failed.isEmpty() + } + + private static void addVirtualFiles(final VirtualFile vFile, final Collection list) { + if (!vFile.isWritable()) { + list.add(vFile) + } + if (!vFile.isSymLink()) { + final VirtualFile[] children = vFile.getChildren() + if (children != null) { + final FileTypeManager fileTypeManager = FileTypeManager.getInstance() + for (VirtualFile virtualFile : children) { + if (fileTypeManager.isFileIgnored(virtualFile)) continue + addVirtualFiles(virtualFile, list) + } + } + } + } + + public static String capitalize(String text) { + return Character.toUpperCase(text.charAt(0)) + text.substring(1) + } + + public static boolean isAncestor(final PsiElement resolved, final Collection scopes) { + for (final PsiElement scope : scopes) { + if (PsiTreeUtil.isAncestor(scope, resolved, false)) return true + } + return false + } +} +''') + } +}