org.eclipse.che.ide.editor.codemirror.client.ShowCompletion.java Source code

Java tutorial

Introduction

Here is the source code for org.eclipse.che.ide.editor.codemirror.client.ShowCompletion.java

Source

/*******************************************************************************
 * Copyright (c) 2014-2015 Codenvy, S.A.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v10.html
 *
 * Contributors:
 *   Codenvy, S.A. - initial API and implementation
 *******************************************************************************/
package org.eclipse.che.ide.editor.codemirror.client;

import java.util.ArrayList;
import java.util.List;
import java.util.logging.Logger;

import org.eclipse.che.ide.editor.codemirrorjso.client.CMEditorOverlay;
import org.eclipse.che.ide.editor.codemirrorjso.client.CMPositionOverlay;
import org.eclipse.che.ide.editor.codemirrorjso.client.CodeMirrorOverlay;
import org.eclipse.che.ide.editor.codemirrorjso.client.EventHandlers;
import org.eclipse.che.ide.editor.codemirrorjso.client.EventTypes;
import org.eclipse.che.ide.editor.codemirrorjso.client.hints.CMCompletionObjectOverlay;
import org.eclipse.che.ide.editor.codemirrorjso.client.hints.CMHintApplyOverlay;
import org.eclipse.che.ide.editor.codemirrorjso.client.hints.CMHintCallback;
import org.eclipse.che.ide.editor.codemirrorjso.client.hints.CMHintFunctionOverlay;
import org.eclipse.che.ide.editor.codemirrorjso.client.hints.CMHintOptionsOverlay;
import org.eclipse.che.ide.editor.codemirrorjso.client.hints.CMHintResultsOverlay;
import org.eclipse.che.ide.editor.codemirrorjso.client.hints.CMRenderFunctionOverlay;
import org.eclipse.che.ide.editor.codemirrorjso.client.hints.CMHintApplyOverlay.HintApplyFunction;
import org.eclipse.che.ide.editor.codemirrorjso.client.hints.CMHintFunctionOverlay.AsyncHintFunction;
import org.eclipse.che.ide.editor.codemirrorjso.client.hints.CMHintFunctionOverlay.HintFunction;
import org.eclipse.che.ide.editor.codemirrorjso.client.hints.CMRenderFunctionOverlay.RenderFunction;
import org.eclipse.che.ide.jseditor.client.codeassist.AdditionalInfoCallback;
import org.eclipse.che.ide.jseditor.client.codeassist.Completion;
import org.eclipse.che.ide.jseditor.client.codeassist.CompletionProposal;
import org.eclipse.che.ide.jseditor.client.codeassist.CompletionProposal.CompletionCallback;
import org.eclipse.che.ide.jseditor.client.codeassist.CompletionReadyCallback;
import org.eclipse.che.ide.jseditor.client.codeassist.CompletionResources.CompletionCss;
import org.eclipse.che.ide.jseditor.client.codeassist.CompletionsSource;
import org.eclipse.che.ide.jseditor.client.document.EmbeddedDocument;
import org.eclipse.che.ide.jseditor.client.text.LinearRange;
import org.eclipse.che.ide.util.dom.Elements;
import com.google.gwt.core.client.JavaScriptObject;
import com.google.gwt.core.client.JsArrayMixed;

import elemental.dom.Document;
import elemental.dom.Element;
import elemental.dom.Node;
import elemental.dom.NodeList;
import elemental.html.ClientRect;
import elemental.html.SpanElement;
import elemental.js.dom.JsElement;
import elemental.js.util.JsMapFromStringTo;
import elemental.util.Timer;

/**
 * Component that handles the showCompletion(...) operations.
 */
public final class ShowCompletion {

    /** The logger. */
    private static final Logger LOG = Logger.getLogger(ShowCompletion.class.getName());

    /** Property name where the additional info is stored in the completion object. */
    private static final String PROP_ADDITIONAL_INFO = "additionalInfo";

    /** Marker class name for additional info popups. */
    private static final String ADDITIONAL_INFO_MARKER = "completion-additional-info-do-not-use-your-element-will-be-removed-anytime";

