org.graalvm.tools.lsp.server.request.CompletionRequestHandler Maven / Gradle / Ivy
/*
* Copyright (c) 2020, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation. Oracle designates this
* particular file as subject to the "Classpath" exception as provided
* by Oracle in the LICENSE file that accompanied this code.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
package org.graalvm.tools.lsp.server.request;
import java.net.URI;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Set;
import java.util.concurrent.Future;
import java.util.logging.Level;
import org.graalvm.tools.api.lsp.LSPLibrary;
import org.graalvm.tools.lsp.exceptions.DiagnosticsNotification;
import org.graalvm.tools.lsp.server.ContextAwareExecutor;
import org.graalvm.tools.lsp.server.LanguageTriggerCharacters;
import org.graalvm.tools.lsp.server.types.CompletionContext;
import org.graalvm.tools.lsp.server.types.CompletionItem;
import org.graalvm.tools.lsp.server.types.CompletionItemKind;
import org.graalvm.tools.lsp.server.types.CompletionList;
import org.graalvm.tools.lsp.server.types.CompletionTriggerKind;
import org.graalvm.tools.lsp.server.types.Diagnostic;
import org.graalvm.tools.lsp.server.types.DiagnosticSeverity;
import org.graalvm.tools.lsp.server.types.MarkupContent;
import org.graalvm.tools.lsp.server.types.MarkupKind;
import org.graalvm.tools.lsp.server.utils.CoverageData;
import org.graalvm.tools.lsp.server.utils.EvaluationResult;
import org.graalvm.tools.lsp.server.utils.NearestNode;
import org.graalvm.tools.lsp.server.utils.NearestSectionsFinder;
import org.graalvm.tools.lsp.server.utils.SourceUtils;
import org.graalvm.tools.lsp.server.utils.SourceUtils.SourceFix;
import org.graalvm.tools.lsp.server.utils.SourceWrapper;
import org.graalvm.tools.lsp.server.utils.TextDocumentSurrogate;
import org.graalvm.tools.lsp.server.utils.TextDocumentSurrogateMap;
import com.oracle.truffle.api.CallTarget;
import com.oracle.truffle.api.CompilerDirectives;
import com.oracle.truffle.api.frame.MaterializedFrame;
import com.oracle.truffle.api.instrumentation.TruffleInstrument;
import com.oracle.truffle.api.interop.InteropException;
import com.oracle.truffle.api.interop.InteropLibrary;
import com.oracle.truffle.api.interop.NodeLibrary;
import com.oracle.truffle.api.interop.TruffleObject;
import com.oracle.truffle.api.interop.UnsupportedMessageException;
import com.oracle.truffle.api.nodes.LanguageInfo;
import com.oracle.truffle.api.nodes.Node;
import com.oracle.truffle.api.source.Source;
import com.oracle.truffle.api.source.SourceSection;
public final class CompletionRequestHandler extends AbstractRequestHandler {
private static final InteropLibrary INTEROP = InteropLibrary.getFactory().getUncached();
private static final LSPLibrary LSP_INTEROP = LSPLibrary.getFactory().getUncached();
private enum CompletionKind {
UNKOWN,
OBJECT_PROPERTY,
GLOBALS_AND_LOCALS
}
public final CompletionList emptyList = CompletionList.create(Collections.emptyList(), false);
private static final int SORTING_PRIORITY_LOCALS = 1;
private static final int SORTING_PRIORITY_GLOBALS = 2;
private final SourceCodeEvaluator sourceCodeEvaluator;
private final LanguageTriggerCharacters languageCompletionTriggerCharacters;
public CompletionRequestHandler(TruffleInstrument.Env envMain, TruffleInstrument.Env env, TextDocumentSurrogateMap surrogateMap, ContextAwareExecutor executor,
SourceCodeEvaluator sourceCodeEvaluator, LanguageTriggerCharacters completionTriggerCharacters) {
super(envMain, env, surrogateMap, executor);
this.sourceCodeEvaluator = sourceCodeEvaluator;
this.languageCompletionTriggerCharacters = completionTriggerCharacters;
}
public CompletionList completionWithEnteredContext(final URI uri, int line, int column, CompletionContext completionContext) throws DiagnosticsNotification {
logger.log(Level.FINER, "Start finding completions for {0}:{1}:{2}", new Object[]{uri, line, column});
TextDocumentSurrogate surrogate = surrogateMap.get(uri);
if (surrogate == null) {
logger.info("Completion requested in an unknown document: " + uri);
return emptyList;
}
Source source = surrogate.getSource();
if (!SourceUtils.isLineValid(line, source) || !SourceUtils.isColumnValid(line, column, source)) {
logger.fine("line or column is out of range, line=" + line + ", column=" + column);
return emptyList;
}
List completionTriggerCharacters = languageCompletionTriggerCharacters.getTriggerCharacters(surrogate.getLanguageId());
CompletionKind completionKind = getCompletionKind(source, SourceUtils.zeroBasedLineToOneBasedLine(line, source), column, completionTriggerCharacters,
completionContext);
if (surrogate.isSourceCodeReadyForCodeCompletion()) {
return createCompletions(surrogate, line, column, completionKind);
} else {
// Try fixing the source code, parse again, then create the completions
SourceFix sourceFix = SourceUtils.removeLastTextInsertion(surrogate, column, logger);
if (sourceFix == null) {
logger.fine("Unable to fix unparsable source code. No completion possible.");
return emptyList;
}
TextDocumentSurrogate fixedSurrogate = surrogate.copy();
// TODO(ds) Should we reset coverage data etc? Or adjust the SourceLocations?
fixedSurrogate.setEditorText(sourceFix.text);
SourceWrapper sourceWrapper = fixedSurrogate.prepareParsing();
CallTarget callTarget = null;
try {
callTarget = env.parse(sourceWrapper.getSource());
} catch (Exception e) {
err.println("Parsing a fixed source caused an exception: " + e.getClass().getSimpleName() + " > " + e.getLocalizedMessage());
return emptyList;
} finally {
fixedSurrogate.notifyParsingDone(callTarget);
}
// We need to replace the original surrogate with the fixed one so that when a run
// script wants to import this fixed source, it will find the fixed surrogate via the
// custom file system callback
surrogateMap.put(uri, fixedSurrogate);
try {
return createCompletions(fixedSurrogate, line, sourceFix.characterIdx, getCompletionKind(sourceFix.removedCharacters, completionTriggerCharacters));
} finally {
surrogateMap.put(uri, surrogate);
}
}
}
private CompletionList createCompletions(TextDocumentSurrogate surrogate, int line, int column, CompletionKind completionKind) throws DiagnosticsNotification {
List completions = new ArrayList<>();
if (completionKind == CompletionKind.GLOBALS_AND_LOCALS) {
fillCompletionsWithGlobalsAndLocals(line, surrogate, column, completions);
} else if (completionKind == CompletionKind.OBJECT_PROPERTY) {
fillCompletionsWithObjectProperties(surrogate, line, column, completions);
}
return CompletionList.create(completions, false);
}
private void fillCompletionsWithGlobalsAndLocals(int line, TextDocumentSurrogate surrogate, int column, List completions) {
Node nearestNode = findNearestNode(surrogate.getSourceWrapper(), line, column);
if (nearestNode == null) {
// Cannot locate a valid node near the caret position, therefore provide globals only
fillCompletionsWithGlobals(surrogate, completions);
return;
}
if (!surrogate.hasCoverageData()) {
// No coverage data, so we simply display locals without specific frame information and
// globals
fillCompletionsWithLocals(surrogate, nearestNode, completions, null);
fillCompletionsWithGlobals(surrogate, completions);
return;
}
// We have coverage data, so we try to derive locals from coverage data
List coverages = surrogate.getCoverageData(nearestNode.getSourceSection());
if (coverages == null || coverages.isEmpty()) {
coverages = SourceCodeEvaluator.findCoverageDataBeforeNode(surrogate, nearestNode);
}
if (coverages != null && !coverages.isEmpty()) {
CoverageData coverageData = coverages.get(coverages.size() - 1);
MaterializedFrame frame = coverageData.getFrame();
fillCompletionsWithLocals(surrogate, nearestNode, completions, frame);
// Call again, regardless if it was called with a frame argument before,
// because duplicates will be filter, but it will add missing local
// variables which were dropped (because of null values) in the call above
fillCompletionsWithLocals(surrogate, nearestNode, completions, null);
fillCompletionsWithGlobals(surrogate, completions);
} else {
// No coverage data found for the designated source section, so use the default look-up
// as fallback
fillCompletionsWithGlobals(surrogate, completions);
fillCompletionsWithLocals(surrogate, nearestNode, completions, null);
}
}
private Node findNearestNode(SourceWrapper sourceWrapper, int line, int column) {
NearestNode nearestNodeHolder = NearestSectionsFinder.findNearestNode(sourceWrapper.getSource(), line, column, env, logger);
return nearestNodeHolder.getNode();
}
private void fillCompletionsWithObjectProperties(TextDocumentSurrogate surrogate, int line, int column, List completions) throws DiagnosticsNotification {
SourceWrapper sourceWrapper = surrogate.getSourceWrapper();
Source source = sourceWrapper.getSource();
NearestNode nearestNodeHolder = NearestSectionsFinder.findExprNodeBeforePos(source, line, column, env);
Node nearestNode = nearestNodeHolder.getNode();
if (nearestNode != null) {
Future future = contextAwareExecutor.executeWithNestedContext(() -> sourceCodeEvaluator.tryDifferentEvalStrategies(surrogate, nearestNode), true);
EvaluationResult evalResult = getFutureResultOrHandleExceptions(future);
if (evalResult != null && evalResult.isEvaluationDone()) {
if (!evalResult.isError()) {
fillCompletionsFromTruffleObject(completions, surrogate.getLanguageInfo(), evalResult.getResult());
} else {
Object result = evalResult.getResult();
if (result != null && INTEROP.isException(result)) {
SourceSection sourceLocation;
String exceptionMessage;
try {
sourceLocation = INTEROP.hasSourceLocation(result) ? INTEROP.getSourceLocation(result) : null;
exceptionMessage = INTEROP.hasExceptionMessage(result) ? INTEROP.asString(INTEROP.getExceptionMessage(result)) : null;
} catch (UnsupportedMessageException um) {
throw CompilerDirectives.shouldNotReachHere(um);
}
throw DiagnosticsNotification.create(surrogate.getUri(),
Diagnostic.create(SourceUtils.sourceSectionToRange(sourceLocation), "An error occurred during execution: " + exceptionMessage,
DiagnosticSeverity.Warning, null, "Graal", null));
} else {
((Exception) evalResult.getResult()).printStackTrace(err);
}
}
} else {
throw DiagnosticsNotification.create(surrogate.getUri(),
Diagnostic.create(SourceUtils.sourceSectionToRange(nearestNode.getSourceSection()), "No type information available for this source section.",
DiagnosticSeverity.Information, null, "Graal", null));
}
} else {
logger.fine("No object property completion possible. Caret is not directly at the end of a source section. Line: " + line + ", column: " + column);
}
}
private static boolean isObjectPropertyCompletionCharacter(String text, List completionTriggerCharacters) {
return completionTriggerCharacters.contains(text);
}
private static CompletionKind getCompletionKind(String text, List completionTriggerCharacters) {
return isObjectPropertyCompletionCharacter(text, completionTriggerCharacters) ? CompletionKind.OBJECT_PROPERTY : CompletionKind.GLOBALS_AND_LOCALS;
}
public static CompletionKind getCompletionKind(Source source, int oneBasedLineNumber, int column, List completionTriggerCharacters, CompletionContext completionContext) {
if (completionContext != null && completionContext.getTriggerKind() == CompletionTriggerKind.TriggerCharacter && completionContext.getTriggerCharacter() != null) {
if (isObjectPropertyCompletionCharacter(completionContext.getTriggerCharacter(), completionTriggerCharacters)) {
return CompletionKind.OBJECT_PROPERTY;
}
// Completion was triggered by a character, which is a completion trigger character of
// another language. Therefore we have to skip the current completion request.
return CompletionKind.UNKOWN;
}
int lineStartOffset = source.getLineStartOffset(oneBasedLineNumber);
if (lineStartOffset + column == 0) {
return CompletionKind.GLOBALS_AND_LOCALS;
}
String text = source.getCharacters().toString();
char charAtOffset = text.charAt(lineStartOffset + column - 1);
return getCompletionKind(String.valueOf(charAtOffset), completionTriggerCharacters);
}
private void fillCompletionsWithLocals(final TextDocumentSurrogate surrogate, Node nearestNode, List completions, MaterializedFrame frame) {
NodeLibrary nodeLibrary = NodeLibrary.getUncached(nearestNode);
if (nodeLibrary.hasScope(nearestNode, frame)) {
try {
Object scope = nodeLibrary.getScope(nearestNode, frame, true);
fillCompletionsWithScopesValues(surrogate, completions, scope, CompletionItemKind.Variable, SORTING_PRIORITY_LOCALS);
} catch (UnsupportedMessageException e) {
throw CompilerDirectives.shouldNotReachHere(e);
}
}
}
private void fillCompletionsWithGlobals(final TextDocumentSurrogate surrogate, List completions) {
Object scope = env.getScope(surrogate.getLanguageInfo());
if (scope != null) {
fillCompletionsWithScopesValues(surrogate, completions, scope, null, SORTING_PRIORITY_GLOBALS);
}
}
private void fillCompletionsWithScopesValues(TextDocumentSurrogate surrogate, List completions, Object scopeOriginal,
CompletionItemKind completionItemKindDefault, int displayPriority) {
LanguageInfo langInfo = surrogate.getLanguageInfo();
String[] existingCompletions = completions.stream().map((item) -> item.getLabel()).toArray(String[]::new);
// Filter duplicates
Set completionKeys = new HashSet<>(Arrays.asList(existingCompletions));
int scopeCounter = 0;
Object scope = scopeOriginal;
while (scope != null) {
Object scopeParent = null;
if (INTEROP.hasScopeParent(scope)) {
try {
scopeParent = INTEROP.getScopeParent(scope);
} catch (UnsupportedMessageException e) {
throw CompilerDirectives.shouldNotReachHere(e);
}
}
++scopeCounter;
Object keys;
long size;
try {
keys = INTEROP.getMembers(scope, false);
size = INTEROP.getArraySize(keys);
if (scopeParent != null) {
size -= INTEROP.getArraySize(INTEROP.getMembers(scopeParent, false));
}
} catch (Exception ex) {
logger.log(Level.INFO, ex.getLocalizedMessage(), ex);
break;
}
for (long i = 0; i < size; i++) {
String key;
Object object;
try {
key = INTEROP.asString(INTEROP.readArrayElement(keys, i));
if (completionKeys.contains(key)) {
// Scopes are provided from inner to outer, so we need to detect duplicate
// keys and only take those from the most inner scope
continue;
} else {
completionKeys.add(key);
}
object = INTEROP.readMember(scope, key);
} catch (ThreadDeath td) {
throw td;
} catch (Throwable t) {
logger.log(Level.CONFIG, scope.toString(), t);
continue;
}
CompletionItem completion = CompletionItem.create(key);
// Inner scopes should be displayed first, so sort by priority and scopeCounter
// (the innermost scope has the lowest counter)
completion.setSortText(format("%s%d.%04d.%s", "+", displayPriority, scopeCounter, key));
if (completionItemKindDefault != null) {
completion.setKind(completionItemKindDefault);
} else {
completion.setKind(findCompletionItemKind(object));
}
completion.setDetail(createCompletionDetail(object, langInfo));
try {
completion.setDocumentation(createDocumentation(object, surrogate.getLanguageInfo(), "in " + INTEROP.asString(INTEROP.toDisplayString(scope))));
} catch (UnsupportedMessageException e) {
throw CompilerDirectives.shouldNotReachHere(e);
}
completions.add(completion);
}
scope = scopeParent;
}
}
private static CompletionItemKind findCompletionItemKind(Object object) {
if (INTEROP.isInstantiable(object)) {
return CompletionItemKind.Class;
}
if (INTEROP.isExecutable(object)) {
return CompletionItemKind.Function;
}
return null;
}
protected boolean fillCompletionsFromTruffleObject(List completions, LanguageInfo langInfo, Object object) {
if (object == null) {
return false;
}
Object metaObject = getMetaObject(langInfo, object);
if (metaObject == null) {
return false;
}
Object languageView = env.getLanguageView(langInfo, object);
Object members = null;
if (INTEROP.hasMembers(languageView)) {
try {
members = INTEROP.getMembers(languageView);
} catch (UnsupportedMessageException ex) {
// No members
}
}
if (members == null || !INTEROP.hasArrayElements(members)) {
logger.fine("No completions found for object: " + languageView);
return false;
}
int counter = 0;
long size;
try {
size = INTEROP.getArraySize(members);
} catch (UnsupportedMessageException ex) {
size = 0;
}
for (long i = 0; i < size; i++) {
String key;
Object value;
try {
key = INTEROP.readArrayElement(members, i).toString();
if (INTEROP.isMemberReadable(languageView, key)) {
value = INTEROP.readMember(languageView, key);
} else {
value = null;
}
} catch (ThreadDeath td) {
throw td;
} catch (Throwable t) {
logger.log(Level.CONFIG, languageView.toString(), t);
continue;
}
CompletionItem completion = CompletionItem.create(key);
++counter;
// Keep the order in which the keys were provided
completion.setSortText(format("%s%06d.%s", "+", counter, key));
completion.setKind(CompletionItemKind.Property);
completion.setDetail(createCompletionDetail(value, langInfo));
try {
completion.setDocumentation(createDocumentation(value, langInfo, "of " + INTEROP.getMetaQualifiedName(metaObject)));
} catch (UnsupportedMessageException e) {
throw new AssertionError(e);
}
completions.add(completion);
}
return counter > 0;
}
private Object createDocumentation(Object value, LanguageInfo langInfo, String scopeInformation) {
Object documentation = getDocumentation(value, langInfo);
if (documentation == null) {
String markupStr = escapeMarkdown(scopeInformation);
Object view = env.getLanguageView(langInfo, value);
if (INTEROP.hasSourceLocation(view)) {
SourceSection section;
try {
section = INTEROP.getSourceLocation(view);
} catch (UnsupportedMessageException e) {
CompilerDirectives.transferToInterpreter();
throw new AssertionError(e);
}
String code = section.getCharacters().toString();
if (!code.isEmpty()) {
markupStr += "\n\n```\n" + section.getCharacters().toString() + "\n```";
}
}
documentation = MarkupContent.create(MarkupKind.Markdown, markupStr);
}
return documentation;
}
static String escapeMarkdown(String original) {
return original.replaceAll("__", "\\\\_\\\\_");
}
String createCompletionDetail(Object obj, LanguageInfo langInfo) {
String detailText = "";
if (obj == null) {
return detailText;
}
Object view = env.getLanguageView(langInfo, obj);
if (INTEROP.isExecutable(view)) {
String formattedSignature = getFormattedSignature(view, langInfo);
detailText = formattedSignature != null ? formattedSignature : "";
}
Object metaObject = null;
if (INTEROP.hasMetaObject(view)) {
try {
metaObject = INTEROP.getMetaObject(view);
} catch (UnsupportedMessageException e) {
CompilerDirectives.transferToInterpreter();
throw new AssertionError("Unexpected unsupported message.", e);
}
}
if (metaObject != null) {
if (!detailText.isEmpty()) {
detailText += " ";
}
try {
detailText += INTEROP.asString(INTEROP.toDisplayString(metaObject));
} catch (UnsupportedMessageException e) {
CompilerDirectives.transferToInterpreter();
throw new AssertionError("Unexpected unsupported message.", e);
}
}
return detailText;
}
public Object getDocumentation(Object value, LanguageInfo langInfo) {
if (!(value instanceof TruffleObject) || INTEROP.isNull(value)) {
return null;
}
try {
Object docu = LSP_INTEROP.getDocumentation(value);
if (docu instanceof String && !((String) docu).isEmpty()) {
return docu;
} else {
if (docu instanceof TruffleObject) {
TruffleObject markup = (TruffleObject) docu;
MarkupKind markupKind = null;
String text = null;
if (INTEROP.isMemberReadable(markup, "kind")) {
Object kind = INTEROP.readMember(markup, "kind");
if (kind instanceof String) {
markupKind = MarkupKind.get((String) kind);
}
}
if (markupKind == null) {
markupKind = MarkupKind.PlainText;
}
if (INTEROP.isMemberReadable(markup, "value")) {
Object v = INTEROP.readMember(markup, "value");
if (v instanceof String) {
text = (String) v;
}
}
assert text != null : "No documentation value is provided from " + docu;
if (text != null) {
return MarkupContent.create(markupKind, text);
} else {
return MarkupContent.create(markupKind, languageToString(langInfo, docu));
}
}
}
} catch (UnsupportedMessageException e) {
// GET_DOCUMENTATION message is not supported
} catch (InteropException e) {
e.printStackTrace(err);
}
return null;
}
private String languageToString(LanguageInfo langInfo, Object value) {
try {
return INTEROP.asString(INTEROP.toDisplayString(env.getLanguageView(langInfo, value)));
} catch (UnsupportedMessageException e) {
CompilerDirectives.transferToInterpreter();
throw new AssertionError(e);
}
}
public String getFormattedSignature(Object truffleObj, LanguageInfo langInfo) {
try {
Object signature = LSP_INTEROP.getSignature(truffleObj);
return languageToString(langInfo, signature);
} catch (UnsupportedMessageException e) {
// GET_SIGNATURE message is not supported
}
return null;
}
private LanguageInfo getObjectLanguageInfo(LanguageInfo defaultInfo, Object object) {
assert object != null;
if (INTEROP.hasLanguage(object)) {
try {
return env.getLanguageInfo(INTEROP.getLanguage(object));
} catch (UnsupportedMessageException e) {
CompilerDirectives.transferToInterpreter();
throw new AssertionError(e);
}
} else {
return defaultInfo;
}
}
private Object getMetaObject(LanguageInfo defaultInfo, Object object) {
LanguageInfo langInfo = getObjectLanguageInfo(defaultInfo, object);
Object view = env.getLanguageView(langInfo, object);
if (INTEROP.hasMetaObject(view)) {
try {
return INTEROP.getMetaObject(view);
} catch (UnsupportedMessageException e) {
CompilerDirectives.transferToInterpreter();
throw new AssertionError("Unexpected unsupported message.", e);
}
}
return null;
}
private static String format(String format, Object... args) {
return String.format(Locale.ENGLISH, format, args);
}
}
© 2015 - 2024 Weber Informatics LLC | Privacy Policy