/******************************************************************************* * Copyright (c) 2012-2017 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.plugin.languageserver.ide.editor.codeassist; import com.google.gwt.dom.client.Style; import com.google.gwt.dom.client.Style.Overflow; import com.google.gwt.safehtml.shared.SafeHtmlBuilder; import com.google.gwt.user.client.rpc.AsyncCallback; import com.google.gwt.user.client.ui.HTML; import com.google.gwt.user.client.ui.Widget; import org.eclipse.che.api.languageserver.shared.model.ExtendedCompletionItem; import org.eclipse.che.api.promises.client.Operation; import org.eclipse.che.api.promises.client.OperationException; import org.eclipse.che.api.promises.client.Promise; import org.eclipse.che.api.promises.client.PromiseError; import org.eclipse.che.ide.api.editor.codeassist.Completion; import org.eclipse.che.ide.api.editor.codeassist.CompletionProposal; import org.eclipse.che.ide.api.editor.document.Document; import org.eclipse.che.ide.api.editor.text.LinearRange; import org.eclipse.che.ide.api.editor.text.TextPosition; import org.eclipse.che.ide.api.icon.Icon; import org.eclipse.che.ide.filters.Match; import org.eclipse.che.plugin.languageserver.ide.LanguageServerResources; import org.eclipse.che.plugin.languageserver.ide.service.TextDocumentServiceClient; import org.eclipse.lsp4j.CompletionItem; import org.eclipse.lsp4j.Range; import org.eclipse.lsp4j.ServerCapabilities; import org.eclipse.lsp4j.TextDocumentIdentifier; import java.util.List; import static org.eclipse.che.ide.api.theme.Style.theme; /** * @author Anatolii Bazko * @author Kaloyan Raev */ public class CompletionItemBasedCompletionProposal implements CompletionProposal { private final TextDocumentServiceClient documentServiceClient; private final TextDocumentIdentifier documentId; private final LanguageServerResources resources; private final Icon icon; private final ServerCapabilities serverCapabilities; private final List<Match> highlights; private final int offset; private ExtendedCompletionItem completionItem; private boolean resolved; CompletionItemBasedCompletionProposal(ExtendedCompletionItem completionItem, TextDocumentServiceClient documentServiceClient, TextDocumentIdentifier documentId, LanguageServerResources resources, Icon icon, ServerCapabilities serverCapabilities, List<Match> highlights, int offset) { this.completionItem = completionItem; this.documentServiceClient = documentServiceClient; this.documentId = documentId; this.resources = resources; this.icon = icon; this.serverCapabilities = serverCapabilities; this.highlights = highlights; this.offset = offset; this.resolved = false; } @Override public void getAdditionalProposalInfo(final AsyncCallback<Widget> callback) { if (completionItem.getDocumentation() == null && canResolve()) { resolve().then(new Operation<ExtendedCompletionItem>() { @Override public void apply(ExtendedCompletionItem item) throws OperationException { completionItem = item; resolved = true; callback.onSuccess(createAdditionalInfoWidget()); } }).catchError(new Operation<PromiseError>() { @Override public void apply(PromiseError e) throws OperationException { callback.onFailure(e.getCause()); } }); } else { callback.onSuccess(createAdditionalInfoWidget()); } } private Widget createAdditionalInfoWidget() { String documentation = completionItem.getDocumentation(); if (documentation == null || documentation.trim().isEmpty()) { documentation = "No documentation found."; } HTML widget = new HTML(documentation); widget.setWordWrap(true); widget.getElement().getStyle().setColor(theme.completionPopupItemTextColor()); widget.getElement().getStyle().setFontSize(13, Style.Unit.PX); widget.getElement().getStyle().setMarginLeft(4, Style.Unit.PX); widget.getElement().getStyle().setOverflow(Overflow.AUTO); widget.getElement().getStyle().setProperty("userSelect", "text"); widget.setHeight("100%"); return widget; } @Override public String getDisplayString() { SafeHtmlBuilder builder = new SafeHtmlBuilder(); String label = completionItem.getLabel(); int pos = 0; for (Match highlight : highlights) { if (highlight.getStart() == highlight.getEnd()) { continue; } if (pos < highlight.getStart()) { appendPlain(builder, label.substring(pos, highlight.getStart())); } appendHighlighted(builder, label.substring(highlight.getStart(), highlight.getEnd())); pos = highlight.getEnd(); } if (pos < label.length()) { appendPlain(builder, label.substring(pos)); } if (completionItem.getDetail() != null) { appendDetail(builder, completionItem.getDetail()); } return builder.toSafeHtml().asString(); } private void appendPlain(SafeHtmlBuilder builder, String text) { builder.appendEscaped(text); } private void appendHighlighted(SafeHtmlBuilder builder, String text) { builder.appendHtmlConstant("<span class=\"" + resources.css().codeassistantHighlight() + "\">"); builder.appendEscaped(text); builder.appendHtmlConstant("</span>"); } private void appendDetail(SafeHtmlBuilder builder, String text) { builder.appendHtmlConstant(" <span class=\"" + resources.css().codeassistantDetail() + "\">"); builder.appendEscaped(text); builder.appendHtmlConstant("</span>"); } @Override public Icon getIcon() { return icon; } @Override public void getCompletion(final CompletionCallback callback) { callback.onCompletion(new CompletionImpl(completionItem, offset)); } private boolean canResolve() { return !resolved && serverCapabilities.getCompletionProvider() != null && serverCapabilities.getCompletionProvider().getResolveProvider() != null && serverCapabilities.getCompletionProvider().getResolveProvider(); } private Promise<ExtendedCompletionItem> resolve() { completionItem.setTextDocumentIdentifier(documentId); return documentServiceClient.resolveCompletionItem(completionItem); } private static class CompletionImpl implements Completion { private CompletionItem completionItem; private int offset; public CompletionImpl(CompletionItem completionItem, int offset) { this.completionItem = completionItem; this.offset = offset; } @Override public void apply(Document document) { if (completionItem.getTextEdit() != null) { Range range = completionItem.getTextEdit().getRange(); int startOffset = document.getIndexFromPosition( new TextPosition(range.getStart().getLine(), range.getStart().getCharacter())); int endOffset = offset + document.getIndexFromPosition( new TextPosition(range.getEnd().getLine(), range.getEnd().getCharacter())); document.replace(startOffset, endOffset - startOffset, completionItem.getTextEdit().getNewText()); } else { String insertText = completionItem.getInsertText() == null ? completionItem.getLabel() : completionItem.getInsertText(); document.replace(document.getCursorOffset() - offset, offset, insertText); } } @Override public LinearRange getSelection(Document document) { Range range = completionItem.getTextEdit().getRange(); int startOffset = document .getIndexFromPosition(new TextPosition(range.getStart().getLine(), range.getStart().getCharacter())) + completionItem.getTextEdit().getNewText().length(); return LinearRange.createWithStart(startOffset).andLength(0); } } }