gethue
Version:
Hue is an Open source SQL Query Editor for Databases/Warehouses
393 lines (344 loc) • 12.5 kB
JavaScript
// 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
);