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 @@
+
+