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.query} +

+ item.query

{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 = () => (
-