Skip to content

Commit 600ab34

Browse files
committed
include custom table renderer
1 parent f84b1fd commit 600ab34

3 files changed

Lines changed: 348 additions & 3 deletions

File tree

demo/customRenderers.ts

Lines changed: 304 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,304 @@
1+
import type { NamespaceTooltipData } from "../src/sql/hover.js";
2+
3+
interface ColumnMetadata {
4+
type: string;
5+
primaryKey?: boolean;
6+
foreignKey?: boolean;
7+
unique?: boolean;
8+
default?: string;
9+
notNull?: boolean;
10+
comment?: string;
11+
}
12+
13+
interface ForeignKeyMetadata {
14+
column: string;
15+
referencedTable: string;
16+
referencedColumn: string;
17+
}
18+
19+
interface IndexMetadata {
20+
name: string;
21+
columns: string[];
22+
unique: boolean;
23+
}
24+
25+
interface TableMetadata {
26+
description: string;
27+
rowCount: string;
28+
columns: Record<string, ColumnMetadata>;
29+
foreignKeys: ForeignKeyMetadata[];
30+
indexes: IndexMetadata[];
31+
}
32+
33+
export const tableTooltipRenderer = (data: NamespaceTooltipData) => {
34+
// Show table name, columns, description, primary key, foreign key, index, unique, check, default, comment
35+
const table = data.item.path.join(".");
36+
const columns = data.item.namespace?.[table] ?? [];
37+
38+
// Enhanced table metadata (simulated for demo purposes)
39+
const tableMetadata = getTableMetadata(table);
40+
41+
let tooltip = `<div style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; font-size: 13px; line-height: 1.4;">`;
42+
43+
// Table header
44+
tooltip += `<div style="font-weight: 600; font-size: 14px; color: #1f2937; margin-bottom: 8px; border-bottom: 1px solid #e5e7eb; padding-bottom: 4px;">📋 Table: ${table}</div>`;
45+
46+
// Description
47+
if (tableMetadata.description) {
48+
tooltip += `<div style="color: #6b7280; font-style: italic; margin-bottom: 12px;">${tableMetadata.description}</div>`;
49+
}
50+
51+
// Column details in a table
52+
if (columns.length > 0) {
53+
tooltip += `<div style="margin-bottom: 12px;">`;
54+
tooltip += `<div style="font-weight: 600; color: #374151; margin-bottom: 6px;">📊 Columns</div>`;
55+
tooltip += `<table style="width: 100%; border-collapse: collapse; font-size: 12px;">`;
56+
tooltip += `<thead><tr style="background-color: #f9fafb;">`;
57+
tooltip += `<th style="text-align: left; padding: 4px 8px; border: 1px solid #e5e7eb; font-weight: 600;">Column</th>`;
58+
tooltip += `<th style="text-align: left; padding: 4px 8px; border: 1px solid #e5e7eb; font-weight: 600;">Type</th>`;
59+
tooltip += `<th style="text-align: left; padding: 4px 8px; border: 1px solid #e5e7eb; font-weight: 600;">Constraints</th>`;
60+
tooltip += `<th style="text-align: left; padding: 4px 8px; border: 1px solid #e5e7eb; font-weight: 600;">Description</th>`;
61+
tooltip += `</tr></thead><tbody>`;
62+
63+
columns.forEach((column) => {
64+
const columnInfo = tableMetadata.columns[column];
65+
const constraints: string[] = [];
66+
67+
if (columnInfo) {
68+
if (columnInfo.primaryKey) constraints.push("🔑 PK");
69+
if (columnInfo.foreignKey) constraints.push("🔗 FK");
70+
if (columnInfo.unique) constraints.push("✨ UNIQUE");
71+
if (columnInfo.notNull) constraints.push("❌ NOT NULL");
72+
if (columnInfo.default) constraints.push(`💡 DEFAULT ${columnInfo.default}`);
73+
74+
tooltip += `<tr style="border-bottom: 1px solid #f3f4f6;">`;
75+
tooltip += `<td style="padding: 4px 8px; font-family: 'SF Mono', Monaco, 'Cascadia Code', monospace; color: #059669;">${column}</td>`;
76+
tooltip += `<td style="padding: 4px 8px; color: #7c3aed;">${columnInfo.type}</td>`;
77+
tooltip += `<td style="padding: 4px 8px; color: #dc2626;">${constraints.join(" ")}</td>`;
78+
tooltip += `<td style="padding: 4px 8px; color: #6b7280; font-size: 11px;">${columnInfo.comment || ""}</td>`;
79+
tooltip += `</tr>`;
80+
} else {
81+
tooltip += `<tr style="border-bottom: 1px solid #f3f4f6;">`;
82+
tooltip += `<td style="padding: 4px 8px; font-family: 'SF Mono', Monaco, 'Cascadia Code', monospace; color: #059669;">${column}</td>`;
83+
tooltip += `<td style="padding: 4px 8px; color: #7c3aed;">-</td>`;
84+
tooltip += `<td style="padding: 4px 8px; color: #dc2626;">-</td>`;
85+
tooltip += `<td style="padding: 4px 8px; color: #6b7280; font-size: 11px;">-</td>`;
86+
tooltip += `</tr>`;
87+
}
88+
});
89+
90+
tooltip += `</tbody></table></div>`;
91+
}
92+
93+
// Foreign key relationships
94+
if (tableMetadata.foreignKeys.length > 0) {
95+
tooltip += `<div style="margin-bottom: 12px;">`;
96+
tooltip += `<div style="font-weight: 600; color: #374151; margin-bottom: 6px;">🔗 Foreign Keys</div>`;
97+
tooltip += `<table style="width: 100%; border-collapse: collapse; font-size: 12px;">`;
98+
tooltip += `<thead><tr style="background-color: #f9fafb;">`;
99+
tooltip += `<th style="text-align: left; padding: 4px 8px; border: 1px solid #e5e7eb; font-weight: 600;">Column</th>`;
100+
tooltip += `<th style="text-align: left; padding: 4px 8px; border: 1px solid #e5e7eb; font-weight: 600;">References</th>`;
101+
tooltip += `</tr></thead><tbody>`;
102+
103+
tableMetadata.foreignKeys.forEach((fk) => {
104+
tooltip += `<tr style="border-bottom: 1px solid #f3f4f6;">`;
105+
tooltip += `<td style="padding: 4px 8px; font-family: 'SF Mono', Monaco, 'Cascadia Code', monospace; color: #059669;">${fk.column}</td>`;
106+
tooltip += `<td style="padding: 4px 8px; color: #7c3aed;">${fk.referencedTable}.${fk.referencedColumn}</td>`;
107+
tooltip += `</tr>`;
108+
});
109+
110+
tooltip += `</tbody></table></div>`;
111+
}
112+
113+
// Indexes
114+
if (tableMetadata.indexes.length > 0) {
115+
tooltip += `<div style="margin-bottom: 12px;">`;
116+
tooltip += `<div style="font-weight: 600; color: #374151; margin-bottom: 6px;">📈 Indexes</div>`;
117+
tooltip += `<table style="width: 100%; border-collapse: collapse; font-size: 12px;">`;
118+
tooltip += `<thead><tr style="background-color: #f9fafb;">`;
119+
tooltip += `<th style="text-align: left; padding: 4px 8px; border: 1px solid #e5e7eb; font-weight: 600;">Name</th>`;
120+
tooltip += `<th style="text-align: left; padding: 4px 8px; border: 1px solid #e5e7eb; font-weight: 600;">Columns</th>`;
121+
tooltip += `<th style="text-align: left; padding: 4px 8px; border: 1px solid #e5e7eb; font-weight: 600;">Type</th>`;
122+
tooltip += `</tr></thead><tbody>`;
123+
124+
tableMetadata.indexes.forEach((index) => {
125+
tooltip += `<tr style="border-bottom: 1px solid #f3f4f6;">`;
126+
tooltip += `<td style="padding: 4px 8px; font-family: 'SF Mono', Monaco, 'Cascadia Code', monospace; color: #059669;">${index.name}</td>`;
127+
tooltip += `<td style="padding: 4px 8px; color: #7c3aed;">${index.columns.join(", ")}</td>`;
128+
tooltip += `<td style="padding: 4px 8px; color: #dc2626;">${index.unique ? "UNIQUE" : "NORMAL"}</td>`;
129+
tooltip += `</tr>`;
130+
});
131+
132+
tooltip += `</tbody></table></div>`;
133+
}
134+
135+
// Table statistics
136+
tooltip += `<div style="margin-top: 12px; padding-top: 8px; border-top: 1px solid #e5e7eb; color: #6b7280; font-size: 11px;">`;
137+
tooltip += `📊 ${tableMetadata.rowCount} rows`;
138+
tooltip += `</div>`;
139+
140+
tooltip += `</div>`;
141+
142+
return tooltip;
143+
};
144+
145+
// Helper function to get enhanced table metadata
146+
function getTableMetadata(tableName: string): TableMetadata {
147+
const metadata: Record<string, TableMetadata> = {
148+
users: {
149+
description: "User accounts and profile information",
150+
rowCount: "1,234",
151+
columns: {
152+
id: { type: "INT", primaryKey: true, notNull: true, comment: "Unique user identifier" },
153+
name: { type: "VARCHAR(255)", notNull: true, comment: "User's full name" },
154+
email: {
155+
type: "VARCHAR(255)",
156+
unique: true,
157+
notNull: true,
158+
comment: "User's email address",
159+
},
160+
active: { type: "BOOLEAN", default: "true", comment: "Whether the user account is active" },
161+
status: {
162+
type: "ENUM('active','inactive','suspended')",
163+
default: "'active'",
164+
comment: "User account status",
165+
},
166+
created_at: {
167+
type: "TIMESTAMP",
168+
default: "CURRENT_TIMESTAMP",
169+
comment: "Account creation date",
170+
},
171+
updated_at: {
172+
type: "TIMESTAMP",
173+
default: "CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP",
174+
comment: "Last update timestamp",
175+
},
176+
profile_id: { type: "INT", foreignKey: true, comment: "Reference to user profile" },
177+
},
178+
foreignKeys: [{ column: "profile_id", referencedTable: "profiles", referencedColumn: "id" }],
179+
indexes: [
180+
{ name: "idx_users_email", columns: ["email"], unique: true },
181+
{ name: "idx_users_status", columns: ["status"], unique: false },
182+
{ name: "idx_users_created", columns: ["created_at"], unique: false },
183+
],
184+
},
185+
posts: {
186+
description: "Blog posts and articles",
187+
rowCount: "5,678",
188+
columns: {
189+
id: { type: "INT", primaryKey: true, notNull: true, comment: "Unique post identifier" },
190+
title: { type: "VARCHAR(255)", notNull: true, comment: "Post title" },
191+
content: { type: "TEXT", comment: "Post content" },
192+
user_id: { type: "INT", notNull: true, foreignKey: true, comment: "Author of the post" },
193+
published: { type: "BOOLEAN", default: "false", comment: "Publication status" },
194+
created_at: {
195+
type: "TIMESTAMP",
196+
default: "CURRENT_TIMESTAMP",
197+
comment: "Post creation date",
198+
},
199+
updated_at: {
200+
type: "TIMESTAMP",
201+
default: "CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP",
202+
comment: "Last update timestamp",
203+
},
204+
category_id: { type: "INT", foreignKey: true, comment: "Post category" },
205+
},
206+
foreignKeys: [
207+
{ column: "user_id", referencedTable: "users", referencedColumn: "id" },
208+
{ column: "category_id", referencedTable: "categories", referencedColumn: "id" },
209+
],
210+
indexes: [
211+
{ name: "idx_posts_user", columns: ["user_id"], unique: false },
212+
{ name: "idx_posts_published", columns: ["published"], unique: false },
213+
{ name: "idx_posts_created", columns: ["created_at"], unique: false },
214+
],
215+
},
216+
orders: {
217+
description: "Customer orders and transactions",
218+
rowCount: "12,345",
219+
columns: {
220+
id: { type: "INT", primaryKey: true, notNull: true, comment: "Unique order identifier" },
221+
customer_id: {
222+
type: "INT",
223+
notNull: true,
224+
foreignKey: true,
225+
comment: "Customer who placed the order",
226+
},
227+
order_date: { type: "DATE", notNull: true, comment: "Date when order was placed" },
228+
total_amount: { type: "DECIMAL(10,2)", notNull: true, comment: "Total order amount" },
229+
status: {
230+
type: "ENUM('pending','processing','shipped','delivered','cancelled')",
231+
default: "'pending'",
232+
comment: "Order status",
233+
},
234+
shipping_address: { type: "TEXT", comment: "Shipping address" },
235+
created_at: {
236+
type: "TIMESTAMP",
237+
default: "CURRENT_TIMESTAMP",
238+
comment: "Order creation timestamp",
239+
},
240+
},
241+
foreignKeys: [
242+
{ column: "customer_id", referencedTable: "customers", referencedColumn: "id" },
243+
],
244+
indexes: [
245+
{ name: "idx_orders_customer", columns: ["customer_id"], unique: false },
246+
{ name: "idx_orders_date", columns: ["order_date"], unique: false },
247+
{ name: "idx_orders_status", columns: ["status"], unique: false },
248+
],
249+
},
250+
customers: {
251+
description: "Customer information and profiles",
252+
rowCount: "2,345",
253+
columns: {
254+
id: { type: "INT", primaryKey: true, notNull: true, comment: "Unique customer identifier" },
255+
first_name: { type: "VARCHAR(100)", notNull: true, comment: "Customer's first name" },
256+
last_name: { type: "VARCHAR(100)", notNull: true, comment: "Customer's last name" },
257+
email: {
258+
type: "VARCHAR(255)",
259+
unique: true,
260+
notNull: true,
261+
comment: "Customer's email address",
262+
},
263+
phone: { type: "VARCHAR(20)", comment: "Customer's phone number" },
264+
address: { type: "TEXT", comment: "Customer's address" },
265+
city: { type: "VARCHAR(100)", comment: "Customer's city" },
266+
country: { type: "VARCHAR(100)", comment: "Customer's country" },
267+
},
268+
foreignKeys: [],
269+
indexes: [
270+
{ name: "idx_customers_email", columns: ["email"], unique: true },
271+
{ name: "idx_customers_name", columns: ["last_name", "first_name"], unique: false },
272+
],
273+
},
274+
categories: {
275+
description: "Product and post categories",
276+
rowCount: "89",
277+
columns: {
278+
id: { type: "INT", primaryKey: true, notNull: true, comment: "Unique category identifier" },
279+
name: { type: "VARCHAR(100)", notNull: true, comment: "Category name" },
280+
description: { type: "TEXT", comment: "Category description" },
281+
parent_id: {
282+
type: "INT",
283+
foreignKey: true,
284+
comment: "Parent category for hierarchical structure",
285+
},
286+
},
287+
foreignKeys: [{ column: "parent_id", referencedTable: "categories", referencedColumn: "id" }],
288+
indexes: [
289+
{ name: "idx_categories_name", columns: ["name"], unique: false },
290+
{ name: "idx_categories_parent", columns: ["parent_id"], unique: false },
291+
],
292+
},
293+
};
294+
295+
return (
296+
metadata[tableName] || {
297+
description: "Table information",
298+
rowCount: "Unknown",
299+
columns: {},
300+
foreignKeys: [],
301+
indexes: [],
302+
}
303+
);
304+
}

