blob: 1149576e4806f9126f55ffac8ece34ebc5bdd44b [file] [log] [blame]
/*
* Copyright 2000-2014 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.
*/
/*
* Created by IntelliJ IDEA.
* User: max
* Date: May 20, 2002
* Time: 6:21:42 PM
* To change template for new class use
* Code Style | Class Templates options (Tools | IDE Options).
*/
package com.intellij.codeInsight.editorActions;
import com.intellij.ide.DataManager;
import com.intellij.lang.ASTNode;
import com.intellij.openapi.actionSystem.CommonDataKeys;
import com.intellij.openapi.actionSystem.DataContext;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.editor.Caret;
import com.intellij.openapi.editor.Editor;
import com.intellij.openapi.editor.LogicalPosition;
import com.intellij.openapi.editor.ScrollType;
import com.intellij.openapi.editor.actionSystem.EditorActionHandler;
import com.intellij.openapi.editor.actionSystem.EditorWriteActionHandler;
import com.intellij.openapi.editor.ex.DocumentEx;
import com.intellij.openapi.extensions.Extensions;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.TextRange;
import com.intellij.psi.PsiComment;
import com.intellij.psi.PsiDocumentManager;
import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiFile;
import com.intellij.psi.codeStyle.CodeStyleManager;
import com.intellij.psi.impl.source.codeStyle.CodeEditUtil;
import com.intellij.psi.util.PsiTreeUtil;
import com.intellij.util.IncorrectOperationException;
import com.intellij.util.NotNullFunction;
import com.intellij.util.text.CharArrayUtil;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import static com.intellij.codeInsight.editorActions.JoinLinesHandlerDelegate.CANNOT_JOIN;
public class JoinLinesHandler extends EditorWriteActionHandler {
private static final Logger LOG = Logger.getInstance("#com.intellij.codeInsight.editorActions.JoinLinesHandler");
private final EditorActionHandler myOriginalHandler;
public JoinLinesHandler(EditorActionHandler originalHandler) {
super(true);
myOriginalHandler = originalHandler;
}
@NotNull
private static TextRange findStartAndEnd(@NotNull CharSequence text, int start, int end, int maxoffset) {
while (start > 0 && (text.charAt(start) == ' ' || text.charAt(start) == '\t')) start--;
while (end < maxoffset && (text.charAt(end) == ' ' || text.charAt(end) == '\t')) end++;
return new TextRange(start, end);
}
@Override
public void executeWriteAction(@NotNull final Editor editor, @Nullable Caret caret, final DataContext dataContext) {
assert caret != null;
if (!(editor.getDocument() instanceof DocumentEx)) {
myOriginalHandler.execute(editor, caret, dataContext);
return;
}
final DocumentEx doc = (DocumentEx)editor.getDocument();
final Project project = CommonDataKeys.PROJECT.getData(DataManager.getInstance().getDataContext(editor.getContentComponent()));
if (project == null) return;
final PsiDocumentManager docManager = PsiDocumentManager.getInstance(project);
PsiFile psiFile = docManager.getPsiFile(doc);
if (psiFile == null) {
myOriginalHandler.execute(editor, caret, dataContext);
return;
}
LogicalPosition caretPosition = caret.getLogicalPosition();
int startLine = caretPosition.line;
int endLine = startLine + 1;
if (caret.hasSelection()) {
startLine = doc.getLineNumber(caret.getSelectionStart());
endLine = doc.getLineNumber(caret.getSelectionEnd());
if (doc.getLineStartOffset(endLine) == caret.getSelectionEnd()) endLine--;
}
final int startReformatOffset = CharArrayUtil.shiftBackward(doc.getCharsSequence(), doc.getLineEndOffset(startLine), " \t");
CodeEditUtil.setNodeReformatStrategy(new NotNullFunction<ASTNode, Boolean>() {
@NotNull
@Override
public Boolean fun(@NotNull ASTNode node) {
return node.getTextRange().getStartOffset() >= startReformatOffset;
}
});
try {
doJob(editor, doc, caret, project, docManager, psiFile, startLine, endLine);
}
finally {
CodeEditUtil.setNodeReformatStrategy(null);
}
}
private static void doJob(@NotNull Editor editor,
@NotNull DocumentEx doc,
@NotNull Caret caret,
@NotNull Project project,
@NotNull PsiDocumentManager docManager,
@NotNull PsiFile psiFile,
int startLine,
int endLine) {
int caretRestoreOffset = -1;
// joining lines, several times if selection is multiline
for (int i = startLine; i < endLine; i++) {
if (i >= doc.getLineCount() - 1) break;
int lineEndOffset = doc.getLineEndOffset(startLine);
docManager.doPostponedOperationsAndUnblockDocument(doc);
docManager.commitDocument(doc);
CharSequence text = doc.getCharsSequence();
int firstNonSpaceOffsetInNextLine = doc.getLineStartOffset(startLine + 1);
while (firstNonSpaceOffsetInNextLine < text.length() - 1
&& (text.charAt(firstNonSpaceOffsetInNextLine) == ' ' || text.charAt(firstNonSpaceOffsetInNextLine) == '\t'))
{
firstNonSpaceOffsetInNextLine++;
}
PsiElement elementAtNextLineStart = psiFile.findElementAt(firstNonSpaceOffsetInNextLine);
boolean isNextLineStartsWithComment = isCommentElement(elementAtNextLineStart);
int lastNonSpaceOffsetInStartLine = lineEndOffset;
while (lastNonSpaceOffsetInStartLine > 0 &&
(text.charAt(lastNonSpaceOffsetInStartLine - 1) == ' ' || text.charAt(lastNonSpaceOffsetInStartLine - 1) == '\t')) {
lastNonSpaceOffsetInStartLine--;
}
int elemOffset = lastNonSpaceOffsetInStartLine > doc.getLineStartOffset(startLine) ? lastNonSpaceOffsetInStartLine - 1 : -1;
PsiElement elementAtStartLineEnd = elemOffset == -1 ? null : psiFile.findElementAt(elemOffset);
boolean isStartLineEndsWithComment = isCommentElement(elementAtStartLineEnd);
int rc = -1;
int start;
int end;
TextRange limits = findStartAndEnd(text, lastNonSpaceOffsetInStartLine, firstNonSpaceOffsetInNextLine, doc.getTextLength());
start = limits.getStartOffset(); end = limits.getEndOffset();
// run raw joiners
for(JoinLinesHandlerDelegate delegate: Extensions.getExtensions(JoinLinesHandlerDelegate.EP_NAME)) {
if (delegate instanceof JoinRawLinesHandlerDelegate) {
rc = ((JoinRawLinesHandlerDelegate)delegate).tryJoinRawLines(doc, psiFile, start, end);
if (rc != CANNOT_JOIN) {
caretRestoreOffset = rc;
break;
}
}
}
if (rc == CANNOT_JOIN) { // remove indents and newline, run non-raw joiners
if (lastNonSpaceOffsetInStartLine == doc.getLineStartOffset(startLine)) {
doc.deleteString(doc.getLineStartOffset(startLine), firstNonSpaceOffsetInNextLine);
int indent = -1;
try {
docManager.commitDocument(doc);
indent = CodeStyleManager.getInstance(project).adjustLineIndent(psiFile, startLine == 0 ? 0 : doc.getLineStartOffset(startLine));
}
catch (IncorrectOperationException e) {
LOG.error(e);
}
if (caretRestoreOffset == CANNOT_JOIN) {
caretRestoreOffset = indent;
}
continue;
}
doc.deleteString(lineEndOffset, lineEndOffset + doc.getLineSeparatorLength(startLine));
text = doc.getCharsSequence();
limits = findStartAndEnd(text, lineEndOffset - 1, lineEndOffset, doc.getTextLength());
start = limits.getStartOffset(); end = limits.getEndOffset();
// Check if we're joining splitted string literal.
docManager.commitDocument(doc);
for(JoinLinesHandlerDelegate delegate: Extensions.getExtensions(JoinLinesHandlerDelegate.EP_NAME)) {
rc = delegate.tryJoinLines(doc, psiFile, start, end);
if (rc != CANNOT_JOIN) break;
}
}
docManager.doPostponedOperationsAndUnblockDocument(doc);
if (rc != CANNOT_JOIN) {
if (caretRestoreOffset == CANNOT_JOIN) caretRestoreOffset = rc;
continue;
}
if (caretRestoreOffset == CANNOT_JOIN) caretRestoreOffset = start == lineEndOffset ? start : start + 1;
if (isStartLineEndsWithComment && isNextLineStartsWithComment) {
if (text.charAt(end) == '*' && end < text.length() && text.charAt(end + 1) != '/') {
end++;
while (end < doc.getTextLength() && (text.charAt(end) == ' ' || text.charAt(end) == '\t')) end++;
}
else if (text.charAt(end) == '/') {
end += 2;
while (end < doc.getTextLength() && (text.charAt(end) == ' ' || text.charAt(end) == '\t')) end++;
}
doc.replaceString(start == lineEndOffset ? start : start + 1, end, " ");
continue;
}
while (end < doc.getTextLength() && (text.charAt(end) == ' ' || text.charAt(end) == '\t')) end++;
doc.replaceString(start == lineEndOffset ? start : start + 1, end, " ");
if (start <= doc.getLineStartOffset(startLine)) {
try {
docManager.commitDocument(doc);
CodeStyleManager.getInstance(project).adjustLineIndent(psiFile, doc.getLineStartOffset(startLine));
}
catch (IncorrectOperationException e) {
LOG.error(e);
}
}
int prevLineCount = doc.getLineCount();
docManager.commitDocument(doc);
try {
CodeStyleManager.getInstance(project).reformatRange(psiFile, start + 1, end, true);
}
catch (IncorrectOperationException e) {
LOG.error(e);
}
if (prevLineCount < doc.getLineCount()) {
docManager.doPostponedOperationsAndUnblockDocument(doc);
end = doc.getLineEndOffset(startLine) + doc.getLineSeparatorLength(startLine);
start = end - doc.getLineSeparatorLength(startLine);
int addedLinesCount = doc.getLineCount() - prevLineCount - 1;
while (end < doc.getTextLength() &&
(text.charAt(end) == ' ' || text.charAt(end) == '\t' || text.charAt(end) == '\n' && addedLinesCount > 0)) {
if (text.charAt(end) == '\n') addedLinesCount--;
end++;
}
doc.replaceString(start, end, " ");
}
docManager.commitDocument(doc);
}
docManager.commitDocument(doc); // cheap on an already-committed doc
if (caret.hasSelection()) {
caret.moveToOffset(caret.getSelectionEnd());
}
else if (caretRestoreOffset != CANNOT_JOIN) {
caret.moveToOffset(caretRestoreOffset);
if (caret == editor.getCaretModel().getPrimaryCaret()) { // performance
editor.getScrollingModel().scrollToCaret(ScrollType.RELATIVE);
}
caret.removeSelection();
}
}
private static boolean isCommentElement(@Nullable final PsiElement element) {
return element != null && PsiTreeUtil.getParentOfType(element, PsiComment.class, false) != null;
}
}