From 5fe1e041baeda63ea0e6f81a074520abb4fc51e8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Victor=20Jes=C3=BAs=20Rosario=20V=C3=A1squez?= <46900196+Victor1890@users.noreply.github.com> Date: Sat, 12 Oct 2024 23:17:43 -0400 Subject: [PATCH 1/5] feat: Add monaco-sql-languages package This commit adds the "monaco-sql-languages" package to the project's dependencies. This package provides support for SQL language features in the Monaco editor. It is added to the "ui/package.json" file. --- ui/package-lock.json | 49 +++++++++++++- ui/package.json | 1 + ui/src/components/editor.config.ts | 101 +++++++++++++++++++++++++++++ ui/src/components/editor.hook.ts | 49 ++++++++++++++ ui/src/components/editor.tsx | 93 +++++++++++++++++++++++++- ui/src/routeTree.gen.ts | 68 +++++++++---------- 6 files changed, 323 insertions(+), 38 deletions(-) create mode 100644 ui/src/components/editor.config.ts create mode 100644 ui/src/components/editor.hook.ts diff --git a/ui/package-lock.json b/ui/package-lock.json index 9be3a4a..f1f4683 100644 --- a/ui/package-lock.json +++ b/ui/package-lock.json @@ -24,6 +24,7 @@ "class-variance-authority": "^0.7.0", "clsx": "^2.1.1", "lucide-react": "^0.394.0", + "monaco-sql-languages": "0.12.2", "react": "^18.2.0", "react-code-blocks": "^0.1.6", "react-data-grid": "7.0.0-beta.44", @@ -3972,6 +3973,31 @@ "node": ">=4" } }, + "node_modules/antlr4-c3": { + "version": "3.3.7", + "resolved": "https://registry.npmjs.org/antlr4-c3/-/antlr4-c3-3.3.7.tgz", + "integrity": "sha512-F3ndE38wwA6z6AjUbL3heSdEGl4TxulGDPf9xB0/IY4dbRHWBh6XNaqFwur8vHKQk9FS5yNABHeg2wqlqIYO0w==", + "dependencies": { + "antlr4ng": "2.0.11" + } + }, + "node_modules/antlr4ng": { + "version": "2.0.11", + "resolved": "https://registry.npmjs.org/antlr4ng/-/antlr4ng-2.0.11.tgz", + "integrity": "sha512-9jM91VVtHSqHkAHQsXHaoaiewFETMvUTI1/tXvwTiFw4f7zke3IGlwEyoKN9NS0FqIwDKFvUNW2e1cKPniTkVQ==", + "peerDependencies": { + "antlr4ng-cli": "1.0.7" + } + }, + "node_modules/antlr4ng-cli": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/antlr4ng-cli/-/antlr4ng-cli-1.0.7.tgz", + "integrity": "sha512-qN2FsDBmLvsQcA5CWTrPz8I8gNXeS1fgXBBhI78VyxBSBV/EJgqy8ks6IDTC9jyugpl40csCQ4sL5K4i2YZ/2w==", + "peer": true, + "bin": { + "antlr4ng": "index.js" + } + }, "node_modules/any-promise": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", @@ -4564,6 +4590,15 @@ "csstype": "^3.0.2" } }, + "node_modules/dt-sql-parser": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/dt-sql-parser/-/dt-sql-parser-4.0.2.tgz", + "integrity": "sha512-8D/kfYLW+wgz7Cwf5K+OCtex7QHiCyIuI18pw0a5vjSXRKCpfQqNQeG7tU5vp4D0RQEZJiMBuKJPBYwoqWxoAA==", + "dependencies": { + "antlr4-c3": "3.3.7", + "antlr4ng": "2.0.11" + } + }, "node_modules/eastasianwidth": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", @@ -5694,8 +5729,18 @@ "node_modules/monaco-editor": { "version": "0.49.0", "resolved": "https://registry.npmjs.org/monaco-editor/-/monaco-editor-0.49.0.tgz", - "integrity": "sha512-2I8/T3X/hLxB2oPHgqcNYUVdA/ZEFShT7IAujifIPMfKkNbLOqY8XCoyHCXrsdjb36dW9MwoTwBCFpXKMwNwaQ==", - "dev": true + "integrity": "sha512-2I8/T3X/hLxB2oPHgqcNYUVdA/ZEFShT7IAujifIPMfKkNbLOqY8XCoyHCXrsdjb36dW9MwoTwBCFpXKMwNwaQ==" + }, + "node_modules/monaco-sql-languages": { + "version": "0.12.2", + "resolved": "https://registry.npmjs.org/monaco-sql-languages/-/monaco-sql-languages-0.12.2.tgz", + "integrity": "sha512-FNo/9FLF9JqYmdSBjQtnCCxg+Bo9hGCRDIatEX+0XUB6zvZSis7JJX7yq64kENqL7KwmX4N2ILkhbBxX5F9wyw==", + "dependencies": { + "dt-sql-parser": "4.0.2" + }, + "peerDependencies": { + "monaco-editor": ">=0.31.0" + } }, "node_modules/ms": { "version": "2.1.2", diff --git a/ui/package.json b/ui/package.json index 877f49b..aa3d03d 100644 --- a/ui/package.json +++ b/ui/package.json @@ -27,6 +27,7 @@ "class-variance-authority": "^0.7.0", "clsx": "^2.1.1", "lucide-react": "^0.394.0", + "monaco-sql-languages": "0.12.2", "react": "^18.2.0", "react-code-blocks": "^0.1.6", "react-data-grid": "7.0.0-beta.44", diff --git a/ui/src/components/editor.config.ts b/ui/src/components/editor.config.ts new file mode 100644 index 0000000..4eb5d2b --- /dev/null +++ b/ui/src/components/editor.config.ts @@ -0,0 +1,101 @@ +import * as monaco from "monaco-editor/esm/vs/editor/editor.api"; + +export const ID_LANGUAGE_SQL = "sql"; + +export const COMMAND_CONFIG: monaco.languages.LanguageConfiguration = { + comments: { + lineComment: "--", + blockComment: ["/*", "*/"], + }, + brackets: [ + ["{", "}"], + ["[", "]"], + ["(", ")"], + ], + autoClosingPairs: [ + { open: "{", close: "}" }, + { open: "[", close: "]" }, + { open: "(", close: ")" }, + { open: '"', close: '"' }, + { open: "'", close: "'" }, + ], + surroundingPairs: [ + { open: '"', close: '"' }, + { open: "'", close: "'" }, + ], + wordPattern: /(-?\d*\.\d\w*)|([a-zA-Z_]\w*)/g, + indentationRules: { + increaseIndentPattern: /(\{|\[|\()/, + decreaseIndentPattern: /(\}|\]|\))/, + }, +}; + +export const autoSuggestionCompletionItems = ( + range: monaco.languages.CompletionItem['range'], +): monaco.languages.CompletionList => { + // const word = model.getWordUntilPosition(position); + // const range = { + // startLineNumber: position.lineNumber, + // endLineNumber: position.lineNumber, + // startColumn: word.startColumn, + // endColumn: word.endColumn, + // }; + const suggestions = [ + { label: "SELECT", kind: monaco.languages.CompletionItemKind.Keyword, insertText: "SELECT ", range }, + { label: "FROM", kind: monaco.languages.CompletionItemKind.Keyword, insertText: "FROM ", range }, + { label: "WHERE", kind: monaco.languages.CompletionItemKind.Keyword, insertText: "WHERE ", range }, + { label: "GROUP BY", kind: monaco.languages.CompletionItemKind.Keyword, insertText: "GROUP BY ", range }, + { label: "HAVING", kind: monaco.languages.CompletionItemKind.Keyword, insertText: "HAVING ", range }, + { label: "ORDER BY", kind: monaco.languages.CompletionItemKind.Keyword, insertText: "ORDER BY ", range }, + { label: "LIMIT", kind: monaco.languages.CompletionItemKind.Keyword, insertText: "LIMIT ", range }, + { label: "AND", kind: monaco.languages.CompletionItemKind.Keyword, insertText: "AND ", range }, + { label: "OR", kind: monaco.languages.CompletionItemKind.Keyword, insertText: "OR ", range }, + { label: "NOT", kind: monaco.languages.CompletionItemKind.Keyword, insertText: "NOT ", range }, + { label: "BETWEEN", kind: monaco.languages.CompletionItemKind.Keyword, insertText: "BETWEEN ", range }, + { label: "IN", kind: monaco.languages.CompletionItemKind.Keyword, insertText: "IN ", range }, + { label: "LIKE", kind: monaco.languages.CompletionItemKind.Keyword, insertText: "LIKE ", range }, + { label: "IS NULL", kind: monaco.languages.CompletionItemKind.Keyword, insertText: "IS NULL ", range }, + { label: "IS NOT NULL", kind: monaco.languages.CompletionItemKind.Keyword, insertText: "IS NOT NULL ", range }, + { label: "INNER JOIN", kind: monaco.languages.CompletionItemKind.Keyword, insertText: "INNER JOIN ", range }, + { label: "LEFT JOIN", kind: monaco.languages.CompletionItemKind.Keyword, insertText: "LEFT JOIN ", range }, + { label: "RIGHT JOIN", kind: monaco.languages.CompletionItemKind.Keyword, insertText: "RIGHT JOIN ", range }, + { label: "FULL JOIN", kind: monaco.languages.CompletionItemKind.Keyword, insertText: "FULL JOIN ", range }, + { label: "ON", kind: monaco.languages.CompletionItemKind.Keyword, insertText: "ON ", range }, + { label: "AS", kind: monaco.languages.CompletionItemKind.Keyword, insertText: "AS ", range }, + { label: "COUNT", kind: monaco.languages.CompletionItemKind.Function, insertText: "COUNT()", range }, + { label: "SUM", kind: monaco.languages.CompletionItemKind.Function, insertText: "SUM()", range }, + { label: "AVG", kind: monaco.languages.CompletionItemKind.Function, insertText: "AVG()", range }, + { label: "MIN", kind: monaco.languages.CompletionItemKind.Function, insertText: "MIN()", range }, + { label: "MAX", kind: monaco.languages.CompletionItemKind.Function, insertText: "MAX()", range }, + { label: "CAST", kind: monaco.languages.CompletionItemKind.Function, insertText: "CAST()", range }, + { label: "DATE", kind: monaco.languages.CompletionItemKind.Function, insertText: "DATE()", range }, + { label: "NOW", kind: monaco.languages.CompletionItemKind.Function, insertText: "NOW()", range }, + { label: "SELECT", kind: monaco.languages.CompletionItemKind.Keyword, insertText: "SELECT ", range }, + { label: "FROM", kind: monaco.languages.CompletionItemKind.Keyword, insertText: "FROM ", range }, + { label: "WHERE", kind: monaco.languages.CompletionItemKind.Keyword, insertText: "WHERE ", range }, + { label: "JOIN", kind: monaco.languages.CompletionItemKind.Keyword, insertText: "JOIN ", range }, + { label: "INSERT INTO", kind: monaco.languages.CompletionItemKind.Keyword, insertText: "INSERT INTO ", range }, + { label: "UPDATE", kind: monaco.languages.CompletionItemKind.Keyword, insertText: "UPDATE ", range }, + { label: "DELETE", kind: monaco.languages.CompletionItemKind.Keyword, insertText: "DELETE ", range }, + { label: "CREATE TABLE", kind: monaco.languages.CompletionItemKind.Snippet, insertText: "CREATE TABLE ", range }, + { label: "DROP TABLE", kind: monaco.languages.CompletionItemKind.Snippet, insertText: "DROP TABLE ", range }, + { label: "PRAGMA", kind: monaco.languages.CompletionItemKind.Keyword, insertText: "PRAGMA ", range }, + { label: "VACUUM", kind: monaco.languages.CompletionItemKind.Keyword, insertText: "VACUUM;", range }, + { label: "ATTACH DATABASE", kind: monaco.languages.CompletionItemKind.Snippet, insertText: "ATTACH DATABASE '' AS '';", range }, + { label: "SERIAL", kind: monaco.languages.CompletionItemKind.Keyword, insertText: "SERIAL ", range }, + { label: "RETURNING", kind: monaco.languages.CompletionItemKind.Keyword, insertText: "RETURNING ", range }, + { label: "CREATE EXTENSION", kind: monaco.languages.CompletionItemKind.Snippet, insertText: "CREATE EXTENSION ", range }, + { label: "AUTO_INCREMENT", kind: monaco.languages.CompletionItemKind.Keyword, insertText: "AUTO_INCREMENT ", range }, + { label: "ENGINE", kind: monaco.languages.CompletionItemKind.Keyword, insertText: "ENGINE=", range }, + { label: "SHOW DATABASES", kind: monaco.languages.CompletionItemKind.Snippet, insertText: "SHOW DATABASES;", range }, + { label: "SHOW TABLES", kind: monaco.languages.CompletionItemKind.Snippet, insertText: "SHOW TABLES;", range }, + { label: "COPY", kind: monaco.languages.CompletionItemKind.Keyword, insertText: "COPY ", range }, + { label: "EXPORT DATABASE", kind: monaco.languages.CompletionItemKind.Snippet, insertText: "EXPORT DATABASE '';", range }, + { label: "IMPORT DATABASE", kind: monaco.languages.CompletionItemKind.Snippet, insertText: "IMPORT DATABASE '';", range }, + { label: "CREATE MATERIALIZED VIEW", kind: monaco.languages.CompletionItemKind.Snippet, insertText: "CREATE MATERIALIZED VIEW ", range }, + { label: "OPTIMIZE TABLE", kind: monaco.languages.CompletionItemKind.Keyword, insertText: "OPTIMIZE TABLE ", range }, + { label: "ALTER TABLE", kind: monaco.languages.CompletionItemKind.Snippet, insertText: "ALTER TABLE ", range }, + ]; + + return { suggestions }; +}; diff --git a/ui/src/components/editor.hook.ts b/ui/src/components/editor.hook.ts new file mode 100644 index 0000000..e773fd0 --- /dev/null +++ b/ui/src/components/editor.hook.ts @@ -0,0 +1,49 @@ +import { fetchTableData, fetchTables } from "@/api"; +import { useQuery } from "@tanstack/react-query"; +import { useEffect, useState } from "react"; + +export function useGetAllTables() { + const query = useQuery({ + queryKey: ["tables", name], + queryFn: () => fetchTables(), + }); + + return query +} + +export function useGetTable() { + + const [tables, handleSetTables] = useState([]) + const [columns, setColumns] = useState([]) + + const fetchData = async (data: string[]): Promise => { + + const table = data.pop() + if (!table) return + + const info = await fetchTableData(table, 1).catch(() => null) + if (!info) return fetchData(data); + + setColumns(prev => { + const isExist = prev.some((column) => info.columns.includes(column)); + if (isExist) return prev; + return [...prev, ...info.columns]; + }) + + return fetchData(data); + } + + // useEffect(() => { + + // if (!tables) return + + // fetchData([...tables]) + + // }, [tables]) + + return { + tables, + columns, + handleSetTables + } +} \ No newline at end of file diff --git a/ui/src/components/editor.tsx b/ui/src/components/editor.tsx index a68392c..5fd024a 100644 --- a/ui/src/components/editor.tsx +++ b/ui/src/components/editor.tsx @@ -1,8 +1,15 @@ import "@/editorWorker"; import * as monaco from "monaco-editor/esm/vs/editor/editor.api"; +import { vsPlusTheme } from "monaco-sql-languages"; import { FunctionComponent, useEffect, useRef, useState } from "react"; import { useTheme } from "@/provider/theme.provider"; +import { + COMMAND_CONFIG, + ID_LANGUAGE_SQL, + autoSuggestionCompletionItems, +} from "./editor.config"; +import { useGetAllTables, useGetTable } from "./editor.hook"; import { Card } from "./ui/card"; type Props = { @@ -16,14 +23,26 @@ export const Editor: FunctionComponent = ({ value, onChange }) => { useState(null); const monacoEl = useRef(null); + const { data: dataTable } = useGetAllTables(); + const { columns, handleSetTables } = useGetTable(); + useEffect(() => { if (monacoEl) { setEditor((editor) => { if (editor) return editor; + monaco.languages.register({ id: ID_LANGUAGE_SQL }); + monaco.languages.setLanguageConfiguration( + ID_LANGUAGE_SQL, + COMMAND_CONFIG + ); + + monaco.editor.defineTheme("sql-dark", vsPlusTheme.darkThemeData); + monaco.editor.defineTheme("sql-light", vsPlusTheme.lightThemeData); + const newEditor = monaco.editor.create(monacoEl.current!, { value, - language: "sql", + language: ID_LANGUAGE_SQL, minimap: { enabled: false, }, @@ -48,12 +67,82 @@ export const Editor: FunctionComponent = ({ value, onChange }) => { return () => editor?.dispose(); }, [monacoEl.current]); + useEffect(() => { + if (!monacoEl.current) return; + + monaco.languages.registerCompletionItemProvider(ID_LANGUAGE_SQL, { + provideCompletionItems: (model, position) => { + const word = model.getWordUntilPosition(position); + const range = { + startLineNumber: position.lineNumber, + endLineNumber: position.lineNumber, + startColumn: word.startColumn, + endColumn: word.endColumn, + }; + const { suggestions } = autoSuggestionCompletionItems(range); + + const tables = dataTable?.tables.map((table) => table.name) || []; + const tableSuggestions = tables.map((table) => ({ + label: table, + kind: monaco.languages.CompletionItemKind.Variable, + insertText: `"${table}"`, + range, + })); + + console.log("tableSuggestions: ", tableSuggestions); + + // TODO: Implement column suggestions + const columnSuggestions = columns.map((column) => ({ + label: column, + kind: monaco.languages.CompletionItemKind.Variable, + insertText: column, + range, + })); + + // TODO: Implement table column suggestions + const tableColumnSuggestions = tables.flatMap((table) => + columns.map((column) => ({ + label: `${table}.${column}`, + kind: monaco.languages.CompletionItemKind.Variable, + insertText: `${table}.${column}`, + range, + })) + ); + + const allSuggestions = [ + ...suggestions, + ...tableSuggestions, + // ...columnSuggestions, + // ...tableColumnSuggestions, + ]; + + console.log("allSuggestions: ", allSuggestions); + + return { suggestions: allSuggestions }; + }, + }); + }, [monacoEl.current, dataTable, columns]); + useEffect(() => { if (monacoEl.current) { - monaco.editor.setTheme(currentTheme === "light" ? "vs" : "vs-dark"); + monaco.editor.setTheme( + currentTheme === "light" ? "sql-light" : "sql-dark" + ); } }, [currentTheme]); + useEffect(() => { + if (!dataTable) return; + + const tables = dataTable.tables.map((table) => table.name); + handleSetTables((prev) => { + if (prev.length === 0) return prev; + const isExist = prev.some((table) => tables.includes(table)); + if (isExist) return prev; + return [...prev, ...tables]; + }); + }, [dataTable]); + return (
diff --git a/ui/src/routeTree.gen.ts b/ui/src/routeTree.gen.ts index 16ec80c..4be1bc8 100644 --- a/ui/src/routeTree.gen.ts +++ b/ui/src/routeTree.gen.ts @@ -8,60 +8,60 @@ // This file is auto-generated by TanStack Router -import { createFileRoute } from "@tanstack/react-router"; +import { createFileRoute } from '@tanstack/react-router' // Import Routes -import { Route as rootRoute } from "./routes/__root"; +import { Route as rootRoute } from './routes/__root' // Create Virtual Routes -const TablesLazyImport = createFileRoute("/tables")(); -const QueryLazyImport = createFileRoute("/query")(); -const IndexLazyImport = createFileRoute("/")(); +const TablesLazyImport = createFileRoute('/tables')() +const QueryLazyImport = createFileRoute('/query')() +const IndexLazyImport = createFileRoute('/')() // Create/Update Routes const TablesLazyRoute = TablesLazyImport.update({ - path: "/tables", + path: '/tables', getParentRoute: () => rootRoute, -} as any).lazy(() => import("./routes/tables.lazy").then((d) => d.Route)); +} as any).lazy(() => import('./routes/tables.lazy').then((d) => d.Route)) const QueryLazyRoute = QueryLazyImport.update({ - path: "/query", + path: '/query', getParentRoute: () => rootRoute, -} as any).lazy(() => import("./routes/query.lazy").then((d) => d.Route)); +} as any).lazy(() => import('./routes/query.lazy').then((d) => d.Route)) const IndexLazyRoute = IndexLazyImport.update({ - path: "/", + path: '/', getParentRoute: () => rootRoute, -} as any).lazy(() => import("./routes/index.lazy").then((d) => d.Route)); +} as any).lazy(() => import('./routes/index.lazy').then((d) => d.Route)) // Populate the FileRoutesByPath interface -declare module "@tanstack/react-router" { +declare module '@tanstack/react-router' { interface FileRoutesByPath { - "/": { - id: "/"; - path: "/"; - fullPath: "/"; - preLoaderRoute: typeof IndexLazyImport; - parentRoute: typeof rootRoute; - }; - "/query": { - id: "/query"; - path: "/query"; - fullPath: "/query"; - preLoaderRoute: typeof QueryLazyImport; - parentRoute: typeof rootRoute; - }; - "/tables": { - id: "/tables"; - path: "/tables"; - fullPath: "/tables"; - preLoaderRoute: typeof TablesLazyImport; - parentRoute: typeof rootRoute; - }; + '/': { + id: '/' + path: '/' + fullPath: '/' + preLoaderRoute: typeof IndexLazyImport + parentRoute: typeof rootRoute + } + '/query': { + id: '/query' + path: '/query' + fullPath: '/query' + preLoaderRoute: typeof QueryLazyImport + parentRoute: typeof rootRoute + } + '/tables': { + id: '/tables' + path: '/tables' + fullPath: '/tables' + preLoaderRoute: typeof TablesLazyImport + parentRoute: typeof rootRoute + } } } @@ -71,7 +71,7 @@ export const routeTree = rootRoute.addChildren({ IndexLazyRoute, QueryLazyRoute, TablesLazyRoute, -}); +}) /* prettier-ignore-end */ From fff737bd5e1e40e09d3f0f0839553e81b6f44d3f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Victor=20Jes=C3=BAs=20Rosario=20V=C3=A1squez?= <46900196+Victor1890@users.noreply.github.com> Date: Sun, 13 Oct 2024 18:07:22 -0400 Subject: [PATCH 2/5] refactor: Update monaco-sql-languages package and add completion item provider This commit refactors the code in the editor component to update the monaco-sql-languages package and add a completion item provider for SQL language. The completion item provider provides suggestions for auto-completion in the editor. The implementation currently includes suggestions for auto-suggestions, but the tables, columns, and tables with columns suggestions are yet to be implemented. This commit also removes the unused code related to tables and columns suggestions. --- ui/src/components/editor.tsx | 109 +++++++++++++++++------------------ 1 file changed, 53 insertions(+), 56 deletions(-) diff --git a/ui/src/components/editor.tsx b/ui/src/components/editor.tsx index 5fd024a..06a76f0 100644 --- a/ui/src/components/editor.tsx +++ b/ui/src/components/editor.tsx @@ -40,6 +40,59 @@ export const Editor: FunctionComponent = ({ value, onChange }) => { monaco.editor.defineTheme("sql-dark", vsPlusTheme.darkThemeData); monaco.editor.defineTheme("sql-light", vsPlusTheme.lightThemeData); + monaco.languages.registerCompletionItemProvider(ID_LANGUAGE_SQL, { + provideCompletionItems: (model, position) => { + const word = model.getWordUntilPosition(position); + const range = { + startLineNumber: position.lineNumber, + endLineNumber: position.lineNumber, + startColumn: word.startColumn, + endColumn: word.endColumn, + }; + const { suggestions } = autoSuggestionCompletionItems(range); + + + // TODO: Implement tables, columns and tables with columns suggestions + + // const tables = dataTable?.tables.map((table) => table.name) || []; + // const tableSuggestions = tables.map((table) => ({ + // label: table, + // kind: monaco.languages.CompletionItemKind.Variable, + // insertText: `"${table}"`, + // range, + // })); + + // console.log("tableSuggestions: ", tableSuggestions); + + // const columnSuggestions = columns.map((column) => ({ + // label: column, + // kind: monaco.languages.CompletionItemKind.Variable, + // insertText: column, + // range, + // })); + + // const tableColumnSuggestions = tables.flatMap((table) => + // columns.map((column) => ({ + // label: `${table}.${column}`, + // kind: monaco.languages.CompletionItemKind.Variable, + // insertText: `${table}.${column}`, + // range, + // })) + // ); + + // const allSuggestions = [ + // ...suggestions, + // ...tableSuggestions, + // // ...columnSuggestions, + // // ...tableColumnSuggestions, + // ]; + + // console.log("allSuggestions: ", allSuggestions); + + return { suggestions }; + }, + }); + const newEditor = monaco.editor.create(monacoEl.current!, { value, language: ID_LANGUAGE_SQL, @@ -67,62 +120,6 @@ export const Editor: FunctionComponent = ({ value, onChange }) => { return () => editor?.dispose(); }, [monacoEl.current]); - useEffect(() => { - if (!monacoEl.current) return; - - monaco.languages.registerCompletionItemProvider(ID_LANGUAGE_SQL, { - provideCompletionItems: (model, position) => { - const word = model.getWordUntilPosition(position); - const range = { - startLineNumber: position.lineNumber, - endLineNumber: position.lineNumber, - startColumn: word.startColumn, - endColumn: word.endColumn, - }; - const { suggestions } = autoSuggestionCompletionItems(range); - - const tables = dataTable?.tables.map((table) => table.name) || []; - const tableSuggestions = tables.map((table) => ({ - label: table, - kind: monaco.languages.CompletionItemKind.Variable, - insertText: `"${table}"`, - range, - })); - - console.log("tableSuggestions: ", tableSuggestions); - - // TODO: Implement column suggestions - const columnSuggestions = columns.map((column) => ({ - label: column, - kind: monaco.languages.CompletionItemKind.Variable, - insertText: column, - range, - })); - - // TODO: Implement table column suggestions - const tableColumnSuggestions = tables.flatMap((table) => - columns.map((column) => ({ - label: `${table}.${column}`, - kind: monaco.languages.CompletionItemKind.Variable, - insertText: `${table}.${column}`, - range, - })) - ); - - const allSuggestions = [ - ...suggestions, - ...tableSuggestions, - // ...columnSuggestions, - // ...tableColumnSuggestions, - ]; - - console.log("allSuggestions: ", allSuggestions); - - return { suggestions: allSuggestions }; - }, - }); - }, [monacoEl.current, dataTable, columns]); - useEffect(() => { if (monacoEl.current) { monaco.editor.setTheme( From e1d7f7b9fd2686ea30823d2d7eeae088b6e18d6a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Victor=20Jes=C3=BAs=20Rosario=20V=C3=A1squez?= <46900196+Victor1890@users.noreply.github.com> Date: Sat, 19 Oct 2024 20:15:45 -0400 Subject: [PATCH 3/5] refactor: :fire: remove unused editor hook --- ui/src/components/editor.hook.ts | 49 -------------------------------- 1 file changed, 49 deletions(-) delete mode 100644 ui/src/components/editor.hook.ts diff --git a/ui/src/components/editor.hook.ts b/ui/src/components/editor.hook.ts deleted file mode 100644 index e773fd0..0000000 --- a/ui/src/components/editor.hook.ts +++ /dev/null @@ -1,49 +0,0 @@ -import { fetchTableData, fetchTables } from "@/api"; -import { useQuery } from "@tanstack/react-query"; -import { useEffect, useState } from "react"; - -export function useGetAllTables() { - const query = useQuery({ - queryKey: ["tables", name], - queryFn: () => fetchTables(), - }); - - return query -} - -export function useGetTable() { - - const [tables, handleSetTables] = useState([]) - const [columns, setColumns] = useState([]) - - const fetchData = async (data: string[]): Promise => { - - const table = data.pop() - if (!table) return - - const info = await fetchTableData(table, 1).catch(() => null) - if (!info) return fetchData(data); - - setColumns(prev => { - const isExist = prev.some((column) => info.columns.includes(column)); - if (isExist) return prev; - return [...prev, ...info.columns]; - }) - - return fetchData(data); - } - - // useEffect(() => { - - // if (!tables) return - - // fetchData([...tables]) - - // }, [tables]) - - return { - tables, - columns, - handleSetTables - } -} \ No newline at end of file From 3716febd66510459a3478aee48ac40bd5e8be84d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Victor=20Jes=C3=BAs=20Rosario=20V=C3=A1squez?= <46900196+Victor1890@users.noreply.github.com> Date: Sat, 19 Oct 2024 21:18:04 -0400 Subject: [PATCH 4/5] refactor: :recycle: remove duplicate suggestions in autoSuggestionCompletionItems --- ui/src/components/editor.config.ts | 17 ++++++----------- ui/src/routes/query.lazy.tsx | 9 +-------- 2 files changed, 7 insertions(+), 19 deletions(-) diff --git a/ui/src/components/editor.config.ts b/ui/src/components/editor.config.ts index 4eb5d2b..ecd2bc5 100644 --- a/ui/src/components/editor.config.ts +++ b/ui/src/components/editor.config.ts @@ -33,14 +33,7 @@ export const COMMAND_CONFIG: monaco.languages.LanguageConfiguration = { export const autoSuggestionCompletionItems = ( range: monaco.languages.CompletionItem['range'], ): monaco.languages.CompletionList => { - // const word = model.getWordUntilPosition(position); - // const range = { - // startLineNumber: position.lineNumber, - // endLineNumber: position.lineNumber, - // startColumn: word.startColumn, - // endColumn: word.endColumn, - // }; - const suggestions = [ + const _suggestions = [ { label: "SELECT", kind: monaco.languages.CompletionItemKind.Keyword, insertText: "SELECT ", range }, { label: "FROM", kind: monaco.languages.CompletionItemKind.Keyword, insertText: "FROM ", range }, { label: "WHERE", kind: monaco.languages.CompletionItemKind.Keyword, insertText: "WHERE ", range }, @@ -70,9 +63,6 @@ export const autoSuggestionCompletionItems = ( { label: "CAST", kind: monaco.languages.CompletionItemKind.Function, insertText: "CAST()", range }, { label: "DATE", kind: monaco.languages.CompletionItemKind.Function, insertText: "DATE()", range }, { label: "NOW", kind: monaco.languages.CompletionItemKind.Function, insertText: "NOW()", range }, - { label: "SELECT", kind: monaco.languages.CompletionItemKind.Keyword, insertText: "SELECT ", range }, - { label: "FROM", kind: monaco.languages.CompletionItemKind.Keyword, insertText: "FROM ", range }, - { label: "WHERE", kind: monaco.languages.CompletionItemKind.Keyword, insertText: "WHERE ", range }, { label: "JOIN", kind: monaco.languages.CompletionItemKind.Keyword, insertText: "JOIN ", range }, { label: "INSERT INTO", kind: monaco.languages.CompletionItemKind.Keyword, insertText: "INSERT INTO ", range }, { label: "UPDATE", kind: monaco.languages.CompletionItemKind.Keyword, insertText: "UPDATE ", range }, @@ -95,7 +85,12 @@ export const autoSuggestionCompletionItems = ( { label: "CREATE MATERIALIZED VIEW", kind: monaco.languages.CompletionItemKind.Snippet, insertText: "CREATE MATERIALIZED VIEW ", range }, { label: "OPTIMIZE TABLE", kind: monaco.languages.CompletionItemKind.Keyword, insertText: "OPTIMIZE TABLE ", range }, { label: "ALTER TABLE", kind: monaco.languages.CompletionItemKind.Snippet, insertText: "ALTER TABLE ", range }, + { label: "EXPLAIN", kind: monaco.languages.CompletionItemKind.Keyword, insertText: "EXPLAIN ", range }, ]; + // Remove duplicates from suggestions using filter method + const suggestions = _suggestions.filter((item, index, self) => self.findIndex(t => t.label === item.label) === index); + + return { suggestions }; }; diff --git a/ui/src/routes/query.lazy.tsx b/ui/src/routes/query.lazy.tsx index e0fc20e..7887eb3 100644 --- a/ui/src/routes/query.lazy.tsx +++ b/ui/src/routes/query.lazy.tsx @@ -18,7 +18,7 @@ import { import { createFileRoute } from "@tanstack/react-router"; import { cn } from "@/lib/utils"; -import { fetchAutocomplete, fetchQuery } from "@/api"; +import { fetchQuery } from "@/api"; import { useQueries, QueriesProvider, @@ -177,13 +177,6 @@ function Query({ sql, onChange, onSave, onDelete, onUpdate }: QueryProps) { retry: false, }); - const { data: autocompleteData } = useQuery({ - queryKey: ["autocomplete"], - queryFn: () => fetchAutocomplete(), - }); - - console.log(autocompleteData); - const grid = !data ? ( !autoExecute && code && error ? ( From e3faef6c6926e5f48d8b348716d88b35003e984e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Victor=20Jes=C3=BAs=20Rosario=20V=C3=A1squez?= <46900196+Victor1890@users.noreply.github.com> Date: Sat, 19 Oct 2024 21:34:24 -0400 Subject: [PATCH 5/5] refactor: :recycle: update autocomplete suggestions in editor --- ui/src/components/editor.tsx | 135 +++++++++++++++++------------------ 1 file changed, 67 insertions(+), 68 deletions(-) diff --git a/ui/src/components/editor.tsx b/ui/src/components/editor.tsx index 06a76f0..9dbdf9b 100644 --- a/ui/src/components/editor.tsx +++ b/ui/src/components/editor.tsx @@ -9,8 +9,9 @@ import { ID_LANGUAGE_SQL, autoSuggestionCompletionItems, } from "./editor.config"; -import { useGetAllTables, useGetTable } from "./editor.hook"; import { Card } from "./ui/card"; +import { fetchAutocomplete } from "@/api"; +import { useQuery } from "@tanstack/react-query"; type Props = { value: string; @@ -23,8 +24,10 @@ export const Editor: FunctionComponent = ({ value, onChange }) => { useState(null); const monacoEl = useRef(null); - const { data: dataTable } = useGetAllTables(); - const { columns, handleSetTables } = useGetTable(); + const { data: autoCompleteData } = useQuery({ + queryKey: ["autocomplete"], + queryFn: () => fetchAutocomplete(), + }); useEffect(() => { if (monacoEl) { @@ -40,59 +43,6 @@ export const Editor: FunctionComponent = ({ value, onChange }) => { monaco.editor.defineTheme("sql-dark", vsPlusTheme.darkThemeData); monaco.editor.defineTheme("sql-light", vsPlusTheme.lightThemeData); - monaco.languages.registerCompletionItemProvider(ID_LANGUAGE_SQL, { - provideCompletionItems: (model, position) => { - const word = model.getWordUntilPosition(position); - const range = { - startLineNumber: position.lineNumber, - endLineNumber: position.lineNumber, - startColumn: word.startColumn, - endColumn: word.endColumn, - }; - const { suggestions } = autoSuggestionCompletionItems(range); - - - // TODO: Implement tables, columns and tables with columns suggestions - - // const tables = dataTable?.tables.map((table) => table.name) || []; - // const tableSuggestions = tables.map((table) => ({ - // label: table, - // kind: monaco.languages.CompletionItemKind.Variable, - // insertText: `"${table}"`, - // range, - // })); - - // console.log("tableSuggestions: ", tableSuggestions); - - // const columnSuggestions = columns.map((column) => ({ - // label: column, - // kind: monaco.languages.CompletionItemKind.Variable, - // insertText: column, - // range, - // })); - - // const tableColumnSuggestions = tables.flatMap((table) => - // columns.map((column) => ({ - // label: `${table}.${column}`, - // kind: monaco.languages.CompletionItemKind.Variable, - // insertText: `${table}.${column}`, - // range, - // })) - // ); - - // const allSuggestions = [ - // ...suggestions, - // ...tableSuggestions, - // // ...columnSuggestions, - // // ...tableColumnSuggestions, - // ]; - - // console.log("allSuggestions: ", allSuggestions); - - return { suggestions }; - }, - }); - const newEditor = monaco.editor.create(monacoEl.current!, { value, language: ID_LANGUAGE_SQL, @@ -120,6 +70,67 @@ export const Editor: FunctionComponent = ({ value, onChange }) => { return () => editor?.dispose(); }, [monacoEl.current]); + useEffect(() => { + + if(!autoCompleteData) return + + monaco.languages.registerCompletionItemProvider(ID_LANGUAGE_SQL, { + provideCompletionItems: (model, position) => { + const word = model.getWordUntilPosition(position); + const range = { + startLineNumber: position.lineNumber, + endLineNumber: position.lineNumber, + startColumn: word.startColumn, + endColumn: word.endColumn, + }; + const { suggestions } = autoSuggestionCompletionItems(range); + + const tableColumnSuggestions = autoCompleteData.tables.reduce((acc: any, { table_name, columns }) => { + + const alias = table_name.substring(0, 3); + + const table = { + label: table_name, + kind: monaco.languages.CompletionItemKind.Variable, + insertText: table_name, + range, + } + + const aliasTable = { + label: `${table_name} AS ${alias}`, + kind: monaco.languages.CompletionItemKind.Variable, + insertText: `${table_name} AS ${alias}`, + range, + } + + const col = columns.map((column) => ({ + label: column, + kind: monaco.languages.CompletionItemKind.Variable, + insertText: column, + range, + })); + + const tableColumn = columns.map((column) => ({ + label: `${table_name}.${column}`, + kind: monaco.languages.CompletionItemKind.Variable, + insertText: `${table_name}.${column}`, + range, + })); + + const tableColumnAlias = columns.map((column) => ({ + label: `${alias}.${column}`, + kind: monaco.languages.CompletionItemKind.Variable, + insertText: `${alias}.${column}`, + })); + + return [...acc, table, aliasTable, ...col, ...tableColumn, ...tableColumnAlias]; + }, []); + + return { suggestions: [...suggestions, ...tableColumnSuggestions] }; + }, + }); + }, [autoCompleteData]); + useEffect(() => { if (monacoEl.current) { monaco.editor.setTheme( @@ -128,18 +139,6 @@ export const Editor: FunctionComponent = ({ value, onChange }) => { } }, [currentTheme]); - useEffect(() => { - if (!dataTable) return; - - const tables = dataTable.tables.map((table) => table.name); - handleSetTables((prev) => { - if (prev.length === 0) return prev; - const isExist = prev.some((table) => tables.includes(table)); - if (isExist) return prev; - return [...prev, ...tables]; - }); - }, [dataTable]); - return (