blob: 423fa49a75412f819dfffaeb25e8580c61329a3e [file] [log] [blame]
/*
* 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.enter;
import com.intellij.codeInsight.CodeInsightSettings;
import com.intellij.codeInsight.editorActions.CodeDocumentationUtil;
import com.intellij.openapi.actionSystem.DataContext;
import com.intellij.openapi.editor.Document;
import com.intellij.openapi.editor.Editor;
import com.intellij.openapi.editor.actionSystem.EditorActionHandler;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.Ref;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.psi.PsiFile;
import com.intellij.psi.codeStyle.CodeStyleSettingsManager;
import com.intellij.util.text.CharArrayUtil;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.HashSet;
import java.util.Set;
/**
* @author Denis Zhdanov
* @since 1/20/11 12:32 PM
*/
public class EnterAfterJavadocTagHandler extends EnterHandlerDelegateAdapter {
private static final Context NOT_MATCHED_CONTEXT = new Context();
@Override
public Result preprocessEnter(@NotNull PsiFile file,
@NotNull Editor editor,
@NotNull Ref<Integer> caretOffset,
@NotNull Ref<Integer> caretAdvance,
@NotNull DataContext dataContext,
EditorActionHandler originalHandler)
{
if (!CodeInsightSettings.getInstance().SMART_INDENT_ON_ENTER) {
return Result.Continue;
}
Document document = editor.getDocument();
CharSequence text = document.getCharsSequence();
int line = document.getLineNumber(caretOffset.get());
int start = document.getLineStartOffset(line);
int end = document.getLineEndOffset(line);
CodeDocumentationUtil.CommentContext commentContext = CodeDocumentationUtil.tryParseCommentContext(file, text, caretOffset.get(), start);
if (!commentContext.docAsterisk) {
return Result.Continue;
}
Context context = parse(text, start, end, caretOffset.get());
if (!context.shouldGenerateLine()) {
return context.shouldIndent() ? Result.DefaultForceIndent : Result.Continue;
}
String indentInsideJavadoc = CodeDocumentationUtil.getIndentInsideJavadoc(document, caretOffset.get());
boolean restoreCaret = false;
if (caretOffset.get() != context.endTagStartOffset) {
editor.getCaretModel().moveToOffset(context.endTagStartOffset);
restoreCaret = true;
}
originalHandler.execute(editor, dataContext);
Project project = editor.getProject();
if (indentInsideJavadoc != null && project != null && CodeStyleSettingsManager.getSettings(project).JD_LEADING_ASTERISKS_ARE_ENABLED) {
document.insertString(editor.getCaretModel().getOffset(), "*" + indentInsideJavadoc);
}
if (restoreCaret) {
editor.getCaretModel().moveToOffset(caretOffset.get());
}
return Result.DefaultForceIndent;
}
/**
* Analyzes location at the given offset at the given text and returns the following information about that:
* <pre>
* <ol>
* <li>
* if text line that contains given offset is non-first and non-last javadoc line (has <code>'*'</code>
* as a first non-white space symbol);
* </li>
* <li>
* if there is particular opening tag to the left of the given offset (its end offset is returned in case of
* the positive answer)
* </li>
* <li>
* if there is particular closing tag to the left of the given offset (its start offset is returned in case of
* the positive answer)
* </li>
* </ol>
</pre>
*
* @param text target text to analyze
* @param startOffset start offset to use within the given text (inclusive)
* @param endOffset end offset to use withing the given text (exclusive)
* @param offset interested offset
* @return object that encapsulates information about javadoc tags within the given text and offset
*/
@NotNull
static Context parse(@NotNull CharSequence text, int startOffset, int endOffset, int offset) {
int asteriskOffset = StringUtil.indexOf(text, '*', startOffset, endOffset);
if (asteriskOffset < 0) {
return NOT_MATCHED_CONTEXT;
}
startOffset = asteriskOffset + 1;
int startTagStartOffset = -1;
int startTagEndOffset = -1;
Set<CharSequence> closedTags = new HashSet<CharSequence>();
CharSequence startTag = null;
// Try to find start tag to the left of the given offset.
for (int i = offset - 1; i >= startOffset; i--) {
char c = text.charAt(i);
if (c == ' ' || c == '\t') {
continue;
}
if (c == '>' && (startTagEndOffset < 0)) {
if (i > startOffset && text.charAt(i - 1) == '/') {
// Handle situation like '<p/>[offset]'
break;
}
else {
startTagEndOffset = i;
continue;
}
}
if (c == '<') {
if (startTagEndOffset < 0 || i >= endOffset) { // We are inside the tag.
break;
}
if (text.charAt(i + 1) == '/') {
CharSequence tag = text.subSequence(i + 2, startTagEndOffset);
closedTags.add(tag);
startTagEndOffset = -1;
}
else {
CharSequence tag = text.subSequence(i + 1, startTagEndOffset);
if (closedTags.remove(tag)) {
startTagEndOffset = -1;
continue;
}
startTagStartOffset = i;
startTag = text.subSequence(i + 1, startTagEndOffset + 1);
break;
}
}
}
if (startTagStartOffset < 0 || startTagEndOffset < 0) {
return NOT_MATCHED_CONTEXT;
}
int endTagStartOffset = -1;
// Try to find closing tag at or after the given offset.
for (int i = offset; i < endOffset; i++) {
char c = text.charAt(i);
if (c == '<' && i < endOffset && text.charAt(i + 1) == '/' && startTag != null
&& CharArrayUtil.regionMatches(text, i + 2, endOffset, startTag))
{
endTagStartOffset = i;
break;
}
}
return new Context(text, startTagEndOffset, endTagStartOffset, offset);
}
static class Context {
public final int startTagEndOffset;
public final int endTagStartOffset;
@Nullable private final CharSequence myText;
private final int myOffset;
Context() {
this(null, -1, -1, -1);
}
Context(@Nullable CharSequence text, int startTagEndOffset, int endTagStartOffset, int offset) {
myText = text;
this.startTagEndOffset = startTagEndOffset;
this.endTagStartOffset = endTagStartOffset;
myOffset = offset;
}
public boolean shouldGenerateLine() {
return endTagStartOffset >= 0 && shouldIndent();
}
public boolean shouldIndent() {
if (startTagEndOffset < 0 || myText == null) {
return false;
}
for (int i = startTagEndOffset + 1; i < myOffset; i++) {
char c = myText.charAt(i);
if (c != ' ' && c != '\t') {
return false;
}
}
return true;
}
}
}