// 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.client.editor.search;
import com.google.collide.client.editor.ViewportModel;
import com.google.collide.client.editor.search.SearchModel.SearchProgressListener;
import com.google.collide.client.util.IncrementalScheduler;
import com.google.collide.client.util.IncrementalScheduler.Task;
import com.google.collide.shared.document.Document;
import com.google.collide.shared.document.Line;
import com.google.collide.shared.document.LineInfo;
import com.google.collide.shared.document.anchor.Anchor;
import com.google.collide.shared.document.anchor.AnchorManager;
import com.google.collide.shared.document.anchor.AnchorType;
import com.google.collide.shared.document.anchor.Anchor.RemovalStrategy;
import com.google.common.base.Preconditions;
/**
* A class which can be used to iterate through the lines of a document. It will
* synchronously callback for each line in the viewport then asynchronously
* callback for the remaining lines in the document. The direction and start
* line within the viewport are configurable.
*/
public class SearchTask {
public interface SearchTaskExecutor {
/**
* Called for each line in the document as it is searched.
*
* @param line the current line
* @param number the current line number
* @param shouldRenderLine if this line is in the viewport and a render must
* be performed if any visible changes are made.
* @return false to stop the search.
*/
public boolean onSearchLine(Line line, int number, boolean shouldRenderLine);
}
/**
* The direction of the document search.
*/
public enum SearchDirection {
UP, DOWN
}
/**
* An object which simplifies searches by hiding the logic which depends on
* the search direction.
*/
private static class SearchDirectionHelper {
private final ViewportModel viewport;
private final Document document;
private SearchDirection direction;
public SearchDirectionHelper(ViewportModel viewport, Document document) {
this.viewport = viewport;
this.document = document;
}
/**
* Sets the direction of the search so the helper can return valid line
* information.
*/
public void setDirection(SearchDirection direction) {
this.direction = direction;
}
/**
* Gets the starting line of the viewport, bottom if
* {@link SearchDirection#DOWN}, top if {@link SearchDirection#UP}.
*/
public Line getViewportEndLine() {
return isGoingDown() ? viewport.getBottomLine() : viewport.getTopLine();
}
/**
* Gets the starting line of the viewport, bottom if
* {@link SearchDirection#DOWN}, top if {@link SearchDirection#UP}.
*/
public LineInfo getViewportEndLineInfo() {
return isGoingDown() ? viewport.getBottomLineInfo() : viewport.getTopLineInfo();
}
/**
* Gets the starting line of the viewport, top if
* {@link SearchDirection#DOWN}, bottom if {@link SearchDirection#UP}.
*/
public LineInfo getViewportStartLineInfo() {
return isGoingDown() ? viewport.getTopLineInfo() : viewport.getBottomLineInfo();
}
/**
* Returns the line necessary to wrap around the document. i.e. if you are
* searching down it will return the top, if you are searching up it will
* return the bottom.
*/
public LineInfo getWrapDocumentLine() {
return isGoingDown() ? document.getFirstLineInfo() : document.getLastLineInfo();
}
/**
* Returns if the search is going down.
*/
public boolean isGoingDown() {
return direction == SearchDirection.DOWN;
}
/**
* Returns true if the line to be wrapped to is not at the corresponding
* edge of the document. i.e. Don't wrap to the top of the document if the
* viewport is at the top already (which we've already scanned).
*/
public boolean canWrapDocument() {
boolean atEdge = isGoingDown() ? viewport.getTopLine() == document.getFirstLine() :
viewport.getBottomLine() == document.getLastLine();
return !atEdge;
}
}
/**
* Indicates that the search task should start a search starting at either the
* top or bottom of the viewport depending on the selected direction.
*/
public static final LineInfo DEFAULT_START_LINE = new LineInfo(null, -1);
private static final AnchorType SEARCH_TASK_ANCHOR =
AnchorType.create(SearchTask.class, "SearchAnchor");
private final ViewportModel viewport;
private final IncrementalScheduler scheduler;
private final Document document;
private final Task asyncSearchTask;
private final SearchDirectionHelper searchDirectionHelper;
private Anchor stopLineAnchor;
private Anchor searchTaskAnchor;
private SearchProgressListener progressListener;
private SearchTaskExecutor executor;
private boolean shouldWrapDocument = true;
public SearchTask(Document document, ViewportModel viewport, IncrementalScheduler scheduler) {
this.document = document;
this.viewport = viewport;
this.scheduler = scheduler;
this.searchDirectionHelper = new SearchDirectionHelper(viewport, document);
asyncSearchTask = new AsyncSearchTask();
}
public void teardown() {
scheduler.teardown();
removeSearchTaskAnchors();
}
/**
* Starts searching the document in the down direction starting at the
* default line.
*/
public void searchDocument(
SearchTaskExecutor executor, SearchProgressListener progressListener) {
searchDocumentStartingAtLine(
executor, progressListener, SearchDirection.DOWN, DEFAULT_START_LINE);
}
/**
* Starts searching the document in the down direction starting at the given
* line.
*/
public void searchDocument(
SearchTaskExecutor executor, SearchProgressListener progressListener, LineInfo startLine) {
searchDocumentStartingAtLine(
executor, progressListener, SearchDirection.DOWN, startLine);
}
/**
* Starts searching the document in the given direction starting at the
* default start line.
*/
public void searchDocument(SearchTaskExecutor executor, SearchProgressListener progressListener,
SearchDirection direction) {
searchDocumentStartingAtLine(executor, progressListener, direction, DEFAULT_START_LINE);
}
/**
* Starts searching the document at the given line in the viewport and in the
* specified direction. If the startLine is not within the viewport then
* behavior is undefined and terrible things will likely happen.
*/
public void searchDocumentStartingAtLine(SearchTaskExecutor executor,
SearchProgressListener progressListener, SearchDirection direction, LineInfo startLine) {
scheduler.cancel();
this.progressListener = progressListener;
this.executor = executor;
searchDirectionHelper.setDirection(direction);
if (startLine == DEFAULT_START_LINE) {
startLine = searchDirectionHelper.getViewportStartLineInfo();
}
dispatchSearchBegin();
boolean doAsyncSearch = true;
if (startLine.number() >= viewport.getTopLineNumber() &&
startLine.number() <= viewport.getBottomLineNumber()) {
doAsyncSearch = scanViewportStartingAtLine(startLine);
}
if (doAsyncSearch) {
setupSearchTaskAnchors(startLine);
scheduler.schedule(asyncSearchTask);
} else {
dispatchSearchDone();
}
}
/**
* Returns if the search will wrap around the document when it gets to the
* bottom or top.
*/
public boolean isShouldWrapDocument() {
return shouldWrapDocument;
}
/**
* Determines if the search should wrap around the document either from the
* top to the bottom or vice versa.
*/
public void setShouldWrapDocument(boolean shouldWrapDocument) {
this.shouldWrapDocument = shouldWrapDocument;
}
/**
* Cancels any currently running search task.
*/
public void cancelTask() {
scheduler.cancel();
}
/**
* Starts a scan of the viewport at the given line. If the given lineInfo is
* not a line within the viewport then behavior is undefined (and likely not
* going to end well).
*/
private boolean scanViewportStartingAtLine(LineInfo startLineInfo) {
Preconditions.checkArgument(
startLineInfo.number() >= viewport.getTopLineNumber() &&
startLineInfo.number() <= viewport.getBottomLineNumber(),
"Editor: Search start line number not within viewport.");
LineInfo lineInfo = startLineInfo.copy();
do {
if (!executor.onSearchLine(lineInfo.line(), lineInfo.number(), true)) {
return false;
}
} while (lineInfo.line() != searchDirectionHelper.getViewportEndLine()
&& lineInfo.moveTo(searchDirectionHelper.isGoingDown()));
/*
* If we stopped because lineInfo == endline then we need to continue async
* scanning, otherwise this moveTo call will fail and we won't bother. We
* also have to check for the case where the viewport was already scrolled
* to the very bottom or top of the document.
*/
return lineInfo.moveTo(searchDirectionHelper.isGoingDown())
|| (searchDirectionHelper.canWrapDocument() && shouldWrapDocument);
}
/**
* Removes the search task anchors after the task has completed.
*/
private void removeSearchTaskAnchors() {
if (stopLineAnchor != null) {
document.getAnchorManager().removeAnchor(stopLineAnchor);
stopLineAnchor = null;
}
if (searchTaskAnchor != null) {
document.getAnchorManager().removeAnchor(searchTaskAnchor);
searchTaskAnchor = null;
}
}
/**
* Sets an anchor at the top of the current viewport and one line below the
* end of the viewport so we can scan the rest of the document.
*/
private void setupSearchTaskAnchors(LineInfo stopLine) {
if (stopLineAnchor == null) {
stopLineAnchor = document.getAnchorManager().createAnchor(
SEARCH_TASK_ANCHOR, stopLine.line(), stopLine.number(), AnchorManager.IGNORE_COLUMN);
stopLineAnchor.setRemovalStrategy(RemovalStrategy.SHIFT);
} else {
document.getAnchorManager().moveAnchor(
stopLineAnchor, stopLine.line(), stopLine.number(), AnchorManager.IGNORE_COLUMN);
}
// Try to set the line at the top or bottom of viewport (depending on
// direction), if we fail then wrap around the document. We don't have to
// check shoudl wrap here since the viewport scan would have returned false.
LineInfo startAnchorLine = searchDirectionHelper.getViewportEndLineInfo();
if (!startAnchorLine.moveTo(searchDirectionHelper.isGoingDown())) {
startAnchorLine = searchDirectionHelper.getWrapDocumentLine();
}
if (searchTaskAnchor == null) {
searchTaskAnchor =
document.getAnchorManager().createAnchor(SEARCH_TASK_ANCHOR, startAnchorLine.line(),
startAnchorLine.number(), AnchorManager.IGNORE_COLUMN);
searchTaskAnchor.setRemovalStrategy(RemovalStrategy.SHIFT);
} else {
document.getAnchorManager().moveAnchor(searchTaskAnchor, startAnchorLine.line(),
startAnchorLine.number(), AnchorManager.IGNORE_COLUMN);
}
}
private void dispatchSearchBegin() {
if (progressListener != null) {
progressListener.onSearchBegin();
}
}
private void dispatchSearchProgress() {
if (progressListener != null) {
progressListener.onSearchProgress();
}
}
private void dispatchSearchDone() {
removeSearchTaskAnchors();
if (progressListener != null) {
progressListener.onSearchDone();
}
}
private class AsyncSearchTask implements IncrementalScheduler.Task {
@Override
public boolean run(int workAmount) {
LineInfo lineInfo = searchTaskAnchor.getLineInfo();
for (; lineInfo.line() != stopLineAnchor.getLine() && workAmount > 0; workAmount--) {
if (!executor.onSearchLine(lineInfo.line(), lineInfo.number(), false)) {
dispatchSearchDone();
return false;
}
if (!lineInfo.moveTo(searchDirectionHelper.isGoingDown())) {
if (shouldWrapDocument) {
lineInfo = searchDirectionHelper.getWrapDocumentLine();
} else {
dispatchSearchDone();
return false;
}
}
}
if (lineInfo.line() == stopLineAnchor.getLine()) {
dispatchSearchDone();
return false;
}
document.getAnchorManager().moveAnchor(
searchTaskAnchor, lineInfo.line(), lineInfo.number(), AnchorManager.IGNORE_COLUMN);
dispatchSearchProgress();
return true;
}
}
}