// Licensed to Cloudera, Inc. under one // or more contributor license agreements. See the NOTICE file // distributed with this work for additional information // regarding copyright ownership. Cloudera, Inc. licenses this file // to you under the Apache License, Version 2.0 (the // "License"); you may not use this file except in compliance // with the License. You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. import { ACTIVE_STATEMENT_CHANGED_EVENT, GET_ACTIVE_LOCATIONS_EVENT, REFRESH_STATEMENT_LOCATIONS_EVENT } from '../AceLocationHandler'; import { ActiveStatementChangedEventDetails } from '../types'; import Executor from '../../../../../apps/editor/execution/executor'; import { SqlAnalyzerProvider } from '../../../../../catalog/analyzer/types'; import SubscriptionTracker, { Disposable } from '../../../../../components/utils/SubscriptionTracker'; import { Ace } from '../../../../../ext/ace'; import { ParsedSqlStatement } from '../../../../../parse/sqlStatementsParser'; import { AutocompleteParser, AutocompleteParseResult } from '../../../../../parse/types'; import { EditorInterpreter } from '../../../../../config/types'; import AutocompleteResults from './AutocompleteResults'; import { SqlReferenceProvider } from '../../../../../sql/reference/types'; import huePubSub from '../../../../../utils/huePubSub'; interface SqlAutocompleterOptions { editorId: string; executor: Executor; temporaryOnly?: boolean; editor: Ace.Editor; fixedPrefix?: () => string; fixedPostfix?: () => string; autocompleteParser: AutocompleteParser; sqlReferenceProvider: SqlReferenceProvider; sqlAnalyzerProvider: SqlAnalyzerProvider; } export default class SqlAutocompleter implements Disposable { editor: Ace.Editor; executor: Executor; fixedPrefix: () => string; fixedPostfix: () => string; autocompleteParser: AutocompleteParser; autocompleteResults: AutocompleteResults; editorId: string; subTracker = new SubscriptionTracker(); activeStatement: ParsedSqlStatement | null = null; onPartial?: (partial: string) => void; constructor({ editorId, executor, temporaryOnly = false, editor, fixedPrefix, fixedPostfix, autocompleteParser, sqlReferenceProvider, sqlAnalyzerProvider }: SqlAutocompleterOptions) { this.editorId = editorId; this.editor = editor; this.executor = executor; this.fixedPrefix = fixedPrefix || (() => ''); this.fixedPostfix = fixedPostfix || (() => ''); this.autocompleteParser = autocompleteParser; this.autocompleteResults = new AutocompleteResults({ sqlReferenceProvider, sqlAnalyzerProvider, executor, editor, temporaryOnly }); this.subTracker.subscribe( ACTIVE_STATEMENT_CHANGED_EVENT, (event: ActiveStatementChangedEventDetails) => { if (event.id === this.editorId) { this.activeStatement = event.activeStatement; } } ); } getDialect(): string { return (this.executor.connector()).dialect; } async parseActiveStatement(): Promise { if (!this.activeStatement) { return; } const activeStatementLocation = this.activeStatement.location; const cursorPosition = this.editor.getCursorPosition(); if ( (activeStatementLocation.first_line - 1 < cursorPosition.row || (activeStatementLocation.first_line - 1 === cursorPosition.row && activeStatementLocation.first_column <= cursorPosition.column)) && (activeStatementLocation.last_line - 1 > cursorPosition.row || (activeStatementLocation.last_line - 1 === cursorPosition.row && activeStatementLocation.last_column >= cursorPosition.column)) ) { const beforeCursor = this.fixedPrefix() + this.editor.session.getTextRange({ start: { row: activeStatementLocation.first_line - 1, column: activeStatementLocation.first_column }, end: cursorPosition }); const afterCursor = this.editor.session.getTextRange({ start: cursorPosition, end: { row: activeStatementLocation.last_line - 1, column: activeStatementLocation.last_column } }) + this.fixedPostfix(); try { return this.autocompleteParser.parseSql(beforeCursor, afterCursor); } catch (err) { console.warn(err); } } } async parseAll(): Promise { try { return this.autocompleteParser.parseSql( this.editor.getTextBeforeCursor(), this.editor.getTextAfterCursor() ); } catch (err) { console.warn(err); } } async autocomplete(): Promise { let parseResult; try { huePubSub.publish( GET_ACTIVE_LOCATIONS_EVENT, (locations: ActiveStatementChangedEventDetails) => { // This could happen in case the user is editing at the borders of the statement and the locations haven't // been updated yet, in that case we have to force a location update before parsing if (locations.editorChangeTime !== this.editor.lastChangeTime) { huePubSub.publish(REFRESH_STATEMENT_LOCATIONS_EVENT, this.editorId); } } ); parseResult = await this.parseActiveStatement(); } catch (e) { if (typeof console.warn !== 'undefined') { console.warn(e); } } // In the unlikely case the statement parser fails we fall back to parsing all of it if (!parseResult) { try { parseResult = await this.parseAll(); } catch (e) { if (typeof console.warn !== 'undefined') { console.warn(e); } } } return parseResult; } dispose(): void { this.subTracker.dispose(); } }