{
+ setLineNumber(line);
+ setColumnNumber(col);
+ }}
onKeyDown={(e) => {
if (KEY_BINDING.run.match(e)) {
onRunClicked();
@@ -67,13 +92,23 @@ export default function QueryWindow() {
-
diff --git a/src/components/SqlEditor.tsx b/src/components/SqlEditor.tsx
index 8fe153e7..5f17390a 100644
--- a/src/components/SqlEditor.tsx
+++ b/src/components/SqlEditor.tsx
@@ -1,8 +1,11 @@
import { tags as t } from "@lezer/highlight";
import { createTheme } from "@uiw/codemirror-themes";
-import CodeMirror from "@uiw/react-codemirror";
+import CodeMirror, {
+ EditorView,
+ ReactCodeMirrorRef,
+} from "@uiw/react-codemirror";
import { sql, SQLite } from "@codemirror/lang-sql";
-import { KeyboardEventHandler, useMemo } from "react";
+import { forwardRef, KeyboardEventHandler, useMemo } from "react";
import { defaultKeymap } from "@codemirror/commands";
import { keymap } from "@codemirror/view";
@@ -45,48 +48,63 @@ interface SqlEditorProps {
onChange: (value: string) => void;
schema?: Record
;
onKeyDown?: KeyboardEventHandler;
+ onCursorChange?: (
+ pos: number,
+ lineNumber: number,
+ columnNumber: number
+ ) => void;
}
-export default function SqlEditor({
- value,
- onChange,
- schema,
- onKeyDown,
-}: SqlEditorProps) {
- const keyExtensions = useMemo(() => {
- return keymap.of([
- {
- key: KEY_BINDING.run.toCodeMirrorKey(),
- preventDefault: true,
- run: () => true,
- },
- ...defaultKeymap,
- ]);
- }, []);
+const SqlEditor = forwardRef(
+ function SqlEditor(
+ { value, onChange, schema, onKeyDown, onCursorChange }: SqlEditorProps,
+ ref
+ ) {
+ const keyExtensions = useMemo(() => {
+ return keymap.of([
+ {
+ key: KEY_BINDING.run.toCodeMirrorKey(),
+ preventDefault: true,
+ run: () => true,
+ },
+ ...defaultKeymap,
+ ]);
+ }, []);
- return (
-
- );
-}
+ return (
+ {
+ const pos = state.state.selection.main.head;
+ const line = state.state.doc.lineAt(pos);
+ const lineNumber = line.number;
+ const columnNumber = pos - line.from;
+ if (onCursorChange) onCursorChange(pos, lineNumber, columnNumber);
+ }),
+ ]}
+ />
+ );
+ }
+);
+
+export default SqlEditor;
diff --git a/src/components/result/ResultTable.tsx b/src/components/result/ResultTable.tsx
index 81d77deb..ecb9f137 100644
--- a/src/components/result/ResultTable.tsx
+++ b/src/components/result/ResultTable.tsx
@@ -86,7 +86,64 @@ export default function ResultTable({ data, tableName }: ResultTableProps) {
state: OptimizeTableState;
event: React.MouseEvent;
}) => {
+ const randomUUID = crypto.randomUUID();
+ const timestamp = Math.floor(Date.now() / 1000).toString();
+ const hasFocus = !!state.getFocus();
+
+ function setFocusValue(newValue: unknown) {
+ const focusCell = state.getFocus();
+ if (focusCell) {
+ state.changeValue(focusCell.y, focusCell.x, newValue);
+ }
+ }
+
openContextMenuFromEvent([
+ {
+ title: "Insert Value",
+ disabled: !hasFocus,
+ subWidth: 200,
+ sub: [
+ {
+ title: NULL
,
+ onClick: () => {
+ setFocusValue(null);
+ },
+ },
+ {
+ title: DEFAULT
,
+ onClick: () => {
+ setFocusValue(undefined);
+ },
+ },
+ { separator: true },
+ {
+ title: (
+
+ Unix Timestamp
+ {timestamp}
+
+ ),
+ onClick: () => {
+ setFocusValue(timestamp);
+ },
+ },
+ { separator: true },
+ {
+ title: (
+
+ UUID
+ {randomUUID}
+
+ ),
+ onClick: () => {
+ setFocusValue(randomUUID);
+ },
+ },
+ ],
+ },
+ {
+ separator: true,
+ },
{
title: "Copy Cell Value",
shortcut: KEY_BINDING.copy.toString(),
diff --git a/src/lib/sql-helper.test.ts b/src/lib/sql-helper.test.ts
index d5415274..4aea1e23 100644
--- a/src/lib/sql-helper.test.ts
+++ b/src/lib/sql-helper.test.ts
@@ -9,8 +9,10 @@ import {
generateDeleteStatement,
generateInsertStatement,
generateUpdateStatement,
+ selectStatementFromPosition,
unescapeIdentity,
} from "./sql-helper";
+import { identify } from "sql-query-identifier";
describe("Escape SQL", () => {
it("escape sql string", () => {
@@ -136,3 +138,29 @@ describe("Mapping sqlite column type to our table type", () => {
expect(convertSqliteType(type)).toBe(TableColumnDataType.REAL));
}
});
+
+function ss(sql: string) {
+ const pos = sql.indexOf("|");
+ const statements = identify(sql.replace("|", ""));
+ return selectStatementFromPosition(statements, pos);
+}
+
+describe("Select current query", () => {
+ it("select current query", () => {
+ expect(ss("select * from |t1; update t1 set name='visal';")?.text).toBe(
+ "select * from t1;"
+ );
+
+ expect(ss("select * from t1|; update t1 set name='visal';")?.text).toBe(
+ "select * from t1;"
+ );
+
+ expect(ss("select * from t1;| update t1 set name='visal';")?.text).toBe(
+ "select * from t1;"
+ );
+
+ expect(ss("select * from t1; update| t1 set name='visal';")?.text).toBe(
+ "update t1 set name='visal';"
+ );
+ });
+});
diff --git a/src/lib/sql-helper.ts b/src/lib/sql-helper.ts
index b5010288..6b5cd254 100644
--- a/src/lib/sql-helper.ts
+++ b/src/lib/sql-helper.ts
@@ -1,5 +1,6 @@
import { TableColumnDataType } from "@/app/(components)/OptimizeTable";
import { hex } from "./bit-operation";
+import { IdentifyResult } from "sql-query-identifier/lib/defines";
export function escapeIdentity(str: string) {
return `"${str.replace(/"/g, `""`)}"`;
@@ -123,3 +124,13 @@ export function generateUpdateStatement(
tableName
)} SET ${setPart} WHERE ${wherePart};`;
}
+
+export function selectStatementFromPosition(
+ statements: IdentifyResult[],
+ pos: number
+): IdentifyResult | undefined {
+ for (const statement of statements) {
+ if (statement.end + 1 >= pos) return statement;
+ }
+ return undefined;
+}
diff --git a/src/messages/openContextMenu.tsx b/src/messages/openContextMenu.tsx
index 2b6ab5ab..d0f13602 100644
--- a/src/messages/openContextMenu.tsx
+++ b/src/messages/openContextMenu.tsx
@@ -5,13 +5,14 @@ import { LucideIcon } from "lucide-react";
export type OpenContextMenuList = {
type?: "check";
checked?: boolean;
- title?: string;
+ title?: string | JSX.Element;
shortcut?: string;
separator?: boolean;
disabled?: boolean;
destructive?: boolean;
onClick?: () => void;
sub?: OpenContextMenuList;
+ subWidth?: number;
icon?: LucideIcon;
}[];