    private final CompletionCss completionCss;

    private final CodeMirrorEditorWidget editorWidget;

    public ShowCompletion(final CodeMirrorEditorWidget editorWidget, final CompletionCss css) {
        this.completionCss = css;
        this.editorWidget = editorWidget;
    }

    public void showCompletionProposals(final List<CompletionProposal> proposals,
            final AdditionalInfoCallback additionalInfoCallback) {
        if (!editorWidget.getEditorOverlay().hasShowHint() || proposals == null || proposals.isEmpty()) {
            // no support for hints or no proposals
            return;
        }

        final CMHintOptionsOverlay hintOptions = createDefaultHintOptions();

        final CMHintFunctionOverlay hintFunction = CMHintFunctionOverlay.createFromHintFunction(new HintFunction() {

            @Override
            public CMHintResultsOverlay getHints(final CMEditorOverlay editor, final CMHintOptionsOverlay options) {
                final CMHintResultsOverlay result = CMHintResultsOverlay.create();
                final JsArrayMixed list = result.getList();
                for (final CompletionProposal proposal : proposals) {

                    final CMHintApplyOverlay hintApply = createApplyHintFunc(proposal);
                    final CMRenderFunctionOverlay renderFunc = createRenderHintFunc(proposal,
                            additionalInfoCallback);

                    final CMCompletionObjectOverlay completionObject = JavaScriptObject.createObject().cast();

                    completionObject.setHint(hintApply);
                    completionObject.setRender(renderFunc);
                    setAdditionalInfo(completionObject, proposal.getAdditionalProposalInfo());

                    list.push(completionObject);
                }
                result.setFrom(editor.getDoc().getCursor());
                setupShowAdditionalInfo(result, additionalInfoCallback);
                return result;
            }

        });
        hintOptions.setHint(hintFunction);

        editorWidget.getEditorOverlay().showHint(hintOptions);
    }

    private CMHintOptionsOverlay createDefaultHintOptions() {
        final CMHintOptionsOverlay hintOptions = CMHintOptionsOverlay.create();
        hintOptions.setCloseOnUnfocus(false); // default=true
        hintOptions.setAlignWithWord(true); //default
        hintOptions.setCompleteSingle(true); //default
        return hintOptions;
    }

    /* async version */
    public void showCompletionProposals(final CompletionsSource completionsSource,
            final AdditionalInfoCallback additionalInfoCallback) {
        if (!editorWidget.getEditorOverlay().hasShowHint()) {
            // no support for hints
            return;
        }
        if (completionsSource == null) {
            showCompletionProposals();
        }

        final CMHintOptionsOverlay hintOptions = createDefaultHintOptions();
        final CMHintFunctionOverlay hintFunction = CMHintFunctionOverlay
                .createFromAsyncHintFunction(new AsyncHintFunction() {

                    @Override
                    public void getHints(final CMEditorOverlay editor, final CMHintCallback callback,
                            final CMHintOptionsOverlay options) {
                        completionsSource.computeCompletions(new CompletionReadyCallback() {

                            @Override
                            public void onCompletionReady(final List<CompletionProposal> proposals) {
                                final CMHintResultsOverlay result = CMHintResultsOverlay.create();
                                final JsArrayMixed list = result.getList();
                                for (final CompletionProposal proposal : proposals) {

                                    final CMHintApplyOverlay hintApply = createApplyHintFunc(proposal);
                                    final CMRenderFunctionOverlay renderFunc = createRenderHintFunc(proposal,
                                            additionalInfoCallback);

                                    final CMCompletionObjectOverlay completionObject = JavaScriptObject
                                            .createObject().cast();

                                    completionObject.setHint(hintApply);
                                    completionObject.setRender(renderFunc);
                                    setAdditionalInfo(completionObject, proposal.getAdditionalProposalInfo());

                                    list.push(completionObject);
                                }
                                result.setFrom(editor.getDoc().getCursor());
                                setupShowAdditionalInfo(result, additionalInfoCallback);
                                callback.call(result);
                            }
                        });
                    }
                });

        // set the async hint function and trigger the delayed display of hints
        hintOptions.setHint(hintFunction);
        editorWidget.getEditorOverlay().showHint(hintOptions);
    }

