All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.graalvm.tools.lsp.server.TruffleAdapter Maven / Gradle / Ivy

/*
 * Copyright (c) 2022, 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;

import java.io.IOException;
import java.net.URI;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import java.util.function.Function;
import java.util.logging.Level;

import org.graalvm.tools.api.lsp.LSPCommand;
import org.graalvm.tools.api.lsp.LSPExtension;
import org.graalvm.tools.api.lsp.LSPServerAccessor;
import org.graalvm.tools.lsp.exceptions.DiagnosticsNotification;
import org.graalvm.tools.lsp.exceptions.UnknownLanguageException;
import org.graalvm.tools.lsp.server.request.AbstractRequestHandler;
import org.graalvm.tools.lsp.server.request.CompletionRequestHandler;
import org.graalvm.tools.lsp.server.request.CoverageRequestHandler;
import org.graalvm.tools.lsp.server.request.HighlightRequestHandler;
import org.graalvm.tools.lsp.server.request.HoverRequestHandler;
import org.graalvm.tools.lsp.server.request.SignatureHelpRequestHandler;
import org.graalvm.tools.lsp.server.request.SourceCodeEvaluator;
import org.graalvm.tools.lsp.server.types.CompletionContext;
import org.graalvm.tools.lsp.server.types.CompletionList;
import org.graalvm.tools.lsp.server.types.CompletionOptions;
import org.graalvm.tools.lsp.server.types.Coverage;
import org.graalvm.tools.lsp.server.types.DocumentHighlight;
import org.graalvm.tools.lsp.server.types.ExecuteCommandParams;
import org.graalvm.tools.lsp.server.types.Hover;
import org.graalvm.tools.lsp.server.types.ServerCapabilities;
import org.graalvm.tools.lsp.server.types.SignatureHelp;
import org.graalvm.tools.lsp.server.types.SignatureHelpOptions;
import org.graalvm.tools.lsp.server.types.TextDocumentContentChangeEvent;
import org.graalvm.tools.lsp.server.utils.SourceUtils;
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.InstrumentInfo;
import com.oracle.truffle.api.Truffle;
import com.oracle.truffle.api.TruffleFile;
import com.oracle.truffle.api.TruffleLogger;
import com.oracle.truffle.api.instrumentation.TruffleInstrument;
import com.oracle.truffle.api.instrumentation.TruffleInstrument.Env;
import com.oracle.truffle.api.nodes.LanguageInfo;
import com.oracle.truffle.api.source.Source;

/**
 * This class delegates LSP requests of {@link LanguageServerImpl} to specific implementations of
 * {@link AbstractRequestHandler}. It is responsible for wrapping requests into tasks for an
 * instance of {@link ContextAwareExecutor}, so that these tasks are executed by a Thread which has
 * entered a {@link org.graalvm.polyglot.Context}.
 *
 */
public final class TruffleAdapter implements VirtualLanguageServerFileProvider {

    private final boolean developerMode;
    private final TruffleLogger logger;
    private final TruffleInstrument.Env envMain;
    private TruffleInstrument.Env envInternal;
    ContextAwareExecutor contextAwareExecutor;
    private SourceCodeEvaluator sourceCodeEvaluator;
    CompletionRequestHandler completionHandler;
    private HoverRequestHandler hoverHandler;
    private SignatureHelpRequestHandler signatureHelpHandler;
    private CoverageRequestHandler coverageHandler;
    private HighlightRequestHandler highlightHandler;
    private List extensionCommands;
    private LSPServerAccessor lspServer;
    private TextDocumentSurrogateMap surrogateMap;
    private final LanguageTriggerCharacters completionTriggerCharacters = new LanguageTriggerCharacters();
    private final LanguageTriggerCharacters signatureTriggerCharacters = new LanguageTriggerCharacters();

    public TruffleAdapter(TruffleInstrument.Env mainEnv, boolean developerMode) {
        this.envMain = mainEnv;
        this.developerMode = developerMode;
        this.logger = envMain.getLogger("");
    }

    public void register(Env environment, ContextAwareExecutor executor) {
        this.envInternal = environment;
        this.contextAwareExecutor = executor;
        initSurrogateMap();
        createLSPRequestHandlers();
    }

    public TruffleLogger getLogger() {
        return logger;
    }

    private void createLSPRequestHandlers() {
        this.sourceCodeEvaluator = new SourceCodeEvaluator(envMain, envInternal, surrogateMap, contextAwareExecutor);
        this.completionHandler = new CompletionRequestHandler(envMain, envInternal, surrogateMap, contextAwareExecutor, sourceCodeEvaluator, completionTriggerCharacters);
        this.hoverHandler = new HoverRequestHandler(envMain, envInternal, surrogateMap, contextAwareExecutor, completionHandler, developerMode);
        this.signatureHelpHandler = new SignatureHelpRequestHandler(envMain, envInternal, surrogateMap, contextAwareExecutor, sourceCodeEvaluator, completionHandler, signatureTriggerCharacters);
        this.coverageHandler = new CoverageRequestHandler(envMain, envInternal, surrogateMap, contextAwareExecutor, sourceCodeEvaluator);
        this.highlightHandler = new HighlightRequestHandler(envMain, envInternal, surrogateMap, contextAwareExecutor);
    }

