From d4989c75ca8e41f96906942899fbb28c6736ffff Mon Sep 17 00:00:00 2001 From: Jan Prochazka Date: Sun, 21 Mar 2021 20:03:46 +0100 Subject: [PATCH] code completion --- packages/web/src/query/SqlEditor.svelte | 30 ++++- packages/web/src/query/analyseQuerySources.ts | 38 ++++++ packages/web/src/query/codeCompletion.ts | 122 ++++++++++++++++++ packages/web/src/tabs/QueryTab.svelte | 2 + 4 files changed, 191 insertions(+), 1 deletion(-) create mode 100644 packages/web/src/query/analyseQuerySources.ts create mode 100644 packages/web/src/query/codeCompletion.ts diff --git a/packages/web/src/query/SqlEditor.svelte b/packages/web/src/query/SqlEditor.svelte index e94f5b5a..aaefede7 100644 --- a/packages/web/src/query/SqlEditor.svelte +++ b/packages/web/src/query/SqlEditor.svelte @@ -9,11 +9,20 @@ - + diff --git a/packages/web/src/query/analyseQuerySources.ts b/packages/web/src/query/analyseQuerySources.ts new file mode 100644 index 00000000..d9e25554 --- /dev/null +++ b/packages/web/src/query/analyseQuerySources.ts @@ -0,0 +1,38 @@ +export default function analyseQuerySources(sql, sourceNames) { + const upperSourceNames = sourceNames.map(x => x.toUpperCase()); + const tokens = sql.split(/\s+/); + const res = []; + for (let i = 0; i < tokens.length; i += 1) { + const lastWordMatch = tokens[i].match(/([^.]+)$/); + if (lastWordMatch) { + const word = lastWordMatch[1]; + const wordUpper = word.toUpperCase(); + if (upperSourceNames.includes(wordUpper)) { + const preWord = tokens[i - 1]; + if (preWord && /^((join)|(from)|(update)|(delete)|(insert))$/i.test(preWord)) { + let postWord = tokens[i + 1]; + if (postWord && /^as$/i.test(postWord)) { + postWord = tokens[i + 2]; + } + if (!postWord) { + res.push({ + name: word, + }); + } else if ( + /^((where)|(inner)|(left)|(right)|(on)|(join))$/i.test(postWord) || + !/^[a-zA-Z][a-zA-Z0-9]*$/i.test(postWord) + ) { + res.push({ + name: word, + }); + } else + res.push({ + name: word, + alias: postWord, + }); + } + } + } + } + return res; +} diff --git a/packages/web/src/query/codeCompletion.ts b/packages/web/src/query/codeCompletion.ts new file mode 100644 index 00000000..e36c191b --- /dev/null +++ b/packages/web/src/query/codeCompletion.ts @@ -0,0 +1,122 @@ +import { addCompleter, setCompleters } from 'ace-builds/src-noconflict/ext-language_tools'; +import { getDatabaseInfo } from '../utility/metadataLoaders'; +import analyseQuerySources from './analyseQuerySources'; + +const COMMON_KEYWORDS = [ + 'select', + 'where', + 'update', + 'delete', + 'group', + 'order', + 'from', + 'by', + 'create', + 'table', + 'drop', + 'alter', + 'view', + 'execute', + 'procedure', + 'distinct', + 'go', +]; + +export function mountCodeCompletion({ conid, database, editor }) { + setCompleters([]); + addCompleter({ + getCompletions: async function (editor, session, pos, prefix, callback) { + const cursor = session.selection.cursor; + const line = session.getLine(cursor.row).slice(0, cursor.column); + const dbinfo = await getDatabaseInfo({ conid, database }); + + let list = COMMON_KEYWORDS.map(word => ({ + name: word, + value: word, + caption: word, + meta: 'keyword', + score: 800, + })); + + if (dbinfo) { + const colMatch = line.match(/([a-zA-Z0-9_]+)\.([a-zA-Z0-9_]*)?$/); + if (colMatch) { + const table = colMatch[1]; + const sources = analyseQuerySources(editor.getValue(), [ + ...dbinfo.tables.map(x => x.pureName), + ...dbinfo.views.map(x => x.pureName), + ]); + const source = sources.find(x => (x.alias || x.name) == table); + console.log('sources', sources); + console.log('table', table, source); + if (source) { + const table = dbinfo.tables.find(x => x.pureName == source.name); + if (table) { + list = [ + ...table.columns.map(x => ({ + name: x.columnName, + value: x.columnName, + caption: x.columnName, + meta: 'column', + score: 1000, + })), + ]; + } + } + } else { + list = [ + ...list, + ...dbinfo.tables.map(x => ({ + name: x.pureName, + value: x.pureName, + caption: x.pureName, + meta: 'table', + score: 1000, + })), + ...dbinfo.views.map(x => ({ + name: x.pureName, + value: x.pureName, + caption: x.pureName, + meta: 'view', + score: 1000, + })), + ]; + } + } + + // if (/(join)|(from)|(update)|(delete)|(insert)\s*([a-zA-Z0-9_]*)?$/i.test(line)) { + // if (dbinfo) { + // } + // } + + callback(null, list); + }, + }); + + const doLiveAutocomplete = function (e) { + const editor = e.editor; + var hasCompleter = editor.completer && editor.completer.activated; + const session = editor.session; + const cursor = session.selection.cursor; + const line = session.getLine(cursor.row).slice(0, cursor.column); + + // We don't want to autocomplete with no prefix + if (e.command.name === 'backspace') { + // do not hide after backspace + } else if (e.command.name === 'insertstring') { + if ((!hasCompleter && /^[a-zA-Z]/.test(e.args)) || e.args == '.') { + editor.execCommand('startAutocomplete'); + } + + if (e.args == ' ' && /((from)|(join))\s*$/i.test(line)) { + editor.execCommand('startAutocomplete'); + } + } + }; + + editor.commands.on('afterExec', doLiveAutocomplete); + + return () => { + editor.commands.removeListener('afterExec', doLiveAutocomplete); + }; +} diff --git a/packages/web/src/tabs/QueryTab.svelte b/packages/web/src/tabs/QueryTab.svelte index 60580445..eaf0e69e 100644 --- a/packages/web/src/tabs/QueryTab.svelte +++ b/packages/web/src/tabs/QueryTab.svelte @@ -204,6 +204,8 @@ setEditorData(e.detail)}