/* * AceBackgroundHighlighter.java * * Copyright (C) 2009-17 by RStudio, Inc. * * Unless you have received this program directly from RStudio pursuant * to the terms of a commercial license agreement with RStudio, then * this program is licensed to you under the terms of version 3 of the * GNU Affero General Public License. This program is distributed WITHOUT * ANY EXPRESS OR IMPLIED WARRANTY, INCLUDING THOSE OF NON-INFRINGEMENT, * MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE. Please refer to the * AGPL (http://www.gnu.org/licenses/agpl-3.0.txt) for more details. * */ package org.rstudio.studio.client.workbench.views.source.editors.text.ace; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import org.rstudio.core.client.HandlerRegistrations; import org.rstudio.core.client.JsVector; import org.rstudio.core.client.JsVectorInteger; import org.rstudio.core.client.ListUtil; import org.rstudio.core.client.StringUtil; import org.rstudio.core.client.regex.Pattern; import org.rstudio.studio.client.workbench.views.source.editors.text.AceEditor; import org.rstudio.studio.client.workbench.views.source.editors.text.events.DocumentChangedEvent; import org.rstudio.studio.client.workbench.views.source.editors.text.events.EditorModeChangedEvent; import com.google.gwt.core.client.JavaScriptObject; import com.google.gwt.event.logical.shared.AttachEvent; import com.google.gwt.user.client.Timer; public class AceBackgroundHighlighter implements EditorModeChangedEvent.Handler, DocumentChangedEvent.Handler, AttachEvent.Handler { private static class HighlightPattern { public HighlightPattern(String begin, String end) { this.begin = Pattern.create(begin, ""); this.end = Pattern.create(end, ""); } public Pattern begin; public Pattern end; } private class Worker { public Worker() { timer_ = new Timer() { @Override public void run() { work(); } }; } private void work() { // determine range to update int n = editor_.getRowCount(); int startRow = row_; int endRow = Math.min(startRow + CHUNK_SIZE, editor_.getRowCount()); activeHighlightPattern_ = findActiveHighlightPattern(startRow); // first, update local background state for each row for (int row = startRow; row < endRow; row++) { // determine what state this row is in int state = computeState(row); // if there's been no change, bail boolean isConsistentState = rowStates_.isSet(row) && rowPatterns_.isSet(row) && (rowStates_.get(row) == state) && (rowPatterns_.get(row) == activeHighlightPattern_); if (isConsistentState) break; // update state for this row rowStates_.set(row, state); rowPatterns_.set(row, activeHighlightPattern_); } // then, notify Ace and perform actual rendering of markers for (int row = startRow; row < endRow; row++) { int state = rowStates_.get(row); // don't show background highlighting if this // chunk lies within a fold AceFold fold = session_.getFoldAt(row, 0); if (fold != null) continue; int marker = markerIds_.get(row, 0); // bail early if no action is necessary boolean isConsistentState = (state == STATE_TEXT && marker == 0) || (state != STATE_TEXT && marker != 0); if (isConsistentState) continue; // clear a pre-existing marker if necessary if (marker != 0) { session_.removeMarker(marker); markerIds_.set(row, 0); } // if this is a non-text state, then draw a marker if (state != STATE_TEXT) { int markerId = session_.addMarker( Range.create(row, 0, row, Integer.MAX_VALUE), MARKER_CLASS, MARKER_TYPE, false); markerIds_.set(row, markerId); } } // update worker state and reschedule if there's // more work to be done row_ = endRow; if (endRow != n) timer_.schedule(DELAY_MS); } public void start(int row) { row_ = Math.min(row, row_); timer_.schedule(0); } private final Timer timer_; private int row_; private static final int DELAY_MS = 5; private static final int CHUNK_SIZE = 200; } public AceBackgroundHighlighter(AceEditor editor) { editor_ = editor; session_ = editor.getSession(); highlightPatterns_ = new ArrayList<HighlightPattern>(); handlers_ = new HandlerRegistrations( editor.addEditorModeChangedHandler(this), editor.addDocumentChangedHandler(this), editor.addAttachHandler(this)); int n = editor.getRowCount(); rowStates_ = JavaScriptObject.createArray(n).cast(); rowPatterns_ = JavaScriptObject.createArray(n).cast(); markerIds_ = JavaScriptObject.createArray(n).cast(); worker_ = new Worker(); activeModeId_ = editor.getSession().getMode().getId(); refreshHighlighters(); } // Handlers ---- @Override public void onEditorModeChanged(EditorModeChangedEvent event) { // nothing to do if mode did not change if (event.getMode().equals(activeModeId_)) return; activeModeId_ = event.getMode(); clearMarkers(); clearRowState(); refreshHighlighters(); synchronizeFrom(0); } @Override public void onDocumentChanged(DocumentChangedEvent event) { AceDocumentChangeEventNative nativeEvent = event.getEvent(); String action = nativeEvent.getAction(); Range range = nativeEvent.getRange(); int startRow = range.getStart().getRow(); int endRow = range.getEnd().getRow(); // NOTE: this will need to change with the next version of Ace, // as the layout of document changed events will have changed there rowStates_.unset(startRow); rowPatterns_.unset(startRow); if (action.startsWith("insert")) { int newlineCount = endRow - startRow; rowStates_.insert(startRow, JsVectorInteger.ofLength(newlineCount)); rowPatterns_.insert(startRow, JsVector.<HighlightPattern>ofLength(newlineCount)); } else if (action.startsWith("remove")) { int newlineCount = endRow - startRow; if (newlineCount > 0) { rowStates_.remove(startRow, newlineCount); rowPatterns_.remove(startRow,newlineCount); } } synchronizeFrom(startRow); } @Override public void onAttachOrDetach(AttachEvent event) { if (!event.isAttached()) { handlers_.removeHandler(); } } // Private Methods ---- HighlightPattern selectBeginPattern(String line) { for (HighlightPattern pattern : highlightPatterns_) if (pattern.begin.test(line)) return pattern; return null; } private HighlightPattern findActiveHighlightPattern(int startRow) { for (int row = startRow; row >= 0; row--) { // check for a cached highlight pattern HighlightPattern pattern = rowPatterns_.get(row); if (pattern != null) return pattern; // no pattern available; re-compute based on current state int state = rowStates_.get(row); switch (state) { case STATE_TEXT: break; case STATE_CHUNK_START: String line = editor_.getLine(row); return selectBeginPattern(line); case STATE_CHUNK_BODY: case STATE_CHUNK_END: default: continue; } } return null; } private int computeState(int row) { String line = editor_.getLine(row); int state = (row > 0) ? rowStates_.get(row - 1, STATE_TEXT) : STATE_TEXT; switch (state) { case STATE_TEXT: case STATE_CHUNK_END: { HighlightPattern pattern = selectBeginPattern(line); if (pattern != null) { activeHighlightPattern_ = pattern; return STATE_CHUNK_START; } return STATE_TEXT; } case STATE_CHUNK_START: case STATE_CHUNK_BODY: { assert activeHighlightPattern_ != null : "Unexpected null highlight pattern"; if (activeHighlightPattern_.end.test(line)) { activeHighlightPattern_ = null; return STATE_CHUNK_END; } return STATE_CHUNK_BODY; } } // shouldn't be reached return STATE_TEXT; } private void synchronizeFrom(int startRow) { // if this row has no state, then we need to look // back until we find a row with cached state while (startRow > 0 && !rowStates_.isSet(startRow - 1)) startRow--; // start the worker that will update ace worker_.start(startRow); } private void refreshHighlighters() { highlightPatterns_.clear(); String modeId = editor_.getModeId(); if (StringUtil.isNullOrEmpty(modeId)) return; if (HIGHLIGHT_PATTERN_REGISTRY.containsKey(modeId)) { highlightPatterns_.addAll(HIGHLIGHT_PATTERN_REGISTRY.get(modeId)); } } private void clearRowState() { rowStates_.fill(0); rowPatterns_.fill((HighlightPattern) null); } private void clearMarkers() { for (int i = 0, n = markerIds_.length(); i < n; i++) { int markerId = markerIds_.get(i); if (markerId != 0) session_.removeMarker(markerId); } markerIds_.fill(0); } private static List<HighlightPattern> cStyleHighlightPatterns() { return ListUtil.create( new HighlightPattern( "^\\s*[/][*]{3,}\\s*[Rr]\\s*$", "^\\s*[*]+[/]") ); } private static List<HighlightPattern> htmlStyleHighlightPatterns() { return ListUtil.create( new HighlightPattern( "^<!--\\s*begin[.]rcode\\s*(?:.*)", "^\\s*end[.]rcode\\s*-->") ); } private static List<HighlightPattern> sweaveHighlightPatterns() { return ListUtil.create( new HighlightPattern( "<<(.*?)>>", "^\\s*@\\s*$") ); } private static final List<HighlightPattern> rMarkdownHighlightPatterns() { return ListUtil.create( // code chunks new HighlightPattern( "^(?:[ ]{4})?`{3,}\\s*\\{.*\\}\\s*$", "^(?:[ ]{4})?`{3,}\\s*$"), // latex blocks new HighlightPattern( "^[$][$]\\s*$", "^[$][$]\\s*$") ); } private final AceEditor editor_; private final EditSession session_; private HighlightPattern activeHighlightPattern_; private String activeModeId_; private final List<HighlightPattern> highlightPatterns_; private final HandlerRegistrations handlers_; private final JsVectorInteger rowStates_; private final JsVectorInteger markerIds_; private final JsVector<HighlightPattern> rowPatterns_; private final Worker worker_; private static final String MARKER_CLASS = "ace_foreign_line background_highlight"; private static final String MARKER_TYPE = "fullLine"; private static final Map<String, List<HighlightPattern>> HIGHLIGHT_PATTERN_REGISTRY; static { HIGHLIGHT_PATTERN_REGISTRY = new HashMap<String, List<HighlightPattern>>(); HIGHLIGHT_PATTERN_REGISTRY.put("mode/rmarkdown", rMarkdownHighlightPatterns()); HIGHLIGHT_PATTERN_REGISTRY.put("mode/c_cpp", cStyleHighlightPatterns()); HIGHLIGHT_PATTERN_REGISTRY.put("mode/sweave", sweaveHighlightPatterns()); HIGHLIGHT_PATTERN_REGISTRY.put("mode/rhtml", htmlStyleHighlightPatterns()); } private static final int STATE_TEXT = 1; private static final int STATE_CHUNK_START = 2; private static final int STATE_CHUNK_BODY = 3; private static final int STATE_CHUNK_END = 4; }