diff --git a/platform/lang-impl/src/com/intellij/application/options/editor/EditorOptionsPanel.form b/platform/lang-impl/src/com/intellij/application/options/editor/EditorOptionsPanel.form index df44f0ec4488..a4b285923591 100644 --- a/platform/lang-impl/src/com/intellij/application/options/editor/EditorOptionsPanel.form +++ b/platform/lang-impl/src/com/intellij/application/options/editor/EditorOptionsPanel.form @@ -2,7 +2,7 @@
- + @@ -64,7 +64,7 @@ - + @@ -76,7 +76,7 @@ - + @@ -84,7 +84,7 @@ - + @@ -92,13 +92,22 @@ - + + + + + + + + + + diff --git a/platform/lang-impl/src/com/intellij/application/options/editor/EditorOptionsPanel.java b/platform/lang-impl/src/com/intellij/application/options/editor/EditorOptionsPanel.java index 0aca55584554..a7e782dfd520 100644 --- a/platform/lang-impl/src/com/intellij/application/options/editor/EditorOptionsPanel.java +++ b/platform/lang-impl/src/com/intellij/application/options/editor/EditorOptionsPanel.java @@ -72,6 +72,7 @@ public class EditorOptionsPanel { private JCheckBox myCbEnsureBlankLineBeforeCheckBox; private JCheckBox myShowReformatCodeDialogCheckBox; private JCheckBox myShowOptimizeImportsDialogCheckBox; + private JCheckBox myCbUseSoftWraps; private final ErrorHighlightingPanel myErrorHighlightingPanel = new ErrorHighlightingPanel(); private final MyConfigurable myConfigurable; @@ -115,6 +116,7 @@ public class EditorOptionsPanel { // Virtual space + myCbUseSoftWraps.setSelected(editorSettings.isUseSoftWraps()); myCbVirtualSpace.setSelected(editorSettings.isVirtualSpace()); myCbCaretInsideTabs.setSelected(editorSettings.isCaretInsideTabs()); myCbVirtualPageAtBottom.setSelected(editorSettings.isAdditionalPageAtBottom()); @@ -175,6 +177,7 @@ public class EditorOptionsPanel { // Virtual space + editorSettings.setUseSoftWraps(myCbUseSoftWraps.isSelected()); editorSettings.setVirtualSpace(myCbVirtualSpace.isSelected()); editorSettings.setCaretInsideTabs(myCbCaretInsideTabs.isSelected()); editorSettings.setAdditionalPageAtBottom(myCbVirtualPageAtBottom.isSelected()); @@ -292,6 +295,7 @@ public class EditorOptionsPanel { isModified |= isModified(myCbHighlightIdentifierUnderCaret, codeInsightSettings.HIGHLIGHT_IDENTIFIER_UNDER_CARET); // Virtual space + isModified |= isModified(myCbUseSoftWraps, editorSettings.isUseSoftWraps()); isModified |= isModified(myCbVirtualSpace, editorSettings.isVirtualSpace()); isModified |= isModified(myCbCaretInsideTabs, editorSettings.isCaretInsideTabs()); isModified |= isModified(myCbVirtualPageAtBottom, editorSettings.isAdditionalPageAtBottom()); diff --git a/platform/platform-api/src/com/intellij/openapi/editor/EditorSettings.java b/platform/platform-api/src/com/intellij/openapi/editor/EditorSettings.java index 452f49646df9..b3403b120c61 100644 --- a/platform/platform-api/src/com/intellij/openapi/editor/EditorSettings.java +++ b/platform/platform-api/src/com/intellij/openapi/editor/EditorSettings.java @@ -96,6 +96,8 @@ public interface EditorSettings { void setRefrainFromScrolling(boolean b); boolean isIndentGuidesShown(); - void setIndentGuidesShown(boolean val); + + boolean isUseSoftWraps(); + void setUseSoftWraps(boolean use); } diff --git a/platform/platform-api/src/com/intellij/openapi/editor/LogicalPosition.java b/platform/platform-api/src/com/intellij/openapi/editor/LogicalPosition.java index 611673124e10..aa203871aeb1 100644 --- a/platform/platform-api/src/com/intellij/openapi/editor/LogicalPosition.java +++ b/platform/platform-api/src/com/intellij/openapi/editor/LogicalPosition.java @@ -21,6 +21,8 @@ import org.jetbrains.annotations.NonNls; * Represents a logical position in the editor. Logical positions ignore folding - * for example, if the top 10 lines of the document are folded, the 10th line in the document * will have the line number 10 in its logical position. + *

+ * //TODO den add doc equals/compareTo() behavior difference because of soft wraps. * * @see Editor#offsetToLogicalPosition(int) * @see Editor#logicalPositionToOffset(LogicalPosition) @@ -33,30 +35,56 @@ import org.jetbrains.annotations.NonNls; public class LogicalPosition implements Comparable { public final int line; public final int column; + //TODO den add doc + public final int softWrapLines; + //TODO den add doc + public final int linesFromActiveSoftWrap; + //TODO den add doc + public final int softWrapColumns; public LogicalPosition(int line, int column) { + this(line, column, 0, 0, 0); + } + + public LogicalPosition(int line, int column, int softWrapLines, int linesFromActiveSoftWrap, int softWrapColumns) { + assert linesFromActiveSoftWrap <= softWrapLines; + this.line = line; this.column = column; + this.softWrapLines = softWrapLines; + this.linesFromActiveSoftWrap = linesFromActiveSoftWrap; + this.softWrapColumns = softWrapColumns; + } + + //TODO den add doc + public boolean isOnSoftWrappedLine() { + return softWrapColumns != 0; } public boolean equals(Object o) { if (!(o instanceof LogicalPosition)) return false; final LogicalPosition logicalPosition = (LogicalPosition) o; - return column == logicalPosition.column && line == logicalPosition.line; + return column == logicalPosition.column && line == logicalPosition.line && softWrapLines == logicalPosition.softWrapLines + && softWrapColumns == logicalPosition.softWrapColumns; } public int hashCode() { - return 29 * line + column; + int result = 29 * line + column; + result = result * 29 + softWrapLines; + return 29 * result + softWrapColumns; } @NonNls public String toString() { - return "LogicalPosition: line = " + line + " column = " + column; + return "LogicalPosition: line = " + line + " column = " + column + "; soft wrap: lines = " + softWrapLines + + " (active = " + linesFromActiveSoftWrap + ") columns = " + softWrapColumns; } public int compareTo(LogicalPosition position) { if (line != position.line) return line - position.line; - return column - position.column; + if (column != position.column) return column - position.column; + if (softWrapLines != position.softWrapLines) return softWrapLines - position.softWrapLines; + return softWrapColumns - position.softWrapColumns; } } diff --git a/platform/platform-api/src/com/intellij/openapi/editor/SoftWrapModel.java b/platform/platform-api/src/com/intellij/openapi/editor/SoftWrapModel.java new file mode 100644 index 000000000000..bf391b437bd5 --- /dev/null +++ b/platform/platform-api/src/com/intellij/openapi/editor/SoftWrapModel.java @@ -0,0 +1,123 @@ +/* + * Copyright 2000-2010 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 gnu.trove.TIntObjectHashMap; +import org.jetbrains.annotations.Nullable; + +import java.util.Collection; + +/** + * Defines contract for the services that bring 'soft wrapping' to the editor. + *

+ * 'Soft wrap' here means representation of the line that exceeds right component viewport margin as if it is wrapped. + * No changes are introduced to the actual document/file system during that, i.e. it's a pure 'view' facility. + *

+ * Example: + *

+ * Raw text (as stored at file system) + *

+ *

+ *     public class Test {                                         | <- right margin
+ *                                                                 |
+ *         public void baz() {                                     |
+ *             foo("test1", "test2", "test3", "test4", "test5", "test6");
+ *         }                                                       |
+ *                                                                 |
+ *         public void foo(Object ... args) {                      |
+ *         }                                                       |
+ *     }                                                           |
+ *      
+ *

+ * The same text with soft wraps as shown to end-user at editor + *

+ *

+ *     public class Test {                                         | <- right margin
+ *                                                                 |
+ *         public void baz() {                                     |
+ *             foo("test1", "test2", "test3", "test4", "test5",    |
+ *                 "test6");                                       |
+ *         }                                                       |
+ *                                                                 |
+ *         public void foo(Object ... args) {                      |
+ *         }                                                       |
+ *     }                                                           |
+ *      
+ *

+ * Another important soft wrap feature is that as soon as the user starts typing on a line which representation is affected by + * soft wrap (e.g. starts adding new call argument after "test6" at example above), that soft wraps becomes + * 'hard wrap', i.e. virtual changes introduced by it are flushed to the underlying document. + *

+ * Note: soft wrap is assumed to provide as user-friendly indentation for those wrapped line as possible + * (note that "test6" at example below is aligned to the parameters start). + *

+ * Implementations of this interface are not obliged to be thread-safe. + * + * @author Denis Zhdanov + * @since Jun 8, 2010 3:15:18 PM + */ +public interface SoftWrapModel { + + /** + * Allows to answer if 'soft wrap' feature is enabled. + * + * @return true if 'soft wraps' are enabled; false otherwise + */ + boolean isSoftWrappingEnabled(); + + /** + * Allows to retrieve number of lines feeds from 'soft wraps' located before the given visual position. + *

+ * There is a possible case that given visual position points to location inside soft wrap - line feeds of that soft wrap + * are not counted then. + *

+ * Example: + *

+ *

+   *     public class Test {                      | <- right margin
+   *                                              |
+   *         public void baz() {                  |
+   *             foo("test1", "test2", "test3", [p1]
+   *  [p2            "test4", "test5", "test6",   |
+   *                 "test7", "test8", "test8"); [p3]
+   *         }                                    |
+   *                                              |
+   *  [p4]   public void foo(Object ... args) {   |
+   *         }                                    |
+   *     }                                        |
+   *      
+ *

+ * Here all 'foo()' call arguments are soft wrapped into three lines. However zero is returned for visual position + * 'p1'-'p3' because they are located inside soft wrap; + * '2' is returned for 'p4'. + * + * @param position target visual position + * @return number of lines feeds from 'soft wraps' located before the given visual position if any; + * 0 otherwise + */ + int getSoftWrapLineFeedsBefore(VisualPosition position); + + /** + * Asks current model for the soft wrap registered for the given document offset if any. + * + * @param offset target document offset + * @return soft wrap registered for the given offset within the current model if any; null otherwise + */ + @Nullable + TextChange getSoftWrap(int offset); + + LogicalPosition adjustLogicalPositionIfNecessary(LogicalPosition logical, VisualPosition visual); +} diff --git a/platform/platform-api/src/com/intellij/openapi/editor/TextChange.java b/platform/platform-api/src/com/intellij/openapi/editor/TextChange.java new file mode 100644 index 000000000000..062275c6aab8 --- /dev/null +++ b/platform/platform-api/src/com/intellij/openapi/editor/TextChange.java @@ -0,0 +1,175 @@ +/* + * Copyright 2000-2010 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.util.text.CharArrayUtil; +import com.intellij.util.text.CharSequenceBackedByArray; +import org.jetbrains.annotations.NotNull; + +import java.util.concurrent.atomic.AtomicReference; + +/** + * Provides generic contract for object encapsulating information about single unit of text change. + *

+ * Thread-safe. + * + * @author Denis Zhdanov + * @since May 31, 2010 12:26:51 PM + */ +public class TextChange { + + private final AtomicReference myChars = new AtomicReference(); + private final CharSequence myText; + private final int myStart; + private final int myEnd; + + /** + * Shorthand for creating {@link TextChange} with the given arguments where 'end index' has the same value as + * 'start index'. + * + * @param text text affected by the current change + * @param start start index (inclusive) of text range affected by the change encapsulated by the current object + * @throws IllegalArgumentException if given start index is invalid + */ + public TextChange(@NotNull CharSequence text, int start) throws IllegalArgumentException { + this(text, start, start); + } + + /** + * Creates new TextChange object with the given arguments. It encapsulates information about the change that + * may be applied to the target document. + * + * @param text text that is introduced by the current change + * @param start start index of the target document location where current change is to be applied + * @param end end index of the target document where current change is to be applied, i.e. it's assumed that current text + * change appliance to particular document causes replacement of its original text at [start; end) + * interval by the text encapsulated by the current change. I.e. original text is replaced by the new one + * @throws IllegalArgumentException if given start or end index in invalid or they are inconsistent to each other + */ + public TextChange(@NotNull CharSequence text, int start, int end) throws IllegalArgumentException { + if (start < 0) { + throw new IllegalArgumentException(String.format("Can't construct new %s object. Reason: given start index (%d) is negative. " + + "End index: %d, text: '%s'", getClass().getName(), start, end, text)); + } + if (end < start) { + throw new IllegalArgumentException(String.format("Can't construct new %s object. Reason: given end index (%d) is less than " + + "start index (%d). Text: '%s'", getClass().getName(), end, start, text)); + } + myText = text; + myStart = start; + myEnd = end; + } + + /** + * @return start index (inclusive) of text range affected by the change encapsulated at the current object + */ + public int getStart() { + return myStart; + } + + /** + * @return end index (exclusive) of text range affected by the change encapsulated at the current object + */ + public int getEnd() { + return myEnd; + } + + + /** + * Allows to retrieve text that is directly affected by the change encapsulated by the current object. + * + * @return text related to the change encapsulated by the current object + */ + @NotNull + public CharSequence getText() { + return myText; + } + + /** + * Allows to get change text as a char array. Note that it's not guaranteed that change text directly maps to the returned char array, + * i.e. change to array content is not obeyed to be reflected in {@link #getText()} result. + *

+ * Generally speaking, this method is introduced just as a step toward existing high-performance services that work in terms + * of char arrays. Resulting array is instantiated on-demand via {@link CharArrayUtil#fromSequence(CharSequence)}, hence, it + * doesn't hit memory if, for example, {@link CharSequenceBackedByArray} is used as initial change text. + * + * @return stored change text as a char array + */ + @NotNull + public char[] getChars() { + char[] result = myChars.get(); + if (result != null) { + return result; + } + myChars.compareAndSet(null, CharArrayUtil.fromSequence(myText)); + return myChars.get(); + } + + /** + * Difference in document symbols number after current change appliance. + *

+ * Note: returned number may be either positive or not. For example it may be negative for 'remove' + * or 'replace' changes (number of text symbols is less than number of symbols at target change interval) + * + * @return difference in document symbols number after current change appliance + */ + public int getDiff() { + return myText.length() - myEnd + myStart; + } + + /** + * Creates new {@link TextChange} on the basis of the current object with given offset applied to its {@link #getStart() start} + * and {@link #getEnd() end} properties. + * + * @param offset offset to apply to the current change object + * @return text change that is built on the basis of the current object that with {@link #getStart() start} + * and {@link #getEnd() end} positions shifted to the given offset + * @throws IllegalArgumentException if start index becomes zero after given offset appliance (it is not applied then) + */ + public TextChange advance(int offset) throws IllegalArgumentException { + if (offset == 0) { + return this; + } + int newStart = myStart + offset; + if (newStart < 0) { + throw new IllegalArgumentException(String.format("Can't apply given offset (%d) to the current text change object (%s). Reason: " + + "new start index becomes negative after that (%d)", offset, this, newStart)); + } + + return new TextChange(myText, newStart, myEnd + offset); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + TextChange that = (TextChange)o; + return myText.equals(that.myText) && myStart == that.myStart && myEnd == that.myEnd; + } + + @Override + public int hashCode() { + int result = myText.hashCode(); + result = 31 * result + myStart; + return 31 * result + myEnd; + } + + @Override + public String toString() { + return String.format("%d-%d: '%s'", myStart, myEnd, myText); + } +} diff --git a/platform/platform-impl/src/com/intellij/openapi/editor/ex/EditorSettingsExternalizable.java b/platform/platform-impl/src/com/intellij/openapi/editor/ex/EditorSettingsExternalizable.java index 88773955bd3a..473734e1a5cf 100644 --- a/platform/platform-impl/src/com/intellij/openapi/editor/ex/EditorSettingsExternalizable.java +++ b/platform/platform-impl/src/com/intellij/openapi/editor/ex/EditorSettingsExternalizable.java @@ -40,6 +40,7 @@ public class EditorSettingsExternalizable implements NamedJDOMExternalizable, Ex //Q: make it interface? public static class OptionSet implements Cloneable { public String LINE_SEPARATOR; + public boolean USE_SOFT_WRAPS = true; public boolean IS_VIRTUAL_SPACE = true; public boolean IS_CARET_INSIDE_TABS; @NonNls public String STRIP_TRAILING_SPACES = "Changed"; @@ -211,6 +212,14 @@ public class EditorSettingsExternalizable implements NamedJDOMExternalizable, Ex myOptions.SMART_HOME = val; } + public boolean isUseSoftWraps() { + return myOptions.USE_SOFT_WRAPS; + } + + public void setUseSoftWraps(boolean use) { + myOptions.USE_SOFT_WRAPS = use; + } + public boolean isVirtualSpace() { return myOptions.IS_VIRTUAL_SPACE; } diff --git a/platform/platform-impl/src/com/intellij/openapi/editor/impl/CaretModelImpl.java b/platform/platform-impl/src/com/intellij/openapi/editor/impl/CaretModelImpl.java index c89912f99f1c..a8fe1d75119b 100644 --- a/platform/platform-impl/src/com/intellij/openapi/editor/impl/CaretModelImpl.java +++ b/platform/platform-impl/src/com/intellij/openapi/editor/impl/CaretModelImpl.java @@ -227,11 +227,21 @@ public class CaretModelImpl implements CaretModel, PrioritizedDocumentListener { validateCallContext(); int column = pos.column; int line = pos.line; + int softWrapLines = pos.softWrapLines; + int linesFromCurrentSoftWrap = pos.linesFromActiveSoftWrap; + int softWrapColumns = pos.softWrapColumns; Document doc = myEditor.getDocument(); - if (column < 0) column = 0; - if (line < 0) line = 0; + if (column < 0) { + column = 0; + softWrapColumns = 0; + } + if (line < 0) { + line = 0; + softWrapLines = 0; + linesFromCurrentSoftWrap = 0; + } int lineCount = doc.getLineCount(); if (lineCount == 0) { @@ -239,6 +249,8 @@ public class CaretModelImpl implements CaretModel, PrioritizedDocumentListener { } else if (line > lineCount - 1) { line = lineCount - 1; + softWrapLines = 0; + linesFromCurrentSoftWrap = 0; } EditorSettings editorSettings = myEditor.getSettings(); @@ -248,6 +260,9 @@ public class CaretModelImpl implements CaretModel, PrioritizedDocumentListener { int lineEndColumnNumber = myEditor.offsetToLogicalPosition(lineEndOffset).column; if (column > lineEndColumnNumber) { column = lineEndColumnNumber; + if (softWrapColumns != 0) { + softWrapColumns -= column - lineEndColumnNumber; + } } } @@ -257,7 +272,7 @@ public class CaretModelImpl implements CaretModel, PrioritizedDocumentListener { LogicalPosition oldCaretPosition = myLogicalCaret; - myLogicalCaret = new LogicalPosition(line, column); + myLogicalCaret = new LogicalPosition(line, column, softWrapLines, linesFromCurrentSoftWrap, softWrapColumns); final int offset = myEditor.logicalPositionToOffset(myLogicalCaret); 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 2eafb38d65a9..783e66fad3fc 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 @@ -327,7 +327,11 @@ class EditorGutterComponentImpl extends EditorGutterComponentEx implements Mouse } for (int i = startLineNumber; i < endLineNumber; i++) { - int logLine = myEditor.visualToLogicalPosition(new VisualPosition(i, 0)).line; + LogicalPosition logicalPosition = myEditor.visualToLogicalPosition(new VisualPosition(i, 0)); + if (logicalPosition.isOnSoftWrappedLine()) { + continue; + } + int logLine = logicalPosition.line; String s = String.valueOf(logLine + 1); g.drawString(s, getLineNumberAreaOffset() + getLineNumberAreaWidth() - 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 2c72189e1f16..6b0ee8732f63 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 @@ -49,6 +49,7 @@ import com.intellij.openapi.editor.highlighter.EditorHighlighter; import com.intellij.openapi.editor.highlighter.HighlighterClient; import com.intellij.openapi.editor.impl.event.MarkupModelEvent; import com.intellij.openapi.editor.impl.event.MarkupModelListener; +import com.intellij.openapi.editor.TextChange; import com.intellij.openapi.editor.markup.*; import com.intellij.openapi.fileEditor.FileDocumentManager; import com.intellij.openapi.progress.ProgressManager; @@ -68,7 +69,7 @@ import com.intellij.util.ui.ButtonlessScrollBarUI; import com.intellij.util.ui.EmptyClipboardOwner; import com.intellij.util.ui.UIUtil; import com.intellij.util.ui.update.UiNotifyConnector; -import gnu.trove.TIntArrayList; +import gnu.trove.*; import org.jdom.Element; import org.jetbrains.annotations.NonNls; import org.jetbrains.annotations.NotNull; @@ -157,6 +158,7 @@ public final class EditorImpl extends UserDataHolderBase implements EditorEx, Hi private final FoldingModelImpl myFoldingModel; private final ScrollingModelImpl myScrollingModel; private final CaretModelImpl myCaretModel; + private final SoftWrapModelImpl mySoftWrapModel; private static final RepaintCursorCommand ourCaretBlinkingCommand; @@ -173,7 +175,7 @@ public final class EditorImpl extends UserDataHolderBase implements EditorEx, Hi private EditorHighlighter myHighlighter; - private int myScrollbarOrientation; + private int myScrollBarOrientation; private boolean myMousePressedInsideSelection; private FontMetrics myPlainFontMetrics; private FontMetrics myBoldFontMetrics; @@ -243,6 +245,7 @@ public final class EditorImpl extends UserDataHolderBase implements EditorEx, Hi myMarkupModel = new EditorMarkupModelImpl(this); myFoldingModel = new FoldingModelImpl(this); myCaretModel = new CaretModelImpl(this); + mySoftWrapModel = new SoftWrapModelImpl(this); mySizeContainer.reset(); myCommandProcessor = CommandProcessor.getInstance(); @@ -316,7 +319,7 @@ public final class EditorImpl extends UserDataHolderBase implements EditorEx, Hi myCaretCursor = new CaretCursor(); myFoldingModel.flushCaretShift(); - myScrollbarOrientation = VERTICAL_SCROLLBAR_RIGHT; + myScrollBarOrientation = VERTICAL_SCROLLBAR_RIGHT; EditorHighlighter highlighter = new EmptyEditorHighlighter(myScheme.getAttributes(HighlighterColors.TEXT)); setHighlighter(highlighter); @@ -611,11 +614,11 @@ public final class EditorImpl extends UserDataHolderBase implements EditorEx, Hi final DropTarget dropTarget = myEditorComponent.getDropTarget(); if (dropTarget != null) { // might be null in headless environment dropTarget.addDropTargetListener(new DropTargetAdapter() { - public void drop(DropTargetDropEvent dtde) { + public void drop(DropTargetDropEvent e) { } - public void dragOver(DropTargetDragEvent dtde) { - Point location = dtde.getLocation(); + public void dragOver(DropTargetDragEvent e) { + Point location = e.getLocation(); moveCaretToScreenPos(location.x, location.y); getScrollingModel().scrollToCaret(ScrollType.RELATIVE); @@ -782,11 +785,19 @@ public final class EditorImpl extends UserDataHolderBase implements EditorEx, Hi } } - int offset = logicalPositionToOffset(visualToLogicalPosition(new VisualPosition(line, 0))); + LogicalPosition logicalPosition = visualToLogicalPosition(new VisualPosition(line, 0)); + int offset = logicalPositionToOffset(logicalPosition); int textLength = myDocument.getTextLength(); if (offset >= textLength) return new VisualPosition(line, 0); + // There is a possible case that starting logical line is split by soft-wraps and it's part after the split should be drawn. + // We mark that we're under such circumstances then. + boolean activeSoftWrapProcessed = true; + if (logicalPosition.isOnSoftWrappedLine()) { + activeSoftWrapProcessed = false; + } + int column = 0; int prevX = 0; CharSequence text = myDocument.getCharsNoThreadCheck(); @@ -799,7 +810,9 @@ public final class EditorImpl extends UserDataHolderBase implements EditorEx, Hi int x = 0; outer: while (true) { - if (offset >= textLength) break; + if (offset >= textLength) { + break; + } if (offset >= state.getEndOffset()) { state.advance(); @@ -812,32 +825,56 @@ public final class EditorImpl extends UserDataHolderBase implements EditorEx, Hi for (char aPlaceholder : placeholder) { c = aPlaceholder; x += EditorUtil.charWidth(c, fontType, this); - if (x >= px) break outer; + if (x >= px) { + break outer; + } column++; } offset = region.getEndOffset(); } else { + TextChange softWrap = mySoftWrapModel.getSoftWrap(offset); + if (softWrap != null) { + // There is a possible case that soft wrap contains more than one line feed inside and we need to start counting not + // from its first line. + int softWrapLinesToSkip = activeSoftWrapProcessed ? 0 : logicalPosition.linesFromActiveSoftWrap; + CharSequence softWrapText = softWrap.getText(); + for (int i = 0; i < softWrapText.length(); i++) { + c = softWrapText.charAt(i); + if (softWrapLinesToSkip > 0) { + if (c == '\n') { + softWrapLinesToSkip--; + } + continue; + } + prevX = x; + int diff = charToVisibleWidth(c, fontType, x); + if (diff == 0) { + break outer; + } + + x += diff; + if (x >= px) { + break outer; + } + column += columnsNumber(c, x, prevX, spaceSize); + } + activeSoftWrapProcessed = true; + } + prevX = x; c = text.charAt(offset); - if (c == '\n') { + int diff = charToVisibleWidth(c, fontType, x); + if (diff == 0) { + break; + } else { + x += diff; + } + + if (x >= px) { break; } - if (c == '\t') { - x = EditorUtil.nextTabStop(x, this); - } - else { - x += EditorUtil.charWidth(c, fontType, this); - } - - if (x >= px) break; - - if (c == '\t') { - column += (x - prevX) / spaceSize; - } - else { - column++; - } + column += columnsNumber(c, x, prevX, spaceSize); offset++; } @@ -868,6 +905,42 @@ public final class EditorImpl extends UserDataHolderBase implements EditorEx, Hi return new VisualPosition(line, column); } + /** + * Allows to answer how much width requires given char to be represented on a screen. + * + * @param c target character + * @param fontType font type to use for representation of the given character + * @param currentX current 'x' position on a line where given character should be displayed + * @return width required to represent given char with the given settings on a screen; + * '0' if given char is a line break + */ + private int charToVisibleWidth(char c, int fontType, int currentX) { + if (c == '\n') { + return 0; + } + + if (c == '\t') { + return EditorUtil.nextTabStop(currentX, this) - currentX; + } + return EditorUtil.charWidth(c, fontType, this); + } + + /** + * Allows to answer how many columns are necessary for representation of the given char on a screen. + * + * @param c target char + * @param x 'x' coordinate of the line where given char is represented that indicates char end location + * @param prevX 'x' coordinate of the line where given char is represented that indicates char start location + * @param spaceSize 'space' symbol width + * @return number of columns necessary for representation of the given char on a screen. + */ + private static int columnsNumber(char c, int x, int prevX, int spaceSize) { + if (c != '\t') { + return 1; + } + return (x - prevX) / spaceSize; + } + @NotNull public VisualPosition offsetToVisualPosition(int offset) { return logicalToVisualPosition(offsetToLogicalPosition(offset)); @@ -1031,7 +1104,7 @@ public final class EditorImpl extends UserDataHolderBase implements EditorEx, Hi return myGutterComponent != null && myGutterComponent.isShowing(); } - private void repaintToScreenBotton(int startLine) { + private void repaintToScreenBottom(int startLine) { Rectangle visibleRect = getScrollingModel().getVisibleArea(); int yStartLine = logicalLineToY(startLine); int yEndLine = visibleRect.y + visibleRect.height; @@ -1082,7 +1155,7 @@ public final class EditorImpl extends UserDataHolderBase implements EditorEx, Hi if (countLineFeeds(e.getOldFragment()) != countLineFeeds(e.getNewFragment())) { // Lines removed. Need to repaint till the end of the screen - repaintToScreenBotton(startLine); + repaintToScreenBottom(startLine); painted = true; } } @@ -1598,7 +1671,8 @@ public final class EditorImpl extends UserDataHolderBase implements EditorEx, Hi int visibleLineNumber = clip.y / lineHeight; - int startLineNumber = xyToLogicalPosition(new Point(0, clip.y)).line; + LogicalPosition logicalPosition = xyToLogicalPosition(new Point(0, clip.y)); + int startLineNumber = logicalPosition.line; Point position = new Point(0, visibleLineNumber * lineHeight); if (startLineNumber == 0 && myPrefixText != null) { @@ -1610,7 +1684,16 @@ public final class EditorImpl extends UserDataHolderBase implements EditorEx, Hi return; } + // There is a possible case that starting logical line is split by soft-wraps and it's part after the split should be drawn. + // We do the following then: + // 1. Mark that we're under such circumstances ('activeSoftWrapProcessed = false'); + // 2. Use active soft wrap offset as a start offset in order to paint its symbols in correct way; + boolean activeSoftWrapProcessed = true; int start = myDocument.getLineStartOffset(startLineNumber); + if (logicalPosition.isOnSoftWrappedLine()) { + start = logicalPositionToOffset(logicalPosition); + activeSoftWrapProcessed = false; + } IterationState iterationState = new IterationState(this, start, paintSelection()); @@ -1640,7 +1723,9 @@ public final class EditorImpl extends UserDataHolderBase implements EditorEx, Hi drawString(g, chars, start, lEnd - lIterator.getSeparatorLength(), position, clip, effectColor, effectType, fontType, currentColor); position.x = 0; - if (position.y > clip.y + clip.height) break; + if (position.y > clip.y + clip.height) { + break; + } position.y += lineHeight; start = lEnd; } @@ -1663,6 +1748,63 @@ public final class EditorImpl extends UserDataHolderBase implements EditorEx, Hi fontType, currentColor); } else { + boolean breakWholeIteration = false; + if (mySoftWrapModel.shouldWrap(chars, start, hEnd, position)) { + TextChange softWrap = mySoftWrapModel.wrap(start); + + // There is a possible case that soft wrap contains more than one line feed inside and we need to start drawing not + // from its first line. + int softWrapLinesToSkip = activeSoftWrapProcessed ? 0 : logicalPosition.linesFromActiveSoftWrap; + char[] softWrapChars = softWrap.getChars(); + + // We don't draw every soft wrap symbol one-by-one but whole visual line. Current variable holds index that points + // to the first soft wrap symbol that is not drawn yet. + int softWrapSegmentStartIndex = 0; + for (int i = 0; i < softWrapChars.length; i++) { + // Delay soft wraps symbols drawing until EOL if found. + if (softWrapChars[i] != '\n') { + continue; + } + + // Skip top soft wrap lines if necessary. + if (softWrapLinesToSkip-- > 0) { + position.x = 0; + softWrapSegmentStartIndex = i + 1; + continue; + } + + // Draw soft wrap symbols on current visual line if any. + if (i - softWrapSegmentStartIndex > 0) { + drawString( + g, softWrapChars, softWrapSegmentStartIndex, i, position, clip, currentColor, effectType, fontType, currentColor + ); + } + + // Reset 'x' coordinate because of new line start. + position.x = 0; + + // Stop the processing if we drew the whole clip. + if (position.y > clip.y + clip.height) { + breakWholeIteration = true; + break; + } + position.y += lineHeight; + softWrapSegmentStartIndex = i + 1; + } + + // Draw remaining soft wrap symbols from its last line if any. + if (softWrapSegmentStartIndex < softWrapChars.length - 1) { + position.x += drawString( + g, softWrapChars, softWrapSegmentStartIndex, softWrapChars.length, position, clip, effectColor, effectType, + fontType, currentColor + ); + } + + activeSoftWrapProcessed = true; + } + if (breakWholeIteration) { + break; + } position.x = drawString(g, chars, start, hEnd, position, clip, effectColor, effectType, fontType, currentColor); } } @@ -2203,7 +2345,9 @@ public final class EditorImpl extends UserDataHolderBase implements EditorEx, Hi @NotNull public VisualPosition logicalToVisualPosition(@NotNull LogicalPosition logicalPos) { assertReadAccess(); - if (!myFoldingModel.isFoldingEnabled()) return new VisualPosition(logicalPos.line, logicalPos.column); + if (!myFoldingModel.isFoldingEnabled() && !mySoftWrapModel.isSoftWrappingEnabled()) { + return new VisualPosition(logicalPos.line, logicalPos.column); + } int offset = logicalPositionToOffset(logicalPos); @@ -2224,9 +2368,9 @@ public final class EditorImpl extends UserDataHolderBase implements EditorEx, Hi line -= myFoldingModel.getFoldedLinesCountBefore(offset); - FoldRegion[] toplevel = myFoldingModel.fetchTopLevel(); + FoldRegion[] topLevel = myFoldingModel.fetchTopLevel(); for (int idx = myFoldingModel.getLastTopLevelIndexBefore(offset); idx >= 0; idx--) { - FoldRegion region = toplevel[idx]; + FoldRegion region = topLevel[idx]; if (region.isValid()) { if (region.getDocument().getLineNumber(region.getEndOffset()) == logicalPos.line && region.getEndOffset() <= offset) { LogicalPosition foldStart = offsetToLogicalPosition(region.getStartOffset()); @@ -2243,7 +2387,7 @@ public final class EditorImpl extends UserDataHolderBase implements EditorEx, Hi LOG.assertTrue(line >= 0); - return new VisualPosition(line, Math.max(0, column)); + return new VisualPosition(line + logicalPos.softWrapLines, Math.max(0, column + logicalPos.softWrapColumns)); } @Nullable @@ -2312,7 +2456,9 @@ public final class EditorImpl extends UserDataHolderBase implements EditorEx, Hi @NotNull public LogicalPosition visualToLogicalPosition(@NotNull VisualPosition visiblePos) { assertReadAccess(); - if (!myFoldingModel.isFoldingEnabled()) return new LogicalPosition(visiblePos.line, visiblePos.column); + if (!myFoldingModel.isFoldingEnabled() && !mySoftWrapModel.isSoftWrappingEnabled()) { + return new LogicalPosition(visiblePos.line, visiblePos.column); + } int line = visiblePos.line; int column = visiblePos.column; @@ -2329,14 +2475,14 @@ public final class EditorImpl extends UserDataHolderBase implements EditorEx, Hi column = logFoldEnd.column + (visiblePos.column - visFoldEnd.column); } else { - return offsetToLogicalPosition(lastCollapsedBefore.getStartOffset()); + return offsetToLogicalPosition(lastCollapsedBefore.getStartOffset()); //TODO den check if we need soft-wrap processing here } } } if (column < 0) column = 0; - return new LogicalPosition(line, column); + return mySoftWrapModel.adjustLogicalPositionIfNecessary(new LogicalPosition(line, column), visiblePos); } private int calcLogicalLineNumber(int offset) { @@ -2372,10 +2518,16 @@ public final class EditorImpl extends UserDataHolderBase implements EditorEx, Hi int columnNumber = pos.column; int lineNumber = pos.line; + int softWrapLines = pos.softWrapLines; + int linesFromCurrentSoftWrap = pos.linesFromActiveSoftWrap; + int softWrapColumns = pos.softWrapColumns; if (lineNumber < 0) { lineNumber = 0; columnNumber = 0; + softWrapLines = 0; + linesFromCurrentSoftWrap = 0; + softWrapColumns = 0; } final int totalLines = myDocument.getLineCount(); @@ -2394,6 +2546,9 @@ public final class EditorImpl extends UserDataHolderBase implements EditorEx, Hi int lineEndColumnNumber = calcColumnNumber(lineEndOffset, lineNumber); if (columnNumber > lineEndColumnNumber) { columnNumber = lineEndColumnNumber; + if (softWrapColumns != 0) { + softWrapColumns -= columnNumber - lineEndColumnNumber; + } } } @@ -2406,7 +2561,7 @@ public final class EditorImpl extends UserDataHolderBase implements EditorEx, Hi } } } - LogicalPosition pos1 = new LogicalPosition(lineNumber, columnNumber); + LogicalPosition pos1 = new LogicalPosition(lineNumber, columnNumber, softWrapLines, linesFromCurrentSoftWrap, softWrapColumns); getCaretModel().moveToLogicalPosition(pos1); } @@ -3197,7 +3352,7 @@ public final class EditorImpl extends UserDataHolderBase implements EditorEx, Hi public void setVerticalScrollbarOrientation(int type) { assertIsDispatchThread(); int currentHorOffset = myScrollingModel.getHorizontalScrollOffset(); - myScrollbarOrientation = type; + myScrollBarOrientation = type; if (type == VERTICAL_SCROLLBAR_LEFT) { myScrollPane.setLayout(new LeftHandScrollbarLayout()); } @@ -3226,7 +3381,7 @@ public final class EditorImpl extends UserDataHolderBase implements EditorEx, Hi } int getVerticalScrollbarOrientation() { - return myScrollbarOrientation; + return myScrollBarOrientation; } MyScrollBar getVerticalScrollBar() { diff --git a/platform/platform-impl/src/com/intellij/openapi/editor/impl/SettingsImpl.java b/platform/platform-impl/src/com/intellij/openapi/editor/impl/SettingsImpl.java index b7a82b17ec26..17085851d78e 100644 --- a/platform/platform-impl/src/com/intellij/openapi/editor/impl/SettingsImpl.java +++ b/platform/platform-impl/src/com/intellij/openapi/editor/impl/SettingsImpl.java @@ -72,6 +72,7 @@ public class SettingsImpl implements EditorSettings { private Boolean myIsMouseClickSelectionHonorsCamelWords = null; private Boolean myIsRenameVariablesInplace = null; private Boolean myIsRefrainFromScrolling = null; + private Boolean myUseSoftWraps = null; public boolean isRightMarginShown() { return myIsRightMarginShown != null @@ -383,6 +384,18 @@ public class SettingsImpl implements EditorSettings { myIsRefrainFromScrolling = b ? Boolean.TRUE : Boolean.FALSE; } + public boolean isUseSoftWraps() { + return myUseSoftWraps != null ? myUseSoftWraps.booleanValue() + : EditorSettingsExternalizable.getInstance().isUseSoftWraps(); + } + + public void setUseSoftWraps(boolean use) { + final Boolean newValue = use ? Boolean.TRUE : Boolean.FALSE; + if (newValue.equals(myUseSoftWraps)) return; + myUseSoftWraps = newValue; + fireEditorRefresh(); + } + private void fireEditorRefresh() { if (myEditor != null) { myEditor.reinitSettings(); 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 new file mode 100644 index 000000000000..20247b9faa53 --- /dev/null +++ b/platform/platform-impl/src/com/intellij/openapi/editor/impl/SoftWrapModelImpl.java @@ -0,0 +1,365 @@ +/* + * Copyright 2000-2010 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.openapi.editor.*; +import com.intellij.openapi.editor.event.VisibleAreaEvent; +import com.intellij.openapi.editor.event.VisibleAreaListener; +import com.intellij.openapi.editor.ex.util.EditorUtil; +import com.intellij.openapi.editor.TextChange; +import com.intellij.openapi.util.text.StringUtil; +import com.intellij.util.text.CharArrayUtil; +import gnu.trove.TIntArrayList; +import gnu.trove.TIntObjectHashMap; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.awt.*; + +/** + * //TODO den add doc + *

+ * Not thread-safe. + * + * @author Denis Zhdanov + * @since Jun 8, 2010 12:47:32 PM + */ +public class SoftWrapModelImpl implements SoftWrapModel { + + private static final TIntObjectHashMap EMPTY = new TIntObjectHashMap(); + + /** Holds mappings like {@code 'soft wrap start offset at document' -> 'soft wrap indent spaces'}. */ + private final TIntObjectHashMap myWraps = new TIntObjectHashMap(); + private final EditorImpl myEditor; + private int myFirstVisibleSymbolOffset = -1; + private int myFirstVisibleLine = -1; + private int myRightEdgeLocation = -1; + private boolean myInitialized; + + /** + * There is a possible case that visible area is changed but document representation of shown at viewport is still the same + * (e.g. the document doesn't contain long lines and viewport width is expanded). + *

+ * We don't drop previously registered soft wraps during visible area change then but mark them as 'dirty' in order to + * drop when document content is being repainted. + */ + private boolean myDataIsDirty; + + /** Holds number of 'active' calls, i.e. number of methods calls of the current object within the current call stack. */ + private int myActive; + + public SoftWrapModelImpl(@NotNull EditorImpl editor) { + myEditor = editor; + } + + public boolean isSoftWrappingEnabled() { + return myEditor.getSettings().isUseSoftWraps(); + } + + //TODO den add doc + public boolean shouldWrap(char[] chars, int start, int end, Point position) { + if (!isSoftWrappingEnabled() || containsOnlyWhiteSpaces(chars, start, end)) { + return false; + } + initIfNecessary(); + dropDataIfNecessary(); + + if (myWraps.contains(start)) { + return true; + } + + if (myRightEdgeLocation < 0) { + assert false; + return false; + } + + //TODO den implement + boolean b = position.x + (end - start) * 7 > myRightEdgeLocation; + //TODO den remove + if (b) { + int i = 1; + } + return b; + } + + private static boolean containsOnlyWhiteSpaces(char[] chars, int start, int end) { + int i = CharArrayUtil.shiftForward(chars, start, "\n \t"); + return i >= end; + } + + //TODO den add doc + public TextChange wrap(int offset) { + TextChange result = myWraps.get(offset); + if (result != null) { + return result; + } + + dropDataIfNecessary(); + result = new TextChange("\n ", offset); //TODO den implement indent calculation on formatting options basis. + myWraps.put(offset, result); + return result; + } + + /** + * Drops information about registered soft wraps if they are marked as 'dirty'. + * + * @see #myDataIsDirty + */ + private void dropDataIfNecessary() { + if (myDataIsDirty) { + myWraps.clear(); + myDataIsDirty = false; + } + } + + public TextChange getSoftWrap(int offset) { + return myWraps.get(offset); + } + + public LogicalPosition adjustLogicalPositionIfNecessary(LogicalPosition logical, VisualPosition visual) { + if (myActive > 0 || !isSoftWrappingEnabled() || myWraps.isEmpty()) { + return logical; + } + myActive++; + try { + return doAdjustLogicalPositionIfNecessary(logical, visual); + } + finally { + myActive--; + } + } + + @SuppressWarnings({"AssignmentToForLoopParameter"}) + private LogicalPosition doAdjustLogicalPositionIfNecessary(LogicalPosition logical, VisualPosition visual) { + CharSequence chars = ((DocumentImpl)myEditor.getDocument()).getCharsNoThreadCheck(); + int softWrapIntroducedLines = 0; + int linesFromCurrentSoftWrap = 0; + int symbolsOnCurrentLogicalLine = 0; + int symbolsOnCurrentVisibleLine = 0; + int softWrapsSymbolsOnCurrentVisibleLine = 0; + + FoldingModel foldingModel = myEditor.getFoldingModel(); + int currentLine = myFirstVisibleLine; + for (int i = myFirstVisibleSymbolOffset, max = chars.length(); i < max && currentLine <= visual.line; i++) { + if (currentLine == visual.line) { + if (symbolsOnCurrentVisibleLine >= visual.column) { + int softWrapColumns = softWrapsSymbolsOnCurrentVisibleLine > 0 + ? symbolsOnCurrentVisibleLine - symbolsOnCurrentLogicalLine : 0; + return new LogicalPosition( + logical.line - softWrapIntroducedLines, symbolsOnCurrentLogicalLine, softWrapIntroducedLines, + linesFromCurrentSoftWrap, softWrapColumns + ); + } + } + + FoldRegion region = foldingModel.getCollapsedRegionAtOffset(i); + if (region != null && !region.isExpanded()) { + // Assuming that folded region placeholder doesn't contain line feed symbols. + i = region.getEndOffset(); + continue; + } + + TextChange softWrap = myWraps.get(i); + if (softWrap != null) { + CharSequence softWrapText = softWrap.getText(); + for (int j = 0; j < softWrapText.length(); j++) { + if (currentLine == visual.line && symbolsOnCurrentVisibleLine >= visual.column) { + return new LogicalPosition( + logical.line - softWrapIntroducedLines, symbolsOnCurrentLogicalLine, softWrapIntroducedLines, + linesFromCurrentSoftWrap, softWrapsSymbolsOnCurrentVisibleLine - symbolsOnCurrentLogicalLine + ); + } + + if (softWrapText.charAt(j) == '\n') { + if (currentLine == visual.line) { + return new LogicalPosition( + logical.line - softWrapIntroducedLines, symbolsOnCurrentLogicalLine, softWrapIntroducedLines, + linesFromCurrentSoftWrap, visual.column - symbolsOnCurrentLogicalLine + ); + } + else { + softWrapIntroducedLines++; + linesFromCurrentSoftWrap++; + currentLine++; + symbolsOnCurrentVisibleLine = 0; + softWrapsSymbolsOnCurrentVisibleLine = 0; + } + } + else { + symbolsOnCurrentVisibleLine++; + softWrapsSymbolsOnCurrentVisibleLine++; + } + } + } + + if (currentLine == visual.line && symbolsOnCurrentVisibleLine >= visual.column) { + return new LogicalPosition( + logical.line - softWrapIntroducedLines, symbolsOnCurrentLogicalLine, softWrapIntroducedLines, + linesFromCurrentSoftWrap, softWrapsSymbolsOnCurrentVisibleLine - symbolsOnCurrentLogicalLine + ); + } + + char c = chars.charAt(i); + + // Check if there is a line break at the document. + if (c == '\n') { + if (currentLine == visual.line) { + int columnToUse = symbolsOnCurrentLogicalLine + visual.column - symbolsOnCurrentVisibleLine; + return new LogicalPosition( + logical.line - softWrapIntroducedLines, columnToUse, softWrapIntroducedLines, + linesFromCurrentSoftWrap, visual.column - columnToUse + ); + } + else { + currentLine++; + linesFromCurrentSoftWrap = 0; + symbolsOnCurrentVisibleLine = 0; + symbolsOnCurrentLogicalLine = 0; + softWrapsSymbolsOnCurrentVisibleLine = 0; + } + } + else { + symbolsOnCurrentLogicalLine++; + symbolsOnCurrentVisibleLine++; + } + } + return logical; + } + + public int getSoftWrapLineFeedsBefore(VisualPosition position) { + if (myActive > 0 || !isSoftWrappingEnabled() || myWraps.isEmpty()) { + return 0; + } + myActive++; + try { + return doGetSoftWrappedLinesFor(position); + } + finally { + myActive--; + } + } + + @SuppressWarnings({"AssignmentToForLoopParameter"}) + private int doGetSoftWrappedLinesFor(VisualPosition position) { + // It's assumed that there are two possible cases when visual position may differ from logical - folding and soft wraps. + // The main idea is to start from document offset that corresponds to the top left position of visible editor area and count + // number of occurred soft wraps before the line of given position (avoiding to count soft wraps from folded regions) + CharSequence chars = ((DocumentImpl)myEditor.getDocument()).getCharsNoThreadCheck(); + int result = 0; + int processedLinesNumber = 0; + FoldingModel foldingModel = myEditor.getFoldingModel(); + int maxLinesToCheck = position.line - myFirstVisibleLine; // Number of the first line that shouldn't be checked. + int charsInRow = 0; + for (int i = myFirstVisibleSymbolOffset, max = chars.length(); i < max && processedLinesNumber < maxLinesToCheck; i++) { + // There is a possible situation that there is a soft wrap at the end of the row denoted by given position. + // We need to avoid counting it then, hence, we stop processing if current line is a line of visual position + // and we checked necessary number of chars. + if (processedLinesNumber == maxLinesToCheck && charsInRow >= position.column) { + break; + } + + TextChange softWrap = myWraps.get(i); + if (softWrap != null) { + CharSequence softWrapText = softWrap.getText(); + for (int j = 0; j < softWrapText.length(); j++) { + if (softWrapText.charAt(j) == '\n') { + processedLinesNumber++; + charsInRow = -1; // We set this to '-1' assuming that it's incremented in loop's 'update' block + continue; + } + charsInRow += charToVisibleSymbolsNumber(softWrapText.charAt(j)); + } + continue; + } + + char c = chars.charAt(i); + + // Check if there is a line break at the document. + if (c == '\n') { + processedLinesNumber++; + charsInRow = -1; // We set this to '-1' assuming that it's incremented in loop's 'update' block + continue; + } + + FoldRegion region = foldingModel.getCollapsedRegionAtOffset(i); + if (region != null && !region.isExpanded()) { + i = region.getEndOffset(); + // Assuming that folded region is not represented in more than one line. + charsInRow += region.getPlaceholderText().length() - 1; + continue; + } + + charsInRow += charToVisibleSymbolsNumber(c); + } + return result; + } + + private int charToVisibleSymbolsNumber(char c) { + if (c != '\t') { + return 1; + } + int result = EditorUtil.getTabSize(myEditor); + return result <= 0 ? 1 : result; + } + + private void initIfNecessary() { + if (myInitialized) { + return; + } + myInitialized = true; + ScrollingModel scrollingModel = myEditor.getScrollingModel(); + scrollingModel.addVisibleAreaListener(new VisibleAreaListener() { + public void visibleAreaChanged(VisibleAreaEvent e) { + updateVisibleAreaChange(e.getNewRectangle()); + } + }); + updateVisibleAreaChange(scrollingModel.getVisibleArea()); + } + + private void updateVisibleAreaChange(@Nullable Rectangle visibleArea) { + if (visibleArea == null || !isSoftWrappingEnabled()) { + return; + } + myActive++; + try { + doUpdateVisibleAreaChange(visibleArea); + } + finally { + myActive--; + } + } + + /** + * Notifies current model that visible rectangle is changed in order for it to update the state accordingly. + *

+ * This method is introduced mostly for business-logic vs try/finally separation. + * + * @param visibleArea current visible area of the editor managed by the current model + */ + private void doUpdateVisibleAreaChange(@NotNull Rectangle visibleArea) { + // Update information about the first visible line. + LogicalPosition logicalPositionOfVisibleAreaStart = myEditor.xyToLogicalPosition(visibleArea.getLocation()); + myFirstVisibleSymbolOffset = myEditor.logicalPositionToOffset(logicalPositionOfVisibleAreaStart); + myFirstVisibleLine = myEditor.logicalToVisualPosition(logicalPositionOfVisibleAreaStart).line; + + // Update right edge. + int currentRightEdgeLocation = visibleArea.x + visibleArea.width; + if (myRightEdgeLocation != currentRightEdgeLocation) { + myDataIsDirty = true; + myRightEdgeLocation = currentRightEdgeLocation; + } + } +} diff --git a/platform/platform-resources-en/src/messages/ApplicationBundle.properties b/platform/platform-resources-en/src/messages/ApplicationBundle.properties index 3d53c996f90d..8676e25d3ca4 100644 --- a/platform/platform-resources-en/src/messages/ApplicationBundle.properties +++ b/platform/platform-resources-en/src/messages/ApplicationBundle.properties @@ -298,6 +298,7 @@ radio.close.less.frequently.used.files=Close less frequently used files radio.close.non.modified.files.first=Close non-modified files first label.when.number.of.opened.editors.exceeds.tab.limit=When number of opened editors exceeds tab limit: group.virtual.space=Virtual Space +checkbox.use.soft.wraps=Use soft wraps checkbox.allow.placement.of.caret.after.end.of.line=Allow placement of caret after end of line checkbox.allow.placement.of.caret.inside.tabs=Allow placement of caret inside tabs checkbox.show.virtual.space.at.file.bottom=Show virtual space at file bottom