demo/index.html

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,14 +13,16 @@
1313
<body>
1414
<main class="max-w-4xl mx-auto px-4 py-8">
1515
<h1 class="text-4xl font-bold mb-4">
16-
<a href="https://github.com/marimo-team/codemirror-sql" class="text-blue-600 hover:text-blue-800"
16+
<a href="https://github.com/marimo-team/codemirror-sql" class="text-blue-600 hover:text-blue-800" target="_blank"
1717
>codemirror-sql</a
1818
>
1919
</h1>
20-
<p class="mb-2">by <a href="https://marimo.io/" class="text-blue-600 hover:text-blue-800">marimo</a></p>
20+
<p class="mb-2">by <a href="https://marimo.io/" class="text-blue-600 hover:text-blue-800" target="_blank">marimo</a></p>
2121
<p class="mb-6 text-gray-700">
2222
A CodeMirror extension for SQL with real-time syntax validation and error diagnostics using
23-
<a href="https://www.npmjs.com/package/node-sql-parser" class="text-blue-600 hover:text-blue-800">node-sql-parser</a>.
23+
<a href="https://www.npmjs.com/package/node-sql-parser" class="text-blue-600 hover:text-blue-800" target="_blank"
24+
>node-sql-parser</a
25+
>.
2426
</p>
2527

