Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add dot behavior to autocomplete #3092

Merged
merged 9 commits into from
Dec 26, 2018
164 changes: 90 additions & 74 deletions client/app/components/QueryEditor.jsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import React from 'react';
import PropTypes from 'prop-types';
import { map } from 'lodash';
import Tooltip from 'antd/lib/tooltip';
import { react2angular } from 'react2angular';

Expand All @@ -17,6 +16,7 @@ import 'brace/ext/searchbox';

import localOptions from '@/lib/localOptions';
import AutocompleteToggle from '@/components/AutocompleteToggle';
import keywordBuilder from './keywordBuilder';
import { DataSource, Schema } from './proptypes';

const langTools = ace.acequire('ace/ext/language_tools');
Expand All @@ -35,24 +35,6 @@ defineDummySnippets('python');
defineDummySnippets('sql');
defineDummySnippets('json');

function buildKeywordsFromSchema(schema) {
const keywords = {};
schema.forEach((table) => {
keywords[table.name] = 'Table';
table.columns.forEach((c) => {
keywords[c] = 'Column';
keywords[`${table.name}.${c}`] = 'Column';
});
});

return map(keywords, (v, k) => ({
name: k,
value: k,
score: 0,
meta: v,
}));
}

class QueryEditor extends React.Component {
static propTypes = {
queryText: PropTypes.string.isRequired,
Expand Down Expand Up @@ -81,75 +63,47 @@ class QueryEditor extends React.Component {

constructor(props) {
super(props);

this.state = {
schema: null, // eslint-disable-line react/no-unused-state
keywords: [], // eslint-disable-line react/no-unused-state
keywords: {
table: [],
column: [],
tableColumn: [],
},
autocompleteQuery: localOptions.get('liveAutocomplete', true),
liveAutocompleteDisabled: false,
// XXX temporary while interfacing with angular
queryText: props.queryText,
};
langTools.addCompleter({

const schemaCompleter = {
identifierRegexps: [/[a-zA-Z_0-9.\-\u00A2-\uFFFF]/],
getCompletions: (state, session, pos, prefix, callback) => {
if (prefix.length === 0) {
const tableKeywords = this.state.keywords.table;
const columnKeywords = this.state.keywords.column;
const tableColumnKeywords = this.state.keywords.tableColumn;

if (prefix.length === 0 || tableKeywords.length === 0) {
callback(null, []);
return;
}
callback(null, this.state.keywords);
},
});

this.onLoad = (editor) => {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Was there a reason for those methods to be inside QueryEditor's constructor?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤷‍♂️

@washort ?

// Release Cmd/Ctrl+L to the browser
editor.commands.bindKey('Cmd+L', null);
editor.commands.bindKey('Ctrl+L', null);

// Ignore Ctrl+P to open new parameter dialog
editor.commands.bindKey({ win: 'Ctrl+P', mac: null }, null);
// Lineup only mac
editor.commands.bindKey({ win: null, mac: 'Ctrl+P' }, 'golineup');

// eslint-disable-next-line react/prop-types
this.props.QuerySnippet.query((snippets) => {
const snippetManager = snippetsModule.snippetManager;
const m = {
snippetText: '',
};
m.snippets = snippetManager.parseSnippetFile(m.snippetText);
snippets.forEach((snippet) => {
m.snippets.push(snippet.getSnippet());
});
snippetManager.register(m.snippets || [], m.scope);
});
editor.focus();
this.props.listenForResize(() => editor.resize());
this.props.listenForEditorCommand((e, command, ...args) => {
switch (command) {
case 'focus': {
editor.focus();
break;
}
case 'paste': {
const [text] = args;
editor.session.doc.replace(editor.selection.getRange(), text);
const range = editor.selection.getRange();
this.props.updateQuery(editor.session.getValue());
editor.selection.setRange(range);
break;
}
default:
break;
if (prefix[prefix.length - 1] === '.') {
const tableName = prefix.substring(0, prefix.length - 1);
callback(null, tableKeywords.concat(tableColumnKeywords[tableName]));
return;
}
});
callback(null, tableKeywords.concat(columnKeywords));
},
};

this.formatQuery = () => {
// eslint-disable-next-line react/prop-types
const format = this.props.Query.format;
format(this.props.dataSource.syntax || 'sql', this.props.queryText)
.then(this.updateQuery)
.catch(error => toastr.error(error));
};
langTools.setCompleters([
langTools.snippetCompleter,
langTools.keyWordCompleter,
langTools.textCompleter,
schemaCompleter,
]);
}

static getDerivedStateFromProps(nextProps, prevState) {
Expand All @@ -159,18 +113,80 @@ class QueryEditor extends React.Component {
const tokensCount = nextProps.schema.reduce((totalLength, table) => totalLength + table.columns.length, 0);
return {
schema: nextProps.schema,
keywords: buildKeywordsFromSchema(nextProps.schema),
keywords: keywordBuilder.buildKeywordsFromSchema(nextProps.schema),
liveAutocompleteDisabled: tokensCount > 5000,
};
}
return null;
}

onLoad = (editor) => {
// Release Cmd/Ctrl+L to the browser
editor.commands.bindKey('Cmd+L', null);
editor.commands.bindKey('Ctrl+P', null);
editor.commands.bindKey('Ctrl+L', null);

// Ignore Ctrl+P to open new parameter dialog
editor.commands.bindKey({ win: 'Ctrl+P', mac: null }, null);
// Lineup only mac
editor.commands.bindKey({ win: null, mac: 'Ctrl+P' }, 'golineup');

// Reset Completer in case dot is pressed
editor.commands.on('afterExec', (e) => {
if (e.command.name === 'insertstring' && e.args === '.'
&& editor.completer) {
editor.completer.showPopup(editor);
}
});

// eslint-disable-next-line react/prop-types
this.props.QuerySnippet.query((snippets) => {
const snippetManager = snippetsModule.snippetManager;
const m = {
snippetText: '',
};
m.snippets = snippetManager.parseSnippetFile(m.snippetText);
snippets.forEach((snippet) => {
m.snippets.push(snippet.getSnippet());
});
snippetManager.register(m.snippets || [], m.scope);
});

editor.focus();
this.props.listenForResize(() => editor.resize());
this.props.listenForEditorCommand((e, command, ...args) => {
switch (command) {
case 'focus': {
editor.focus();
break;
}
case 'paste': {
const [text] = args;
editor.session.doc.replace(editor.selection.getRange(), text);
const range = editor.selection.getRange();
this.props.updateQuery(editor.session.getValue());
editor.selection.setRange(range);
break;
}
default:
break;
}
});
};

updateQuery = (queryText) => {
this.props.updateQuery(queryText);
this.setState({ queryText });
};

formatQuery = () => {
// eslint-disable-next-line react/prop-types
const format = this.props.Query.format;
format(this.props.dataSource.syntax || 'sql', this.props.queryText)
.then(this.updateQuery)
.catch(error => toastr.error(error));
};

toggleAutocomplete = (state) => {
this.setState({ autocompleteQuery: state });
localOptions.set('liveAutocomplete', state);
Expand Down
50 changes: 50 additions & 0 deletions client/app/components/keywordBuilder.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { map } from 'lodash';

function buildTableColumnKeywords(table) {
const keywords = [];
table.columns.forEach((column) => {
keywords.push({
caption: column,
name: `${table.name}.${column}`,
value: `${table.name}.${column}`,
score: 100,
meta: 'Column',
className: 'completion',
});
});
return keywords;
}

function buildKeywordsFromSchema(schema) {
const tableKeywords = [];
const columnKeywords = {};
const tableColumnKeywords = {};

schema.forEach((table) => {
tableKeywords.push({
name: table.name,
value: table.name,
score: 100,
meta: 'Table',
});
tableColumnKeywords[table.name] = buildTableColumnKeywords(table);
table.columns.forEach((c) => {
columnKeywords[c] = 'Column';
});
});

return {
table: tableKeywords,
column: map(columnKeywords, (v, k) => ({
name: k,
value: k,
score: 50,
meta: v,
})),
tableColumn: tableColumnKeywords,
};
}

export default {
buildKeywordsFromSchema,
};