diff --git a/web/components/QueryHistory.tsx b/web/components/QueryHistory.tsx
index 32049ba..c4b3553 100644
--- a/web/components/QueryHistory.tsx
+++ b/web/components/QueryHistory.tsx
@@ -1,6 +1,7 @@
import { ChevronRight, Clock, Play, Search, Trash2 } from 'lucide-preact'
import { A, navigate, url } from '@01edu/signal-router'
import { queriesHistory, runQuery } from '../lib/shared.tsx'
+import { highlightSQL } from '../lib/highlight-sql.ts'
const deleteQuery = (hash: string) => {
const updatedHistory = { ...queriesHistory.value }
@@ -58,8 +59,12 @@ export const QueryHistory = () => {
{new Date(item.timestamp).toLocaleString()}
-
{item.columns} columns, {item.rows} rows
diff --git a/web/lib/highlight-sql.ts b/web/lib/highlight-sql.ts
new file mode 100644
index 0000000..6e1703b
--- /dev/null
+++ b/web/lib/highlight-sql.ts
@@ -0,0 +1,96 @@
+const SQLITE_KEYWORDS =
+ 'CURRENT_TIMESTAMP|AUTOINCREMENT|CURRENT_DATE|CURRENT_TIME|MATERIALIZED|TRANSACTION|CONSTRAINT|DEFERRABLE|REFERENCES|EXCLUSIVE|FOLLOWING|GENERATED|IMMEDIATE|INITIALLY|INTERSECT|PARTITION|PRECEDING|RECURSIVE|RETURNING|SAVEPOINT|TEMPORARY|UNBOUNDED|CONFLICT|DATABASE|DEFERRED|DISTINCT|RESTRICT|ROLLBACK|ANALYZE|BETWEEN|CASCADE|COLLATE|CURRENT|DEFAULT|EXCLUDE|EXPLAIN|FOREIGN|INDEXED|INSTEAD|NATURAL|NOTHING|NOTNULL|PRIMARY|REINDEX|RELEASE|REPLACE|TRIGGER|VIRTUAL|WITHOUT|ACTION|ALWAYS|ATTACH|BEFORE|COLUMN|COMMIT|CREATE|DELETE|DETACH|ESCAPE|EXCEPT|EXISTS|FILTER|GROUPS|HAVING|IGNORE|INSERT|ISNULL|OFFSET|OTHERS|PRAGMA|REGEXP|RENAME|SELECT|UNIQUE|UPDATE|VACUUM|VALUES|WINDOW|ABORT|AFTER|ALTER|BEGIN|CHECK|CROSS|FIRST|GROUP|INDEX|INNER|LIMIT|MATCH|NULLS|ORDER|OUTER|QUERY|RAISE|RANGE|RIGHT|TABLE|UNION|USING|WHERE|CASE|CAST|DESC|DROP|EACH|ELSE|FAIL|FROM|FULL|GLOB|INTO|JOIN|LAST|LEFT|LIKE|NULL|OVER|PLAN|ROWS|TEMP|THEN|TIES|VIEW|WHEN|WITH|ADD|ALL|AND|ASC|END|FOR|KEY|NOT|ROW|SET|AS|BY|DO|IF|IN|IS|NO|OF|ON|OR|TO'
+
+const TOKENS = {
+ comment: /--[^\n\r]*|\/\*[\s\S]*?\*\//y,
+ string: /'(?:''|[^'])*'/y,
+ 'id-quoted': /"(?:""|[^"])*"|\[(?:[^\]])*\]|`(?:``|[^`]*)`/y,
+ 'param-named': /[:@$][A-Za-z_][A-Za-z0-9_]*/y,
+ 'param-qmark': /\?(?:\d+)?/y,
+ operator: /\|\||<<|>>|<=|>=|==|!=|<>|[-+*/%&|<>=~]/y,
+ punct: /[(),.;]/y,
+ keyword: new RegExp(String.raw`\b(?:${SQLITE_KEYWORDS})\b`, 'iy'),
+ id: /[A-Za-z_][A-Za-z0-9_$]*/y,
+ number: /\b(?:0x[0-9A-Fa-f]+|\d+(?:\.\d+)?(?:[eE][+-]?\d+)?)\b/y,
+} as const
+
+type TokenType = keyof typeof TOKENS
+
+const defs =
+ (Object.entries(TOKENS) as [TokenType, (typeof TOKENS)[TokenType]][])
+ .map(([type, re]) => ({ type, re, hl: new Highlight() }))
+
+export function highlightSQL(elem: HTMLElement | null): (() => void) | void {
+ const firstChild = elem?.firstChild
+ if (!firstChild || firstChild.nodeType !== Node.TEXT_NODE) return
+ const sql = (firstChild as Text).data
+ let i = -1
+ const cleanups = new Set<{ hl: Highlight; range: Range }>()
+ main: while (++i < sql.length) {
+ if (sql[i] === ' ' || sql[i] === '\t' || sql[i] === '\n') continue
+ for (const { hl, re } of defs) {
+ re.lastIndex = i
+ const m = re.exec(sql)
+ if (!m) continue
+ const value = m[0]
+ const end = i + value.length
+ const range = new Range()
+ range.setStart(firstChild, i)
+ range.setEnd(firstChild, end)
+ hl.add(range)
+ cleanups.add({ hl, range })
+ i = end - 1
+ continue main
+ }
+ }
+ return () => {
+ for (const { hl, range } of cleanups) hl.delete(range)
+ }
+}
+
+// Setup the style and register the highligths
+const css = String.raw
+const style = document.createElement('style')
+style.innerHTML = css`
+ ::highlight(comment) {
+ color: #a6acb9;
+ }
+ ::highlight(string) {
+ color: #5c99d6;
+ }
+ ::highlight(number) {
+ color: #c594c5;
+ }
+ ::highlight(id-quoted) {
+ color: #99c794;
+ }
+ ::highlight(id) {
+ color: #5fb4b4;
+ }
+ ::highlight(param-named) {
+ color: #f7f7f7;
+ }
+ ::highlight(param-qmark) {
+ color: #f9ae58;
+ }
+ ::highlight(operator) {
+ color: #f97b58;
+ }
+ ::highlight(punct) {
+ color: #596878;
+ }
+ ::highlight(keyword) {
+ color: #c695c6;
+ }
+`
+// Unused theme colors:
+// normal #D8DEE9
+// end #F9AE58
+// error #EC5F66
+// selection --background=#3E4347
+// search_match --background=#596673
+// option #5FB4B4
+// host_remote #A3CE9E
+document.head.append(style)
+
+for (const { hl, type } of defs) CSS.highlights.set(type, hl)
diff --git a/web/pages/DeploymentPage.tsx b/web/pages/DeploymentPage.tsx
index 748747f..cd38f2c 100644
--- a/web/pages/DeploymentPage.tsx
+++ b/web/pages/DeploymentPage.tsx
@@ -37,6 +37,7 @@ import {
} from '../components/Filtre.tsx'
import { computed, effect, Signal, untracked } from '@preact/signals'
import { api, type ApiOutput } from '../lib/api.ts'
+import { highlightSQL } from '../lib/highlight-sql.ts'
import { QueryHistory } from '../components/QueryHistory.tsx'
import type { ComponentChildren } from 'preact'
@@ -59,9 +60,7 @@ export const metricsData = api['GET/api/deployment/metrics-sql'].signal()
const toastSignal = new Signal<
{ message: string; type: 'info' | 'error' } | null
->(
- null,
-)
+>(null)
function toast(message: string, type: 'info' | 'error' = 'info') {
toastSignal.value = { message, type }
@@ -184,17 +183,16 @@ const SQLEditor = () => (
-
+ contenteditable='plaintext-only'
+ >{url.params.q || 'SELECT * FROM users WHERE active = true;'}
)
@@ -1607,7 +1605,10 @@ function MetricDetail() {
Query
-
{metric.query}
+
{metric.query}