2020-05-07 17:45:04 +02:00
|
|
|
<template>
|
|
|
|
<div class="editor-wrapper">
|
2020-12-23 18:07:50 +01:00
|
|
|
<div
|
2021-02-21 19:22:03 +01:00
|
|
|
:id="`editor-${id}`"
|
2020-12-23 18:07:50 +01:00
|
|
|
class="editor"
|
|
|
|
:style="{height: `${height}px`}"
|
|
|
|
/>
|
2020-05-07 17:45:04 +02:00
|
|
|
</div>
|
|
|
|
</template>
|
|
|
|
|
2022-06-04 18:37:16 +02:00
|
|
|
<script setup lang="ts">
|
2023-08-18 15:57:31 +02:00
|
|
|
/* eslint-disable simple-import-sort/imports */
|
2020-12-21 11:06:41 +01:00
|
|
|
import * as ace from 'ace-builds';
|
2020-12-22 22:31:31 +01:00
|
|
|
import '../libs/ext-language_tools';
|
2023-08-18 15:57:31 +02:00
|
|
|
import 'ace-builds/webpack-resolver';
|
|
|
|
/* eslint-enable simple-import-sort/imports */
|
|
|
|
|
2022-04-30 17:15:33 +02:00
|
|
|
import { uidGen } from 'common/libs/uidGen';
|
2023-08-18 15:57:31 +02:00
|
|
|
import { storeToRefs } from 'pinia';
|
|
|
|
import { computed, onMounted, Prop, Ref, ref, toRef, watch } from 'vue';
|
|
|
|
|
|
|
|
import Tables from '@/ipc-api/Tables';
|
2022-04-27 18:23:48 +02:00
|
|
|
import { useApplicationStore } from '@/stores/application';
|
2022-04-30 00:47:37 +02:00
|
|
|
import { useSettingsStore } from '@/stores/settings';
|
2022-06-04 18:37:16 +02:00
|
|
|
import { Workspace } from '@/stores/workspaces';
|
2020-05-07 17:45:04 +02:00
|
|
|
|
2022-06-04 18:37:16 +02:00
|
|
|
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);
|
|
|
|
|
2022-09-15 19:03:18 +02:00
|
|
|
const sizes = {
|
2022-09-21 10:33:44 +02:00
|
|
|
xsmall: '10px',
|
2022-09-15 19:03:18 +02:00
|
|
|
small: '12px',
|
|
|
|
medium: '14px',
|
2022-09-21 10:33:44 +02:00
|
|
|
large: '16px',
|
|
|
|
xlarge: '18px',
|
|
|
|
xxlarge: '20px'
|
2022-09-15 19:03:18 +02:00
|
|
|
};
|
|
|
|
|
2022-06-04 18:37:16 +02:00
|
|
|
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);
|
2022-07-14 16:09:04 +02:00
|
|
|
const lastTableFields = ref([]);
|
2022-06-04 18:37:16 +02:00
|
|
|
const customCompleter = ref([]);
|
|
|
|
const id = ref(uidGen());
|
|
|
|
const lastSchema: Ref<string> = ref(null);
|
2022-07-14 16:09:04 +02:00
|
|
|
const fields: Ref<{name: string; type: string}[]> = ref([]);
|
2022-06-04 18:37:16 +02:00
|
|
|
|
|
|
|
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,
|
2022-07-14 16:09:04 +02:00
|
|
|
type: table.type as string
|
2022-06-04 18:37:16 +02:00
|
|
|
};
|
|
|
|
})
|
|
|
|
: [];
|
|
|
|
});
|
|
|
|
|
2022-07-14 16:09:04 +02:00
|
|
|
const tablesInQuery = computed(() => {
|
2022-07-18 14:43:38 +02:00
|
|
|
if (!props.modelValue) return [];
|
2022-07-14 16:09:04 +02:00
|
|
|
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;
|
|
|
|
});
|
|
|
|
|
2022-06-04 18:37:16 +02:00
|
|
|
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';
|
2022-06-14 20:02:17 +02:00
|
|
|
// case 'mssql':
|
|
|
|
// return 'sqlserver';
|
2022-06-04 18:37:16 +02:00
|
|
|
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));
|
|
|
|
|
2022-07-14 16:09:04 +02:00
|
|
|
const tableFieldsCompleter = computed(() => {
|
2022-06-04 18:37:16 +02:00
|
|
|
return {
|
|
|
|
getCompletions: (editor: never, session: never, pos: never, prefix: never, callback: (err: null, response: ace.Ace.Completion[]) => void) => {
|
|
|
|
const completions: ace.Ace.Completion[] = [];
|
2022-07-14 16:09:04 +02:00
|
|
|
lastTableFields.value.forEach(field => {
|
2022-06-04 18:37:16 +02:00
|
|
|
completions.push({
|
|
|
|
value: field,
|
|
|
|
meta: 'column',
|
|
|
|
score: 1000
|
2020-12-24 10:40:22 +01:00
|
|
|
});
|
2022-06-04 18:37:16 +02:00
|
|
|
});
|
|
|
|
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,
|
2022-07-14 16:09:04 +02:00
|
|
|
...schedulers.value,
|
|
|
|
...fields.value
|
2022-06-04 18:37:16 +02:00
|
|
|
].forEach(el => {
|
|
|
|
completions.push({
|
|
|
|
value: el.name,
|
|
|
|
meta: el.type,
|
|
|
|
score: 1000
|
2020-12-24 15:33:51 +01:00
|
|
|
});
|
2022-06-04 18:37:16 +02:00
|
|
|
});
|
|
|
|
callback(null, completions);
|
2020-12-23 18:07:50 +01:00
|
|
|
}
|
2022-06-04 18:37:16 +02:00
|
|
|
});
|
|
|
|
|
|
|
|
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());
|
|
|
|
});
|
|
|
|
|
2022-07-14 16:09:04 +02:00
|
|
|
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;
|
2022-11-04 16:31:10 +01:00
|
|
|
setTimeout(() => {
|
|
|
|
setCustomCompleter();
|
|
|
|
}, 100);
|
2022-07-14 16:09:04 +02:00
|
|
|
});
|
|
|
|
|
2022-06-04 18:37:16 +02:00
|
|
|
watch(editorTheme, () => {
|
|
|
|
if (editor.value)
|
|
|
|
editor.value.setTheme(`ace/theme/${editorTheme.value}`);
|
|
|
|
});
|
|
|
|
|
|
|
|
watch(editorFontSize, () => {
|
|
|
|
if (editor.value) {
|
|
|
|
editor.value.setOptions({
|
|
|
|
fontSize: sizes[editorFontSize.value]
|
2020-08-20 18:06:02 +02:00
|
|
|
});
|
2022-06-04 18:37:16 +02:00
|
|
|
}
|
|
|
|
});
|
2020-05-07 17:45:04 +02:00
|
|
|
|
2022-06-04 18:37:16 +02:00
|
|
|
watch(autoComplete, () => {
|
|
|
|
if (editor.value) {
|
|
|
|
editor.value.setOptions({
|
|
|
|
enableLiveAutocompletion: autoComplete.value
|
2020-12-21 11:06:41 +01:00
|
|
|
});
|
2022-06-04 18:37:16 +02:00
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
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,
|
2022-09-15 19:03:18 +02:00
|
|
|
enableLiveAutocompletion: autoComplete.value,
|
|
|
|
fontSize: sizes[editorFontSize.value]
|
2022-06-04 18:37:16 +02:00
|
|
|
});
|
|
|
|
|
|
|
|
if (!baseCompleter.value.length)
|
|
|
|
setBaseCompleters(editor.value.completers.map(el => Object.assign({}, el)));
|
|
|
|
|
|
|
|
setCustomCompleter();
|
2020-12-21 11:06:41 +01:00
|
|
|
|
2022-06-04 18:37:16 +02:00
|
|
|
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)
|
2022-07-14 16:09:04 +02:00
|
|
|
lastTableFields.value = res.response.map((field: { name: string }) => field.name);
|
|
|
|
editor.value.completers = [tableFieldsCompleter.value];
|
2022-06-04 18:37:16 +02:00
|
|
|
editor.value.execCommand('startAutocomplete');
|
|
|
|
}).catch(console.log);
|
2020-12-22 22:31:31 +01:00
|
|
|
}
|
|
|
|
else
|
2022-06-04 18:37:16 +02:00
|
|
|
editor.value.completers = customCompleter.value;
|
2020-12-22 22:31:31 +01:00
|
|
|
}
|
|
|
|
else
|
2022-06-04 18:37:16 +02:00
|
|
|
editor.value.completers = customCompleter.value;
|
2020-12-22 22:31:31 +01:00
|
|
|
}
|
2022-06-04 18:37:16 +02:00
|
|
|
else
|
|
|
|
editor.value.completers = customCompleter.value;
|
|
|
|
}
|
|
|
|
});
|
2020-12-22 22:31:31 +01:00
|
|
|
|
2022-06-04 18:37:16 +02:00
|
|
|
// 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);
|
|
|
|
});
|
2020-12-11 15:55:18 +01:00
|
|
|
|
2022-06-04 18:37:16 +02:00
|
|
|
// 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;
|
2021-03-08 18:11:00 +01:00
|
|
|
|
2022-06-04 18:37:16 +02:00
|
|
|
const row = e.getDocumentPosition().row;
|
2022-06-24 17:26:28 +02:00
|
|
|
const breakpoints = e.editor.session.getBreakpoints(row, 0);
|
2022-06-04 18:37:16 +02:00
|
|
|
if (typeof breakpoints[row] === typeof undefined)
|
2022-06-24 17:26:28 +02:00
|
|
|
e.editor.session.setBreakpoint(row);
|
2022-06-04 18:37:16 +02:00
|
|
|
else
|
2022-06-24 17:26:28 +02:00
|
|
|
e.editor.session.clearBreakpoint(row);
|
2022-06-04 18:37:16 +02:00
|
|
|
e.stop();
|
|
|
|
});
|
2020-12-26 15:37:34 +01:00
|
|
|
|
2023-05-26 18:17:23 +02:00
|
|
|
editor.value.commands.removeCommand('showSettingsMenu');
|
|
|
|
|
2022-06-04 18:37:16 +02:00
|
|
|
if (props.autoFocus) {
|
2020-12-26 15:37:34 +01:00
|
|
|
setTimeout(() => {
|
2022-06-04 18:37:16 +02:00
|
|
|
editor.value.focus();
|
|
|
|
editor.value.resize();
|
2020-12-26 15:37:34 +01:00
|
|
|
}, 20);
|
2020-05-07 17:45:04 +02:00
|
|
|
}
|
2022-06-04 18:37:16 +02:00
|
|
|
|
|
|
|
setTimeout(() => {
|
|
|
|
editor.value.resize();
|
|
|
|
}, 20);
|
|
|
|
});
|
|
|
|
|
|
|
|
defineExpose({ editor });
|
2020-05-07 17:45:04 +02:00
|
|
|
</script>
|
|
|
|
|
2020-06-05 21:00:15 +02:00
|
|
|
<style lang="scss">
|
2020-12-22 22:31:31 +01:00
|
|
|
.editor-wrapper {
|
|
|
|
border-bottom: 1px solid #444;
|
2020-08-20 18:06:02 +02:00
|
|
|
|
2020-12-22 22:31:31 +01:00
|
|
|
.editor {
|
|
|
|
width: 100%;
|
2020-07-31 18:16:28 +02:00
|
|
|
}
|
2020-12-22 22:31:31 +01:00
|
|
|
}
|
|
|
|
|
2021-03-08 18:11:00 +01:00
|
|
|
.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;
|
|
|
|
}
|
|
|
|
}
|
2020-05-07 17:45:04 +02:00
|
|
|
</style>
|