From 25fcf99e0c81b4007a782fab75a3b15385005a0b Mon Sep 17 00:00:00 2001 From: "Visal .In" Date: Wed, 17 Apr 2024 17:43:49 +0700 Subject: [PATCH] feat: improve code editor with better auto complete and coloring --- gui/package.json | 3 + gui/src/components/sql-editor/index.tsx | 34 +++++++++- .../sql-editor/sql-tablename-highlight.ts | 67 +++++++++++++++++++ gui/src/contexts/schema-provider.tsx | 7 +- gui/src/drivers/base-driver.ts | 1 + gui/src/drivers/sqlite-base-driver.ts | 10 ++- gui/src/index.css | 8 +++ pnpm-lock.yaml | 9 +++ 8 files changed, 136 insertions(+), 3 deletions(-) create mode 100644 gui/src/components/sql-editor/sql-tablename-highlight.ts diff --git a/gui/package.json b/gui/package.json index b4d82d82..a7680f89 100644 --- a/gui/package.json +++ b/gui/package.json @@ -36,8 +36,11 @@ "react-dom": "^18.2.0" }, "dependencies": { + "@codemirror/autocomplete": "^6.16.0", "@codemirror/commands": "^6.3.3", "@codemirror/lang-sql": "^6.5.5", + "@codemirror/language": "^6.10.1", + "@codemirror/state": "^6.4.1", "@codemirror/view": "^6.26.3", "@dnd-kit/core": "^6.1.0", "@dnd-kit/sortable": "^8.0.0", diff --git a/gui/src/components/sql-editor/index.tsx b/gui/src/components/sql-editor/index.tsx index 3b6199d5..f76d6e62 100644 --- a/gui/src/components/sql-editor/index.tsx +++ b/gui/src/components/sql-editor/index.tsx @@ -2,13 +2,19 @@ import CodeMirror, { EditorView, ReactCodeMirrorRef, } from "@uiw/react-codemirror"; +import { + acceptCompletion, + completionStatus, + startCompletion, +} from "@codemirror/autocomplete"; import { sql, SQLite } from "@codemirror/lang-sql"; import { forwardRef, KeyboardEventHandler, useMemo } from "react"; -import { defaultKeymap } from "@codemirror/commands"; +import { defaultKeymap, insertTab } from "@codemirror/commands"; import { keymap } from "@codemirror/view"; import { KEY_BINDING } from "@gui/lib/key-matcher"; import useCodeEditorTheme from "./use-editor-theme"; +import createSQLTableNameHighlightPlugin from "./sql-tablename-highlight"; interface SqlEditorProps { value: string; @@ -37,6 +43,13 @@ const SqlEditor = forwardRef( ) { const theme = useCodeEditorTheme(); + const tableNameHighlightPlugin = useMemo(() => { + if (schema) { + return createSQLTableNameHighlightPlugin(Object.keys(schema)); + } + return createSQLTableNameHighlightPlugin([]); + }, [schema]); + const keyExtensions = useMemo(() => { return keymap.of([ { @@ -44,6 +57,24 @@ const SqlEditor = forwardRef( preventDefault: true, run: () => true, }, + { + key: "Tab", + preventDefault: true, + run: (target) => { + if (completionStatus(target.state) === "active") { + acceptCompletion(target); + } else { + insertTab(target); + } + return true; + }, + }, + { + key: "Ctrl-Space", + mac: "Cmd-i", + preventDefault: true, + run: startCompletion, + }, ...defaultKeymap, ]); }, []); @@ -72,6 +103,7 @@ const SqlEditor = forwardRef( dialect: SQLite, schema, }), + tableNameHighlightPlugin, EditorView.updateListener.of((state) => { const pos = state.state.selection.main.head; const line = state.state.doc.lineAt(pos); diff --git a/gui/src/components/sql-editor/sql-tablename-highlight.ts b/gui/src/components/sql-editor/sql-tablename-highlight.ts new file mode 100644 index 00000000..70fde33a --- /dev/null +++ b/gui/src/components/sql-editor/sql-tablename-highlight.ts @@ -0,0 +1,67 @@ +import { + EditorView, + ViewPlugin, + Decoration, + DecorationSet, + ViewUpdate, +} from "@codemirror/view"; +import { Range } from "@codemirror/state"; +import { syntaxTree } from "@codemirror/language"; + +const underlineMark = Decoration.mark({ class: "cm-table-name" }); + +export default function createSQLTableNameHighlightPlugin( + tableNameList: string[] +) { + const tableNameSet = new Set( + tableNameList.map((table) => table.toLowerCase()) + ); + + function highlightTableName(view: EditorView) { + const decorationList: Range[] = []; + + for (const { from, to } of view.visibleRanges) { + syntaxTree(view.state).iterate({ + from, + to, + enter: (node) => { + if (node.name == "Identifier") { + const word = view.state.doc + .sliceString(node.from, node.to) + .toLowerCase(); + + const lastChar = node.node.prevSibling + ? view.state.doc + .sliceString( + node.node.prevSibling.from, + node.node.prevSibling.to + ) + .toLowerCase() + : ""; + + if (tableNameSet.has(word) && lastChar !== ".") { + decorationList.push(underlineMark.range(node.from, node.to)); + } + } + }, + }); + } + + return Decoration.set(decorationList); + } + + return ViewPlugin.fromClass( + class { + decorations: DecorationSet; + + constructor(view: EditorView) { + this.decorations = highlightTableName(view); + } + + update(update: ViewUpdate) { + this.decorations = highlightTableName(update.view); + } + }, + { decorations: (v) => v.decorations } + ); +} diff --git a/gui/src/contexts/schema-provider.tsx b/gui/src/contexts/schema-provider.tsx index f632e01e..dcda8184 100644 --- a/gui/src/contexts/schema-provider.tsx +++ b/gui/src/contexts/schema-provider.tsx @@ -27,7 +27,7 @@ export function useSchema() { } export function SchemaProvider({ children }: Readonly) { - const { updateTableList } = useAutoComplete(); + const { updateTableList, updateTableSchema } = useAutoComplete(); const [error, setError] = useState(); const [schemaItems, setSchemaItems] = useState([]); const [loading, setLoading] = useState(true); @@ -49,6 +49,11 @@ export function SchemaProvider({ children }: Readonly) { setSchemaItems(sortedTableList); updateTableList(tableList.map((table) => table.name)); + for (const table of tableList) { + if (table.tableSchema) { + updateTableSchema(table.name, table.tableSchema.columns); + } + } setError(undefined); setLoading(false); diff --git a/gui/src/drivers/base-driver.ts b/gui/src/drivers/base-driver.ts index 284604aa..e2ca6f17 100644 --- a/gui/src/drivers/base-driver.ts +++ b/gui/src/drivers/base-driver.ts @@ -56,6 +56,7 @@ export interface DatabaseSchemaItem { type: "table" | "trigger" | "view"; name: string; tableName?: string; + tableSchema?: DatabaseTableSchema; } export interface DatabaseTableColumn { diff --git a/gui/src/drivers/sqlite-base-driver.ts b/gui/src/drivers/sqlite-base-driver.ts index b8f67ef0..a31b19c5 100644 --- a/gui/src/drivers/sqlite-base-driver.ts +++ b/gui/src/drivers/sqlite-base-driver.ts @@ -46,7 +46,15 @@ export abstract class SqliteLikeBaseDriver extends BaseDriver { for (const row of rows) { if (row.type === "table") { - tmp.push({ type: "table", name: row.name }); + try { + tmp.push({ + type: "table", + name: row.name, + tableSchema: parseCreateTableScript(row.sql), + }); + } catch { + tmp.push({ type: "table", name: row.name }); + } } else if (row.type === "trigger") { tmp.push({ type: "trigger", name: row.name, tableName: row.tbl_name }); } else if (row.type === "view") { diff --git a/gui/src/index.css b/gui/src/index.css index c5b73da0..5e2c261d 100644 --- a/gui/src/index.css +++ b/gui/src/index.css @@ -98,6 +98,14 @@ } } +/* ------------------------------- */ +.cm-table-name { + color: #e84393; +} + +.dark .cm-table-name { + color: #fd79a8; +} /* ------------------------------- */ .libsql-window-tab .libsql-window-close { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2939e35a..ec2218e1 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -64,12 +64,21 @@ importers: gui: dependencies: + '@codemirror/autocomplete': + specifier: ^6.16.0 + version: 6.16.0(@codemirror/language@6.10.1)(@codemirror/state@6.4.1)(@codemirror/view@6.26.3)(@lezer/common@1.2.1) '@codemirror/commands': specifier: ^6.3.3 version: 6.3.3 '@codemirror/lang-sql': specifier: ^6.5.5 version: 6.6.3(@codemirror/view@6.26.3) + '@codemirror/language': + specifier: ^6.10.1 + version: 6.10.1 + '@codemirror/state': + specifier: ^6.4.1 + version: 6.4.1 '@codemirror/view': specifier: ^6.26.3 version: 6.26.3