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

Encapsulate codemirror #1907

Merged
merged 13 commits into from
Feb 12, 2024
474 changes: 92 additions & 382 deletions apps/studio/src/components/TabQueryEditor.vue

Large diffs are not rendered by default.

179 changes: 179 additions & 0 deletions apps/studio/src/components/common/texteditor/SQLTextEditor.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
<template>
<text-editor
v-bind="$attrs"
:value="value"
@input="$emit('input', $event)"
:lang="lang || 'sql'"
:extra-keybindings="keybindings"
:hint-options="hintOptions"
:columns-getter="columnsGetter"
:context-menu-options="handleContextMenuOptions"
@initialized="handleInitialized"
@paste="handlePaste"
@keyup="handleKeyup"
@interface="handleInterface"
/>
</template>

<script lang="ts">
import Vue from "vue";
import TextEditor from "./TextEditor.vue";
import CodeMirror from "codemirror";
import { mapState } from "vuex";
import { registerAutoquote } from "@/lib/codemirror";
import { removeQueryQuotes } from "@/lib/db/sql_tools";
import { format } from "sql-formatter";
import { FormatterDialect, dialectFor } from "@shared/lib/dialects/models";

export default Vue.extend({
components: { TextEditor },
props: ["value", "lang", "extraKeybindings", "contextMenuOptions"],
data() {
return {
cursorIndex: null,
editorInterface: {},
};
},
computed: {
...mapState(["tables"]),
hintOptions() {
const firstTables = {};
const secondTables = {};
const thirdTables = {};

this.tables.forEach((table) => {
// don't add table names that can get in conflict with database schema
if (/\./.test(table.name)) return;

// Previously we had to provide a table: column[] mapping.
// we don't need to provide the columns anymore because we fetch them dynamically.
if (!table.schema) {
firstTables[table.name] = [];
return;
}

if (table.schema === this.defaultSchema) {
firstTables[table.name] = [];
secondTables[`${table.schema}.${table.name}`] = [];
} else {
thirdTables[`${table.schema}.${table.name}`] = [];
}
});

const sorted = Object.assign(
firstTables,
Object.assign(secondTables, thirdTables)
);

return { tables: sorted };
},
keybindings() {
return {
"Shift-Ctrl-F": this.formatSql,
"Shift-Cmd-F": this.formatSql,
...this.extraKeybindings,
};
},
},
methods: {
formatSql() {
const formatted = format(this.value, {
language: FormatterDialect(dialectFor(this.lang)),
});
this.editorInterface.setValue(formatted);
azmy60 marked this conversation as resolved.
Show resolved Hide resolved
this.editorInterface.focus();
},
async columnsGetter(tableName: string) {
const tableToFind = this.tables.find(
(t) => t.name === tableName || `${t.schema}.${t.name}` === tableName
);
if (!tableToFind) return null;
// Only refresh columns if we don't have them cached.
if (!tableToFind.columns?.length) {
await this.$store.dispatch("updateTableColumns", tableToFind);
}

return tableToFind?.columns.map((c) => c.columnName);
},
handleInterface(editorInterface: any) {
this.editorInterface = editorInterface;
azmy60 marked this conversation as resolved.
Show resolved Hide resolved
this.$emit("interface", editorInterface);
},
handleInitialized(cm: CodeMirror.Editor) {
azmy60 marked this conversation as resolved.
Show resolved Hide resolved
registerAutoquote(cm);
this.$emit("initialized", ...arguments);
},
handlePaste(cm: CodeMirror.Editor, e) {
azmy60 marked this conversation as resolved.
Show resolved Hide resolved
e.preventDefault();
let clipboard = (e.clipboardData.getData("text") as string).trim();
clipboard = removeQueryQuotes(clipboard, this.identifyDialect);
if (this.hasSelectedText) {
cm.replaceSelection(clipboard, "around");
} else {
const cursor = cm.getCursor();
cm.replaceRange(clipboard, cursor);
}
},
handleKeyup(editor: CodeMirror.Editor, e) {
azmy60 marked this conversation as resolved.
Show resolved Hide resolved
// TODO: make this not suck
// BUGS:
// 1. only on periods if not in a quote
// 2. post-space trigger after a few SQL keywords
// - from, join
const triggerWords = ["from", "join"];
const triggers = {
"190": "period",
};
const space = 32;
if (editor.state.completionActive) return;
if (triggers[e.keyCode]) {
// eslint-disable-next-line
// @ts-ignore
CodeMirror.commands.autocomplete(editor, null, {
completeSingle: false,
});
}
if (e.keyCode === space) {
try {
const pos = _.clone(editor.getCursor());
if (pos.ch > 0) {
pos.ch = pos.ch - 2;
}
const word = editor.findWordAt(pos);
const lastWord = editor.getRange(word.anchor, word.head);
if (!triggerWords.includes(lastWord.toLowerCase())) return;
// eslint-disable-next-line
// @ts-ignore
CodeMirror.commands.autocomplete(editor, null, {
completeSingle: false,
});
} catch (ex) {
// do nothing
}
}
},
handleContextMenuOptions(e: unknown, options: any[]) {
const pivot = options.findIndex((o) => o.slug === "find");
const newOptions = [
...options.slice(0, pivot),
{
name: "Format Query",
slug: "format",
handler: this.formatSql,
shortcut: this.ctrlOrCmd("shift+f"),
},
{
type: "divider",
},
...options.slice(pivot + 1),
];

if (this.contextMenuOptions) {
return this.contextMenuOptions(e, newOptions);
}

return newOptions;
},
},
});
</script>