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

io.deephaven.lang.completion.CompletionRequest Maven / Gradle / Ivy

There is a newer version: 0.37.0
Show newest version
//
// Copyright (c) 2016-2024 Deephaven Data Labs and Patent Pending
//
package io.deephaven.lang.completion;

import io.deephaven.base.verify.Require;
import io.deephaven.engine.context.QueryScope;
import io.deephaven.engine.table.ColumnDefinition;
import io.deephaven.engine.table.Table;
import io.deephaven.engine.table.TableDefinition;
import io.deephaven.internal.log.LoggerFactory;
import io.deephaven.io.logger.Logger;
import io.deephaven.lang.generated.ChunkerAssign;
import io.deephaven.lang.generated.ChunkerInvoke;
import io.deephaven.lang.generated.ChunkerString;
import io.deephaven.lang.generated.Node;
import io.deephaven.lang.parse.ParsedDocument;
import org.jetbrains.annotations.NotNull;

import java.util.*;

/**
 * A stateful object to represent a document search at a given position.
 *
 * When we search left or right from an intermediate node like whitespace, dot, comma or EOF, we will create a new
 * CompletionRequest at the new index.
 *
 * This currently uses absolute cursor positions, but we want this to be a line/column position instead, so we can
 * completely remove consideration of absolute cursors.
 *
 * This mistake was made by trying to base V2 on V1 semantics which are not really relevant when considering Monaco, LSP
 * and Javacc which all use line/column semantics.
 *
 * Absolute cursor positions are unfortunately deeply entwined in {@link ChunkerCompleter}, so we are leaving it in
 * place for now.
 *
 * Note that this class also maintains a map of loaded table definitions, so that repeated completions will not pay to
 * load the same table definition more than once.
 */
public class CompletionRequest {

    private static final Logger LOGGER = LoggerFactory.getLogger(CompletionRequest.class);

    private final String source;
    private final int offset;
    private final ChunkerCompleter completer;
    private int candidate;
    private final Map localDefs;

    public CompletionRequest(@NotNull ChunkerCompleter completer, String command, int offset) {
        this(completer, command, offset, new LinkedHashMap<>());
    }

    private CompletionRequest(@NotNull ChunkerCompleter completer, String command, int offset,
            Map localDefs) {
        this.source = command;
        this.offset = this.candidate = offset;
        this.completer = completer;
        this.localDefs = localDefs;
        Require.geqZero(offset, "offset");
        Require.leq(offset, "offset", command.length(), "command.length()");
    }

    public String getSource() {
        return source;
    }

    public int getOffset() {
        return offset;
    }

    public String getBeforeCursor() {
        return source.substring(0, offset);
    }

    public int getCandidate() {
        return candidate;
    }

    public CompletionRequest candidate(int index) {
        final CompletionRequest req = new CompletionRequest(completer, source, offset, localDefs);
        req.candidate = index;
        return req;
    }

    public TableDefinition getTableDefinition(final ChunkerCompleter completer, final ParsedDocument doc,
            QueryScope variables, String name) {
        // Each request maintains a local cache of looked-up table definitions, to avoid going to the VariableHandler
        // unless needed
        // Note that we do NOT go to the completer.getReferencedTables map at all;
        // we don't want to cache anything local-to-script-session any further
        // than a single completion invocation (i.e. local to a CompletionRequest)
        if (localDefs.containsKey(name)) {
            // if there wasn't a table definition in the script session once, it won't magically appear again later.
            // This might seem a little excessive, but in python at least, it is non-"free" to check if binding a
            // variable exists.
            return localDefs.get(name);
        }
        Object value = variables.readParamValue(name, null);
        TableDefinition result = null;
        if (value instanceof Table) {
            result = ((Table) value).getDefinition();
        } else {
            // If the result was null, we can try to search for an assign statement that is initialized w/ something we
            // _can_ grok.
            final List assignment = completer.findAssignment(doc, this, name);
            if (!assignment.isEmpty()) {
                // ok! there was an assignment to our table variable that occurred before user's cursor (this is,
                // of course, bad when user creates some random functions that are defined in any order).
                final ListIterator itr = assignment.listIterator(assignment.size());
                while (itr.hasPrevious()) {
                    final ChunkerAssign check = itr.previous();
                    result = findTableDefFromAssign(check);
                    if (result != null) {
                        break;
                    }
                }
            }
        }
        localDefs.put(name, result);
        return result;
    }

