// 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.code.gotodefinition;
import com.google.collide.client.code.popup.EditorPopupController;
import com.google.collide.client.editor.Editor;
import com.google.collide.client.editor.renderer.LineRenderer;
import com.google.collide.client.editor.renderer.Renderer;
import com.google.collide.client.ui.menu.PositionController.VerticalAlign;
import com.google.collide.client.util.Elements;
import com.google.collide.json.client.JsoArray;
import com.google.collide.shared.document.Line;
import com.google.collide.shared.document.LineFinder;
import com.google.collide.shared.document.LineInfo;
import com.google.collide.shared.util.StringUtils;
import com.google.common.base.Objects;
import com.google.gwt.resources.client.ClientBundle;
import com.google.gwt.resources.client.CssResource;
import elemental.html.Element;
import javax.annotation.Nullable;
/**
* A line renderer that highlights code references "on demand".
* It always highlights only one reference at a time.
*
*/
public class GoToDefinitionRenderer implements LineRenderer {
private static final int POPUP_DELAY_MS = 500;
/**
* CssResource for the editor.
*/
public interface Css extends CssResource {
String referenceLink();
String referencePopup();
}
/**
* ClientBundle for the editor.
*/
public interface Resources extends ClientBundle {
@Source({"GoToDefinitionRenderer.css"})
Css goToDefinitionCss();
}
private class SnippetPopupRenderer implements EditorPopupController.PopupRenderer {
private final Element root = Elements.createDivElement();
private final Element textContainer =
(Element) root.appendChild(Elements.createPreElement(
resources.goToDefinitionCss().referencePopup()));
private String content;
@Override
public Element renderDom() {
textContainer.setTextContent(content);
return root;
}
public void setContent(String content) {
this.content = content;
}
}
private final Resources resources;
private final Editor editor;
private final EditorPopupController popupController;
private final SnippetPopupRenderer snippetPopupRenderer;
private EditorPopupController.Remover popupRemover;
private int currentLineNumber;
private int currentLineLength;
// Current render position on current line.
private int linePosition;
// Reference that should be highlighted on next render.
private NavigableReference highlightedReference;
// Whether we have rendered the highlighted reference on current line.
private boolean renderedHighlightedReference;
public GoToDefinitionRenderer(Resources res, Editor editor,
EditorPopupController popupController) {
this.resources = res;
this.editor = editor;
this.popupController = popupController;
snippetPopupRenderer = new SnippetPopupRenderer();
}
@Override
public void renderNextChunk(LineRenderer.Target target) {
if (highlightedReference == null || renderedHighlightedReference) {
// No references to render. So render the rest of the line with null.
renderNothingAndProceed(target, currentLineLength - linePosition);
} else if (highlightedReference.getLineNumber() == currentLineNumber &&
highlightedReference.getStartColumn() == linePosition) {
// Reference starts at current line and current line position.
int referenceLength;
// Reference ends at current line.
referenceLength = highlightedReference.getEndColumn() + 1 - linePosition;
renderReferenceAndProceed(target, referenceLength);
} else {
// Wait until we get to the highlighted reference.
renderNothingAndProceed(target, highlightedReference.getStartColumn() - linePosition);
}
}
private void renderReferenceAndProceed(LineRenderer.Target target, int characterCount) {
target.render(characterCount, resources.goToDefinitionCss().referenceLink());
renderedHighlightedReference = true;
linePosition += characterCount;
}
private void renderNothingAndProceed(LineRenderer.Target target, int characterCount) {
target.render(characterCount, null);
linePosition += characterCount;
}
@Override
public boolean resetToBeginningOfLine(Line line, int lineNumber) {
if (highlightedReference == null ||
highlightedReference.getLineNumber() > lineNumber ||
highlightedReference.getLineNumber() < lineNumber) {
return false;
}
this.renderedHighlightedReference = false;
this.currentLineNumber = lineNumber;
this.currentLineLength = line.getText().length();
this.linePosition = 0;
return true;
}
@Override
public boolean shouldLastChunkFillToRight() {
return false;
}
/**
* Highlights given reference. This automatically turns off highlighting
* of the previously highlighed reference.
* {@code renderChanges()} must be called to make the changes effective in UI.
*
* @param reference reference to highlight
*/
public void highlightReference(@Nullable NavigableReference reference, Renderer renderer,
LineFinder lineFinder) {
if (Objects.equal(highlightedReference, reference)) {
return;
}
// Request clear of currently highlighted reference.
if (highlightedReference != null) {
requestRenderReference(highlightedReference, renderer, lineFinder);
}
highlightedReference = reference;
if (reference != null) {
requestRenderReference(reference, renderer, lineFinder);
} else {
removeTooltip();
}
}
/**
* Un-highlights currently highlighted reference.
*/
public void resetReferences(Renderer renderer, LineFinder lineFinder) {
highlightReference(null, renderer, lineFinder);
}
private void requestRenderReference(NavigableReference reference, Renderer renderer,
LineFinder lineFinder) {
renderer.requestRenderLine(lineFinder.findLine(reference.getLineNumber()).line());
createTooltipForReference(reference);
}
private void createTooltipForReference(NavigableReference reference) {
removeTooltip();
if (reference.getSnippet() == null && reference.getTargetName() == null) {
return;
}
// TODO: Fix scroller appearing in front of cursor if snippet is too long.
String tooltipContent = JsoArray.from(
StringUtils.ensureNotEmpty(reference.getTargetName(), ""),
StringUtils.ensureNotEmpty(reference.getSnippet(), "")).join("\n");
snippetPopupRenderer.setContent(tooltipContent);
LineInfo lineInfo = editor.getDocument().getLineFinder().findLine(reference.getLineNumber());
popupRemover = popupController.showPopup(lineInfo, reference.getStartColumn(),
reference.getEndColumn(), null, snippetPopupRenderer, null, VerticalAlign.TOP,
false /* shouldCaptureOutsideClickOnClose */, POPUP_DELAY_MS);
}
private void removeTooltip() {
if (popupRemover != null) {
popupRemover.remove();
}
}
}