ContextualQueryHelp getQueryHelpContext(final String partialQuery) {
        if (NullSafe.isBlankString(partialQuery)) {
            return EMPTY_QUERY_CONTEXT;
        } else {
            final Set<QueryHelpType> types = new HashSet<>();
            final Set<String> applicableStructureItems;
            final List<Token> tokens = Tokeniser.parse(partialQuery);
            TokenType lastKeyword = null;
            // The list of token types seen since (and including) lastKeyword
            final List<TokenType> lastKeywordSequence = new ArrayList<>();
            final Set<TokenType> keywordsSeen = new HashSet<>();

            for (final Token token : tokens) {
                final int endIdx = token.getEnd();
                if (endIdx >= partialQuery.length()) {
                    // Past the cursor position
                    break;
                }
                final TokenType tokenType = token.getTokenType();
                if (TokenType.isKeyword(tokenType)) {
                    keywordsSeen.add(tokenType);
                    lastKeyword = tokenType;
                    lastKeywordSequence.clear();
                }
                lastKeywordSequence.add(tokenType);

                LOGGER.debug("""

                                token: {}
                                endIdx: {}
                                lastKeyword: {}
                                keywordsSeen: {}
                                lastKeywordSequence: {}""",
                        token, token.getEnd(), lastKeyword, keywordsSeen, lastKeywordSequence);
            }

            // Determine what keywords can come after the last one seen
            final Set<TokenType> keywordsValidAfter = getKeywordsValidAfter(lastKeyword, lastKeywordSequence);

            if (seenInDictionary(tokens)) {
                // If we see 'in dictionary ' then the only thing we want after that is a dictionary
                types.add(QueryHelpType.DICTIONARY);
                applicableStructureItems = Collections.emptySet();
            } else {
                if (includeDataSources(lastKeywordSequence)) {
                    types.add(QueryHelpType.DATA_SOURCE);
                }

                applicableStructureItems = keywordsValidAfter.stream()
                        .filter(tokenType -> TokenType.BY != tokenType)
                        .map(tokenType -> {
                            if (TokenType.GROUP == tokenType) {
                                return Structures.name(TokenType.GROUP, TokenType.BY);
                            } else if (TokenType.SORT == tokenType) {
                                return Structures.name(TokenType.SORT, TokenType.BY);
                            } else if (TokenType.IN == tokenType) {
                                return Structures.name(TokenType.IN, TokenType.DICTIONARY);
                            } else {
                                return Structures.name(tokenType);
                            }
                        })
                        .collect(Collectors.toSet());
                final boolean includeStructure = !keywordsValidAfter.isEmpty();
                if (lastKeyword != null
                    && keywordsSeen.contains(TokenType.FROM)
                    && tokens.size() >= 4) {
                    types.addAll(getHelpTypes(lastKeyword, lastKeywordSequence, includeStructure));
                }
            }

            LOGGER.debug("returning type: {}, applicableStructureItems: {}", types, applicableStructureItems);
            return new ContextualQueryHelp(types, applicableStructureItems);
        }
    }
Here to help testing, so we don't have to work out the row/col postitions
stroom-query/stroom-query-impl/src/main/java/stroom/query/impl/QueryServiceImpl.java