| /* |
| * 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. |
| */ |
| package com.intellij.codeInsight.editorActions; |
| |
| import com.intellij.codeInsight.CodeInsightSettings; |
| import com.intellij.codeInsight.CodeInsightUtilBase; |
| import com.intellij.ide.PasteProvider; |
| import com.intellij.lang.LanguageFormatting; |
| import com.intellij.openapi.actionSystem.CommonDataKeys; |
| import com.intellij.openapi.actionSystem.DataContext; |
| import com.intellij.openapi.application.ApplicationManager; |
| import com.intellij.openapi.diagnostic.Logger; |
| import com.intellij.openapi.editor.*; |
| import com.intellij.openapi.editor.actionSystem.EditorActionHandler; |
| import com.intellij.openapi.editor.actionSystem.EditorActionManager; |
| import com.intellij.openapi.editor.actionSystem.EditorTextInsertHandler; |
| import com.intellij.openapi.editor.actions.PasteAction; |
| import com.intellij.openapi.editor.ex.EditorEx; |
| import com.intellij.openapi.extensions.ExtensionPointName; |
| import com.intellij.openapi.extensions.Extensions; |
| import com.intellij.openapi.fileEditor.FileDocumentManager; |
| import com.intellij.openapi.ide.CopyPasteManager; |
| import com.intellij.openapi.project.Project; |
| import com.intellij.openapi.util.Ref; |
| import com.intellij.openapi.util.TextRange; |
| import com.intellij.openapi.util.text.StringUtil; |
| import com.intellij.openapi.vfs.VirtualFile; |
| import com.intellij.psi.PsiDocumentManager; |
| import com.intellij.psi.PsiFile; |
| import com.intellij.psi.SingleRootFileViewProvider; |
| import com.intellij.psi.codeStyle.CodeStyleManager; |
| import com.intellij.util.DocumentUtil; |
| import com.intellij.util.IncorrectOperationException; |
| import com.intellij.util.Producer; |
| import com.intellij.util.containers.HashMap; |
| import com.intellij.util.text.CharArrayUtil; |
| import org.jetbrains.annotations.NonNls; |
| import org.jetbrains.annotations.Nullable; |
| |
| import java.awt.datatransfer.DataFlavor; |
| import java.awt.datatransfer.Transferable; |
| import java.util.*; |
| |
| public class PasteHandler extends EditorActionHandler implements EditorTextInsertHandler { |
| private static final Logger LOG = Logger.getInstance("#com.intellij.codeInsight.editorActions.PasteHandler"); |
| private static final ExtensionPointName<PasteProvider> EP_NAME = ExtensionPointName.create("com.intellij.customPasteProvider"); |
| |
| private final EditorActionHandler myOriginalHandler; |
| |
| public PasteHandler(EditorActionHandler originalAction) { |
| myOriginalHandler = originalAction; |
| } |
| |
| @Override |
| public void doExecute(final Editor editor, Caret caret, final DataContext dataContext) { |
| assert caret == null : "Invocation of 'paste' operation for specific caret is not supported"; |
| execute(editor, dataContext, null); |
| } |
| |
| @Override |
| public void execute(final Editor editor, final DataContext dataContext, @Nullable final Producer<Transferable> producer) { |
| if (!CodeInsightUtilBase.prepareEditorForWrite(editor)) return; |
| |
| final Document document = editor.getDocument(); |
| if (!FileDocumentManager.getInstance().requestWriting(document, CommonDataKeys.PROJECT.getData(dataContext))) { |
| return; |
| } |
| |
| DataContext context = dataContext; |
| if (producer != null) { |
| context = new DataContext() { |
| @Override |
| public Object getData(@NonNls String dataId) { |
| return PasteAction.TRANSFERABLE_PROVIDER.is(dataId) ? producer : dataContext.getData(dataId); |
| } |
| }; |
| } |
| |
| final Project project = editor.getProject(); |
| if (project == null || editor.isColumnMode() || editor.getSelectionModel().hasBlockSelection() |
| || editor.getCaretModel().getCaretCount() > 1) { |
| if (myOriginalHandler != null) { |
| myOriginalHandler.execute(editor, null, context); |
| } |
| return; |
| } |
| |
| final PsiFile file = PsiDocumentManager.getInstance(project).getPsiFile(document); |
| if (file == null) { |
| if (myOriginalHandler != null) { |
| myOriginalHandler.execute(editor, null, context); |
| } |
| return; |
| } |
| |
| document.startGuardedBlockChecking(); |
| try { |
| for (PasteProvider provider : Extensions.getExtensions(EP_NAME)) { |
| if (provider.isPasteEnabled(context)) { |
| provider.performPaste(context); |
| return; |
| } |
| } |
| doPaste(editor, project, file, document, producer); |
| } |
| catch (ReadOnlyFragmentModificationException e) { |
| EditorActionManager.getInstance().getReadonlyFragmentModificationHandler(document).handle(e); |
| } |
| finally { |
| document.stopGuardedBlockChecking(); |
| } |
| } |
| |
| private static void doPaste(final Editor editor, |
| final Project project, |
| final PsiFile file, |
| final Document document, |
| final Producer<Transferable> producer) { |
| Transferable content = null; |
| |
| if (producer != null) { |
| content = producer.produce(); |
| } |
| else { |
| CopyPasteManager manager = CopyPasteManager.getInstance(); |
| if (manager.areDataFlavorsAvailable(DataFlavor.stringFlavor)) { |
| content = manager.getContents(); |
| if (content != null) { |
| manager.stopKillRings(); |
| } |
| } |
| } |
| |
| if (content != null) { |
| String text = null; |
| try { |
| text = (String)content.getTransferData(DataFlavor.stringFlavor); |
| } |
| catch (Exception e) { |
| editor.getComponent().getToolkit().beep(); |
| } |
| if (text == null) return; |
| |
| final CodeInsightSettings settings = CodeInsightSettings.getInstance(); |
| |
| final Map<CopyPastePostProcessor, List<? extends TextBlockTransferableData>> extraData = new HashMap<CopyPastePostProcessor, List<? extends TextBlockTransferableData>>(); |
| Collection<TextBlockTransferableData> allValues = new ArrayList<TextBlockTransferableData>(); |
| for (CopyPastePostProcessor<? extends TextBlockTransferableData> processor : Extensions.getExtensions(CopyPastePostProcessor.EP_NAME)) { |
| List<? extends TextBlockTransferableData> data = processor.extractTransferableData(content); |
| if (!data.isEmpty()) { |
| extraData.put(processor, data); |
| allValues.addAll(data); |
| } |
| } |
| |
| text = TextBlockTransferable.convertLineSeparators(text, "\n", allValues); |
| |
| final CaretModel caretModel = editor.getCaretModel(); |
| final SelectionModel selectionModel = editor.getSelectionModel(); |
| final int col = caretModel.getLogicalPosition().column; |
| |
| // There is a possible case that we want to perform paste while there is an active selection at the editor and caret is located |
| // inside it (e.g. Ctrl+A is pressed while caret is not at the zero column). We want to insert the text at selection start column |
| // then, hence, inserted block of text should be indented according to the selection start as well. |
| final int blockIndentAnchorColumn; |
| final int caretOffset = caretModel.getOffset(); |
| if (selectionModel.hasSelection() && caretOffset >= selectionModel.getSelectionStart()) { |
| blockIndentAnchorColumn = editor.offsetToLogicalPosition(selectionModel.getSelectionStart()).column; |
| } |
| else { |
| blockIndentAnchorColumn = col; |
| } |
| |
| // We assume that EditorModificationUtil.insertStringAtCaret() is smart enough to remove currently selected text (if any). |
| |
| RawText rawText = RawText.fromTransferable(content); |
| String newText = text; |
| for (CopyPastePreProcessor preProcessor : Extensions.getExtensions(CopyPastePreProcessor.EP_NAME)) { |
| newText = preProcessor.preprocessOnPaste(project, file, editor, newText, rawText); |
| } |
| int indentOptions = text.equals(newText) ? settings.REFORMAT_ON_PASTE : CodeInsightSettings.REFORMAT_BLOCK; |
| text = newText; |
| |
| if (LanguageFormatting.INSTANCE.forContext(file) == null && indentOptions != CodeInsightSettings.NO_REFORMAT) { |
| indentOptions = CodeInsightSettings.INDENT_BLOCK; |
| } |
| |
| final String _text = text; |
| ApplicationManager.getApplication().runWriteAction( |
| new Runnable() { |
| @Override |
| public void run() { |
| EditorModificationUtil.insertStringAtCaret(editor, _text, false, true); |
| } |
| } |
| ); |
| |
| int length = text.length(); |
| int offset = caretModel.getOffset() - length; |
| if (offset < 0) { |
| length += offset; |
| offset = 0; |
| } |
| final RangeMarker bounds = document.createRangeMarker(offset, offset + length); |
| |
| caretModel.moveToOffset(bounds.getEndOffset()); |
| editor.getScrollingModel().scrollToCaret(ScrollType.RELATIVE); |
| selectionModel.removeSelection(); |
| |
| final Ref<Boolean> indented = new Ref<Boolean>(Boolean.FALSE); |
| for (Map.Entry<CopyPastePostProcessor, List<? extends TextBlockTransferableData>> e : extraData.entrySet()) { |
| //noinspection unchecked |
| e.getKey().processTransferableData(project, editor, bounds, caretOffset, indented, e.getValue()); |
| } |
| |
| boolean pastedTextContainsWhiteSpacesOnly = |
| CharArrayUtil.shiftForward(document.getCharsSequence(), bounds.getStartOffset(), " \n\t") >= bounds.getEndOffset(); |
| |
| VirtualFile virtualFile = file.getVirtualFile(); |
| if (!pastedTextContainsWhiteSpacesOnly && (virtualFile == null || !SingleRootFileViewProvider.isTooLargeForIntelligence(virtualFile))) { |
| final int indentOptions1 = indentOptions; |
| |
| ApplicationManager.getApplication().runWriteAction( |
| new Runnable() { |
| @Override |
| public void run() { |
| PsiDocumentManager.getInstance(project).doPostponedOperationsAndUnblockDocument(document); |
| switch (indentOptions1) { |
| case CodeInsightSettings.INDENT_BLOCK: |
| if (!indented.get()) { |
| indentBlock(project, editor, bounds.getStartOffset(), bounds.getEndOffset(), blockIndentAnchorColumn); |
| } |
| break; |
| |
| case CodeInsightSettings.INDENT_EACH_LINE: |
| if (!indented.get()) { |
| indentEachLine(project, editor, bounds.getStartOffset(), bounds.getEndOffset()); |
| } |
| break; |
| |
| case CodeInsightSettings.REFORMAT_BLOCK: |
| indentEachLine(project, editor, bounds.getStartOffset(), bounds.getEndOffset()); // this is needed for example when inserting a comment before method |
| reformatBlock(project, editor, bounds.getStartOffset(), bounds.getEndOffset()); |
| break; |
| } |
| } |
| } |
| ); |
| } |
| |
| if (bounds.isValid()) { |
| caretModel.moveToOffset(bounds.getEndOffset()); |
| editor.getScrollingModel().scrollToCaret(ScrollType.RELATIVE); |
| selectionModel.removeSelection(); |
| editor.putUserData(EditorEx.LAST_PASTED_REGION, TextRange.create(bounds)); |
| } |
| } |
| } |
| |
| static void indentBlock(Project project, Editor editor, final int startOffset, final int endOffset, int originalCaretCol) { |
| final PsiDocumentManager documentManager = PsiDocumentManager.getInstance(project); |
| documentManager.commitAllDocuments(); |
| final Document document = editor.getDocument(); |
| PsiFile file = documentManager.getPsiFile(document); |
| if (file == null) { |
| return; |
| } |
| |
| if (LanguageFormatting.INSTANCE.forContext(file) != null) { |
| indentBlockWithFormatter(project, document, startOffset, endOffset, file); |
| } |
| else { |
| indentPlainTextBlock(document, startOffset, endOffset, originalCaretCol); |
| } |
| } |
| |
| private static void indentEachLine(Project project, Editor editor, int startOffset, int endOffset) { |
| PsiDocumentManager.getInstance(project).commitAllDocuments(); |
| PsiFile file = PsiDocumentManager.getInstance(project).getPsiFile(editor.getDocument()); |
| |
| CodeStyleManager codeStyleManager = CodeStyleManager.getInstance(project); |
| final CharSequence text = editor.getDocument().getCharsSequence(); |
| if (startOffset > 0 && endOffset > startOffset + 1 && text.charAt(endOffset - 1) == '\n' && text.charAt(startOffset - 1) == '\n') { |
| // There is a possible situation that pasted text ends by a line feed. We don't want to proceed it when a text is |
| // pasted at the first line column. |
| // Example: |
| // text to paste: |
| //'if (true) { |
| //' |
| // source: |
| // if (true) { |
| // int i = 1; |
| // int j = 1; |
| // } |
| // |
| // |
| // We get the following on paste then: |
| // if (true) { |
| // if (true) { |
| // int i = 1; |
| // int j = 1; |
| // } |
| // |
| // We don't want line 'int i = 1;' to be indented here. |
| endOffset--; |
| } |
| try { |
| codeStyleManager.adjustLineIndent(file, new TextRange(startOffset, endOffset)); |
| } |
| catch (IncorrectOperationException e) { |
| LOG.error(e); |
| } |
| } |
| |
| private static void reformatBlock(final Project project, final Editor editor, final int startOffset, final int endOffset) { |
| PsiDocumentManager.getInstance(project).commitAllDocuments(); |
| Runnable task = new Runnable() { |
| @Override |
| public void run() { |
| PsiFile file = PsiDocumentManager.getInstance(project).getPsiFile(editor.getDocument()); |
| try { |
| CodeStyleManager.getInstance(project).reformatRange(file, startOffset, endOffset, true); |
| } |
| catch (IncorrectOperationException e) { |
| LOG.error(e); |
| } |
| } |
| }; |
| |
| if (endOffset - startOffset > 1000) { |
| DocumentUtil.executeInBulk(editor.getDocument(), true, task); |
| } |
| else { |
| task.run(); |
| } |
| } |
| |
| @SuppressWarnings("ForLoopThatDoesntUseLoopVariable") |
| private static void indentPlainTextBlock(final Document document, final int startOffset, final int endOffset, final int indentLevel) { |
| CharSequence chars = document.getCharsSequence(); |
| int spaceEnd = CharArrayUtil.shiftForward(chars, startOffset, " \t"); |
| int line = document.getLineNumber(startOffset); |
| if (spaceEnd > endOffset || indentLevel <= 0 || line >= document.getLineCount() - 1 || chars.charAt(spaceEnd) == '\n') { |
| return; |
| } |
| |
| int linesToAdjustIndent = 0; |
| for (int i = line + 1; i < document.getLineCount(); i++) { |
| if (document.getLineStartOffset(i) >= endOffset) { |
| break; |
| } |
| linesToAdjustIndent++; |
| } |
| |
| String indentString = StringUtil.repeatSymbol(' ', indentLevel); |
| |
| for (; linesToAdjustIndent > 0; linesToAdjustIndent--) { |
| int lineStartOffset = document.getLineStartOffset(++line); |
| document.insertString(lineStartOffset, indentString); |
| } |
| } |
| |
| private static void indentBlockWithFormatter(Project project, Document document, int startOffset, int endOffset, PsiFile file) { |
| |
| // Algorithm: the main idea is to process the first line of the pasted block, adjust its indent if necessary, calculate indent |
| // adjustment string and apply to each line of the pasted block starting from the second one. |
| // |
| // We differentiate the following possible states here: |
| // --- pasted block doesn't start new line, i.e. there are non-white space symbols before it at the first line. |
| // Example: |
| // old content [pasted line 1 |
| // pasted line 2] |
| // Indent adjustment string is just the first line indent then. |
| // |
| // --- pasted block starts with empty line(s) |
| // Example: |
| // old content [ |
| // pasted line 1 |
| // pasted line 2] |
| // We parse existing indents of the pasted block then, adjust its first non-blank line via formatter and adjust indent |
| // of subsequent pasted lines in order to preserve old indentation. |
| // |
| // --- pasted block is located at the new line and starts with white space symbols. |
| // Example: |
| // [ pasted line 1 |
| // pasted line 2] |
| // We parse existing indents of the pasted block then, adjust its first line via formatter and adjust indent of the pasted lines |
| // starting from the second one in order to preserve old indentation. |
| // |
| // --- pasted block is located at the new line but doesn't start with white space symbols. |
| // Example: |
| // [pasted line 1 |
| // pasted line 2] |
| // We adjust the first line via formatter then and apply first line's indent to all subsequent pasted lines. |
| |
| CharSequence chars = document.getCharsSequence(); |
| final int firstLine = document.getLineNumber(startOffset); |
| final int firstLineStart = document.getLineStartOffset(firstLine); |
| |
| // There is a possible case that we paste block that ends with new line that is empty or contains only white space symbols. |
| // We want to preserve indent for the original document line where paste was performed. |
| // Example: |
| // Original: |
| // if (test) { |
| // <caret> } |
| // |
| // Pasting: 'int i = 1;\n' |
| // Expected: |
| // if (test) { |
| // int i = 1; |
| // } |
| boolean saveLastLineIndent = false; |
| for (int i = endOffset - 1; i >= startOffset; i--) { |
| final char c = chars.charAt(i); |
| if (c == '\n') { |
| saveLastLineIndent = true; |
| break; |
| } |
| if (c != ' ' && c != '\t') { |
| break; |
| } |
| } |
| |
| final int lastLine; |
| if (saveLastLineIndent) { |
| lastLine = document.getLineNumber(endOffset) - 1; |
| // Remove white space symbols at the pasted text if any. |
| int start = document.getLineStartOffset(lastLine + 1); |
| if (start < endOffset) { |
| int i = CharArrayUtil.shiftForward(chars, start, " \t"); |
| if (i > start) { |
| i = Math.min(i, endOffset); |
| document.deleteString(start, i); |
| } |
| } |
| |
| // Insert white space from the start line of the pasted block. |
| int indentToKeepEndOffset = Math.min(startOffset, CharArrayUtil.shiftForward(chars, firstLineStart, " \t")); |
| if (indentToKeepEndOffset > firstLineStart) { |
| document.insertString(start, chars.subSequence(firstLineStart, indentToKeepEndOffset)); |
| } |
| } |
| else { |
| lastLine = document.getLineNumber(endOffset); |
| } |
| |
| final int i = CharArrayUtil.shiftBackward(chars, startOffset - 1, " \t"); |
| |
| // Handle a situation when pasted block doesn't start a new line. |
| if (chars.charAt(startOffset) != '\n' && i > 0 && chars.charAt(i) != '\n') { |
| int firstNonWsOffset = CharArrayUtil.shiftForward(chars, firstLineStart, " \t"); |
| if (firstNonWsOffset > firstLineStart) { |
| CharSequence toInsert = chars.subSequence(firstLineStart, firstNonWsOffset); |
| for (int line = firstLine + 1; line <= lastLine; line++) { |
| document.insertString(document.getLineStartOffset(line), toInsert); |
| } |
| } |
| return; |
| } |
| |
| // Sync document and PSI for correct formatting processing. |
| PsiDocumentManager.getInstance(project).commitAllDocuments(); |
| if (file == null) { |
| return; |
| } |
| CodeStyleManager codeStyleManager = CodeStyleManager.getInstance(project); |
| |
| final int j = CharArrayUtil.shiftForward(chars, startOffset, " \t\n"); |
| if (j >= endOffset) { |
| // Pasted text contains white space/line feed symbols only, do nothing. |
| return; |
| } |
| |
| final int anchorLine = document.getLineNumber(j); |
| final int anchorLineStart = document.getLineStartOffset(anchorLine); |
| codeStyleManager.adjustLineIndent(file, j); |
| |
| // Handle situation when pasted block starts with non-white space symbols. |
| if (anchorLine == firstLine && j == startOffset) { |
| int indentOffset = CharArrayUtil.shiftForward(chars, firstLineStart, " \t"); |
| if (indentOffset > firstLineStart) { |
| CharSequence toInsert = chars.subSequence(firstLineStart, indentOffset); |
| for (int line = firstLine + 1; line <= lastLine; line++) { |
| document.insertString(document.getLineStartOffset(line), toInsert); |
| } |
| } |
| return; |
| } |
| |
| // Handle situation when pasted block starts from white space symbols. Assume that the pasted text started at the line start, |
| // i.e. correct indentation level is stored at the blocks structure. |
| final int firstNonWsOffset = CharArrayUtil.shiftForward(chars, anchorLineStart, " \t"); |
| final int diff = firstNonWsOffset - j; |
| if (diff == 0) { |
| return; |
| } |
| if (diff > 0) { |
| CharSequence toInsert = chars.subSequence(anchorLineStart, anchorLineStart + diff); |
| for (int line = anchorLine + 1; line <= lastLine; line++) { |
| document.insertString(document.getLineStartOffset(line), toInsert); |
| } |
| return; |
| } |
| |
| // We've pasted text to the non-first column and exact white space between the line start and caret position on the moment of paste |
| // has been removed by formatter during 'adjust line indent' |
| // Example: |
| // copied text: |
| // ' line1 |
| // line2' |
| // after paste: |
| // line start -> ' I line1 |
| // line2' (I - caret position during 'paste') |
| // formatter removed white space between the line start and caret position, so, current document state is: |
| // ' line1 |
| // line2' |
| if (anchorLine == firstLine && -diff == startOffset - firstLineStart) { |
| return; |
| } |
| if (anchorLine != firstLine || -diff > startOffset - firstLineStart) { |
| final int desiredSymbolsToRemove; |
| if (anchorLine == firstLine) { |
| desiredSymbolsToRemove = -diff - (startOffset - firstLineStart); |
| } |
| else { |
| desiredSymbolsToRemove = -diff; |
| } |
| |
| for (int line = anchorLine + 1; line <= lastLine; line++) { |
| int currentLineStart = document.getLineStartOffset(line); |
| int currentLineIndentOffset = CharArrayUtil.shiftForward(chars, currentLineStart, " \t"); |
| int symbolsToRemove = Math.min(currentLineIndentOffset - currentLineStart, desiredSymbolsToRemove); |
| if (symbolsToRemove > 0) { |
| document.deleteString(currentLineStart, currentLineStart + symbolsToRemove); |
| } |
| } |
| } |
| else { |
| CharSequence toInsert = chars.subSequence(anchorLineStart, diff + startOffset); |
| for (int line = anchorLine + 1; line <= lastLine; line++) { |
| document.insertString(document.getLineStartOffset(line), toInsert); |
| } |
| } |
| } |
| } |