rawsql-ts
Version:
[beta]High-performance SQL parser and AST analyzer written in TypeScript. Provides fast parsing and advanced transformation capabilities.
338 lines • 14.6 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.CursorContextAnalyzer = void 0;
const LexemeCursor_1 = require("./LexemeCursor");
const TextPositionUtils_1 = require("./TextPositionUtils");
const KeywordCache_1 = require("./KeywordCache");
class CursorContextAnalyzer {
/**
* Process existing dictionaries into IntelliSense-friendly patterns
* Single source of truth: existing CommandTokenReader dictionaries
*/
static getKeywordPatterns() {
if (this.patternCache !== null) {
return this.patternCache;
}
const requiresKeywords = new Map();
const suggestsTables = new Set();
const suggestsColumns = new Set();
// Extract all patterns by systematically testing the dictionaries
this.extractKeywordPatterns(requiresKeywords, suggestsTables, suggestsColumns);
// Cache the processed patterns
this.patternCache = {
requiresKeywords,
suggestsTables,
suggestsColumns
};
return this.patternCache;
}
/**
* Extract all keyword patterns from the existing dictionaries
*/
static extractKeywordPatterns(requiresKeywords, suggestsTables, suggestsColumns) {
// Define SQL contexts and their expected behavior
const tableContexts = ['from', 'join']; // Keywords that introduce table names
const columnContexts = ['select', 'where', 'on', 'having', 'by']; // Keywords that introduce columns
// Check table context keywords
for (const keyword of tableContexts) {
if (this.isKeywordInDictionary(keyword)) {
suggestsTables.add(keyword);
}
}
// Check column context keywords
for (const keyword of columnContexts) {
if (this.isKeywordInDictionary(keyword)) {
suggestsColumns.add(keyword);
}
}
// Extract all keyword patterns that require followups
this.extractRequiresKeywordPatterns(requiresKeywords);
}
/**
* Check if a keyword exists in the command dictionaries using existing parsers
*/
static isKeywordInDictionary(keyword) {
// Use KeywordCache for JOIN keywords
if (KeywordCache_1.KeywordCache.isValidJoinKeyword(keyword)) {
return true;
}
// Check if keyword exists in command dictionary
// Since we can't directly query the trie, use known keyword list
const knownKeywords = ['from', 'join', 'select', 'where', 'on', 'having', 'by', 'group', 'order'];
return knownKeywords.includes(keyword);
}
/**
* Extract all keywords that require specific followup keywords
*/
static extractRequiresKeywordPatterns(requiresKeywords) {
// Test all potential first words that might require followup keywords
const potentialFirstWords = [
// JOIN modifiers
'inner', 'left', 'right', 'full', 'cross', 'natural', 'outer',
// Other composite keywords
'group', 'order'
];
for (const word of potentialFirstWords) {
const possibleFollowups = this.findPossibleFollowups(word);
if (possibleFollowups.length > 0) {
requiresKeywords.set(word, possibleFollowups);
}
}
}
/**
* Find all possible followup keywords for a given word using KeywordCache
*/
static findPossibleFollowups(word) {
const followups = new Set();
// Use KeywordCache for JOIN-related suggestions
const joinSuggestions = KeywordCache_1.KeywordCache.getJoinSuggestions(word.toLowerCase());
joinSuggestions.forEach(s => followups.add(s.toUpperCase()));
// Use KeywordCache for command-related suggestions
const commandSuggestions = KeywordCache_1.KeywordCache.getCommandSuggestions(word.toLowerCase());
commandSuggestions.forEach(s => followups.add(s.toUpperCase()));
return Array.from(followups);
}
/**
* Helper function to check if a token requires specific keywords
*/
static requiresSpecificKeywords(tokenValue) {
const patterns = this.getKeywordPatterns();
const requiredKeywords = patterns.requiresKeywords.get(tokenValue);
if (requiredKeywords) {
return {
suggestKeywords: true,
requiredKeywords
};
}
return null;
}
/**
* Analyze cursor position for IntelliSense suggestions
*
* Direct implementation that determines what suggestions can be provided
* without legacy context conversion overhead.
*
* @param sql - SQL text to analyze
* @param cursorPosition - Character position (0-based)
* @returns IntelliSense context focused on what suggestions can be provided
*/
static analyzeIntelliSense(sql, cursorPosition) {
try {
// Get all lexemes with position information
const allLexemes = LexemeCursor_1.LexemeCursor.getAllLexemesWithPosition(sql);
// Find token at cursor position
let actualTokenIndex = -1;
let actualCurrentToken;
// Find the token that contains or precedes the cursor
for (let i = 0; i < allLexemes.length; i++) {
const lexeme = allLexemes[i];
if (!lexeme.position)
continue;
if (cursorPosition >= lexeme.position.startPosition &&
cursorPosition <= lexeme.position.endPosition) {
// Cursor is within this token
actualCurrentToken = lexeme;
actualTokenIndex = i;
break;
}
else if (lexeme.position.startPosition > cursorPosition) {
// Cursor is before this token (in whitespace)
actualTokenIndex = Math.max(0, i - 1);
actualCurrentToken = actualTokenIndex >= 0 ? allLexemes[actualTokenIndex] : undefined;
break;
}
}
// If not found, cursor is after all tokens
if (actualTokenIndex === -1 && allLexemes.length > 0) {
actualTokenIndex = allLexemes.length - 1;
actualCurrentToken = allLexemes[actualTokenIndex];
}
const previousToken = actualTokenIndex > 0 ? allLexemes[actualTokenIndex - 1] : undefined;
// Check for dot completion (highest priority)
const isAfterDot = this.isAfterDot(sql, cursorPosition, previousToken);
if (isAfterDot) {
const precedingIdentifier = this.findPrecedingIdentifier(sql, cursorPosition, allLexemes);
return {
suggestTables: false,
suggestColumns: true,
suggestKeywords: false,
tableScope: precedingIdentifier,
currentToken: actualCurrentToken,
previousToken: previousToken
};
}
// Check for keywords that require additional keywords
if (actualCurrentToken) {
const currentValue = actualCurrentToken.value.toLowerCase();
const keywordRequirement = this.requiresSpecificKeywords(currentValue);
if (keywordRequirement) {
return {
suggestTables: false,
suggestColumns: false,
...keywordRequirement,
currentToken: actualCurrentToken,
previousToken: previousToken
};
}
}
// Check tokens for context-based suggestions
const tokenValue = actualCurrentToken === null || actualCurrentToken === void 0 ? void 0 : actualCurrentToken.value.toLowerCase();
const prevValue = previousToken === null || previousToken === void 0 ? void 0 : previousToken.value.toLowerCase();
// Check current token first (when cursor is at end of token)
if (tokenValue) {
const patterns = this.getKeywordPatterns();
// Keywords that suggest tables after them
if (patterns.suggestsTables.has(tokenValue)) {
return {
suggestTables: true,
suggestColumns: false,
suggestKeywords: false,
currentToken: actualCurrentToken,
previousToken: previousToken
};
}
// Keywords that suggest columns after them
if (patterns.suggestsColumns.has(tokenValue)) {
return {
suggestTables: false,
suggestColumns: true,
suggestKeywords: false,
currentToken: actualCurrentToken,
previousToken: previousToken
};
}
}
// Check previous token (when cursor is in whitespace after token)
if (prevValue) {
const patterns = this.getKeywordPatterns();
// Check if previous token requires specific keywords (and next token doesn't already fulfill it)
const keywordRequirement = this.requiresSpecificKeywords(prevValue);
if (keywordRequirement && tokenValue !== 'join' && tokenValue !== 'outer' && tokenValue !== 'by') {
return {
suggestTables: false,
suggestColumns: false,
...keywordRequirement,
currentToken: actualCurrentToken,
previousToken: previousToken
};
}
// Keywords that suggest tables
if (patterns.suggestsTables.has(prevValue)) {
return {
suggestTables: true,
suggestColumns: false,
suggestKeywords: false,
currentToken: actualCurrentToken,
previousToken: previousToken
};
}
// Keywords that suggest columns
if (patterns.suggestsColumns.has(prevValue)) {
return {
suggestTables: false,
suggestColumns: true,
suggestKeywords: false,
currentToken: actualCurrentToken,
previousToken: previousToken
};
}
}
// Default fallback - suggest keywords
return {
suggestTables: false,
suggestColumns: false,
suggestKeywords: true,
currentToken: actualCurrentToken,
previousToken: previousToken
};
}
catch (error) {
// Return minimal context on error
return {
suggestTables: false,
suggestColumns: false,
suggestKeywords: false,
};
}
}
/**
* Analyze cursor position for IntelliSense at line/column position
*/
static analyzeIntelliSenseAt(sql, position) {
const charOffset = TextPositionUtils_1.TextPositionUtils.lineColumnToCharOffset(sql, position);
if (charOffset === -1) {
return {
suggestTables: false,
suggestColumns: false,
suggestKeywords: false,
};
}
return this.analyzeIntelliSense(sql, charOffset);
}
static isAfterDot(sql, cursorPosition, previousToken) {
// Check if character before cursor is a dot
if (cursorPosition > 0 && sql[cursorPosition - 1] === '.') {
return true;
}
// Check if previous token is a dot
if (previousToken && previousToken.value === '.') {
return true;
}
// Check for dot in nearby characters (handle whitespace)
let pos = cursorPosition - 1;
while (pos >= 0 && /\s/.test(sql[pos])) {
pos--; // Skip whitespace backwards
}
if (pos >= 0 && sql[pos] === '.') {
return true;
}
return false;
}
static findPrecedingIdentifier(sql, cursorPosition, lexemes) {
// If cursor is after a dot, look for identifier before the dot
if (this.isAfterDot(sql, cursorPosition)) {
// Find dot position in SQL text
let pos = cursorPosition - 1;
while (pos >= 0 && /\s/.test(sql[pos])) {
pos--; // Skip whitespace backwards
}
if (pos >= 0 && sql[pos] === '.') {
// Found the dot, now look for identifier before it
let identifierEnd = pos;
while (pos >= 0 && /\s/.test(sql[pos])) {
pos--; // Skip whitespace
}
// Extract identifier backwards
while (pos >= 0 && /[a-zA-Z0-9_]/.test(sql[pos])) {
pos--;
}
const identifierStart = pos + 1;
if (identifierStart < identifierEnd) {
return sql.substring(identifierStart, identifierEnd);
}
}
// Fallback: try to find dot token in lexemes and get identifier before it
for (let i = lexemes.length - 1; i >= 0; i--) {
if (lexemes[i].value === '.' &&
lexemes[i].position &&
lexemes[i].position.startPosition < cursorPosition) {
// Found a dot before cursor, get identifier before it
if (i > 0 && this.isIdentifier(lexemes[i - 1])) {
return lexemes[i - 1].value;
}
break;
}
}
}
return undefined;
}
static isIdentifier(lexeme) {
return /^[a-zA-Z_][a-zA-Z0-9_]*$/.test(lexeme.value);
}
}
exports.CursorContextAnalyzer = CursorContextAnalyzer;
/**
* Cache for processed keyword patterns
*/
CursorContextAnalyzer.patternCache = null;
//# sourceMappingURL=CursorContextAnalyzer.js.map