    private void initSurrogateMap() {
        try {
            contextAwareExecutor.executeWithDefaultContext(() -> {
                logger.log(Level.CONFIG, "Truffle Runtime: {0}", Truffle.getRuntime().getName());
                return null;
            }).get();

            this.surrogateMap = new TextDocumentSurrogateMap(envInternal);
        } catch (InterruptedException | ExecutionException e) {
            throw new RuntimeException(e);
        }
    }

    TextDocumentSurrogate getOrCreateSurrogate(URI uri, String text, LanguageInfo languageInfo) {
        TextDocumentSurrogate surrogate = surrogateMap.getOrCreateSurrogate(uri, languageInfo);
        surrogate.setEditorText(text);
        return surrogate;
    }

    public void didClose(URI uri) {
        surrogateMap.remove(uri);
    }

    public Future parse(final String text, final String langId, final URI uri) {
        return contextAwareExecutor.executeWithDefaultContext(() -> parseWithEnteredContext(text, langId, uri));
    }

    protected CallTarget parseWithEnteredContext(final String text, final String langId, final URI uri) throws DiagnosticsNotification {
        LanguageInfo languageInfo = findLanguageInfo(langId, envInternal.getTruffleFile(null, uri));
        TextDocumentSurrogate surrogate = getOrCreateSurrogate(uri, text, languageInfo);
        return parseWithEnteredContext(surrogate);
    }

    CallTarget parseWithEnteredContext(TextDocumentSurrogate surrogate) throws DiagnosticsNotification {
        return sourceCodeEvaluator.parse(surrogate);
    }

    public Future reparse(URI uri) {
        TextDocumentSurrogate surrogate = surrogateMap.get(uri);
        return contextAwareExecutor.executeWithDefaultContext(() -> parseWithEnteredContext(surrogate));
    }

    /**
     * Special handling needed, because some LSP clients send a MIME type as langId.
     *
     * @param langId an id for a language, e.g. "sl" or "python", or a MIME type
     * @param truffleFile of the concerning file
     * @return a language info
     */
    private LanguageInfo findLanguageInfo(final String langId, final TruffleFile truffleFile) {
        Map languages = envInternal.getLanguages();
        LanguageInfo langInfo = languages.get(langId);
        if (langInfo != null) {
            return langInfo;
        }

        String possibleMimeType = langId;
        String actualLangId = Source.findLanguage(possibleMimeType);
        if (actualLangId == null) {
            try {
                actualLangId = Source.findLanguage(truffleFile);
            } catch (IOException e) {
            }

            if (actualLangId == null) {
                actualLangId = langId;
            }
        }

        langInfo = languages.get(actualLangId);
        if (langInfo == null) {
            throw new UnknownLanguageException("Unknown language: " + actualLangId + ". Known languages are: " + languages.keySet());
        }

        return langInfo;
    }

    public Future processChangesAndParse(List list, URI uri) {
        return contextAwareExecutor.executeWithDefaultContext(() -> processChangesAndParseWithContextEntered(list, uri));
    }

    protected TextDocumentSurrogate processChangesAndParseWithContextEntered(List list, URI uri) throws DiagnosticsNotification {
        TextDocumentSurrogate surrogate = surrogateMap.get(uri);

        if (surrogate == null) {
            throw new IllegalStateException("No internal mapping for uri=" + uri.toString() + " found.");
        }

        if (list.isEmpty()) {
            return surrogate;
        }

        surrogate.getChangeEventsSinceLastSuccessfulParsing().addAll(list);
        surrogate.setLastChange(list.get(list.size() - 1));
        surrogate.setEditorText(SourceUtils.applyTextDocumentChanges(list, surrogate.getSource(), surrogate, logger));

        sourceCodeEvaluator.parse(surrogate);

        return surrogate;
    }

    String getLanguageId(URI uri) {
        TextDocumentSurrogate doc = surrogateMap.get(uri);
        if (doc != null) {
            return doc.getLanguageId();
        }

        Future future = contextAwareExecutor.executeWithDefaultContext(() -> {
            try {
                return Source.findLanguage(envInternal.getTruffleFile(null, uri));
            } catch (IOException ex) {
                return null;
            }
        });

        try {
            return future.get();
        } catch (InterruptedException | ExecutionException e) {
            throw new RuntimeException(e);
        }
    }

