// 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.renderer.LineRenderer;
import com.google.collide.json.shared.JsonArray;
import com.google.collide.shared.document.Line;
import com.google.collide.shared.util.JsonCollections;
import com.google.gwt.regexp.shared.MatchResult;
import com.google.gwt.regexp.shared.RegExp;
import com.google.gwt.resources.client.ClientBundle;
import com.google.gwt.resources.client.CssResource;
/**
* Search match highlighting.
*
*/
public class SearchMatchRenderer implements LineRenderer {
public interface Css extends CssResource {
String match();
}
public interface Resources extends ClientBundle {
@Source("SearchMatchRenderer.css")
Css searchMatchRendererCss();
}
private final SearchModel model;
private boolean inMatch = false;
private final JsonArray<Integer> edges;
private final Css css;
public SearchMatchRenderer(Resources resource, SearchModel model) {
this.model = model;
edges = JsonCollections.createArray();
css = resource.searchMatchRendererCss();
}
@Override
public void renderNextChunk(Target target) {
int end = edges.get(1);
int start = edges.remove(0);
/*
* TODO: This is caused by back to back matches (which we want to
* render separately since we can highlight a single match), maybe a better
* way to fix this in the future?
*/
if (start == end) {
end = edges.get(1);
start = edges.remove(0);
inMatch = !inMatch;
}
target.render(end - start, inMatch ? css.match() : null);
inMatch = !inMatch;
}
@Override
public boolean resetToBeginningOfLine(Line line, int lineNumber) {
assert model.getQuery() != null;
assert model.getSearchPattern() != null;
edges.clear();
String text = line.getText();
RegExp regex = model.getSearchPattern();
/*
* We must not forget to clear the lastIndex since it is a global regex, if
* we don't it can lead to a false negative for matches.
*/
regex.setLastIndex(0);
MatchResult match = regex.exec(text);
if (match == null || match.getGroup(0).isEmpty()) {
return false;
}
do {
int start = regex.getLastIndex() - match.getGroup(0).length();
edges.add(start);
edges.add(regex.getLastIndex());
match = regex.exec(text);
} while (match != null && !match.getGroup(0).isEmpty());
// Handles the edge cases of matching at beginning or end of a line
inMatch = true;
if (edges.get(0) != 0) {
inMatch = false;
edges.splice(0, 0, 0);
}
if (edges.peek() != text.length()) {
edges.add(text.length());
}
return true;
}
@Override
public boolean shouldLastChunkFillToRight() {
return false;
}
}