// 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.renderer;
import com.google.collide.client.editor.Buffer;
import com.google.collide.client.editor.Editor;
import com.google.collide.client.editor.ViewportModel;
import com.google.collide.client.editor.gutter.Gutter;
import com.google.collide.client.editor.selection.SelectionModel;
import com.google.collide.client.util.Elements;
import com.google.collide.client.util.JsIntegerMap;
import com.google.collide.json.shared.JsonArray;
import com.google.collide.shared.document.LineInfo;
import com.google.collide.shared.util.JsonCollections;
import com.google.collide.shared.util.ListenerRegistrar;
import com.google.gwt.resources.client.ClientBundle;
import elemental.css.CSSStyleDeclaration;
import elemental.html.Element;
/**
* A renderer for the line numbers in the left gutter.
*/
public class LineNumberRenderer {
private static final int NONE = -1;
private final Buffer buffer;
private final Gutter leftGutter;
/**
* Current editor instance.
*
* Used to track if current fie can be edited (i.e. is not readonly).
*
* TODO: add new abstraction to avoid editor passing.
*/
private final Editor editor;
private int previousBottomLineNumber = -1;
private int previousTopLineNumber = -1;
private JsIntegerMap<Element> lineNumberToElementCache;
private final ViewportModel viewport;
private final Css css;
private int activeLineNumber = NONE;
private int renderedActiveLineNumber = NONE;
private final JsonArray<ListenerRegistrar.Remover> listenerRemovers =
JsonCollections.createArray();
private final SelectionModel.CursorListener cursorListener = new SelectionModel.CursorListener() {
@Override
public void onCursorChange(LineInfo lineInfo, int column, boolean isExplicitChange) {
activeLineNumber = lineInfo.number();
updateActiveLine();
}
};
private Editor.ReadOnlyListener readonlyListener = new Editor.ReadOnlyListener() {
@Override
public void onReadOnlyChanged(boolean isReadOnly) {
updateActiveLine();
}
};
private void updateActiveLine() {
int lineNumber = this.activeLineNumber;
if (editor.isReadOnly()) {
lineNumber = NONE;
}
if (lineNumber == renderedActiveLineNumber) {
return;
}
if (renderedActiveLineNumber != NONE) {
Element renderedActiveLine = lineNumberToElementCache.get(renderedActiveLineNumber);
if (renderedActiveLine != null) {
renderedActiveLine.removeClassName(css.activeLineNumber());
renderedActiveLineNumber = NONE;
}
}
Element newActiveLine = lineNumberToElementCache.get(lineNumber);
// Add class if it's in the viewport.
if (newActiveLine != null) {
newActiveLine.addClassName(css.activeLineNumber());
renderedActiveLineNumber = lineNumber;
}
}
public void teardown() {
for (int i = 0, n = listenerRemovers.size(); i < n; i++) {
listenerRemovers.get(i).remove();
}
}
/**
* Line number CSS.
*/
public interface Css extends Editor.EditorSharedCss {
String lineNumber();
String activeLineNumber();
}
/**
* Line number resources.
*/
public interface Resources extends ClientBundle {
@Source({"com/google/collide/client/common/constants.css",
"LineNumberRenderer.css"})
Css lineNumberRendererCss();
}
LineNumberRenderer(Buffer buffer, Resources res, Gutter leftGutter, ViewportModel viewport,
SelectionModel selection, Editor editor) {
this.buffer = buffer;
this.leftGutter = leftGutter;
this.editor = editor;
this.lineNumberToElementCache = JsIntegerMap.create();
this.viewport = viewport;
this.css = res.lineNumberRendererCss();
listenerRemovers.add(selection.getCursorListenerRegistrar().add(cursorListener));
listenerRemovers.add(editor.getReadOnlyListenerRegistrar().add(readonlyListener));
}
void renderImpl(int updateBeginLineNumber) {
int topLineNumber = viewport.getTopLineNumber();
int bottomLineNumber = viewport.getBottomLineNumber();
if (previousBottomLineNumber == -1 || topLineNumber > previousBottomLineNumber
|| bottomLineNumber < previousTopLineNumber) {
if (previousBottomLineNumber > -1) {
garbageCollectLines(previousTopLineNumber, previousBottomLineNumber);
}
fillOrUpdateLines(topLineNumber, bottomLineNumber);
} else {
/*
* The viewport was shifted and part of the old viewport will be in the
* new viewport.
*/
// first garbage collect any lines that have gone off the screen
if (previousTopLineNumber < topLineNumber) {
// off the top
garbageCollectLines(previousTopLineNumber, topLineNumber - 1);
}
if (previousBottomLineNumber > bottomLineNumber) {
// off the bottom
garbageCollectLines(bottomLineNumber + 1, previousBottomLineNumber);
}
/*
* Re-create any line numbers that are now visible or have had their
* positions shifted.
*/
if (previousTopLineNumber > topLineNumber) {
// new lines at the top
fillOrUpdateLines(topLineNumber, previousTopLineNumber - 1);
}
if (updateBeginLineNumber >= 0 && updateBeginLineNumber <= bottomLineNumber) {
// lines updated in the middle; redraw everything below
fillOrUpdateLines(updateBeginLineNumber, bottomLineNumber);
} else {
// only check new lines scrolled in from the bottom
if (previousBottomLineNumber < bottomLineNumber) {
fillOrUpdateLines(previousBottomLineNumber, bottomLineNumber);
}
}
}
previousTopLineNumber = viewport.getTopLineNumber();
previousBottomLineNumber = viewport.getBottomLineNumber();
}
void render() {
renderImpl(-1);
}
/**
* Re-render all line numbers including and after lineNumber to account for
* spacer movement.
*/
void renderLineAndFollowing(int lineNumber) {
renderImpl(lineNumber);
}
private void fillOrUpdateLines(int beginLineNumber, int endLineNumber) {
for (int i = beginLineNumber; i <= endLineNumber; i++) {
Element lineElement = lineNumberToElementCache.get(i);
if (lineElement != null) {
updateElementPosition(lineElement, i);
} else {
Element element = createElement(i);
lineNumberToElementCache.put(i, element);
leftGutter.addUnmanagedElement(element);
}
}
}
private void updateElementPosition(Element lineNumberElement, int lineNumber) {
lineNumberElement.getStyle().setTop(
buffer.calculateLineTop(lineNumber), CSSStyleDeclaration.Unit.PX);
}
private Element createElement(int lineNumber) {
Element element = Elements.createDivElement(css.lineNumber());
// Line 0 will be rendered as Line 1
element.setTextContent(String.valueOf(lineNumber + 1));
element.getStyle().setTop(buffer.calculateLineTop(lineNumber), CSSStyleDeclaration.Unit.PX);
if (lineNumber == activeLineNumber) {
element.addClassName(css.activeLineNumber());
renderedActiveLineNumber = activeLineNumber;
}
return element;
}
private void garbageCollectLines(int beginLineNumber, int endLineNumber) {
for (int i = beginLineNumber; i <= endLineNumber; i++) {
Element lineElement = lineNumberToElementCache.get(i);
if (lineElement != null) {
leftGutter.removeUnmanagedElement(lineElement);
lineNumberToElementCache.erase(i);
} else {
throw new IndexOutOfBoundsException(
"Tried to garbage collect line number " + i + " when it does not exist.");
}
}
if (beginLineNumber <= renderedActiveLineNumber && renderedActiveLineNumber <= endLineNumber) {
renderedActiveLineNumber = NONE;
}
}
}