2628
<div class="grid gap-8">

demo/index.ts

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
1+
import { acceptCompletion } from "@codemirror/autocomplete";
12
import { StandardSQL, sql } from "@codemirror/lang-sql";
3+
import { keymap } from "@codemirror/view";
24
import { basicSetup, EditorView } from "codemirror";
35
import { cteCompletionSource } from "../src/sql/cte-completion-source.js";
46
import { sqlExtension } from "../src/sql/extension.js";
7+
import { tableTooltipRenderer } from "./customRenderers.js";
58

69
// Default SQL content for the demo
710
const defaultSqlDoc = `-- Welcome to the SQL Editor Demo!
@@ -96,11 +99,43 @@ const completionKindStyles = {
9699

97100
const dialect = StandardSQL;
98101

102+
const defaultKeymap = [
103+
{
104+
key: "Tab",
105+
run: (view: EditorView) => {
106+
// Try to accept completion first
107+
if (acceptCompletion(view)) {
108+
return true;
109+
}
110+
// In production, you can use @codemirror/commands.indentWithTab instead of custom logic
111+
// If no completion to accept, insert a tab character
112+
const { state } = view;
113+
const { selection } = state;
114+
if (selection.main.empty) {
115+
// Insert tab at cursor position
116+
view.dispatch({
117+
changes: {
118+
from: selection.main.from,
119+
insert: "\t",
120+
},
121+
selection: {
122+
anchor: selection.main.from + 1,
123+
head: selection.main.from + 1,
124+
},
125+
});
126+
return true;
127+
}
128+
return false;
129+
},
130+
},
131+
];
132+
99133
// Initialize the SQL editor
100134
function initializeEditor() {
101135
const extensions = [
102136
basicSetup,
103137
EditorView.lineWrapping,
138+
keymap.of(defaultKeymap),
104139
sql({
105140
dialect: dialect,
106141
// Example schema for autocomplete
@@ -132,6 +167,10 @@ function initializeEditor() {
132167
const keywords = await import("../src/data/common-keywords.json");
133168
return keywords.default.keywords;
134169
},
170+
tooltipRenderers: {
171+
// Custom renderer for tables
172+
table: tableTooltipRenderer,
173+
},
135174
},
136175
}),
137176
dialect.language.data.of({

0 commit comments

Comments
 (0)