    public void setServerCapabilities(String languageId, ServerCapabilities capabilities) {
        CompletionOptions completionProvider = capabilities.getCompletionProvider();
        if (completionProvider != null) {
            List triggerCharacters = completionProvider.getTriggerCharacters();
            if (triggerCharacters != null) {
                completionTriggerCharacters.add(languageId, triggerCharacters);
            }
        }
        SignatureHelpOptions signatureHelpProvider = capabilities.getSignatureHelpProvider();
        if (signatureHelpProvider != null) {
            List triggerCharacters = signatureHelpProvider.getTriggerCharacters();
            if (triggerCharacters != null) {
                signatureTriggerCharacters.add(languageId, triggerCharacters);
            }
        }
    }

    /**
     * Provides completions for a specific position in the document. If line or column are out of
     * range, items of global scope (top scope) are provided.
     *
     * @param uri
     * @param line 0-based line number
     * @param column 0-based column number (character offset)
     * @param completionContext has kind and completion character if client supports it
     * @return a {@link Future} of {@link CompletionList} containing all completions for the cursor
     *         position
     */
    public Future completion(final URI uri, int line, int column, CompletionContext completionContext) {
        return contextAwareExecutor.executeWithDefaultContext(() -> completionHandler.completionWithEnteredContext(uri, line, column, completionContext));
    }

    public Future hover(URI uri, int line, int column) {
        return contextAwareExecutor.executeWithDefaultContext(() -> hoverHandler.hoverWithEnteredContext(uri, line, column));
    }

    public Future signatureHelp(URI uri, int line, int character) {
        return contextAwareExecutor.executeWithNestedContext(() -> signatureHelpHandler.signatureHelpWithEnteredContext(uri, line, character), true);
    }

    public Future runCoverageAnalysis(final URI uri) {
        Future future = contextAwareExecutor.executeWithDefaultContext(() -> {
            contextAwareExecutor.resetContextCache(); // We choose coverage runs as checkpoints to
                                                      // clear the cached context. A coverage run
                                                      // can be triggered by the user via the
                                                      // editor, so that the user can actively
                                                      // control the reset of the current cached
                                                      // context.
            Future futureCoverage = contextAwareExecutor.executeWithNestedContext(() -> coverageHandler.runCoverageAnalysisWithEnteredContext(uri), true);
            try {
                return futureCoverage.get();
            } catch (ExecutionException e) {
                if (e.getCause() instanceof Exception) {
                    throw (Exception) e.getCause();
                } else {
                    throw e;
                }
            }
        });
        return future;
    }

    public Future getCoverage(URI uri) {
        return contextAwareExecutor.executeWithDefaultContext(() -> {
            return coverageHandler.getCoverageWithEnteredContext(uri);
        });
    }

    public Future> documentHighlight(URI uri, int line, int character) {
        return contextAwareExecutor.executeWithDefaultContext(() -> highlightHandler.highlightWithEnteredContext(uri, line, character));
    }

    public boolean hasCoverageData(URI uri) {
        TextDocumentSurrogate surrogate = surrogateMap.get(uri);
        return surrogate != null ? surrogate.hasCoverageData() : false;
    }

    @Override
    public String getSourceText(Path path) {
        if (surrogateMap == null) {
            return null;
        }

        TextDocumentSurrogate surrogate = surrogateMap.get(path.toUri());
        return surrogate != null ? surrogate.getEditorText() : null;
    }

    public Source getSource(URI uri) {
        if (surrogateMap == null) {
            return null;
        }

        TextDocumentSurrogate surrogate = surrogateMap.get(uri);
        return surrogate != null ? surrogate.getSource() : null;
    }

    @Override
    public boolean isVirtualFile(Path path) {
        return surrogateMap.containsSurrogate(path.toUri());
    }

    public Function surrogateGetter(LanguageInfo languageInfo) {
        return (sourceUri) -> {
            return surrogateMap.getOrCreateSurrogate(sourceUri, () -> languageInfo);
        };
    }

    public void initializeLSPServer(LSPServerAccessor server) {
        this.lspServer = server;
    }

    private List getExternalCommands() {
        if (extensionCommands == null) {
            extensionCommands = new ArrayList<>();
            for (InstrumentInfo instrument : envMain.getInstruments().values()) {
                if ("lsp".equals(instrument.getId())) {
                    continue;
                }
                LSPExtension extension = envMain.lookup(instrument, LSPExtension.class);
                if (extension != null) {
                    for (LSPCommand command : extension.getCommands()) {
                        extensionCommands.add(command);
                    }
                }
            }
        }
        return extensionCommands;
    }

    public Collection getExtensionCommandNames() {
        ArrayList result = new ArrayList<>();
        for (LSPCommand command : getExternalCommands()) {
            result.add(command.getName());
        }
        return result;
    }

    public Future createExtensionCommand(ExecuteCommandParams params) {
        String commandName = params.getCommand();
        for (LSPCommand command : getExternalCommands()) {
            if (commandName.equals(command.getName())) {
                List args = params.getArguments();
                return contextAwareExecutor.executeWithNestedContext(() -> command.execute(lspServer, envInternal, args), command.getTimeoutMillis(), () -> command.onTimeout(args));
            }
        }
        return null;
    }
}