// 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.AppContext; import com.google.collide.client.editor.ViewportModel; import com.google.collide.client.editor.renderer.Renderer; import com.google.collide.client.editor.selection.SelectionModel; import com.google.collide.client.util.BasicIncrementalScheduler; import com.google.collide.client.util.ClientStringUtils; import com.google.collide.client.util.IncrementalScheduler; import com.google.collide.shared.document.Document; import com.google.collide.shared.document.DocumentMutator; import com.google.collide.shared.document.Line; import com.google.collide.shared.document.LineInfo; import com.google.collide.shared.util.ListenerRegistrar; import com.google.collide.shared.util.RegExpUtils; import com.google.gwt.regexp.shared.RegExp; /** * External handle to search functions in the editor. * * TODO: Handle number of matches changing due to document mutations. * */ public class SearchModel { public static SearchModel create(AppContext context, Document document, Renderer renderer, ViewportModel viewport, SelectionModel selectionModel, DocumentMutator editorDocumentMutator) { /* * This is a pretty fast operation so by default we guess about 5000 lines * in 100 ms. */ IncrementalScheduler scheduler = new BasicIncrementalScheduler(context.getUserActivityManager(), 100, 5000); SearchTask searchTask = new SearchTask(document, viewport, scheduler); IncrementalScheduler matchScheduler = new BasicIncrementalScheduler(context.getUserActivityManager(), 100, 5000); SearchTask matchTask = new SearchTask(document, viewport, matchScheduler); return new SearchModel(context, document, renderer, viewport, new SearchMatchManager(document, selectionModel, editorDocumentMutator, matchTask), searchTask, selectionModel); } public static SearchModel createWithManagerAndScheduler(AppContext context, Document document, Renderer renderer, ViewportModel viewport, SearchMatchManager matchManager, IncrementalScheduler scheduler, SelectionModel selectionModel) { return new SearchModel(context, document, renderer, viewport, matchManager, new SearchTask(document, viewport, scheduler), selectionModel); } public interface SearchProgressListener { public void onSearchBegin(); public void onSearchProgress(); public void onSearchDone(); } public interface MatchCountListener { public void onMatchCountChanged(int total); } private class SearchTaskHandler implements SearchTask.SearchTaskExecutor { private RegExp oldSearchPattern; public void setOldSearchPattern(RegExp oldSearchPattern) { this.oldSearchPattern = oldSearchPattern; } @Override public boolean onSearchLine(Line line, int number, boolean shouldRenderLine) { int matches = RegExpUtils.resetAndGetNumberOfMatches(searchPattern, line.getText()); matchManager.addMatches(new LineInfo(line, number), matches); if (shouldRenderLine) { handleViewportLine(line, matches); } return true; } private void handleViewportLine(Line line, int matches) { if (matches > 0) { renderer.requestRenderLine(line); } else if (oldSearchPattern != null && RegExpUtils.resetAndTest(oldSearchPattern, line.getText())) { renderer.requestRenderLine(line); } } } private final SearchMatchRenderer lineRenderer; private String query; private final Renderer renderer; private RegExp searchPattern; private final SearchMatchManager matchManager; private final ViewportModel viewport; private final SearchTask searchTask; private final SelectionModel selectionModel; private final SearchTaskHandler searchTaskHandler; protected SearchModel(AppContext context, Document document, Renderer renderer, ViewportModel viewport, SearchMatchManager matchManager, SearchTask searchTask, SelectionModel selectionModel) { this.lineRenderer = new SearchMatchRenderer(context.getResources(), this); this.matchManager = matchManager; this.query = ""; this.renderer = renderer; this.viewport = viewport; this.searchTask = searchTask; this.selectionModel = selectionModel; searchTaskHandler = new SearchTaskHandler(); } /** * @return currently active query */ public String getQuery() { return query; } /** * @return currently active search pattern */ public RegExp getSearchPattern() { return searchPattern; } /** * Matches a wildcard type search query in the editor */ public void setQuery(String query) { setQuery(query, null); } /** * Matches a wildcard type search query in the editor * * @param progressListener optional search progress listener. */ public void setQuery(final String query, SearchProgressListener progressListener) { if (query == null) { throw new IllegalArgumentException("Query cannot be null"); } this.query = query; if (query.isEmpty()) { if (searchPattern != null) { matchManager.clearMatches(); cleanupAfterQuery(); } return; } if (searchPattern == null) { // moving from no query to an active query; add the line renderer renderer.addLineRenderer(lineRenderer); } /* * Heuristic for case sensitivity: If the string is all lower-case we match * case-insensitively; otherwise the pattern is case sensitive. */ String regExpOptions = ClientStringUtils.containsUppercase(query) ? "g" : "gi"; // Create the new search pattern searchTaskHandler.setOldSearchPattern(searchPattern); searchPattern = RegExpUtils.createRegExpForWildcardPattern(query, regExpOptions); // setSearchPattern automatically clears any match data matchManager.setSearchPattern(searchPattern); Line line = selectionModel.getCursorLine(); int lineNumber = selectionModel.getCursorLineNumber(); searchTask.searchDocument(searchTaskHandler, progressListener, new LineInfo(line, lineNumber)); } public SearchMatchManager getMatchManager() { return matchManager; } public ListenerRegistrar<MatchCountListener> getMatchCountChangedListenerRegistrar() { return matchManager.getMatchCountChangedListenerRegistrar(); } public void teardown() { searchTask.teardown(); } /** * Cleans up the viewport when we no longer have a query. Rerenders lines that * the last searchPattern has highlighted. */ private void cleanupAfterQuery() { renderer.removeLineRenderer(lineRenderer); LineInfo lineInfo = viewport.getTopLineInfo(); do { if (RegExpUtils.resetAndTest(searchPattern, lineInfo.line().getText())) { renderer.requestRenderLine(lineInfo.line()); } } while (lineInfo.line() != viewport.getBottomLine() && lineInfo.moveToNext()); searchPattern = null; } }