// Copyright 2012 Google Inc. All Rights Reserved.
//
// 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.google.collide.shared.document.util;
import com.google.collide.shared.document.Document;
import com.google.collide.shared.document.Line;
import com.google.collide.shared.document.LineFinder;
import com.google.collide.shared.document.LineInfo;
import com.google.collide.shared.document.Position;
import com.google.collide.shared.document.PositionOutOfBoundsException;
import com.google.collide.shared.document.anchor.Anchor;
import com.google.collide.shared.util.TextUtils;
import com.google.common.base.Preconditions;
/**
* Utility methods for line manipulations.
*/
public final class LineUtils {
/**
* Interface for a visitor of lines.
*/
public interface LineVisitor {
/**
* @return true to continue visiting more lines, false to abort the visit
*/
boolean accept(Line line, int lineNumber, int beginColumn, int endColumn);
}
/*
* TODO: Move to newly created PositionUtils (in a future
* standalone CL since this has a lot of callers)
*/
/**
* Returns a negative number if {@code a} is earlier than {@code b}, a
* positive number if {@code a} is later than {@code b}, and zero if they are
* the same.
*/
public static int comparePositions(int aLineNumber, int aColumn, int bLineNumber, int bColumn) {
int lineNumberDelta = aLineNumber - bLineNumber;
return lineNumberDelta != 0 ? lineNumberDelta : aColumn - bColumn;
}
/**
* Returns a cached {@link LineInfo} for this {@link Line} if it exists, or
* null.
*/
public static LineInfo getCachedLineInfo(Line line) {
Anchor anchorWithLineNumber =
line.getDocument().getAnchorManager().findAnchorWithLineNumber(line);
return anchorWithLineNumber != null ? anchorWithLineNumber.getLineInfo() : null;
}
/**
* Returns a cached line number for this line if it exists, or -1.
*/
public static int getCachedLineNumber(Line line) {
Anchor anchorWithLineNumber =
line.getDocument().getAnchorManager().findAnchorWithLineNumber(line);
return anchorWithLineNumber != null ? anchorWithLineNumber.getLineNumber() : -1;
}
/**
* Returns the last column (inclusive) of the line, or -1 if the line is
* empty.
*/
public static int getLastColumn(Line line) {
return line.getText().length() - 1;
}
/**
* Returns the first column for the given line where a cursor can be placed.
* This will skip any strange control characters or zero-width characters that
* are prefixing a line.
*/
public static int getFirstCursorColumn(Line line) {
String lineText = line.getText();
return TextUtils.findNonMarkNorOtherCharacter(lineText, -1);
}
/**
* Returns the max column for the given line where a cursor can be placed.
* This can also be thought of as the index after the last character in the
* line (the newline does not count as a character here.)
*/
public static int getLastCursorColumn(Line line) {
// "- 1" because we cannot position after the invisible newline
return getLastCursorColumn(line.getText());
}
/**
* @see #getLastCursorColumn(Line)
*/
public static int getLastCursorColumn(String lineText) {
return lineText.endsWith("\n") ? lineText.length() - 1 : lineText.length();
}
/**
* Iterates to and returns the line that is {@code relativePosition} away from
* the given {@code line}. If {@code relativePosition} is large, consider
* using {@link LineFinder}.
*/
public static Line getLine(Line line, int relativePosition) {
if (relativePosition > 0) {
for (; line != null && relativePosition > 0; relativePosition--) {
line = line.getNextLine();
}
} else {
for (; line != null && relativePosition < 0; relativePosition++) {
line = line.getPreviousLine();
}
}
return line;
}
/**
* Gets the number of characters (including newlines) in the given inclusive
* range.
*/
public static int getTextCount(Line beginLine, int beginColumn, Line endLine, int endColumn) {
Preconditions.checkArgument(beginLine.isAttached(), "beginLine must be attached");
Preconditions.checkArgument(endLine.isAttached(), "endLine must be attached");
if (beginLine == endLine) {
return endColumn - beginColumn + 1;
}
int count = beginLine.getText().length() - beginColumn;
Line line = beginLine.getNextLine();
while (line != null && line != endLine) {
count += line.getText().length();
line = line.getNextLine();
}
if (line == null) {
throw new IndexOutOfBoundsException("can't find endLine");
}
count += endColumn + 1;
return count;
}
/**
* Gets the text from the beginning position to the end position (inclusive).
*/
public static String getText(Line beginLine, int beginColumn, Line endLine, int endColumn) {
if (beginLine == endLine) {
return beginLine.getText().substring(beginColumn, endColumn + 1);
}
StringBuilder s = new StringBuilder(beginLine.getText().substring(beginColumn));
Line line = beginLine.getNextLine();
while (line != null && line != endLine) {
s.append(line.getText());
line = line.getNextLine();
}
if (line == null) {
throw new IndexOutOfBoundsException();
}
s.append(endLine.getText().substring(0, endColumn + 1));
return s.toString();
}
/**
* Returns a line number in the range of the document closest to the
* {@code targetLineNumber}.
*/
public static int getValidLineNumber(int targetLineNumber, Document document) {
int lastLineNumber = document.getLastLineNumber();
if (targetLineNumber <= 0) {
return 0;
} else if (targetLineNumber >= lastLineNumber) {
return lastLineNumber;
} else {
return targetLineNumber;
}
}
/**
* Returns the target column, or the max column for the line if the target
* column is too large, or 0 if the target column is negative.
*/
public static int rubberbandColumn(Line line, int targetColumn) {
return (int) rubberbandColumn(line, (double) targetColumn);
}
public static double rubberbandColumn(Line line, double targetColumn) {
if (targetColumn < 0) {
return 0;
}
int maxColumnFromLineText = getLastCursorColumn(line);
return maxColumnFromLineText < targetColumn ? maxColumnFromLineText : targetColumn;
}
static Position getPositionBackwards(Line line, int lineNumber, int column,
int numCharsToTraverse) {
while (numCharsToTraverse > 0) {
int remainingCharsOnThisLine = column;
/*
* In the case that remainingCharsOnLine == numCharsToTraverse, we want to
* move to the first column of this line
*/
if (remainingCharsOnThisLine < numCharsToTraverse) {
// Skip over this line
line = line.getPreviousLine();
if (line == null) {
throw new PositionOutOfBoundsException(
"Reached the document beginning, but still wanted to go " + numCharsToTraverse
+ " characters backwards.");
}
lineNumber--;
column = line.getText().length();
numCharsToTraverse -= remainingCharsOnThisLine;
} else {
// It's within this line
column -= numCharsToTraverse;
numCharsToTraverse = 0;
}
}
return new Position(new LineInfo(line, lineNumber), column);
}
static Position getPositionForward(Line line, int lineNumber, int column,
int numCharsToTraverse) {
while (numCharsToTraverse > 0) {
int remainingCharsOnThisLine = line.getText().length() - column;
/*
* In the case that remainingCharsOnLine == numCharsToTraverse, we want to
* move to the first column of the next line
*/
if (remainingCharsOnThisLine <= numCharsToTraverse) {
if (line.getNextLine() == null) {
/*
* For the last line we have no newline character and want to move to
* the line end
*/
if (remainingCharsOnThisLine < numCharsToTraverse) {
throw new PositionOutOfBoundsException(
"Reached the document end, but still wanted to go "
+ (numCharsToTraverse - remainingCharsOnThisLine) + " characters forward.");
} else {
column += numCharsToTraverse;
}
} else {
// Skip over this line
line = line.getNextLine();
lineNumber++;
column = 0;
}
numCharsToTraverse -= remainingCharsOnThisLine;
} else {
// It's within this line
column += numCharsToTraverse;
numCharsToTraverse = 0;
}
}
return new Position(new LineInfo(line, lineNumber), column);
}
}