code completion

This commit is contained in:
Jan Prochazka 2021-03-21 20:03:46 +01:00
parent b7b9dde5ae
commit d4989c75ca
4 changed files with 191 additions and 1 deletions

View File

@ -9,11 +9,20 @@
<script lang="ts">
import AceEditor from './AceEditor.svelte';
import * as ace from 'ace-builds/src-noconflict/ace';
import useEffect from '../utility/useEffect';
import { getContext } from 'svelte';
import { mountCodeCompletion } from './codeCompletion';
export let engine;
export let conid;
export let database;
export let readOnly;
let domEditor;
let mode;
const tabVisible: any = getContext('tabVisible');
$: {
const match = (engine || '').match(/^([^@]*)@/);
mode = engineToMode[match ? match[1] : engine] || 'sql';
@ -22,6 +31,25 @@
export function getEditor(): ace.Editor {
return domEditor.getEditor();
}
$: effect = useEffect(() => {
const editor = domEditor?.getEditor();
if ($tabVisible && conid && database && !readOnly && editor) {
return mountCodeCompletion({ conid, database, editor });
}
return () => {};
});
$: $effect;
</script>
<AceEditor {mode} {...$$props} on:input on:focus on:blur bind:this={domEditor} />
<AceEditor
{mode}
{...$$props}
on:input
on:focus
on:blur
bind:this={domEditor}
options={{
enableBasicAutocompletion: true,
}}
/>

View File

@ -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;
}

View File

@ -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);
};
}

View File

@ -204,6 +204,8 @@
<svelte:fragment slot="1">
<SqlEditor
engine={$connection && $connection.engine}
{conid}
{database}
value={$editorState.value || ''}
menu={createMenu()}
on:input={e => setEditorData(e.detail)}