diff --git a/java/java-impl/java-impl.iml b/java/java-impl/java-impl.iml index 262ae95b6624..01eaac91530f 100644 --- a/java/java-impl/java-impl.iml +++ b/java/java-impl/java-impl.iml @@ -4,6 +4,7 @@ + diff --git a/java/java-impl/src/com/intellij/application/options/JavadocOptionsProvider.form b/java/java-impl/src/com/intellij/application/options/JavadocOptionsProvider.form new file mode 100644 index 000000000000..4f65e6e9108b --- /dev/null +++ b/java/java-impl/src/com/intellij/application/options/JavadocOptionsProvider.form @@ -0,0 +1,36 @@ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
diff --git a/java/java-impl/src/com/intellij/application/options/JavadocOptionsProvider.java b/java/java-impl/src/com/intellij/application/options/JavadocOptionsProvider.java new file mode 100644 index 000000000000..e45728ad8a83 --- /dev/null +++ b/java/java-impl/src/com/intellij/application/options/JavadocOptionsProvider.java @@ -0,0 +1,91 @@ +/* + * 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.application.options; + +import com.intellij.application.options.editor.EditorOptionsProvider; +import com.intellij.codeInsight.CodeInsightSettings; +import com.intellij.javadoc.JavadocBundle; +import com.intellij.openapi.options.ConfigurationException; +import org.jetbrains.annotations.Nls; +import org.jetbrains.annotations.NotNull; + +import javax.swing.*; + +/** + * @author Denis Zhdanov + * @since 2/2/11 12:32 PM + */ +public class JavadocOptionsProvider implements EditorOptionsProvider { + + private JPanel myWholePanel; + private JCheckBox myAutoGenerateClosingTagCheckBox; + + @NotNull + @Override + public String getId() { + return "editor.preferences.javadocOptions"; + } + + @Override + public Runnable enableSearch(String option) { + return null; + } + + @Nls + @Override + public String getDisplayName() { + return JavadocBundle.message("javadoc.generate.message.title"); + } + + @Override + public Icon getIcon() { + return null; + } + + @Override + public String getHelpTopic() { + return null; + } + + @Override + public JComponent createComponent() { + return myWholePanel; + } + + @Override + public boolean isModified() { + CodeInsightSettings settings = getSettings(); + return myAutoGenerateClosingTagCheckBox.isSelected() ^ settings.JAVADOC_GENERATE_CLOSING_TAG; + } + + @Override + public void apply() throws ConfigurationException { + getSettings().JAVADOC_GENERATE_CLOSING_TAG = myAutoGenerateClosingTagCheckBox.isSelected(); + } + + @Override + public void reset() { + myAutoGenerateClosingTagCheckBox.setSelected(getSettings().JAVADOC_GENERATE_CLOSING_TAG); + } + + @Override + public void disposeUIResources() { + } + + private static CodeInsightSettings getSettings() { + return CodeInsightSettings.getInstance(); + } +} diff --git a/java/java-impl/src/com/intellij/codeInsight/editorActions/JavadocTypedHandler.java b/java/java-impl/src/com/intellij/codeInsight/editorActions/JavadocTypedHandler.java new file mode 100644 index 000000000000..f8de3b44ae76 --- /dev/null +++ b/java/java-impl/src/com/intellij/codeInsight/editorActions/JavadocTypedHandler.java @@ -0,0 +1,149 @@ +/* + * 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.codeInsight.editorActions; + +import com.intellij.codeInsight.CodeInsightSettings; +import com.intellij.lang.ASTNode; +import com.intellij.openapi.editor.Document; +import com.intellij.openapi.editor.Editor; +import com.intellij.openapi.project.Project; +import com.intellij.psi.*; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +/** + * Advises typing in javadoc if necessary. + * + * @author Denis Zhdanov + * @since 2/2/11 11:17 AM + */ +public class JavadocTypedHandler extends TypedHandlerDelegate { + + private static final char START_TAG_SYMBOL = '<'; + private static final char CLOSE_TAG_SYMBOL = '>'; + private static final char SLASH = '/'; + + @Override + public Result charTyped(char c, Project project, Editor editor, PsiFile file) { + if (project == null || editor == null || file == null) { + return Result.CONTINUE; + } + insertClosingTagIfNecessary(c, project, editor, file); + return Result.CONTINUE; + } + + /** + * Checks if it's necessary to insert closing tag on typed character. + * + * @param c typed symbol + * @param project current project + * @param editor current editor + * @param file current file + * @return true if closing tag is inserted; false otherwise + */ + private static boolean insertClosingTagIfNecessary(char c, @NotNull Project project, @NotNull Editor editor, @NotNull PsiFile file) { + if (c != CLOSE_TAG_SYMBOL || !CodeInsightSettings.getInstance().JAVADOC_GENERATE_CLOSING_TAG) { + return false; + } + + PsiDocumentManager.getInstance(project).commitAllDocuments(); + + if (!isTypingInsideJavadoc(editor, file)) { + return false; + } + + // Inspect symbols to the left of the current caret position, insert closing tag only if valid tag is just typed + // (e.g. don't insert anything on single '>' symbol typing). + int offset = editor.getCaretModel().getOffset(); + Document document = editor.getDocument(); + CharSequence tagName = getTagName(document.getText(), offset); + if (tagName == null) { + return false; + } + + document.insertString(offset, String.valueOf(START_TAG_SYMBOL) + SLASH + tagName + CLOSE_TAG_SYMBOL); + return true; + } + + /** + * Tries to derive start tag name assuming that given offset points to position just after '>' symbol. + *

+ * Is expected to return null when offset is not located just after start tag, e.g. the following situations: + *

+   * 
    + *
  • standalone {@code '>'} symbol (surrounded by white spaces);
  • + *
  • after end tag {@code [caret]};
  • + *
  • after empty element tag {@code

    [caret]};

  • + *
+ *
+ * + * @param text target text + * @param afterTagOffset offset that points after + * @return tag name if the one is parsed; null otherwise + */ + @Nullable + static CharSequence getTagName(@NotNull CharSequence text, int afterTagOffset) { + if (afterTagOffset > text.length()) { + return null; + } + int endOffset = afterTagOffset - 1; + + // Check empty element like

+ if (endOffset > 0 && text.charAt(endOffset - 1) == SLASH) { + return null; + } + + for (int i = endOffset - 1; i >= 0; i--) { + char c = text.charAt(i); + switch (c) { + case '\n': return null; + case CLOSE_TAG_SYMBOL: return null; + case START_TAG_SYMBOL: + if (text.charAt(i + 1) == SLASH) { + // Handle situation like [offset]. + return null; + } + return text.subSequence(i + 1, endOffset).toString(); + } + } + return null; + } + + private static boolean isTypingInsideJavadoc(Editor editor, PsiFile file) { + FileViewProvider provider = file.getViewProvider(); + int offset = editor.getCaretModel().getOffset(); + + final PsiElement elementAtCaret; + if (offset < editor.getDocument().getTextLength()) { + elementAtCaret = provider.findElementAt(offset); + } + else { + elementAtCaret = provider.findElementAt(editor.getDocument().getTextLength() - 1); + } + + PsiElement element = elementAtCaret; + while(element instanceof PsiWhiteSpace) { + element = element.getPrevSibling(); + } + + if (element == null) { + return false; + } + + ASTNode node = element.getNode(); + return node != null && JavaDocTokenType.ALL_JAVADOC_TOKENS.contains(node.getElementType()); + } +} diff --git a/java/java-impl/testSrc/com/intellij/codeInsight/editorActions/JavadocTypedHandlerTest.java b/java/java-impl/testSrc/com/intellij/codeInsight/editorActions/JavadocTypedHandlerTest.java new file mode 100644 index 000000000000..891157c1bdaa --- /dev/null +++ b/java/java-impl/testSrc/com/intellij/codeInsight/editorActions/JavadocTypedHandlerTest.java @@ -0,0 +1,48 @@ +package com.intellij.codeInsight.editorActions; + +import org.junit.Test; + +import static org.junit.Assert.assertEquals; + +/** + * @author Denis Zhdanov + * @since 02/02/2011 + */ +public class JavadocTypedHandlerTest { + + private static final String CARET_MARKER = ""; + + @Test + public void correctEmptyTagStart() { + doTest("", "second"); + } + + @Test + public void standaloneBracket() { + doTest("asdf >", null); + } + + @Test + public void emptyElement() { + doTest("", null); + } + + @Test + public void closingTag() { + doTest("", null); + } + + @Test + public void startTagOnNewLine() { + doTest("", null); + } + + private static void doTest(String text, String expected) { + StringBuilder normalized = new StringBuilder(); + int offset = text.indexOf(CARET_MARKER); + normalized.append(text.substring(0, offset)); + normalized.append(text.substring(offset + CARET_MARKER.length())); + CharSequence actual = JavadocTypedHandler.getTagName(normalized.toString(), offset); + assertEquals(expected, actual); + } +} diff --git a/platform/lang-impl/src/com/intellij/codeInsight/CodeInsightSettings.java b/platform/lang-impl/src/com/intellij/codeInsight/CodeInsightSettings.java index 23fcdf2cb11a..d08d346ce5f8 100644 --- a/platform/lang-impl/src/com/intellij/codeInsight/CodeInsightSettings.java +++ b/platform/lang-impl/src/com/intellij/codeInsight/CodeInsightSettings.java @@ -103,6 +103,7 @@ public class CodeInsightSettings implements PersistentStateComponent, C public boolean INSERT_SCRIPTLET_END_ON_ENTER = true; public boolean JAVADOC_STUB_ON_ENTER = true; public boolean SMART_END_ACTION = true; + public boolean JAVADOC_GENERATE_CLOSING_TAG = true; public boolean SURROUND_SELECTION_ON_QUOTE_TYPED = false; diff --git a/resources-en/src/messages/JavadocBundle.properties b/resources-en/src/messages/JavadocBundle.properties index bd98a5c71852..ab36b7157319 100644 --- a/resources-en/src/messages/JavadocBundle.properties +++ b/resources-en/src/messages/JavadocBundle.properties @@ -28,4 +28,5 @@ javadoc.generate.no.jdk.path=JDK path is not specified\nCannot generate JavaDoc javadoc.generate.no.classes.in.selected.packages.error=Selected scope contain no Java classes javadoc.generate.exited=\njavadoc exited with exit code $EXIT_CODE$\n javadoc.generate.output.directory.browse=Browse Output directory -javadoc.generate.locale=&Locale: \ No newline at end of file +javadoc.generate.locale=&Locale: +javadoc.generate.closing.tag=Automatically insert closing tag \ No newline at end of file diff --git a/resources/src/META-INF/IdeaPlugin.xml b/resources/src/META-INF/IdeaPlugin.xml index ef45868895d7..57b6ac2cab12 100644 --- a/resources/src/META-INF/IdeaPlugin.xml +++ b/resources/src/META-INF/IdeaPlugin.xml @@ -772,6 +772,8 @@ + +