    public void showCompletionProposals() {
        if (!editorWidget.getEditorOverlay().hasShowHint()) {
            // no support for hints
            return;
        }
        final CMHintFunctionOverlay hintAuto = CMHintFunctionOverlay.createFromName(editorWidget.getCodeMirror(),
                "auto");
        final CMHintResultsOverlay result = hintAuto.apply(editorWidget.getEditorOverlay());
        if (result != null) {
            final List<String> proposals = new ArrayList<>();
            final JsArrayMixed list = result.getList();
            int nonStrings = 0;
            //jsarray aren't iterable
            for (int i = 0; i < list.length(); i++) {
                if (result.isString(i)) {
                    proposals.add(result.getCompletionItemAsString(i));
                } else {
                    nonStrings++;
                }
            }
            LOG.info("CM Completion returned " + list.length() + " items, of which " + nonStrings
                    + " were not strings.");

            showCompletionProposals(proposals, result.getFrom(), result.getTo());
        }
    }

    private void showCompletionProposals(final List<String> proposals, final CMPositionOverlay from,
            final CMPositionOverlay to) {
        if (!editorWidget.getEditorOverlay().hasShowHint() || proposals == null || proposals.isEmpty()) {
            // no support for hints or no proposals
            return;
        }

        final CMHintOptionsOverlay hintOptions = createDefaultHintOptions();

        final CMHintFunctionOverlay hintFunction = CMHintFunctionOverlay.createFromHintFunction(new HintFunction() {

            @Override
            public CMHintResultsOverlay getHints(final CMEditorOverlay editor, final CMHintOptionsOverlay options) {
                final CMHintResultsOverlay result = CMHintResultsOverlay.create();
                final JsArrayMixed list = result.getList();
                for (final String proposal : proposals) {

                    final CMCompletionObjectOverlay completionObject = JavaScriptObject.createObject().cast();

                    completionObject.setText(proposal);
                    final CMRenderFunctionOverlay renderFunc = createRenderHintFunc(proposal);
                    completionObject.setRender(renderFunc);

                    list.push(completionObject);
                }
                result.setFrom(from);
                result.setTo(to);
                return result;
            }

        });
        hintOptions.setHint(hintFunction);

        editorWidget.getEditorOverlay().showHint(hintOptions);
    }

    private CMHintApplyOverlay createApplyHintFunc(final CompletionProposal proposal) {
        return CMHintApplyOverlay.create(new HintApplyFunction() {

            @Override
            public void applyHint(final CMEditorOverlay editor, final CMHintResultsOverlay data,
                    final JavaScriptObject completion) {
                proposal.getCompletion(new CompletionCallback() {

                    @Override
                    public void onCompletion(final Completion completion) {
                        EmbeddedDocument document = editorWidget.getDocument();
                        // apply the completion
                        completion.apply(document);
                        // set the selection
                        final LinearRange selection = completion.getSelection(document);
                        if (selection != null) {
                            editorWidget.getDocument().setSelectedRange(selection, true);
                        }
                    }
                });

            }
        });
    }

    private CMRenderFunctionOverlay createRenderHintFunc(final CompletionProposal proposal,
            final AdditionalInfoCallback additionalInfoCallback) {
        return CMRenderFunctionOverlay.create(new RenderFunction() {

            @Override
            public void renderHint(final Element element, final CMHintResultsOverlay data,
                    final JavaScriptObject completion) {
                final SpanElement icon = Elements.createSpanElement(completionCss.proposalIcon());
                final SpanElement label = Elements.createSpanElement(completionCss.proposalLabel());
                final SpanElement group = Elements.createSpanElement(completionCss.proposalGroup());
                if (proposal.getIcon() != null && proposal.getIcon().getSVGImage() != null) {
                    icon.appendChild((Node) proposal.getIcon().getSVGImage().getElement());
                } else if (proposal.getIcon() != null && proposal.getIcon().getImage() != null) {
                    icon.appendChild((Node) proposal.getIcon().getImage().getElement());
                }
                label.setInnerHTML(proposal.getDisplayString());
                element.appendChild(icon);
                element.appendChild(label);
                element.appendChild(group);

            }
        });
    }