    private TableDefinition findTableDefFromAssign(final ChunkerAssign check) {
        final Node value = check.getValue();
        if (value instanceof ChunkerInvoke) {
            ChunkerInvoke invoke = (ChunkerInvoke) value;
            if ("newTable".equals(invoke.getName())) {
                // TODO: consider checking the scope of newTable... for now, too messy for ~0 value.
                return convertNewTableInvocation(invoke);
            }
        }
        return null;
    }

    private TableDefinition convertNewTableInvocation(final ChunkerInvoke invoke) {
        final List> columns = new ArrayList<>();
        for (Node argument : invoke.getArguments()) {
            if (argument instanceof ChunkerInvoke) {
                final ChunkerInvoke colInvoke = ((ChunkerInvoke) argument);
                String colMethod = colInvoke.getName();
                final List colArgs = colInvoke.getArguments();
                if (colArgs.isEmpty()) {
                    // this is normal, if user has typed `newTable(stringCol(`
                    // in this case, there is no valid table definition, so we'll just skip this column.
                    // In the future, we may want to consider user-defined zero-arg functions that return
                    // ColumnDefinitions
                    continue;
                }
                final String colName = toStringLiteral(colArgs.get(0));
                if (colName == null) {
                    if (LOGGER.isTraceEnabled()) {
                        LOGGER.trace()
                                .append("Unable to trace first argument back to a string literal: ")
                                .append(colInvoke.toSource())
                                .endl();
                    }
                    continue;
                }

                switch (colMethod) {
                    case "stringCol":
                        columns.add(ColumnDefinition.ofString(colName));
                        break;
                    case "instantCol":
                        columns.add(ColumnDefinition.ofTime(colName));
                        break;
                    case "longCol":
                        columns.add(ColumnDefinition.ofLong(colName));
                        break;
                    case "intCol":
                        columns.add(ColumnDefinition.ofInt(colName));
                        break;
                    case "shortCol":
                        columns.add(ColumnDefinition.ofShort(colName));
                        break;
                    case "byteCol":
                        columns.add(ColumnDefinition.ofByte(colName));
                        break;
                    case "charCol":
                        columns.add(ColumnDefinition.ofChar(colName));
                        break;
                    case "doubleCol":
                        columns.add(ColumnDefinition.ofDouble(colName));
                        break;
                    case "floatCol":
                        columns.add(ColumnDefinition.ofFloat(colName));
                        break;
                    case "col":
                        // We _could_ technically try to guess from the col() varargs what the type is, but, not worth
                        // it atm.
                        columns.add(ColumnDefinition.fromGenericType(colName, Object.class));
                        break;
                    default:
                        LOGGER.warn()
                                .append("Unhandled newTable() argument ")
                                .append(argument.toSource())
                                .append(" not a recognized invocation")
                                .endl();
                        break;
                }
            } else {
                // TODO: handle ColumnDefition/etc variables
                LOGGER.warn()
                        .append("Unhandled newTable() argument ")
                        .append(argument.toSource())
                        .append(" of type ")
                        .append(argument.getClass().getName())
                        .endl();
            }
        }
        return TableDefinition.of(columns);
    }

    private String toStringLiteral(final Node node) {
        if (node instanceof ChunkerString) {
            return ((ChunkerString) node).getRaw();
        } // TODO: if it's a variable name, try to trace it back to a static assignment of a string, or a binding
          // variable.
        return null;
    }

    @Override
    public String toString() {
        return "CompletionRequest{" +
                "source='" + source + '\'' +
                ", offset=" + offset +
                ", candidate=" + candidate +
                '}';
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy