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 extends PsiElement> elements) {
+ return checkReadOnlyStatus(elements, project, RefactoringBundle.message("refactoring.cannot.be.performed"), true, false);
+ }
+ public static boolean checkReadOnlyStatusRecursively(@NotNull Project project, @NotNull Collection extends PsiElement> elements, boolean notifyOnFail) {
+ return checkReadOnlyStatus(elements, project, RefactoringBundle.message("refactoring.cannot.be.performed"), true, notifyOnFail);
+ }
+
+ private static boolean checkReadOnlyStatus(@NotNull Collection extends PsiElement> 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 extends PsiElement> 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 extends PsiElement> elements) {
+ return checkReadOnlyStatus(elements, project, RefactoringBundle.message("refactoring.cannot.be.performed"), true, false)
+ }
+ public static boolean checkReadOnlyStatusRecursively(@NotNull Project project, @NotNull Collection extends PsiElement> elements, boolean notifyOnFail) {
+ return checkReadOnlyStatus(elements, project, RefactoringBundle.message("refactoring.cannot.be.performed"), true, notifyOnFail)
+ }
+
+ private static boolean checkReadOnlyStatus(@NotNull Collection extends PsiElement> 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 extends PsiElement> scopes) {
+ for (final PsiElement scope : scopes) {
+ if (PsiTreeUtil.isAncestor(scope, resolved, false)) return true
+ }
+ return false
+ }
+}
+''')
+ }
+}