UNPKG

gethue

Version:

Hue is an Open source SQL Query Editor for Databases/Warehouses

393 lines (344 loc) 12.5 kB
// 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 $ from 'jquery'; import * as ko from 'knockout'; import ace from '../../../ext/aceHelper'; import SolrFormulaAutocompleter from './solrFormulaAutocompleter'; import SolrQueryAutocompleter from './solrQueryAutocompleter'; import { findEditorConnector } from '../../../config/hueConfig'; import AceLocationHandler from '../../../ko/bindings/ace/aceLocationHandler'; import componentUtils from '../../../ko/components/componentUtils'; import { registerHueWorkers } from '../../../sql/workers/hueWorkerHandler'; import SqlAutocompleter from '../../../sql/sqlAutocompleter'; import AceGutterHandler from '../../../ko/bindings/ace/aceGutterHandler'; import UUID from '../../../utils/string/UUID'; import { hueLocalStorage } from '../../../utils/storageUtils'; export const NAME = 'hue-simple-ace-editor'; export const MULTI_NAME = 'hue-simple-ace-editor-multi'; const SIMPLE_ACE_TEMPLATE = ` <div class="simple-ace-single-line"> <div class="ace-clearable" data-bind="css: { 'visible': value() !== '' }, click: clear"><i class="fa fa-times-circle"></i></div> <div class="ace-editor"></div> </div> <!-- ko if: autocompleter !== null --> <!-- ko component: { name: 'hueAceAutocompleter', params: { editor: ace, autocompleter: autocompleter } } --><!-- /ko --> <!-- /ko --> `; const SIMPLE_ACE_MULTI_TEMPLATE = ` <div class="simple-ace-multi-line"> <div class="ace-editor"></div> </div> <!-- ko if: autocompleter !== null --> <!-- ko component: { name: 'hueAceAutocompleter', params: { editor: ace, autocompleter: autocompleter } } --><!-- /ko --> <!-- /ko --> `; const AVAILABLE_AUTOCOMPLETERS = { solrFormula: SolrFormulaAutocompleter, solrQuery: SolrQueryAutocompleter, impalaQuery: SqlAutocompleter, hiveQuery: SqlAutocompleter, impala: SqlAutocompleter, hive: SqlAutocompleter }; class SimpleAceEditor { constructor(params, element) { const $element = $(element); const self = this; self.value = params.value; self.parsedValue = params.parsedValue; self.ace = ko.observable(); self.disposeFunctions = []; self.singleLine = !!params.singleLine; let aceOptions = params.aceOptions || {}; if (!$element.attr('id')) { $element.attr('id', UUID()); } const editor = ace.edit($element.find('.ace-editor')[0]); if (params.mode) { editor.getSession().setMode('ace/mode/' + ko.unwrap(params.mode)); } editor.$blockScrolling = Infinity; editor.setTheme(hueLocalStorage('hue.ace.theme') || 'ace/theme/hue'); self.ace(editor); if (params.activeExecutable) { editor.commands.addCommand({ name: 'execute', bindKey: { win: 'Ctrl-Enter', mac: 'Command-Enter|Ctrl-Enter' }, exec: async () => { editor._signal('hideAutocomplete'); const executable = params.activeExecutable(); if (executable) { await executable.reset(); executable.execute(); } } }); } if (params.autocomplete && ko.unwrap(params.autocomplete)) { const autocomplete = ko.unwrap(params.autocomplete); let connector = ko.unwrap(autocomplete.connector); if (!connector || !connector.id) { const connectorId = autocomplete.type.indexOf('Query') !== -1 ? autocomplete.type.replace('Query', '') : autocomplete.type; connector = findEditorConnector(connector => connector.id === connectorId); // TODO: Temporary fix for dashboard if (!connector) { connector = { id: connectorId }; } } const snippet = { autocompleteSettings: { temporaryOnly: params.temporaryOnly }, type: ko.observable(connector.id), dialect: ko.observable(connector.dialect), connector: ko.observable(connector), id: ko.observable($element.attr('id')), namespace: params.namespace, compute: params.compute, database: ko.isObservable(params.database) ? params.database : ko.observable(params.database || 'default'), availableDatabases: params.availableDatabases ? params.availableDatabases : ko.observableArray([ params.database && params.database() ? params.database() : 'default' ]), positionStatement: ko.observable({ location: { first_line: 1, last_line: 1, first_column: 0, last_column: editor.getValue().length } }), whenContextSet: function () { const promise = $.Deferred().resolve().promise(); promise.dispose = function () {}; return promise; }, isSqlDialect: ko.observable(connector.is_sql), aceCursorPosition: ko.observable(), inFocus: ko.observable() }; if (connector.is_sql && !params.disableWorkers) { registerHueWorkers(); const aceLocationHandler = new AceLocationHandler({ editor: editor, editorId: $element.attr('id'), snippet: snippet }); const aceGutterHandler = new AceGutterHandler({ editor: editor, editorId: $element.attr('id'), executor: snippet.executor }); self.disposeFunctions.push(() => { aceLocationHandler.dispose(); aceGutterHandler.dispose(); }); aceLocationHandler.attachSqlSyntaxWorker(); } const focusListener = editor.on('focus', () => { snippet.inFocus(true); }); const blurListener = editor.on('blur', () => { snippet.inFocus(false); }); self.disposeFunctions.push(() => { editor.off('focus', focusListener); editor.off('blur', blurListener); }); const autocompleteArgs = { editor: function () { return editor; }, snippet: snippet, fixedPrefix: params.fixedPrefix, fixedPostfix: params.fixedPostfix, support: autocomplete.support }; const AutocompleterClass = AVAILABLE_AUTOCOMPLETERS[autocomplete.type] || SqlAutocompleter; self.autocompleter = new AutocompleterClass(autocompleteArgs); } else { self.autocompleter = null; } if (self.singleLine) { aceOptions = $.extend(aceOptions, { fontSize: '13px', maxLines: 1, // make it 1 line autoScrollEditorIntoView: true, highlightActiveLine: false, printMargin: false, showGutter: false }); } else { aceOptions = $.extend(aceOptions, { maxLines: params.lines || 2 }); } if (params.autocomplete) { aceOptions = $.extend(aceOptions, { enableLiveAutocompletion: true, enableBasicAutocompletion: true }); } editor.setOptions(aceOptions); if (params.onExec) { const bindPrefix = params.singleLine ? 'Enter|Shift-Enter|' : ''; editor.commands.addCommand({ name: 'enter', bindKey: { win: bindPrefix + 'Ctrl-Enter', mac: bindPrefix + 'Ctrl-Enter|Command-Enter' }, exec: params.onExec }); } if (params.singleLine) { editor.renderer.screenToTextCoordinates = function (x, y) { const pos = this.pixelToScreenCoordinates(x, y); return this.session.screenToDocumentPosition( Math.min(this.session.getScreenLength() - 1, Math.max(pos.row, 0)), Math.max(pos.column, 0) ); }; if (!params.onExec) { editor.commands.bindKey('Enter|Shift-Enter', 'null'); } const pasteListener = editor.on('paste', e => { e.text = e.text.replace(/[\r\n]+/g, ' '); }); const changeListener = editor.on('change', e => { if (e.action === 'insert' && (e.start.row !== 0 || e.end.row !== 0)) { editor.setValue(editor.getValue().replace(/[\r\n]+/g, ' ')); editor.clearSelection(); } }); self.disposeFunctions.push(() => { editor.off('paste', pasteListener); editor.off('change', changeListener); }); } if (self.value()) { editor.setValue(self.value()); editor.clearSelection(); } if (params.placeHolder && ko.unwrap(params.placeHolder)) { let placeHolderVisible = false; const $placeHolder = $('<div>') .text(ko.unwrap(params.placeHolder)) .addClass('ace_invisible ace_emptyMessage'); if (editor.getValue().length === 0) { $placeHolder.appendTo(editor.renderer.scroller); placeHolderVisible = true; } const inputListener = editor.on('input', () => { if (editor.getValue().length > 0 && placeHolderVisible) { $placeHolder.remove(); placeHolderVisible = false; } else if (editor.getValue().length === 0 && !placeHolderVisible) { $placeHolder.appendTo(editor.renderer.scroller); placeHolderVisible = true; } }); self.disposeFunctions.push(() => { editor.off('input', inputListener); }); } if (params.autocomplete) { const AceAutocomplete = ace.require('ace/autocomplete').Autocomplete; if (!editor.completer) { editor.completer = new AceAutocomplete(); } editor.completer.exactMatch = false; editor.useHueAutocompleter = true; const afterExecListener = editor.commands.on('afterExec', e => { if (e.command.name === 'insertstring') { let triggerAutocomplete = false; if (/\S+\(\)$/.test(e.args)) { editor.moveCursorTo( editor.getCursorPosition().row, editor.getCursorPosition().column - 1 ); triggerAutocomplete = true; } else if (params.autocomplete.type === 'solrQuery' && /:$/.test(e.args)) { triggerAutocomplete = true; } if (triggerAutocomplete) { window.setTimeout(() => { editor.execCommand('startAutocomplete'); }, 1); } } }); self.disposeFunctions.push(() => { editor.commands.off('afterExec', afterExecListener); }); } let parseThrottle = -1; const valueUpdateListener = editor.on('input', () => { self.value(editor.getValue()); if (self.parsedValue && self.autocompleter && self.autocompleter.parse) { window.clearTimeout(parseThrottle); parseThrottle = window.setTimeout(() => { const parseResult = self.autocompleter.parse(editor.getValue()); if (parseResult) { self.parsedValue(parseResult.parsedValue); } else { // TODO: What to do when we can't parse? self.parsedValue(editor.getValue()); } }, 200); } }); self.disposeFunctions.push(() => { editor.off('input', valueUpdateListener); }); } clear() { const self = this; if (self.ace()) { self.ace().setValue(''); } else { self.value(''); } } dispose() { const self = this; self.disposeFunctions.forEach(dispose => { dispose(); }); } } // Ace requires that the child to clone is first child, so we need separate components for single line & multi line componentUtils.registerComponent( NAME, { createViewModel: function (params, componentInfo) { return new SimpleAceEditor(params, componentInfo.element); } }, SIMPLE_ACE_TEMPLATE ); componentUtils.registerComponent( MULTI_NAME, { createViewModel: function (params, componentInfo) { return new SimpleAceEditor(params, componentInfo.element); } }, SIMPLE_ACE_MULTI_TEMPLATE );