diff --git a/demo/custom-renderers.ts b/demo/custom-renderers.ts new file mode 100644 index 0000000..1949c20 --- /dev/null +++ b/demo/custom-renderers.ts @@ -0,0 +1,306 @@ +import type { NamespaceTooltipData } from "../src/sql/hover.js"; + +interface ColumnMetadata { + type: string; + primaryKey?: boolean; + foreignKey?: boolean; + unique?: boolean; + default?: string; + notNull?: boolean; + comment?: string; +} + +interface ForeignKeyMetadata { + column: string; + referencedTable: string; + referencedColumn: string; +} + +interface IndexMetadata { + name: string; + columns: string[]; + unique: boolean; +} + +interface TableMetadata { + description: string; + rowCount: string; + columns: Record; + foreignKeys: ForeignKeyMetadata[]; + indexes: IndexMetadata[]; +} + +export type Schema = "users" | "posts" | "orders" | "customers" | "categories"; + +export const tableTooltipRenderer = (data: NamespaceTooltipData) => { + // Show table name, columns, description, primary key, foreign key, index, unique, check, default, comment + const table = data.item.path.join("."); + const columns = data.item.namespace?.[table] ?? []; + + // Enhanced table metadata (simulated for demo purposes) + const tableMetadata = getTableMetadata(table); + + let tooltip = `
`; + + // Table header + tooltip += `
📋 Table: ${table}
`; + + // Description + if (tableMetadata.description) { + tooltip += `
${tableMetadata.description}
`; + } + + // Column details in a table + if (columns.length > 0) { + tooltip += `
`; + tooltip += `
📊 Columns
`; + tooltip += ``; + tooltip += ``; + tooltip += ``; + tooltip += ``; + tooltip += ``; + tooltip += ``; + tooltip += ``; + + columns.forEach((column) => { + const columnInfo = tableMetadata.columns[column]; + const constraints: string[] = []; + + if (columnInfo) { + if (columnInfo.primaryKey) constraints.push("🔑 PK"); + if (columnInfo.foreignKey) constraints.push("🔗 FK"); + if (columnInfo.unique) constraints.push("✨ UNIQUE"); + if (columnInfo.notNull) constraints.push("❌ NOT NULL"); + if (columnInfo.default) constraints.push(`💡 DEFAULT ${columnInfo.default}`); + + tooltip += ``; + tooltip += ``; + tooltip += ``; + tooltip += ``; + tooltip += ``; + tooltip += ``; + } else { + tooltip += ``; + tooltip += ``; + tooltip += ``; + tooltip += ``; + tooltip += ``; + tooltip += ``; + } + }); + + tooltip += `
ColumnTypeConstraintsDescription
${column}${columnInfo.type}${constraints.join(" ")}${columnInfo.comment || ""}
${column}---
`; + } + + // Foreign key relationships + if (tableMetadata.foreignKeys.length > 0) { + tooltip += `
`; + tooltip += `
🔗 Foreign Keys
`; + tooltip += ``; + tooltip += ``; + tooltip += ``; + tooltip += ``; + tooltip += ``; + + tableMetadata.foreignKeys.forEach((fk) => { + tooltip += ``; + tooltip += ``; + tooltip += ``; + tooltip += ``; + }); + + tooltip += `
ColumnReferences
${fk.column}${fk.referencedTable}.${fk.referencedColumn}
`; + } + + // Indexes + if (tableMetadata.indexes.length > 0) { + tooltip += `
`; + tooltip += `
📈 Indexes
`; + tooltip += ``; + tooltip += ``; + tooltip += ``; + tooltip += ``; + tooltip += ``; + tooltip += ``; + + tableMetadata.indexes.forEach((index) => { + tooltip += ``; + tooltip += ``; + tooltip += ``; + tooltip += ``; + tooltip += ``; + }); + + tooltip += `
NameColumnsType
${index.name}${index.columns.join(", ")}${index.unique ? "UNIQUE" : "NORMAL"}
`; + } + + // Table statistics + tooltip += `
`; + tooltip += `📊 ${tableMetadata.rowCount} rows`; + tooltip += `
`; + + tooltip += `
`; + + return tooltip; +}; + +// Helper function to get enhanced table metadata +function getTableMetadata(tableName: string): TableMetadata { + const metadata: Record = { + users: { + description: "User accounts and profile information", + rowCount: "1,234", + columns: { + id: { type: "INT", primaryKey: true, notNull: true, comment: "Unique user identifier" }, + name: { type: "VARCHAR(255)", notNull: true, comment: "User's full name" }, + email: { + type: "VARCHAR(255)", + unique: true, + notNull: true, + comment: "User's email address", + }, + active: { type: "BOOLEAN", default: "true", comment: "Whether the user account is active" }, + status: { + type: "ENUM('active','inactive','suspended')", + default: "'active'", + comment: "User account status", + }, + created_at: { + type: "TIMESTAMP", + default: "CURRENT_TIMESTAMP", + comment: "Account creation date", + }, + updated_at: { + type: "TIMESTAMP", + default: "CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP", + comment: "Last update timestamp", + }, + profile_id: { type: "INT", foreignKey: true, comment: "Reference to user profile" }, + }, + foreignKeys: [{ column: "profile_id", referencedTable: "profiles", referencedColumn: "id" }], + indexes: [ + { name: "idx_users_email", columns: ["email"], unique: true }, + { name: "idx_users_status", columns: ["status"], unique: false }, + { name: "idx_users_created", columns: ["created_at"], unique: false }, + ], + }, + posts: { + description: "Blog posts and articles", + rowCount: "5,678", + columns: { + id: { type: "INT", primaryKey: true, notNull: true, comment: "Unique post identifier" }, + title: { type: "VARCHAR(255)", notNull: true, comment: "Post title" }, + content: { type: "TEXT", comment: "Post content" }, + user_id: { type: "INT", notNull: true, foreignKey: true, comment: "Author of the post" }, + published: { type: "BOOLEAN", default: "false", comment: "Publication status" }, + created_at: { + type: "TIMESTAMP", + default: "CURRENT_TIMESTAMP", + comment: "Post creation date", + }, + updated_at: { + type: "TIMESTAMP", + default: "CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP", + comment: "Last update timestamp", + }, + category_id: { type: "INT", foreignKey: true, comment: "Post category" }, + }, + foreignKeys: [ + { column: "user_id", referencedTable: "users", referencedColumn: "id" }, + { column: "category_id", referencedTable: "categories", referencedColumn: "id" }, + ], + indexes: [ + { name: "idx_posts_user", columns: ["user_id"], unique: false }, + { name: "idx_posts_published", columns: ["published"], unique: false }, + { name: "idx_posts_created", columns: ["created_at"], unique: false }, + ], + }, + orders: { + description: "Customer orders and transactions", + rowCount: "12,345", + columns: { + id: { type: "INT", primaryKey: true, notNull: true, comment: "Unique order identifier" }, + customer_id: { + type: "INT", + notNull: true, + foreignKey: true, + comment: "Customer who placed the order", + }, + order_date: { type: "DATE", notNull: true, comment: "Date when order was placed" }, + total_amount: { type: "DECIMAL(10,2)", notNull: true, comment: "Total order amount" }, + status: { + type: "ENUM('pending','processing','shipped','delivered','cancelled')", + default: "'pending'", + comment: "Order status", + }, + shipping_address: { type: "TEXT", comment: "Shipping address" }, + created_at: { + type: "TIMESTAMP", + default: "CURRENT_TIMESTAMP", + comment: "Order creation timestamp", + }, + }, + foreignKeys: [ + { column: "customer_id", referencedTable: "customers", referencedColumn: "id" }, + ], + indexes: [ + { name: "idx_orders_customer", columns: ["customer_id"], unique: false }, + { name: "idx_orders_date", columns: ["order_date"], unique: false }, + { name: "idx_orders_status", columns: ["status"], unique: false }, + ], + }, + customers: { + description: "Customer information and profiles", + rowCount: "2,345", + columns: { + id: { type: "INT", primaryKey: true, notNull: true, comment: "Unique customer identifier" }, + first_name: { type: "VARCHAR(100)", notNull: true, comment: "Customer's first name" }, + last_name: { type: "VARCHAR(100)", notNull: true, comment: "Customer's last name" }, + email: { + type: "VARCHAR(255)", + unique: true, + notNull: true, + comment: "Customer's email address", + }, + phone: { type: "VARCHAR(20)", comment: "Customer's phone number" }, + address: { type: "TEXT", comment: "Customer's address" }, + city: { type: "VARCHAR(100)", comment: "Customer's city" }, + country: { type: "VARCHAR(100)", comment: "Customer's country" }, + }, + foreignKeys: [], + indexes: [ + { name: "idx_customers_email", columns: ["email"], unique: true }, + { name: "idx_customers_name", columns: ["last_name", "first_name"], unique: false }, + ], + }, + categories: { + description: "Product and post categories", + rowCount: "89", + columns: { + id: { type: "INT", primaryKey: true, notNull: true, comment: "Unique category identifier" }, + name: { type: "VARCHAR(100)", notNull: true, comment: "Category name" }, + description: { type: "TEXT", comment: "Category description" }, + parent_id: { + type: "INT", + foreignKey: true, + comment: "Parent category for hierarchical structure", + }, + }, + foreignKeys: [{ column: "parent_id", referencedTable: "categories", referencedColumn: "id" }], + indexes: [ + { name: "idx_categories_name", columns: ["name"], unique: false }, + { name: "idx_categories_parent", columns: ["parent_id"], unique: false }, + ], + }, + }; + + return ( + metadata[tableName] || { + description: "Table information", + rowCount: "Unknown", + columns: {}, + foreignKeys: [], + indexes: [], + } + ); +} diff --git a/demo/index.html b/demo/index.html index 5cc6199..ba59925 100644 --- a/demo/index.html +++ b/demo/index.html @@ -13,14 +13,16 @@

- codemirror-sql

-

by marimo

+

by marimo

A CodeMirror extension for SQL with real-time syntax validation and error diagnostics using - node-sql-parser. + node-sql-parser.

diff --git a/demo/index.ts b/demo/index.ts index 3147b7d..24c53bc 100644 --- a/demo/index.ts +++ b/demo/index.ts @@ -1,7 +1,10 @@ -import { StandardSQL, sql } from "@codemirror/lang-sql"; +import { acceptCompletion } from "@codemirror/autocomplete"; +import { PostgreSQL, sql } from "@codemirror/lang-sql"; +import { keymap } from "@codemirror/view"; import { basicSetup, EditorView } from "codemirror"; import { cteCompletionSource } from "../src/sql/cte-completion-source.js"; import { sqlExtension } from "../src/sql/extension.js"; +import { type Schema, tableTooltipRenderer } from "./custom-renderers.js"; // Default SQL content for the demo const defaultSqlDoc = `-- Welcome to the SQL Editor Demo! @@ -51,7 +54,7 @@ WHERE order_date >= '2024-01-01' ORDER BY total_amount DESC; `; -const schema = { +const schema: Record = { // Users table users: ["id", "name", "email", "active", "status", "created_at", "updated_at", "profile_id"], // Posts table @@ -94,13 +97,45 @@ const completionKindStyles = { height: "12px", }; -const dialect = StandardSQL; +const dialect = PostgreSQL; + +const defaultKeymap = [ + { + key: "Tab", + run: (view: EditorView) => { + // Try to accept completion first + if (acceptCompletion(view)) { + return true; + } + // In production, you can use @codemirror/commands.indentWithTab instead of custom logic + // If no completion to accept, insert a tab character + const { state } = view; + const { selection } = state; + if (selection.main.empty) { + // Insert tab at cursor position + view.dispatch({ + changes: { + from: selection.main.from, + insert: "\t", + }, + selection: { + anchor: selection.main.from + 1, + head: selection.main.from + 1, + }, + }); + return true; + } + return false; + }, + }, +]; // Initialize the SQL editor function initializeEditor() { const extensions = [ basicSetup, EditorView.lineWrapping, + keymap.of(defaultKeymap), sql({ dialect: dialect, // Example schema for autocomplete @@ -132,6 +167,10 @@ function initializeEditor() { const keywords = await import("../src/data/common-keywords.json"); return keywords.default.keywords; }, + tooltipRenderers: { + // Custom renderer for tables + table: tableTooltipRenderer, + }, }, }), dialect.language.data.of({