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