antares/src/renderer/components/QueryEditor.vue

422 lines
11 KiB
Vue

<template>
<div class="editor-wrapper">
<div
:id="`editor-${id}`"
class="editor"
:style="{height: `${height}px`}"
/>
</div>
</template>
<script setup lang="ts">
import { computed, onMounted, Prop, Ref, ref, toRef, watch } from 'vue';
import * as ace from 'ace-builds';
import 'ace-builds/webpack-resolver';
import '../libs/ext-language_tools';
import { storeToRefs } from 'pinia';
import { uidGen } from 'common/libs/uidGen';
import { useApplicationStore } from '@/stores/application';
import { useSettingsStore } from '@/stores/settings';
import { Workspace } from '@/stores/workspaces';
import Tables from '@/ipc-api/Tables';
const editor: Ref<ace.Ace.Editor> = ref(null);
const applicationStore = useApplicationStore();
const settingsStore = useSettingsStore();
const { setBaseCompleters } = applicationStore;
const { baseCompleter } = storeToRefs(applicationStore);
const {
editorTheme,
editorFontSize,
autoComplete,
lineWrap
} = storeToRefs(settingsStore);
const sizes = {
xsmall: '10px',
small: '12px',
medium: '14px',
large: '16px',
xlarge: '18px',
xxlarge: '20px'
};
const props = defineProps({
modelValue: String,
workspace: Object as Prop<Workspace>,
isSelected: Boolean,
schema: { type: String, default: '' },
autoFocus: { type: Boolean, default: false },
readOnly: { type: Boolean, default: false },
height: { type: Number, default: 200 }
});
const emit = defineEmits(['update:modelValue']);
const cursorPosition = ref(0);
const lastTableFields = ref([]);
const customCompleter = ref([]);
const id = ref(uidGen());
const lastSchema: Ref<string> = ref(null);
const fields: Ref<{name: string; type: string}[]> = ref([]);
const tables = computed(() => {
return props.workspace
? props.workspace.structure.filter(schema => schema.name === props.schema)
.reduce((acc, curr) => {
acc.push(...curr.tables);
return acc;
}, []).map(table => {
return {
name: table.name as string,
type: table.type as string
};
})
: [];
});
const tablesInQuery = computed(() => {
if (!props.modelValue) return [];
const words = props.modelValue
.replaceAll(/[.'"`]/g, ' ')
.split(' ')
.filter(Boolean);
const includedTables = tables.value.reduce((acc, curr) => {
acc.push(curr.name);
return acc;
}, [] as string[]).filter((t) => words.includes(t));
return includedTables;
});
const triggers = computed(() => {
return props.workspace
? props.workspace.structure.filter(schema => schema.name === props.schema)
.reduce((acc, curr) => {
acc.push(...curr.triggers);
return acc;
}, []).map(trigger => {
return {
name: trigger.name as string,
type: 'trigger'
};
})
: [];
});
const procedures = computed(() => {
return props.workspace
? props.workspace.structure.filter(schema => schema.name === props.schema)
.reduce((acc, curr) => {
acc.push(...curr.procedures);
return acc;
}, []).map(procedure => {
return {
name: `${procedure.name}()`,
type: 'routine'
};
})
: [];
});
const functions = computed(() => {
return props.workspace
? props.workspace.structure.filter(schema => schema.name === props.schema)
.reduce((acc, curr) => {
acc.push(...curr.functions);
return acc;
}, []).map(func => {
return {
name: `${func.name}()`,
type: 'function'
};
})
: [];
});
const schedulers = computed(() => {
return props.workspace
? props.workspace.structure.filter(schema => schema.name === props.schema)
.reduce((acc, curr) => {
acc.push(...curr.schedulers);
return acc;
}, []).map(scheduler => {
return {
name: scheduler.name as string,
type: 'scheduler'
};
})
: [];
});
const mode = computed(() => {
switch (props.workspace.client) {
case 'mysql':
case 'maria':
return 'mysql';
// case 'mssql':
// return 'sqlserver';
case 'pg':
return 'pgsql';
default:
return 'sql';
}
});
const lastWord = computed(() => {
const charsBefore = props.modelValue.slice(0, cursorPosition.value);
const words = charsBefore.replaceAll('\n', ' ').split(' ').filter(Boolean);
return words.pop();
});
const isLastWordATable = computed(() => /\w+\.\w*/gm.test(lastWord.value));
const tableFieldsCompleter = computed(() => {
return {
getCompletions: (editor: never, session: never, pos: never, prefix: never, callback: (err: null, response: ace.Ace.Completion[]) => void) => {
const completions: ace.Ace.Completion[] = [];
lastTableFields.value.forEach(field => {
completions.push({
value: field,
meta: 'column',
score: 1000
});
});
callback(null, completions);
}
};
});
const setCustomCompleter = () => {
editor.value.completers.push({
getCompletions: (editor, session, pos, prefix, callback: (err: null, response: ace.Ace.Completion[]) => void) => {
const completions: ace.Ace.Completion[] = [];
[
...tables.value,
...triggers.value,
...procedures.value,
...functions.value,
...schedulers.value,
...fields.value
].forEach(el => {
completions.push({
value: el.name,
meta: el.type,
score: 1000
});
});
callback(null, completions);
}
});
customCompleter.value = editor.value.completers;
};
watch(() => props.modelValue, () => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
cursorPosition.value = (editor.value.session as any).doc.positionToIndex(editor.value.getCursorPosition());
});
watch(() => tablesInQuery.value.length, () => {
const localFields: {name: string; type: string}[] = [];
tablesInQuery.value.forEach(async table => {
const params = {
uid: props.workspace.uid,
schema: props.schema,
table: table
};
const { response } = await Tables.getTableColumns(params);
response.forEach((field: { name: string }) => {
localFields.push({
name: field.name,
type: 'column'
});
});
});
fields.value = localFields;
setTimeout(() => {
setCustomCompleter();
}, 100);
});
watch(editorTheme, () => {
if (editor.value)
editor.value.setTheme(`ace/theme/${editorTheme.value}`);
});
watch(editorFontSize, () => {
if (editor.value) {
editor.value.setOptions({
fontSize: sizes[editorFontSize.value]
});
}
});
watch(autoComplete, () => {
if (editor.value) {
editor.value.setOptions({
enableLiveAutocompletion: autoComplete.value
});
}
});
watch(lineWrap, () => {
if (editor.value) {
editor.value.setOptions({
wrap: lineWrap.value
});
}
});
watch(() => props.isSelected, () => {
if (props.isSelected) {
lastSchema.value = props.schema;
editor.value.resize();
}
});
watch(() => props.height, () => {
setTimeout(() => {
editor.value.resize();
}, 20);
});
watch(lastSchema, () => {
if (editor.value) {
editor.value.completers = baseCompleter.value.map(el => Object.assign({}, el));
setCustomCompleter();
}
});
lastSchema.value = toRef(props, 'schema').value;
onMounted(() => {
editor.value = ace.edit(`editor-${id.value}`, {
mode: `ace/mode/${mode.value}`,
theme: `ace/theme/${editorTheme.value}`,
value: props.modelValue,
fontSize: 14,
printMargin: false,
readOnly: props.readOnly
});
editor.value.setOptions({
enableBasicAutocompletion: true,
wrap: lineWrap.value,
enableSnippets: true,
enableLiveAutocompletion: autoComplete.value,
fontSize: sizes[editorFontSize.value]
});
if (!baseCompleter.value.length)
setBaseCompleters(editor.value.completers.map(el => Object.assign({}, el)));
setCustomCompleter();
editor.value.commands.on('afterExec', (e: { args: string; command: { name: string } }) => {
if (['insertstring', 'backspace', 'del'].includes(e.command.name)) {
if (isLastWordATable.value || e.args === '.') {
if (e.args !== ' ') {
const table = tables.value.find(t => t.name === lastWord.value.split('.').pop().trim());
if (table) {
const params = {
uid: props.workspace.uid,
schema: props.schema,
table: table.name
};
Tables.getTableColumns(params).then(res => {
if (res.response.length)
lastTableFields.value = res.response.map((field: { name: string }) => field.name);
editor.value.completers = [tableFieldsCompleter.value];
editor.value.execCommand('startAutocomplete');
}).catch(console.log);
}
else
editor.value.completers = customCompleter.value;
}
else
editor.value.completers = customCompleter.value;
}
else
editor.value.completers = customCompleter.value;
}
});
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(editor.value.session as any).on('change', () => {
const content = editor.value.getValue();
emit('update:modelValue', content);
});
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(editor.value as any).on('guttermousedown', (e: any) => {
const target = e.domEvent.target;
if (target.className.indexOf('ace_gutter-cell') === -1)
return;
if (e.clientX > 25 + target.getBoundingClientRect().left)
return;
const row = e.getDocumentPosition().row;
const breakpoints = e.editor.session.getBreakpoints(row, 0);
if (typeof breakpoints[row] === typeof undefined)
e.editor.session.setBreakpoint(row);
else
e.editor.session.clearBreakpoint(row);
e.stop();
});
if (props.autoFocus) {
setTimeout(() => {
editor.value.focus();
editor.value.resize();
}, 20);
}
setTimeout(() => {
editor.value.resize();
}, 20);
});
defineExpose({ editor });
</script>
<style lang="scss">
.editor-wrapper {
border-bottom: 1px solid #444;
.editor {
width: 100%;
}
}
.ace_.mdi {
display: inline-block;
width: 17px;
}
.ace_gutter-cell.ace_breakpoint {
&::before {
content: '\F0403';
position: absolute;
left: 3px;
top: 2px;
color: $primary-color;
display: inline-block;
font: normal normal normal 24px/1 "Material Design Icons", sans-serif;
font-size: inherit;
text-rendering: auto;
line-height: inherit;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
}
</style>