Home Reference Source Test

src/SqlAutocomplete.js

// @flow
import * as React from 'react';
import type {Node} from 'react';

type AutocompleteItem = {
  text: string,
  type: 'table' | 'column',
  table?: string,
};

type Props = {
  items: Array<AutocompleteItem>,
  selectedIndex: number,
  position: {top: number, left: number},
  onSelect: (item: AutocompleteItem) => void,
  onClose: () => void,
};

/**
 * Dropdown component for SQL autocomplete suggestions
 */
class SqlAutocompleteDropdown extends React.Component<Props> {
  render(): Node {
    const {items, selectedIndex, position, onSelect} = this.props;

    if (items.length === 0) {
      return null;
    }

    return (
      <div
        className="sql-autocomplete-dropdown"
        style={{
          position: 'fixed', // Changed from 'absolute' to 'fixed' to position relative to viewport
          top: position.top,
          left: position.left,
          backgroundColor: 'white',
          border: '1px solid #ccc',
          borderRadius: '4px',
          boxShadow: '0 2px 8px rgba(0,0,0,0.1)',
          maxHeight: '200px',
          overflowY: 'auto',
          zIndex: 1000,
          minWidth: '150px',
        }}
      >
        {items.map((item, index) => (
          <div
            key={`${item.type}-${item.text}`}
            className={`sql-autocomplete-item ${
              index === selectedIndex ? 'selected' : ''
            }`}
            style={{
              padding: '8px 12px',
              cursor: 'pointer',
              backgroundColor: index === selectedIndex ? '#e6f3ff' : 'white',
              borderBottom:
                index < items.length - 1 ? '1px solid #eee' : 'none',
            }}
            onClick={() => onSelect(item)}
          >
            <span
              style={{
                fontWeight: item.type === 'table' ? 'bold' : 'normal',
                color: item.type === 'table' ? '#0066cc' : '#333',
              }}
            >
              {item.text}
            </span>
            {item.type === 'column' && item.table && (
              <span
                style={{
                  marginLeft: '8px',
                  fontSize: '12px',
                  color: '#666',
                }}
              >
                ({item.table})
              </span>
            )}
          </div>
        ))}
      </div>
    );
  }
}

/**
 * Parse SQL text to detect table/column references being typed
 */
