diff options
Diffstat (limited to 'platform/platform-impl/src/com/intellij/openapi/editor')
15 files changed, 1118 insertions, 188 deletions
diff --git a/platform/platform-impl/src/com/intellij/openapi/editor/CaretStateTransferableData.java b/platform/platform-impl/src/com/intellij/openapi/editor/CaretStateTransferableData.java new file mode 100644 index 000000000000..f0d2085cbb6b --- /dev/null +++ b/platform/platform-impl/src/com/intellij/openapi/editor/CaretStateTransferableData.java @@ -0,0 +1,56 @@ +/* + * 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.openapi.editor; + +import com.intellij.codeInsight.editorActions.TextBlockTransferableData; + +import java.awt.datatransfer.DataFlavor; + +public class CaretStateTransferableData implements TextBlockTransferableData { + public static final DataFlavor FLAVOR = new DataFlavor(CaretStateTransferableData.class, "Caret state"); + + public final int[] startOffsets; + public final int[] endOffsets; + + public CaretStateTransferableData(int[] startOffsets, int[] endOffsets) { + this.startOffsets = startOffsets; + this.endOffsets = endOffsets; + } + + @Override + public DataFlavor getFlavor() { + return FLAVOR; + } + + @Override + public int getOffsetCount() { + return startOffsets.length + endOffsets.length; + } + + @Override + public int getOffsets(int[] offsets, int index) { + System.arraycopy(startOffsets, 0, offsets, index, startOffsets.length); + System.arraycopy(endOffsets, 0, offsets, index + startOffsets.length, endOffsets.length); + return index + getOffsetCount(); + } + + @Override + public int setOffsets(int[] offsets, int index) { + System.arraycopy(offsets, index, startOffsets, 0, startOffsets.length); + System.arraycopy(offsets, index + startOffsets.length, endOffsets, 0, endOffsets.length); + return index + getOffsetCount(); + } +} diff --git a/platform/platform-impl/src/com/intellij/openapi/editor/ClipboardTextPerCaretSplitter.java b/platform/platform-impl/src/com/intellij/openapi/editor/ClipboardTextPerCaretSplitter.java new file mode 100644 index 000000000000..a3e62412f4a4 --- /dev/null +++ b/platform/platform-impl/src/com/intellij/openapi/editor/ClipboardTextPerCaretSplitter.java @@ -0,0 +1,68 @@ +/* + * 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.openapi.editor; + +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +public class ClipboardTextPerCaretSplitter { + @NotNull + public List<String> split(@NotNull String input, @Nullable CaretStateTransferableData caretData, int caretCount) { + if (caretCount <= 0) { + throw new IllegalArgumentException("Caret count must be positive"); + } + if (caretCount == 1) { + return Collections.singletonList(input); + } + List<String> result = new ArrayList<String>(caretCount); + int sourceCaretCount = caretData == null ? -1 : caretData.startOffsets.length; + String[] lines = sourceCaretCount == 1 || sourceCaretCount == caretCount ? null : input.split("\n", -1); + for (int i = 0; i < caretCount; i++) { + if (sourceCaretCount == 1) { + result.add(input); + } + else if (sourceCaretCount == caretCount) { + //noinspection ConstantConditions + result.add(new String(input.substring(caretData.startOffsets[i], caretData.endOffsets[i]))); + } + else if (lines.length == 0) { + result.add(""); + } + else if (lines.length == 1) { + result.add(lines[0]); + } + else if (lines.length % caretCount == 0) { + StringBuilder b = new StringBuilder(); + int linesPerSegment = lines.length / caretCount; + for (int j = 0; j < linesPerSegment; j++) { + if (j > 0) { + b.append('\n'); + } + b.append(lines[i * linesPerSegment + j]); + } + result.add(b.toString()); + } + else { + result.add(i < lines.length ? lines[i] : ""); + } + } + return result; + } +} diff --git a/platform/platform-impl/src/com/intellij/openapi/editor/actions/CloneCaretAbove.java b/platform/platform-impl/src/com/intellij/openapi/editor/actions/CloneCaretAbove.java index f8b3cf7bc83a..c42cd6533e1c 100644 --- a/platform/platform-impl/src/com/intellij/openapi/editor/actions/CloneCaretAbove.java +++ b/platform/platform-impl/src/com/intellij/openapi/editor/actions/CloneCaretAbove.java @@ -15,31 +15,10 @@ */ package com.intellij.openapi.editor.actions; -import com.intellij.openapi.actionSystem.DataContext; -import com.intellij.openapi.editor.Caret; -import com.intellij.openapi.editor.Editor; import com.intellij.openapi.editor.actionSystem.EditorAction; -import com.intellij.openapi.editor.actionSystem.EditorActionHandler; -import org.jetbrains.annotations.NotNull; public class CloneCaretAbove extends EditorAction { public CloneCaretAbove() { - super(new Handler()); - } - - private static class Handler extends EditorActionHandler { - public Handler() { - super(true); - } - - @Override - public void doExecute(Editor editor, @NotNull Caret caret, DataContext dataContext) { - caret.clone(true); - } - - @Override - public boolean isEnabled(Editor editor, DataContext dataContext) { - return editor.getCaretModel().supportsMultipleCarets(); - } + super(new CloneCaretActionHandler(true)); } } diff --git a/platform/platform-impl/src/com/intellij/openapi/editor/actions/CloneCaretActionHandler.java b/platform/platform-impl/src/com/intellij/openapi/editor/actions/CloneCaretActionHandler.java new file mode 100644 index 000000000000..5c5528a38153 --- /dev/null +++ b/platform/platform-impl/src/com/intellij/openapi/editor/actions/CloneCaretActionHandler.java @@ -0,0 +1,113 @@ +/* + * 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.openapi.editor.actions; + +import com.intellij.openapi.actionSystem.DataContext; +import com.intellij.openapi.actionSystem.IdeActions; +import com.intellij.openapi.editor.Caret; +import com.intellij.openapi.editor.Editor; +import com.intellij.openapi.editor.EditorLastActionTracker; +import com.intellij.openapi.editor.ScrollType; +import com.intellij.openapi.editor.actionSystem.EditorActionHandler; +import com.intellij.openapi.util.Key; +import com.intellij.util.containers.HashSet; +import org.jetbrains.annotations.Nullable; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Set; + +public class CloneCaretActionHandler extends EditorActionHandler { + private static final Key<Integer> LEVEL = Key.create("CloneCaretActionHandler.level"); + + private static final Set<String> OUR_ACTIONS = new HashSet<String>(Arrays.asList( + IdeActions.ACTION_EDITOR_CLONE_CARET_ABOVE, + IdeActions.ACTION_EDITOR_CLONE_CARET_BELOW, + IdeActions.ACTION_EDITOR_MOVE_CARET_LEFT_WITH_SELECTION, + IdeActions.ACTION_EDITOR_MOVE_CARET_RIGHT_WITH_SELECTION + )); + + private final boolean myCloneAbove; + + public CloneCaretActionHandler(boolean above) { + myCloneAbove = above; + } + + @Override + public boolean isEnabled(Editor editor, DataContext dataContext) { + return editor.getCaretModel().supportsMultipleCarets(); + } + + @Override + protected void doExecute(Editor editor, @Nullable Caret targetCaret, DataContext dataContext) { + if (targetCaret != null) { + targetCaret.clone(myCloneAbove); + return; + } + int currentLevel = 0; + List<Caret> currentCarets = new ArrayList<Caret>(); + for (Caret caret : editor.getCaretModel().getAllCarets()) { + int level = getLevel(caret); + if (Math.abs(level) > Math.abs(currentLevel)) { + currentLevel = level; + currentCarets.clear(); + } + if (Math.abs(level) == Math.abs(currentLevel)) { + currentCarets.add(caret); + } + } + boolean removeCarets = currentLevel > 0 && myCloneAbove || currentLevel < 0 && !myCloneAbove; + Integer newLevel = myCloneAbove ? currentLevel - 1 : currentLevel + 1; + for (Caret caret : currentCarets) { + if (removeCarets) { + editor.getCaretModel().removeCaret(caret); + } + else { + Caret clone = caret; + do { + Caret original = clone; + clone = clone.clone(myCloneAbove); + if (original != caret) { + editor.getCaretModel().removeCaret(original); + } + } while (clone != null && caret.hasSelection() && !clone.hasSelection()); + if (clone != null) { + clone.putUserData(LEVEL, newLevel); + } + } + } + if (removeCarets) { + editor.getScrollingModel().scrollToCaret(ScrollType.RELATIVE); + } + } + + private static int getLevel(Caret caret) { + if (isRepeatedActionInvocation()) { + Integer value = caret.getUserData(LEVEL); + return value == null ? 0 : value; + } + else { + caret.putUserData(LEVEL, null); + return 0; + } + } + + private static boolean isRepeatedActionInvocation() { + String lastActionId = EditorLastActionTracker.getInstance().getLastActionId(); + return OUR_ACTIONS.contains(lastActionId); + } +} diff --git a/platform/platform-impl/src/com/intellij/openapi/editor/actions/CloneCaretBelow.java b/platform/platform-impl/src/com/intellij/openapi/editor/actions/CloneCaretBelow.java index 17d17708b900..974087796e08 100644 --- a/platform/platform-impl/src/com/intellij/openapi/editor/actions/CloneCaretBelow.java +++ b/platform/platform-impl/src/com/intellij/openapi/editor/actions/CloneCaretBelow.java @@ -15,35 +15,10 @@ */ package com.intellij.openapi.editor.actions; -import com.intellij.openapi.actionSystem.DataContext; -import com.intellij.openapi.editor.Caret; -import com.intellij.openapi.editor.CaretModel; -import com.intellij.openapi.editor.Editor; import com.intellij.openapi.editor.actionSystem.EditorAction; -import com.intellij.openapi.editor.actionSystem.EditorActionHandler; -import org.jetbrains.annotations.NotNull; public class CloneCaretBelow extends EditorAction { public CloneCaretBelow() { - super(new Handler()); - } - - private static class Handler extends EditorActionHandler { - public Handler() { - super(true); - } - - @Override - public void doExecute(Editor editor, @NotNull Caret caret, DataContext dataContext) { - CaretModel caretModel = editor.getCaretModel(); - if (caretModel.supportsMultipleCarets()) { - caret.clone(false); - } - } - - @Override - public boolean isEnabled(Editor editor, DataContext dataContext) { - return editor.getCaretModel().supportsMultipleCarets(); - } + super(new CloneCaretActionHandler(false)); } } diff --git a/platform/platform-impl/src/com/intellij/openapi/editor/impl/CaretImpl.java b/platform/platform-impl/src/com/intellij/openapi/editor/impl/CaretImpl.java index cfd039a5d504..c769f92fff78 100644 --- a/platform/platform-impl/src/com/intellij/openapi/editor/impl/CaretImpl.java +++ b/platform/platform-impl/src/com/intellij/openapi/editor/impl/CaretImpl.java @@ -62,7 +62,13 @@ public class CaretImpl extends UserDataHolderBase implements Caret { private int myVisualLineEnd; private RangeMarker savedBeforeBulkCaretMarker; private boolean mySkipChangeRequests; + /** + * Initial horizontal caret position during vertical navigation. + * Similar to {@link #myDesiredX}, but represents logical caret position (<code>getLogicalPosition().column</code>) rather than visual. + */ private int myLastColumnNumber = 0; + private int myDesiredSelectionStartColumn = -1; + private int myDesiredSelectionEndColumn = -1; /** * We check that caret is located at the target offset at the end of {@link #moveToOffset(int, boolean)} method. However, * it's possible that the following situation occurs: @@ -82,11 +88,10 @@ public class CaretImpl extends UserDataHolderBase implements Caret { */ private boolean myReportCaretMoves; /** - * There is a possible case that user defined non-monospaced font for editor. That means that various symbols have different - * visual widths. That means that if we move caret vertically it may deviate to the left/right. However, we can try to preserve - * its initial visual position when possible. + * This field holds initial horizontal caret position during vertical navigation. It's used to determine target position when + * moving to the new line. It is stored in pixels, not in columns, to account for non-monospaced fonts as well. * <p/> - * This field holds desired value for visual <code>'x'</code> caret coordinate (negative value if no coordinate should be preserved). + * Negative value means no coordinate should be preserved. */ private int myDesiredX = -1; @@ -255,10 +260,11 @@ public class CaretImpl extends UserDataHolderBase implements Caret { EditorSettings editorSettings = myEditor.getSettings(); VisualPosition visualCaret = getVisualPosition(); + int lastColumnNumber = myLastColumnNumber; int desiredX = myDesiredX; if (columnShift == 0) { if (myDesiredX < 0) { - desiredX = myEditor.visualPositionToXY(visualCaret).x; + desiredX = getCurrentX(); } } else { @@ -267,15 +273,12 @@ public class CaretImpl extends UserDataHolderBase implements Caret { int newLineNumber = visualCaret.line + lineShift; int newColumnNumber = visualCaret.column + columnShift; - if (desiredX >= 0 && !ApplicationManager.getApplication().isUnitTestMode()) { + if (desiredX >= 0) { newColumnNumber = myEditor.xyToVisualPosition(new Point(desiredX, Math.max(0, newLineNumber) * myEditor.getLineHeight())).column; } Document document = myEditor.getDocument(); - if (!editorSettings.isVirtualSpace() && columnShift == 0 && getLogicalPosition().softWrapLinesOnCurrentLogicalLine <= 0) { - newColumnNumber = supportsMultipleCarets() ? myLastColumnNumber : myEditor.getLastColumnNumber(); - } - else if (!editorSettings.isVirtualSpace() && lineShift == 0 && columnShift == 1) { + if (!editorSettings.isVirtualSpace() && lineShift == 0 && columnShift == 1) { int lastLine = document.getLineCount() - 1; if (lastLine < 0) lastLine = 0; if (EditorModificationUtil.calcAfterLineEnd(myEditor) >= 0 && @@ -303,18 +306,21 @@ public class CaretImpl extends UserDataHolderBase implements Caret { // We want to move caret to the first column if it's already located at the first line and 'Up' is pressed. newColumnNumber = 0; desiredX = -1; + lastColumnNumber = -1; } VisualPosition pos = new VisualPosition(newLineNumber, newColumnNumber); - int lastColumnNumber = newColumnNumber; if (!myEditor.getSoftWrapModel().isInsideSoftWrap(pos)) { LogicalPosition log = myEditor.visualToLogicalPosition(new VisualPosition(newLineNumber, newColumnNumber)); int offset = myEditor.logicalPositionToOffset(log); if (offset >= document.getTextLength()) { int lastOffsetColumn = myEditor.offsetToVisualPosition(document.getTextLength()).column; // We want to move caret to the last column if if it's located at the last line and 'Down' is pressed. - newColumnNumber = lastColumnNumber = Math.max(lastOffsetColumn, newColumnNumber); - desiredX = -1; + if (lastOffsetColumn > newColumnNumber) { + newColumnNumber = lastOffsetColumn; + desiredX = -1; + lastColumnNumber = -1; + } } if (!editorSettings.isCaretInsideTabs()) { CharSequence text = document.getCharsSequence(); @@ -353,7 +359,7 @@ public class CaretImpl extends UserDataHolderBase implements Caret { } else { moveToVisualPosition(pos); - if (!editorSettings.isVirtualSpace() && columnShift == 0) { + if (!editorSettings.isVirtualSpace() && columnShift == 0 && lastColumnNumber >=0) { setLastColumnNumber(lastColumnNumber); } } @@ -550,6 +556,7 @@ public class CaretImpl extends UserDataHolderBase implements Caret { } setLastColumnNumber(myLogicalCaret.column); + myDesiredSelectionStartColumn = myDesiredSelectionEndColumn = -1; myVisibleCaret = myEditor.logicalToVisualPosition(myLogicalCaret); updateOffsetsFromLogicalPosition(); @@ -705,7 +712,8 @@ public class CaretImpl extends UserDataHolderBase implements Caret { myEditor.getFoldingModel().flushCaretPosition(); - setLastColumnNumber(column); + setLastColumnNumber(myLogicalCaret.column); + myDesiredSelectionStartColumn = myDesiredSelectionEndColumn = -1; myEditor.updateCaretCursor(); requestRepaint(oldInfo); @@ -962,6 +970,8 @@ public class CaretImpl extends UserDataHolderBase implements Caret { clone.myLastColumnNumber = this.myLastColumnNumber; clone.myReportCaretMoves = this.myReportCaretMoves; clone.myDesiredX = this.myDesiredX; + clone.myDesiredSelectionStartColumn = -1; + clone.myDesiredSelectionEndColumn = -1; return clone; } @@ -971,10 +981,10 @@ public class CaretImpl extends UserDataHolderBase implements Caret { assertIsDispatchThread(); int lineShift = above ? -1 : 1; final CaretImpl clone = cloneWithoutSelection(); - final int newSelectionStartOffset, newSelectionEndOffset; + final int newSelectionStartOffset, newSelectionEndOffset, newSelectionStartColumn, newSelectionEndColumn; final VisualPosition newSelectionStartPosition, newSelectionEndPosition; final boolean hasNewSelection; - if (hasSelection()) { + if (hasSelection() || myDesiredSelectionStartColumn >=0 || myDesiredSelectionEndColumn >= 0) { VisualPosition startPosition = getSelectionStartPosition(); VisualPosition endPosition = getSelectionEndPosition(); VisualPosition leadPosition = getLeadSelectionPosition(); @@ -982,8 +992,10 @@ public class CaretImpl extends UserDataHolderBase implements Caret { boolean leadIsEnd = leadPosition.equals(endPosition); LogicalPosition selectionStart = myEditor.visualToLogicalPosition(leadIsStart || leadIsEnd ? leadPosition : startPosition); LogicalPosition selectionEnd = myEditor.visualToLogicalPosition(leadIsEnd ? startPosition : endPosition); - LogicalPosition newSelectionStart = truncate(new LogicalPosition(selectionStart.line + lineShift, selectionStart.column)); - LogicalPosition newSelectionEnd = truncate(new LogicalPosition(selectionEnd.line + lineShift, selectionEnd.column)); + newSelectionStartColumn = myDesiredSelectionStartColumn < 0 ? selectionStart.column : myDesiredSelectionStartColumn; + newSelectionEndColumn = myDesiredSelectionEndColumn < 0 ? selectionEnd.column : myDesiredSelectionEndColumn; + LogicalPosition newSelectionStart = truncate(selectionStart.line + lineShift, newSelectionStartColumn); + LogicalPosition newSelectionEnd = truncate(selectionEnd.line + lineShift, newSelectionEndColumn); newSelectionStartOffset = myEditor.logicalPositionToOffset(newSelectionStart); newSelectionEndOffset = myEditor.logicalPositionToOffset(newSelectionEnd); newSelectionStartPosition = myEditor.logicalToVisualPosition(newSelectionStart); @@ -996,6 +1008,8 @@ public class CaretImpl extends UserDataHolderBase implements Caret { newSelectionStartPosition = null; newSelectionEndPosition = null; hasNewSelection = false; + newSelectionStartColumn = -1; + newSelectionEndColumn = -1; } LogicalPosition oldPosition = getLogicalPosition(); int newLine = oldPosition.line + lineShift; @@ -1003,7 +1017,12 @@ public class CaretImpl extends UserDataHolderBase implements Caret { Disposer.dispose(clone); return null; } - clone.moveToLogicalPosition(new LogicalPosition(newLine, oldPosition.column), false, null, false); + clone.moveToLogicalPosition(new LogicalPosition(newLine, myLastColumnNumber), false, null, false); + clone.myLastColumnNumber = myLastColumnNumber; + clone.myDesiredX = myDesiredX >= 0 ? myDesiredX : getCurrentX(); + clone.myDesiredSelectionStartColumn = newSelectionStartColumn; + clone.myDesiredSelectionEndColumn = newSelectionEndColumn; + if (myEditor.getCaretModel().addCaret(clone)) { if (hasNewSelection) { myEditor.getCaretModel().doWithCaretMerging(new Runnable() { @@ -1025,15 +1044,15 @@ public class CaretImpl extends UserDataHolderBase implements Caret { } } - private LogicalPosition truncate(LogicalPosition position) { - if (position.line < 0) { + private LogicalPosition truncate(int line, int column) { + if (line < 0) { return new LogicalPosition(0, 0); } - else if (position.line >= myEditor.getDocument().getLineCount()) { + else if (line >= myEditor.getDocument().getLineCount()) { return myEditor.offsetToLogicalPosition(myEditor.getDocument().getTextLength()); } else { - return position; + return new LogicalPosition(line, column); } } @@ -1396,9 +1415,8 @@ public class CaretImpl extends UserDataHolderBase implements Caret { } try { - EditorActionHandler handler = EditorActionManager.getInstance().getActionHandler( - IdeActions.ACTION_EDITOR_SELECT_WORD_AT_CARET); - handler.execute(myEditor, myEditor.getDataContext()); + EditorActionHandler handler = EditorActionManager.getInstance().getActionHandler(IdeActions.ACTION_EDITOR_SELECT_WORD_AT_CARET); + handler.execute(myEditor, CaretImpl.this, myEditor.getDataContext()); } finally { if (needOverrideSetting) { @@ -1453,6 +1471,10 @@ public class CaretImpl extends UserDataHolderBase implements Caret { return marker != null && marker.isValid() && isVirtualSelectionEnabled() && myEndVirtualOffset > myStartVirtualOffset; } + private int getCurrentX() { + return myEditor.visualPositionToXY(myVisibleCaret).x; + } + @Override @NotNull public EditorImpl getEditor() { diff --git a/platform/platform-impl/src/com/intellij/openapi/editor/impl/EditorCopyPasteHelperImpl.java b/platform/platform-impl/src/com/intellij/openapi/editor/impl/EditorCopyPasteHelperImpl.java new file mode 100644 index 000000000000..c26c7e99ba63 --- /dev/null +++ b/platform/platform-impl/src/com/intellij/openapi/editor/impl/EditorCopyPasteHelperImpl.java @@ -0,0 +1,150 @@ +/* + * 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.openapi.editor.impl; + +import com.intellij.codeInsight.editorActions.TextBlockTransferable; +import com.intellij.codeInsight.editorActions.TextBlockTransferableData; +import com.intellij.openapi.application.ApplicationManager; +import com.intellij.openapi.diagnostic.Logger; +import com.intellij.openapi.editor.*; +import com.intellij.openapi.ide.CopyPasteManager; +import com.intellij.openapi.util.TextRange; +import com.intellij.openapi.util.text.LineTokenizer; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.awt.datatransfer.DataFlavor; +import java.awt.datatransfer.StringSelection; +import java.awt.datatransfer.Transferable; +import java.awt.datatransfer.UnsupportedFlavorException; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Iterator; +import java.util.List; + +public class EditorCopyPasteHelperImpl extends EditorCopyPasteHelper { + private static final Logger LOG = Logger.getInstance(EditorCopyPasteHelperImpl.class); + + @Override + public void copySelectionToClipboard(@NotNull Editor editor) { + ApplicationManager.getApplication().assertIsDispatchThread(); + List<TextBlockTransferableData> extraData = new ArrayList<TextBlockTransferableData>(); + String s = editor.getCaretModel().supportsMultipleCarets() ? getSelectedTextForClipboard(editor, extraData) + : editor.getSelectionModel().getSelectedText(); + if (s == null) return; + + s = TextBlockTransferable.convertLineSeparators(s, "\n", extraData); + Transferable contents = editor.getCaretModel().supportsMultipleCarets() ? new TextBlockTransferable(s, extraData, null) : new StringSelection(s); + CopyPasteManager.getInstance().setContents(contents); + } + + public static String getSelectedTextForClipboard(@NotNull Editor editor, @NotNull Collection<TextBlockTransferableData> extraDataCollector) { + final StringBuilder buf = new StringBuilder(); + String separator = ""; + List<Caret> carets = editor.getCaretModel().getAllCarets(); + int[] startOffsets = new int[carets.size()]; + int[] endOffsets = new int[carets.size()]; + for (int i = 0; i < carets.size(); i++) { + buf.append(separator); + String caretSelectedText = carets.get(i).getSelectedText(); + startOffsets[i] = buf.length(); + if (caretSelectedText != null) { + buf.append(caretSelectedText); + } + endOffsets[i] = buf.length(); + separator = "\n"; + } + extraDataCollector.add(new CaretStateTransferableData(startOffsets, endOffsets)); + return buf.toString(); + } + + @Nullable + @Override + public TextRange[] pasteFromClipboard(@NotNull Editor editor) { + CopyPasteManager manager = CopyPasteManager.getInstance(); + if (manager.areDataFlavorsAvailable(DataFlavor.stringFlavor)) { + Transferable clipboardContents = manager.getContents(); + if (clipboardContents != null) { + return pasteTransferable(editor, clipboardContents); + } + } + return null; + } + + @Nullable + @Override + public TextRange[] pasteTransferable(final @NotNull Editor editor, @NotNull Transferable content) { + String text = getStringContent(content); + if (text == null) return null; + + if (editor.getCaretModel().supportsMultipleCarets()) { + int caretCount = editor.getCaretModel().getCaretCount(); + if (caretCount == 1 && editor.isColumnMode()) { + int pastedLineCount = LineTokenizer.calcLineCount(text, true); + EditorModificationUtil.deleteSelectedText(editor); + Caret caret = editor.getCaretModel().getPrimaryCaret(); + for (int i = 0; i < pastedLineCount - 1; i++) { + caret = caret.clone(false); + if (caret == null) { + break; + } + } + caretCount = editor.getCaretModel().getCaretCount(); + } + CaretStateTransferableData caretData = null; + try { + caretData = content.isDataFlavorSupported(CaretStateTransferableData.FLAVOR) + ? (CaretStateTransferableData)content.getTransferData(CaretStateTransferableData.FLAVOR) : null; + } + catch (Exception e) { + LOG.error(e); + } + final TextRange[] ranges = new TextRange[caretCount]; + final Iterator<String> segments = new ClipboardTextPerCaretSplitter().split(text, caretData, caretCount).iterator(); + final int[] index = {0}; + editor.getCaretModel().runForEachCaret(new CaretAction() { + @Override + public void perform(Caret caret) { + String segment = segments.next(); + int caretOffset = caret.getOffset(); + ranges[index[0]++] = new TextRange(caretOffset, caretOffset + segment.length()); + EditorModificationUtil.insertStringAtCaret(editor, segment, false, true); + } + }); + return ranges; + } + else { + int caretOffset = editor.getCaretModel().getOffset(); + EditorModificationUtil.insertStringAtCaret(editor, text, false, true); + return new TextRange[] { new TextRange(caretOffset, caretOffset + text.length())}; + } + } + + @Nullable + private static String getStringContent(@NotNull Transferable content) { + RawText raw = RawText.fromTransferable(content); + if (raw != null) return raw.rawText; + + try { + return (String)content.getTransferData(DataFlavor.stringFlavor); + } + catch (UnsupportedFlavorException ignore) { } + catch (IOException ignore) { } + + return null; + } +} diff --git a/platform/platform-impl/src/com/intellij/openapi/editor/impl/EditorGutterComponentImpl.java b/platform/platform-impl/src/com/intellij/openapi/editor/impl/EditorGutterComponentImpl.java index f600c606a98d..39706f016adc 100644 --- a/platform/platform-impl/src/com/intellij/openapi/editor/impl/EditorGutterComponentImpl.java +++ b/platform/platform-impl/src/com/intellij/openapi/editor/impl/EditorGutterComponentImpl.java @@ -992,7 +992,16 @@ class EditorGutterComponentImpl extends EditorGutterComponentEx implements Mouse } public void setLineNumberAreaWidth(@NotNull TIntFunction calculator) { - final int lineNumberAreaWidth = calculator.execute(myLineNumberConvertor.execute(endLineNumber())); + int maxLineNumber = 0; + for (int i = endLineNumber(); i >= 0; i--) { + int number = myLineNumberConvertor.execute(i); + if (number >= 0) { + maxLineNumber = number; + break; + } + } + + final int lineNumberAreaWidth = calculator.execute(maxLineNumber); if (myLineNumberAreaWidth != lineNumberAreaWidth) { myLineNumberAreaWidth = lineNumberAreaWidth; fireResized(); diff --git a/platform/platform-impl/src/com/intellij/openapi/editor/impl/EditorHighlighterCache.java b/platform/platform-impl/src/com/intellij/openapi/editor/impl/EditorHighlighterCache.java index c1499a71a71b..8be2284e8360 100644 --- a/platform/platform-impl/src/com/intellij/openapi/editor/impl/EditorHighlighterCache.java +++ b/platform/platform-impl/src/com/intellij/openapi/editor/impl/EditorHighlighterCache.java @@ -15,10 +15,19 @@ */ package com.intellij.openapi.editor.impl; +import com.intellij.lexer.Lexer; import com.intellij.openapi.editor.Document; import com.intellij.openapi.editor.ex.util.LexerEditorHighlighter; import com.intellij.openapi.editor.highlighter.EditorHighlighter; +import com.intellij.openapi.editor.highlighter.EditorHighlighterFactory; +import com.intellij.openapi.project.Project; import com.intellij.openapi.util.Key; +import com.intellij.openapi.vfs.VirtualFile; +import com.intellij.psi.PsiDocumentManager; +import com.intellij.psi.PsiFile; +import com.intellij.psi.PsiManager; +import com.intellij.psi.impl.cache.impl.id.PlatformIdTableBuilding; +import com.intellij.psi.impl.search.LexerEditorHighlighterLexer; import com.intellij.reference.SoftReference; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -55,4 +64,28 @@ public class EditorHighlighterCache { return null; } + @Nullable + public static Lexer getLexerBasedOnLexerHighlighter(CharSequence text, VirtualFile virtualFile, Project project) { + EditorHighlighter highlighter = null; + + PsiFile psiFile = virtualFile != null ? PsiManager.getInstance(project).findFile(virtualFile) : null; + final Document document = psiFile != null ? PsiDocumentManager.getInstance(project).getDocument(psiFile) : null; + final EditorHighlighter cachedEditorHighlighter; + boolean alreadyInitializedHighlighter = false; + + if (document != null && + (cachedEditorHighlighter = getEditorHighlighterForCachesBuilding(document)) != null && + PlatformIdTableBuilding.checkCanUseCachedEditorHighlighter(text, cachedEditorHighlighter)) { + highlighter = cachedEditorHighlighter; + alreadyInitializedHighlighter = true; + } + else if (virtualFile != null) { + highlighter = EditorHighlighterFactory.getInstance().createEditorHighlighter(project, virtualFile); + } + + if (highlighter != null) { + return new LexerEditorHighlighterLexer(highlighter, alreadyInitializedHighlighter); + } + return null; + } } diff --git a/platform/platform-impl/src/com/intellij/openapi/editor/impl/EditorImpl.java b/platform/platform-impl/src/com/intellij/openapi/editor/impl/EditorImpl.java index b708f38a9190..c0ffbd10aca4 100644 --- a/platform/platform-impl/src/com/intellij/openapi/editor/impl/EditorImpl.java +++ b/platform/platform-impl/src/com/intellij/openapi/editor/impl/EditorImpl.java @@ -33,7 +33,7 @@ import com.intellij.openapi.actionSystem.ex.ActionManagerEx; import com.intellij.openapi.actionSystem.impl.MouseGestureManager; import com.intellij.openapi.application.ApplicationManager; import com.intellij.openapi.application.ModalityState; -import com.intellij.openapi.application.ex.ApplicationManagerEx; +import com.intellij.openapi.application.impl.LaterInvocator; import com.intellij.openapi.command.CommandProcessor; import com.intellij.openapi.command.UndoConfirmationPolicy; import com.intellij.openapi.diagnostic.Logger; @@ -147,7 +147,6 @@ public final class EditorImpl extends UserDataHolderBase implements EditorEx, Hi @NotNull private final EditorComponentImpl myEditorComponent; @NotNull private final EditorGutterComponentImpl myGutterComponent; private final TraceableDisposable myTraceableDisposable = new TraceableDisposable(new Throwable()); - private volatile boolean hasTabs; // optimisation flag: when editor contains no tabs it is dramatically easier to calculate positions static { ComplementaryFontsRegistry.getFontAbleToDisplay(' ', 0, 0, UIManager.getFont("Label.font").getFamily()); // load costly font info @@ -168,6 +167,7 @@ public final class EditorImpl extends UserDataHolderBase implements EditorEx, Hi @NotNull private final CaretCursor myCaretCursor; private final ScrollingTimer myScrollingTimer = new ScrollingTimer(); + @SuppressWarnings("RedundantStringConstructorCall") private final Object MOUSE_DRAGGED_GROUP = new String("MouseDraggedGroup"); @NotNull private final SettingsImpl mySettings; @@ -246,8 +246,6 @@ public final class EditorImpl extends UserDataHolderBase implements EditorEx, Hi @Nullable private Color myForcedBackground = null; @Nullable private Dimension myPreferredSize; private int myVirtualPageHeight; - @Nullable private Runnable myGutterSizeUpdater = null; - private boolean myGutterNeedsUpdate = false; private Alarm myAppleRepaintAlarm; private final Alarm myMouseSelectionStateAlarm = new Alarm(Alarm.ThreadToUse.SWING_THREAD); @@ -317,8 +315,12 @@ public final class EditorImpl extends UserDataHolderBase implements EditorEx, Hi } EditorImpl(@NotNull Document document, boolean viewer, @Nullable Project project) { + assertIsDispatchThread(); myProject = project; myDocument = (DocumentEx)document; + if (myDocument instanceof DocumentImpl) { + ((DocumentImpl)myDocument).requestTabTracking(); + } myScheme = createBoundColorSchemeDelegate(null); initTabPainter(); myIsViewer = viewer; @@ -433,13 +435,13 @@ public final class EditorImpl extends UserDataHolderBase implements EditorEx, Hi if (myPrimaryCaret != null) { myPrimaryCaret.updateVisualPosition(); // repainting old primary caret's row background } - ((CaretImpl)e.getCaret()).updateVisualPosition(); // repainting caret region + updateCaretVisualPosition(e); myPrimaryCaret = myCaretModel.getPrimaryCaret(); } @Override public void caretRemoved(CaretEvent e) { - ((CaretImpl)e.getCaret()).updateVisualPosition(); // repainting caret region + updateCaretVisualPosition(e); myPrimaryCaret = myCaretModel.getPrimaryCaret(); // repainting new primary caret's row background myPrimaryCaret.updateVisualPosition(); } @@ -548,7 +550,13 @@ public final class EditorImpl extends UserDataHolderBase implements EditorEx, Hi } }); } - updateHasTabsFlag(document.getImmutableCharSequence()); + } + + private static void updateCaretVisualPosition(CaretEvent e) { + CaretImpl caretImpl = ((CaretImpl)e.getCaret()); + if (caretImpl != null) { + caretImpl.updateVisualPosition(); // repainting caret region + } } @NotNull @@ -575,7 +583,10 @@ public final class EditorImpl extends UserDataHolderBase implements EditorEx, Hi myPrefixWidthInPixels = 0; if (myPrefixText != null) { for (char c : myPrefixText) { - myPrefixWidthInPixels += EditorUtil.charWidth(c, myPrefixAttributes.getFontType(), this); + LOG.assertTrue(myPrefixAttributes != null); + if (myPrefixAttributes != null) { + myPrefixWidthInPixels += EditorUtil.charWidth(c, myPrefixAttributes.getFontType(), this); + } } } mySoftWrapModel.recalculate(); @@ -791,6 +802,9 @@ public final class EditorImpl extends UserDataHolderBase implements EditorEx, Hi if (myConnection != null) { myConnection.disconnect(); } + if (myDocument instanceof DocumentImpl) { + ((DocumentImpl)myDocument).giveUpTabTracking(); + } Disposer.dispose(myDisposable); } @@ -898,9 +912,7 @@ public final class EditorImpl extends UserDataHolderBase implements EditorEx, Hi new UiNotifyConnector(myEditorComponent, new Activatable.Adapter(){ @Override public void showNotify() { - if (myGutterNeedsUpdate) { - updateGutterSize(); - } + myGutterComponent.updateSize(); } }); @@ -1505,7 +1517,8 @@ public final class EditorImpl extends UserDataHolderBase implements EditorEx, Hi private final int[] myLastXOffsets = new int[myLastStartOffsets.length]; private final int[] myLastXs = new int[myLastStartOffsets.length]; private int myCurrentCachePosition; - private int myLastCacheHits, myTotalRequests; // todo remove + private int myLastCacheHits; + private int myTotalRequests; // todo remove private int getTabbedTextWidth(int startOffset, int targetColumn, int xOffset) { int x = xOffset; @@ -1621,7 +1634,7 @@ public final class EditorImpl extends UserDataHolderBase implements EditorEx, Hi @Override public void repaint(final int startOffset, int endOffset) { - if (!isShowing() || myScrollPane == null || myDocument.isInBulkUpdate()) { + if (!isShowing() || myDocument.isInBulkUpdate()) { return; } @@ -1638,7 +1651,7 @@ public final class EditorImpl extends UserDataHolderBase implements EditorEx, Hi } private boolean isShowing() { - return myGutterComponent != null && myGutterComponent.isShowing(); + return myGutterComponent.isShowing(); } private void repaintToScreenBottom(int startLine) { @@ -1679,9 +1692,6 @@ public final class EditorImpl extends UserDataHolderBase implements EditorEx, Hi } private void bulkUpdateFinished() { - if (myScrollPane == null) { - return; - } clearTextWidthCache(); @@ -1700,7 +1710,7 @@ public final class EditorImpl extends UserDataHolderBase implements EditorEx, Hi if (isStickySelection()) { setStickySelection(false); } - if (myDocument.isInBulkUpdate() || myScrollingModel == null) { + if (myDocument.isInBulkUpdate()) { // Assuming that the job is done at bulk listener callback methods. return; } @@ -1716,7 +1726,7 @@ public final class EditorImpl extends UserDataHolderBase implements EditorEx, Hi } private void changedUpdate(DocumentEvent e) { - if (myScrollPane == null || myDocument.isInBulkUpdate()) return; + if (myDocument.isInBulkUpdate()) return; clearTextWidthCache(); mySelectionModel.removeBlockSelection(); @@ -1754,16 +1764,10 @@ public final class EditorImpl extends UserDataHolderBase implements EditorEx, Hi Point caretLocation = visualPositionToXY(getCaretModel().getVisualPosition()); int scrollOffset = caretLocation.y - myCaretUpdateVShift; getScrollingModel().scrollVertically(scrollOffset); - updateHasTabsFlag(e.getNewFragment()); } - private void updateHasTabsFlag(@NotNull CharSequence newChars) { - if (!hasTabs) { - hasTabs = StringUtil.contains(newChars, 0, newChars.length(), '\t'); - } - } public boolean hasTabs() { - return hasTabs; + return !(myDocument instanceof DocumentImpl) || ((DocumentImpl)myDocument).mightContainTabs(); } public boolean isScrollToCaret() { @@ -1784,24 +1788,12 @@ public final class EditorImpl extends UserDataHolderBase implements EditorEx, Hi } private void updateGutterSize() { - if (myGutterSizeUpdater != null) return; - myGutterSizeUpdater = new Runnable() { + LaterInvocator.invokeLater(new Runnable() { @Override public void run() { - if (!isDisposed()) { - if (isShowing()) { - myGutterComponent.updateSize(); - myGutterNeedsUpdate = false; - } - else { - myGutterNeedsUpdate = true; - } - } - myGutterSizeUpdater = null; + myGutterComponent.updateSize(); } - }; - - SwingUtilities.invokeLater(myGutterSizeUpdater); + }); } void validateSize() { @@ -1945,7 +1937,7 @@ public final class EditorImpl extends UserDataHolderBase implements EditorEx, Hi } if (isReleased) { - g.setColor(new Color(128, 255, 128)); + g.setColor(new JBColor(new Color(128, 255, 128), new Color(128, 255, 128))); g.fillRect(clip.x, clip.y, clip.width, clip.height); return; } @@ -2239,7 +2231,7 @@ public final class EditorImpl extends UserDataHolderBase implements EditorEx, Hi myLastBackgroundColor = null; int start = clipStartOffset; - int end = clipEndOffset; + if (!myPurePaintingMode) { getSoftWrapModel().registerSoftWrapsIfNecessary(); } @@ -2250,7 +2242,7 @@ public final class EditorImpl extends UserDataHolderBase implements EditorEx, Hi return; } - IterationState iterationState = new IterationState(this, start, end, isPaintSelection()); + IterationState iterationState = new IterationState(this, start, clipEndOffset, isPaintSelection()); TextAttributes attributes = iterationState.getMergedAttributes(); Color backColor = getBackgroundColor(attributes); int fontType = attributes.getFontType(); @@ -2322,7 +2314,7 @@ public final class EditorImpl extends UserDataHolderBase implements EditorEx, Hi position.y += lineHeight; start = lEnd; } - else if (collapsedFolderAt.getEndOffset() == end) { + else if (collapsedFolderAt.getEndOffset() == clipEndOffset) { softWrap = mySoftWrapModel.getSoftWrap(collapsedFolderAt.getStartOffset()); if (softWrap != null) { position.x = drawSoftWrapAwareBackground( @@ -3677,19 +3669,16 @@ public final class EditorImpl extends UserDataHolderBase implements EditorEx, Hi return logicalToVisualPosition(logicalPos, true); } - // TODO den remove as soon as the problem is fixed. - private final ThreadLocal<Integer> stackDepth = new ThreadLocal<Integer>(); - - // TODO den remove as soon as the problem is fixed. @Override @NotNull public VisualPosition logicalToVisualPosition(@NotNull LogicalPosition logicalPos, boolean softWrapAware) { - stackDepth.set(0); - return doLogicalToVisualPosition(logicalPos, softWrapAware); + return doLogicalToVisualPosition(logicalPos, softWrapAware,0); } @NotNull - private VisualPosition doLogicalToVisualPosition(@NotNull LogicalPosition logicalPos, boolean softWrapAware) { + private VisualPosition doLogicalToVisualPosition(@NotNull LogicalPosition logicalPos, boolean softWrapAware, + // TODO den remove as soon as the problem is fixed. + int stackDepth) { assertReadAccess(); if (!myFoldingModel.isFoldingEnabled() && !mySoftWrapModel.isSoftWrappingEnabled()) { return new VisualPosition(logicalPos.line, logicalPos.column); @@ -3703,25 +3692,12 @@ public final class EditorImpl extends UserDataHolderBase implements EditorEx, Hi offset = outermostCollapsed.getStartOffset(); LogicalPosition foldStart = offsetToLogicalPosition(offset); // TODO den remove as soon as the problem is fixed. - Integer depth = stackDepth.get(); - if (depth >= 0) { - stackDepth.set(depth + 1); - if (depth > 15) { - LOG.error("Detected potential StackOverflowError at logical->visual position mapping. Given logical position: '" + - logicalPos + "'. State: " + dumpState()); - stackDepth.set(-1); - } - } - // TODO den remove as soon as the problem is fixed. - try { - return doLogicalToVisualPosition(foldStart, true); - } - finally { - depth = stackDepth.get(); - if (depth > 0) { - stackDepth.set(depth - 1); - } + if (stackDepth > 15) { + LOG.error("Detected potential StackOverflowError at logical->visual position mapping. Given logical position: '" + + logicalPos + "'. State: " + dumpState()); + stackDepth = -1; } + return doLogicalToVisualPosition(foldStart, true, stackDepth+1); } else { offset = outermostCollapsed.getEndOffset() + 3; // WTF? @@ -3988,7 +3964,7 @@ public final class EditorImpl extends UserDataHolderBase implements EditorEx, Hi return false; } - if (e.getComponent() != myInitialMouseEvent.getComponent() || !e.getPoint().equals(myInitialMouseEvent.getPoint())) { + if (myInitialMouseEvent!= null && (e.getComponent() != myInitialMouseEvent.getComponent() || !e.getPoint().equals(myInitialMouseEvent.getPoint()))) { myIgnoreMouseEventsConsecutiveToInitial = false; myInitialMouseEvent = null; return false; @@ -4197,7 +4173,9 @@ public final class EditorImpl extends UserDataHolderBase implements EditorEx, Hi } } else { final LogicalPosition blockStart = selectionModel.hasBlockSelection() ? selectionModel.getBlockStart() : oldLogicalCaret; - setBlockSelectionAndBlockActions(e, blockStart, getCaretModel().getLogicalPosition()); + if (blockStart != null) { + setBlockSelectionAndBlockActions(e, blockStart, getCaretModel().getLogicalPosition()); + } } } else { @@ -5002,7 +4980,7 @@ public final class EditorImpl extends UserDataHolderBase implements EditorEx, Hi } void assertIsDispatchThread() { - ApplicationManagerEx.getApplicationEx().assertIsDispatchThread(); + ApplicationManager.getApplication().assertIsDispatchThread(); } private static void assertReadAccess() { @@ -5392,7 +5370,9 @@ public final class EditorImpl extends UserDataHolderBase implements EditorEx, Hi public void run() { int docLength = doc.getTextLength(); ProperTextRange range = composedTextRange.intersection(new TextRange(0, docLength)); - doc.deleteString(range.getStartOffset(), range.getEndOffset()); + if (range != null) { + doc.deleteString(range.getStartOffset(), range.getEndOffset()); + } } }); } @@ -6745,7 +6725,7 @@ public final class EditorImpl extends UserDataHolderBase implements EditorEx, Hi private class TablessBorder extends SideBorder { private TablessBorder() { - super(UIUtil.getBorderColor(), SideBorder.ALL); + super(JBColor.border(), SideBorder.ALL); } @Override @@ -6769,7 +6749,7 @@ public final class EditorImpl extends UserDataHolderBase implements EditorEx, Hi public Insets getBorderInsets(Component c) { Container splitters = SwingUtilities.getAncestorOfClass(EditorsSplitters.class, c); boolean thereIsSomethingAbove = !SystemInfo.isMac || UISettings.getInstance().SHOW_MAIN_TOOLBAR || UISettings.getInstance().SHOW_NAVIGATION_BAR || - EditorImpl.this.myProject != null && !ToolWindowManagerEx.getInstanceEx(EditorImpl.this.myProject).getIdsOn(ToolWindowAnchor.TOP).isEmpty(); + myProject != null && !ToolWindowManagerEx.getInstanceEx(myProject).getIdsOn(ToolWindowAnchor.TOP).isEmpty(); return splitters == null ? super.getBorderInsets(c) : new Insets(thereIsSomethingAbove ? 1 : 0, 0, 0, 0); } diff --git a/platform/platform-impl/src/com/intellij/openapi/editor/impl/EditorMarkupModelImpl.java b/platform/platform-impl/src/com/intellij/openapi/editor/impl/EditorMarkupModelImpl.java index a2f45fe33857..6ec29e58dc0b 100644 --- a/platform/platform-impl/src/com/intellij/openapi/editor/impl/EditorMarkupModelImpl.java +++ b/platform/platform-impl/src/com/intellij/openapi/editor/impl/EditorMarkupModelImpl.java @@ -36,6 +36,7 @@ import com.intellij.openapi.command.CommandProcessor; import com.intellij.openapi.command.UndoConfirmationPolicy; import com.intellij.openapi.editor.*; import com.intellij.openapi.editor.actionSystem.DocCommandGroupId; +import com.intellij.openapi.editor.colors.EditorColorsManager; import com.intellij.openapi.editor.colors.EditorFontType; import com.intellij.openapi.editor.ex.*; import com.intellij.openapi.editor.markup.ErrorStripeRenderer; @@ -45,7 +46,7 @@ import com.intellij.openapi.ui.MessageType; import com.intellij.openapi.ui.popup.Balloon; import com.intellij.openapi.util.Disposer; import com.intellij.openapi.util.ProperTextRange; -import com.intellij.openapi.util.SystemInfo; +import com.intellij.openapi.util.registry.Registry; import com.intellij.openapi.wm.ToolWindowAnchor; import com.intellij.openapi.wm.ex.ToolWindowManagerEx; import com.intellij.ui.*; @@ -428,7 +429,7 @@ public class EditorMarkupModelImpl extends MarkupModelImpl implements EditorMark } @Override - public void paint(Graphics g) { + public void paint(@NotNull Graphics g) { ((ApplicationImpl)ApplicationManager.getApplication()).editorPaintStart(); final Rectangle bounds = getBounds(); @@ -437,7 +438,7 @@ public class EditorMarkupModelImpl extends MarkupModelImpl implements EditorMark errorIconBounds.y = bounds.height / 2 - errorIconBounds.height / 2; try { - if (UISettings.getInstance().PRESENTATION_MODE || SystemInfo.isMac) { + if (UISettings.getInstance().PRESENTATION_MODE || ButtonlessScrollBarUI.isMacOverlayScrollbarSupported()) { g.setColor(getEditor().getColorsScheme().getDefaultBackground()); g.fillRect(0, 0, bounds.width, bounds.height); @@ -493,7 +494,7 @@ public class EditorMarkupModelImpl extends MarkupModelImpl implements EditorMark } @Override - public void uninstallUI(JComponent c) { + public void uninstallUI(@NotNull JComponent c) { super.uninstallUI(c); myCachedTrack = null; } @@ -528,7 +529,7 @@ public class EditorMarkupModelImpl extends MarkupModelImpl implements EditorMark @Override protected void paintThumb(Graphics g, JComponent c, Rectangle thumbBounds) { - if (UISettings.getInstance().PRESENTATION_MODE || SystemInfo.isMac) { + if (UISettings.getInstance().PRESENTATION_MODE || ButtonlessScrollBarUI.isMacOverlayScrollbarSupported()) { super.paintThumb(g, c, thumbBounds); return; } @@ -565,13 +566,13 @@ public class EditorMarkupModelImpl extends MarkupModelImpl implements EditorMark @Override protected int getThickness() { - if (UISettings.getInstance().PRESENTATION_MODE || SystemInfo.isMac) return super.getThickness(); + if (UISettings.getInstance().PRESENTATION_MODE || ButtonlessScrollBarUI.isMacOverlayScrollbarSupported()) return super.getThickness(); return super.getThickness() + (isMacOverlayScrollbar() ? 2 : 7); } @Override protected void doPaintTrack(Graphics g, JComponent c, Rectangle bounds) { - if (UISettings.getInstance().PRESENTATION_MODE || SystemInfo.isMac) { + if (UISettings.getInstance().PRESENTATION_MODE || ButtonlessScrollBarUI.isMacOverlayScrollbarSupported()) { g.setColor(getEditor().getColorsScheme().getDefaultBackground()); g.fillRect(bounds.x, bounds.y, bounds.width, bounds.height); //return; @@ -609,7 +610,9 @@ public class EditorMarkupModelImpl extends MarkupModelImpl implements EditorMark } private void paintTrackBasement(Graphics g, Rectangle bounds) { - if (UISettings.getInstance().PRESENTATION_MODE || SystemInfo.isMac) { + if (UISettings.getInstance().PRESENTATION_MODE || ButtonlessScrollBarUI.isMacOverlayScrollbarSupported()) { + g.setColor(EditorColorsManager.getInstance().getGlobalScheme().getDefaultBackground()); + g.fillRect(bounds.x, bounds.y, bounds.width, bounds.height); return; } @@ -647,13 +650,13 @@ public class EditorMarkupModelImpl extends MarkupModelImpl implements EditorMark private void drawMarkup(final Graphics g, final int width, int startOffset, int endOffset, MarkupModelEx markup) { final Queue<PositionedStripe> thinEnds = new PriorityQueue<PositionedStripe>(5, new Comparator<PositionedStripe>() { @Override - public int compare(PositionedStripe o1, PositionedStripe o2) { + public int compare(@NotNull PositionedStripe o1, @NotNull PositionedStripe o2) { return o1.yEnd - o2.yEnd; } }); final Queue<PositionedStripe> wideEnds = new PriorityQueue<PositionedStripe>(5, new Comparator<PositionedStripe>() { @Override - public int compare(PositionedStripe o1, PositionedStripe o2) { + public int compare(@NotNull PositionedStripe o1, @NotNull PositionedStripe o2) { return o1.yEnd - o2.yEnd; } }); @@ -757,12 +760,21 @@ public class EditorMarkupModelImpl extends MarkupModelImpl implements EditorMark boolean drawBottomDecoration) { int x = isMirrored() ? 3 : 5; int paintWidth = width; + boolean flatStyle = Registry.is("ide.new.markup.markers"); if (thinErrorStripeMark) { paintWidth /= 2; - paintWidth += 1; + paintWidth += flatStyle ? 0 : 1; x = isMirrored() ? width + 2 : 0; } if (color == null) return; + Color darker = UIUtil.isUnderDarcula()? color : ColorUtil.shift(color, 0.75); + + if (flatStyle) { + g.setColor(darker); + g.fillRect(x, yStart, paintWidth, yEnd - yStart + 1); + return; + } + g.setColor(color); g.fillRect(x + 1, yStart, paintWidth - 2, yEnd - yStart + 1); @@ -774,7 +786,6 @@ public class EditorMarkupModelImpl extends MarkupModelImpl implements EditorMark //top decoration UIUtil.drawLine(g, x + 1, yStart, x + paintWidth - 2, yStart); } - Color darker = ColorUtil.shift(color, 0.75); g.setColor(darker); if (drawBottomDecoration) { @@ -787,7 +798,7 @@ public class EditorMarkupModelImpl extends MarkupModelImpl implements EditorMark // mouse events @Override - public void mouseClicked(final MouseEvent e) { + public void mouseClicked(@NotNull final MouseEvent e) { CommandProcessor.getInstance().executeCommand(myEditor.getProject(), new Runnable() { @Override public void run() { @@ -801,11 +812,11 @@ public class EditorMarkupModelImpl extends MarkupModelImpl implements EditorMark } @Override - public void mousePressed(MouseEvent e) { + public void mousePressed(@NotNull MouseEvent e) { } @Override - public void mouseReleased(MouseEvent e) { + public void mouseReleased(@NotNull MouseEvent e) { } private int getWidth() { @@ -824,7 +835,7 @@ public class EditorMarkupModelImpl extends MarkupModelImpl implements EditorMark } @Override - public void mouseMoved(MouseEvent e) { + public void mouseMoved(@NotNull MouseEvent e) { EditorImpl.MyScrollBar scrollBar = myEditor.getVerticalScrollBar(); int buttonHeight = scrollBar.getDecScrollButtonHeight(); int lineCount = getDocument().getLineCount() + myEditor.getSettings().getAdditionalLinesCount(); @@ -897,16 +908,16 @@ public class EditorMarkupModelImpl extends MarkupModelImpl implements EditorMark } @Override - public void mouseEntered(MouseEvent e) { + public void mouseEntered(@NotNull MouseEvent e) { } @Override - public void mouseExited(MouseEvent e) { + public void mouseExited(@NotNull MouseEvent e) { cancelMyToolTips(e, true); } @Override - public void mouseDragged(MouseEvent e) { + public void mouseDragged(@NotNull MouseEvent e) { cancelMyToolTips(e, true); } @@ -943,13 +954,15 @@ public class EditorMarkupModelImpl extends MarkupModelImpl implements EditorMark } public void markDirtied(@NotNull ProperTextRange yPositions) { - int start = Math.max(0, yPositions.getStartOffset() - myEditor.getLineHeight()); - int end = myEditorScrollbarTop + myEditorTargetHeight == 0 ? yPositions.getEndOffset() + myEditor.getLineHeight() - : Math - .min(myEditorScrollbarTop + myEditorTargetHeight, yPositions.getEndOffset() + myEditor.getLineHeight()); - ProperTextRange adj = new ProperTextRange(start, Math.max(end, start)); + if (myDirtyYPositions != WHOLE_DOCUMENT) { + int start = Math.max(0, yPositions.getStartOffset() - myEditor.getLineHeight()); + int end = myEditorScrollbarTop + myEditorTargetHeight == 0 ? yPositions.getEndOffset() + myEditor.getLineHeight() + : Math + .min(myEditorScrollbarTop + myEditorTargetHeight, yPositions.getEndOffset() + myEditor.getLineHeight()); + ProperTextRange adj = new ProperTextRange(start, Math.max(end, start)); - myDirtyYPositions = myDirtyYPositions == null ? adj : myDirtyYPositions.union(adj); + myDirtyYPositions = myDirtyYPositions == null ? adj : myDirtyYPositions.union(adj); + } myEditorScrollbarTop = 0; myEditorSourceHeight = 0; @@ -1137,7 +1150,7 @@ public class EditorMarkupModelImpl extends MarkupModelImpl implements EditorMark myHighlighters.add(rangeHighlighter); } Collections.sort(myHighlighters, new Comparator<RangeHighlighterEx>() { - public int compare(RangeHighlighterEx ex1, RangeHighlighterEx ex2) { + public int compare(@NotNull RangeHighlighterEx ex1, @NotNull RangeHighlighterEx ex2) { LogicalPosition startPos1 = myEditor.offsetToLogicalPosition(ex1.getAffectedAreaStartOffset()); LogicalPosition startPos2 = myEditor.offsetToLogicalPosition(ex2.getAffectedAreaStartOffset()); if (startPos1.line != startPos2.line) return 0; @@ -1167,7 +1180,7 @@ public class EditorMarkupModelImpl extends MarkupModelImpl implements EditorMark } @Override - protected void paintComponent(Graphics g) { + protected void paintComponent(@NotNull Graphics g) { if (myVisualLine ==-1) return; Dimension size = getPreferredSize(); EditorGutterComponentEx gutterComponentEx = myEditor.getGutterComponentEx(); diff --git a/platform/platform-impl/src/com/intellij/openapi/editor/impl/LazyRangeMarkerFactoryImpl.java b/platform/platform-impl/src/com/intellij/openapi/editor/impl/LazyRangeMarkerFactoryImpl.java new file mode 100644 index 000000000000..5a9c704173aa --- /dev/null +++ b/platform/platform-impl/src/com/intellij/openapi/editor/impl/LazyRangeMarkerFactoryImpl.java @@ -0,0 +1,323 @@ +/* + * 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.openapi.editor.impl; + +import com.intellij.codeStyle.CodeStyleFacade; +import com.intellij.openapi.application.ApplicationManager; +import com.intellij.openapi.editor.Document; +import com.intellij.openapi.editor.EditorFactory; +import com.intellij.openapi.editor.LazyRangeMarkerFactory; +import com.intellij.openapi.editor.RangeMarker; +import com.intellij.openapi.editor.event.DocumentAdapter; +import com.intellij.openapi.editor.event.DocumentEvent; +import com.intellij.openapi.fileEditor.FileDocumentManager; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.util.Computable; +import com.intellij.openapi.util.Key; +import com.intellij.openapi.util.UserDataHolderBase; +import com.intellij.openapi.vfs.VirtualFile; +import com.intellij.util.containers.WeakList; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.List; + +public class LazyRangeMarkerFactoryImpl extends LazyRangeMarkerFactory { + private final Project myProject; + private static final Key<WeakList<LazyMarker>> LAZY_MARKERS_KEY = Key.create("LAZY_MARKERS_KEY"); + + public LazyRangeMarkerFactoryImpl(@NotNull Project project, @NotNull final FileDocumentManager fileDocumentManager) { + myProject = project; + + EditorFactory.getInstance().getEventMulticaster().addDocumentListener(new DocumentAdapter() { + @Override + public void beforeDocumentChange(DocumentEvent e) { + transformRangeMarkers(e); + } + + @Override + public void documentChanged(DocumentEvent e) { + transformRangeMarkers(e); + } + + private void transformRangeMarkers(@NotNull DocumentEvent e) { + Document document = e.getDocument(); + VirtualFile file = fileDocumentManager.getFile(document); + if (file == null) { + return; + } + + WeakList<LazyMarker> lazyMarkers = getMarkers(file); + if (lazyMarkers == null) { + return; + } + + List<LazyMarker> markers = lazyMarkers.toStrongList(); + for (LazyMarker marker : markers) { + if (file.equals(marker.getFile())) { + marker.getOrCreateDelegate(); + } + } + } + }, project); + } + + static WeakList<LazyMarker> getMarkers(@NotNull VirtualFile file) { + return file.getUserData(LazyRangeMarkerFactoryImpl.LAZY_MARKERS_KEY); + } + + private static void addToLazyMarkersList(@NotNull LazyMarker marker, @NotNull VirtualFile file) { + WeakList<LazyMarker> markers = getMarkers(file); + + if (markers == null) { + markers = file.putUserDataIfAbsent(LAZY_MARKERS_KEY, new WeakList<LazyMarker>()); + } + markers.add(marker); + } + + private static void removeFromLazyMarkersList(@NotNull LazyMarker marker, @NotNull VirtualFile file) { + WeakList<LazyMarker> markers = getMarkers(file); + + if (markers != null) { + markers.remove(marker); + } + } + + @Override + @NotNull + public RangeMarker createRangeMarker(@NotNull final VirtualFile file, final int offset) { + return ApplicationManager.getApplication().runReadAction(new Computable<RangeMarker>() { + @Override + public RangeMarker compute() { + // even for already loaded document do not create range marker yet - wait until it really needed when e.g. user clicked to jump to OpenFileDescriptor + final LazyMarker marker = new OffsetLazyMarker(file, offset); + addToLazyMarkersList(marker, file); + return marker; + } + }); + } + + @Override + @NotNull + public RangeMarker createRangeMarker(@NotNull final VirtualFile file, final int line, final int column, final boolean persistent) { + return ApplicationManager.getApplication().runReadAction(new Computable<RangeMarker>() { + @Override + public RangeMarker compute() { + final Document document = FileDocumentManager.getInstance().getCachedDocument(file); + if (document != null) { + final int offset = calculateOffset(myProject, file, document, line, column); + return document.createRangeMarker(offset, offset, persistent); + } + + final LazyMarker marker = new LineColumnLazyMarker(file, line, column); + addToLazyMarkersList(marker, file); + return marker; + } + }); + } + + abstract static class LazyMarker extends UserDataHolderBase implements RangeMarker { + protected RangeMarker myDelegate; // the real range marker which is created only when document is opened, or (this) which means it's disposed + protected final VirtualFile myFile; + protected final int myInitialOffset; + + private LazyMarker(@NotNull VirtualFile file, int offset) { + myFile = file; + myInitialOffset = offset; + } + + boolean isDelegated() { + return myDelegate != null; + } + + @NotNull + public VirtualFile getFile() { + return myFile; + } + + @Nullable + protected final RangeMarker getOrCreateDelegate() { + if (myDelegate == null) { + Document document = FileDocumentManager.getInstance().getDocument(myFile); + if (document == null) { + return null; + } + myDelegate = createDelegate(myFile, document); + removeFromLazyMarkersList(this, myFile); + } + return isDisposed() ? null : myDelegate; + } + + @Nullable + protected abstract RangeMarker createDelegate(@NotNull VirtualFile file, @NotNull Document document); + + @Override + @NotNull + public Document getDocument() { + RangeMarker delegate = getOrCreateDelegate(); + if (delegate == null) { + //noinspection ConstantConditions + return FileDocumentManager.getInstance().getDocument(myFile); + } + return delegate.getDocument(); + } + + @Override + public int getStartOffset() { + return myDelegate == null || isDisposed() ? myInitialOffset : myDelegate.getStartOffset(); + } + + public boolean isDisposed() { + return myDelegate == this; + } + + + @Override + public int getEndOffset() { + return myDelegate == null || isDisposed() ? myInitialOffset : myDelegate.getEndOffset(); + } + + @Override + public boolean isValid() { + RangeMarker delegate = getOrCreateDelegate(); + return delegate != null && !isDisposed() && delegate.isValid(); + } + + @Override + public void setGreedyToLeft(boolean greedy) { + getOrCreateDelegate().setGreedyToLeft(greedy); + } + + @Override + public void setGreedyToRight(boolean greedy) { + getOrCreateDelegate().setGreedyToRight(greedy); + } + + @Override + public boolean isGreedyToRight() { + return getOrCreateDelegate().isGreedyToRight(); + } + + @Override + public boolean isGreedyToLeft() { + return getOrCreateDelegate().isGreedyToLeft(); + } + + @Override + public void dispose() { + assert !isDisposed(); + RangeMarker delegate = myDelegate; + if (delegate == null) { + removeFromLazyMarkersList(this, myFile); + myDelegate = this; // mark of disposed marker + } + else { + delegate.dispose(); + } + } + } + + private static class OffsetLazyMarker extends LazyMarker { + private OffsetLazyMarker(@NotNull VirtualFile file, int offset) { + super(file, offset); + } + + @Override + public boolean isValid() { + RangeMarker delegate = myDelegate; + if (delegate == null) { + Document document = FileDocumentManager.getInstance().getDocument(myFile); + return document != null; + } + + return super.isValid(); + } + + @Override + @NotNull + public RangeMarker createDelegate(@NotNull VirtualFile file, @NotNull final Document document) { + final int offset = Math.min(myInitialOffset, document.getTextLength()); + return document.createRangeMarker(offset, offset); + } + } + + private class LineColumnLazyMarker extends LazyMarker { + private final int myLine; + private final int myColumn; + + private LineColumnLazyMarker(@NotNull VirtualFile file, int line, int column) { + super(file, -1); + myLine = line; + myColumn = column; + } + + @Override + @Nullable + public RangeMarker createDelegate(@NotNull VirtualFile file, @NotNull Document document) { + if (document.getTextLength() == 0 && !(myLine == 0 && myColumn == 0)) { + return null; + } + + int offset = calculateOffset(myProject, file, document, myLine, myColumn); + return document.createRangeMarker(offset, offset); + } + + @Override + public boolean isValid() { + RangeMarker delegate = myDelegate; + if (delegate == null) { + Document document = FileDocumentManager.getInstance().getDocument(myFile); + return document != null && (document.getTextLength() != 0 || myLine == 0 && myColumn == 0); + } + + return super.isValid(); + } + + @Override + public int getStartOffset() { + getOrCreateDelegate(); + return super.getStartOffset(); + } + + @Override + public int getEndOffset() { + getOrCreateDelegate(); + return super.getEndOffset(); + } + } + + private static int calculateOffset(@NotNull Project project, @NotNull VirtualFile file, @NotNull Document document, final int line, final int column) { + int offset; + if (line < document.getLineCount()) { + final int lineStart = document.getLineStartOffset(line); + final int lineEnd = document.getLineEndOffset(line); + final CharSequence docText = document.getCharsSequence(); + final int tabSize = CodeStyleFacade.getInstance(project).getTabSize(file.getFileType()); + + offset = lineStart; + int col = 0; + while (offset < lineEnd && col < column) { + col += docText.charAt(offset) == '\t' ? tabSize : 1; + offset++; + } + } + else { + offset = document.getTextLength(); + } + return offset; + } + +} diff --git a/platform/platform-impl/src/com/intellij/openapi/editor/impl/SelectionModelImpl.java b/platform/platform-impl/src/com/intellij/openapi/editor/impl/SelectionModelImpl.java index 2f5a81153579..731d1e26a289 100644 --- a/platform/platform-impl/src/com/intellij/openapi/editor/impl/SelectionModelImpl.java +++ b/platform/platform-impl/src/com/intellij/openapi/editor/impl/SelectionModelImpl.java @@ -567,7 +567,7 @@ public class SelectionModelImpl implements SelectionModel, PrioritizedDocumentLi @Override public void copySelectionToClipboard() { - CopyPasteSupport.copySelectionToClipboard(myEditor); + EditorCopyPasteHelper.getInstance().copySelectionToClipboard(myEditor); } @Override diff --git a/platform/platform-impl/src/com/intellij/openapi/editor/impl/SoftWrapModelImpl.java b/platform/platform-impl/src/com/intellij/openapi/editor/impl/SoftWrapModelImpl.java index 6f1b9820132b..d8b6697dc8d1 100644 --- a/platform/platform-impl/src/com/intellij/openapi/editor/impl/SoftWrapModelImpl.java +++ b/platform/platform-impl/src/com/intellij/openapi/editor/impl/SoftWrapModelImpl.java @@ -303,14 +303,7 @@ public class SoftWrapModelImpl implements SoftWrapModelEx, PrioritizedDocumentLi if (!isSoftWrappingEnabled()) { return 0; } - int result = 0; - FoldingModel foldingModel = myEditor.getFoldingModel(); - for (SoftWrap softWrap : myStorage.getSoftWraps()) { - if (!foldingModel.isOffsetCollapsed(softWrap.getStart())) { - result++; // Assuming that soft wrap has single line feed all the time - } - } - return result; + return myStorage.getSoftWraps().size(); // Assuming that soft wrap has single line feed all the time } /** diff --git a/platform/platform-impl/src/com/intellij/openapi/editor/impl/TrailingSpacesStripper.java b/platform/platform-impl/src/com/intellij/openapi/editor/impl/TrailingSpacesStripper.java new file mode 100644 index 000000000000..165ff5776756 --- /dev/null +++ b/platform/platform-impl/src/com/intellij/openapi/editor/impl/TrailingSpacesStripper.java @@ -0,0 +1,216 @@ +/* + * 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.openapi.editor.impl; + +import com.intellij.ide.DataManager; +import com.intellij.injected.editor.DocumentWindow; +import com.intellij.openapi.actionSystem.CommonDataKeys; +import com.intellij.openapi.actionSystem.DataContext; +import com.intellij.openapi.application.ApplicationManager; +import com.intellij.openapi.command.CommandProcessor; +import com.intellij.openapi.editor.*; +import com.intellij.openapi.editor.ex.EditorSettingsExternalizable; +import com.intellij.openapi.fileEditor.FileDocumentManager; +import com.intellij.openapi.fileEditor.FileDocumentManagerAdapter; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.util.Key; +import com.intellij.openapi.util.ShutDownTracker; +import com.intellij.openapi.vfs.VirtualFile; +import com.intellij.openapi.wm.IdeFocusManager; +import com.intellij.util.ArrayUtil; +import com.intellij.util.text.CharArrayUtil; +import gnu.trove.THashSet; +import org.jetbrains.annotations.NotNull; + +import java.awt.*; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.Set; + +public final class TrailingSpacesStripper extends FileDocumentManagerAdapter { + public static final Key<String> OVERRIDE_STRIP_TRAILING_SPACES_KEY = Key.create("OVERRIDE_TRIM_TRAILING_SPACES_KEY"); + public static final Key<Boolean> OVERRIDE_ENSURE_NEWLINE_KEY = Key.create("OVERRIDE_ENSURE_NEWLINE_KEY"); + + private final Set<Document> myDocumentsToStripLater = new THashSet<Document>(); + + @Override + public void beforeAllDocumentsSaving() { + Set<Document> documentsToStrip = new THashSet<Document>(myDocumentsToStripLater); + myDocumentsToStripLater.clear(); + for (Document document : documentsToStrip) { + strip(document); + } + } + + @Override + public void beforeDocumentSaving(@NotNull Document document) { + strip(document); + } + + private void strip(@NotNull final Document document) { + if (!document.isWritable()) return; + FileDocumentManager fileDocumentManager = FileDocumentManager.getInstance(); + VirtualFile file = fileDocumentManager.getFile(document); + if (file == null || !file.isValid()) return; + + final EditorSettingsExternalizable settings = EditorSettingsExternalizable.getInstance(); + if (settings == null) return; + + final String overrideStripTrailingSpacesData = file.getUserData(OVERRIDE_STRIP_TRAILING_SPACES_KEY); + final Boolean overrideEnsureNewlineData = file.getUserData(OVERRIDE_ENSURE_NEWLINE_KEY); + final String stripTrailingSpaces = overrideStripTrailingSpacesData != null ? overrideStripTrailingSpacesData : settings.getStripTrailingSpaces(); + final boolean doStrip = !stripTrailingSpaces.equals(EditorSettingsExternalizable.STRIP_TRAILING_SPACES_NONE); + final boolean ensureEOL = overrideEnsureNewlineData != null ? overrideEnsureNewlineData.booleanValue() : settings.isEnsureNewLineAtEOF(); + + if (doStrip) { + final boolean inChangedLinesOnly = !stripTrailingSpaces.equals(EditorSettingsExternalizable.STRIP_TRAILING_SPACES_WHOLE); + boolean success = stripIfNotCurrentLine(document, inChangedLinesOnly); + if (!success) { + myDocumentsToStripLater.add(document); + } + } + + final int lines = document.getLineCount(); + if (ensureEOL && lines > 0) { + final int start = document.getLineStartOffset(lines - 1); + final int end = document.getLineEndOffset(lines - 1); + if (start != end) { + final CharSequence content = document.getCharsSequence(); + ApplicationManager.getApplication().runWriteAction(new DocumentRunnable(document, null) { + @Override + public void run() { + CommandProcessor.getInstance().runUndoTransparentAction(new Runnable() { + @Override + public void run() { + if (CharArrayUtil.containsOnlyWhiteSpaces(content.subSequence(start, end)) && doStrip) { + document.deleteString(start, end); + } + else { + document.insertString(end, "\n"); + } + } + }); + } + }); + } + } + } + + // clears line modification flags except lines which was not stripped because the caret was in the way + public void clearLineModificationFlags(@NotNull Document document) { + if (document instanceof DocumentWindow) { + document = ((DocumentWindow)document).getDelegate(); + } + if (!(document instanceof DocumentImpl)) { + return; + } + + Component focusOwner = IdeFocusManager.getGlobalInstance().getFocusOwner(); + DataContext dataContext = DataManager.getInstance().getDataContext(focusOwner); + boolean isDisposeInProgress = ApplicationManager.getApplication().isDisposeInProgress(); // ignore caret placing when exiting + Editor activeEditor = isDisposeInProgress ? null : CommonDataKeys.EDITOR.getData(dataContext); + + // when virtual space enabled, we can strip whitespace anywhere + boolean isVirtualSpaceEnabled = activeEditor == null || activeEditor.getSettings().isVirtualSpace(); + + final EditorSettingsExternalizable settings = EditorSettingsExternalizable.getInstance(); + if (settings == null) return; + + String stripTrailingSpaces = settings.getStripTrailingSpaces(); + final boolean doStrip = !stripTrailingSpaces.equals(EditorSettingsExternalizable.STRIP_TRAILING_SPACES_NONE); + final boolean inChangedLinesOnly = !stripTrailingSpaces.equals(EditorSettingsExternalizable.STRIP_TRAILING_SPACES_WHOLE); + + int[] caretLines; + if (activeEditor != null && inChangedLinesOnly && doStrip && !isVirtualSpaceEnabled) { + List<Caret> carets = activeEditor.getCaretModel().getAllCarets(); + caretLines = new int[carets.size()]; + for (int i = 0; i < carets.size(); i++) { + Caret caret = carets.get(i); + caretLines[i] = caret.getLogicalPosition().line; + } + } + else { + caretLines = ArrayUtil.EMPTY_INT_ARRAY; + } + ((DocumentImpl)document).clearLineModificationFlagsExcept(caretLines); + } + + public static boolean stripIfNotCurrentLine(@NotNull Document document, boolean inChangedLinesOnly) { + if (document instanceof DocumentWindow) { + document = ((DocumentWindow)document).getDelegate(); + } + if (!(document instanceof DocumentImpl)) { + return true; + } + DataContext dataContext = DataManager.getInstance().getDataContext(IdeFocusManager.getGlobalInstance().getFocusOwner()); + boolean isDisposeInProgress = ApplicationManager.getApplication().isDisposeInProgress(); // ignore caret placing when exiting + Editor activeEditor = isDisposeInProgress ? null : CommonDataKeys.EDITOR.getData(dataContext); + + // when virtual space enabled, we can strip whitespace anywhere + boolean isVirtualSpaceEnabled = activeEditor == null || activeEditor.getSettings().isVirtualSpace(); + + boolean markAsNeedsStrippingLater; + + if (activeEditor != null && activeEditor.getCaretModel().supportsMultipleCarets()) { + List<Caret> carets = activeEditor.getCaretModel().getAllCarets(); + List<VisualPosition> visualCarets = new ArrayList<VisualPosition>(carets.size()); + int[] caretOffsets = new int[carets.size()]; + for (int i = 0; i < carets.size(); i++) { + Caret caret = carets.get(i); + visualCarets.add(caret.getVisualPosition()); + caretOffsets[i] = caret.getOffset(); + } + + markAsNeedsStrippingLater = ((DocumentImpl)document).stripTrailingSpaces(activeEditor.getProject(), inChangedLinesOnly, isVirtualSpaceEnabled, caretOffsets); + + if (!ShutDownTracker.isShutdownHookRunning()) { + final Iterator<VisualPosition> visualCaretIterator = visualCarets.iterator(); + activeEditor.getCaretModel().runForEachCaret(new CaretAction() { + @Override + public void perform(Caret caret) { + if (visualCaretIterator.hasNext()) { + caret.moveToVisualPosition(visualCaretIterator.next()); + } + } + }); + } + } + else { + VisualPosition visualCaret = activeEditor == null ? null : activeEditor.getCaretModel().getVisualPosition(); + int caretLine = activeEditor == null ? -1 : activeEditor.getCaretModel().getLogicalPosition().line; + int caretOffset = activeEditor == null ? -1 : activeEditor.getCaretModel().getOffset(); + + final Project project = activeEditor == null ? null : activeEditor.getProject(); + markAsNeedsStrippingLater = ((DocumentImpl)document).stripTrailingSpaces(project, inChangedLinesOnly, isVirtualSpaceEnabled, + caretLine, caretOffset); + + if (!ShutDownTracker.isShutdownHookRunning() && activeEditor != null) { + activeEditor.getCaretModel().moveToVisualPosition(visualCaret); + } + } + return !markAsNeedsStrippingLater; + } + + public void documentDeleted(@NotNull Document doc) { + myDocumentsToStripLater.remove(doc); + } + + @Override + public void unsavedDocumentsDropped() { + myDocumentsToStripLater.clear(); + } +} |