diff --git a/conditional_operator_intention/conditional_operator_intention.iml b/conditional_operator_intention/conditional_operator_intention.iml index 3b8cfcf82..636ca6e47 100644 --- a/conditional_operator_intention/conditional_operator_intention.iml +++ b/conditional_operator_intention/conditional_operator_intention.iml @@ -1,11 +1,12 @@ - + + diff --git a/conditional_operator_intention/source/META-INF/plugin.xml b/conditional_operator_intention/source/META-INF/plugin.xml deleted file mode 100644 index b5cb404c2..000000000 --- a/conditional_operator_intention/source/META-INF/plugin.xml +++ /dev/null @@ -1,35 +0,0 @@ - - com.your.company.unique.plugin.id - Plugin display name here - 1.0 - YourCompany - - - most HTML tags may be used - ]]> - - - most HTML tags may be used - ]]> - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/conditional_operator_intention/source/com/intellij/codeInsight/intention/ConditionalOperatorConvertor.java b/conditional_operator_intention/source/com/intellij/codeInsight/intention/ConditionalOperatorConvertor.java index f61e97c3e..6c8865f87 100644 --- a/conditional_operator_intention/source/com/intellij/codeInsight/intention/ConditionalOperatorConvertor.java +++ b/conditional_operator_intention/source/com/intellij/codeInsight/intention/ConditionalOperatorConvertor.java @@ -1,3 +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.codeInsight.intention; import com.intellij.openapi.editor.Editor; @@ -9,59 +13,115 @@ import com.intellij.util.IncorrectOperationException; import org.jetbrains.annotations.*; /** + * Implements an intention action to replace a ternary statement with if-then-else + * * @author dsl */ @NonNls public class ConditionalOperatorConvertor extends PsiElementBaseIntentionAction implements IntentionAction { + /** + * If this action is applicable, returns the text to be shown in the list of + * intention actions available. + */ @NotNull public String getText() { return "Convert ternary operator to if statement"; } + + /** + * Returns text for name of this family of intentions. It is used to externalize + * "auto-show" state of intentions. Only one intention action is being provided, + * so the family name is the same text as the intention action list entry. + * + * @return the intention family name. + * @see ConditionalOperatorConvertor#getText() + */ @NotNull public String getFamilyName() { return getText(); } - public boolean isAvailable(@NotNull Project project, Editor editor, @Nullable PsiElement element) { - if (element == null) return false; - if (!element.isWritable()) return false; + /** + * Checks whether this intention is available at the caret offset in file - the caret + * must sit just before a "?" character in a ternary statement. If this condition is met, + * this intention's entry is shown in the available intentions list. + * + * Note: this method must do its checks quickly and return. + * + * @param project a reference to the Project object being edited. + * @param editor a reference to the object editing the project source + * @param element a reference to the PSI element currently under the caret + * @return + *
    + *
  • true if the caret is in a literal string element, so this functionality + * should be added to the intention menu.
  • + *
  • false for all other types of caret positions
  • + *
+ */ + public boolean isAvailable(@NotNull Project project, Editor editor, @Nullable PsiElement element) { + + // Quick sanity check + if (element == null) return false; + + // Is this a token of type representing a "?" character? if (element instanceof PsiJavaToken) { final PsiJavaToken token = (PsiJavaToken) element; if (token.getTokenType() != JavaTokenType.QUEST) return false; + // Is this token part of a fully formed conditional, i.e. a ternary? if (token.getParent() instanceof PsiConditionalExpression) { final PsiConditionalExpression conditionalExpression = (PsiConditionalExpression) token.getParent(); if (conditionalExpression.getThenExpression() == null || conditionalExpression.getElseExpression() == null) { return false; } - return true; + return true; // Satisfies all criteria; call back invoke method } return false; } return false; } + /** + * Modifies the Psi to change a ternary expression to an if-then-else statement. + * If the ternary is part of a declaration, the declaration is separated and + * moved above the if-then-else statement. Called when user selects this intention action + * from the available intentions list. + * + * @param project a reference to the Project object being edited. + * @param editor a reference to the object editing the project source + * @param element a reference to the PSI element currently under the caret + * @throws IncorrectOperationException Thrown by underlying (Psi model) write action context + * when manipulation of the psi tree fails. + * @see ConditionalOperatorConvertor#startInWriteAction() + */ public void invoke(@NotNull Project project, Editor editor, PsiElement element) throws IncorrectOperationException { - final int offset = editor.getCaretModel().getOffset(); - PsiConditionalExpression conditionalExpression = PsiTreeUtil.getParentOfType(element, - PsiConditionalExpression.class, false); + + // Get the factory for making new PsiElements, and the code style manager to format new statements + final PsiElementFactory factory = JavaPsiFacade.getInstance(project).getElementFactory(); + final CodeStyleManager codeStylist = CodeStyleManager.getInstance(project); + + // Get the parent of the "?" element in the ternary statement to find the conditional expression that contains it + PsiConditionalExpression conditionalExpression = PsiTreeUtil.getParentOfType(element, PsiConditionalExpression.class, false); + // Verify the conditional expression exists and has two outcomes in the ternary statement. if (conditionalExpression == null) return; if (conditionalExpression.getThenExpression() == null || conditionalExpression.getElseExpression() == null) return; - final PsiElementFactory factory = JavaPsiFacade.getInstance(project).getElementFactory(); - + // Keep searching up the Psi Tree in case the ternary is part of a FOR statement. PsiElement originalStatement = PsiTreeUtil.getParentOfType(conditionalExpression, PsiStatement.class, false); while (originalStatement instanceof PsiForStatement) { originalStatement = PsiTreeUtil.getParentOfType(originalStatement, PsiStatement.class, true); } if (originalStatement == null) return; - // Maintain declrations + // If the original statement is a declaration based on a ternary operator, + // split the declaration and assignment if (originalStatement instanceof PsiDeclarationStatement) { final PsiDeclarationStatement declaration = (PsiDeclarationStatement) originalStatement; + + // Find the local variable within the declaration statement final PsiElement[] declaredElements = declaration.getDeclaredElements(); PsiLocalVariable variable = null; for (PsiElement declaredElement : declaredElements) { @@ -72,38 +132,73 @@ public class ConditionalOperatorConvertor extends PsiElementBaseIntentionAction } } if (variable == null) return; + + // Ensure that the variable declaration is not combined with other declarations, and add a mark variable.normalizeDeclaration(); final Object marker = new Object(); PsiTreeUtil.mark(conditionalExpression, marker); + + // Create a new expression to declare the local variable PsiExpressionStatement statement = (PsiExpressionStatement) factory.createStatementFromText(variable.getName() + " = 0;", null); - statement = (PsiExpressionStatement) CodeStyleManager.getInstance(project).reformat(statement); + statement = (PsiExpressionStatement) codeStylist.reformat(statement); + + // Replace initializer with the ternary expression, making an assignment statement using the ternary ((PsiAssignmentExpression) statement.getExpression()).getRExpression().replace(variable.getInitializer()); + + // Remove the initializer portion of the local variable statement, + // making it a declaration statement with no initializer variable.getInitializer().delete(); + + // Get the grandparent of the local var declaration, and add the new declaration just beneath it final PsiElement variableParent = variable.getParent(); originalStatement = variableParent.getParent().addAfter(statement, variableParent); conditionalExpression = (PsiConditionalExpression) PsiTreeUtil.releaseMark(originalStatement, marker); } - // create then and else branches - final PsiElement[] originalElements = new PsiElement[]{originalStatement, conditionalExpression}; - final PsiExpression condition = (PsiExpression) conditionalExpression.getCondition().copy(); - final PsiElement[] thenElements = PsiTreeUtil.copyElements(originalElements); - final PsiElement[] elseElements = PsiTreeUtil.copyElements(originalElements); - thenElements[1].replace(conditionalExpression.getThenExpression()); - elseElements[1].replace(conditionalExpression.getElseExpression()); + // Create an IF statement from a string with placeholder elements. + // This will replace the ternary statement + PsiIfStatement newIfStmt = (PsiIfStatement) factory.createStatementFromText("if (true) {a=b;} else {c=d;}",null); + newIfStmt = (PsiIfStatement) codeStylist.reformat(newIfStmt); - PsiIfStatement statement = (PsiIfStatement) factory.createStatementFromText("if (true) { a = b } else { c = d }", - null); - statement = (PsiIfStatement) CodeStyleManager.getInstance(project).reformat(statement); - statement.getCondition().replace(condition); - statement = (PsiIfStatement) originalStatement.replace(statement); + // Replace the conditional expression with the one from the original ternary expression + final PsiReferenceExpression condition = (PsiReferenceExpression) conditionalExpression.getCondition().copy(); + newIfStmt.getCondition().replace(condition); + + // Begin building the assignment string for the THEN and ELSE clauses using the + // parent of the ternary conditional expression + PsiAssignmentExpression assignmentExpression = PsiTreeUtil.getParentOfType(conditionalExpression, PsiAssignmentExpression.class, false); + // Get the contents of the assignment expression up to the start of the ternary expression + String exprFrag = assignmentExpression.getLExpression().getText() + assignmentExpression.getOperationSign().getText() ; + + // Build the THEN statement string for the new IF statement, + // make a PsiExpressionStatement from the string, and switch the placeholder + String thenStr = exprFrag + conditionalExpression.getThenExpression().getText() + ";" ; + PsiExpressionStatement thenStmt = (PsiExpressionStatement) factory.createStatementFromText(thenStr, null); + ( (PsiBlockStatement) newIfStmt.getThenBranch() ).getCodeBlock().getStatements()[0].replace(thenStmt); + + // Build the ELSE statement string for the new IF statement, + // make a PsiExpressionStatement from the string, and switch the placeholder + String elseStr = exprFrag + conditionalExpression.getElseExpression().getText() + ";" ; + PsiExpressionStatement elseStmt = (PsiExpressionStatement) factory.createStatementFromText(elseStr, null); + ( (PsiBlockStatement) newIfStmt.getElseBranch() ).getCodeBlock().getStatements()[0].replace(elseStmt); + + // Replace the entire original statement with the new IF + newIfStmt = (PsiIfStatement) originalStatement.replace(newIfStmt); - ((PsiBlockStatement) statement.getThenBranch()).getCodeBlock().getStatements()[0].replace(thenElements[0]); - ((PsiBlockStatement) statement.getElseBranch()).getCodeBlock().getStatements()[0].replace(elseElements[0]); } - public boolean startInWriteAction() { - return true; - } + /** + * Indicates this intention action expects the Psi framework to provide the write action + * context for any changes. + * + * @return
    + *
  • true if the intention requires a write action context to be provided
  • + *
  • false if this intention action will start a write action
  • + *
+ */ + public boolean startInWriteAction() {return true;} + + } + diff --git a/conditional_operator_intention/testData/before.template.java b/conditional_operator_intention/testData/before.template.java index 9d5f5bc71..7b3fcba70 100644 --- a/conditional_operator_intention/testData/before.template.java +++ b/conditional_operator_intention/testData/before.template.java @@ -1,6 +1,6 @@ public class X { void f(boolean isMale) { - String title = isMale < caret > ? "Mr." : "Ms."; + String title = isMale ? "Mr." : "Ms."; System.out.println("title = " + title); } } \ No newline at end of file diff --git a/conditional_operator_intention/testSource/testPlugin/YourTest.java b/conditional_operator_intention/testSource/testPlugin/YourTest.java index 0c0531cf6..aa5021a53 100644 --- a/conditional_operator_intention/testSource/testPlugin/YourTest.java +++ b/conditional_operator_intention/testSource/testPlugin/YourTest.java @@ -1,6 +1,7 @@ package testPlugin; import com.intellij.codeInsight.intention.IntentionAction; +import com.intellij.openapi.application.PathManager; import com.intellij.testFramework.UsefulTestCase; import com.intellij.testFramework.builders.JavaModuleFixtureBuilder; import com.intellij.testFramework.fixtures.*; @@ -17,10 +18,7 @@ import org.junit.*; public class YourTest extends UsefulTestCase { protected CodeInsightTestFixture myFixture; - // Specify path to your test data - // e.g. final String dataPath = "c:\\users\\john - // .doe\\idea\\community\\samples\\conditionalOperatorConvertor/testData"; - final String dataPath = "c:\\users\\John.Doe\\idea\\community\\samples\\conditionalOperatorConvertor/testData"; + final String dataPath = PathManager.getResourceRoot(YourTest.class, "/testPlugin/YourTest.class"); @Before @@ -41,10 +39,9 @@ public class YourTest extends UsefulTestCase { @After public void tearDown() throws Exception { myFixture.tearDown(); - myFixture = null; } - protected void doTest(String testName, String hint) throws Throwable { + protected void doTest(String testName, String hint) { // Messages.showInfoMessage("Test started", "Info"); myFixture.configureByFile(testName + ".java"); final IntentionAction action = myFixture.findSingleIntention(hint); @@ -54,7 +51,7 @@ public class YourTest extends UsefulTestCase { } @Test - public void test() throws Throwable { + public void test() { doTest("before.template", "Convert ternary operator to if statement"); }