    private CMRenderFunctionOverlay createRenderHintFunc(final String proposal) {
        return CMRenderFunctionOverlay.create(new RenderFunction() {

            @Override
            public void renderHint(final Element element, final CMHintResultsOverlay data,
                    final JavaScriptObject completion) {
                final SpanElement label = Elements.createSpanElement(completionCss.proposalLabel());
                label.setInnerHTML(proposal);
                element.appendChild(label);
            }
        });
    }

    private void setupShowAdditionalInfo(final CMHintResultsOverlay data,
            final AdditionalInfoCallback additionalInfoCallback) {

        if (additionalInfoCallback != null) {
            final CodeMirrorOverlay codeMirror = editorWidget.getCodeMirror();
            final Element bodyElement = Elements.getBody();
            codeMirror.on(data, EventTypes.COMPLETION_SELECT, new EventHandlers.EventHandlerMixedParameters() {
                @Override
                public void onEvent(final JsArrayMixed param) {
                    // param 0 -> completion object (string or object)
                    final CMCompletionObjectOverlay completionObject = param.getObject(0);
                    // param 1 -> DOM node in the menu
                    final JsElement itemElement = param.getObject(1);
                    final ClientRect itemRect = itemElement.getBoundingClientRect();
                    Element popup = itemElement;
                    while (popup.getParentElement() != null && !popup.getParentElement().equals(bodyElement)) {
                        popup = popup.getParentElement();
                    }
                    final ClientRect popupRect = popup.getBoundingClientRect();
                    final float pixelX = Math.max(itemRect.getRight(), popupRect.getRight());
                    final float pixelY = itemRect.getTop();
                    final Element info = getAdditionalInfo(completionObject);

                    // there can be only one
                    // remove any other body child with the additional info marker
                    removeStaleInfoPopups(ADDITIONAL_INFO_MARKER);

                    // Don't show anything if there is no additional info
                    if (info == null) {
                        return;
                    }

                    final Element infoDisplayElement = additionalInfoCallback.onAdditionalInfoNeeded(pixelX, pixelY,
                            info);
                    // set the additional info marker on the popup element
                    infoDisplayElement.getClassList().add(ADDITIONAL_INFO_MARKER);
                }
            });

            // close the additional info along with the completion popup
            codeMirror.on(data, EventTypes.COMPLETION_CLOSE, new EventHandlers.EventHandlerNoParameters() {
                @Override
                public void onEvent() {
                    delayedRemoveStaleInfoPopups(ADDITIONAL_INFO_MARKER);
                }
            });
        }
    }

    private static void delayedRemoveStaleInfoPopups(final String markerClass) {
        new Timer() {
            @Override
            public void run() {
                removeStaleInfoPopups(markerClass);
            }
        }.schedule(100);
    }

    private static void removeStaleInfoPopups(final String markerClass) {
        final Document documentElement = Elements.getDocument();
        final NodeList markersToRemove = documentElement.getElementsByClassName(markerClass);
        for (int i = 0; i < markersToRemove.getLength(); i++) {
            final Node childToRemove = markersToRemove.item(i);
            final Node parent = childToRemove.getParentNode();
            if (parent != null) {
                parent.removeChild(childToRemove);
            }
        }
    }

    private static void setAdditionalInfo(final CMCompletionObjectOverlay completion, final Element value) {
        JsMapFromStringTo<Element> element = completion.cast();
        element.put(PROP_ADDITIONAL_INFO, value);
    }

    private static Element getAdditionalInfo(final CMCompletionObjectOverlay completion) {
        JsMapFromStringTo<Element> element = completion.cast();
        return element.get(PROP_ADDITIONAL_INFO);
    }
}