export function parseForAutocomplete(
  text: string,
  cursorPosition: number
): {
  prefix: string,
  context: 'table' | 'column' | null,
  needsTablePrefix?: boolean,
} {
  // Get text up to cursor position
  const beforeCursor = text.slice(0, cursorPosition);

  // Find the current word being typed (ignoring trailing spaces)
  const wordMatch = beforeCursor.trimEnd().match(/[\w.]*$/);
  const currentWord = wordMatch ? wordMatch[0] : '';

  // If cursor is after spaces and there's no current word, look for the last word
  let actualWord = currentWord;
  if (!currentWord && beforeCursor.endsWith(' ')) {
    // Look for the word before the spaces
    const trimmed = beforeCursor.trimEnd();
    const lastWordMatch = trimmed.match(/[\w.]*$/);
    actualWord = lastWordMatch ? lastWordMatch[0] : '';
  }

  // Check if we're in a context where table/column suggestions make sense
  const upperText = beforeCursor.toUpperCase();

  // Common SQL keywords that typically precede table names
  const tableKeywords = ['FROM', 'JOIN', 'INTO', 'UPDATE'];
  // Common SQL keywords that typically precede column names
  const columnKeywords = [
    'SELECT',
    'WHERE',
    'GROUP BY',
    'ORDER BY',
    'HAVING',
    'ON',
  ];

  // Check if we're currently typing a keyword (don't autocomplete keywords)
  const allKeywords = [...tableKeywords, ...columnKeywords];
  for (const keyword of allKeywords) {
    if (keyword.startsWith(actualWord.toUpperCase()) && actualWord.length > 0) {
      return {prefix: '', context: null};
    }
  }

  // Check for table context - must be right after a table keyword
  for (const keyword of tableKeywords) {
    const keywordIndex = upperText.lastIndexOf(keyword);
    if (keywordIndex >= 0) {
      const afterKeyword = beforeCursor
        .slice(keywordIndex + keyword.length)
        .trim();
      // Check if the cursor is within the first word after the keyword
      const wordsAfter = afterKeyword.split(/\s+/).filter((w) => w.length > 0);
      if (wordsAfter.length <= 1) {
        // We're in table context - return the actual word being typed
        return {prefix: actualWord, context: 'table'};
      }
    }
  }

  // Check for column context - must be after column keyword but not after table keyword
  for (const keyword of columnKeywords) {
    const keywordIndex = upperText.lastIndexOf(keyword);
    if (keywordIndex >= 0) {
      const afterKeyword = beforeCursor.slice(keywordIndex + keyword.length);

      // Check if there's a table keyword after this column keyword
      let hasTableKeywordAfter = false;
      for (const tk of tableKeywords) {
        const tableKeywordIndex = afterKeyword.toUpperCase().lastIndexOf(tk);
        if (tableKeywordIndex >= 0) {
          // Check if we're past the table keyword and its table name
          const afterTableKeyword = afterKeyword
            .slice(tableKeywordIndex + tk.length)
            .trim();
          const tableWords = afterTableKeyword
            .split(/\s+/)
            .filter((w) => w.length > 0);
          if (tableWords.length === 0) {
            // We're still naming the table
            hasTableKeywordAfter = true;
          } else if (
            tableWords.length === 1 &&
            afterTableKeyword.startsWith(actualWord)
          ) {
            // We're still typing the table name
            hasTableKeywordAfter = true;
          }
          break;
        }
      }

      if (!hasTableKeywordAfter) {
        // Check if it's a qualified column name (table.column)
        if (actualWord.includes('.')) {
          const parts = actualWord.split('.');
          return {
            prefix: parts[1] || '',
            context: 'column',
            needsTablePrefix: true,
          };
        }
        return {prefix: actualWord, context: 'column'};
      }
    }
  }

  return {prefix: '', context: null};
}

/**
 * Generate autocomplete suggestions based on available tables and columns
 */
export function generateSuggestions(
  prefix: string,
  context: 'table' | 'column' | null,
  types: {[string]: Array<string>},
  needsTablePrefix?: boolean
): Array<AutocompleteItem> {
  if (!context || !prefix) {
    return [];
  }

  const lowerPrefix = prefix.toLowerCase();
  const suggestions: Array<AutocompleteItem> = [];

  if (context === 'table') {
    // Suggest table names
    Object.keys(types).forEach((tableName) => {
      if (tableName.toLowerCase().startsWith(lowerPrefix)) {
        suggestions.push({
          text: tableName,
          type: 'table',
        });
      }
    });
  } else if (context === 'column') {
    if (needsTablePrefix) {
      // For qualified column names (table.column), suggest columns from specific table
      // Extract table name from current word before the dot
      const currentWord = prefix;
      const dotIndex = currentWord.lastIndexOf('.');
      if (dotIndex > 0) {
        const tableName = currentWord.slice(0, dotIndex);
        const columnPrefix = currentWord.slice(dotIndex + 1).toLowerCase();

        if (types[tableName]) {
          types[tableName].forEach((column) => {
            if (column.toLowerCase().startsWith(columnPrefix)) {
              suggestions.push({
                text: `${tableName}.${column}`,
                type: 'column',
                table: tableName,
              });
            }
          });
        }
      }
    } else {
      // Suggest all columns from all tables
      Object.entries(types).forEach(([tableName, columns]) => {
        columns.forEach((column) => {
          if (column.toLowerCase().startsWith(lowerPrefix)) {
            suggestions.push({
              text: column,
              type: 'column',
              table: tableName,
            });
          }
        });
      });

      // Also suggest qualified column names
      Object.entries(types).forEach(([tableName, columns]) => {
        if (tableName.toLowerCase().startsWith(lowerPrefix)) {
          columns.forEach((column) => {
            suggestions.push({
              text: `${tableName}.${column}`,
              type: 'column',
              table: tableName,
            });
          });
        }
      });
    }
  }

  return suggestions.slice(0, 10); // Limit to 10 suggestions
}

export default SqlAutocompleteDropdown;