// 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(); } } }