// 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.codeunderstanding.CubeClient; import com.google.collide.client.documentparser.DocumentParser; import com.google.collide.client.editor.Buffer; import com.google.collide.client.editor.Editor; import com.google.collide.client.editor.input.CommonActions; import com.google.collide.client.editor.input.DefaultActionExecutor; import com.google.collide.client.editor.input.InputScheme; import com.google.collide.client.editor.input.RootActionExecutor; import com.google.collide.client.editor.input.Shortcut; import com.google.collide.client.editor.selection.SelectionModel; import com.google.collide.client.history.Place; import com.google.collide.client.util.PathUtil; import com.google.collide.client.workspace.FileTreeModel; import com.google.collide.shared.document.LineFinder; import com.google.collide.shared.document.LineInfo; import com.google.collide.shared.util.ListenerRegistrar; import com.google.collide.shared.util.StringUtils; import com.google.common.base.Preconditions; import com.google.gwt.event.dom.client.KeyCodes; import org.waveprotocol.wave.client.common.util.SignalEvent; import org.waveprotocol.wave.client.common.util.SignalEvent.KeyModifier; import org.waveprotocol.wave.client.common.util.UserAgent; import elemental.events.Event; /** * Implementation of the "go to definition" feature. * */ public class GoToDefinitionHandler extends DefaultActionExecutor { private static final int COMMAND_KEY_CODE = 91; private final Editor editor; private final FileTreeModel fileTreeModel; private final GoToDefinitionRenderer referenceRenderer; private final Shortcut gotoDefinitionAction = new Shortcut() { @Override public boolean event(InputScheme scheme, SignalEvent event) { goToDefinitionAtCurrentCaretPosition(); return true; } }; private LineFinder lineFinder; /** * Flag which specifies that object is "activated". * * <p>Activated object plugs renderer and listens to mouse to highlight * hovered items. */ private boolean activated; private ListenerRegistrar.Remover mouseMoveListenerRemover; private ListenerRegistrar.Remover mouseClickListenerRemover; private ListenerRegistrar.Remover nativeKeyUpListenerRemover; /** * Flag which specifies that object is "attached" to editor. * * <p>Registered object listens for keypeseesd (to become activated) and * provides actions. */ private boolean registered; private ListenerRegistrar.Remover keyListenerRemover; private RootActionExecutor.Remover actionRemover; private ReferenceStore referenceStore; private ReferenceNavigator referenceNavigator; private AnchorTagParser anchorParser; private static boolean isActionOnlyKey(SignalEvent signal) { // When modifier key is pressed on Mac it is instantly applied to modifiers. // So we check on Mac that only "CMD" is pressed. // On non-Mac when "CTRL" is the first key in sequence, there are no // modifiers applied yet. KeyModifier modifier = UserAgent.isMac() ? KeyModifier.META : KeyModifier.NONE; return modifier.check(signal) && (signal.getKeyCode() == getActionKeyCode()); } private static int getActionKeyCode() { return UserAgent.isMac() ? COMMAND_KEY_CODE : KeyCodes.KEY_CTRL; } private final Buffer.MouseMoveListener mouseMoveListener = new Buffer.MouseMoveListener() { @Override public void onMouseMove(int x, int y) { NavigableReference reference = findReferenceAtMousePosition(x, y, false); referenceRenderer.highlightReference(reference, editor.getRenderer(), lineFinder); } }; private final Buffer.MouseClickListener mouseClickListener = new Buffer.MouseClickListener() { @Override public void onMouseClick(int x, int y) { NavigableReference reference = findReferenceAtMousePosition(x, y, true); if (reference != null) { navigateReference(reference); } } }; private final Editor.NativeKeyUpListener keyUpListener = new Editor.NativeKeyUpListener() { @Override public boolean onNativeKeyUp(Event event) { com.google.gwt.user.client.Event gwtEvent = (com.google.gwt.user.client.Event) event; if (gwtEvent.getKeyCode() == getActionKeyCode()) { setActivated(false); return true; } else { return false; } } }; private final Editor.KeyListener keyListener = new Editor.KeyListener() { @Override public boolean onKeyPress(SignalEvent signal) { boolean shouldActivate = isActionOnlyKey(signal); setActivated(shouldActivate); return shouldActivate; } }; public GoToDefinitionHandler(Place currentPlace, Editor editor, FileTreeModel fileTreeModel, GoToDefinitionRenderer.Resources resources, CubeClient cubeClient, EditorPopupController popupController) { Preconditions.checkNotNull(editor); Preconditions.checkNotNull(cubeClient); this.editor = editor; this.fileTreeModel = fileTreeModel; this.referenceRenderer = new GoToDefinitionRenderer(resources, editor, popupController); this.referenceNavigator = new ReferenceNavigator(currentPlace, editor); addAction(CommonActions.GOTO_DEFINITION, gotoDefinitionAction); addAction(CommonActions.GOTO_SOURCE, gotoDefinitionAction); referenceStore = new ReferenceStore(cubeClient); } public void editorContentsReplaced(PathUtil filePath, DocumentParser parser) { setActivated(false); if (anchorParser != null) { anchorParser.cleanup(); anchorParser = null; } DynamicReferenceProvider dynamicReferenceProvider = null; if (isGoToDefinitionSupported(filePath)) { if (StringUtils.endsWithIgnoreCase(filePath.getPathString(), ".html")) { anchorParser = new AnchorTagParser(parser); } DeferringLineParser deferringParser = new DeferringLineParser(parser); dynamicReferenceProvider = new DynamicReferenceProvider( filePath.getPathString(), deferringParser, fileTreeModel, anchorParser); this.referenceNavigator.setCurrentFilePath(filePath.getPathString()); this.lineFinder = editor.getDocument().getLineFinder(); setRegistered(true); } else { setRegistered(false); } this.referenceStore.onDocumentChanged(editor.getDocument(), dynamicReferenceProvider); } /** * Registers or unregisters keyListener, actions, etc. */ private void setRegistered(boolean registered) { if (registered == this.registered) { return; } if (registered) { keyListenerRemover = editor.getKeyListenerRegistrar().add(keyListener); actionRemover = editor.getInput().getActionExecutor().addDelegate(this); } else { keyListenerRemover.remove(); keyListenerRemover = null; actionRemover.remove(); actionRemover = null; } this.registered = registered; } private void setActivated(boolean activated) { if (activated == this.activated) { return; } if (activated) { mouseMoveListenerRemover = editor.getBuffer().getMouseMoveListenerRegistrar().add(mouseMoveListener); mouseClickListenerRemover = editor.getBuffer().getMouseClickListenerRegistrar().add(mouseClickListener); nativeKeyUpListenerRemover = editor.getNativeKeyUpListenerRegistrar().add(keyUpListener); editor.addLineRenderer(referenceRenderer); } else { mouseMoveListenerRemover.remove(); mouseMoveListenerRemover = null; mouseClickListenerRemover.remove(); mouseClickListenerRemover = null; nativeKeyUpListenerRemover.remove(); nativeKeyUpListenerRemover = null; referenceRenderer.resetReferences(editor.getRenderer(), lineFinder); editor.removeLineRenderer(referenceRenderer); } this.activated = activated; } private static boolean isGoToDefinitionSupported(PathUtil filePath) { String pathString = filePath.getPathString(); return StringUtils.endsWithIgnoreCase(pathString, ".js") || StringUtils.endsWithIgnoreCase(pathString, ".html") || StringUtils.endsWithIgnoreCase(pathString, ".py"); } private void goToDefinitionAtCurrentCaretPosition() { // TODO: Check here that our code model is fresh enough. // int caretOffset = getCaretOffset(editor.getWidget().getElement()); SelectionModel selection = editor.getSelection(); LineInfo lineInfo = new LineInfo(selection.getCursorLine(), selection.getCursorLineNumber()); NavigableReference reference = referenceStore.findReference(lineInfo, selection.getCursorColumn(), true); navigateReference(reference); } private void navigateReference(NavigableReference reference) { if (reference == null) { return; } setActivated(false); reference.navigate(referenceNavigator); } private NavigableReference findReferenceAtMousePosition(int x, int y, boolean blocking) { int lineNumber = editor.getBuffer().convertYToLineNumber(y, true); LineInfo lineInfo = lineFinder.findLine(lineNumber); int column = editor.getBuffer().convertXToRoundedVisibleColumn(x, lineInfo.line()); return referenceStore.findReference(lineInfo, column, blocking); } public void cleanup() { referenceStore.cleanup(); if (anchorParser != null) { anchorParser.cleanup(); } setActivated(false); setRegistered(false); } }