Skip to content

Commit

Permalink
Features - Implement Auto-Suggestion - SQL Editor (#49)
Browse files Browse the repository at this point in the history
* 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.

* 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.

* refactor: 🔥 remove unused editor hook

* refactor: ♻️ remove duplicate suggestions in autoSuggestionCompletionItems

* refactor: ♻️ update autocomplete suggestions in editor
  • Loading branch information
Victor1890 authored Oct 20, 2024
1 parent 8202e9c commit 2a1aa2a
Show file tree
Hide file tree
Showing 6 changed files with 266 additions and 46 deletions.
49 changes: 47 additions & 2 deletions ui/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions ui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
96 changes: 96 additions & 0 deletions ui/src/components/editor.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
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 _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: "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 },
{ 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 };
};
89 changes: 87 additions & 2 deletions ui/src/components/editor.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,17 @@
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 { Card } from "./ui/card";
import { fetchAutocomplete } from "@/api";
import { useQuery } from "@tanstack/react-query";

type Props = {
value: string;
Expand All @@ -16,14 +24,28 @@ export const Editor: FunctionComponent<Props> = ({ value, onChange }) => {
useState<monaco.editor.IStandaloneCodeEditor | null>(null);
const monacoEl = useRef<HTMLDivElement>(null);

const { data: autoCompleteData } = useQuery({
queryKey: ["autocomplete"],
queryFn: () => fetchAutocomplete(),
});

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,
},
Expand All @@ -48,9 +70,72 @@ export const Editor: FunctionComponent<Props> = ({ 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(currentTheme === "light" ? "vs" : "vs-dark");
monaco.editor.setTheme(
currentTheme === "light" ? "sql-light" : "sql-dark"
);
}
}, [currentTheme]);

Expand Down
Loading

0 comments on commit 2a1aa2a

Please sign in to comment.