// 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;
import com.google.collide.shared.document.anchor.Anchor;
import com.google.collide.shared.document.util.LineUtils;
/*
* Implementation notes:
*
* - This class needs to efficiently resolve line numbers or lines. To do this,
* it relies heavily on anchors with line numbers in the document. Basically, it
* finds the closest anchor with line number, and then iterates to the line of
* interest. Most document edits will originate on a line with an anchor
* (local/collaborator cursors use anchors), so the common case is fast.
*/
/**
* Helper to efficiently resolve a line number given the line, or vice versa.
*/
public class LineFinder {
private final Document document;
LineFinder(Document document) {
this.document = document;
}
/**
* Finds the closest {@link LineInfo} for the given line number (must be
* within the document range). Use {@link #findLine(LineInfo, int)} if you
* know of a good starting point for the search.
*/
public LineInfo findLine(int targetLineNumber) {
if (targetLineNumber >= document.getLineCount()) {
throw new IndexOutOfBoundsException("Asking for " + targetLineNumber
+ " but document length is " + document.getLineCount());
}
int distanceFromFirstLine = targetLineNumber;
int distanceFromLastLine = document.getLineCount() - targetLineNumber - 1;
int distanceFromClosestLineAnchor;
Anchor closestLineAnchor =
document.getAnchorManager().findClosestAnchorWithLineNumber(targetLineNumber);
if (closestLineAnchor != null) {
distanceFromClosestLineAnchor =
Math.abs(closestLineAnchor.getLineInfo().number() - targetLineNumber);
} else {
distanceFromClosestLineAnchor = Integer.MAX_VALUE;
}
LineInfo lineInfo;
if (distanceFromClosestLineAnchor < distanceFromFirstLine
&& distanceFromClosestLineAnchor < distanceFromLastLine) {
lineInfo = closestLineAnchor.getLineInfo();
} else if (distanceFromFirstLine < distanceFromLastLine) {
lineInfo = new LineInfo(document.getFirstLine(), 0);
} else {
lineInfo = new LineInfo(document.getLastLine(), document.getLineCount() - 1);
}
return findLine(lineInfo, targetLineNumber);
}
public LineInfo findLine(Line line) {
Line forwardIteratingLine = line;
int forwardLineCount = 0;
Line backwardIteratingLine = line.getPreviousLine();
int backwardLineCount = 1;
while (forwardIteratingLine != null && backwardIteratingLine != null) {
LineInfo cachedLineInfo = LineUtils.getCachedLineInfo(forwardIteratingLine);
if (cachedLineInfo != null) {
return new LineInfo(line, cachedLineInfo.number() - forwardLineCount);
}
cachedLineInfo = LineUtils.getCachedLineInfo(backwardIteratingLine);
if (cachedLineInfo != null) {
return new LineInfo(line, cachedLineInfo.number() + backwardLineCount);
}
backwardIteratingLine = backwardIteratingLine.getPreviousLine();
backwardLineCount++;
forwardIteratingLine = forwardIteratingLine.getNextLine();
forwardLineCount++;
}
if (forwardIteratingLine == null) {
return new LineInfo(line, line.getDocument().getLineCount() - forwardLineCount);
} else {
return new LineInfo(line, backwardLineCount - 1);
}
}
/*
* TODO: really, this should be merged with the other findLine
* which would then just consider {@code begin} as another known line number
* to begin the search (along with top, bottom, and closest anchor).
*/
/**
* Finds the closest {@link LineInfo} for the given line number. This iterates
* from the given {@code begin}. Use {@link #findLine(int)} if you DO NOT know
* of a good starting point for the search.
*/
public LineInfo findLine(LineInfo begin, int targetLineNumber) {
if (targetLineNumber >= document.getLineCount()) {
throw new IndexOutOfBoundsException("Asking for " + targetLineNumber
+ " but document length is " + document.getLineCount());
}
if (begin == null) {
return findLine(targetLineNumber);
}
Line line = begin.line();
int number = begin.number();
// TODO: see if there's a closer anchor
if (number < targetLineNumber) {
while (line.getNextLine() != null && number < targetLineNumber) {
line = line.getNextLine();
number++;
}
} else if (number > targetLineNumber) {
while (line.getPreviousLine() != null && number > targetLineNumber) {
line = line.getPreviousLine();
number--;
}
}
return new LineInfo(line, number);
}
}