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

org.neo4j.shell.terminal.JlineCompleter Maven / Gradle / Ivy

The newest version!
/*
 * Copyright (c) "Neo4j"
 * Neo4j Sweden AB [https://neo4j.com]
 *
 * This file is part of Neo4j.
 *
 * Neo4j is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program 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 for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see .
 */
package org.neo4j.shell.terminal;

import java.io.IOException;
import java.util.List;
import java.util.stream.Stream;
import org.jline.reader.Candidate;
import org.jline.reader.Completer;
import org.jline.reader.LineReader;
import org.jline.reader.ParsedLine;
import org.neo4j.shell.commands.CommandHelper.CommandFactoryHelper;
import org.neo4j.shell.completions.CompletionEngine;
import org.neo4j.shell.completions.Suggestion;
import org.neo4j.shell.completions.SuggestionType;
import org.neo4j.shell.terminal.StatementJlineParser.BlankCompletion;
import org.neo4j.shell.terminal.StatementJlineParser.CommandCompletion;
import org.neo4j.shell.terminal.StatementJlineParser.CypherCompletion;

/**
 * Provides autocompletion for cypher shell statements.
 */
public class JlineCompleter implements Completer {
    private final CommandCompleter commandCompleter;
    private final CypherCompleter cypherCompleter;

    public JlineCompleter(CommandFactoryHelper commands, CompletionEngine completionEngine) {
        this.commandCompleter = CommandCompleter.from(commands);
        this.cypherCompleter = new CypherCompleter(completionEngine);
    }

    @Override
    public void complete(LineReader reader, ParsedLine line, List candidates) {
        // Note, the JavaCC parser doesn't provide good enough completion for
        // us to release it yet. For this reason, cypher completion is
        // disabled by default for now until we decide exactly where we want to
        // go with this.
        try {
            if (line instanceof BlankCompletion) {
                candidates.addAll(commandCompleter.complete());
                if (cypherCompleter.completionsEnabled()) {
                    cypherCompleter.completeBlank().forEach(candidates::add);
                }
            } else if (line instanceof CommandCompletion) {
                candidates.addAll(commandCompleter.complete());
            } else if (cypherCompleter.completionsEnabled() && line instanceof CypherCompletion cypher) {
                cypherCompleter.complete(cypher).forEach(candidates::add);
            }
        } catch (Exception e) {
            // Ignore
        }
    }

    private record CommandCompleter(List allCommands) {
        List complete() {
            return allCommands;
        }

        public static CommandCompleter from(CommandFactoryHelper commands) {
            return new CommandCompleter(
                    commands.metadata().map(Suggestion::command).toList());
        }
    }

    private record CypherCompleter(CompletionEngine completionEngine) {
        public boolean completionsEnabled() {
            return completionEngine.completionsEnabled();
        }

        Stream complete(CypherCompletion cypher) throws IOException {
            return concat(completions(queryUntilCompletionWord(cypher), cypher.word()));
        }

        Stream completeBlank() throws IOException {
            return concat(completions("", ""));
        }

        private String textUntilSentinel(String lastToken, char sentinel) {
            var sentinelIndex = lastToken.lastIndexOf(sentinel);
            return lastToken.substring(0, sentinelIndex + 1);
        }

        /*
         * Returns cypher keyword suggestions, for example `MATCH`.
         */
        private Stream completions(String query, String lastToken) throws IOException {
            // Adjust some completions because jline is unable to properly
            // distinguish when a word that contains . or : starts
            var completions = completionEngine.completeQuery(query).stream().map(completion -> {
                var completionType = completion.group();
                if (completionType.equals(SuggestionType.LABEL_OR_RELATIONSHIP.name)) {
                    return new Suggestion(
                            textUntilSentinel(lastToken, ':') + completion.value(),
                            completion.value(),
                            SuggestionType.LABEL_OR_RELATIONSHIP,
                            completion.descr(),
                            completion.complete());
                } else if (completionType.equals(SuggestionType.PROPERTY.name)) {
                    return new Suggestion(
                            textUntilSentinel(lastToken, '.') + completion.value(),
                            completion.value(),
                            SuggestionType.PROPERTY,
                            completion.descr(),
                            completion.complete());
                } else if (completionType.equals(SuggestionType.FUNCTION.name)) {
                    return new Suggestion(
                            textUntilSentinel(lastToken, '.') + completion.value(),
                            completion.value(),
                            SuggestionType.FUNCTION,
                            completion.descr(),
                            completion.complete());
                } else if (completionType.equals(SuggestionType.PROCEDURE.name)) {
                    return new Suggestion(
                            textUntilSentinel(lastToken, '.') + completion.value(),
                            completion.value(),
                            SuggestionType.PROCEDURE,
                            completion.descr(),
                            completion.complete());
                } else {
                    return completion;
                }
            });

            return completions;
        }

        private String queryUntilCompletionWord(CypherCompletion cypher) {
            int cutAt =
                    cypher.cursor() - cypher.wordCursor() - cypher.statement().beginOffset();
            return cypher.statement().statement().substring(0, cutAt);
        }

        @SafeVarargs
        private static  Stream concat(Stream... streams) {
            return Stream.of(streams).reduce(Stream::concat).orElseGet(Stream::empty);
        }
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy