feat: add SQL single quote caret pairing
This commit is contained in:
parent
0c8bf24977
commit
aed2192d8c
|
|
@ -350,6 +350,18 @@ function requestExecute() {
|
|||
return true;
|
||||
}
|
||||
|
||||
function handleSqlSingleQuote(view: EditorViewType): boolean {
|
||||
const { state } = view;
|
||||
if (state.readOnly) return false;
|
||||
if (state.selection.ranges.some((range) => !range.empty || range.from === 0 || state.doc.sliceString(range.from - 1, range.from) !== "'")) return false;
|
||||
const transaction = state.changeByRange((range) => ({
|
||||
changes: { from: range.from, insert: "'" },
|
||||
range,
|
||||
}));
|
||||
view.dispatch(transaction, { userEvent: "input.type" });
|
||||
return true;
|
||||
}
|
||||
|
||||
function executionPickerAnchor(currentView: EditorViewType, cursorPos: number, candidateCount: number): { left: number; top: number } | undefined {
|
||||
const cursorRect = currentView.coordsAtPos(cursorPos);
|
||||
const rootRect = editorRef.value?.getBoundingClientRect();
|
||||
|
|
@ -1933,6 +1945,7 @@ onMounted(async () => {
|
|||
previewRangeComp.of(buildPreviewRangeExtension()),
|
||||
Prec.highest(
|
||||
keymap.of([
|
||||
{ key: "'", run: handleSqlSingleQuote },
|
||||
...closeBracketsKeymap,
|
||||
{ key: "Tab", run: handleTab },
|
||||
{
|
||||
|
|
|
|||
|
|
@ -141,6 +141,7 @@ import { useSettingsStore } from "@/stores/settingsStore";
|
|||
import type { DataGridSortDirection } from "@/lib/dataGridSort";
|
||||
import { getTableMetadataCapabilities } from "@/lib/tableMetadataCapabilities";
|
||||
import { forgetDataGridConditionHistory, loadDataGridConditionHistory, rememberDataGridConditionHistory } from "@/lib/dataGridConditionHistory";
|
||||
import { caretPositionInsideInsertedSqlSingleQuotes, insertedSqlSingleQuoteAtCaret } from "@/lib/sqlQuoteCaret";
|
||||
|
||||
const SqlPreviewPanel = defineAsyncComponent(() => import("@/components/editor/SqlPreviewPanel.vue"));
|
||||
|
||||
|
|
@ -453,6 +454,7 @@ const orderBySuggestionPosition = ref({ left: 0, top: 0 });
|
|||
const orderByInput = ref(props.initialOrderByInput ?? "");
|
||||
const hasOrderByInput = computed(() => orderByInput.value.trim().length > 0);
|
||||
const whereFilterInput = ref(props.initialWhereInput ?? "");
|
||||
let previousWhereFilterInputValue = whereFilterInput.value;
|
||||
const hasWhereFilterInput = computed(() => whereFilterInput.value.trim().length > 0);
|
||||
const conditionHistoryScope = computed(() => ({
|
||||
connectionId: props.connectionId,
|
||||
|
|
@ -1327,9 +1329,38 @@ function deleteWhereHistorySuggestion(value: string) {
|
|||
whereSuggestionIndex.value = whereSuggestions.value.length ? Math.min(whereSuggestionIndex.value, whereSuggestions.value.length - 1) : -1;
|
||||
}
|
||||
|
||||
function onWhereFilterInput(event: Event) {
|
||||
const input = event.target instanceof HTMLInputElement ? event.target : null;
|
||||
if (!input) return;
|
||||
const nextValue = input.value;
|
||||
if (
|
||||
insertedSqlSingleQuoteAtCaret({
|
||||
previousValue: previousWhereFilterInputValue,
|
||||
nextValue,
|
||||
selectionStart: input.selectionStart,
|
||||
})
|
||||
) {
|
||||
const caret = input.selectionStart ?? nextValue.length;
|
||||
const pairedValue = `${nextValue.slice(0, caret)}'${nextValue.slice(caret)}`;
|
||||
whereFilterInput.value = pairedValue;
|
||||
previousWhereFilterInputValue = pairedValue;
|
||||
nextTick(() => input.setSelectionRange(caret, caret));
|
||||
return;
|
||||
}
|
||||
const nextCaret = caretPositionInsideInsertedSqlSingleQuotes({
|
||||
previousValue: previousWhereFilterInputValue,
|
||||
nextValue,
|
||||
selectionStart: input.selectionStart,
|
||||
});
|
||||
previousWhereFilterInputValue = nextValue;
|
||||
if (nextCaret == null) return;
|
||||
nextTick(() => input.setSelectionRange(nextCaret, nextCaret));
|
||||
}
|
||||
|
||||
watch(whereFilterInput, (val) => {
|
||||
emit("update:whereInput", currentWhereInput() ?? "");
|
||||
persistStructuredFilterState();
|
||||
previousWhereFilterInputValue = val;
|
||||
whereSuggestions.value = [];
|
||||
if (!props.tableMeta?.columns?.length) return;
|
||||
const trimmed = val.trim();
|
||||
|
|
@ -6603,6 +6634,7 @@ const gridContextMenuItems = computed<ContextMenuItem[]>(() => {
|
|||
spellcheck="false"
|
||||
class="flex-1 h-5 min-w-0 text-xs bg-transparent outline-none placeholder:text-muted-foreground/60"
|
||||
placeholder=""
|
||||
@input="onWhereFilterInput"
|
||||
@keydown="onWhereFilterKeydown"
|
||||
@focus="showWhereHistorySuggestions"
|
||||
@click="updateWhereSuggestionPosition"
|
||||
|
|
|
|||
|
|
@ -0,0 +1,26 @@
|
|||
export interface SqlSingleQuoteCaretOptions {
|
||||
previousValue: string;
|
||||
nextValue: string;
|
||||
selectionStart: number | null | undefined;
|
||||
}
|
||||
|
||||
export function insertedSqlSingleQuoteAtCaret(options: SqlSingleQuoteCaretOptions): boolean {
|
||||
const { previousValue, nextValue, selectionStart } = options;
|
||||
if (typeof selectionStart !== "number" || selectionStart < 1) return false;
|
||||
if (nextValue.charAt(selectionStart - 1) !== "'") return false;
|
||||
return nextValue.slice(0, selectionStart - 1) + nextValue.slice(selectionStart) === previousValue;
|
||||
}
|
||||
|
||||
export function caretPositionInsideInsertedSqlSingleQuotes(options: SqlSingleQuoteCaretOptions): number | null {
|
||||
const { previousValue, nextValue, selectionStart } = options;
|
||||
if (typeof selectionStart !== "number" || selectionStart < 2) return null;
|
||||
if (nextValue.slice(selectionStart - 2, selectionStart) !== "''") return null;
|
||||
|
||||
const singleQuoteInsertPrevious = nextValue.slice(0, selectionStart - 1) + nextValue.slice(selectionStart);
|
||||
if (singleQuoteInsertPrevious === previousValue) return selectionStart - 1;
|
||||
|
||||
const pairInsertPrevious = nextValue.slice(0, selectionStart - 2) + nextValue.slice(selectionStart);
|
||||
if (pairInsertPrevious === previousValue) return selectionStart - 1;
|
||||
|
||||
return null;
|
||||
}
|
||||
|
|
@ -0,0 +1,63 @@
|
|||
import { strict as assert } from "node:assert";
|
||||
import { test } from "vitest";
|
||||
import { caretPositionInsideInsertedSqlSingleQuotes, insertedSqlSingleQuoteAtCaret } from "../../apps/desktop/src/lib/sqlQuoteCaret.ts";
|
||||
|
||||
test("detects a newly typed SQL single quote", () => {
|
||||
assert.equal(
|
||||
insertedSqlSingleQuoteAtCaret({
|
||||
previousValue: "name = ",
|
||||
nextValue: "name = '",
|
||||
selectionStart: 8,
|
||||
}),
|
||||
true,
|
||||
);
|
||||
assert.equal(
|
||||
insertedSqlSingleQuoteAtCaret({
|
||||
previousValue: "name = '",
|
||||
nextValue: "name = ''",
|
||||
selectionStart: 9,
|
||||
}),
|
||||
true,
|
||||
);
|
||||
});
|
||||
|
||||
test("moves the caret between SQL single quotes after typing the closing quote", () => {
|
||||
assert.equal(
|
||||
caretPositionInsideInsertedSqlSingleQuotes({
|
||||
previousValue: "name = '",
|
||||
nextValue: "name = ''",
|
||||
selectionStart: 9,
|
||||
}),
|
||||
8,
|
||||
);
|
||||
});
|
||||
|
||||
test("moves the caret between pasted SQL single quotes", () => {
|
||||
assert.equal(
|
||||
caretPositionInsideInsertedSqlSingleQuotes({
|
||||
previousValue: "name = ",
|
||||
nextValue: "name = ''",
|
||||
selectionStart: 9,
|
||||
}),
|
||||
8,
|
||||
);
|
||||
});
|
||||
|
||||
test("keeps the caret unchanged for ordinary quote edits", () => {
|
||||
assert.equal(
|
||||
caretPositionInsideInsertedSqlSingleQuotes({
|
||||
previousValue: "name = ",
|
||||
nextValue: "name = '",
|
||||
selectionStart: 8,
|
||||
}),
|
||||
null,
|
||||
);
|
||||
assert.equal(
|
||||
caretPositionInsideInsertedSqlSingleQuotes({
|
||||
previousValue: "name = 'a",
|
||||
nextValue: "name = 'a'",
|
||||
selectionStart: 10,
|
||||
}),
|
||||
null,
|
||||
);
|
||||
});
|
||||
Loading…
Reference in New Issue