2 * Copyright 2000-2009 JetBrains s.r.o.
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
8 * http://www.apache.org/licenses/LICENSE-2.0
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
16 package com.intellij.openapi.editor.ex.util;
18 import com.intellij.openapi.application.Result;
19 import com.intellij.openapi.application.WriteAction;
20 import com.intellij.openapi.editor.*;
21 import com.intellij.openapi.editor.colors.EditorColorsScheme;
22 import com.intellij.openapi.editor.ex.EditorEx;
23 import com.intellij.openapi.editor.impl.ComplementaryFontsRegistry;
24 import com.intellij.openapi.editor.impl.FontInfo;
25 import com.intellij.openapi.editor.impl.IterationState;
26 import com.intellij.openapi.util.Pair;
27 import com.intellij.openapi.util.text.StringUtil;
28 import org.jetbrains.annotations.NotNull;
31 import java.util.List;
33 public class EditorUtil {
34 private EditorUtil() { }
36 public static int getLastVisualLineColumnNumber(Editor editor, int line) {
37 Document document = editor.getDocument();
38 int lastLine = document.getLineCount() - 1;
43 // Filter all lines that are not shown because of collapsed folding region.
44 VisualPosition visStart = new VisualPosition(line, 0);
45 LogicalPosition logStart = editor.visualToLogicalPosition(visStart);
46 int lastLogLine = logStart.line;
47 while (lastLogLine < document.getLineCount() - 1) {
48 logStart = new LogicalPosition(logStart.line + 1, logStart.column);
49 VisualPosition tryVisible = editor.logicalToVisualPosition(logStart);
50 if (tryVisible.line != visStart.line) break;
51 lastLogLine = logStart.line;
54 int resultLogLine = Math.min(lastLogLine, lastLine);
55 VisualPosition resVisStart = editor.offsetToVisualPosition(document.getLineStartOffset(resultLogLine));
56 VisualPosition resVisEnd = editor.offsetToVisualPosition(document.getLineEndOffset(resultLogLine));
58 // Target logical line is not soft wrap affected.
59 if (resVisStart.line == resVisEnd.line) {
60 return resVisEnd.column;
63 int visualLinesToSkip = line - resVisStart.line;
64 List<? extends SoftWrap> softWraps = editor.getSoftWrapModel().getSoftWrapsForLine(resultLogLine);
65 for (int i = 0; i < softWraps.size(); i++) {
66 SoftWrap softWrap = softWraps.get(i);
67 CharSequence text = document.getCharsSequence();
68 if (visualLinesToSkip <= 0) {
69 VisualPosition visual = editor.offsetToVisualPosition(softWrap.getStart() - 1);
70 int result = visual.column;
71 int x = editor.visualPositionToXY(visual).x;
72 // We need to add width of the next symbol because current result column points to the last symbol before the soft wrap.
73 return result + textWidthInColumns(editor, text, softWrap.getStart() - 1, softWrap.getStart(), x);
76 int softWrapLineFeeds = StringUtil.countNewLines(softWrap.getText());
77 if (softWrapLineFeeds < visualLinesToSkip) {
78 visualLinesToSkip -= softWrapLineFeeds;
82 // Target visual column is located on the last visual line of the current soft wrap.
83 if (softWrapLineFeeds == visualLinesToSkip) {
84 if (i >= softWraps.size() - 1) {
85 return resVisEnd.column;
87 // We need to find visual column for line feed of the next soft wrap.
88 SoftWrap nextSoftWrap = softWraps.get(i + 1);
89 VisualPosition visual = editor.offsetToVisualPosition(nextSoftWrap.getStart() - 1);
90 int result = visual.column;
91 int x = editor.visualPositionToXY(visual).x;
93 // We need to add symbol width because current column points to the last symbol before the next soft wrap;
94 result += textWidthInColumns(editor, text, nextSoftWrap.getStart() - 1, nextSoftWrap.getStart(), x);
96 int lineFeedIndex = StringUtil.indexOf(nextSoftWrap.getText(), '\n');
97 result += textWidthInColumns(editor, nextSoftWrap.getText(), 0, lineFeedIndex, 0);
101 // Target visual column is the one before line feed introduced by the current soft wrap.
102 int softWrapStartOffset = 0;
103 int softWrapEndOffset = 0;
104 int softWrapTextLength = softWrap.getText().length();
105 while (visualLinesToSkip-- > 0) {
106 softWrapStartOffset = softWrapEndOffset + 1;
107 if (softWrapStartOffset >= softWrapTextLength) {
109 return resVisEnd.column;
111 softWrapEndOffset = StringUtil.indexOf(softWrap.getText(), '\n', softWrapStartOffset, softWrapTextLength);
112 if (softWrapEndOffset < 0) {
114 return resVisEnd.column;
117 VisualPosition visual = editor.offsetToVisualPosition(softWrap.getStart() - 1);
118 int result = visual.column; // Column of the symbol just before the soft wrap
119 int x = editor.visualPositionToXY(visual).x;
121 // Target visual column is located on the last visual line of the current soft wrap.
122 result += textWidthInColumns(editor, text, softWrap.getStart() - 1, softWrap.getStart(), x);
123 result += calcColumnNumber(editor, softWrap.getText(), softWrapStartOffset, softWrapEndOffset);
128 return resVisEnd.column;
131 public static float calcVerticalScrollProportion(Editor editor) {
132 Rectangle viewArea = editor.getScrollingModel().getVisibleAreaOnScrollingFinished();
133 if (viewArea.height == 0) {
136 LogicalPosition pos = editor.getCaretModel().getLogicalPosition();
137 Point location = editor.logicalPositionToXY(pos);
138 return (location.y - viewArea.y) / (float) viewArea.height;
141 public static void setVerticalScrollProportion(Editor editor, float proportion) {
142 Rectangle viewArea = editor.getScrollingModel().getVisibleArea();
143 LogicalPosition caretPosition = editor.getCaretModel().getLogicalPosition();
144 Point caretLocation = editor.logicalPositionToXY(caretPosition);
145 int yPos = caretLocation.y;
146 yPos -= viewArea.height * proportion;
147 editor.getScrollingModel().scrollVertically(yPos);
150 public static void fillVirtualSpaceUntilCaret(final Editor editor) {
151 final LogicalPosition position = editor.getCaretModel().getLogicalPosition();
152 fillVirtualSpaceUntil(editor, position.column, position.line);
155 public static void fillVirtualSpaceUntil(final Editor editor, int columnNumber, int lineNumber) {
156 final int offset = editor.logicalPositionToOffset(new LogicalPosition(lineNumber, columnNumber));
157 final String filler = EditorModificationUtil.calcStringToFillVirtualSpace(editor);
158 if (filler.length() > 0) {
160 protected void run(final Result result) throws Throwable {
161 editor.getDocument().insertString(offset, filler);
162 editor.getCaretModel().moveToOffset(offset + filler.length());
169 * Allows to calculate offset of the given column assuming that it belongs to the given text line identified by the
170 * given <code>[start; end)</code> intervals.
172 * @param editor editor that is used for representing given text
173 * @param text target text
174 * @param start start offset of the logical line that holds target column (inclusive)
175 * @param end end offset of the logical line that holds target column (exclusive)
176 * @param columnNumber target column number
177 * @param tabSize number of desired visual columns to use for tabulation representation
178 * @return given text offset that identifies the same position that is pointed by the given visual column
180 public static int calcOffset(Editor editor, CharSequence text, int start, int end, int columnNumber, int tabSize) {
181 final int maxScanIndex = Math.min(start + columnNumber + 1, end);
182 SoftWrapModel softWrapModel = editor.getSoftWrapModel();
183 List<? extends SoftWrap> softWraps = softWrapModel.getSoftWrapsForRange(start, maxScanIndex);
184 int startToUse = start;
186 int[] currentColumn = {0};
187 for (SoftWrap softWrap : softWraps) {
188 // There is a possible case that target column points inside soft wrap-introduced virtual space.
189 if (currentColumn[0] >= columnNumber) {
192 int result = calcSoftWrapUnawareOffset(editor, text, startToUse, softWrap.getEnd(), columnNumber, tabSize, x, currentColumn);
197 startToUse = softWrap.getStart();
198 x = softWrap.getIndentInPixels();
201 // There is a possible case that target column points inside soft wrap-introduced virtual space.
202 if (currentColumn[0] >= columnNumber) {
206 int result = calcSoftWrapUnawareOffset(editor, text, startToUse, end, columnNumber, tabSize, x, currentColumn);
211 // We assume that given column points to the virtual space after the line end if control flow reaches this place,
212 // hence, just return end of line offset then.
217 * Tries to match given logical column to the document offset assuming that it's located at <code>[start; end)</code> region.
219 * @param editor editor that is used to represent target document
220 * @param text target document text
221 * @param start start offset to check (inclusive)
222 * @param end end offset to check (exclusive)
223 * @param columnNumber target logical column number
224 * @param tabSize user-defined desired number of columns to use for tabulation symbol representation
225 * @param x <code>'x'</code> coordinate that corresponds to the given <code>'start'</code> offset
226 * @param currentColumn logical column that corresponds to the given <code>'start'</code> offset
227 * @return target offset that belongs to the <code>[start; end)</code> range and points to the target logical
228 * column if any; <code>-1</code> otherwise
230 private static int calcSoftWrapUnawareOffset(Editor editor, CharSequence text, int start, int end, int columnNumber, int tabSize, int x,
233 // The main problem in a calculation is that target text may contain tabulation symbols and every such symbol may take different
234 // number of logical columns to represent. E.g. it takes two columns if tab size is four and current column is two; three columns
235 // if tab size is four and current column is one etc. So, first of all we check if there are tabulation symbols at the target
237 boolean useOptimization = true;
238 boolean hasNonTabs = false;
239 boolean hasTabs = false;
240 for (int i = start; i < end; i++) {
241 if (text.charAt(i) == '\t') {
244 useOptimization = false;
252 // Perform optimized processing if possible. 'Optimized' here means the processing when we exactly know how many logical
253 // columns are occupied by tabulation symbols.
254 if (editor == null || useOptimization) {
256 int result = start + columnNumber - currentColumn[0];
261 currentColumn[0] += end - start;
266 // This variable holds number of 'virtual' tab-introduced columns, e.g. there is a possible case that particular tab owns
267 // three columns, hence, it increases 'shift' by two (3 - 1).
271 for (; offset < end && offset + shift + currentColumn[0] < start + columnNumber; offset++) {
272 if (text.charAt(offset) == '\t') {
273 int nextX = nextTabStop(prevX, editor, tabSize);
274 shift += columnsNumber(nextX - prevX, getSpaceWidth(Font.PLAIN, editor)) - 1;
278 int diff = start + columnNumber - offset - shift - currentColumn[0];
282 else if (diff == 0) {
286 currentColumn[0] += offset - start + shift;
291 // It means that there are tabulation symbols that can't be explicitly mapped to the occupied logical columns number,
292 // hence, we need to perform special calculations to get know that.
293 EditorEx editorImpl = (EditorEx)editor;
295 IterationState state = new IterationState(editorImpl, offset, false);
296 int fontType = state.getMergedAttributes().getFontType();
297 int column = currentColumn[0];
298 int spaceSize = getSpaceWidth(fontType, editorImpl);
299 for (; column < columnNumber && offset < end; offset++) {
300 if (offset >= state.getEndOffset()) {
302 fontType = state.getMergedAttributes().getFontType();
305 char c = text.charAt(offset);
308 x = nextTabStop(x, editorImpl);
309 column += columnsNumber(x - prevX, spaceSize);
312 x += charWidth(c, fontType, editorImpl);
317 if (column == columnNumber) {
320 if (column > columnNumber && offset > 0 && text.charAt(offset - 1) == '\t') {
323 currentColumn[0] = column;
327 private static int getTabLength(int colNumber, int tabSize) {
331 return tabSize - colNumber % tabSize;
334 public static int calcColumnNumber(Editor editor, CharSequence text, int start, int offset) {
335 return calcColumnNumber(editor, text, start, offset, getTabSize(editor));
338 public static int calcColumnNumber(Editor editor, CharSequence text, int start, int offset, int tabSize) {
339 boolean useOptimization = true;
340 if (editor != null) {
341 SoftWrap softWrap = editor.getSoftWrapModel().getSoftWrap(start);
342 useOptimization = softWrap == null;
344 if (useOptimization) {
345 boolean hasNonTabs = false;
346 for (int i = start; i < offset; i++) {
347 if (text.charAt(i) == '\t') {
349 useOptimization = false;
358 if (editor == null || useOptimization) {
361 for (int i = start; i < offset; i++) {
362 char c = text.charAt(i);
363 assert c != '\n' && c != '\r';
365 shift += getTabLength(i + shift - start, tabSize) - 1;
368 return offset - start + shift;
371 EditorEx editorImpl = (EditorEx) editor;
372 return editorImpl.calcColumnNumber(text, start, offset, tabSize);
375 public static void setHandCursor(Editor view) {
376 Cursor c = Cursor.getPredefinedCursor(Cursor.HAND_CURSOR);
377 // XXX: Workaround, simply view.getContentComponent().setCursor(c) doesn't work
378 if (view.getContentComponent().getCursor() != c) {
379 view.getContentComponent().setCursor(c);
383 public static FontInfo fontForChar(final char c, int style, Editor editor) {
384 EditorColorsScheme colorsScheme = editor.getColorsScheme();
385 return ComplementaryFontsRegistry.getFontAbleToDisplay(c, colorsScheme.getEditorFontSize(), style, colorsScheme.getEditorFontName());
388 public static int charWidth(char c, int fontType, Editor editor) {
389 return fontForChar(c, fontType, editor).charWidth(c, editor.getContentComponent());
392 public static int getSpaceWidth(int fontType, Editor editor) {
393 int width = charWidth(' ', fontType, editor);
394 return width > 0 ? width : 1;
397 public static int getTabSize(Editor editor) {
398 return editor.getSettings().getTabSize(editor.getProject());
401 public static int nextTabStop(int x, Editor editor) {
402 int tabSize = getTabSize(editor);
406 return nextTabStop(x, editor, tabSize);
409 public static int nextTabStop(int x, Editor editor, int tabSize) {
411 return x + getSpaceWidth(Font.PLAIN, editor);
413 tabSize *= getSpaceWidth(Font.PLAIN, editor);
415 int nTabs = x / tabSize;
416 return (nTabs + 1) * tabSize;
419 public static int textWidthInColumns(@NotNull Editor editor, CharSequence text, int start, int end, int x) {
420 int startToUse = start;
421 int lastTabSymbolIndex = -1;
423 // Skip all lines except the last.
425 for (int i = end - 1; i >= start; i--) {
426 switch (text.charAt(i)) {
427 case '\n': startToUse = i + 1; break loop;
428 case '\t': if (lastTabSymbolIndex < 0) lastTabSymbolIndex = i;
432 // Tabulation is assumed to be the only symbol which representation may take various number of visual columns, hence,
433 // we return eagerly if no such symbol is found.
434 if (lastTabSymbolIndex < 0) {
435 return end - startToUse;
440 int spaceSize = getSpaceWidth(Font.PLAIN, editor);
442 // Calculate number of columns up to the latest tabulation symbol.
443 for (int i = startToUse; i <= lastTabSymbolIndex; i++) {
444 SoftWrap softWrap = editor.getSoftWrapModel().getSoftWrap(i);
445 if (softWrap != null) {
446 x = softWrap.getIndentInPixels();
448 char c = text.charAt(i);
452 x = nextTabStop(x, editor);
453 result += columnsNumber(x - prevX, spaceSize);
455 case '\n': x = result = 0; break;
456 default: x += charWidth(c, Font.PLAIN, editor); result++;
460 // Add remaining tabulation-free columns.
461 result += end - lastTabSymbolIndex - 1;
466 * Allows to answer how many columns are necessary for representation of the given char on a screen.
468 * @param c target char
469 * @param x <code>'x'</code> coordinate of the line where given char is represented that indicates char end location
470 * @param prevX <code>'x'</code> coordinate of the line where given char is represented that indicates char start location
471 * @param spaceSize <code>'space'</code> symbol width
472 * @return number of columns necessary for representation of the given char on a screen.
474 public static int columnsNumber(char c, int x, int prevX, int spaceSize) {
478 int result = (x - prevX) / spaceSize;
479 if ((x - prevX) % spaceSize > 0) {
486 * Allows to answer how many visual columns are occupied by the given width.
488 * @param width target width
489 * @param spaceSize width of the single space symbol within the target editor
490 * @return number of visual columns are occupied by the given width
492 public static int columnsNumber(int width, int spaceSize) {
493 int result = width / spaceSize;
494 if (width % spaceSize > 0) {
501 * Allows to answer what width in pixels is required to draw fragment of the given char array from <code>[start; end)</code> interval
502 * at the given editor.
504 * Tabulation symbols is processed specially, i.e. it's ta
506 * <b>Note:</b> it's assumed that target text fragment remains to the single line, i.e. line feed symbols within it are not
509 * @param editor editor that will be used for target text representation
510 * @param text target text holder
511 * @param start offset within the given char array that points to target text start (inclusive)
512 * @param end offset within the given char array that points to target text end (exclusive)
513 * @param fontType font type to use for target text representation
514 * @param x <code>'x'</code> coordinate that should be used as a starting point for target text representation.
515 * It's necessity is implied by the fact that IDEA editor may represent tabulation symbols in any range
516 * from <code>[1; tab size]</code> (check {@link #nextTabStop(int, Editor)} for more details)
517 * @return width in pixels required for target text representation
519 public static int textWidth(@NotNull Editor editor, CharSequence text, int start, int end, int fontType, int x) {
521 for (int i = start; i < end; i++) {
522 char c = text.charAt(i);
524 FontInfo font = fontForChar(c, fontType, editor);
525 result += font.charWidth(c, editor.getContentComponent());
529 result += nextTabStop(x + result, editor) - result - x;
535 * Calculates the closest non-soft-wrapped logical positions for current caret position.
537 * @param editor target editor to use
538 * @return pair of non-soft-wrapped logical positions closest to the caret position of the given editor
540 public static Pair<LogicalPosition, LogicalPosition> calcCaretLinesRange(Editor editor) {
541 VisualPosition caret = editor.getCaretModel().getVisualPosition();
542 int visualLine = caret.line;
544 LogicalPosition lineStart = editor.visualToLogicalPosition(new VisualPosition(visualLine, 0));
545 while (lineStart.softWrapLinesOnCurrentLogicalLine > 0) {
546 lineStart = editor.visualToLogicalPosition(new VisualPosition(--visualLine, 0));
549 visualLine = caret.line + 1;
550 LogicalPosition nextLineStart = editor.visualToLogicalPosition(new VisualPosition(caret.line + 1, 0));
551 while (nextLineStart.line == lineStart.line) {
552 nextLineStart = editor.visualToLogicalPosition(new VisualPosition(++visualLine, 0));
554 return new Pair<LogicalPosition, LogicalPosition>(lineStart, nextLineStart);